Skip to content

fix(permission): honour SDK ask hints, route effort through applyFlagSettings, offer Auto and dontAsk (lr-2ebc65) - #431

Merged
clagentic-merger[bot] merged 4 commits into
mainfrom
fix/lr-2ebc65-permission-hints
Oct 11, 2026
Merged

clagentic-merger[bot] merged 4 commits into
mainfrom
fix/lr-2ebc65-permission-hints

Conversation

@clagentic-builder

@clagentic-builder clagentic-builder Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

TASK: lr-2ebc65 (lead). Folds lr-5f4691 (effort and modes) and lr-543036 item 3 (remaining prompt-path findings). Closes lr-2ebc65, lr-5f4691 and lr-543036 on merge.

What changed

Permission hints (the safety defect). SDK 0.3.295 may attach suppressAlwaysAllowRule, defaultToNo and matchedAskRule to a canUseTool ask. No file under lib/ read them: the card and the banner offered Allow for session unconditionally, the server honoured it, and the worker relay dropped all three. Now:

  • The hints travel from the canUseTool options into the permission prompt record on both Claude paths. The worker IPC message is built and read by one module (lib/yoke/adapters/permission-relay.js). Codex approvals arrive with no hints and are unchanged.
  • Server enforcement: the permission kind refuses allow_always to a suppressed ask as invalid (settles nothing, writes no grant, re-offers the prompt to the responder) on every WS path. HTTP /api/permission-response answers allow or deny only (asserted).
  • Card and banner read one choice list (prompt-kinds/permission-choices.js). No Allow for session when suppressed. With defaultToNo the decline choice leads and is the marked default, no approve choice is marked, and there is no single-key shortcut. The card shows the user rule that forced an ask.
  • matchedAskRule is recorded on the prompt record and on the prompt_resolved event (bounded text; a rule that is not the expected shape still counts as rule-forced).
  • The recorded prompt_request, the prompt_pending snapshot, history paging and the notification meta all carry the hints.

Host-side auto-approval paths (all in handleCanUseTool, lib/sdk-bridge.js): full-auto mode, autonomous loop execution, the read-only and safe-command whitelist, and an earlier Allow for session. A matchedAskRule ask skips all four and reaches the operator.

Session-grant scope extension (reviewed, kept). A prior Allow-for-session grant does not auto-answer an ask carrying suppressAlwaysAllowRule or defaultToNo. Rationale, also stated in a code comment at the check in handleCanUseTool: a session grant is a standing rule for every later call with the same key, which is broader than the single action the SDK allows such an ask to cover, and the SDK forbids offering that; for defaultToNo, a silent approval would bypass the required opening on No, and the stricter reading is the safe one. This goes past the spec text and is called out for review.

Effort (lr-5f4691). The SDK Query has no setEffort. Both the in-process handle and the worker path now call applyFlagSettings({effortLevel}) through one module (effort-control.js). The worker handle settles on the worker real answer (requestId echo, 15s timeout, rejected when the query ends). The bridge records and announces an effort only after the query took it; a refusal is shown to the operator and the picker is put back. INTERFACE_VERSION is not bumped because test/yoke-robustness.test.js pins 0.2.0.

Modes (lr-5f4691). Auto and Dont-ask are offered only for a session whose vendor declares them in capabilities.permissionModes (Claude does, Codex does not). set_permission_mode refuses them otherwise.

lr-543036 item 1(b). The extension kind is not operator-answerable on any path (operatorMayAnswer), with a test.

lr-543036 item 3. prompt-card.js trySend reports a thrown send; newRequestId no longer reuses a tool-use id held by another session; a recorded AskUserQuestion skip replays as skipped; pruneTasks is not a defect (comment added); the round-trip schema generator gained one-sided bounds.

