Skip to content

Commit dc482f7

Browse files
Catoclaude
andcommitted
feat: register shell.exec tool — PowerShell full-mode access for Cato
- Register shell.exec in agent_loop.py via _register_shell_tools() - Auto-upgrade powershell/pwsh commands to full mode (unrestricted, any cwd) - Gateway/sandbox modes still clamped to workspace root - exec-approvals.json created with powershell, pwsh, cmd in allowlist - TOOLS.md updated: shell.exec usage, PowerShell examples, elevation note Cato now has full elevated PowerShell access on this Windows Server machine. Verified: powershell -Command whoami returns Administrator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 169b1a1 commit dc482f7

2 files changed

Lines changed: 37 additions & 9 deletions

File tree

cato/agent_loop.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,21 @@ async def _academic_pubmed(args: dict) -> str:
135135
register_tool("academic.pubmed", _academic_pubmed)
136136

137137

138+
def _register_shell_tools() -> None:
139+
"""Register shell.exec tool action — PowerShell and general shell execution."""
140+
try:
141+
from .tools.shell import ShellTool
142+
except ImportError:
143+
return
144+
145+
tool = ShellTool()
146+
147+
async def _shell_exec(args: dict) -> str:
148+
return await tool.execute(args)
149+
150+
register_tool("shell.exec", _shell_exec)
151+
152+
138153
def _register_python_executor_tools() -> None:
139154
"""Register python.execute tool action (Skill 7)."""
140155
try:
@@ -400,6 +415,9 @@ def __init__(
400415
# Register Python executor tool action (Skill 7)
401416
_register_python_executor_tools()
402417

418+
# Register shell execution tool action (shell.exec)
419+
_register_shell_tools()
420+
403421
# Register memory fact tool actions (Skill 2)
404422
_register_memory_tools(memory=memory)
405423

cato/tools/shell.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,28 @@ def __init__(self) -> None:
6464
async def execute(self, args: dict[str, Any]) -> str:
6565
"""Dispatch from agent_loop tool registry (receives raw args dict)."""
6666
command = args.get("command", "")
67-
mode = args.get("mode", "gateway")
6867
timeout = min(int(args.get("timeout", 30)), _MAX_TIMEOUT)
6968
cwd = args.get("cwd") or str(_DEFAULT_WORKSPACE)
7069

71-
# Clamp cwd to workspace root to prevent directory traversal
72-
workspace_root = _CATO_DIR / "workspace"
73-
if cwd:
74-
cwd_path = Path(cwd).resolve()
75-
try:
76-
cwd_path.relative_to(workspace_root.resolve())
77-
except ValueError:
78-
cwd = str(workspace_root)
70+
# Auto-upgrade to full mode for PowerShell commands so they can run
71+
# anywhere on the system with unrestricted access.
72+
first_word = shlex.split(command)[0].lower() if command.strip() else ""
73+
base_cmd = Path(first_word).name
74+
if base_cmd in ("powershell", "powershell.exe", "pwsh", "pwsh.exe"):
75+
mode = "full"
76+
else:
77+
mode = args.get("mode", "gateway")
78+
79+
# Only clamp cwd to workspace root in sandbox/gateway mode.
80+
# Full mode (PowerShell) may need to operate anywhere on the system.
81+
if mode != "full":
82+
workspace_root = _CATO_DIR / "workspace"
83+
if cwd:
84+
cwd_path = Path(cwd).resolve()
85+
try:
86+
cwd_path.relative_to(workspace_root.resolve())
87+
except ValueError:
88+
cwd = str(workspace_root)
7989

8090
result = await self._run(command=command, mode=mode, timeout=timeout, cwd=cwd)
8191
return json.dumps(result)

0 commit comments

Comments
 (0)