Skip to content

Add a uniform per-command approval relay (ask mode)#42

Merged
konard merged 7 commits into
mainfrom
issue-40-4903ca5eeb15
Jun 17, 2026
Merged

Add a uniform per-command approval relay (ask mode)#42
konard merged 7 commits into
mainfrom
issue-40-4903ca5eeb15

Conversation

@konard

@konard konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements a uniform per-command approval ("ask" mode) relay, closing #40.

A single option — --approve-each (alias --permission-mode ask, library approveEach / 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 NDJSON permission_request event 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-only uses (precedent: #41 / #39).

Fixes #40

What each part of the issue maps to

  1. Uniform option → --approve-each / --permission-mode ask, threaded through CLI parser → command builder → controller in both JS and Rust.
  2. Relay + scope → a normalized request carries { type, tool, id, sessionId, callId, toolName, title, command, pattern, scope, input }. The decision is translated to the native frame: agent's permission_response, Claude's control_response (allow/deny, echoing updatedInput).
  3. Parity table with a scope column → documented in docs/common-concepts.md (addresses @m13v's note that always differs per backend: agent = session, claude = tool-input).

Parity (scope column included)

Tool Native mechanism Scope Relay
agent --permission-mode ask (+ --input-format stream-json) session
claude --permission-mode default (stream-json can_use_tool) tool-input
codex --ask-for-approval (coupled with --sandbox) sandbox-coupled
qwen --approval-mode default interactive-only
gemini --approval-mode default interactive-only
opencode OPENCODE_PERMISSION (static policy) static-policy

Only agent and claude can drive the handshake. For the others, --approve-each is 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):

node examples/permission-relay.mjs

It feeds native permission_request / can_use_tool frames into a PermissionRelay and prints the exact native response frames forwarded to the CLI's stdin (e.g. agent permission_response, Claude control_response allow/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-each

start-agent emits one normalized permission_request NDJSON line per command to stdout and reads {"type":"permission_response","id":"...","decision":"once|always|reject"} from stdin.

Tests

  • JS: new js/test/permissions.test.mjs (21 tests) plus additions to cli-parser, command-builder, index, and tools suites — 230 passing, lint + format clean.
  • Rust: new rust/tests/permissions_tests.rs (20 tests) plus inline tests in command_builder.rs and cli_parser.rs — full suite green, cargo fmt --check, cargo clippy --all-targets --all-features (with -Dwarnings) clean.

Release

  • JS changeset (minor) and Rust changelog fragment (minor); versions are bumped automatically by the release workflows on merge.

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #40
@konard konard self-assigned this Jun 17, 2026
konard added 4 commits June 17, 2026 16:23
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)
@konard konard changed the title [WIP] Add a uniform per-command approval relay (ask mode) forwarding native permission_request/response frames Add a uniform per-command approval relay (ask mode) Jun 17, 2026
@konard konard marked this pull request as ready for review June 17, 2026 16:28
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.
@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Working session summary

The deno CI failure is fixed and everything is green. Here's the final state:

Summary

The remaining issue was a deno-only CI failure for the test agent - approve-each on unsupported tool throws at start.

Root cause: The ask-mode unsupported-tool guard lived inside buildAgentCommand, which runs after preparePromptFile in start(). On deno (whose CI runs without --allow-write), the temporary prompt-file write threw a NotCapable: Requires write access error first, masking the intended does not support enforceable per-command approval error.

Fix (js/src/index.mjs): Moved the guard to the top of start(), before any filesystem side effect, so it fails clearly regardless of write permissions — matching the --read-only failure pattern:

if (approveEach && !(toolConfig && toolConfig.supportsAsk)) {
  throw new Error(askUnsupportedError({ tool }));
}

Verification:

  • ✅ Node: 230/230 tests pass
  • ✅ Deno: 227 passed, 0 failed (the previously failing test now passes locally)
  • ✅ eslint: 0 errors; prettier: clean
  • ✅ Example examples/permission-relay.mjs runs
  • CI on commit 8eca65e: JavaScript CI/CD success + Rust CI/CD success (all platforms)

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 --approve-each/--permission-mode ask with a normalized relay carrying a scope field, the parity table, and JS+Rust feature parity) is complete with all CI green: #42


This summary was automatically extracted from the AI working session output.

@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $22.935984
  • Calculated by Anthropic: $25.702880
  • Difference: $2.766896 (+12.06%)

📊 Context and tokens usage:

Claude Opus 4.8: (8 sub-sessions)

  1. 109.7K / 1M (11%) input tokens, 7.5K / 128K (6%) output tokens
  2. 112.3K / 1M (11%) input tokens, 14.8K / 128K (12%) output tokens
  3. 116.8K / 1M (12%) input tokens, 27.6K / 128K (22%) output tokens
  4. 112.7K / 1M (11%) input tokens, 26.0K / 128K (20%) output tokens
  5. 116.8K / 1M (12%) input tokens, 19.7K / 128K (15%) output tokens
  6. 112.3K / 1M (11%) input tokens, 13.0K / 128K (10%) output tokens
  7. 115.8K / 1M (12%) input tokens, 32.4K / 128K (25%) output tokens
  8. 43.6K / 1M (4%) input tokens, 6.3K / 128K (5%) output tokens

Total: (172.9K new + 737.8K cache writes + 24.8M cache reads) input tokens, 202.9K output tokens, $22.935984 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus
  • Model: Claude Opus 4.8 (claude-opus-4-8)

📎 Log file uploaded as Gist (10099KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

@konard konard merged commit 9838f01 into main Jun 17, 2026
50 checks passed
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.

Add a uniform per-command approval relay (ask mode) forwarding native permission_request/response frames

1 participant