From bfe5b6402240b9a9b93dfa02db6efc5ebd48526a Mon Sep 17 00:00:00 2001 From: Haisam Date: Wed, 17 Jun 2026 11:39:19 +0500 Subject: [PATCH] fix: allow git commands in workspace subdirectories The shell safety guard in _guard_command() checked extracted absolute paths only against cwd_path (the command's working directory). When cwd was a subdirectory like ~/.nanobot/workspace/obsidian_notes, any absolute path referencing the broader workspace (e.g. a sibling directory or the workspace root itself) was incorrectly blocked with "path outside working dir". Fix: pass the workspace_root from _prepare_command() into _guard_command() and check absolute paths against it as a fallback when they are outside cwd_path. Paths truly outside the workspace root are still blocked. Added tests: - test_exec_allows_workspace_paths_from_subdirectory - test_exec_blocks_outside_paths_from_subdirectory --- nanobot/agent/tools/shell.py | 14 ++++++++-- tests/tools/test_exec_security.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index 808beaf0..4da7895d 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -397,6 +397,7 @@ class ExecTool(Tool): command, cwd, restrict_to_workspace=access.restrict_to_workspace, + workspace_root=workspace_root, ) if guard_error: return guard_error @@ -591,6 +592,7 @@ class ExecTool(Tool): cwd: str, *, restrict_to_workspace: bool | None = None, + workspace_root: str | None = None, ) -> str | None: """Best-effort safety guard for potentially destructive commands.""" cmd = command.strip() @@ -629,6 +631,11 @@ class ExecTool(Tool): ) cwd_path = Path(cwd).resolve() + resolved_workspace = ( + Path(workspace_root).expanduser().resolve() + if workspace_root + else None + ) for raw in self._extract_absolute_paths(cmd): try: @@ -646,10 +653,13 @@ class ExecTool(Tool): continue media_path = get_media_dir().resolve() - if p.is_absolute() and not ( + allowed = ( is_path_within(p, cwd_path) or is_path_within(p, media_path) - ): + ) + if not allowed and resolved_workspace is not None: + allowed = is_path_within(p, resolved_workspace) + if p.is_absolute() and not allowed: return ( "Error: Command blocked by safety guard (path outside working dir)" + _WORKSPACE_BOUNDARY_NOTE diff --git a/tests/tools/test_exec_security.py b/tests/tools/test_exec_security.py index 7540f87b..72263600 100644 --- a/tests/tools/test_exec_security.py +++ b/tests/tools/test_exec_security.py @@ -349,3 +349,49 @@ def test_exec_allows_format_in_url_and_args(command): tool = ExecTool() result = tool._guard_command(command, "/tmp") assert result is None + + +# --- workspace_root allows paths inside workspace but outside cwd ---------- + + +def test_exec_allows_workspace_paths_from_subdirectory(tmp_path): + """Absolute paths inside the workspace root must be allowed even when cwd + is a subdirectory. This is the scenario reported in the issue: git + commands in ``~/.nanobot/workspace/obsidian_notes`` reference paths + under the broader workspace that are outside the subdirectory cwd.""" + workspace = tmp_path / "workspace" + subdir = workspace / "obsidian_notes" + subdir.mkdir(parents=True) + sibling = workspace / "other_project" + sibling.mkdir() + + tool = ExecTool(working_dir=str(workspace), restrict_to_workspace=True) + + # A command run from the subdirectory that references a sibling path + # inside the workspace should be allowed. + result = tool._guard_command( + f"git clone {sibling}", + str(subdir), + workspace_root=str(workspace), + ) + assert result is None + + +def test_exec_blocks_outside_paths_from_subdirectory(tmp_path): + """Paths truly outside the workspace must still be blocked even when + workspace_root is provided.""" + workspace = tmp_path / "workspace" + subdir = workspace / "project" + subdir.mkdir(parents=True) + outside = tmp_path / "secrets" + outside.mkdir() + + tool = ExecTool(working_dir=str(workspace), restrict_to_workspace=True) + + result = tool._guard_command( + f"cat {outside / 'key.pem'}", + str(subdir), + workspace_root=str(workspace), + ) + assert result is not None + assert "path outside working dir" in result