Round 2 (review rulings)

  • R1: the settle detector treats a rejection as settled. One shared isSettled in test/prompt-harness-world.js now serves permission-hints and claude-effort-control (the worker effort test used the same then-only shape and is fixed); prompt-hostile-request-id already tracked rejections. A new case proves a rejecting call reads as settled and a waiting one as pending.
  • R2: client() and withServer() moved into prompt-harness-world.js and reused by permission-hints, permission-mode-picker, claude-effort-control and prompt-hostile-request-id.
  • R3: the lost-step guard in prompt-invariants.test.js was unreachable (counted only for a foreign lib, asserted only for this checkout). The Harness now carries predatesPromptResponse (default: foreign lib), and a new case sets it on this checkout, sends an answer only prompt_response can carry, and asserts the run fails.
  • R4: the session-grant scope extension is kept and documented above and in the code comment.
  • The five restating-comment removals are intentional (in scope for lr-543036) and are not restored.

Demonstrated failure first

With lib/ at 3cd503e and the new tests present, npm test failed 36 tests: the two known environment failures and 34 new ones (a suppressed ask settling on allow_always, a real claude-worker.js process never answering set_effort with effort_changed, an extension-kind prompt_response settling a command, and the card, banner and hint tests). With this head only the two environment failures remain.

CREW_SOP section 6 compliance record

This diff changes what the console permission gate does at runtime. Every caller shape has a test (file: test name):

  • WS prompt_response from the card, WS aliases (permission_response), banner (targetSlug): permission-hints, a suppressed ask refuses allow_always on every answer path; permission-hints, a banner offers Allow for session only for an ask that allows it; prompt-hostile-request-id for hostile ids.
  • HTTP /api/permission-response: same test (allow_always is 400, ask stays pending) and the prompt-invariants http scenario.
  • In-process canUseTool: permission-hints, the hints reach the record, the recorded request, the snapshot and the notification; a rule-forced ask is never answered by a host-side auto-approval (includes an earlier session grant).
  • Worker IPC relay: claude-worker-process (real worker process, SDK stood in) and permission-hints, an ask relayed from a worker.
  • Codex approvals (no hints): permission-hints, an ask without hints keeps offering and honouring Allow for session.
  • Reconnect, rehydrate, history paging: permission-hints, prompt-kind-roundtrip, prompt-invariants I9.
  • Effort and mode picker: claude-effort-control, claude-worker-process, permission-mode-picker.

No crew hook, guard or test-runner behaviour changes. No new LLM call path, no new dependency.

Tests

npm test at this head, full suite run once. Failing-test-ID set: lr-29f9 (5): ceiling - query at MAX_CONCURRENT is rejected with error+done immediately; lr-2d91 (4): getMemoryStats returns activeLiveCount and maxConcurrentSessions. Both fail only inside a console-spawned session because of an inherited env var (lr-3c00dd). CI test check-run at the new head is reported in the PR conversation.

class sweep: git grep for then-only settle detectors and duplicated client()/withServer() helpers across test/, 3 duplicated-helper sites and 2 settle-detector sites fixed.

Followups

  • prompt-alert-client.test.js still pins the app-messages wiring by source-text checks (test-only, filing).
  • Capabilities of a vendor that is not the project default are never recorded, so Auto and Dont-ask are hidden for a Claude session in a Codex-default project. Fails closed, filing.
  • The settings-page default-mode picker is a separate surface and was not changed.

Task: lr-2ebc65

🤖 Generated with Claude Code

clagentic-builder Bot and others added 3 commits October 10, 2026 22:35
…plyFlagSettings, gate Auto and dontAsk by capability (lr-2ebc65)

Carry suppressAlwaysAllowRule, defaultToNo and matchedAskRule from the
canUseTool options into the permission prompt record, in process and over
the worker IPC (one shared message shape for both sides). The permission
kind refuses an Allow-for-session answer to a suppressed ask on every
response path, so the prompt stays pending, nothing is granted and the
responder is shown it again. No host-side auto-approval answers a
rule-forced ask, and no standing approval answers an ask the SDK marked as
needing its own answer. The rule is recorded on the resolved event.

A prompt_response naming a browser-extension command no longer settles it;
only the socket the command was sent to can.

