From 071c0eeb9f4e012e6d743a75932b91a8d31c9338 Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Wed, 17 Jun 2026 23:49:01 -0500 Subject: [PATCH] feat(agentic): scope file/command tools to an agent workspace root Confine the agentic file tools (read_file, write_file, list_files, grep) to a configured workspace subtree via a lexical path check, and run run_command with its cwd set to that root. Root comes from state key "agent_workspace_root" or env NEURON_AGENT_ROOT. When no root is set, behavior is unchanged (unscoped) for backward compatibility. Defense-in-depth, NOT a hard boundary: the lexical guard does not resolve symlinks and cannot stop an arbitrary shell command from cd-ing out of the root. Real confinement needs runtime support (cwd-locked exec / sandbox-exec / chroot) in el_runtime.c. Compile-checked with elc (darwin arm64); not link/run-gated locally (darwin elb unavailable). Needs a soul build + smoke test before merge. Co-Authored-By: Claude Opus 4.8 (1M context) --- chat.el | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/chat.el b/chat.el index 5398c4e..996c567 100644 --- a/chat.el +++ b/chat.el @@ -377,16 +377,78 @@ fn call_neuron_mcp(tool_name: String, args: String) -> String { return json_safe(result) } +// --------------------------------------------------------------------------- +// Agent workspace scope (defense-in-depth, NOT a hard security boundary). +// +// When a workspace root is configured (state key "agent_workspace_root", else +// env NEURON_AGENT_ROOT), the path-based tools (read_file, write_file, +// list_files, grep) are confined to that subtree by a lexical check, and +// run_command runs with its cwd set to the root. With no root set, behavior is +// unchanged (unscoped) for backward compatibility. +// +// LIMITATION — FLAGGED FOR WILL'S REVIEW: this is a lexical guard. It does not +// resolve symlinks and cannot stop an arbitrary shell command from cd-ing out +// of the root. Real confinement needs runtime support (cwd-locked exec / +// sandbox-exec / chroot) in el_runtime.c. This raises the floor; it is not a +// boundary. The default-allow-when-unset policy and the "cd && (...)" +// wrapping are deliberate choices to confirm against the intended design. +// --------------------------------------------------------------------------- + +fn agent_workspace_root() -> String { + let s: String = state_get("agent_workspace_root") + if !str_eq(s, "") { + return s + } + return env("NEURON_AGENT_ROOT") +} + +// Allow if path stays under root. Empty root = no sandbox = allow. Rejects +// parent traversal and ~ expansion; absolute paths must live under root. +fn path_within_root(path: String, root: String) -> Bool { + if str_eq(root, "") { + return true + } + if str_contains(path, "..") { + return false + } + if str_starts_with(path, "~") { + return false + } + if str_starts_with(path, "/") { + return str_starts_with(path, root) + } + return true +} + +// Resolve a relative tool path against the root so it lands inside the subtree. +fn resolve_in_root(path: String, root: String) -> String { + if str_eq(root, "") { + return path + } + if str_starts_with(path, "/") { + return path + } + return root + "/" + path +} + fn dispatch_tool(tool_name: String, tool_input: String) -> String { if str_eq(tool_name, "read_file") { let path: String = json_get(tool_input, "path") - let content: String = fs_read(path) + let root: String = agent_workspace_root() + if !path_within_root(path, root) { + return json_safe("denied: path is outside the agent workspace root") + } + let content: String = fs_read(resolve_in_root(path, root)) return json_safe(content) } if str_eq(tool_name, "write_file") { let path: String = json_get(tool_input, "path") let content: String = json_get(tool_input, "content") - fs_write(path, content) + let root: String = agent_workspace_root() + if !path_within_root(path, root) { + return json_safe("denied: path is outside the agent workspace root") + } + fs_write(resolve_in_root(path, root), content) return json_safe("{\"ok\":true}") } if str_eq(tool_name, "web_get") { @@ -401,7 +463,9 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String { } if str_eq(tool_name, "run_command") { let cmd: String = json_get(tool_input, "command") - let result: String = exec_capture(cmd) + let root: String = agent_workspace_root() + let scoped: String = if str_eq(root, "") { cmd } else { "cd " + root + " && ( " + cmd + " )" } + let result: String = exec_capture(scoped) return json_safe(result) } // MCP connector tools (namespaced mcp____) are routed through @@ -421,13 +485,21 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String { } if str_eq(tool_name, "list_files") { let path: String = json_get(tool_input, "path") - let result: String = exec_capture("ls -la " + path + " 2>&1") + let root: String = agent_workspace_root() + if !path_within_root(path, root) { + return json_safe("denied: path is outside the agent workspace root") + } + let result: String = exec_capture("ls -la " + resolve_in_root(path, root) + " 2>&1") return json_safe(result) } if str_eq(tool_name, "grep") { let pattern: String = json_get(tool_input, "pattern") let path: String = json_get(tool_input, "path") - let result: String = exec_capture("grep -rn \"" + pattern + "\" " + path + " 2>&1 | head -50") + let root: String = agent_workspace_root() + if !path_within_root(path, root) { + return json_safe("denied: path is outside the agent workspace root") + } + let result: String = exec_capture("grep -rn \"" + pattern + "\" " + resolve_in_root(path, root) + " 2>&1 | head -50") return json_safe(result) } if str_eq(tool_name, "edit_file") {