Skip to content

Commit f6c017b

Browse files
feat: tool filter infrastructure, path scoping, and Gen glTFexport (#32)
* feat(core): add tool filter infrastructure and path scoping utilities ToolFilter/CompiledToolFilter for deny/allow pattern matching on tool inputs, hardcoded deny lists for bash (sudo, pipe-to-shell) and web_fetch (SSRF), path resolution with symlink handling and directory scoping. Also adds allowed_directories to SecurityConfig, per-tool filters to ToolsConfig, and PathDenied audit action. * feat(cli): apply tool filters and path scoping to CLI tools BashTool now checks commands against compiled deny/allow filters (with hardcoded sudo, pipe-to-shell defaults) before execution. Strict mode errors on protected file references instead of warning. ReadFileTool, WriteFileTool, EditFileTool now resolve symlinks via resolve_real_path(), enforce allowed_directories scoping, and audit PathDenied violations. Filter checks run before existing sandbox and protected-file checks. * feat(core): apply hardcoded SSRF filters to WebFetchTool WebFetchTool now checks URLs against compiled deny filters before fetching. Hardcoded patterns block localhost, private IPs (10.x, 172.16-31.x, 192.168.x, 127.x), metadata endpoints, file:// and [::1]. User config under [tools.filters.web_fetch] can extend but never remove these defaults via merge_hardcoded(). * refactor: strip xAI, Tavily, Perplexity, and SSRF code redundant with main These features already exist on the main branch: - XaiProvider + grok alias + native search trait methods - TavilyProvider + PerplexityProvider + SearchUsageStats - SSRF hardcoded filters (validate_web_fetch_url, is_private_ip) - Hybrid native search passthrough on Anthropic/xAI providers - Web search session tracking + status display Remaining in this branch: tool filter infrastructure, path scoping, bash deny patterns, allowed_directories, PathDenied audit, Gen glTF export. * fix(ci): resolve clippy, format, and license audit failures - Remove blank line between #[cfg] attr and struct (clippy) - Run cargo fmt to fix line wrapping in tests (format) - Add MIT-0 to deny.toml allow list for encase/Bevy deps (license)
1 parent aa78201 commit f6c017b

22 files changed

Lines changed: 722 additions & 1596 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,6 @@
22

33
All notable changes to LocalGPT are documented in this file.
44

5-
## [Unreleased]
6-
7-
### Added
8-
9-
- **Hybrid web search support** with configurable providers (`searxng`, `brave`, `tavily`, `perplexity`) and native-search passthrough controls.
10-
- **xAI provider support** (`xai/*`, `grok-*`) with native `web_search` tool passthrough.
11-
- **Web search docs and CLI surfaces**: `localgpt search test`, `localgpt search stats`, and a dedicated `docs/web-search.md` guide.
12-
13-
### Changed
14-
15-
- **`web_fetch` extraction upgraded** to use the `readability` crate with fallback text sanitization.
16-
- **Config templates expanded** with `providers.xai` and full `[tools.web_search]` examples in both default and example config files.
17-
185
## [0.2.0] - 2026-02-14
196

207
A milestone release introducing LocalGPT Gen for 3D scene generation, XDG Base Directory compliance, Docker Compose support, and workspace restructuring.

README.md

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ A local device focused AI assistant built in Rust — persistent memory, autonom
1010
- **Single binary** — no Node.js, Docker, or Python required
1111
- **Local device focused** — runs entirely on your machine, your memory data stays yours
1212
- **Persistent memory** — markdown-based knowledge store with full-text and semantic search
13-
- **Hybrid web search** — native provider search passthrough plus client-side fallback providers
1413
- **Autonomous heartbeat** — delegate tasks and let it work in the background
1514
- **Multiple interfaces** — CLI, web UI, desktop GUI, Telegram bot
1615
- **Defense-in-depth security** — signed policy files, kernel-enforced sandbox, prompt injection defenses
17-
- **Multiple LLM providers** — Anthropic (Claude), OpenAI, xAI (Grok), Ollama, GLM (Z.AI)
16+
- **Multiple LLM providers** — Anthropic (Claude), OpenAI, Ollama, GLM (Z.AI)
1817
- **OpenClaw compatible** — works with SOUL, MEMORY, HEARTBEAT markdown files and skills format
1918

2019
## Install
@@ -104,17 +103,6 @@ If you run a local server that speaks the OpenAI API (e.g., LM Studio, llamafile
104103

105104
Tip: If you see `Failed to spawn Claude CLI`, change `agent.default_model` away from `claude-cli/*` or install the `claude` CLI.
106105

107-
### Web Search
108-
109-
Configure web search providers under `[tools.web_search]` and validate with:
110-
111-
```bash
112-
localgpt search test "rust async runtime"
113-
localgpt search stats
114-
```
115-
116-
Full setup guide: [`docs/web-search.md`](docs/web-search.md)
117-
118106
## Telegram Bot
119107

120108
Access LocalGPT from Telegram with full chat, tool use, and memory support.
@@ -206,10 +194,6 @@ localgpt memory search "query" # Search memory
206194
localgpt memory reindex # Reindex files
207195
localgpt memory stats # Show statistics
208196

209-
# Web search
210-
localgpt search test "query" # Validate search provider config
211-
localgpt search stats # Show cumulative search usage/cost
212-
213197
# Security
214198
localgpt md sign # Sign LocalGPT.md policy
215199
localgpt md verify # Verify policy signature

config.example.toml

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
# OpenAI API (requires OPENAI_API_KEY):
1919
# - "openai/gpt-4o", "openai/gpt-4o-mini", "openai/gpt-4-turbo"
2020
#
21-
# xAI API (requires XAI_API_KEY):
22-
# - "xai/grok-3-mini", "xai/grok-3"
23-
#
2421
# GLM / Z.AI API (requires GLM API key):
2522
# - "glm/glm-4.7" (or use alias "glm")
2623
#
@@ -52,11 +49,6 @@ base_url = "https://api.anthropic.com"
5249
# api_key = "not-needed"
5350
# base_url = "http://127.0.0.1:8080/v1" # LM Studio default is http://127.0.0.1:1234/v1
5451

55-
# xAI configuration (optional, for xai/* models)
56-
# [providers.xai]
57-
# api_key = "${XAI_API_KEY}"
58-
# base_url = "https://api.x.ai/v1"
59-
6052
# Ollama configuration (for local models)
6153
# [providers.ollama]
6254
# endpoint = "http://localhost:11434"
@@ -143,11 +135,10 @@ bind = "127.0.0.1"
143135

144136
# Web search (optional)
145137
# [tools.web_search]
146-
# provider = "searxng" # searxng | brave | tavily | perplexity | none
138+
# provider = "searxng" # searxng | brave | none
147139
# cache_enabled = true
148140
# cache_ttl = 900 # seconds (default: 15 min)
149141
# max_results = 5 # 1-10
150-
# prefer_native = true # use provider-native search if available
151142
#
152143
# [tools.web_search.searxng]
153144
# base_url = "http://localhost:8080"
@@ -159,15 +150,6 @@ bind = "127.0.0.1"
159150
# api_key = "${BRAVE_API_KEY}"
160151
# country = ""
161152
# freshness = "" # pd | pw | pm | ""
162-
#
163-
# [tools.web_search.tavily]
164-
# api_key = "${TAVILY_API_KEY}"
165-
# search_depth = "basic" # basic | advanced
166-
# include_answer = true
167-
#
168-
# [tools.web_search.perplexity]
169-
# api_key = "${PERPLEXITY_API_KEY}"
170-
# model = "sonar" # sonar | sonar-pro | sonar-reasoning-pro
171153

172154
# Telegram bot (optional)
173155
# Create a bot via @BotFather on Telegram to get an API token

crates/cli/src/cli/chat.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -794,18 +794,6 @@ async fn handle_command(
794794
status.api_input_tokens + status.api_output_tokens
795795
);
796796
}
797-
798-
if status.search_queries > 0 {
799-
let cache_pct =
800-
(status.search_cached_hits as f64 / status.search_queries as f64) * 100.0;
801-
println!("\nSearch:");
802-
println!(" Queries: {}", status.search_queries);
803-
println!(
804-
" Cached hits: {} ({:.0}%)",
805-
status.search_cached_hits, cache_pct
806-
);
807-
println!(" Estimated cost: ${:.3}", status.search_cost_usd);
808-
}
809797
println!();
810798
CommandResult::Continue
811799
}