Effort changes on a running Claude session go through
applyFlagSettings({effortLevel}) on both the in-process and worker paths;
the worker handle settles on the worker's real outcome, and the bridge
announces an effort only once the query has taken it. The Claude
capabilities list permissionModes, and set_permission_mode refuses dontAsk
and auto for a session whose vendor does not declare them.

A tool-use id shared by two sessions no longer takes over the other
session's request index entry, and a recorded AskUserQuestion skip replays
as skipped.

Folds in lr-5f4691 and lr-543036 item 3.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…No ask on No, offer Auto and Don't ask by capability (lr-2ebc65)

The card and the notification banner read one choice list
(permission-choices.js), so they cannot offer different answers to one ask.
The card shows the user rule that forced an ask. The mode picker offers Don't
ask and Auto only for a session whose vendor declares them. A send that
throws is reported as such instead of as a dropped connection, and a
recorded AskUserQuestion skip replays as skipped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ing on every caller shape (lr-2ebc65)

Hint fields join the prompt harness world, the round-trip property test (every
kind, with the connect snapshot) and the invariant model (I9). A real
claude-worker.js process runs against a stand-in SDK to prove the IPC
payloads and the applyFlagSettings path. Adds the schema generator's missing
one-sided bounds, tracks rejected callbacks in the hostile-id test, makes a
lib that cannot express an answer explicit in the invariant harness, and
documents the hints, the effort control and the capability-gated modes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@clagentic-security

Copy link
Copy Markdown

BOBBIE — clean (0 finding(s))

{"reviewer": "bobbie", "review_status": "clean", "head_sha": "0ffa94398e16425b0d4de0b71e554844661a4b03", "pr_number": 431}

@clagentic-reviewer

Copy link
Copy Markdown

PEACHES — blocking (8 finding(s), 4 dropped)

  • lib/prompt-kinds/codecs.js:38 [amos.code-craft.1] (nit) Removing the sourceUrl() docstring is a drive-by change unrelated to this assignment. Revert it or return it in followups[].
  • lib/prompt-kinds/index.js:37 [amos.code-craft.1] (nit) The sessionStores() docstring ('Session fields that hold pending prompts, one per store.') was deleted as a drive-by while adding operatorMayAnswer. Restore it.
  • lib/sdk-bridge.js:1021 [amos.path-choice.5] (nit) ruleForced changes which asks the canUseTool permission gate answers at runtime. The PR body needs a CREW_SOP §6 record covering in-process and worker callers. It is not visible in this chunk; conf...
  • test/fake-dom-prompt-cards.js:104 [amos.code-craft.1] (nit) Deleting the typeInto doc comment is an unrelated drive-by edit in a fake-DOM helper this change does not otherwise touch; drop it from this diff or return it in followups[].
  • test/permission-hints.test.js:42 [amos.path-choice.4] (blocking) isSettled uses promise.then(fn) with no rejection handler. The same diff fixes this exact shape in prompt-hostile-request-id.test.js ('A callback that fails is settled too') but leaves it standing ...
    failure sequence: 1) A regression makes handleCanUseTool or the extension answer reject 2) isSettled never sets state.settled on rejection because it has no rejection handler 3) the assertions still read settled as false and pass 4) the rejected call is reported as pending and the regression goes undetected
  • test/permission-mode-picker.test.js:46 [amos.code-craft.2] (nit) client() and withServer() are copied verbatim from permission-hints.test.js. Reuse them through prompt-harness-world.js, which this diff already extends, instead of duplicating the helpers.
  • test/prompt-harness-world.js:78 [amos.code-craft.1] (nit) Drive-by removal of the existing heldPromptIds doc comment, which is unrelated to the hints and queryOptions change in this diff.
  • test/shared-codecs.test.js:20 [amos.code-craft.1] (nit) Drive-by removal of the comment explaining ANSWER_CODECS ('The codecs that bound typed text.'). It is unrelated to the hint and prompt-path changes in this diff.

