Skip to content

Commit 0a2872e

Browse files
authored
Add command filtering and timeout config to zeph-tools (#40) (#48)
- ShellExecutor blocks dangerous commands before execution - Default blocked list: rm -rf /, sudo, mkfs, dd if=, curl, wget, nc, ncat, netcat, shutdown, reboot, halt - Config: tools.shell.blocked_commands, tools.shell.timeout - User patterns normalized to lowercase before merge+dedup - Unit tests for exact match, case-insensitive, network exfiltration, system control - Integration with zeph-core Config [tools] section and ZEPH_TOOLS_TIMEOUT env var
1 parent 8681744 commit 0a2872e

File tree

6 files changed

+353
-2
lines changed

6 files changed

+353
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ zeph-core = { path = "crates/zeph-core", version = "0.2.0" }
3232
zeph-llm = { path = "crates/zeph-llm", version = "0.2.0" }
3333
zeph-memory = { path = "crates/zeph-memory", version = "0.2.0" }
3434
zeph-skills = { path = "crates/zeph-skills", version = "0.2.0" }
35+
zeph-tools = { path = "crates/zeph-tools", version = "0.2.0" }
3536

3637
[workspace.lints.clippy]
3738
all = "warn"

config/default.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,10 @@ paths = ["./skills"]
1616
[memory]
1717
sqlite_path = "./data/zeph.db"
1818
history_limit = 50
19+
20+
[tools]
21+
enabled = true
22+
23+
[tools.shell]
24+
timeout = 30
25+
blocked_commands = []

crates/zeph-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ toml.workspace = true
1515
tracing.workspace = true
1616
zeph-llm.workspace = true
1717
zeph-memory.workspace = true
18+
zeph-tools.workspace = true
1819

1920
[dev-dependencies]
2021
tempfile.workspace = true

crates/zeph-core/src/config.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::path::Path;
22

33
use anyhow::Context;
44
use serde::Deserialize;
5+
use zeph_tools::ToolsConfig;
56

67
#[derive(Debug, Deserialize)]
78
pub struct Config {
@@ -10,6 +11,8 @@ pub struct Config {
1011
pub skills: SkillsConfig,
1112
pub memory: MemoryConfig,
1213
pub telegram: Option<TelegramConfig>,
14+
#[serde(default)]
15+
pub tools: ToolsConfig,
1316
}
1417

1518
#[derive(Debug, Deserialize)]
@@ -89,6 +92,11 @@ impl Config {
8992
});
9093
tg.token = Some(v);
9194
}
95+
if let Ok(v) = std::env::var("ZEPH_TOOLS_TIMEOUT")
96+
&& let Ok(secs) = v.parse::<u64>()
97+
{
98+
self.tools.shell.timeout = secs;
99+
}
92100
}
93101

94102
fn default() -> Self {
@@ -110,6 +118,7 @@ impl Config {
110118
history_limit: 50,
111119
},
112120
telegram: None,
121+
tools: ToolsConfig::default(),
113122
}
114123
}
115124
}
@@ -143,6 +152,9 @@ mod tests {
143152
assert_eq!(config.memory.history_limit, 50);
144153
assert!(config.llm.cloud.is_none());
145154
assert!(config.telegram.is_none());
155+
assert!(config.tools.enabled);
156+
assert_eq!(config.tools.shell.timeout, 30);
157+
assert!(config.tools.shell.blocked_commands.is_empty());
146158
}
147159

148160
#[test]
@@ -281,4 +293,103 @@ allowed_users = ["alice", "bob"]
281293
let tg = config.telegram.unwrap();
282294
assert_eq!(tg.token.as_deref(), Some("env-token"));
283295
}
296+
297+
#[test]
298+
fn config_with_tools_section() {
299+
let dir = tempfile::tempdir().unwrap();
300+
let path = dir.path().join("tools.toml");
301+
let mut f = std::fs::File::create(&path).unwrap();
302+
write!(
303+
f,
304+
r#"
305+
[agent]
306+
name = "Zeph"
307+
308+
[llm]
309+
provider = "ollama"
310+
base_url = "http://localhost:11434"
311+
model = "mistral:7b"
312+
313+
[skills]
314+
paths = ["./skills"]
315+
316+
[memory]
317+
sqlite_path = "./data/zeph.db"
318+
history_limit = 50
319+
320+
[tools]
321+
enabled = true
322+
323+
[tools.shell]
324+
timeout = 60
325+
blocked_commands = ["custom-danger"]
326+
"#
327+
)
328+
.unwrap();
329+
330+
clear_llm_env();
331+
332+
let config = Config::load(&path).unwrap();
333+
assert!(config.tools.enabled);
334+
assert_eq!(config.tools.shell.timeout, 60);
335+
assert_eq!(config.tools.shell.blocked_commands, vec!["custom-danger"]);
336+
}
337+
338+
#[test]
339+
fn config_without_tools_section() {
340+
let dir = tempfile::tempdir().unwrap();
341+
let path = dir.path().join("no_tools.toml");
342+
let mut f = std::fs::File::create(&path).unwrap();
343+
write!(
344+
f,
345+
r#"
346+
[agent]
347+
name = "Zeph"
348+
349+
[llm]
350+
provider = "ollama"
351+
base_url = "http://localhost:11434"
352+
model = "mistral:7b"
353+
354+
[skills]
355+
paths = ["./skills"]
356+
357+
[memory]
358+
sqlite_path = "./data/zeph.db"
359+
history_limit = 50
360+
"#
361+
)
362+
.unwrap();
363+
364+
clear_llm_env();
365+
366+
let config = Config::load(&path).unwrap();
367+
assert!(config.tools.enabled);
368+
assert_eq!(config.tools.shell.timeout, 30);
369+
assert!(config.tools.shell.blocked_commands.is_empty());
370+
}
371+
372+
#[test]
373+
fn env_override_tools_timeout() {
374+
let mut config = Config::default();
375+
assert_eq!(config.tools.shell.timeout, 30);
376+
377+
unsafe { std::env::set_var("ZEPH_TOOLS_TIMEOUT", "120") };
378+
config.apply_env_overrides();
379+
unsafe { std::env::remove_var("ZEPH_TOOLS_TIMEOUT") };
380+
381+
assert_eq!(config.tools.shell.timeout, 120);
382+
}
383+
384+
#[test]
385+
fn env_override_tools_timeout_invalid_ignored() {
386+
let mut config = Config::default();
387+
assert_eq!(config.tools.shell.timeout, 30);
388+
389+
unsafe { std::env::set_var("ZEPH_TOOLS_TIMEOUT", "not-a-number") };
390+
config.apply_env_overrides();
391+
unsafe { std::env::remove_var("ZEPH_TOOLS_TIMEOUT") };
392+
393+
assert_eq!(config.tools.shell.timeout, 30);
394+
}
284395
}

0 commit comments

Comments
 (0)