@sema-agent/core 7.35.2 → 7.37.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 (85) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/dist/brain/anthropic.js +6 -6
  3. package/dist/brain/errors.d.ts +1 -14
  4. package/dist/brain/errors.js +2 -6
  5. package/dist/brain/open-responses.js +7 -7
  6. package/dist/brain/openai.js +7 -7
  7. package/dist/brain/provider-echo-shape.d.ts +26 -0
  8. package/dist/brain/provider-echo-shape.js +12 -0
  9. package/dist/brain/tool-call-id.d.ts +27 -15
  10. package/dist/brain/tool-call-id.js +43 -0
  11. package/dist/core/ask-origin.d.ts +35 -5
  12. package/dist/core/ask-origin.js +23 -4
  13. package/dist/core/ask-question.d.ts +0 -30
  14. package/dist/core/ask-question.js +3 -2
  15. package/dist/core/checkpoint-store.d.ts +18 -6
  16. package/dist/core/checkpoint-store.js +2 -1
  17. package/dist/core/context-edit.d.ts +4 -0
  18. package/dist/core/context-edit.js +5 -2
  19. package/dist/core/decision-receipt.d.ts +21 -0
  20. package/dist/core/decision-receipt.js +63 -0
  21. package/dist/core/engine-notice.d.ts +15 -5
  22. package/dist/core/gate-fold.js +2 -1
  23. package/dist/core/gate-lanes.js +20 -23
  24. package/dist/core/gate-outcome.d.ts +7 -5
  25. package/dist/core/gate-outcome.js +5 -1
  26. package/dist/core/governance-codes.d.ts +1 -1
  27. package/dist/core/governance-codes.js +2 -0
  28. package/dist/core/hooks.d.ts +14 -2
  29. package/dist/core/hooks.js +7 -3
  30. package/dist/core/mode-policy.js +2 -1
  31. package/dist/core/park-selfcheck.js +2 -0
  32. package/dist/core/permission-rule-model.d.ts +2 -2
  33. package/dist/core/permission-rule-model.js +2 -2
  34. package/dist/core/permission-rules.js +2 -5
  35. package/dist/core/runner/active-skill-scope.js +5 -5
  36. package/dist/core/runner/decide-continuation.js +2 -1
  37. package/dist/core/runner/denial-limit-arms.d.ts +28 -18
  38. package/dist/core/runner/denial-limit-arms.js +11 -7
  39. package/dist/core/runner/inherited-ask-grants.d.ts +32 -31
  40. package/dist/core/runner/inherited-ask-grants.js +5 -0
  41. package/dist/core/runner/permission-rule-lanes.d.ts +3 -0
  42. package/dist/core/runner/permission-rule-lanes.js +7 -6
  43. package/dist/core/runner/prepare-context-lane.d.ts +4 -0
  44. package/dist/core/runner/prepare-context-lane.js +7 -5
  45. package/dist/core/runner/prepare-gate-stations.js +17 -12
  46. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  47. package/dist/core/runner/prepare-park-ask.d.ts +4 -2
  48. package/dist/core/runner/prepare-park-ask.js +8 -11
  49. package/dist/core/runner/prepare-policy-chain.d.ts +5 -8
  50. package/dist/core/runner/prepare-policy-chain.js +50 -31
  51. package/dist/core/runner/prepare-safety-scan.js +9 -2
  52. package/dist/core/runner/prepare-task.js +3 -3
  53. package/dist/core/runner/resume-apply.js +2 -1
  54. package/dist/core/runner/run-attachment-seats.js +3 -1
  55. package/dist/core/runner/run-harness-handlers.js +6 -1
  56. package/dist/core/runner/run-leg.js +11 -9
  57. package/dist/core/runner/run-recovery-lanes.js +2 -0
  58. package/dist/core/runner/session-rule-policy.js +5 -4
  59. package/dist/core/runner/tool-call-id-fold-seat.d.ts +13 -0
  60. package/dist/core/runner/tool-call-id-fold-seat.js +31 -0
  61. package/dist/core/sensitive-path-policy.js +2 -2
  62. package/dist/core/side-query.d.ts +5 -1
  63. package/dist/core/side-query.js +5 -1
  64. package/dist/core/store-contracts/checkpoint-store-contract.js +18 -0
  65. package/dist/core/tool-name-readings.d.ts +370 -0
  66. package/dist/core/tool-name-readings.js +137 -0
  67. package/dist/core/tool-policy.d.ts +79 -89
  68. package/dist/core/tool-policy.js +86 -49
  69. package/dist/core/tool-registry.d.ts +1 -31
  70. package/dist/core/tool-registry.js +2 -22
  71. package/dist/core/tool-result-budget.d.ts +3 -0
  72. package/dist/core/tool-result-budget.js +5 -3
  73. package/dist/core/tool-roster.d.ts +30 -9
  74. package/dist/core/tool-roster.js +45 -5
  75. package/dist/engine/llm/types.d.ts +4 -0
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/tools/fs/file-read-check.d.ts +1 -1
  79. package/dist/tools/fs/file-read-check.js +4 -3
  80. package/dist/tools/fs/shell-read-check.d.ts +15 -1
  81. package/dist/tools/fs/shell-read-check.js +5 -2
  82. package/package.json +2 -1
  83. package/test/export-surface.snapshot.json +7 -1
  84. package/wire-changes/core@7.36.0.json +288 -0
  85. package/wire-changes/core@7.37.0.json +405 -0