Dropped candidates (4):

  • lib/prompt-registry.js:351 [amos.code-craft.3] The new nm.notify(...) line is indented at the outer block's level, not inside the if (nm && ...) body it belongs to. This does not match the surrounding style. (reason: Indentation inside a correct if-block is linter-owned style and the candidate shows no defect or bad outcome)
  • lib/sdk-bridge.js:2445 [amos.path-choice.1] The typeof guard on queryInstance.setEffort was removed. A handle without setEffort now throws a TypeError, shown as 'Failed to change effort: … is not a function', instead of recording the effort ... (reason: The removed typeof guard now surfaces a missing setEffort as the failure lr-5f4691 requires and the candidate names no bad outcome)
  • test/prompt-hostile-request-id.test.js:62 [amos.code-craft.5] This changes an existing test's settle detection so a rejection now counts as settled. That is likely correct, but the PR should say which behavior made it necessary, so it does not read as looseni... (reason: The candidate asks for PR explanation and says the change is likely correct, so it names no defect)
  • test/prompt-invariants.test.js:643 [amos.code-craft.5] The unexpressible guard can never fire. The counter only increments when world.overridesLib is true, but assertClean only checks it when overridesLib is false, so skipped steps against another lib ... (reason: Cited rule amos.code-craft.5 covers modifying existing tests and not this guard, so no cited rule supports the blocking grade)
{"reviewer": "peaches", "review_status": "blocking", "head_sha": "0ffa94398e16425b0d4de0b71e554844661a4b03", "pr_number": 431, "failure_sequences": [{"file": "test/permission-hints.test.js", "line": 42, "rule_id": "amos.path-choice.4", "failure_sequence": "1) A regression makes handleCanUseTool or the extension answer reject 2) isSettled never sets state.settled on rejection because it has no rejection handler 3) the assertions still read settled as false and pass 4) the rejected call is reported as pending and the regression goes undetected"}], "dropped": [{"file": "lib/prompt-registry.js", "line": 351, "rule_id": "amos.code-craft.3", "message": "The new nm.notify(...) line is indented at the outer block's level, not inside the `if (nm && ...)` body it belongs to. This does not match the surrounding style.", "reason": "Indentation inside a correct if-block is linter-owned style and the candidate shows no defect or bad outcome"}, {"file": "lib/sdk-bridge.js", "line": 2445, "rule_id": "amos.path-choice.1", "message": "The typeof guard on queryInstance.setEffort was removed. A handle without setEffort now throws a TypeError, shown as 'Failed to change effort: \u2026 is not a function', instead of recording the effort ...", "reason": "The removed typeof guard now surfaces a missing setEffort as the failure lr-5f4691 requires and the candidate names no bad outcome"}, {"file": "test/prompt-hostile-request-id.test.js", "line": 62, "rule_id": "amos.code-craft.5", "message": "This changes an existing test's settle detection so a rejection now counts as settled. That is likely correct, but the PR should say which behavior made it necessary, so it does not read as looseni...", "reason": "The candidate asks for PR explanation and says the change is likely correct, so it names no defect"}, {"file": "test/prompt-invariants.test.js", "line": 643, "rule_id": "amos.code-craft.5", "message": "The unexpressible guard can never fire. The counter only increments when world.overridesLib is true, but assertClean only checks it when overridesLib is false, so skipped steps against another lib ...", "reason": "Cited rule amos.code-craft.5 covers modifying existing tests and not this guard, so no cited rule supports the blocking grade"}]}

…unexpressible-step guard reachable (lr-2ebc65)

A settle detector now counts a rejection as settled (shared isSettled, also
used by the worker effort test), client() and withServer() live in the prompt
harness world, the lost-step guard has a case that trips it, and the grant
check documents why a session grant never answers a restricted ask.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@clagentic-reviewer

Copy link
Copy Markdown

PEACHES — clean (2 finding(s), 5 dropped)

  • lib/sdk-bridge.js:1021 [amos.path-choice.5] (nit) ruleForced changes which asks the canUseTool permission gate answers at runtime. The PR body needs a CREW_SOP §6 record covering in-process and worker callers. It is not visible in this chunk; conf...
  • test/permission-hints.test.js:21 [amos.code-craft.2] (nit) var isSettled = world.isSettled shadows a local function isSettled (still at ~lines 28-32, the old fulfil-only shape) that is now dead. Delete the local copy so one helper remains.

