@sema-agent/core 7.42.1 → 7.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -3
  5. package/dist/agents/teacher.js +1 -1
  6. package/dist/agents/verify.js +2 -0
  7. package/dist/core/allow-entry.d.ts +50 -31
  8. package/dist/core/allow-entry.js +100 -37
  9. package/dist/core/ask-origin.d.ts +16 -5
  10. package/dist/core/ask-origin.js +11 -3
  11. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  12. package/dist/core/ask-unresolvable-notice.js +1 -1
  13. package/dist/core/auto-mode.d.ts +5 -4
  14. package/dist/core/auto-mode.js +3 -1
  15. package/dist/core/checkpoint-store.d.ts +18 -7
  16. package/dist/core/checkpoint-store.js +6 -3
  17. package/dist/core/child-clamp-seats.d.ts +15 -1
  18. package/dist/core/child-clamp-seats.js +6 -2
  19. package/dist/core/classifier-unavailable-arm.d.ts +42 -23
  20. package/dist/core/classifier-unavailable-arm.js +14 -3
  21. package/dist/core/decision-receipt.js +2 -0
  22. package/dist/core/gate-lanes.js +37 -29
  23. package/dist/core/hooks.d.ts +6 -1
  24. package/dist/core/hooks.js +3 -0
  25. package/dist/core/mode-policy.d.ts +5 -4
  26. package/dist/core/mode-policy.js +4 -0
  27. package/dist/core/permission-rule-consent.d.ts +3 -2
  28. package/dist/core/permission-rule-consent.js +25 -20
  29. package/dist/core/permission-rule-model.d.ts +47 -7
  30. package/dist/core/permission-rule-model.js +80 -6
  31. package/dist/core/permission-rules.d.ts +8 -2
  32. package/dist/core/permission-rules.js +26 -9
  33. package/dist/core/runner/classifier-ladder.d.ts +10 -8
  34. package/dist/core/runner/classifier-ladder.js +3 -1
  35. package/dist/core/runner/denial-limit-arms.d.ts +25 -8
  36. package/dist/core/runner/denial-limit-arms.js +24 -10
  37. package/dist/core/runner/gate-exit.js +2 -1
  38. package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
  39. package/dist/core/runner/permission-rule-lanes.js +8 -2
  40. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  41. package/dist/core/runner/prepare-ask-lane.js +2 -1
  42. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
  43. package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
  44. package/dist/core/runner/prepare-config-doors.d.ts +3 -0
  45. package/dist/core/runner/prepare-config-doors.js +6 -5
  46. package/dist/core/runner/prepare-gate-stations.js +1 -1
  47. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  48. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  49. package/dist/core/runner/prepare-park-ask.d.ts +4 -4
  50. package/dist/core/runner/prepare-park-ask.js +2 -4
  51. package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
  52. package/dist/core/runner/prepare-policy-chain.js +59 -38
  53. package/dist/core/runner/prepare-project-context.js +6 -2
  54. package/dist/core/runner/prepare-task.js +6 -6
  55. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
  56. package/dist/core/runner/prepare-wiring-manifest.js +8 -9
  57. package/dist/core/runner/resume-checkpoint-screen.js +1 -1
  58. package/dist/core/runner/synthetic-tools.d.ts +3 -2
  59. package/dist/core/runner/synthetic-tools.js +1 -1
  60. package/dist/core/session-policy-store.d.ts +3 -3
  61. package/dist/core/session-policy-store.js +18 -4
  62. package/dist/core/skill-grant-binding.d.ts +44 -0
  63. package/dist/core/skill-grant-binding.js +22 -0
  64. package/dist/core/skill-grant.d.ts +59 -28
  65. package/dist/core/skill-grant.js +96 -23
  66. package/dist/core/task-spec.d.ts +14 -1
  67. package/dist/core/tighten-task-spec.d.ts +2 -0
  68. package/dist/core/tighten-task-spec.js +3 -0
  69. package/dist/core/tool-name-aliases.d.ts +35 -5
  70. package/dist/core/tool-name-aliases.js +29 -4
  71. package/dist/core/tool-policy.d.ts +10 -0
  72. package/dist/core/tool-policy.js +9 -2
  73. package/dist/core/tool-spec.d.ts +7 -0
  74. package/dist/core/wiring-manifest.d.ts +56 -14
  75. package/dist/core/wiring-manifest.js +29 -4
  76. package/dist/index.d.ts +3 -2
  77. package/dist/index.js +2 -2
  78. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  79. package/dist/orchestration/run-workflow-tool.js +1 -1
  80. package/dist/orchestration/workflow-primitives.d.ts +3 -1
  81. package/dist/orchestration/workflow-primitives.js +6 -6
  82. package/package.json +1 -1
  83. package/test/export-surface.snapshot.json +11 -1
  84. package/wire-changes/core@7.43.0.json +985 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,103 @@