@@ -5,7 +5,7 @@ import { attestClassifierDenyCause, attestClassifierRound } from "../tool-policy
5
5
  import { SETTLEMENT_NOBODY_JUDGED } from "../gate-outcome.js";
6
6
  import { deliverEngineNotice } from "../types.js";
7
7
  import { inlineUntrusted } from "../untrusted-text.js";
8
- import { askOriginOf, classifyAsk } from "../ask-origin.js";
8
+ import { askClassMembersOf, askOriginOf, classifyAsk } from "../ask-origin.js";
9
9
  export function attachRebuiltDenialTrackers(entries, denialLimit) {
10
10
  if (entries === undefined)
11
11
  return undefined;
@@ -68,30 +68,34 @@ function fallbackCarry(fallback, liveApprover, tracker) {
68
68
  export function gateAskCarry(decision, liveApprover, tracker) {
69
69
  return {
70
70
  ...(decision.origin !== undefined ? { origin: decision.origin } : {}),
71
+ ...(decision.askClass !== undefined && decision.classes !== undefined ? { askClass: decision.askClass, classes: decision.classes } : {}),
71
72
  ...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker),
72
73
  };
73
74
  }
74
75
  export function inheritedAskCarry(judged, liveApprover, tracker) {
75
76
  return {
76
77
  origin: judged.origin,
78
+ ...askClassMembersOf(judged.classification),
77
79
  ...fallbackCarry(judged.fallback, liveApprover, tracker),
78
80
  };
79
81
  }
80
82
  export async function judgeInheritedClassifier(opts) {
81
83
  const { autoMode, ask, req } = opts;
82
- const origin = askOriginOf(ask, INHERITED_STATION_FACTS);
84
+ const facts = opts.facts ?? INHERITED_STATION_FACTS;
85
+ const origin = askOriginOf(ask, facts);
86
+ const classification = classifyAsk(ask, facts);
83
87
  const classifierStandsDown = () => {
84
88
  const live = opts.liveMode?.();
85
89
  return live === "dontAsk" || live === "bypassPermissions" || live === "plan" || (autoMode?.liveMode !== undefined && autoMode.liveMode.current !== "auto");
86
90
  };
87
- if (autoMode === undefined || classifierStandsDown() || !classifyAsk(ask, INHERITED_STATION_FACTS).row.classifierEligible) {
88
- return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
91
+ if (autoMode === undefined || classifierStandsDown() || !classification.row.classifierEligible) {
92
+ return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification };
89
93
  }
90
94
  const verdict = await autoMode.decider
91
95
  .decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal)
92
96
  .catch(() => ({ kind: "unavailable", cause: "error" }));
93
97
  if (verdict.kind === "allow" && classifierStandsDown())
94
- return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
98
+ return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification };
95
99
  const attributed = (d) => (verdict.classifier !== undefined ? attestClassifierRound(d, verdict.classifier, req) : d);
96
100
  if (verdict.kind === "allow") {
97
101
  autoMode.denialTracking?.recordAllow();
@@ -100,7 +104,7 @@ export async function judgeInheritedClassifier(opts) {
100
104
  if (verdict.kind === "unavailable") {
101
105
  const arm = classifierUnavailableArm({ ask, req, cause: verdict.cause, seat: opts.unavailableSeat, aborted: opts.signal.aborted });
102
106
  if (arm.kind === "asked")
103
- return { kind: "resolve", ask: arm.ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
107
+ return { kind: "resolve", ask: arm.ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification: classifyAsk(arm.ask, facts) };
104
108
  return { kind: "deny", result: attributed(arm.deny) };
105
109
  }
106
110
  const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
@@ -116,7 +120,7 @@ export async function judgeInheritedClassifier(opts) {
116
120
  }
117
121
  const fallback = tracked.fallback;
118
122
  const minted = { ...ask, message: denialLimitFallbackMessage(fallback, reason || category || req.toolName), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback };
119
- return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, INHERITED_STATION_FACTS) };
123
+ return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, facts), classification: classifyAsk(minted, facts) };
120
124
  }
