Add a uniform per-command approval relay (ask mode)#42
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: #40
Add a uniform --approve-each / --permission-mode ask option that maps to each backend's native per-command approval mechanism and relays native permission requests to the consumer as normalized NDJSON events, forwarding the consumer's decision (once|always|reject) back in the correct wire format. - agent: --permission-mode ask + --input-format stream-json (permission_request/permission_response, scope=session) - claude: --permission-mode default stream-json can_use_tool (control_request/control_response, scope=tool-input) - codex/qwen/gemini/opencode: supportsAsk=false, fail clearly like --read-only does for tools that cannot enforce it Mirrored across JS (js/) and Rust (rust/) for feature parity, with a transport-agnostic PermissionRelay, normalization layer carrying a scope field, a 6-row parity table, and full unit/integration tests.
- docs/common-concepts.md: add ask-mode parity table with a scope column - js/README.md, rust/README.md: document --approve-each / --permission-mode ask - js changeset (minor) + rust changelog fragment (minor)
The approve-each unsupported-tool guard ran inside buildAgentCommand, which executes after preparePromptFile in start(). On Deno (no --allow-write in CI) the temp prompt write failed first, masking the intended 'does not support enforceable per-command approval' error. Move the guard to the top of start() so it fails clearly before any filesystem side effect, matching the --read-only failure pattern.
Working session summaryThe deno CI failure is fixed and everything is green. Here's the final state: SummaryThe remaining issue was a deno-only CI failure for the test Root cause: The ask-mode unsupported-tool guard lived inside Fix ( if (approveEach && !(toolConfig && toolConfig.supportsAsk)) {
throw new Error(askUnsupportedError({ tool }));
}Verification:
PR #42 final state: ready (not draft), OPEN, MERGEABLE, clean working tree, main merged in, version bumped to 0.7.0. The full issue #40 implementation (uniform This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost estimation:
📊 Context and tokens usage:Claude Opus 4.8: (8 sub-sessions)
Total: (172.9K new + 737.8K cache writes + 24.8M cache reads) input tokens, 202.9K output tokens, $22.935984 cost 🤖 Models used:
📎 Log file uploaded as Gist (10099KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
✅ Ready to mergeThis pull request is now ready to be merged:
Monitored by hive-mind with --auto-restart-until-mergeable flag |
This reverts commit 8e37dab.
Summary
Implements a uniform per-command approval ("ask" mode) relay, closing #40.
A single option —
--approve-each(alias--permission-mode ask, libraryapproveEach/approve_each) — maps to each backend's native per-command approval mechanism. For backends that expose a drivable JSON handshake, every native permission prompt is relayed to the consumer as a normalized NDJSONpermission_requestevent and the consumer's normalized decision (once|always|reject) is forwarded back to the native CLI in its own wire format. Backends with no relayable handshake fail clearly up front — the same pattern--read-onlyuses (precedent: #41 / #39).Fixes #40
What each part of the issue maps to
--approve-each/--permission-mode ask, threaded through CLI parser → command builder → controller in both JS and Rust.{ type, tool, id, sessionId, callId, toolName, title, command, pattern, scope, input }. The decision is translated to the native frame: agent'spermission_response, Claude'scontrol_response(allow/deny, echoingupdatedInput).docs/common-concepts.md(addresses @m13v's note thatalwaysdiffers per backend: agent =session, claude =tool-input).Parity (scope column included)
agent--permission-mode ask(+--input-format stream-json)sessionclaude--permission-mode default(stream-jsoncan_use_tool)tool-inputcodex--ask-for-approval(coupled with--sandbox)sandbox-coupledqwen--approval-mode defaultinteractive-onlygemini--approval-mode defaultinteractive-onlyopencodeOPENCODE_PERMISSION(static policy)static-policyOnly
agentandclaudecan drive the handshake. For the others,--approve-eachis rejected with:Tool "<tool>" does not support enforceable per-command approval (ask mode). Choose one of: agent, claude; or run without --approve-each.How to reproduce / try it
A runnable, dependency-free demo of the relay (no agent process required):
It feeds native
permission_request/can_use_toolframes into aPermissionRelayand prints the exact native response frames forwarded to the CLI's stdin (e.g. agentpermission_response, Claudecontrol_responseallow/deny). It also shows that unsupported tools and non-permission messages are handled correctly.End-to-end, a consumer runs:
start-agent --tool agent --working-directory /tmp/project --prompt "..." --approve-eachstart-agentemits one normalizedpermission_requestNDJSON line per command to stdout and reads{"type":"permission_response","id":"...","decision":"once|always|reject"}from stdin.Tests
js/test/permissions.test.mjs(21 tests) plus additions to cli-parser, command-builder, index, and tools suites — 230 passing, lint + format clean.rust/tests/permissions_tests.rs(20 tests) plus inline tests incommand_builder.rsandcli_parser.rs— full suite green,cargo fmt --check,cargo clippy --all-targets --all-features(with-Dwarnings) clean.Release
minor) and Rust changelog fragment (minor); versions are bumped automatically by the release workflows on merge.