Uninstall floor: mirror the gateway's per-statement rule and coverage; runLocal uses the vetted engine (gatewaystack-connect#1277) - #42
Merged
Conversation
…teway down Mirrors the gateway's uninstall floor (gatewaystack-connect#1229) in the client-side offline floors, since an outage or a deleted key is exactly when an agent-initiated removal has no human to ask remotely. uninstallFloor() in decide.mjs catches the sanctioned uninstaller in command position, fetching the hosted uninstall script, and a recursive delete of .acp (POSIX or PowerShell, scanned across the whole payload so a variable-bound path in one statement and the delete in another still match) — wired into decide() for local-policy mode. applyOfflineFloors() in govern.mjs did not call it: loadEngine() already required uninstallFloor to exist on the engine module, but the actual outage/no-key/key-rejected code path never invoked it, so the floor was inert exactly where the task needs it most. Wires it in ahead of the destructive floor, with harness-appropriate messaging — Codex hard-denies since it can't ask mid-run; other harnesses get a native ask. Refs gatewaystack-connect#1229
…; runLocal uses the vetted engine (gatewaystack-connect#1277) decide.mjs keeps parity with the gateway's riskClassifier.ts: - HIGH-1: `.acp;` / `.acp&` / `.acp,` are the directory — the semicolon-joined variable spellings fire offline too. - HIGH-2: classifyTool strips data heredocs (and their terminator) before segmenting, so a heredoc authoring a .ps1 is Bash.cat, not Bash.Remove-Item; the floor's PowerShell label ignores comments and heredoc bodies the same way. - MED-3: per-statement variable rule with cross-segment bindings and pipeline operands; `.acp` elsewhere in the payload no longer asks. - MED-4: quoted full path, npx, source/./& launchers, direct execution, find -delete/-exec, inline python/node/perl/ruby tree deletes, cmd.exe `rd /s` + `%VAR%`, `claude plugin disable|uninstall|remove`. - MED-6: runLocal() takes the engine loadEngine() already vetted, so ACP_LOCAL=1 never imports a stale ~/.acp/decide.mjs that lacks uninstallFloor; the fail-open notice is unchanged when no engine loads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plugin half of the
feat/uninstall-floorrework per the 2026-09-21 adversarial review, davidcrowe/gatewaystack-connect#1277. Companion gateway PR: https://github.com/davidcrowe/gatewaystack-connect/pull/1286 (carries the full before/after probe table;bin/decide.mjsmirrors the gatewayriskClassifier.tsand agrees with it on every row).What changed
ACP_DIR_MENTION_RElookahead is(?![\w.-]):d=~/.acp; rm -rf "$d",d=$HOME/.acp; rm -rf $d,export ACP=~/.acp; rm -rf $ACP,sh -c 'd=~/.acp; rm -rf $d'fire offline;.acpx/.acp-cache/.acp_olddo not.classifyToolstrips data heredocs (and, via a newstripHeredocTerminators, the loneEOFline) before segmenting: a heredoc authoring a.ps1isBash.cat, notBash.Remove-Item/Bash.EOF;rm -rf ~/work/scratch # Remove-ItemstaysBash.rm. The floor's "(PowerShell)" label is computed on comment- and heredoc-stripped text, matching the gateway's routing.recursiveDeleteOfAcp), identical to the gateway's: split on; && || | &keeping the operator; bindings (d=,export,$d =,$env:,set,for … in,foreach ($d in) tracked across segments and$var/%var%resolved; pipeline operand from a.acp-naming source.rm -rf $TMP && cat ~/.acp/lapse.logand thefor … done; cat .acp/configloop are null.npx acp-uninstall,source/./&launchers,bash < …, direct execution of~/.acp/uninstall.sh,find … -delete|-exec rm, inlinepython3 -c/node -e/perl -e/ruby -etree deletes ("(script)" label), cmd.exerd /s+%VAR%,claude plugin disable|uninstall|remove agentic-control-plane|acp("disables the ACP plugin").runLocal()no longer imports~/.acp/decide.mjson its own: it takes the module-levelENGINEthatloadEngine()already vetted foruninstallFloor, soACP_LOCAL=1with a stale installed engine still asks onacp-uninstall. The fail-open notice + audit line are unchanged when no engine loads at all.Left out, on purpose
Same list as the gateway PR: glob / quote-splitting spellings,
rm -rf "$(echo ~/.acp)"(whitespace-bearing quoted span blanked by the shared prose mask),read … <<<,rsync --delete, symlink-then-delete,echo 'Write-Host'as a PowerShell shape, the plugin cache dir under~/.claude/plugins.Verification
node --test test/: 262/262 (was 257; +4 intest/uninstall-floor.test.mjs, +1 MED-6 end-to-end intest/local-mode.test.mjswith a stale~/.acp/decide.mjsthat exports onlydecide/hardlineFloor/destructiveFloor).df3dafdare the "plugin before / plugin after" columns in the gateway PR.