@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
@@ -1,4 +1,4 @@
1
- import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown } from "./tool-policy.js";
1
+ import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown, refuseOutOfContractDecision } from "./tool-policy.js";
2
2
  import { arbitrateByMode, classifyAsk, fileCommandOptionsSentence, mandateDisclosureOf, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
3
3
  import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
4
4
  import { classifierUnavailableArm } from "./classifier-unavailable-arm.js";
@@ -11,13 +11,15 @@ import { CD_GIT_COMPOUND_SENTENCE, changesDirectoryBeforeGit, readOnlyShellVerdi
11
11
  import { readShellCommand } from "./shell-lexer.js";
12
12
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
13
13
  import { catalogRuleFaceOf } from "./tool-registry.js";
14
- import { callAnswersTo, callFaceSeat } from "./tool-roster.js";
14
+ import { callFaceSeat } from "./tool-roster.js";
15
+ import { heldBy, wireOf } from "./tool-name-readings.js";
15
16
  import { modeAcceptsFileCommand } from "./mode-policy.js";
16
17
  import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
17
18
  import { exitGate } from "./runner/gate-exit.js";
18
19
  import { traceHookCrash } from "./gate-crash-trace.js";
19
20
  import { createGateDeadline, awaitWriteProtectionJudgment, withGateTimeout } from "./gate-bounded-await.js";
20
21
  import { thrownText } from "./thrown-value.js";
22
+ const QUESTION_TOOL_NAMES = new Set([ASK_USER_QUESTION_TOOL_NAME]);
21
23
  function isPlainOwnRecord(x) {
22
24
  try {
23
25
  if (x === null || typeof x !== "object" || Array.isArray(x))
@@ -170,7 +172,7 @@ export async function runGateLanes(pass) {
170
172
  writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
171
173
  });
172
174
  const askOriginFacts = (org, ruleCallUnreadable) => ({
173
- contentQuestion: callAnswersTo(pass.req, ASK_USER_QUESTION_TOOL_NAME),
175
+ contentQuestion: heldBy("lanes.contentQuestionClass", QUESTION_TOOL_NAMES, pass.req),
174
176
  markedUnresolvable: markedUnresolvable(),
175
177
  org,
176
178
  ...(ruleCallUnreadable ? { ruleCallUnreadable } : {}),
@@ -179,9 +181,7 @@ export async function runGateLanes(pass) {
179
181
  ...(input.toolMarkSources !== undefined ? { toolMarkSources: input.toolMarkSources } : {}),
180
182
  ...(input.a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
181
183
  });
182
- const classifyStanding = (unstamped, facts) => {
183
- pass.askClassification = classifyAsk(unstamped, facts);
184
- };
184
+ const classifyStanding = (unstamped, facts) => (pass.askClassification = classifyAsk(unstamped, facts));
185
185
  let orgTightenCount = 0;
186
186
  let orgEvidence = (() => {
187
187
  const w = input.orgRules === undefined ? "not_wired" : "not_adjudicated";
@@ -195,7 +195,7 @@ export async function runGateLanes(pass) {
195
195
  const applyOrgLayer = async (current, args) => {
196
196
  if (input.orgRules === undefined)
197
197
  return { decision: current, org: undefined };
198
- if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
198
+ if (wireOf("lanes.orgContentAskExemption", pass.req) === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
199
199
  return { decision: current, org: undefined };
200
200
  let decided = current;
201
201
  let pending;
@@ -271,7 +271,7 @@ export async function runGateLanes(pass) {
271
271
  return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
272
272
  };
273
273
  const laneReq = { ...pass.req, ...(pass.policyRewrite !== undefined ? { args: pass.policyRewrite } : {}) };
274
- const laneAnswer = input.persistedRules !== undefined && pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
274
+ const laneAnswer = input.persistedRules !== undefined && wireOf("lanes.persistedLaneSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
275
275
  {
276
276
  const tightened = applyPersistedTightening(pass.decision, laneAnswer);
277
277
  pass.decision = tightened.decision;
@@ -299,7 +299,7 @@ export async function runGateLanes(pass) {
299
299
  pass.decision.action === "ask" &&
300
300
  pass.decision.requiresRealApproval !== true &&
301
301
  pass.decision.decisionReason !== "hook" &&
302
- pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
302
+ wireOf("lanes.allowLayerQuestionSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME &&
303
303
  !markedUnresolvable();
304
304
  if (input.persistedRules && allowLayerMayClear()) {
305
305
  const answer = laneAnswer ?? {};
@@ -353,10 +353,9 @@ export async function runGateLanes(pass) {
353
353
  const originFacts = askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer));
354
354
  if (pass.decision.action === "ask") {
355
355
  const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
356
- classifyStanding(stamped, originFacts);
357
- pass.decision = stampAskOrigin(stamped, originFacts);
356
+ pass.decision = stampAskOrigin(stamped, originFacts, classifyStanding(stamped, originFacts));
358
357
  }
359
- if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && pass.probeBoundaryDeclined !== true && pass.req.toolName === COMMAND_RULE_TOOL) {
358
+ if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && pass.probeBoundaryDeclined !== true && wireOf("lanes.readOnlyShellAllow", pass.req) === COMMAND_RULE_TOOL) {
360
359
  const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
361
360
  const args = pass.req.args;
362
361
  const command = typeof args === "object" && args !== null && !Array.isArray(args) ? args[commandParam] : undefined;
@@ -397,8 +396,7 @@ export async function runGateLanes(pass) {
397
396
  else if (verdict?.kind === "unavailable") {
398
397
  const arm = classifierUnavailableArm({ ask: pass.decision, req: pass.req, cause: verdict.cause, attribution: verdict.classifier, seat: input.autoMode.unavailable, aborted: classifySignal?.aborted === true });
399
398
  if (arm.kind === "asked") {
400
- classifyStanding(arm.ask, originFacts);
401
- pass.decision = stampAskOrigin(arm.ask, originFacts);
399
+ pass.decision = stampAskOrigin(arm.ask, originFacts, classifyStanding(arm.ask, originFacts));
402
400
  pass.classifierRound = undefined;
403
401
  }
404
402
  else
@@ -416,8 +414,7 @@ export async function runGateLanes(pass) {
416
414
  requiresRealApproval: true,
417
415
  denialLimitFallback: tracked.fallback,
418
416
  };
419
- classifyStanding(fallbackAsk, originFacts);
420
- pass.decision = stampAskOrigin(fallbackAsk, originFacts);
417
+ pass.decision = stampAskOrigin(fallbackAsk, originFacts, classifyStanding(fallbackAsk, originFacts));
421
418
  }
422
419
  else {
423
420
  const denied = {
@@ -435,7 +432,7 @@ export async function runGateLanes(pass) {
435
432
  }
436
433
  if (input.sandboxAdmission !== undefined &&
437
434
  pass.decision.action === "ask" &&
438
- pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
435
+ wireOf("lanes.sandboxQuestionSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME &&
439
436
  input.isMarkedUnresolvable?.(input.event.toolCallId) !== true &&
440
437
  pass.decision.requiresRealApproval !== true &&
441
438
  pass.decision.decisionReason !== "hook" &&
@@ -463,7 +460,7 @@ export async function runGateLanes(pass) {
463
460
  if (pass.decision.action === "ask" && input.permissionMode !== undefined && pass.askClassification !== undefined) {
464
461
  const standing = pass.decision;
465
462
  const mode = input.permissionMode();
466
- const fileCommandByWord = modeAcceptsFileCommand(mode, toolName, pass.policyRewrite ?? pass.currentInput);
463
+ const fileCommandByWord = modeAcceptsFileCommand(mode, wireOf("lanes.modeFileCommand", pass.req), pass.policyRewrite ?? pass.currentInput);
467
464
  const verdict = arbitrateByMode(mode, pass.askClassification, { ...(standing.origin !== undefined ? { origin: standing.origin } : {}), ...(standing.mandate !== undefined ? { mandate: standing.mandate } : {}) }, { fileCommand: pass.shellWritesPlaced === true && fileCommandByWord });
468
465
  if (verdict.kind === "ask" && fileCommandByWord && pass.shellWritesPlaced !== true && pass.shellUnplacedOptions !== undefined && pass.askClassification.classes.size === 1 && pass.askClassification.classes.has("ordinary")) {
469
466
  pass.decision = { ...standing, message: fileCommandOptionsSentence(pass.shellUnplacedOptions.program) };
@@ -515,6 +512,7 @@ export async function runGateLanes(pass) {
515
512
  ...(callSignal !== undefined ? { signal: callSignal } : {}),
516
513
  ...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
517
514
  ...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
515
+ ...(d.action === "ask" && d.askClass !== undefined && d.classes !== undefined ? { askClass: d.askClass, classes: d.classes } : {}),
518
516
  ...(d.action === "ask" && d.mandated === true ? { mandated: true, ...(d.mandate !== undefined ? { mandate: d.mandate } : {}) } : {}),
519
517
  ...(d.action === "ask" && d.authoredBy === "deployment" ? { authoredBy: "deployment" } : {}),
520
518
  });
@@ -528,7 +526,7 @@ export async function runGateLanes(pass) {
528
526
  return { suspend: suspended, preToolContext: pass.preToolContext };
529
527
  }
530
528
  }
531
- if (pass.decision.action === "ask" && pass.req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
529
+ if (pass.decision.action === "ask" && wireOf("lanes.contentAskRoute", pass.req) === ASK_USER_QUESTION_TOOL_NAME) {
532
530
  const outcome = input.resolveContentAsk !== undefined ? await (callSignal !== undefined ? input.resolveContentAsk(pass.req, callSignal) : input.resolveContentAsk(pass.req)) : { kind: "unavailable", parkDeclined: false };
533
531
  if (outcome.kind === "answered") {
534
532
  pass.decision = { action: "allow", updatedInput: outcome.presentedInput };
@@ -660,7 +658,7 @@ export async function runGateLanes(pass) {
660
658
  if (adjudicate) {
661
659
  try {
662
660
  const editReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
663
- recheck = await (callSignal !== undefined ? adjudicate(editReq, callSignal) : adjudicate(editReq));
661
+ recheck = refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(editReq, callSignal) : adjudicate(editReq)));
664
662
  }
665
663
  catch (err) {
666
664
  recheck = {
@@ -692,7 +690,7 @@ export async function runGateLanes(pass) {
692
690
  break;
693
691
  }
694
692
  let editRuleCallUnreadable = false;
695
- if (input.persistedRules !== undefined && toolName !== ASK_USER_QUESTION_TOOL_NAME) {
693
+ if (input.persistedRules !== undefined && wireOf("lanes.persistedEditSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME) {
696
694
  const editLane = await readPersistedRules({ toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) });
697
695
  editRuleCallUnreadable = ruleCallUnreadableOf(editLane);
698
696
  const editTightened = applyPersistedTightening(recheck, editLane);
@@ -718,8 +716,7 @@ export async function runGateLanes(pass) {
718
716
  const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
719
717
  markedThisRound = undefined;
720
718
  const editFacts = askOriginFacts(editOrg.org, editRuleCallUnreadable);
721
- classifyStanding(editAskSnapshot, editFacts);
722
- const editAskDecision = stampAskOrigin(editAskSnapshot, editFacts);
719
+ const editAskDecision = stampAskOrigin(editAskSnapshot, editFacts, classifyStanding(editAskSnapshot, editFacts));
723
720
  if (input.permissionMode !== undefined && pass.askClassification !== undefined) {
724
721
  const editVerdict = arbitrateByMode(input.permissionMode(), pass.askClassification, { ...(editAskDecision.origin !== undefined ? { origin: editAskDecision.origin } : {}), ...(editAskDecision.mandate !== undefined ? { mandate: editAskDecision.mandate } : {}) });
725
722
  if (editVerdict.kind === "released") {
@@ -7,7 +7,7 @@
7
7
  * policy composes: a policy says allow / ask / deny; this file says how the pass that consumed that
8
8
  * verdict ended.
9
9
  */
10
- import { type AskClass, type AskOrigin, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
10
+ import { type AskClass, type AskOrigin, type AssertAllKeysHandled, type ModeReleaseReason, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
11
11
  import { type AutoModeClassifierRound } from "./auto-mode.js";
12
12
  /**
13
13
  * How the wait an ask was in ENDED — the closed set. One word per way a wait can end; the `who` on the
@@ -303,7 +303,8 @@ export type GateDisposition = {
303
303
  * A permission MODE released a surviving ask — the allowed disposition's account of the question it did not put
304
304
  * to anyone: the mode, the disclosed class word (`askClass`, the first member of the folded set in `ASK_CLASSES`
305
305
  * order — one of {@link RELEASABLE_ASK_CLASSES}), the whole set, and the origin / mandate words the card would
306
- * have shown. `reason: "rule_unreadable"` marks a release over a rule the call could not be read against.
306
+ * have shown. `reason` is a member of {@link import("./ask-origin.js").MODE_RELEASE_REASONS} (`isModeReleaseReason`):
307
+ * `"rule_unreadable"` marks a release over a rule the call could not be read against; absent, the release names none.
307
308
  */
308
309
  export interface ModeRelease {
309
310
  mode: PermissionMode;
@@ -311,7 +312,7 @@ export interface ModeRelease {
311
312
  classes: readonly AskClass[];
312
313
  origin?: AskOrigin;
313
314
  mandate?: PersistedRuleMandate;
314
- reason?: "rule_unreadable";
315
+ reason?: ModeReleaseReason;
315
316
  }
316
317
  /** The two modes that refuse on their own account (`deniedBy: "mode"`): `dontAsk` every non-authority ask,
317
318
  * `bypassPermissions` a write whose protection could not be judged. `plan` refuses nothing here — its questions ask. */
@@ -341,8 +342,9 @@ export interface ModeRefusal {
341
342
  * {@link import("./auto-mode.js").AUTO_MODE_FALLBACK_CAUSES} — a record whose `to` and `modelUsed`
342
343
  * disagree would let a reader answer "which model decided this call?" two ways;
343
344
  * I7 `disposition.released` present ⇒ `kind === "allowed"`, no settlement, `mode` is `bypassPermissions`,
344
- * `askClass` is a releasable class and every member of `classes` is an ask class — a mode releases nothing
345
- * a person answered and nothing the table calls immune;
345
+ * `askClass` is a releasable class, every member of `classes` is an ask class, `origin` / `reason`, when
346
+ * present, are members of their closed sets, and `reason` names a member of `classes` (the mint takes it from
347
+ * that set) — a mode releases nothing a person answered and nothing the table calls immune;
346
348
  * I8 `deniedBy === "mode"` ⇔ `mode` present (`bypassPermissions` | `dontAsk`) with `askClass` and `classes`.
347
349
  * Nothing here is read out of an ABSENCE: an ordinary allow with no ask is `{ disposition: { kind: "allowed" } }`,
348
350
  * a direct policy deny is `{ disposition: { kind: "denied", deniedBy: "policy" } }`, and neither carries a
@@ -1,4 +1,4 @@
1
- import { isAskClass, isAskOrigin, isPermissionMode, RELEASABLE_ASK_CLASSES } from "./ask-origin.js";
1
+ import { isAskClass, isAskOrigin, isModeReleaseReason, isPermissionMode, RELEASABLE_ASK_CLASSES } from "./ask-origin.js";
2
2
  import { isAutoModeFallbackCause } from "./auto-mode.js";
3
3
  export const SETTLEMENT_KINDS = [
4
4
  "human_allowed",
@@ -128,6 +128,10 @@ export function screenGateOutcome(g) {
128
128
  defects.push("I7: released.classes must be a non-empty list of ask classes");
129
129
  if (r.origin !== undefined && !isAskOrigin(r.origin))
130
130
  defects.push(`I7: released.origin "${String(r.origin)}" is outside the closed set`);
131
+ if (r.reason !== undefined && !isModeReleaseReason(r.reason))
132
+ defects.push(`I7: released.reason "${String(r.reason)}" is outside the closed set`);
133
+ else if (r.reason !== undefined && Array.isArray(r.classes) && !r.classes.includes(r.reason))
134
+ defects.push(`I7: released.reason "${r.reason}" is not a member of released.classes`);
131
135
  }
132
136
  if (d.kind === "denied") {
133
137
  const modeRefusal = d.deniedBy === "mode";
@@ -135,7 +135,7 @@ export type NoticeAudience = "user" | "operator";
135
135
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
136
136
  * so the catalog is not written a second time here and the two cannot drift.
137
137
  */
138
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "resume.legacy_mode_seed_migrated", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
138
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "resume.legacy_mode_seed_migrated", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "route.tool_call_id_folded", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
139
139
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
140
140
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
141
141
  * sink is a supported shape, and narrowing that field would break it. */
@@ -178,6 +178,7 @@ export const ENGINE_NOTICE_CODES = [
178
178
  "route.classifier_unroutable",
179
179
  "route.base_url_changed_key_unchanged",
180
180
  "route.response_model_mismatch",
181
+ "route.tool_call_id_folded",
181
182
  "task.user_steer_undrained",
182
183
  "task.user_followup_undrained",
183
184
  "steering.parked_input_blocked",
@@ -269,6 +270,7 @@ const NOTICE_AUDIENCE_TABLE = {
269
270
  "route.classifier_unroutable": "operator",
270
271
  "route.base_url_changed_key_unchanged": "operator",
271
272
  "route.response_model_mismatch": "operator",
273
+ "route.tool_call_id_folded": "operator",
272
274
  "skills.listing_truncated": "operator",
273
275
  "agents.listing_truncated": "operator",
274
276
  "tool_result.offload_put_failed": "operator",
@@ -474,6 +474,9 @@ export interface PostToolUseFailureResult {
474
474
  /** One resolved call in a postToolBatch payload (CC PostToolBatchToolCall shape, thin projection —
475
475
  * `response` is a bounded text digest of the result, never the full details object). */
476
476
  export interface PostToolBatchCall {
477
+ /** The name the model spelled on the tool_use block (an alias stays an alias) — the reference's PostToolBatch payload
478
+ * names each block the same way, while the per-call post hooks receive the resolved tool's wire name. Correlate
479
+ * the two faces by `toolCallId`. */
477
480
  toolName: string;
478
481
  input: unknown;
479
482
  toolCallId: string;
@@ -1734,6 +1737,12 @@ export interface AskCarry {
1734
1737
  /** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
1735
1738
  * on the row so a durable card is rendered by the same word the synchronous card is. */
1736
1739
  readonly origin?: import("./ask-origin.js").AskOrigin;
1740
+ /** The surviving ask's engine-stamped class ({@link import("./tool-policy.js").AskRequest.askClass}), persisted on the
1741
+ * row so a durable approval seat answers by the class the synchronous one reads. Present exactly when
1742
+ * {@link classes} is. */
1743
+ readonly askClass?: import("./ask-origin.js").AskClass;
1744
+ /** The whole class set beside it ({@link import("./tool-policy.js").AskRequest.classes}), in `ASK_CLASSES` order. */
1745
+ readonly classes?: readonly import("./ask-origin.js").AskClass[];
1737
1746
  /** The stamped mandate ({@link import("./tool-policy.js").AskRequest.mandated}) — the park site's
1738
1747
  * rule-offer facts read it, so a parked mandated ask offers no rule, like the live card. */
1739
1748
  readonly mandated?: true;
@@ -1744,11 +1753,14 @@ export interface AskCarry {
1744
1753
  }
1745
1754
  /** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
1746
1755
  * the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
1747
- * number) and the origin word only when it is a member of the closed set (the carry is typed, but the
1748
- * row is the boundary a host reads back through — the same screen the summary applies). */
1756
+ * number), the origin word only when it is a member of the closed set, and the class pair only when it is
1757
+ * well-formed, as a fresh copy (the carry is typed, but the row is the boundary a host reads back through —
1758
+ * the same screen the summary applies to the word). */
1749
1759
  export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1750
1760
  denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1751
1761
  origin?: import("./ask-origin.js").AskOrigin;
1762
+ askClass?: import("./ask-origin.js").AskClass;
1763
+ classes?: readonly import("./ask-origin.js").AskClass[];
1752
1764
  };
1753
1765
  /**
1754
1766
  * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
@@ -7,10 +7,11 @@ import { runGateLanes } from "./gate-lanes.js";
7
7
  export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
8
8
  export { cloneObserverInput } from "./runner/gate-exit.js";
9
9
  import { brandPolicyAskClass } from "./ask-class.js";
10
- import { isAskOrigin } from "./ask-origin.js";
10
+ import { isAskOrigin, screenAskClassMembers } from "./ask-origin.js";
11
11
  import { inlineUntrusted } from "./untrusted-text.js";
12
12
  export { formatHookFeedback } from "./reminder-mint.js";
13
13
  import { createSafeNotifier } from "./safe-notify.js";
14
+ import { wireOf } from "./tool-name-readings.js";
14
15
  export const DEFAULT_HOOK_TIMEOUT_MS = 600_000;
15
16
  export const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
16
17
  const POST_ABORT_GRACE_MS = 1_000;
@@ -238,18 +239,21 @@ export function askCarryRowMembers(carry) {
238
239
  if (carry === undefined)
239
240
  return {};
240
241
  const origin = carry.origin;
242
+ const classed = screenAskClassMembers(carry.askClass, carry.classes);
241
243
  return {
242
244
  ...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
243
245
  ...(isAskOrigin(origin) ? { origin } : {}),
246
+ ...(classed !== undefined ? classed : {}),
244
247
  };
245
248
  }
246
249
  export async function runToolGate(input) {
247
250
  const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
248
251
  const { toolCallId, toolName } = event;
249
252
  const callSignal = input.callSignal;
253
+ const screenedName = wireOf("gate.preToolUse", event);
250
254
  const hookCtx = (seatSignal) => ({
251
255
  toolCallId,
252
- toolName,
256
+ toolName: screenedName,
253
257
  ...(seatSignal !== undefined ? { signal: seatSignal } : {}),
254
258
  ...(() => {
255
259
  const c = input.trackedCwd?.();
@@ -284,7 +288,7 @@ export async function runToolGate(input) {
284
288
  };
285
289
  const screening = preToolUse !== undefined
286
290
  ? {
287
- screen: (args) => runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(toolName, args, hookCtx(sig))),
291
+ screen: (args) => runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(screenedName, args, hookCtx(sig))),
288
292
  screened: screenPreToolUseResult,
289
293
  crashReason: preToolUseCrashReason,
290
294
  unansweredReason: preToolUseUnansweredBlock,
@@ -1,6 +1,7 @@
1
1
  import { createFsWriteGatePolicy } from "./fs-write-gate-policy.js";
2
2
  import { isProtectedWrite } from "./tool-registry.js";
3
3
  import { rosterEntryNamed } from "./tool-roster.js";
4
+ import { wireOf } from "./tool-name-readings.js";
4
5
  import { clampPermissionMode } from "./ask-origin.js";
5
6
  import { argvOf, lexShellText } from "../shell/lex.js";
6
7
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
@@ -42,7 +43,7 @@ const ENGINE_ROSTER_SOURCES = new Set(["builtin", "synthetic"]);
42
43
  export function planRefusesWrite(req, roster, readOnlyDeclared) {
43
44
  if (isProtectedWrite(req))
44
45
  return true;
45
- const row = roster === undefined ? undefined : rosterEntryNamed(roster, req.toolName);
46
+ const row = roster === undefined ? undefined : rosterEntryNamed(roster, wireOf("mode.planRosterRow", req));
46
47
  return row !== undefined && !ENGINE_ROSTER_SOURCES.has(row.source) && row.effect !== "read" && !(row.effectProvenance === "default" && readOnlyDeclared?.(row.name) === true);
47
48
  }
48
49
  const planAsk = (message) => ({ action: "ask", message, decisionReason: "mode" });
@@ -77,6 +77,8 @@ function syntheticCheckpoint(scope) {
77
77
  mandated: true,
78
78
  denialLimitFallback: { consecutive: 3, total: 3, limit: "consecutive", autoDenyAfterMs: 0 },
79
79
  origin: "denial_limit_fallback",
80
+ askClass: "denial_limit_fallback",
81
+ classes: ["denial_limit_fallback"],
80
82
  batchToolCallIds: [`${scope}:call`],
81
83
  completedCallIds: [],
82
84
  },
@@ -141,8 +141,8 @@ export declare const READ_RULE_TOOL = "Read";
141
141
  * catalog's declarations, exactly as the protective fences' covered set does.
142
142
  */
143
143
  export declare function ruleToolGrammarOf(toolName: string): "command" | "path" | undefined;
144
- /** The names the persisted grammar judges a call under: every one of the call's rule names (`ruleNamesOf` — the wire
145
- * name, then the aliases that resolve to it) the grammar speaks for, so a stored `Bash(…)` or `Edit(…)` row reaches a
144
+ /** The names the persisted grammar judges a call under: every name the call answers to (the model's spelling, the wire
145
+ * name, the other aliases that resolve to it) the grammar speaks for, so a stored `Bash(…)` or `Edit(…)` row reaches a
146
146
  * tool that answers to that name under another wire name, as a policy's rule does. Empty ⇒ the grammar speaks for none. */
147
147
  export declare function laneToolNamesOf(req: Pick<import("./tool-policy.js").ToolCallRequest, "toolName" | "face">): readonly string[];
148
148
  /**
@@ -4,7 +4,7 @@ import { compileReadDeny } from "../tools/fs/read-deny.js";
4
4
  import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
5
5
  import { inlineUntrusted } from "./untrusted-text.js";
6
6
  import { pathTargetOf } from "./tool-registry.js";
7
- import { ruleNamesOf } from "./tool-roster.js";
7
+ import { heldValues } from "./tool-name-readings.js";
8
8
  import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
9
9
  import { readShellCommand } from "./shell-lexer.js";
10
10
  import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
@@ -32,7 +32,7 @@ export function ruleToolGrammarOf(toolName) {
32
32
  return pathTargetOf(toolName) !== undefined ? "path" : undefined;
33
33
  }
34
34
  export function laneToolNamesOf(req) {
35
- return ruleNamesOf(req).filter((n) => ruleToolGrammarOf(n) !== undefined);
35
+ return heldValues("policy.persistedGrammar", { get: (n) => (ruleToolGrammarOf(n) !== undefined ? n : undefined) }, req);
36
36
  }
37
37
  export const MAX_RULE_TEXT_CHARS = 512;
38
38
  export const BARE_INTERPRETER_NAMES = new Set([
@@ -6,7 +6,7 @@ import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/sa
6
6
  import { effectivePathTargetOf } from "./effective-path-target.js";
7
7
  import { protectivePathTargetOf } from "./tool-registry.js";
8
8
  import { ruleDenialSentence } from "./rule-denial-sentence.js";
9
- import { ruleNamesOf } from "./tool-roster.js";
9
+ import { heldValues } from "./tool-name-readings.js";
10
10
  export { parsePermissionRule };
11
11
  export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
12
12
  const DEFAULT_CAPS = {
@@ -381,10 +381,7 @@ export function createPermissionRulePolicy(rules, opts) {
381
381
  requiresRealApproval: true,
382
382
  });
383
383
  const entryFor = (req) => {
384
- const entries = ruleNamesOf(req).flatMap((n) => {
385
- const e = byTool.get(n);
386
- return e !== undefined ? [e] : [];
387
- });
384
+ const entries = heldValues("policy.permissionRules", byTool, req);
388
385
  if (entries.length <= 1)
389
386
  return entries[0];
390
387
  const bare = {};
@@ -3,7 +3,7 @@ import { pathTargetValue } from "../tool-registry.js";
3
3
  import { pathTargetBaseOf } from "../effective-path-target.js";
4
4
  import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
5
5
  import { parseSkillToolEntry, skillSpecifierRejection } from "../skill-tool-specifier.js";
6
- import { ruleNamesOf } from "../tool-roster.js";
6
+ import { heldValues, wireOf } from "../tool-name-readings.js";
7
7
  export class ActiveSkillScope {
8
8
  frames = [];
9
9
  push(frame) {
@@ -23,7 +23,7 @@ export function createActiveSkillScopePolicy(opts) {
23
23
  const { scope, env, rootPath, toolEffects } = opts;
24
24
  return {
25
25
  async check(req, signal) {
26
- const names = ruleNamesOf(req);
26
+ const entriesFor = (byName) => heldValues("policy.skillScope", byName, req).flat();
27
27
  const frames = scope.active();
28
28
  if (frames.length === 0)
29
29
  return { action: "allow" };
@@ -48,7 +48,7 @@ export function createActiveSkillScopePolicy(opts) {
48
48
  }
49
49
  return { manifest: m, byName };
50
50
  });
51
- if (perFrame.length === 0 || perFrame.some((f) => !names.some((n) => f.byName.has(n)))) {
51
+ if (perFrame.length === 0 || perFrame.some((f) => entriesFor(f.byName).length === 0)) {
52
52
  const ids = manifests.map((m) => m.lineageId).join(", ");
53
53
  return {
54
54
  action: "deny",
@@ -57,7 +57,7 @@ export function createActiveSkillScopePolicy(opts) {
57
57
  };
58
58
  }
59
59
  for (const frame of perFrame) {
60
- const alternatives = names.flatMap((n) => frame.byName.get(n) ?? []);
60
+ const alternatives = entriesFor(frame.byName);
61
61
  const rejections = [];
62
62
  let admitted = false;
63
63
  for (const entry of alternatives) {
@@ -80,7 +80,7 @@ export function createActiveSkillScopePolicy(opts) {
80
80
  const declaredTarget = req.face?.pathTarget;
81
81
  const skillScopeEligible = declaredTarget !== undefined && declaredTarget.skillScopeEligible === true;
82
82
  if (pathConstrainingActive && !skillScopeEligible) {
83
- const eff = toolEffects?.get(req.toolName) ?? "write";
83
+ const eff = toolEffects?.get(wireOf("policy.skillScope.effect", req)) ?? "write";
84
84
  if (eff !== "read") {
85
85
  const ids = manifests.filter((m) => m.allowPaths?.length).map((m) => m.lineageId).join(", ");
86
86
  return {
@@ -3,6 +3,7 @@ import { boundInputHashOf } from "../canonical-json.js";
3
3
  import { CheckpointError } from "../checkpoint-store.js";
4
4
  import { formatHookFeedback } from "../hooks.js";
5
5
  import { delimitUntrusted, REVIEWER_NOTE_MAX_BODY } from "../untrusted-text.js";
6
+ import { wireOf } from "../tool-name-readings.js";
6
7
  export function resumeDecisionWasNegative(resume) {
7
8
  const o = resume.outcome;
8
9
  return o.decision === "reject" || o.decision === "deny";
@@ -49,7 +50,7 @@ export function sameWinner(incoming, persisted) {
49
50
  (persisted.reason === undefined || incoming.reason === persisted.reason));
50
51
  }
51
52
  export function pendingContentAskCallId(cp) {
52
- return cp.pendingAction.kind === "tool_approval" && cp.pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME
53
+ return cp.pendingAction.kind === "tool_approval" && wireOf("resume.contentAskRow", cp.pendingAction) === ASK_USER_QUESTION_TOOL_NAME
53
54
  ? cp.pendingAction.toolCallId
54
55
  : undefined;
55
56
  }
@@ -3,7 +3,7 @@ import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
3
3
  import { type ClassifierUnavailableSeat } from "../classifier-unavailable-arm.js";
4
4
  import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
5
5
  import { type EngineNotice } from "../types.js";
6
- import { type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
6
+ import { type AskClass, type AskClassFacts, type AskClassification, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
7
7
  import type { Prepared, StopForDenialLimit } from "./contracts.js";
8
8
  /**
9
9
  * The classifier DENIAL-LIMIT arms of the tool gate's inherited (delegation) lane, extracted
@@ -98,34 +98,38 @@ export type InheritedAsk = Extract<PermissionResult, {
98
98
  * classifier runs inside the policy fold, BEFORE the live gate's org layer has spoken and before its
99
99
  * question-tool and marked-call lanes run, so they read as absent here and those asks meet their
100
100
  * exclusions downstream, where the facts exist. Excluded asks fall through to the frozen approver
101
- * chain, the delegation lane's original chain. The word is derived inside {@link judgeInheritedClassifier}
102
- * — once off the incoming ask for the exclusion and the unchanged-ask branches, once off the re-spoken
103
- * ask this station mints at the bound — and threaded to the request mint, never re-read off the
104
- * caller-owned object there.
105
- * The MANDATE marks are empty for the same reason — no tool seat of this task, no tighten of its own;
106
- * what the ancestor's ask carries on ITSELF still answers, read off the decision by the one predicate.
101
+ * chain, the delegation lane's original chain. The word and the class set are derived inside
102
+ * {@link judgeInheritedClassifier} and threaded to every reader ({@link inheritedAskCarry}).
103
+ * The MANDATE marks here are empty (the base names no seat): the policy-chain phase builds each call's facts over this
104
+ * base with THIS task's tool seat — the seat the request's risk axes are read from — so a marked tool's ask is
105
+ * classified by the seat at the station as at the gate. What the ancestor's ask carries on ITSELF still answers.
107
106
  */
108
107
  export declare const INHERITED_STATION_FACTS: AskOriginFacts;
109
108
  /** The members of a surviving ask that ride onto the approval request with a COMPUTED value — what the
110
109
  * four request mint stations (the gate's own and the three inherited-lane ones) spread after the seats
111
- * they spell themselves: the ask's origin word and its denial-limit fallback. */
110
+ * they spell themselves: the ask's origin word, its class (the word and the set, carried together) and its
111
+ * denial-limit fallback. */
112
112
  export interface AskRequestCarry {
113
113
  origin?: AskOrigin;
114
+ askClass?: AskClass;
115
+ classes?: readonly AskClass[];
114
116
  denialLimitFallback?: DenialLimitFallback;
115
117
  }
116
- /** The GATE's own mint station: both members are read off the surviving decision, which is the gate's
117
- * OWN snapshot (its stamp point spreads the policy's object and stamps the word onto the copy), so the
118
- * origin is the engine-derived one and the member is the one the gate's classifier step minted. */
118
+ /** The GATE's own mint station: every member is read off the surviving decision, which is the gate's
119
+ * OWN snapshot (its stamp point spreads the policy's object and stamps the word and the class onto the copy),
120
+ * so the origin and the class are the engine-derived ones and the fallback member is the one the gate's
121
+ * classifier step minted. */
119
122
  export declare function gateAskCarry(decision: InheritedAsk, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
120
123
  /** An INHERITED-lane mint station (the two wrapper arms and the approved-edit re-check): the ask is an
121
- * ancestor policy's own decision, never engine-stamped, so BOTH members come from the judgment
122
- * {@link judgeInheritedClassifier} handed back — the word it derived under the station facts and the
123
- * fallback member it captured — each read ONCE there and threaded here, never re-read off the
124
+ * ancestor policy's own decision, never engine-stamped, so EVERY member comes from the judgment
125
+ * {@link judgeInheritedClassifier} handed back — the word and the classification it derived under the station
126
+ * facts and the fallback member it captured — each read ONCE there and threaded here, never re-read off the
124
127
  * caller-owned object (a stateful accessor could otherwise answer the card with one value and the
125
- * settlement with another; a self-declared `origin` on the ancestor's decision is never read). */
128
+ * settlement with another; a self-declared `origin` / `askClass` / `classes` on the ancestor's decision is never
129
+ * read). */
126
130
  export declare function inheritedAskCarry(judged: Pick<Extract<InheritedClassifierJudgment<InheritedAsk>, {
127
131
  kind: "resolve";
128
- }>, "origin" | "fallback" | "ask">, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
132
+ }>, "origin" | "classification" | "fallback" | "ask">, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
129
133
  /** What {@link judgeInheritedClassifier} hands back to the arm that called it. */
130
134
  export type InheritedClassifierJudgment<A extends InheritedAsk> =
131
135
  /** The frozen classifier allowed. `result` is the allow the arm RETURNS — minted at the judging station so
@@ -146,8 +150,11 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
146
150
  * `origin` is the ask's word under the station facts, threaded to the request mint: on the two
147
151
  * unchanged-ask branches it is the SAME read the exclusion judged by (a caller-owned decision with a
148
152
  * stateful accessor cannot answer the exclusion with one word and the card with another); on the
149
- * bound branch it is derived from the re-spoken ask this station minted (its own object). */
150
- /** `ask` is the incoming ask unchanged (the caller's own object), or — an UNAVAILABLE classification with a live frozen
153
+ * bound branch it is derived from the re-spoken ask this station minted (its own object).
154
+ * `classification` is the class set of the ask handed on, under the same station facts: on the two unchanged-ask
155
+ * branches the one read the exclusion judged by, on the unavailable and bound branches a read of the object this
156
+ * station minted (each adds a real-approval bit, which the set must reflect). */
157
+ /** `ask` is the incoming ask unchanged (the arm's receipt snapshot of the ancestor's decision), or — an UNAVAILABLE classification with a live frozen
151
158
  * approver — that ask with the classifier sentence appended; without one it is a `deny` whose attested cause reads `unavailable`. */
152
159
  | {
153
160
  kind: "resolve";
@@ -155,6 +162,7 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
155
162
  fallback: UnarmedDenialLimitFallback | undefined;
156
163
  mintedHere: boolean;
157
164
  origin: AskOrigin;
165
+ classification: AskClassification;
158
166
  };
159
167
  /**
160
168
  * The ancestor's frozen classifier, run BEFORE its frozen approver in the ancestor's own gate order,
@@ -195,6 +203,8 @@ export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
195
203
  allowRewrite?: unknown;
196
204
  /** An `unavailable` verdict's fork facts (`classifierUnavailableArm`): is the frozen approver live; who hears it. */
197
205
  unavailableSeat?: ClassifierUnavailableSeat;
206
+ /** The station's facts for this call (the base with this task's tool seat); absent ⇒ {@link INHERITED_STATION_FACTS}. */
207
+ facts?: AskClassFacts;
198
208
  }): Promise<InheritedClassifierJudgment<A>>;
199
209
  /** The resolver's answer as the settlement reads it. */
200
210
  export type FallbackResolution = Pick<ResolvedAsk, "action" | "settlement" | "approverUnavailable">;