1
+ ## 7.43.0 — 2026-10-06
2
+
3
+ **Hard-breaking changes:** one — #1318. **#1318 — What breaks:** the parameter spelling of a Skill rule — `Skill(skill:<name>)`, and `Skill(args:…)` / `Skill(skill:*)` with it — a working but non-reference spelling in a caller policy (`createPermissionRulePolicy`), is refused at construction (`config.invalid_permission_rules`, one `issues[]` entry per rule with `code: "invalid.command_rule"`, the sentence naming `Skill(<name>)` and `Skill(<name>:*)`; under `onInvalidRule: "skip"` it is reported through `onIssue` and dropped, so a deny written that way stops firing until it is respelled); the reference's own spelling `Skill(<name>)` / `Skill(<prefix>:*)`, which 7.42.x refused (`invalid.param_split` in a policy, `unsupported.tool` in the persisted grammar), is now the one spelling, read by both compilers. **Who is hurt:** a host that wrote the parameter spelling — the serving layer that encodes a `permissions.ask` / `permissions.deny` entry of the Skill tool into policy rules, a hand-written policy; a probe that pinned `Skill(<name>)` as refused; a consumer that switches exhaustively over `PersistedRuleMatch`. An organization rule file with a `Skill(…)` row still refuses the WHOLE snapshot (the org grammar is the shell's alone — unchanged). **Compensation:** respell `Skill(skill:deploy)` as `Skill(deploy)`, and a wildcard on the skill (`Skill(skill:deploy*)`) as `Skill(deploy:*)` (a name PREFIX — no word boundary).
4
+
5
+ **Package exports (the whole release):** +5 / −0 — `APPROVAL_REACH`, `ApprovalReach`, `isApprovalReach` (#1354), `BoundSkillGrants` (#1348 / #1357), `abortedSeatDeny` (#1371). The paragraphs below state each ticket's own closed sets and its own share of the exports.
6
+
7
+ **Closed sets (#1318):** `PersistedRuleMatch` +2 (`name`, `name_prefix`); `ruleToolGrammarOf` answers +`"name"` (type-level only); `PermissionRuleIssue.code` unchanged (the retired spelling rides the existing `invalid.command_rule`). **Package exports (#1318):** none added or removed.
8
+
9
+ **Behavior:** a run can now declare that nobody on it can be asked to approve (`TaskSpec.approvalReach: "none"`; absent = unchanged, byte for byte); in auto mode a classification whose reply carried no verdict now goes to the person where the run has an approval seat (C-R238) instead of being refused and counted — and a card the seat's deployment POLICY refused counts on the denial limit, so a shell that refuses every card still reaches the bound (K-14; at the bound the same call is put to the seat again as the denial-limit fallback); the inherited two-frames collapse no longer lets an ancestor's "classifier did not rule" card answer a child's own real-approval question (one more card); a retained child spawned under posture `"interactive"` and woken by a hard-headless or declared-none run now revives (its posture yields) instead of being refused; the headless refusal's bracket and three engine sentences no longer say "no approver is wired" (seat-neutral wording; the way out names the seat); a call to the engine's `Skill` tool now carries the skill and its two lists (`boundGrants`) and their hash (`boundGrantDigest`) on its live ask, an ancestor's ask, the parked row and the inbox projection (the hash was on the parked row alone; the resume still judges the digest alone, its bytes unchanged). **Closed sets:** `AskSeamForm` +1 (`declared_none`); `AskClass` +1 (`classifier_no_ruling`); the `classifier.unavailable` notice's `detail.cause` +1 (`parse_error`); new `APPROVAL_REACH` (`none`); new thrown code `config.approval_reach_invalid` (also a `checkpoint.config_refused` `detail.code`); `CHECKPOINT_CONFIG_REFUSAL_CODES` +1 and `HeadlessApproverSeat` +1 (engine-internal). **Package exports:** +4 — `APPROVAL_REACH`, `ApprovalReach`, `isApprovalReach`, `BoundSkillGrants` (type); `ClassifierUnavailableInfo.cause` widens to `AutoModeFallbackCause`; new optional members `TaskSpec.approvalReach`, `ToolExecuteContext.approvalReach`, `AskRequest.classifierNoRuling`, the `PermissionResult` ask arm's `classifierNoRuling`, `PendingAction.tool_approval.classifierNoRuling`, `CheckpointSummary.classifierNoRuling`, `AskCarry.classifierNoRuling`, `ToolGateInput.askSeatForm`, `AskRequest.boundGrantDigest`, `AskRequest.boundGrants`, `PendingAction.tool_approval.boundGrants`, `CheckpointSummary.boundGrantDigest`, `CheckpointSummary.boundGrants`; `resolveAskSeamForm`'s spec reads `approvalReach` too.
10
+
11
+ **Behavior narrowing (#1372):** a skill whose `disallowed-tools` holds an entry the list grammar reads as nothing (a surrounding space, a retired tool name, a case twin of a mounted tool, `*`, `mcp__*`, `WebFetch(domain:…)`, `Skill(<name>)`, any other content form outside the shell and path grammars) no longer activates — the model's `Skill` call naming it is refused where the run's skill deny rows are read (`tool_end.gate.disposition` denied / `policy`; no card, no park, whatever the approver or mode), and a person's `userInvokedSkills` refuses the run (`config.skill_deny_unreadable`) — and a park's frozen deny record, or a parent's captured one, that reads as nothing against the leg's roster refuses the leg; `config.user_invoked_skill_unknown` is refused before a resume's claim. **Closed sets:** new thrown code `config.skill_deny_unreadable`; `CHECKPOINT_CONFIG_REFUSAL_CODES` +2 (`config.user_invoked_skill_unknown`, `config.skill_deny_unreadable`). **Package exports:** none added or removed; new optional member `SkillGrantHit.readsAsNothing` (on a skill lane's refusal: the named skill's deny entries that read as nothing).
12
+
13
+ **Behavior (#1355 (b), #1358):** a rule's tool name whose peer segment no mint spells (`mcp__github.com__*`, where the mounted tools are `mcp__github_com__…`) is now a stale name: the prepare audit refuses to start the run (`config.legacy_tool_name`; it was an advisory) and the session store refuses the write, both naming the spelling to write; and the allow lists — `TaskSpec.allowedTools`, `RunnerDeps.allowedTools`, a skill's `allowed-tools` / `disallowed-tools` — read an entry's tool name against the run's roster: a stale name, or a name that differs from a mounted tool's only by case, is announced (`permission.allowed_tool_not_granted`) and read as nothing; a name the run does not mount stays silent. **Closed sets:** `RULE_TOOL_NAME_VERDICTS` +1 (`unmintable_segment`; `RuleToolNameVerdict`, `SessionRuleNameRefusal.verdict` follow); `ALLOW_ENTRY_INERT_REASONS` +2 (`tool_name`, `near_miss`). **Package exports:** none added or removed; `compileAllowEntry` / `compileAllowList` take an optional third argument (the roster), and a `compileAllowList` inert item may carry `verdict` and `currentName`.
14
+
15
+ ### Release tooling: the wire manifest reads the keys of an array's element type (#1369; tooling, no package behavior)
16
+
17
+ - **Before.** `scripts/wire-changes.mjs` read `T[]` / `Array<T>` as leaves: a key added or removed inside an array's element shape (`WorkflowRun.parks[].parkedBy`) never reached `typeKeys`, so a removal there shipped with no BREAKING line.
18
+ - **Now.** The element type is read under a `[]` hop (`parks[].parkedBy`); the added-parent rule, the move reconciliation through a referenced type and the optional-member claim token all understand the hop; a member that existed and now points into a type the other ref lacks has that type's keys compared under it (recursively), while a brand-new member typed as a new type stays one added key. Pins: `test/backlog1369-wire-changes-array-element-keys.test.ts` ①–⑪ (baseline 3 red / 1 control).
19
+ - **Who is hurt.** Nobody at runtime; release posts from this version on may list more `typeKeys` rows than before (keys that were invisible).
20
+ ### A Skill rule has one spelling — `Skill(<name>)` / `Skill(<prefix>:*)` — and the activation card offers it (#1318; behavior + hard-breaking surface) (@server @cli @client-core @test)
21
+
22
+ - **Before (v7.42.x).** The persisted rule grammar answered `unsupported.tool` to every Skill rule; the caller policy's compiler answered `invalid.param_split` ("param-level ruleContent must be `param:pattern`") to the reference's `Skill(s1)` and took the non-reference parameter spelling `Skill(skill:s1)` instead; a server that encoded `settings.permissions.ask` into policy rules had `Skill(s1)` refused and listed in `config.invalid_permission_rules`. The skill activation card carried no "don't ask again" offer (the offer lane spoke for the shell tool alone), so a shell wrote the whole tool `Skill` as the remembered rule — over a person's own ask rule.
23
+ - **Now.** One reader for every consumer: `Skill(<name>)` is the exact name, `Skill(<prefix>:*)` (a trailing ` *` reads the same) a name PREFIX with no word boundary — the reference's matcher (`Y()`, cli280.js 19681196: `n === name`, else `name.startsWith(prefix)`); one leading `/` is read off; a name may carry a colon (`Skill(apps/web:deploy)`). The caller policy compiles it as a deny / ask (the call's `skill` argument is the string it is read against — the one the `Skill` tool resolves a call by); the persisted grammar takes it under `deny`, `ask` and `allow` (`tool: "Skill"`, `match: "name" | "name_prefix"`, `command: <name or prefix>`) and the personal rule lane reads such rows on every `Skill` call through the one precedence walk (deny > ask > allow; live, this tool, scope covers the call): a stored deny refuses (`deniedBy: "persisted_rule"`), an ask asks (`askClass: "ask_rule"`), an allow clears the activation card for the skills it names and no other. The activation card (an `unstated` ask) carries ONE offer — `ruleOffers: [{ kind: "single", rule: "Skill(<name>)", match: "name", command: "<name>" }]`, the reference's "don't ask again for <skill> in <cwd>" row — when a rule lane is armed and a stored allow could clear it (`ruleOffersAbsence` `"shadowed"` when an ask row names the skill, `"lane_cannot_speak"` when no rule spells exactly that name); `prepareCardApproval({ toolName: "Skill", command: <the name> })` mints its record and `confirmRuleApproval` / `redeemRuleTicket` write the row (a skill card has no edit row). The retired parameter spelling is refused loudly by both compilers (policy: `invalid.command_rule`; persisted: `invalid.grammar`), the sentence naming `Skill(<name>)` / `Skill(<name>:*)`. Stricter than the reference on three points, all toward asking: `Skill(:*)` (its empty prefix reaches every skill) and a persisted whole-tool `Skill` rule have no spelling; a spelling whose canonical form would not read back as the same rule is refused (a name beginning with a slash, a prefix that is itself a parameter word — `Skill(skill *)`); the card does not mint the reference's second row (the first word of a name containing a space as a prefix).
24
+ - **Who is hurt.** See the hard-breaking line. A rule file that listed `Skill(<name>)` in a store import (CC settings) used to be reported `unsupported.tool` and is now imported; an engine of this version reading a store written by 7.42.x finds no such row (nothing was ever stored); the other way round, a 7.42.x replica refuses a stored `Skill(…)` row it syncs in (KL-1318-d: a stored deny does not fire there until it is upgraded). A stored `Skill(<prefix>:*)` allow reaches every skill whose name begins with the prefix, `deploy-prod` and `deployment` included — the reference's reading.
25
+ - **Compensation.** Respell as above. To keep a deny that must fire whatever the spelling, stay on the caller policy's bare `Skill` (the whole tool) or list every name.
26
+ - **Consumers.** server — `permissions.ask` / `permissions.deny` entries of the Skill tool go to `createPermissionRulePolicy` in the CC form (the `config.invalid_permission_rules` listing of a `Skill(s1)` entry ends); cli — render the card's offer from `ruleOffers` (a `single` with `match: "name"` is a skill name, not a command; the shell's "commands" wording does not apply) and redeem it through the consent door; never write the whole tool `Skill` as the remembered rule; client-core — `PersistedRuleMatch` +2 words in the closed-set mirror; test — the Skill card's offer and the deny / ask / allow rows are wire-observable (`ruleOffers`, `askClass`, `tool_end.gate`).
27
+ - **Known limits.** KL-1318-a (the allow layer's entry lists — `TaskSpec.allowedTools`, `RunnerDeps.allowedTools`, a skill's `allowed-tools` — still read a `Skill(<name>)` entry as one that grants nothing; the store and the policy read it) · b (the card's second reference row is not minted) · c (`Skill(:*)`, a prefix that is a parameter word and a name beginning with a slash have no spelling) · d (a 7.42.x replica refuses a stored Skill row).
28
+ - Pins: `test/backlog1318-skill-rule-cc-form.test.ts` — ① the persisted grammar and a stored row read by the lane (allow clears only that skill, ask asks, deny refuses), ② the caller policy, ③ one rule × name table through both compilers against the reference's matcher (prefix without a boundary, ` *`, slash, colon names), ④ the retired spelling refused by each compiler, ⑤ the card's offer, the consent round trip (the next activation asks nothing) and no edit row, ⑥ deny through the real Runner lane (policy and stored), H1–H4 (no shell leak, org unchanged, allow-entry boundary, round-trip law, the table row equal to the real tool's schema).
29
+
30
+ ### "Is there a person on this run who can answer an approval?" has one reading, and a run can say "nobody" (#1354) (@server @cli @client-core @sdk @test)
31
+
32
+ - **Before (v7.42.x).** One prepare read the approval seat four times, two ways: the manifest's form and the posture door judged presence (`spec.onAsk !== undefined`, a `null` refused), the approver lane and the inherited gate took `spec.onAsk ?? deps.onAsk` (a `null` fell through to the deployment) — a stateful accessor on `spec.onAsk` was read 4 times in one run (measured on 7.42.0). Nothing could say "nobody on this run can answer": a `-p` lane whose deployment wires its coordinator as `RunnerDeps.onAsk` read as `human_reachable`, its cards were shown to nobody and refused by the shell in the deployment's name, and the denial limit's stop said "no approver is wired to fall back to" while one was wired. A retained child spawned under posture `"interactive"` and woken by a hard-headless sender (`interactiveTools: false`) was refused at its own door (`config.interaction_posture`, measured on 7.42.0). In `combinePolicies`, copying the denial-limit fallback onto a winning bare real-approval ask dropped that ask's `policy_rra`, so the folded class set depended on the composition order (measured: `[denial_limit_fallback]` vs `[denial_limit_fallback, policy_rra]`).
33
+ - **Now.** `resolveAskSeat(spec, deps)` is the one read — at prepare's config doors, before anything is acquired, frozen and handed down to every phase; the seat is the spec's when its key is PRESENT (a spec `null` is refused, `config.interaction_wiring`, never a fall-through). `TaskSpec.approvalReach: "none"` is one sentence in it: a callback (or no seat) becomes `ask.form: "declared_none"` with no value — so the run never calls the deployment's (or its own) approver — and a blanket `"allow"` / `"deny"` stays a policy. With a park facility the ask parks for a person (`park_only`, the run ends `paused`); without one it is refused as nobody's (`auto_deny`, the CC headless sentence, a way out naming the declaration); under `dontAsk` the mode refuses first and nothing parks.
34
+ - **Inherited by every engine-built child** (a tighten-only clamp seat — `CHILD_CLAMP_SEATS` +1): the delegation lanes, the observer sidecar, the verifier / fix and helper legs, a workflow agent (the auto-mount carries it), a retained child woken by a declared-none sender. A deployment-pinned workflow `base.onAsk` beside it is set aside the same way (not refused). A declared-none leg asks no ancestor's live approver either: a function on the frozen chain it sees is emptied (that ask resolves as nobody's — a durable mandate still parks it); a blanket on the chain stays.
35
+ - **Not checkpointed:** a resumed leg holds it only when its taskConfig supplies it again.
36
+ - **Refused when unreadable** — `"None"`, `""`, `true`, an object, any other word — at prepare, by `describeStaticWiring`, and by a resume's pre-claim screen (`checkpoint.config_refused`, `detail.code: "config.approval_reach_invalid"`, `detail.field: "taskConfig"`, the row untouched).
37
+ - **`interactionPosture: "interactive"` beside it is refused** (`config.interaction_posture`, naming the declaration). An inherited interactive posture (a workflow baseline's, a retained child's spawn) yields to an inherited clamp (`interactiveTools: false` or `approvalReach: "none"`) — it becomes `"headless"`, stated, so a deployment default of `"interactive"` does not come back through the leg's own resolution — instead of refusing the leg (`postureUnderClamps`, one rule for both lanes).
38
+ - **Seat-neutral sentences:** the headless refusal reads "(nobody can be asked to approve "<tool>" in this run — headless auto-deny)"; the `delegation.ask_unresolvable` clause "nobody in this run can be asked to answer it"; the durable-gate-unavailable notice "nobody in this run can be asked to approve them"; the denial-limit stop "nobody in this run can be asked to answer it". The seat's own words stay in the way out: wire an approver (no seat), make a person reachable through it (`unavailable`), or — a declared-none run — run it where a person can be asked, or arm a durable park.
39
+ - `combinePolicies`: when an explaining member (the denial-limit fallback, the no-ruling member below) is copied onto the winning ask, the winner's own classes join the folded union — one class set in every order.
40
+ - **Who is hurt.** A `-p` shell that declares `"none"` (cli 1.0.143 onward) and runs on a deployment with a park facility (the cli's local engine by default): the first gated call parks, the process exits 1 with its pending-approval line, and the call is answered in the REPL or the approvals API — where 7.42.x refused it and the model went on. A host that pinned the old headless refusal bracket "(no approver is wired for …)", the old `delegation.ask_unresolvable` clause or the old stop sentence in logs or probes. A deployment whose request sets `interactionPosture: "interactive"` (spec, deps or a parent's posture) and `approvalReach: "none"` together: refused at the door. A host that read a spec `null` seat as "use the deployment's": refused (it already was, later in prepare, for the manifest).
41
+ - **Compensation.** "Refuse and go on" whatever the deployment's park facility says: `permissionMode: "dontAsk"`. A run that should be asked: omit `approvalReach`. The seat's form is on the manifest (`ask.form`); `resolveAskSeamForm(spec, deps)` takes the request's `approvalReach` beside the seat.
42
+ - **Consumers.** server (S-1106) — pass the request body's `approvalReach` into `TaskSpec.approvalReach` untranslated; hand it to `resolveAskSeamForm` in the per-leg interactive judgment (else the leg is refused at core's door); re-supply it on an unattended resume; project `ask.form: "declared_none"`; a `checkpoint.config_refused` can carry `detail.code: "config.approval_reach_invalid"`. cli (L-1058) — declare `"none"` on the `-p` lane; a declared run never receives an approval frame (the shell's "print mode has no approval channel" refusal retires for it); a parked first gated call exits 1. client-core (CC-365) — assemble the request field; mirror `APPROVAL_REACH` and the `AskSeamForm` word. sdk — the request member. test — UX-7 print-missing splits into `nopark` / `park` (K-12).
43
+ - **Known limits.** KL-1354-a (a declared-none run's unanswerable content question keeps the generic way out — the question channel is `onQuestion`'s, not the declaration's).
44
+ - Pins: `test/backlog1354-approval-reach.test.ts` (①–⑨, ⑰, ⑱, ⑳–㉒, ③c; real Runner); re-pinned the seat-neutral sentences (1019 P5, 1137, 548 inherited arms, 709, 7340rg, core-toolgate-coverage, irreversibility), the clamp carrier (777), the phase fixtures (prepare-wiring-manifest / -inherited-gate / -caps-and-workflow).
45
+
46
+ ### A classification that did not rule has one arm, at both classifier stations; the card says why (#1353; behavior) (@server @cli @client-core @test)
47
+
48
+ - **Before (v7.42.x).** The leg's gate and the inherited station each had two arms: an `unavailable` classification forked on the approval seat (with one: a card whose class was `policy_rra`, the "why" only in the second message paragraph; without one: the `x1t` refusal, uncounted), while a reply with no verdict was refused and counted like a block wherever a person was — so a TUI user whose classifier answered in prose saw two refusals and a third "denial limit" card. The `classifier.unavailable` notice said nothing about a reply with no verdict.
49
+ - **Now.** One predicate (`rungDidNotRule`) and one exit (`classifierNoRulingArm`) at both stations. With an approval seat (a live approver, or a durable park) the call goes to the person on a card carrying `classifierNoRuling` (`error` / `timeout` / `parse_error`), `requiresRealApproval`, class `classifier_no_ruling` (`classes[0]`) and a second paragraph saying why ("… replied without a verdict, so auto mode could not judge this call …" for `parse_error`; the unavailable paragraph byte for byte); the park row and its summary carry the member. Never counted — **except** a card THIS station handed on that the seat's deployment POLICY refused (settlement `policy_refused`), which counts like a ruling refusal (K-14; a policy's own `classifierNoRuling`, or a card another station minted, counts nothing): at the bound the same call goes to the same seat again as the denial-limit fallback, and a deny nobody judged there stops the run through the existing headless arm. Without a seat: `error` / `timeout` refused with `x1t`, uncounted; `parse_error` refused with its 7.42.0 sentence and counted (the bound falls back to a person; with nobody there the run stops). A reply that ruled or contradicted itself is unchanged. Every such classification raises `classifier.unavailable` (`detail.cause` gains `parse_error`; "the classifier's reply carried no verdict").
50
+ - Class `classifier_no_ruling` carries the denial-limit fallback's arbitration row (bypass-immune, never put to the classifier again, no sandbox admission, no allow rule clears it) and explains the real-approval bit, so the card no longer reads `policy_rra`.
51
+ - **Who is hurt.** Under the same class the inherited two-frames collapse no longer merges the ancestor's "classifier did not rule" card with the child's own real-approval question on the same call: one more card. A shell on core 7.43.0 that has not yet declared `approvalReach` (cli ≤1.0.142 on `-p`) and refuses every card by policy: each reply with no verdict now shows a card (refused), and the third such call shows a SECOND refused card (the denial-limit fallback) before the run stops — 7.42.x refused the first two silently and stopped on the third's fallback. A host that answers or deduplicates cards by class: a new class word; a host whose card identity hashes the class set sees a new identity for this card (inferred). A probe that pinned "a reply with no verdict is refused and counted" with a live approver.
52
+ - **Compensation.** Read `classifierNoRuling` (or `classes[0] === "classifier_no_ruling"`) to render the cause; the second paragraph stays for clients that do not. A deployment that wants the CC route (refuse, never ask) for this case runs that leg with no approval seat.
53
+ - **Consumers.** server — the member on `tool_approval` frames, the park row and the summary; the class word in its frame projection and key-set guard; `classifier.unavailable` `detail.cause` +`parse_error` (mirror). cli (L-1059) — render the cause from the member / class; the shell writes no sentence of its own. client-core — `AskClass` +1, `AskRequest.classifierNoRuling`, the notice cause +1. test — UX-7 tui-missing flips on the engine side (the card face waits on L-1059); `c1b-auto-cls-unparsable` re-cores.
54
+ - **Known limits.** A durable decide made by the deployment's policy on a PARKED no-ruling card is settled on the resumed leg and counts nothing (K-14 counts at the synchronous seat). KL-1353-a (the park wiring self-check's stub does not yet carry the new twin).
55
+ - Pins: `test/backlog1353-classifier-no-ruling-arm.test.ts` (⑩–⑯, ⑲, K-14; real Runner + the inherited station); re-pinned to the one arm — a cell whose subject is the seat's route moves to the person (661 four-call cell, design276 E4, 1214 F1, 1111's explainer list), a cell whose subject is the classifier's verdict runs with no seat and keeps its assertion (618, 661 ⑤ cells, 742, 805); the arm's rename (1125); the inherited arms' source scan (permission-rule-wiring).
56
+
57
+ ### A tool name that can never match is refused by one judgment — a peer segment no mint spells, and every allow list's entries (#1355 (b), #1358; behavior) (@server @cli @client-core @test)
58
+
59
+ - **Before (v7.42.x).** `judgeRuleToolName` knew three words (`current`, `retired`, `unprefixed_namespace`) and answered `current` for `mcp__github.com__*`: rules compare peer segments exactly and every mounted tool carries the minted spelling (`github.com` → `github_com`), so the rule reached nothing, ever. A caller policy with such a deny ran with only the advisory `config.toolpolicy.unmatched_names`; `putRules({ toolDeny: ["mcp__github.com__*"] })` was accepted. The three allow lists never put an entry's tool name to any judgment: `bash`, `github__x`, `mcp__github.com__*` and `write` (the run mounts `Write`) each read as an entry that grants nothing and nothing said so — the person who listed it believed the tool was allowed, and the card came anyway.
60
+ - **Now.** The judgment's fourth word is `unmintable_segment`: a name with a protocol prefix has its peer segment re-minted (`mintNamespacePrefix`, the mint's own arithmetic); a segment the mint spells differently is unmintable, and `currentName` is the whole name rewritten with the minted segment (`mcp__github.com__*` → `mcp__github_com__*`; the tool segment is carried verbatim, never judged). An empty peer (`mcp____x`) is unmintable with no `currentName`. Its readers render it beside the other two stale words — the prepare audit (every caller policy, a session's stored rows and an ancestor's rules feed it) fails the run with `config.legacy_tool_name`; the session store's `putRules` refuses with `legacy_tool_name` and `detail.verdict: "unmintable_segment"`; removing such a stored `toolDeny` entry is not a loosening. One decomposition of a namespaced name (`splitNamespacedName`) is read by the judgment, the covering matcher and the allow-entry grammar.
61
+ - **The allow lists** read an entry's tool name (a bare name, or the `Tool(` part of a content entry) against the roster of the run: a stale name reads as `tool_name` (`detail.entries[].verdict` + `currentName` when there is one), a name the run mounts only under another case as `near_miss` (`currentName` = the mounted spelling); both read as nothing and are announced. Only the near miss is reported, never the merely unmounted name (C-R245): a name the run simply does not mount (`Wrte`, a real tool of another scenario, a server this run has no connection to) is SILENT — a list shared across runs is not wrong because one run lacks a tool. A retired name the deployment mounts as its own tool (`Task`) stays an ordinary entry (the roster clears it). `Skill(<name>)` is not judged: a skill's name is not a tool name. The Skill card names only the entries that read as rows.
62
+ - `compileAllowEntry(raw, behavior, roster?)` / `compileAllowList(entries, behavior, roster?)`: absent, the grammar alone is read (a resume's pre-claim screen, where no roster exists yet; a carried record is read against the leg's roster when it is seeded — #1372). The deployment's grammar readings are still announced once at construction; its name readings (a fact about a run's roster) once per run.
63
+ - **Who is hurt.** A deployment that wrote a deny / ask (caller policy, a settings file the serving layer compiles, a stored session row) naming a peer segment the mint never produces — a server called `github.com`, `my server`, a long name written out in full: today it runs with a mute rule (and an advisory); from this version the run refuses to start with the sentence naming the whole rule name to write, and the store refuses the write (an older release's stored row can be removed by its owner). A consumer that switches exhaustively over `RuleToolNameVerdict["kind"]`, `SessionRuleNameRefusal.verdict` or `AllowEntryInertReason`. A user whose allow entry is a case twin or a retired name: one more notice at the start of a run (audience `user`); the entry granted nothing before and still grants nothing, and the card still comes. A deployment whose `RunnerDeps.allowedTools` holds a stale name: one notice per run for it (not only one at construction).
64
+ - **Compensation.** Respell the rule with the `currentName` in the sentence (or build the spelling with `mintNamespacePrefix`); spell an allow entry as the tool is mounted. A retired name the deployment mounts as its own tool needs nothing. To keep a list shared across runs quiet, nothing is needed: an unmounted name is silent.
65
+ - **Consumers.** server (S-1113) — `unmintable_segment` replaces any comparison of a rule's segment with `mintNamespacePrefix`'s answer (exported in 7.41.1); mirror the word in the `legacy_tool_name` detail projection; a config-time `compileAllowList` passes the run's mounted names when it knows them (else the grammar alone is read); the notice `entries[]` gain `verdict?` / `currentName?`. client-core — `RuleToolNameVerdict` +1, `AllowEntryInertReason` +2. cli — render a `tool_name` / `near_miss` entry from `detail.entries[]` (`verdict`, `currentName`); the shell writes no sentence of its own. test — a case twin in an allow list still asks, with a notice.
66
+ - **Known limits.** A namespaced name's TOOL segment is never judged (an unminted literal tool segment stays the audit's advisory); a covering allow entry whose peer differs only by case (`mcp__GitHub__*`) is silent — a covering name has no mounted spelling to compare with; `near_miss` compares with the tools mounted when the list is read (the `Skill` tool, the memory pair and `ToolSearch` mount after it, and a server that connects later is not seen — a case twin of one of those is silent; a name of an engine tool is never a near miss, so a deployment's own `SKILL` beside the engine's `Skill` costs no exact entry; and a protocol tool's name — `mcp__…`, `a2a__…` — is never a near miss either: a refresh mounts and unmounts those while the run goes on, so a case twin (`mcp__srv__delete` beside a mounted `mcp__srv__Delete`) stays an ordinary row and is silent); an exact name the run does not mount stays silent by design.
67
+ - Pins: `test/backlog1355b-unmintable-segment-judge.test.ts` (①–⑪: the three spellings, the law over a peer table, empty peer, clamped peer, the store's write, `loosenReasons`, the prepare audit through a real Runner — both policy factories, a stored row, a second protocol — and the minted spelling / exact unknown name / Skill name as positive controls); `test/backlog1358-allow-entry-tool-names.test.ts` (①–⑫: one reading per entry shape with and without a roster, the list detail, the deny direction, the closed set, the three sources through a real Runner, the roster reaching every source, the Skill card, the deployment's once-per-run, near miss, `Skill(<name>)`, boundary controls, an activated skill's rows); re-pinned the verdict set (backlog1156).
68
+
69
+ ### A Skill card says what approving it releases — one value on the live ask, an ancestor's ask, the parked row and the inbox projection (#1348 / #1357; behavior) (@server @cli @client-core @test)
70
+
71
+ - **Before (v7.42.x).** The card of a call to the engine's `Skill` tool told the person what approving it releases only on the live ask, and only as a sentence (`Execute skill: <name>` / "Approving also lets these tools be used without asking …" in `AskRequest.message`). The parked row carried a hash of the skill and its two lists (`boundGrantDigest`) and nothing that says what the hash covers, and `CheckpointSummary` projected neither: the approval inbox showed `Skill({"skill":"sa"})` and an approver could not see what the click releases. The live `AskRequest` carried no binding at all, so a server could not reconcile a live Skill card against its parked twin, and its "allow this session" for a Skill card could only be refused (fail-closed — stricter than the reference, which remembers per skill).
72
+ - **Now.** One factory (`skillGrantBindingMembers`) mints the card's binding for a call to the engine's `Skill` tool: `boundGrants: { skill, allowedTools, disallowedTools }` (the skill as the table spells it, `null` when the call names no loaded skill; the two lists in the rule grammar's canonical spelling, de-duplicated, sorted) and `boundGrantDigest`, the hash of exactly that value. Every ask-mint site spreads it (`grantBindingOf`, five sites): the live `AskRequest` of the leg's own gate, the three inherited stations' (an ancestor's frozen approver), and the parked row; the inbox projection echoes the row's two members (the digest as a string; the lists read whole or not at all, as a fresh copy, and only when they hash to that digest — a stored pair that disagrees, or lists beside no digest, publish the digest alone). The binding is a property of the CALL — whoever asked and however the question was worded (a policy's or a hook's `message`, the engine's unstated sentence) — and every other card carries neither member. The digest's bytes are unchanged (the same skill and lists hash to the same value as in 7.42.x), the resume still judges the digest alone and never reads `boundGrants`, and `AskRequest.message` keeps its sentence. `skillCallGrantDigest` (internal) is retired; its one reader asks the factory.
73
+ - **Who is hurt.** Nobody who read the old members: the additions are optional and every existing member is byte-identical. A consumer that keys an answer on the TOOL NAME alone for a Skill card (a session-scoped "allow this skill" keyed on `Skill`) can now key on "tool + `boundGrantDigest`" — a skill whose lists change has a new digest and is asked again; a consumer that does not read the new members behaves exactly as before. A client that renders `boundGrants` as "lets these be used without asking" overstates: the sentence in `message` names only entries that release something in the leg's mode.
74
+ - **Compensation.** None needed. A reader that wants the sentence's exact reading runs each entry through `compileAllowEntry(entry, "allow")` (an inert entry answers a reason word, and in `auto` mode an entry the mode strips releases nothing); otherwise render the list as what the skill declares.
75
+ - **Consumers.** server (S-1115) — pass the new members through the `tool_approval` frame and the `GET /v1/approvals` projection from `CheckpointSummary.boundGrantDigest` / `boundGrants`, and key the "allow this session" answer for a Skill card on "tool + `boundGrantDigest`" (no digest ⇒ not a Skill activation); cli — render the inbox card's "approving releases" list from `boundGrants` (the declared lists the live card's sentence is a mode-narrowed rendering of; the deployment's text, with contextual escaping, capped as the live sentence caps it); client-core — mirror `BoundSkillGrants` and the five optional members on `AskRequest` / `PendingAction.tool_approval` / `CheckpointSummary`; test — reconcile a live Skill card against its parked twin by `boundGrantDigest`.
76
+ - **Known limits.** None. (The member is the skill's declared lists whole — a skill that declares very long lists grows its row and every summary by that much; capping it would stop the member being the value its digest hashes.)
77
+ - Pins: `test/backlog1348-card-grant-disclosure.test.ts` ①–⑫ (real Runner; the in-memory and the file stores; the inherited lane at each of its three stations, presence and absence; the projection's digest check; the factory's unit face, the digest's bytes frozen at the 7.42.x value) — baseline 24 red / 5 green controls (⑤, ⑤b, ⑧a, ⑧b, ⑨d); mutation table `M1`–`M11` (the inherited lane per station), every mutation turns at least one fixture red.
78
+
79
+ ### A skill's `disallowed-tools` entry that reads as nothing refuses what it guards — the activation, or the leg that carries it (#1372; behavior narrowing) (@server @cli @client-core @test)
80
+
81
+ - **Before (v7.42.x).** An entry the list grammar reads as nothing was one grant less on `allowed-tools` (the safe direction) and one refusal less on `disallowed-tools`, with the same treatment: a notice (audience `user`) and the skill went on. A skill with `disallowed-tools: ["Bash "]` (a surrounding space; 7.41.x read it as `Bash`), `["bash"]` (a retired name, 7.43.0's #1358) or `["probe"]` (the run mounts `Probe`) activated on the model's `Skill` call, on an approved parked activation and on `userInvokedSkills`, and the tool it named ran. A resumed leg re-supplied with such a skill ran the tool too (the server's stored body of a task parked on 7.41.x). A park's frozen deny record that the grammar alone could read but the leg's roster could not (`bash`, `probe` — a row written before the names were judged) was seeded as a row that matched nothing; a parent's captured record that read as nothing was skipped. A frozen record the grammar could not read (`"Bash "`) was already refused before the claim (`checkpoint.invalid_outcome`, `reason: "skill_grants_damaged"`) with a "corruption guard" sentence. A resume whose `userInvokedSkills` named a skill the config did not carry was refused by prepare AFTER the claim (`config.user_invoked_skill_unknown`; the row reopened).
82
+ - **Now.** One reading decides it — `compileAllowList(entries, "deny", roster)`'s inert half, the notice's own (`unreadDenyOf`) — at the run's skill deny read and at the table's two writers. A call to the engine's `Skill` tool naming such a skill is refused where the run's skill deny rows are read (the lane's `denying`: every gate decision, the approved-edit round, the resumed execution boundary): `tool_end.isError: true`, `gate.disposition: { kind: "denied", deniedBy: "policy" }`, `decisionReason: "skill"`, the sentence "Skill "<name>" was not activated: its disallowed-tools entry "Bash " (grammar) reads as nothing — … nothing it lists is granted"; no card is asked and nothing parks, with an approver, with none, under `bypassPermissions`, with a durable approval park; a person who edits a card onto such a skill is refused on the edited bytes; a Skill card parked before the upgrade (its deny entry read then) is refused at the execution boundary ("The approved tool call "Skill" was not executed: …"; the approval is spent). `SkillGrantTable.activate` keeps the same refusal for every activation that reaches the table and writes no row: a person's invocation refuses the run before any call (`config.skill_deny_unreadable` on `terminal.code`). `SkillGrantTable.seed` reads every carried record against the leg's roster: a deny record that reads as nothing refuses the leg with the same code (the sentence says a parked task an earlier release wrote can be finished on that release's worker); an allow record that reads as nothing is not held (it matched no mounted tool — `activeSkillGrants()` is one row shorter, no call's answer moves). A resume's pre-claim screen seeds the re-supplied invoked skills with prepare's own function and no roster, so what no roster changes (a name the config does not carry; a deny entry the grammar alone reads as nothing — `grammar`, `wildcard_all`, `namespace_wildcard`, `path_pattern`, `domain`, `content_unsupported`) is refused before the claim (`checkpoint.config_refused`, `detail.code` = prepare's code, `detail.field: "taskConfig"`, the row untouched; the corrected config resumes the same token); a name only the leg's roster reads as nothing (a retired name — a deployment may mount its own tool under it — or a case twin) is refused by prepare after the claim and the row reopens for a retry of the recorded decision. The frozen-section refusal now names the earlier-release case. The notice and the refusal render an entry one way (quoted as written, so a surrounding space shows); an allow-only notice is byte-identical, a notice naming a `disallowed-tools` entry gains "Such an entry in a disallowed-tools list keeps its Skill from activating".
83
+ - **Who is hurt.** A task parked on an earlier release whose frozen grant rows carry a deny entry this release reads as nothing — a 7.42.x row naming a retired tool (`bash`) or a case twin of a mounted tool, or (refused since 7.42.0) a 7.41.x row with a surrounding space — cannot be resumed here at all: every resume is refused and the row stays pending. The `config.user_invoked_skill_unknown` door, refused after the claim since 7.41.0, is now refused before it and the row is not touched. A skill whose `disallowed-tools` lists a form sema cannot enforce no longer activates (it used to activate with that refusal missing): `*` (deny every tool), `mcp__*`, `WebFetch(domain:…)`, `Skill(<name>)`, a content form of any tool outside the shell and path grammars (`WebSearch(…)`, `mcp__srv__x(…)`), a path pattern the path grammar refuses, an entry with a surrounding space, a retired tool name, a case twin of a mounted tool. A delegated child whose roster reads one of the parent's captured deny records as nothing (it mounts a case twin, or not the deployment's own tool a retired name named) does not start. A run that invokes such a skill by name ends `failed` before any call.
84
+ - **Compensation.** Finish a parked task on a worker of the release that parked it, or expire it and submit the task again with the entry corrected. Spell each `disallowed-tools` entry as the tool is mounted; deny a whole tool by name (`WebFetch`, `mcp__<server>`) where the content form has no sema reading; a deny that must cover every tool belongs in a caller policy or a stored rule, not a skill's list.
85
+ - **Consumers.** server — the model's `Skill` call for such a skill arrives as `tool_end.gate.disposition` `{ kind: "denied", deniedBy: "policy" }` with no card and no park (it never reaches the approvals inbox); the request door already refuses an unreadable deny entry with 400 (7.117.0) and its resume replay judges the stored body the same way; a `checkpoint.config_refused` can now carry `detail.code` `config.user_invoked_skill_unknown` / `config.skill_deny_unreadable` (correct `taskConfig` and resume the same token); a `failed` resumed leg with `config.skill_deny_unreadable` whose sentence names "grant rows carried" cannot be resumed on this release (drain on the release that parked it, or expire). cli — a refused `Skill` call is an ordinary refusal (`denied` / `policy`, the sentence names the entry and the reason): no card was asked, nothing special to render; a `-p` run invoking such a skill fails before any call. client-core — `CheckpointConfigRefusalCode` mirror +2; the new terminal code. test — the three entries (model route, user route, resume) × the three readings (grammar, retired name, case twin), the cross-release frozen row, and the allow side unchanged, all wire-observable (`tool_end.gate` / `isError` / `output`, no `AskRequest`, `terminal.code`, `CheckpointError.detail`).
86
+ - **Known limits.** KL-1372-a (the deny forms sema cannot yet enforce on a skill's list — the skill does not activate) · KL-1372-b (a name-level refusal of a carried record happens after the claim: the leg's roster does not exist before it).
87
+ - Pins: `test/backlog1372-skill-deny-unreadable.test.ts` ①–⑩ (real Runner; the in-memory checkpoint store; a store view rewritten to the row an earlier release wrote; ⑩ the gate refusal × approver / none / bypass / durable park / approved edit, with controls) — baseline 37 red / 10 green controls on the release base (`d30b88d4f`; 12 red / 35 green on the first fix `0b26be89e`: the ⑩ gate cells and ④); mutation table `.gate-logs/criteria-7430-mutations-1372.md` M1–M9 (model route at the table / user route / carried record / allow side refused / name layer uncounted / no pre-claim screen / the table never refuses / the deny read never refuses / both — the two layers each red on their own fixtures); re-pinned 1303 ⑬a′ (a deny list holding `*` refuses the activation) and 1358 ⑫ (a stale deny name refuses it); the engine's `Skill` tool is built over the run's grant table only (no table-less construction — six test files pass one).
88
+
89
+ ### The allow tables dedupe through a set — seeding is linear in the list (#1370; performance, no behavior change) (@server)
90
+
91
+ - **Before (v7.42.x).** `AllowEntryTable.seed` checked every incoming entry against every held one (`held.some(...)`), and the skill grant table's `hold` did the same per row: 1 000 entries 153 ms, 10 000 2.1 s, 20 000 6.4 s, synchronously on the event loop (server's reading on the installed 7.42.0).
92
+ - **Now.** Both tables keep a key set beside their rows (the entry as written; the skill row's skill / origin / behavior / entry). The rows held, their order, the entries returned as inert and every notice are unchanged. The engine still sets no bound on the length of `TaskSpec.allowedTools` — a bound is the host's to set.
93
+ - Pins: `test/backlog1370-allow-table-seed-linear.test.ts` (dedupe and order unchanged across re-seeding; neither method's source scans the held rows per entry).
94
+
95
+ ### `abortedSeatDeny` is exported from the package root (#1371; surface) (@server)
96
+
97
+ - **Before.** The `task_aborted`-attested deny that every engine policy factory answers under an already-aborted signal lived in `src/core/tool-policy.ts` only: a deployment's own policy closure that wanted to follow the cancellation contract could not reach it, and a deny it built by hand was read by the synthetic-Read judge as a refusal (the other half of KL-1311a-a).
98
+ - **Now.** `import { abortedSeatDeny } from "@sema-agent/core"` — `abortedSeatDeny(req, message, origin)`; INTEGRATION §4.6j states the calling shape. Exports +1.
99
+ - Pins: `test/backlog1371-aborted-seat-deny-export.test.ts`; `test/export-surface.snapshot.json` (+1 name).
100
+
1
101
  ## 7.42.1 — 2026-10-05
2
102
 
3
103
  **Hard-breaking changes:** none. **Closed sets:** none added. **Package exports:** +1 / −0 — `steerPriorityOf`. **Behavior narrowing:** none (one reading replaces two; a replay that spelled the default is no longer refused).
@@ -1040,6 +1040,7 @@ export function createSendMessageTool(opts) {
1040
1040
  ...(ctx.writeFace === "roots" ? { writeFaceRoots: true } : {}),
1041
1041
  ...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: ctx.readDenyPatterns } : {}),
1042
1042
  ...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
1043
+ ...(ctx.approvalReach === "none" ? { approvalReach: "none" } : {}),
1043
1044
  ...(ctx.oneShot === true ? { oneShot: true } : {}),
1044
1045
  ...(ctx.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : {}),
1045
1046
  ...(ctx.parentSpecSeat?.memoryOff === true ? { memoryOff: true } : {}),
@@ -444,6 +444,7 @@ export declare function createSubagentResume(deps: {
444
444
  currentClamps?: {
445
445
  handsReadOnly?: true;
446
446
  interactiveTools?: false;
447
+ approvalReach?: "none";
447
448
  oneShot?: true;
448
449
  memoryPersistenceCapable?: false;
449
450
  memoryOff?: true;
@@ -1,7 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { isAbsolute } from "node:path";
3
3
  import { applyParentSpecSeat, memoryTurnedOff, parentSpecSeatOfCtx } from "../core/parent-spec-seat.js";
4
- import { childClampSeats, engineHeadlessLegSeats } from "../core/child-clamp-seats.js";
4
+ import { childClampSeats, engineHeadlessLegSeats, postureUnderClamps } from "../core/child-clamp-seats.js";
5
5
  import { stripSpawnTurnQuestionFace } from "../core/ask-question.js";
6
6
  import { withDelegationProvenance } from "../core/tool-policy.js";
7
7
  import { errorResult } from "../core/tools.js";
@@ -546,6 +546,7 @@ export function createSubagentResume(deps) {
546
546
  ...childClampSeats({
547
547
  handsReadOnly: entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true,
548
548
  interactiveTools: entry.specSnapshot.interactiveTools === false || deps.currentClamps?.interactiveTools === false ? false : undefined,
549
+ approvalReach: entry.specSnapshot.approvalReach === "none" || deps.currentClamps?.approvalReach === "none" ? "none" : undefined,
549
550
  readFace: entry.specSnapshot.readFace === "roots" || deps.currentClamps?.readFaceRoots === true ? "roots" : entry.specSnapshot.readFace,
550
551
  writeFace: entry.specSnapshot.writeFace === "roots" || deps.currentClamps?.writeFaceRoots === true ? "roots" : entry.specSnapshot.writeFace,
551
552
  readDenyPatterns: [...(entry.specSnapshot.readDenyPatterns ?? []), ...(deps.currentClamps?.readDenyPatterns ?? [])],
@@ -628,6 +629,7 @@ export function createSubagentResume(deps) {
628
629
  stream = childRunner.runTaskStream(resumeSpec, undefined, {
629
630
  ...entry.internalsSnapshot,
630
631
  cycleSeq: entry.cycleSeq,
632
+ parentInteractionPosture: postureUnderClamps(entry.internalsSnapshot.parentInteractionPosture, { interactiveTools: resumeSpec.interactiveTools, approvalReach: resumeSpec.approvalReach }),
631
633
  ...(true
632
634
  ? {
633
635
  onForwardEvent: (e) => {
@@ -1906,7 +1908,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1906
1908
  ...(ctx.restoreGatedTools !== undefined ? { restoreGatedTools: ctx.restoreGatedTools === true ? true : [...ctx.restoreGatedTools] } : {}),
1907
1909
  ...(ctx.promptProfile !== undefined ? { promptProfile: ctx.promptProfile } : {}),
1908
1910
  ...engineHeadlessLegSeats(),
1909
- ...childClampSeats({ handsReadOnly: ctx.handsReadOnly, interactiveTools: ctx.interactiveTools, readFace: ctx.readFace, writeFace: ctx.writeFace, readDenyPatterns: ctx.readDenyPatterns }),
1911
+ ...childClampSeats({ handsReadOnly: ctx.handsReadOnly, interactiveTools: ctx.interactiveTools, approvalReach: ctx.approvalReach, readFace: ctx.readFace, writeFace: ctx.writeFace, readDenyPatterns: ctx.readDenyPatterns }),
1910
1912
  enableBlockedReport: false,
1911
1913
  limits: {
1912
1914
  maxTurns: typeof observerDef.maxTurns === "number" && Number.isFinite(observerDef.maxTurns) && observerDef.maxTurns > 0 ? observerDef.maxTurns : 8,
@@ -2179,7 +2181,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2179
2181
  ...(def?.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: def.memoryPersistenceCapable } : {}),
2180
2182
  ...(ctx.checkpointStoreForChildren !== undefined ? { checkpointStore: ctx.checkpointStoreForChildren } : {}),
2181
2183
  ...(childOnAsk !== undefined ? { onAsk: childOnAsk } : {}),
2182
- ...childClampSeats({ handsReadOnly: ctx.handsReadOnly, interactiveTools: ctx.interactiveTools, readFace: ctx.readFace, writeFace: ctx.writeFace, readDenyPatterns: ctx.readDenyPatterns }),
2184
+ ...childClampSeats({ handsReadOnly: ctx.handsReadOnly, interactiveTools: ctx.interactiveTools, approvalReach: ctx.approvalReach, readFace: ctx.readFace, writeFace: ctx.writeFace, readDenyPatterns: ctx.readDenyPatterns }),
2183
2185
  ...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
2184
2186
  ...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
2185
2187
  ...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
@@ -257,7 +257,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
257
257
  ...(studentSpec.principal !== undefined ? { principal: studentSpec.principal } : {}),
258
258
  ...(studentSpec.clientContext !== undefined ? { clientContext: studentSpec.clientContext } : {}),
259
259
  ...(studentSpec.promptProfile !== undefined ? { promptProfile: studentSpec.promptProfile } : {}),
260
- ...childClampSeats({ handsReadOnly: studentSpec.handsReadOnly, interactiveTools: studentSpec.interactiveTools, readFace: studentSpec.readFace, writeFace: studentSpec.writeFace, readDenyPatterns: studentSpec.readDenyPatterns }),
260
+ ...childClampSeats({ handsReadOnly: studentSpec.handsReadOnly, interactiveTools: studentSpec.interactiveTools, approvalReach: studentSpec.approvalReach, readFace: studentSpec.readFace, writeFace: studentSpec.writeFace, readDenyPatterns: studentSpec.readDenyPatterns }),
261
261
  ...(studentSpec.excludeTools !== undefined
262
262
  ? { excludeTools: Array.isArray(studentSpec.excludeTools) ? [...studentSpec.excludeTools] : studentSpec.excludeTools }
263
263
  : {}),
@@ -155,6 +155,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
155
155
  ...childClampSeats({
156
156
  handsReadOnly: specBase.handsReadOnly,
157
157
  interactiveTools: specBase.interactiveTools,
158
+ approvalReach: specBase.approvalReach,
158
159
  readFace: foldedRootsFace ? "roots" : undefined,
159
160
  writeFace: foldedRootsWriteFace ? "roots" : undefined,
160
161
  readDenyPatterns: foldedReadDeny,
@@ -261,6 +262,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
261
262
  ...childClampSeats({
262
263
  handsReadOnly: specBase.handsReadOnly,
263
264
  interactiveTools: specBase.interactiveTools,
265
+ approvalReach: specBase.approvalReach,
264
266
  readFace: foldedRootsFace ? "roots" : specBase.readFace,
265
267
  writeFace: foldedRootsWriteFace ? "roots" : specBase.writeFace,
266
268
  readDenyPatterns: foldedReadDeny,
@@ -1,11 +1,24 @@
1
+ import { type RuleToolNameVerdict } from "./tool-name-aliases.js";
1
2
  import { type ToolCallRequest } from "./tool-policy.js";
2
3
  import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
3
4
  import { type PersistedRule } from "./permission-rule-model.js";
4
5
  import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
5
6
  import { type EngineNotice } from "./engine-notice.js";
6
- /** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. */
7
- export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"];
7
+ /** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. `tool_name`: the
8
+ * entry's tool name is one the name judgment reads as stale against the run's roster (`verdict` says which); `near_miss`: the
9
+ * run mounts no tool of that exact name but mounts one that differs only by case (`currentName` is its spelling). */
10
+ export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"];
8
11
  export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
12
+ /** The facts beside a `tool_name` / `near_miss` reason: the judgment's word, and the spelling to write instead when there is one. */
13
+ export interface AllowEntryInertDetail {
14
+ readonly verdict?: Exclude<RuleToolNameVerdict["kind"], "current">;
15
+ readonly currentName?: string;
16
+ }
17
+ /** One entry a list reads as nothing. */
18
+ export type AllowEntryInert = {
19
+ readonly entry: string;
20
+ readonly reason: AllowEntryInertReason;
21
+ } & AllowEntryInertDetail;
9
22
  /** One entry the grammar reads (kind closed): a whole non-shell tool; a protocol namespace; the whole shell tool; a shell
10
23
  * command rule; the Read directory form; a deny-side path pattern (a skill's `disallowed-tools`). */
11
24
  export type CompiledAllowEntry = {
@@ -27,22 +40,25 @@ export type CompiledAllowEntry = {
27
40
  readonly kind: "path";
28
41
  readonly rule: PersistedRule;
29
42
  };
30
- /** Read one entry under one direction. */
31
- export declare function compileAllowEntry(raw: string, behavior: "allow" | "deny"): CompiledAllowEntry | AllowEntryInertReason;
32
43
  /**
33
- * Compile one list WHOLE: the entries it reads and the ones it reads as nothing. An allow list holding `*` voids the list
34
- * (every other entry on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list reads
35
- * as nothing by itself and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside it.
44
+ * Read one entry under one direction. `roster` = the tool names the run mounts (aliases included): given, the entry's tool name
45
+ * is also read against it (a stale name, or one that differs from a mounted tool's only by case, reads as `tool_name` /
46
+ * `near_miss`); absent, the grammar alone is read — what a resume's pre-claim screen can read before the leg has a roster.
47
+ */
48
+ export declare function compileAllowEntry(raw: string, behavior: "allow" | "deny", roster?: ReadonlySet<string>): CompiledAllowEntry | AllowEntryInertReason;
49
+ /**
50
+ * Compile one list WHOLE: the entries it reads and the ones it reads as nothing (each with its reason and, for a name the
51
+ * roster refuses, the judgment's word and the spelling to write). An allow list holding `*` voids the list (every other entry
52
+ * on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list reads as nothing by itself
53
+ * and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside it. `roster` as in
54
+ * {@link compileAllowEntry}.
36
55
  */
37
- export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny"): {
56
+ export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny", roster?: ReadonlySet<string>): {
38
57
  rows: Array<{
39
58
  entry: string;
40
59
  compiled: CompiledAllowEntry;
41
60
  }>;
42
- inert: Array<{
43
- entry: string;
44
- reason: AllowEntryInertReason;
45
- }>;
61
+ inert: AllowEntryInert[];
46
62
  };
47
63
  /** The programs whose open allow rule the reference implementation drops in auto mode — its interpreter / launcher table
48
64
  * plus the shells and the run-anything builtins (2.1.280). A program that only reaches the network is not here. */
@@ -117,11 +133,10 @@ export interface AllowEntryGateSeats {
117
133
  */
118
134
  export declare class AllowEntryTable {
119
135
  private held;
120
- /** Write a list's rows (a row already held is not held twice). Returns the entries that read as nothing. */
121
- seed(entries: readonly string[]): ReadonlyArray<{
122
- entry: string;
123
- reason: AllowEntryInertReason;
124
- }>;
136
+ private readonly heldEntries;
137
+ /** Write a list's rows (a row already held is not held twice). Returns the entries that read as nothing. `roster` as in
138
+ * {@link compileAllowEntry}: a list the leg itself wrote is read against it, a carried one (a delegating parent's capture) is not. */
139
+ seed(entries: readonly string[], roster?: ReadonlySet<string>): readonly AllowEntryInert[];
125
140
  /** The entries held, as written (a copy) — what a delegated child is handed. */
126
141
  entries(): readonly string[];
127
142
  /** The lane over this table, bound to the leg's call facts. */
@@ -129,8 +144,10 @@ export declare class AllowEntryTable {
129
144
  }
130
145
  /**
131
146
  * Seed one leg's request and deployment tables — a delegated child's captured request entries first (an entry that reads as
132
- * nothing was announced where it was written), then the leg's own list; the deployment's (announced once, at construction) —
133
- * and return the dead entries of the leg's OWN lists, the skills' beside the request's, for its one start-of-run notice.
147
+ * nothing was announced where it was written, and is read without the roster for the same reason), then the leg's own list
148
+ * against `roster`; the deployment's grammar reading is announced once, at construction, so only its name readings
149
+ * (`tool_name`, `near_miss` — facts about THIS leg's roster) are announced here — and return the dead entries of the leg's
150
+ * OWN lists, the skills' beside the request's, for its one start-of-run notice.
134
151
  */
135
152
  export declare function seedAllowEntryTables(tables: {
136
153
  readonly request: AllowEntryTable;
@@ -139,13 +156,11 @@ export declare function seedAllowEntryTables(tables: {
139
156
  skillInert: ReadonlyArray<{
140
157
  skill: string;
141
158
  list: "allowed-tools" | "disallowed-tools";
142
- entry: string;
143
- reason: AllowEntryInertReason;
144
- }>;
159
+ } & AllowEntryInert>;
145
160
  inherited: readonly string[] | undefined;
146
161
  request: readonly string[] | undefined;
147
162
  deployment: readonly string[] | undefined;
148
- }): AllowEntryInertNote[];
163
+ }, roster: ReadonlySet<string>): AllowEntryInertNote[];
149
164
  /** The deployment's allow rules, read ONCE at construction: screened (a value that is not a list of strings refuses), frozen,
150
165
  * and its dead entries announced once (no session). Returns the deps with the snapshot in place of the caller's list. */
151
166
  export declare function withDeploymentAllowedTools<D extends {
@@ -181,6 +196,7 @@ declare const SHADOW_REASON: {
181
196
  readonly rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown";
182
197
  readonly ask_rule: string;
183
198
  readonly denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides";
199
+ readonly classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides";
184
200
  readonly requires_user_interaction: "this tool's server declares that a call must be put to a person";
185
201
  readonly operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule";
186
202
  readonly shell_floor: string;
@@ -221,21 +237,24 @@ export declare function cardDisclosureOf(decision: {
221
237
  /** The notice for list entries that grant (or refuse) nothing — one code for every list. */
222
238
  export declare const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
223
239
  /** One entry that reads as nothing, and the list it came from (a skill's names the skill and which of its two lists). */
224
- export type AllowEntryInertNote = {
240
+ export type AllowEntryInertNote = ({
225
241
  readonly source: "skill";
226
242
  readonly skill: string;
227
243
  readonly list: "allowed-tools" | "disallowed-tools";
228
- readonly entry: string;
229
- readonly reason: AllowEntryInertReason;
230
- } | {
244
+ } & AllowEntryInert) | ({
231
245
  readonly source: AllowEntrySource;
232
- readonly entry: string;
233
- readonly reason: AllowEntryInertReason;
234
- };
246
+ } & AllowEntryInert);
247
+ /**
248
+ * One entry that reads as nothing, as a person reads it: the entry as written (quoted, so a surrounding space shows) and why
249
+ * — the reason word, with the spelling to write when the reading has one. The one phrasing the start-of-run notice and a
250
+ * skill's refused activation share.
251
+ */
252
+ export declare function inertEntryPhrase(e: AllowEntryInert): string;
235
253
  /**
236
254
  * The notice for entries this grammar reads as nothing — delivered only when one exists: once per prepared task for the
237
255
  * skill and request lists (`sessionId` given), once per Runner for the deployment's list. The person whose list it is
238
- * reads it (audience `user`); the run goes on (an entry that grants nothing makes it ask more).
256
+ * reads it (audience `user`); the run goes on (an entry that grants nothing makes it ask more). A skill whose
257
+ * `disallowed-tools` holds such an entry is named as one that does not activate.
239
258
  */
240
259
  export declare function allowedToolsNotGrantedNotice(inert: readonly AllowEntryInertNote[], sessionId: string | undefined): EngineNotice;
241
260
  export {};