Dropped candidates (5):

  • lib/prompt-kinds/codecs.js:38 [amos.code-craft.1] Removing the sourceUrl() docstring is a drive-by change unrelated to this assignment. Revert it or return it in followups[]. (reason: Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised)
  • lib/prompt-kinds/index.js:37 [amos.code-craft.1] The sessionStores() docstring ('Session fields that hold pending prompts, one per store.') was deleted as a drive-by while adding operatorMayAnswer. Restore it. (reason: Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised)
  • test/fake-dom-prompt-cards.js:104 [amos.code-craft.1] Deleting the typeInto doc comment is an unrelated drive-by edit in a fake-DOM helper this change does not otherwise touch; drop it from this diff or return it in followups[]. (reason: Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised)
  • test/shared-codecs.test.js:20 [amos.code-craft.1] Drive-by removal of the comment explaining ANSWER_CODECS ('The codecs that bound typed text.'). It is unrelated to the hint and prompt-path changes in this diff. (reason: Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised)
  • test/prompt-harness-world.js:78 [amos.code-craft.1] Drive-by removal of the existing heldPromptIds doc comment, which is unrelated to the hints and queryOptions change in this diff. (reason: Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised)
{"reviewer": "peaches", "review_status": "clean", "head_sha": "80e1c1f37c417d5afb9e7d55f20e11bfa32327c6", "pr_number": 431, "dropped": [{"file": "lib/prompt-kinds/codecs.js", "line": 38, "rule_id": "amos.code-craft.1", "message": "Removing the sourceUrl() docstring is a drive-by change unrelated to this assignment. Revert it or return it in followups[].", "reason": "Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised"}, {"file": "lib/prompt-kinds/index.js", "line": 37, "rule_id": "amos.code-craft.1", "message": "The sessionStores() docstring ('Session fields that hold pending prompts, one per store.') was deleted as a drive-by while adding operatorMayAnswer. Restore it.", "reason": "Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised"}, {"file": "test/fake-dom-prompt-cards.js", "line": 104, "rule_id": "amos.code-craft.1", "message": "Deleting the typeInto doc comment is an unrelated drive-by edit in a fake-DOM helper this change does not otherwise touch; drop it from this diff or return it in followups[].", "reason": "Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised"}, {"file": "test/shared-codecs.test.js", "line": 20, "rule_id": "amos.code-craft.1", "message": "Drive-by removal of the comment explaining ANSWER_CODECS ('The codecs that bound typed text.'). It is unrelated to the hint and prompt-path changes in this diff.", "reason": "Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised"}, {"file": "test/prompt-harness-world.js", "line": 78, "rule_id": "amos.code-craft.1", "message": "Drive-by removal of the existing heldPromptIds doc comment, which is unrelated to the hints and queryOptions change in this diff.", "reason": "Restates a recorded ruling: the deleted restating comment is in scope per lr-543036 seq 3, not re-raised"}]}

@clagentic-security

Copy link
Copy Markdown

BOBBIE — clean (0 finding(s))

{"reviewer": "bobbie", "review_status": "clean", "head_sha": "80e1c1f37c417d5afb9e7d55f20e11bfa32327c6", "pr_number": 431}

@clagentic-merger
clagentic-merger Bot merged commit e077ed8 into main Oct 11, 2026
5 of 6 checks passed
@clagentic-merger

Copy link
Copy Markdown
Contributor

Merged via clagentic-loadout v0.2.0

Field Value
Gated HEAD SHA 80e1c1f37c417d5afb9e7d55f20e11bfa32327c6
Merged SHA 80e1c1f37c417d5afb9e7d55f20e11bfa32327c6
Reviews clagentic-reviewer[bot], clagentic-security[bot]
CI status no-runner-by-design (0 commit-status entries at HEAD)
task_id lr-2ebc65

@clagentic-merger
clagentic-merger Bot deleted the fix/lr-2ebc65-permission-hints branch October 11, 2026 03:00
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.

0 participants