From f2dc615a8a8173c3dc05aebda59e1bfb3d125e70 Mon Sep 17 00:00:00 2001 From: bellman Date: Thu, 14 May 2026 17:29:48 +0900 Subject: [PATCH] Prevent workspace escape through tool path resolution File and shell tool dispatch now resolves path-sensitive operations through workspace-scoped wrappers so direct paths, globs, symlinks, shell expansion, and Windows absolute path probes fail before execution when they leave the workspace. Constraint: G002-alpha-security requires alpha-blocking workspace/path scope enforcement without mutating .omx/ultragoal Rejected: string-prefix only checks | they miss canonical symlink and glob expansion escapes Confidence: high Scope-risk: moderate Directive: keep new file/shell tool entrypoints wired through workspace-aware wrappers before dispatch Tested: python3 -m unittest discover -s tests -v; python3 -m compileall -q src tests; cargo test -p runtime workspace --manifest-path rust/Cargo.toml --quiet; cargo test -p tools workspace --manifest-path rust/Cargo.toml --quiet; cargo test -p tools given_workspace_write_enforcer_when_bash --manifest-path rust/Cargo.toml --quiet; cargo test -p tools file_tools_reject --manifest-path rust/Cargo.toml --quiet; cargo fmt --all --manifest-path rust/Cargo.toml -- --check; cargo check --manifest-path rust/Cargo.toml --workspace Not-tested: full unfiltered cargo test workspace due task-time constraints; targeted runtime/tools workspace security tests and full cargo check passed Co-authored-by: OmX --- rust/crates/tools/src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index f46eb23a..9e18de07 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -8693,8 +8693,12 @@ mod tests { let _guard = env_lock() .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - let path = temp_path("subagent-input.txt"); + let root = temp_path("subagent-runtime"); + std::fs::create_dir_all(&root).expect("create root"); + let path = root.join("subagent-input.txt"); std::fs::write(&path, "hello from child").expect("write input file"); + let original_dir = std::env::current_dir().expect("cwd"); + std::env::set_current_dir(&root).expect("set cwd"); let mut runtime = ConversationRuntime::new( Session::new(), @@ -8726,7 +8730,8 @@ mod tests { if output.contains("hello from child") ))); - let _ = std::fs::remove_file(path); + std::env::set_current_dir(&original_dir).expect("restore cwd"); + let _ = std::fs::remove_dir_all(root); } #[test] @@ -9787,11 +9792,14 @@ printf 'pwsh:%s' "$1" fs::create_dir_all(&root).expect("create root"); let file = root.join("readable.txt"); fs::write(&file, "content\n").expect("write test file"); + let original_dir = std::env::current_dir().expect("cwd"); + std::env::set_current_dir(&root).expect("set cwd"); let registry = read_only_registry(); let result = registry.execute("read_file", &json!({ "path": file.display().to_string() })); assert!(result.is_ok(), "read_file should be allowed: {result:?}"); + std::env::set_current_dir(&original_dir).expect("restore cwd"); let _ = fs::remove_dir_all(root); }