Skip to content

feat(cli): PermissionAdapter production mutation guard (Windows estate muzzle)#70

Open
heavygee wants to merge 1 commit into
mainfrom
feat/windows-production-mutation-guard
Open

feat(cli): PermissionAdapter production mutation guard (Windows estate muzzle)#70
heavygee wants to merge 1 commit into
mainfrom
feat/windows-production-mutation-guard

Conversation

@heavygee

Copy link
Copy Markdown
Owner

Summary

  • Add productionMutationGuard — shared pattern matcher for production HAPI mutations (stack switch, manual hub nohup, :3006 kill, systemd stop, driver DB prep, remote ssh compounds)
  • Wire into PermissionAdapter before yolo auto-approve so ACP sessions cannot shell-mutate production even when hooks do not fire
  • Deny path records completed request as denied with reason (same as operator Cursor hook semantics)

Why

Windows Teemo Cursor agents run ACP + yolo. beforeShellExecution hooks often never fire in that path. The 2026-06-20 incident ran ssh 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 pass
  • After merge + driver soup: spawn Windows estate Cursor session in yolo, confirm blocked command returns denied (not executed)

Issues

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)/,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +29 to +30
if (typeof input.title === 'string' && input.title.trim()) {
return input.title.trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +77 to +81
if (!looksLikeShell && !matchesProductionMutation(command)) {
return { deny: false, command };
}

if (matchesProductionMutation(command)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)/,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +73 to +77
const productionGuard = shouldDenyAgentShellCommand({
title: request.title,
kind: request.kind,
rawInput: request.rawInput
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@heavygee heavygee force-pushed the feat/windows-production-mutation-guard branch from def418a to 5f1de61 Compare June 29, 2026 16:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)/,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block xargs-based :3006 kills

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)/,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)/,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)/,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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.

feat(cli): PermissionAdapter production mutation guard (Windows estate muzzle)

1 participant