-
Notifications
You must be signed in to change notification settings - Fork 0
feat(cli): PermissionAdapter production mutation guard (Windows estate muzzle) #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { | ||
| matchesProductionMutation, | ||
| matchesRemoteProductionMutation, | ||
| shouldDenyAgentShellCommand, | ||
| } from './productionMutationGuard'; | ||
|
|
||
| describe('productionMutationGuard', () => { | ||
| it('blocks the 2026-06-20 incident command shape over ssh', () => { | ||
| const cmd = | ||
| 'ssh server "kill 60544 && hapi-driver-db-prep.sh cross-flavor && nohup bun run src/index.ts"'; | ||
| expect(matchesRemoteProductionMutation(cmd)).toBe(true); | ||
| expect( | ||
| shouldDenyAgentShellCommand({ title: cmd, kind: 'execute' }).deny, | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it('allows read-only ssh diagnostics', () => { | ||
| const cmd = 'ssh server "cd ~/coding/hapi/driver && git status -sb"'; | ||
| expect(matchesRemoteProductionMutation(cmd)).toBe(false); | ||
| expect(shouldDenyAgentShellCommand({ title: cmd, kind: 'execute' }).deny).toBe(false); | ||
| }); | ||
|
|
||
| it('blocks local manual hub nohup without ssh prefix', () => { | ||
| const cmd = 'cd hub && nohup bun run src/index.ts >> manual-hub.log'; | ||
| expect(matchesProductionMutation(cmd)).toBe(true); | ||
| expect(shouldDenyAgentShellCommand({ title: cmd }).deny).toBe(true); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| /** | ||
| * Block production HAPI mutations from agent shell (manual hub, stack switch, :3006 kill). | ||
| * Shared by CLI PermissionAdapter (HAPI ACP/yolo path) and operator hook scripts. | ||
| */ | ||
|
|
||
| const MUTATION_PATTERNS: RegExp[] = [ | ||
| /hapi-driver-db-prep/, | ||
| /hapi-use-worktree/, | ||
| /hapi-use-driver/, | ||
| /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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The documented persistent hub start command is Useful? React with 👍 / 👎. |
||
| /manual-hub/, | ||
| /(^|[\s;|&])(kill|pkill|fuser)[\s].*(3006|hapi-hub|\/hub\/|src\/index\.ts)/, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Quoted SSH commands such as Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the production stop command finds the PID by port before invoking kill, e.g. Useful? React with 👍 / 👎. |
||
| /systemctl[\s]+(stop|restart|kill|disable|mask)[\s]+hapi-(hub|runner|runner-watchdog)/, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the ACP shell request uses the packaged CLI to manage the runner, e.g. Useful? React with 👍 / 👎. |
||
| /git reset --hard.*(driver|hapi\/driver)/, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Git accepts the path selector before the subcommand ( Useful? React with 👍 / 👎. |
||
| /embeddedassets.*driver/, | ||
| /(\.hapi\/hapi\.db|hapi\.db\.bak)/, | ||
| ]; | ||
|
|
||
| const REMOTE_SSH_PATTERN = /(^|[\s|&;])(ssh|scp|rsync)[\s]|wsl[\s].*ssh/; | ||
|
|
||
| export function extractShellCommandLine(input: { | ||
| title?: string | null; | ||
| kind?: string | null; | ||
| rawInput?: unknown; | ||
| }): string { | ||
| if (typeof input.title === 'string' && input.title.trim()) { | ||
| return input.title.trim(); | ||
|
Comment on lines
+29
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an ACP permission request carries a generic non-empty title such as Useful? React with 👍 / 👎. |
||
| } | ||
| if (input.rawInput && typeof input.rawInput === 'object') { | ||
| const raw = input.rawInput as Record<string, unknown>; | ||
| for (const key of ['command', 'cmd', 'script']) { | ||
| const value = raw[key]; | ||
| if (typeof value === 'string' && value.trim()) { | ||
| return value.trim(); | ||
| } | ||
| } | ||
| } | ||
| return ''; | ||
| } | ||
|
|
||
| export function matchesProductionMutation(command: string): boolean { | ||
| if (!command.trim()) { | ||
| return false; | ||
| } | ||
| const lc = command.toLowerCase(); | ||
| return MUTATION_PATTERNS.some((re) => re.test(lc)); | ||
| } | ||
|
|
||
| export function matchesRemoteProductionMutation(command: string): boolean { | ||
| if (!command.trim()) { | ||
| return false; | ||
| } | ||
| const lc = command.toLowerCase(); | ||
| return REMOTE_SSH_PATTERN.test(lc) && matchesProductionMutation(command); | ||
| } | ||
|
|
||
| export function shouldDenyAgentShellCommand(input: { | ||
| title?: string | null; | ||
| kind?: string | null; | ||
| rawInput?: unknown; | ||
| }): { deny: boolean; command: string; reason?: string } { | ||
| const command = extractShellCommandLine(input); | ||
| if (!command) { | ||
| return { deny: false, command: '' }; | ||
| } | ||
|
|
||
| const lcKind = (input.kind ?? '').toLowerCase(); | ||
| const looksLikeShell = | ||
| lcKind === 'execute' || | ||
| lcKind === 'shell' || | ||
| lcKind === 'run_terminal_cmd' || | ||
| /^(ssh|curl|git|kill|nohup|hapi-|sudo|systemctl)\b/i.test(command); | ||
|
|
||
| if (!looksLikeShell && !matchesProductionMutation(command)) { | ||
| return { deny: false, command }; | ||
| } | ||
|
|
||
| if (matchesProductionMutation(command)) { | ||
|
Comment on lines
+77
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 Useful? React with 👍 / 👎. |
||
| return { | ||
| deny: true, | ||
| command, | ||
| reason: matchesRemoteProductionMutation(command) | ||
| ? 'Production HAPI mutation over SSH blocked (Windows estate muzzle).' | ||
| : 'Production HAPI mutation blocked (manual hub / stack switch / :3006).', | ||
| }; | ||
| } | ||
|
|
||
| return { deny: false, command }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wiring only protects PermissionAdapter-backed agents, but the repo also has ACP remote launchers with their own handlers (
GeminiPermissionHandler,OpencodePermissionHandler, andKimiPermissionHandler) that callresolveAutoApprovalDecisionand 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 👍 / 👎.