121
125
  export function headlessDenyAtFold(r) {
122
126
  return r.settlement !== undefined && SETTLEMENT_NOBODY_JUDGED[r.settlement.kind];
@@ -1,41 +1,42 @@
1
+ import type { AskClass } from "../ask-origin.js";
1
2
  import type { HumanReviewGateRow } from "../task-result.js";
2
3
  import { type OnAsk, type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
3
- /** JSON.stringify throws on a cyclic value (a deployment
4
- * policy's rewrite object carries no acyclicity guarantee) — `undefined` here means "no comparable
5
- * shape", which every consumer treats as a mismatch (skip the record / skip the reuse ⇒ the child
6
- * gate just re-asks — the safe direction, never a gate failure). */
7
- export declare function askGrantShapeOf(args: unknown): string | undefined;
8
- /**
9
- * Duplicate-frame collapse (single-Runner embedded shape), the CONSUME half: an inherited
10
- * layer already resolved THIS call as a clean allow at the SAME approver function, on EXACTLY the
11
- * byte-shape this caller-slot ask now presents. The human's consent covers this consultation
12
- * verbatim — asking again is the 2-frame UX debt, not a second authority. Guards, all mismatches
13
- * re-ask (⇒ `undefined`):
14
- * · SOURCE: only a caller-slot POLICY ask may reuse — a safety tighten
15
- * (egress/irreversible/shellGate stamp `decisionReason:"safety"`), a hook-promoted ask
16
- * ("hook"), or a classifier ask carries a DIFFERENT authority/context and keeps its frame.
17
- * · IDENTITY: the approver FUNCTION reference (never a name) — compared through
18
- * `askApproverIdentity`, not by raw reference (ruled 2026-08-04): a delegated child's approver
19
- * is the ancestor's own, wrapped with delegation provenance — a DIFFERENT function object, the
20
- * SAME operator. Bare `===` therefore stopped collapsing the frame the moment the ask seam
21
- * started forwarding approvers, and one tool call presented the same person the same card
22
- * twice. The wrapper adds metadata and delegates every decision, so it grants no authority of
23
- * its own; identity here only answers "same consultation".
24
- * · SHAPE: byte-equality against the presented SNAPSHOT; reuse re-emits that snapshot
25
- * as the executed shape via `presentedInput` — the same schema-revalidated binding
26
- * as a main-gate approval (shown == executed, by construction).
27
- * · LEDGER: the ONE real consultation (timed on the wrapper arm) is recorded here —
28
- * one frame happened, one frame is booked.
29
- * The grant lives only within one gate evaluation (consumed on reuse, swept at the call site) — a
30
- * REOPENED checkpoint's re-adjudication never sees a stale grant.
31
- */
32
- export declare function consumeInheritedAskGrant(grants: Map<string, {
4
+ /** One inherited ask grant: a person's clean allow at an ancestor's live approver, on ONE presented shape, to ONE question
5
+ * (`classes`, the request's class set in `ASK_CLASSES` order) — it answers no stricter question. */
6
+ export interface InheritedAskGrant {
33
7
  approver: unknown;
34
8
  argsJson: string;
35
9
  presented: unknown;
36
10
  waitMs: number;
37
11
  settled?: object;
38
- }>, onAsk: OnAsk | undefined, humanReviewRef: {
12
+ classes: readonly AskClass[];
13
+ }
14
+ /** Does a grant for `granted` cover a question the gate classified as `asked`? `ordinary` — the plain hesitation every other
15
+ * class is a stricter form of — is covered by any card answered; every other class must have been on the card (the gate reads
16
+ * facts the station cannot hold, and a class those add was asked of nobody). Unclassified, or a card with no class ⇒ never. */
17
+ export declare function askGrantCovers(granted: readonly AskClass[], asked: readonly AskClass[] | undefined): boolean;
18
+ /** JSON.stringify throws on a cyclic value (a rewrite object carries no acyclicity guarantee) — `undefined` means "no
19
+ * comparable shape", which every consumer treats as a mismatch (skip the record / reuse ⇒ re-ask, the safe direction). */
20
+ export declare function askGrantShapeOf(args: unknown): string | undefined;
21
+ /**
22
+ * Duplicate-frame collapse, the CONSUME half: an inherited layer already resolved THIS call as a clean allow at the
23
+ * SAME approver function, on EXACTLY the byte-shape this caller-slot ask now presents, for a question that covers this
24
+ * one — asking again is the 2-frame UX debt, not a second authority. Guards, all mismatches re-ask (⇒ `undefined`):
25
+ * · SOURCE: only a caller-slot POLICY ask may reuse — a safety tighten (`decisionReason:"safety"`), a hook-promoted
26
+ * ask ("hook") or a classifier ask carries a DIFFERENT authority and keeps its frame.
27
+ * · IDENTITY: the approver FUNCTION reference (never a name), compared through `askApproverIdentity`: a delegated
28
+ * child's approver is the ancestor's own wrapped with provenance — a different function object, the same operator
29
+ * (the wrapper grants no authority of its own); identity answers only "same consultation".
30
+ * · SHAPE: byte-equality against the presented SNAPSHOT; reuse re-emits that snapshot as the executed shape via
31
+ * `presentedInput` — the same binding as a main-gate approval (shown == executed, by construction).
32
+ * · CLASS: the gate's stamped `classes` is covered by the set the person answered ({@link askGrantCovers}); a class
33
+ * the gate's own judges add (a protected target, an out-of-root read) was not on the card. Not covered ⇒ the gate
34
+ * asks its own question and the grant stays (it answers what it answered; the call-site sweep ends it).
35
+ * · LEDGER: the ONE real consultation (timed on the wrapper arm) is booked here — one frame happened, one is booked.
36
+ * The grant lives within one gate evaluation (consumed on reuse, swept at the call site): a REOPENED checkpoint's
37
+ * re-adjudication never sees a stale grant.
38
+ */
39
+ export declare function consumeInheritedAskGrant(grants: Map<string, InheritedAskGrant>, onAsk: OnAsk | undefined, humanReviewRef: {
39
40
  count: number;
40
41
  totalWaitMs: number;
41
42
  gates: HumanReviewGateRow[];
@@ -1,5 +1,8 @@
1
1
  import { primaryActivityArg } from "../arg-summary.js";
2
2
  import { askApproverIdentity, carryEngineAttestations } from "../tool-policy.js";
3
+ export function askGrantCovers(granted, asked) {
4
+ return asked !== undefined && asked.length > 0 && granted.length > 0 && asked.every((c) => c === "ordinary" || granted.includes(c));
5
+ }
3
6
  export function askGrantShapeOf(args) {
4
7
  try {
5
8
  return JSON.stringify(args ?? null);
@@ -14,6 +17,8 @@ export function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision
14
17
  const grant = grants.get(req.toolCallId);
15
18
  if (grant === undefined || askApproverIdentity(grant.approver) !== askApproverIdentity(onAsk) || grant.argsJson !== askGrantShapeOf(req.args))
16
19
  return undefined;
20
+ if (!askGrantCovers(grant.classes, decision.action === "ask" ? decision.classes : undefined))
21
+ return undefined;
17
22
  grants.delete(req.toolCallId);
18
23
  humanReviewRef.count += 1;
19
24
  humanReviewRef.totalWaitMs += grant.waitMs;
@@ -241,6 +241,9 @@ export declare function createRuleOffersOf(cfg: {
241
241
  shellGatedTools: {
242
242
  has(name: string): boolean;
243
243
  };
244
+ toolMarkSources: {
245
+ get(name: string): ReadonlySet<string> | undefined;
246
+ };
244
247
  taskRoot: string | undefined;
245
248
  /** The live tracked-cwd ref (read per call — the relative-cd resolution base). */
246
249
  cwdRef: {
@@ -3,7 +3,8 @@ import {} from "../hooks.js";
3
3
  import { adjudicatePersistedPathRules, adjudicatePersistedRules, laneToolNamesOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
4
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
5
5
  import { effectivePathTargetOf } from "../effective-path-target.js";
6
- import { anyNamed, declaredPathTargetOf, firstNamed, protectivePathTargetOf, ruleNamesOf } from "../tool-registry.js";
6
+ import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
7
+ import { heldBy, seatOf, wireOf } from "../tool-name-readings.js";
7
8
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
8
9
  import { ownerStoreOf } from "../permission-rule-provider.js";
9
10
  import { thrownText } from "../thrown-value.js";
@@ -92,7 +93,7 @@ export function createPermissionRuleLanes(cfg) {
92
93
  const org = provider.partitions.org
93
94
  ? {
94
95
  adjudicate: async (req) => {
95
- if (req.toolName === cfg.questionToolName && cfg.questionToolMounted)
96
+ if (wireOf("offer.orgQuestionExemption", req) === cfg.questionToolName && cfg.questionToolMounted)
96
97
  return { status: "available" };
97
98
  let resolution;
98
99
  try {
@@ -158,16 +159,16 @@ export function createPermissionRuleLanes(cfg) {
158
159
  }
159
160
  export function createRuleOffersOf(cfg) {
160
161
  return (call, args, ask) => {
161
- const toolName = call.toolName;
162
+ const toolName = wireOf("offer.laneTool", call);
162
163
  if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
163
164
  return {};
164
165
  if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
165
166
  return {};
166
- const tierSeat = firstNamed(cfg.irreversibilityTier, ruleNamesOf(call));
167
+ const tierSeat = seatOf("offer.marks", cfg.irreversibilityTier, call);
167
168
  const closedDoor = ruleClearanceOf(ask, {
168
- egress: anyNamed(cfg.egressTools, ruleNamesOf(call)),
169
+ egress: heldBy("offer.marks.egress", cfg.egressTools, call),
169
170
  irreversibility: tierSeat?.value,
170
- shellGated: tierSeat !== undefined && cfg.shellGatedTools.has(tierSeat.name),
171
+ shellGated: tierSeat?.shellGated(cfg.shellGatedTools, cfg.toolMarkSources) === true,
171
172
  }).word;
172
173
  if (closedDoor !== undefined)
173
174
  return { ruleOffersAbsence: closedDoor };
@@ -24,6 +24,7 @@ import { type RunnerDeps } from "../types.js";
24
24
  import type { Prepared, PreparedMicroCompact } from "./contracts.js";
25
25
  import { type GitStatusLaneRef } from "./git-status-frame.js";
26
26
  import type { ReadFileState } from "../../tools/fs/safety.js";
27
+ import { type ToolRosterDeltaSeat } from "../tool-roster.js";
27
28
  export interface PrepareContextLaneInput {
28
29
  /** borrowed-readonly — the task model: the budgets, the trigger-window geometry and `charsPerToken` are read off it. */
29
30
  model: Model;
@@ -39,6 +40,8 @@ export interface PrepareContextLaneInput {
39
40
  hostTaskId: string;
40
41
  /** borrowed-readonly — the run's offload store, or undefined (the caps' and the machine's persist seat). */
41
42
  offloadStore: ToolResultStore | undefined;
43
+ /** borrowed-readonly — the leg's live roster: a recorded tool name is judged as the wire name it resolves to. */
44
+ toolRosterDeltas: ToolRosterDeltaSeat;
42
45
  /** borrowed-readonly — the doors' RESOLVED microCompact knob (immutable values, never the deps bag). */
43
46
  microCompactKnob: {
44
47
  machine: MicroCompactMachine;
@@ -135,6 +138,7 @@ export interface GuardChainArgs {
135
138
  offloadStore: import("../tool-result-store.js").ToolResultStore | undefined;
136
139
  sessionId: string;
137
140
  taskId: string;
141
+ toolNameOf?: (recorded: string) => string;
138
142
  deps: RunnerDeps;
139
143
  trimPressureRef: {
140
144
  droppedMessages: boolean;
@@ -8,8 +8,10 @@ import { emitTrace } from "../trace.js";
8
8
  import { deliverEngineNotice } from "../types.js";
9
9
  import { applyGitFrameGuard } from "./git-status-frame.js";
10
10
  import { applyContextShrinkToReadFileState } from "../../tools/fs/index.js";
11
+ import { rosterToolName } from "../tool-roster.js";
11
12
  export function prepareContextLane(input) {
12
- const { model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, readFileState } = input;
13
+ const { model, deps, sessionId, runId, hostTaskId, offloadStore, toolRosterDeltas, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, readFileState } = input;
14
+ const toolNameOf = (recorded) => rosterToolName(toolRosterDeltas.current, recorded);
13
15
  const contextOverlay = input.contextOverlay ?? (() => undefined);
14
16
  const requestGeometry = () => {
15
17
  const current = harness.getModel();
@@ -27,7 +29,7 @@ export function prepareContextLane(input) {
27
29
  const replay = replayClearedProjection(healed, microCompact.ledger);
28
30
  const replayed = replay.messages;
29
31
  const capped = await capAggregateToolResults(replayed, {
30
- store: offloadStore,
32
+ store: offloadStore, toolNameOf,
31
33
  sessionId,
32
34
  onCapped: (info) => pendingProjectionObservations.push(() => emitTrace(deps.tracer, () => ({
33
35
  kind: "tool_result.capped",
@@ -49,7 +51,7 @@ export function prepareContextLane(input) {
49
51
  });
50
52
  let ccPass;
51
53
  const edited = microCompact.machine === "off" ? mediaCapped : clearStaleToolResults(mediaCapped, {
52
- budgetTokens: editAt,
54
+ budgetTokens: editAt, toolNameOf,
53
55
  clearSource: replayed,
54
56
  onCleared: (pass) => { ccPass = pass; },
55
57
  anchoredTotalTokens: estimateContextTokens(mediaCapped, cpt).tokens,
@@ -67,7 +69,7 @@ export function prepareContextLane(input) {
67
69
  }
68
70
  const chain = await runGuardChain({
69
71
  edited, replayed, index: replay.index, microCompact, guardAt, charsPerToken: cpt, offloadStore,
70
- sessionId, taskId: hostTaskId, deps, trimPressureRef, recheck, signal,
72
+ sessionId, taskId: hostTaskId, deps, trimPressureRef, recheck, signal, toolNameOf,
71
73
  pendingProjectionObservations,
72
74
  ...(readFileState !== undefined ? { readFileState } : {}),
73
75
  });
@@ -221,7 +223,7 @@ export async function runGuardChain(args) {
221
223
  if (microCompact.machine === "off" && anchoredWorking > guardAt) {
222
224
  let blockingPass;
223
225
  const blockingEdited = clearStaleToolResults(working, {
224
- budgetTokens: guardAt,
226
+ budgetTokens: guardAt, toolNameOf: args.toolNameOf,
225
227
  clearSource: args.replayed,
226
228
  onCleared: (pass) => { blockingPass = pass; },
227
229
  anchoredTotalTokens: anchoredWorking,
@@ -9,8 +9,10 @@ import { askUnresolvableNotice } from "../ask-unresolvable-notice.js";
9
9
  import { askReachesPerson } from "../wiring-manifest.js";
10
10
  import { classifierUnavailableSeat } from "./classifier-ladder.js";
11
11
  import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
12
- import { anyNamed, callAnswersTo, callFaceSeat, firstNamed, rosterToolName, ruleNamesOf, toolCallFaceOf } from "../tool-roster.js";
12
+ import { callFaceSeat, rosterToolName, toolCallFaceOf } from "../tool-roster.js";
13
+ import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
13
14
  import { noteHostRewrite } from "../frame-settlement.js";
15
+ const PEER_MESSAGE_NAMES = new Set([SEND_MESSAGE_TOOL_NAME]);
14
16
  function screenGateResult(result) {
15
17
  if (result.suspend !== undefined)
16
18
  return { defects: [] };
@@ -88,12 +90,12 @@ export function prepareGateStations(input) {
88
90
  harness.on("tool_call", async (called) => {
89
91
  const e = { ...called, toolName: rosterToolName(toolRosterDeltas.current, called.toolName) };
90
92
  const call = { toolName: e.toolName, ...callFaceSeat(toolCallFaceOf(toolRosterDeltas.current, called.toolName)) };
91
- const names = ruleNamesOf(call);
92
- const tierSeat = firstNamed(irreversibilityTier, names);
93
+ const tierSeat = seatOf("gate.shellSeat", irreversibilityTier, call);
94
+ const markSources = new Set(heldValues("gate.markSources", toolMarkSources, call).flatMap((s) => [...s]));
93
95
  blockedToolCalls.delete(e.toolCallId);
94
96
  inheritedAskGrants.delete(e.toolCallId);
95
97
  {
96
- const complianceDeny = names.map((n) => complianceCallDenial(complianceDenies, n)).find((d) => d !== undefined);
98
+ const complianceDeny = heldValues("gate.compliance", { get: (n) => complianceCallDenial(complianceDenies, n) }, call)[0];
97
99
  if (complianceDeny !== undefined) {
98
100
  if (blockedTracked)
99
101
  blockedToolCalls.add(e.toolCallId);
@@ -122,19 +124,19 @@ export function prepareGateStations(input) {
122
124
  resolveAsk: resolveAskBound,
123
125
  suspendAsk,
124
126
  resolveContentAsk,
125
- egress: anyNamed(egressTools, names),
126
- peerMessage: callAnswersTo(call, SEND_MESSAGE_TOOL_NAME),
127
+ egress: heldBy("gate.egress", egressTools, call),
128
+ peerMessage: heldBy("gate.peerMessage", PEER_MESSAGE_NAMES, call),
127
129
  ...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}),
128
130
  ...(shellReadCheck !== undefined ? { shellReadCheck } : {}),
129
131
  irreversibility: tierSeat?.value,
130
- reversibilityProbe: tierSeat !== undefined ? reversibilityProbes.get(tierSeat.name) : undefined,
132
+ reversibilityProbe: tierSeat?.read(reversibilityProbes),
131
133
  abortSignal: abortController.signal,
132
134
  ...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
133
135
  onHookError: notifyHookError,
134
- shellGated: tierSeat !== undefined && shellGatedTools.has(tierSeat.name),
136
+ shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
135
137
  ...(autoModeDecider.current !== undefined ? { autoMode: { decider: autoModeDecider.current, ...(autoModeDenialTracking.current !== undefined ? { denialTracking: autoModeDenialTracking.current } : {}), unavailable: classifierUnavailableSeat({ station: "leg", ...(autoModeDecider.current.timeoutMs !== undefined ? { budgetMs: autoModeDecider.current.timeoutMs } : {}), approvalSeat: askReachesPerson(askRoute), onNotice: deps.onNotice, sessionId, runId }) } } : {}),
136
138
  permissionMode: () => permissionMode.current,
137
- ...(() => { const s = firstNamed(toolMarkSources, names)?.value; return s !== undefined ? { toolMarkSources: s } : {}; })(),
139
+ ...(markSources.size > 0 ? { toolMarkSources: markSources } : {}),
138
140
  ...(a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
139
141
  onModeReleased,
140
142
  ancestorArbitrationOf: (toolCallId) => ancestorArbitrations.get(toolCallId),
@@ -243,6 +245,7 @@ export function prepareGateStations(input) {
243
245
  }
244
246
  let content = e.content;
245
247
  let changed = false;
248
+ const post = { toolName: rosterToolName(toolRosterDeltas.current, e.toolName) };
246
249
  if (e.isError) {
247
250
  if (hooks?.postToolUseFailure) {
248
251
  let clonedDetails = e.details;
@@ -251,7 +254,8 @@ export function prepareGateStations(input) {
251
254
  }
252
255
  catch {
253
256
  }
254
- const failureSeat = await runHookSeat("postToolUseFailure", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUseFailure(e.toolName, e.input, {
257
+ const failedName = wireOf("hook.postToolUseFailure", post);
258
+ const failureSeat = await runHookSeat("postToolUseFailure", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUseFailure(failedName, e.input, {
255
259
  error: e.content
256
260
  .filter((c) => c.type === "text")
257
261
  .map((c) => c.text)
@@ -259,7 +263,7 @@ export function prepareGateStations(input) {
259
263
  isInterrupt: abortController.signal.aborted || spec.signal?.aborted === true,
260
264
  content: e.content.map((c) => ({ ...c })),
261
265
  details: clonedDetails,
262
- }, { toolCallId: e.toolCallId, toolName: e.toolName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
266
+ }, { toolCallId: e.toolCallId, toolName: failedName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
263
267
  if (failureSeat.expired) {
264
268
  notifyOwnHookCrash(hookSeatExpiredError("postToolUseFailure", hookTimeoutMs, failureSeat.cause, "the observation was abandoned and its additionalContext dropped; the failure result itself is unchanged"));
265
269
  }
@@ -271,7 +275,8 @@ export function prepareGateStations(input) {
271
275
  }
272
276
  }
273
277
  else if (hooks?.postToolUse) {
274
- const successSeat = await runHookSeat("postToolUse", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
278
+ const ranName = wireOf("hook.postToolUse", post);
279
+ const successSeat = await runHookSeat("postToolUse", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUse(ranName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: ranName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
275
280
  if (successSeat.expired) {
276
281
  notifyOwnHookCrash(hookSeatExpiredError("postToolUse", hookTimeoutMs, successSeat.cause, "the seat was abandoned; the tool result is delivered to the model UNMODIFIED (no updatedOutput, no additionalContext)"));
277
282
  }
@@ -36,7 +36,7 @@ export function prepareInheritedGate(input) {
36
36
  if (modeSeed.migrated !== undefined) {
37
37
  deliverEngineNotice(deps.onNotice, {
38
38
  code: "resume.legacy_mode_seed_migrated",
39
- message: `the resumed checkpoint row carried pre-mode keys (${modeSeed.migrated.droppedKeys.join(", ")}); read once into ${modeSeed.migrated.permissionMode !== undefined ? `permissionMode: "${modeSeed.migrated.permissionMode}"` : "no mode of its own (the chain's ceiling stands)"}${modeSeed.shellGate !== undefined ? ' with the shell floor "always" kept' : ""} — the next suspend writes the one key; from 7.35.0 a row still carrying these keys is refused`,
39
+ message: `the resumed checkpoint row carried pre-mode keys (${modeSeed.migrated.droppedKeys.join(", ")}); read once into ${modeSeed.migrated.permissionMode !== undefined ? `permissionMode: "${modeSeed.migrated.permissionMode}"` : "no mode of its own (the chain's ceiling stands)"}${modeSeed.shellGate !== undefined ? ' with the shell floor "always" kept' : ""} — the next suspend writes the one key; this migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys (resume or abandon such rows before upgrading)`,
40
40
  detail: { migrated: { ...(modeSeed.migrated.permissionMode !== undefined ? { permissionMode: modeSeed.migrated.permissionMode } : {}), droppedKeys: [...modeSeed.migrated.droppedKeys] }, sessionId, runId },
41
41
  });
42
42
  }
@@ -9,8 +9,8 @@
9
9
  * decomposition exists to make explicit.
10
10
  *
11
11
  * The thirteen-argument park closure is unchanged: twelve positional seats then the structural {@link AskCarry}, the
12
- * shape the gate's one invocation site (`parkWith`) holds. The ask's origin word and the denial-limit fallback ride the
13
- * carry onto the row exactly as before — this phase mints no origin, changes no word, adds no arm to the closed set.
12
+ * shape the gate's one invocation site (`parkWith`) holds. The ask's origin word, class pair and denial-limit fallback
13
+ * ride the carry onto the row — this phase mints no origin or class, changes no word, adds no arm to the closed set.
14
14
  *
15
15
  * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (both closures run per tool call), so the
16
16
  * phase adds no yield between the boundary parks before it and the gate station after it.
@@ -116,6 +116,8 @@ export interface PrepareParkAskInput {
116
116
  shellGatedTools: ReadonlySet<string>;
117
117
  /** borrowed-readonly — the per-tool tier, read only for the name the call's tier seat is read under (as at the gate). */
118
118
  irreversibilityTier: IrreversibilityTier;
119
+ /** borrowed-readonly — who marked each name's seat (the descriptor's `shellGated` reads it as the gate does). */
120
+ toolMarkSources: ReadonlyMap<string, ReadonlySet<import("../ask-origin.js").ToolMarkSource>>;
119
121
  /** borrowed-readonly — the operator's shell floor (`"always"`), persisted on a shell-gated descriptor; undefined = no floor. */
120
122
  effectiveShellGate: ShellGateDoctrine | undefined;
121
123
  /** borrowed-readonly — the EFFECTIVE durable-approval opt-in (the caller's, or the synthesized mandate scope): picks the
@@ -15,7 +15,8 @@ import { checkpointScopeOf } from "./checkpoint-scope.js";
15
15
  import { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
16
16
  import { DEFAULT_UNATTENDED_APPROVAL_TTL_MS, publishCommittedSuspend, sanitizedTtlMs } from "./park-commit.js";
17
17
  import { remoteEnvFailureNote } from "./remote-env-retry.js";
18
- import { callFaceSeat, firstNamed, parkedToolIdentity, parkedToolFace, ruleNamesOf } from "../tool-roster.js";
18
+ import { callFaceSeat, parkedToolIdentity, parkedToolFace } from "../tool-roster.js";
19
+ import { seatOf, wireOf } from "../tool-name-readings.js";
19
20
  class ParkRefusal extends Error {
20
21
  constructor(message, options) {
21
22
  super(message, options);
@@ -26,7 +27,7 @@ const wordlessDemand = (toolName) => {
26
27
  throw new Error(`a real-approval demand for "${toolName}" reached the park with no origin word — the origin stamp precedes every park`);
27
28
  };
28
29
  export function prepareParkAsk(input) {
29
- const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
30
+ const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
30
31
  if (askLane === undefined || saga === undefined)
31
32
  return { parkAsk: undefined };
32
33
  const { composedCallSignal, approvalPreviewOf } = askLane;
@@ -114,7 +115,7 @@ export function prepareParkAsk(input) {
114
115
  };
115
116
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
116
117
  ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated, carry) => {
117
- const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
118
+ const syncFirstEligible = wireOf("park.syncFirstRoute", req) === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
118
119
  if (syncFirstEligible &&
119
120
  runtimeCaps?.forceDurableGate !== true &&
120
121
  liveFaceUnavailable !== true &&
@@ -172,11 +173,7 @@ export function prepareParkAsk(input) {
172
173
  if (reprojectedRaced.tag === "threw")
173
174
  throw reprojectedRaced.error;
174
175
  const reprojected = refuseOutOfContractDecision(reprojectedRaced.value);
175
- const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
176
- const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
177
- !(demanded?.ok === true &&
178
- samePlainValue(demanded.value, parkedArgs) &&
179
- samePlainValue(reprojected.updatedInput, parkedArgs));
176
+ const rewroteTheFiledValue = reprojected.updatedInput !== undefined && !samePlainValue(reprojected.updatedInput, parkedArgs);
180
177
  if (reprojected.action === "deny" || rewroteTheFiledValue) {
181
178
  throw new ParkRefusal(`the arguments of "${req.toolName}" change shape when stored by this checkpoint backend, and the deployment's tool policy does not accept the stored shape` +
182
179
  `${reprojected.action === "deny" && reprojected.message ? `: ${reprojected.message}` : ""} — ` +
@@ -234,7 +231,7 @@ export function prepareParkAsk(input) {
234
231
  remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
235
232
  }
236
233
  token = mintCheckpointToken();
237
- const tierSeat = firstNamed(irreversibilityTier, ruleNamesOf(req));
234
+ const tierSeat = seatOf("park.riskDescriptor", irreversibilityTier, req);
238
235
  const riskDescriptor = buildRiskDescriptor({
239
236
  toolName: req.toolName,
240
237
  args: parkedArgs,
@@ -242,7 +239,7 @@ export function prepareParkAsk(input) {
242
239
  ...(shadowedRule !== undefined ? { shadowedRule } : {}),
243
240
  ...(probeReason !== undefined ? { probeReason } : {}),
244
241
  ...(probeCause !== undefined ? { probeCause } : {}),
245
- shellGated: tierSeat !== undefined && shellGatedTools.has(tierSeat.name),
242
+ shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
246
243
  ...(effectiveShellGate !== undefined ? { shellGateDoctrine: effectiveShellGate } : {}),
247
244
  });
248
245
  gate =
@@ -303,7 +300,7 @@ export function prepareParkAsk(input) {
303
300
  kind: "tool_approval",
304
301
  toolCallId: req.toolCallId,
305
302
  toolName: req.toolName,
306
- ...parkedToolIdentity(toolRosterDeltas.current, req.toolName), ...parkedToolFace(toolRosterDeltas.current, req.toolName),
303
+ ...parkedToolIdentity(toolRosterDeltas.current, req.toolName), ...parkedToolFace(toolRosterDeltas.current, req),
307
304
  args: parkedArgs,
308
305
  ...(() => {
309
306
  const p = approvalPreviewOf(req.toolName, parkedArgs);
@@ -34,6 +34,7 @@ import { type ActiveSkillScope } from "./active-skill-scope.js";
34
34
  import type { InheritedGate, IrreversibilityTier, PrepareResume, RunInternals, StopForDenialLimit } from "./contracts.js";
35
35
  import { type Arbitration } from "../ask-origin.js";
36
36
  import { type ModeGateSeats, type PermissionModeController } from "../mode-policy.js";
37
+ import { type InheritedAskGrant } from "./inherited-ask-grants.js";
37
38
  import { createRuleOffersOf, type PermissionRuleLanes } from "./permission-rule-lanes.js";
38
39
  import type { ToolCallLike } from "../tool-registry.js";
39
40
  export interface PreparePolicyChainInput {
@@ -45,7 +46,7 @@ export interface PreparePolicyChainInput {
45
46
  * `permissionRuleStore` / `localOwnerRules` (the lanes and the offer factory), `onError` / `onNotice` (the advisories,
46
47
  * the refusal disclosures, the late-ask observer), `tracer` (the admission and rule-store frames), `checkpointStore` (the park facility). Receiver preserved;
47
48
  * the advisory dedupe is KEYED on this object's identity. */
48
- deps: Pick<RunnerDeps, "sessionPolicyStore" | "hooks" | "permissionRuleStore" | "localOwnerRules" | "onError" | "onNotice" | "tracer" | "checkpointStore">;
49
+ deps: Pick<RunnerDeps, "sessionPolicyStore" | "hooks" | "permissionRuleStore" | "localOwnerRules" | "onError" | "onNotice" | "tracer" | "checkpointStore" | "a2a">;
49
50
  /** borrowed-readonly — the trusted spawn-side channel: `parentTaskId` (the from-subagent bit), `explicitAgentName` /
50
51
  * `agentName` (the source agent name) — the ask source identity's local, trusted facts. */
51
52
  internals: Pick<RunInternals, "parentTaskId" | "explicitAgentName" | "agentName"> | undefined;
@@ -85,6 +86,8 @@ export interface PreparePolicyChainInput {
85
86
  envHandToolNames: ReadonlySet<string>;
86
87
  /** borrowed-readonly — the shell-seat provenance set (the offer factory's `shellGated` mandate term, read per call). */
87
88
  shellGatedTools: ReadonlySet<string>;
89
+ /** borrowed-readonly — who marked each name's seat (the offer factory's `shellGated` term reads it as the gate does). */
90
+ toolMarkSources: ReadonlyMap<string, ReadonlySet<import("../ask-origin.js").ToolMarkSource>>;
88
91
  /** borrowed-readonly — the tracked-cwd seat, read per call by the lanes and the offer factory (never written here). */
89
92
  handsCwdRef: CwdRef | undefined;
90
93
  /** borrowed-readonly — the compiled read-deny judge, or undefined (the offer factory's directory negative control). */
@@ -184,13 +187,7 @@ export interface PreparePolicyChainResult {
184
187
  }>>;
185
188
  /** borrowed-mutable — the grant record. Writers: the inherited arms here (record a clean allow at a live approver);
186
189
  * the gate station (consume on reuse, sweep at settlement). */
187
- inheritedAskGrants: Map<string, {
188
- approver: unknown;
189
- argsJson: string;
190
- presented: unknown;
191
- waitMs: number;
192
- settled?: object;
193
- }>;
190
+ inheritedAskGrants: Map<string, InheritedAskGrant>;
194
191
  /** owned — the SINGLE AskRequest source-identity factory every resolveAsk construction site spreads. */
195
192
  askSourceIdentity: () => {
196
193
  principal?: string;