PR3: split compound commands and check each part independently

The old rule -- any shell operator disqualifies the whole command -- was wrong
in both directions, verified by running it:

  find . -delete                  -> ALLOW  (destructive, no prompt)
  find . -exec rm {} +            -> ALLOW  (destructive, no prompt)
  git status && git diff          -> ask    (two allowed reads, refused)

It judged punctuation rather than danger. `-delete` and `-exec` need no
separator, so a bare `find` prefix auto-ran them; meanwhile two independently
allowed reads were refused for containing `&&`.

Now:
- Constructs whose contents we cannot evaluate -- substitution, redirection,
  variable expansion, grouping -- still disqualify the whole command, because
  the unexamined tail after a prefix match must only ever be arguments.
- Compound commands are split on &&, ||, ;, |, |&, & and newlines, and EVERY
  part must be independently covered by an allowlist entry.
- Parts that run code named in their arguments are never prefix-eligible:
  argument executors (xargs, sudo, timeout, env, docker, npx, ssh...),
  interpreters carrying inline code (python -c, bash -c, node -e), and
  execution/deletion flags (-exec, -execdir, -delete, -ok).
- Matching stays on parsed words, so `git status` covers `git status -s` but
  never `git statusfoo` or a bare `git`.

Splitting is textual and does not respect quoted separators. That is
deliberate: over-splitting yields MORE parts to justify, never fewer, so it
cannot loosen a verdict.

37 new tests including metamorphic cases (spacing, quoting, absolute program
path must not loosen `find . -delete`). Golden matrix: three rows flip as
intended, two added. 164 permission tests green.

Design of record: ocw-context/docs/reviewed-auto-mode.md Part 2 (CMD-1/3/4).
This commit is contained in:
Devika Verma
2026-08-11 11:43:57 -07:00
parent 48a498d5e2
commit ab00fe3b55
3 changed files with 232 additions and 25 deletions
+5 -3
View File
@@ -15,9 +15,11 @@ shell-allowlist-chained,interactive,run_shell,"{""command"": ""git status && rm
shell-session-command,interactive,run_shell,"{""command"": ""make build""}",,,,make build,,,,allow,exact session command grant
shell-plan,plan,run_shell,"{""command"": ""ls""}",,,,,,,,deny,plan mode blocks shell
shell-auto,auto,run_shell,"{""command"": ""rm -rf /""}",,,,,,,,allow,BASELINE-WRONG auto allows any command with no sandbox
shell-find-delete,interactive,run_shell,"{""command"": ""find . -delete""}",,find,,,,,,allow,BASELINE-WRONG find prefix auto-allows a destructive form
shell-find-exec,interactive,run_shell,"{""command"": ""find . -exec rm {} +""}",,find,,,,,,allow,BASELINE-WRONG find -exec auto-allowed via prefix
shell-two-reads,interactive,run_shell,"{""command"": ""git status && git diff""}",,git status|git diff,,,,,,ask,BASELINE-ANNOYING two allowed reads rejected for the operator
shell-find-delete,interactive,run_shell,"{""command"": ""find . -delete""}",,find,,,,,,ask,FIXED-PR3 -delete is never prefix-eligible
shell-find-exec,interactive,run_shell,"{""command"": ""find . -exec rm {} +""}",,find,,,,,,ask,FIXED-PR3 -exec is never prefix-eligible
shell-two-reads,interactive,run_shell,"{""command"": ""git status && git diff""}",,git status|git diff,,,,,,allow,FIXED-PR3 each part independently allowed
shell-chain-unallowed,interactive,run_shell,"{""command"": ""git status && rm -rf ~""}",,git status,,,,,,ask,chaining still cannot smuggle an unallowed part
shell-inline-interpreter,interactive,run_shell,"{""command"": ""python -c 'import os'""}",,python,,,,,,ask,FIXED-PR3 inline code is never prefix-eligible
connector-read,interactive,gmail_list,{},read,,,,,,,allow,connector read never gates
connector-write,interactive,gmail_send,{},external,,,,,,,ask,connector write asks
connector-always-ignored,interactive,gmail_send,{},external,,gmail_send,,,,,ask,session tool grant deliberately ignored for connectors
1 id mode tool args meta allowed_commands session_tools session_commands standing auto_allow allowed_domains expected note
15 shell-session-command interactive run_shell {"command": "make build"} make build allow exact session command grant
16 shell-plan plan run_shell {"command": "ls"} deny plan mode blocks shell
17 shell-auto auto run_shell {"command": "rm -rf /"} allow BASELINE-WRONG auto allows any command with no sandbox
18 shell-find-delete interactive run_shell {"command": "find . -delete"} find allow ask BASELINE-WRONG find prefix auto-allows a destructive form FIXED-PR3 -delete is never prefix-eligible
19 shell-find-exec interactive run_shell {"command": "find . -exec rm {} +"} find allow ask BASELINE-WRONG find -exec auto-allowed via prefix FIXED-PR3 -exec is never prefix-eligible
20 shell-two-reads interactive run_shell {"command": "git status && git diff"} git status|git diff ask allow BASELINE-ANNOYING two allowed reads rejected for the operator FIXED-PR3 each part independently allowed
21 shell-chain-unallowed interactive run_shell {"command": "git status && rm -rf ~"} git status ask chaining still cannot smuggle an unallowed part
22 shell-inline-interpreter interactive run_shell {"command": "python -c 'import os'"} python ask FIXED-PR3 inline code is never prefix-eligible
23 connector-read interactive gmail_list {} read allow connector read never gates
24 connector-write interactive gmail_send {} external ask connector write asks
25 connector-always-ignored interactive gmail_send {} external gmail_send ask session tool grant deliberately ignored for connectors