feat: add shell command approval flow - #239
Conversation
84a02c0 to
ac7079f
Compare
|
Reviewed the full range The mechanism itself looks solid to me: one-shot authority only (session/always removed), the two-layer deadline, Below are the points I noticed, with how I reproduced them. Happy to be corrected where I read something wrong. Worth a look before mergeB1 - ordinary guard errors also terminate the whole turn -
With For a fix I would lean toward returning a plain Verify: assert (Side note: the B2 - the subagent loop does not read The inner loop appends the So on the subagent path, "no equivalent retry" currently rests only on the model-facing text instruction. Suggest mirroring the main loop: on Verify: a subagent test asserting no tool call runs after the first denial. B3 - suggest softening "cannot retry through an equivalent command or interpreter" in the description - The module docstring already says this is lexical best-effort, so gaps in the implementation make sense to me. It is just that the sentence in the PR description reads like a runtime guarantee, and someone picking this up later may trust it more than intended. Forms I measured as allowed:
Root cause is that Suggest only the description wording before merge; the implementation side (adding One thing I would like to confirmD1 - deletion capability on non-interactive paths - Default
I read "Fail closed for non-interactive origins" in the description as deliberate; I mainly want to confirm the scope is what you intended, since it means the agent can no longer delete files on channels and cron. The new tests pin this behavior, so if an opt-in is wanted (a config flag, or wiring the broker into the CLI REPL), deciding now is cheaper than changing it later. Follow-up
Minor
|
ac7079f to
72292cf
Compare
|
B1: Fixed the false positives for power-control command names. shutdown, reboot, and poweroff are now classified by executable position after wrapper normalization, so read-only commands such as I intentionally kept workspace-boundary and genuine deny-list violations terminal. The intended behavior is that a safety-policy rejection must stop the current action rather than invite the model to find another path. B2: Fixed. The subagent loop now observes B3: Fixed the common lexical gaps identified in the review. Command classification now preserves LF and CRLF as command boundaries and handles simple parentheses/braces, command substitution, D1: Confirmed as intentional. Only trusted interactive TUI user turns receive an approval responder. CLI prompt mode, channels, cron, sentinel, heartbeat, and subagent paths fail closed when no responder is available. Sandboxed execution retains its existing policy bypass because the sandbox remains the containment boundary. F1, F3 message refinement, F4, N1, and N2 remain suitable follow-up items. Parse failures are still explicitly pinned as fail-closed by a regression test. F2 was not changed in this patch. Marking the entire ExecTool as a blocking interaction would also remove the registry backstop for a wedged executor. The current 660-second ceiling still covers the 35-second approval ceiling plus the 600-second command limit. This should be revisited together if either timeout changes. F5 is intentional: expiry is a denial and terminates the current action. The PR type is Feature only. Verification after these changes:
|
Follow-up reviewRe-checked the latest head (
Your calls on D1, F2 and F5 make sense to me, not pursuing those. Each item below notes how I verified it. Suggest addressing before mergeP1 - moving power detection to argv[0] dropped the Context first: switching to executable-position matching was my suggestion last round, and I did not measure what it narrowed. So this gap comes from that suggestion rather than from your judgement.
Suggested fix (I ran both directions of this locally): _SYSTEM_POWER_COMMANDS = frozenset({"halt", "poweroff", "reboot", "shutdown"})
_POWER_MULTIPLEXERS = frozenset({"busybox", "init", "loginctl", "systemctl", "telinit"})
# in _matches_system_power_command, after the argv[0] check:
if executable in _POWER_MULTIPLEXERS and any(
arg in _SYSTEM_POWER_COMMANDS or arg in {"0", "6"} for arg in segment[1:]
):
return TrueCoverage result: all 14 commands that should be denied are denied, including Follow-up (packaged, no need to answer individually)All four are edges of the area you just touched. 1 - backtick command substitution (measured) The backtick is not in Fix: add a backtick to 2 -
Fix: look at where pos = token.find("c", 1)
if pos != -1:
if pos + 1 < len(token): # -c'rm foo' -> '-crm foo'
return token[pos + 1:]
if index + 1 < len(segment): # -c / -lc -> next token
return segment[index + 1]Verified: 3 -
4 - heredoc bodies are classified as commands (measured) A One observation (no fix proposed)
I understand keeping deny-list hits terminal is deliberate and I am not trying to reopen that. This particular interaction just looks like it may not be the intended design. I am deliberately not proposing a fix: turning Nothing else blocking from my side beyond P1. With P1 addressed I think this is good to go. |
Summary
Add a runtime-owned approval flow for protected shell commands:
-ccommands.git grep shutdownis not falsely rejected.The shell classifier is intentionally lexical. It recognizes supported command families and common shell forms, but it does not attempt to infer arbitrary side effects performed by every program or interpreter.
Type
Verification
Commands and results:
uv run --frozen pytest tests/test_agent_loop_approval.py tests/test_approval_broker.py tests/test_cli_tui_commands.py tests/test_sandbox_unit.py tests/test_shell_approval.py tests/test_subagent_manager.py tests/test_tool_registry_execute.py tests/test_tui_rpc_approval.py tests/test_tui_rpc_spine.py tests/test_tui_rpc_stubs.py -q: 255 passed.uv run --frozen ruff format --check raven/agent/tools/shell.py raven/agent/tools/shell_policy.py raven/agent/subagent/manager.py tests/test_shell_approval.py tests/test_subagent_manager.py: passed.uv run --frozen ruff check raven/agent/tools/shell.py raven/agent/tools/shell_policy.py raven/agent/subagent/manager.py tests/test_shell_approval.py tests/test_subagent_manager.py: passed.npm run type-check: passed.npm run lint: 0 errors, 22 warnings.npm test -- --run src/__tests__/approvalRoundTrip.test.ts: 7 passed.No user-facing documentation or screenshots are required for this runtime and TUI safety-flow change.
Risk
The sandbox keeps its existing policy bypass because it remains the containment boundary. Direct execution preserves hard-deny precedence, and approval transport failures deny execution.
Interactive TUI user turns gain one-shot approval. Gateways and background origins without a trusted approval responder fail closed for recognized deletion commands. Workspace-boundary and deny-list violations remain terminal so the model cannot search for an alternative path after a safety rejection.
The feature can be rolled back by reverting this pull request.
Related Issues
N/A