crates/cli/src/cli/search.rs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::Result;
22
use clap::{Args, Subcommand};
33

4-
use localgpt_core::agent::tools::web_search::{SearchRouter, read_search_usage_stats};
4+
use localgpt_core::agent::tools::web_search::SearchRouter;
55
use localgpt_core::config::Config;
66

77
#[derive(Args)]
@@ -17,14 +17,11 @@ pub enum SearchCommands {
1717
/// The search query to test
1818
query: String,
1919
},
20-
/// Show cumulative web search usage statistics
21-
Stats,
2220
}
2321

2422
pub async fn run(args: SearchArgs) -> Result<()> {
2523
match args.command {
2624
SearchCommands::Test { query } => run_test(&query).await,
27-
SearchCommands::Stats => run_stats(),
2825
}
2926
}
3027

@@ -64,20 +61,3 @@ async fn run_test(query: &str) -> Result<()> {
6461

6562
Ok(())
6663
}
67-
68-
fn run_stats() -> Result<()> {
69-
let stats = read_search_usage_stats()?;
70-
let cache_pct = if stats.total_queries > 0 {
71-
(stats.cached_hits as f64 / stats.total_queries as f64) * 100.0
72-
} else {
73-
0.0
74-
};
75-
76-
println!("Search Statistics (since {}):", stats.since);
77-
println!(" Provider: {}", stats.provider);
78-
println!(" Total queries: {}", stats.total_queries);
79-
println!(" Cached hits: {} ({:.0}%)", stats.cached_hits, cache_pct);
80-
println!(" Estimated cost: ${:.3}", stats.estimated_cost_usd);
81-
82-
Ok(())
83-
}

crates/cli/src/desktop/views/status.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,6 @@ impl StatusView {
8484
));
8585
});
8686
}
87-
88-
if status.search_queries > 0 {
89-
ui.add_space(10.0);
90-
ui.group(|ui| {
91-
ui.label(RichText::new("Web Search (Session)").strong());
92-
ui.label(format!("Queries: {}", status.search_queries));
93-
let cache_pct =
94-
(status.search_cached_hits as f32 / status.search_queries as f32) * 100.0;
95-
ui.label(format!(
96-
"Cached: {} ({:.0}%)",
97-
status.search_cached_hits, cache_pct
98-
));
99-
ui.label(format!("Estimated cost: ${:.3}", status.search_cost_usd));
100-
});
101-
}
10287
}
10388

10489
message_to_send

0 commit comments

Comments
 (0)