feat(cli): PermissionAdapter production mutation guard (Windows estate muzzle)#70
feat(cli): PermissionAdapter production mutation guard (Windows estate muzzle)#70heavygee wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: def418a75f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /hapi_stack_switch_yes=1/, | ||
| /nohup.*(bun run|src\/index\.ts)/, | ||
| /manual-hub/, | ||
| /(^|[\s;|&])(kill|pkill|fuser)[\s].*(3006|hapi-hub|\/hub\/|src\/index\.ts)/, |
There was a problem hiding this comment.
Block quoted remote kill commands
Quoted SSH commands such as ssh server "fuser -k 3006/tcp" or ssh server "pkill -f hapi-hub" are the normal remote-shell shape, but this pattern only matches when the character immediately before kill|pkill|fuser is start/space/;/|/&. Inside the remote command that character is the quote, so these production :3006/hub kills do not match and shouldDenyAgentShellCommand can fall through to yolo auto-approval, defeating the guard for one of the listed mutation classes.
Useful? React with 👍 / 👎.
| if (typeof input.title === 'string' && input.title.trim()) { | ||
| return input.title.trim(); |
There was a problem hiding this comment.
Inspect rawInput command before display title
When an ACP permission request carries a generic non-empty title such as Shell plus the actual command in rawInput.command, this returns the title and never inspects the raw command. In that common shell-tool shape, a dangerous command like ssh server "hapi-driver-db-prep.sh ..." is evaluated as just Shell, so the guard returns allow and yolo can auto-approve the production mutation.
Useful? React with 👍 / 👎.
| if (!looksLikeShell && !matchesProductionMutation(command)) { | ||
| return { deny: false, command }; | ||
| } | ||
|
|
||
| if (matchesProductionMutation(command)) { |
There was a problem hiding this comment.
Limit mutation matching to shell requests
For non-shell ACP requests whose title is a path or label, this condition still falls through to the deny block whenever the text matches a mutation pattern. A read/edit permission for a file such as scripts/tooling/hapi-use-worktree.sh or documentation mentioning .hapi/hapi.db would be denied even though kind is read/edit and no shell command is being executed, which blocks harmless inspection in yolo/read-only sessions.
Useful? React with 👍 / 👎.
| /nohup.*(bun run|src\/index\.ts)/, | ||
| /manual-hub/, | ||
| /(^|[\s;|&])(kill|pkill|fuser)[\s].*(3006|hapi-hub|\/hub\/|src\/index\.ts)/, | ||
| /systemctl[\s]+(stop|restart|kill|disable|mask)[\s]+hapi-(hub|runner|runner-watchdog)/, |
There was a problem hiding this comment.
Block systemctl commands with leading options
systemctl --help documents the form systemctl [OPTIONS...] COMMAND ..., so destructive calls can legally put flags before the verb, e.g. sudo systemctl --no-pager restart hapi-hub.service. This regex requires the verb immediately after systemctl, so those production restarts/stops miss the guard and can still be yolo-approved.
Useful? React with 👍 / 👎.
| const productionGuard = shouldDenyAgentShellCommand({ | ||
| title: request.title, | ||
| kind: request.kind, | ||
| rawInput: request.rawInput | ||
| }); |
There was a problem hiding this comment.
Apply the guard to the other ACP permission handlers
This wiring only protects PermissionAdapter-backed agents, but the repo also has ACP remote launchers with their own handlers (GeminiPermissionHandler, OpencodePermissionHandler, and KimiPermissionHandler) that call resolveAutoApprovalDecision and auto-approve in yolo without this guard. In those flavors, the same production mutation shell request still bypasses the new protection, so the guard needs to move into the shared handler path or be added to each handler.
Useful? React with 👍 / 👎.
…n yolo Shared productionMutationGuard blocks stack-switch, manual hub, :3006 kill, and remote-ssh compound mutations before yolo auto-approves shell. Closes the Windows estate gap where ACP sessions skip beforeShellExecution hooks. Closes #69 Co-authored-by: Cursor <cursoragent@cursor.com>
def418a to
5f1de61
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f1de617c9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /hapi_stack_switch_yes=1/, | ||
| /nohup.*(bun run|src\/index\.ts)/, | ||
| /manual-hub/, | ||
| /(^|[\s;|&])(kill|pkill|fuser)[\s].*(3006|hapi-hub|\/hub\/|src\/index\.ts)/, |
There was a problem hiding this comment.
When the production stop command finds the PID by port before invoking kill, e.g. lsof -ti:3006 | xargs kill -9, the 3006 token appears before kill, so this pattern does not match and none of the other mutation patterns catch it. In yolo ACP sessions that command can still be auto-approved even though it kills the protected hub port this guard is meant to muzzle.
Useful? React with 👍 / 👎.
| /hapi-driver-rebuild.*--activate/, | ||
| /hapi-watch-activate-driver/, | ||
| /hapi_stack_switch_yes=1/, | ||
| /nohup.*(bun run|src\/index\.ts)/, |
There was a problem hiding this comment.
Block documented hapi hub nohup starts
The documented persistent hub start command is nohup hapi hub --relay (docs/guide/installation.md:385), but this pattern only denies nohup when the command contains the source-tree forms bun run or src/index.ts. On hosts running the packaged HAPI CLI, a yolo ACP shell request can still start or restart the production hub with the documented command without hitting the guard, so the manual-hub match needs to include hapi hub/binary launch shapes too.
Useful? React with 👍 / 👎.
| /manual-hub/, | ||
| /(^|[\s;|&])(kill|pkill|fuser)[\s].*(3006|hapi-hub|\/hub\/|src\/index\.ts)/, | ||
| /systemctl[\s]+(stop|restart|kill|disable|mask)[\s]+hapi-(hub|runner|runner-watchdog)/, | ||
| /git reset --hard.*(driver|hapi\/driver)/, |
There was a problem hiding this comment.
Match git resets with -C driver paths
Git accepts the path selector before the subcommand (git -h shows git [-C <path>] ... <command>), so destructive driver resets are commonly written as git -C ~/coding/hapi/driver reset --hard or cd ~/coding/hapi/driver && git reset --hard. In both forms the driver token is before git reset --hard, so this pattern misses the production worktree reset and yolo can still auto-approve it.
Useful? React with 👍 / 👎.
| /nohup.*(bun run|src\/index\.ts)/, | ||
| /manual-hub/, | ||
| /(^|[\s;|&])(kill|pkill|fuser)[\s].*(3006|hapi-hub|\/hub\/|src\/index\.ts)/, | ||
| /systemctl[\s]+(stop|restart|kill|disable|mask)[\s]+hapi-(hub|runner|runner-watchdog)/, |
There was a problem hiding this comment.
Block direct runner lifecycle commands
When the ACP shell request uses the packaged CLI to manage the runner, e.g. hapi runner stop, hapi runner stop-session <id>, or hapi doctor clean, none of these patterns match because only the systemd service spelling is covered here. These commands are documented in cli/README.md and implemented to stop the runner or active runner sessions, so yolo can still auto-approve the same production runner mutation this guard blocks only when it is spelled as systemctl stop hapi-runner.
Useful? React with 👍 / 👎.
Summary
productionMutationGuard— shared pattern matcher for production HAPI mutations (stack switch, manual hub nohup, :3006 kill, systemd stop, driver DB prep, remote ssh compounds)PermissionAdapterbefore yolo auto-approve so ACP sessions cannot shell-mutate production even when hooks do not firedeniedwith reason (same as operator Cursor hook semantics)Why
Windows Teemo Cursor agents run ACP + yolo.
beforeShellExecutionhooks often never fire in that path. The 2026-06-20 incident ranssh server "kill … && hapi-driver-db-prep … && nohup bun run …"against Linux:3006. Rules alone are not enough — this is mechanical enforcement at the permission layer.Test plan
cd cli && bunx vitest run src/modules/common/permission/productionMutationGuard.test.ts src/agent/permissionAdapter.test.ts— 12/12 passIssues
Closes #69
Upstream note
Patterns reference operator fork tooling (
hapi-use-worktree,hapi-driver-db-prep, etc.). Fork-only for now; upstream could adopt a configurable deny-list later if desired.Made with Cursor