Skip to content

Close the wrapper/compound bypass in the local decision engine (0.12.0) - #19

Merged
davidcrowe merged 1 commit into
mainfrom
fix/wrapper-command-bypass
Aug 27, 2026
Merged

Close the wrapper/compound bypass in the local decision engine (0.12.0)#19
davidcrowe merged 1 commit into
mainfrom
fix/wrapper-command-bypass

Conversation

@davidcrowe

Copy link
Copy Markdown
Collaborator

What

bin/decide.mjs is the verbatim mirror of acp-install's decide.mjs and carried the same p0 bypass class (acp-install#19 / acp-install#18): wrapper positional operands (sudo -u root …, timeout 5 …, nice -n 10 …) were classified as the binary, git -C . push --force origin main produced the malformed key Bash.git.. past both the hardline floor and the shipped Bash.git.push rule, subshells / brace groups / fish -c were gaps, and a user-set policy deny was evadable by prefixing any benign segment (true && gcloud …).

The fix

Syncs the repaired engine from acp-install's companion PR (same bytes):

  • parseCommand consumes each wrapper's value-taking flags AND positional operands (sudo -u USER, timeout DURATION, nice -n N), and skips shell keywords.
  • firstSubcommand skips option-arguments (git -C /repo pushBash.git.push).
  • Unquoted ( ) are segment separators, so subshells and $( ) are inspected; compound commands classify by their most-privileged segment and every segment is policy-checked with the strictest verdict winning (deny > ask > allow).
  • Unparseable non-empty commands are the explicit Bash.unknown — governable, falls back to Bash, honestly labeled — never a wrong-segment key.
  • SHELL_BINS gains fish/csh/tcsh; rm danger targets gain /..

Mirrors the gateway-side fix for this class (gsc#516 / gsc#750).

Tests

Adds test/wrapper-bypass.test.mjs — the plugin-side regression guard this mirror never had (acp-install#19 noted the plugin copy was unguarded). Full corpus of the bypass shapes plus negative cases; all verification is string-in / verdict-out through the pure decision functions — no command from the corpus is ever executed.

node --test: 97 pass / 0 fail. node scripts/validate-manifest.mjs green.

Version

0.11.0 → 0.12.0 bumped together across plugin.json, .claude-plugin/marketplace.json, and PLUGIN_VERSION in bin/govern.mjs — the #18 lesson (a bump announced but never made in the manifests never reaches installed plugins).

Refs acp-control-plane/acp-install#19.

bin/decide.mjs is the verbatim mirror of acp-install's decide.mjs, and
carried the same p0 hole (acp-install#19/#18): wrapper positional
operands (sudo -u root, timeout 5, nice -n 10) were classified as the
binary, git -C . push --force origin main produced the malformed key
Bash.git.. past both floor and policy, subshells/braces/fish -c were
gaps, and a policy deny was evadable by prefixing a benign segment
(true && gcloud ...).

Sync the fixed engine (wrapper operand consumption, option-argument
skipping in firstSubcommand, subshell/keyword handling, most-privileged
compound classification, strictest-verdict per-segment policy checks,
explicit Bash.unknown for unparseable commands), and add
test/wrapper-bypass.test.mjs — the plugin-side regression guard this
mirror never had. Bump 0.11.0 -> 0.12.0 across plugin.json,
marketplace.json, and PLUGIN_VERSION together (the #18 lesson).

Companion to acp-install's fix for acp-install#19.
@davidcrowe
davidcrowe merged commit 0553ed4 into main Aug 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant