@sema-agent/core 7.36.0 → 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 (33) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/core/ask-origin.d.ts +35 -5
  3. package/dist/core/ask-origin.js +23 -4
  4. package/dist/core/checkpoint-store.d.ts +13 -3
  5. package/dist/core/decision-receipt.d.ts +21 -0
  6. package/dist/core/decision-receipt.js +63 -0
  7. package/dist/core/engine-notice.d.ts +8 -5
  8. package/dist/core/gate-fold.js +2 -1
  9. package/dist/core/gate-lanes.js +8 -13
  10. package/dist/core/gate-outcome.d.ts +7 -5
  11. package/dist/core/gate-outcome.js +5 -1
  12. package/dist/core/hooks.d.ts +11 -2
  13. package/dist/core/hooks.js +3 -1
  14. package/dist/core/park-selfcheck.js +2 -0
  15. package/dist/core/runner/denial-limit-arms.d.ts +28 -18
  16. package/dist/core/runner/denial-limit-arms.js +11 -7
  17. package/dist/core/runner/inherited-ask-grants.d.ts +32 -31
  18. package/dist/core/runner/inherited-ask-grants.js +5 -0
  19. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  20. package/dist/core/runner/prepare-park-ask.d.ts +2 -2
  21. package/dist/core/runner/prepare-park-ask.js +1 -5
  22. package/dist/core/runner/prepare-policy-chain.d.ts +3 -8
  23. package/dist/core/runner/prepare-policy-chain.js +43 -25
  24. package/dist/core/store-contracts/checkpoint-store-contract.js +18 -0
  25. package/dist/core/tool-name-readings.d.ts +4 -0
  26. package/dist/core/tool-name-readings.js +1 -0
  27. package/dist/core/tool-policy.d.ts +74 -84
  28. package/dist/core/tool-policy.js +71 -36
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +1 -1
  31. package/package.json +1 -1
  32. package/test/export-surface.snapshot.json +7 -1
  33. package/wire-changes/core@7.37.0.json +405 -0
@@ -1737,6 +1737,12 @@ export interface AskCarry {
1737
1737
  /** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
1738
1738
  * on the row so a durable card is rendered by the same word the synchronous card is. */
1739
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[];
1740
1746
  /** The stamped mandate ({@link import("./tool-policy.js").AskRequest.mandated}) — the park site's
1741
1747
  * rule-offer facts read it, so a parked mandated ask offers no rule, like the live card. */
1742
1748
  readonly mandated?: true;
@@ -1747,11 +1753,14 @@ export interface AskCarry {
1747
1753
  }
1748
1754
  /** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
1749
1755
  * the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
1750
- * number) and the origin word only when it is a member of the closed set (the carry is typed, but the
1751
- * 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). */
1752
1759
  export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1753
1760
  denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1754
1761
  origin?: import("./ask-origin.js").AskOrigin;
1762
+ askClass?: import("./ask-origin.js").AskClass;
1763
+ classes?: readonly import("./ask-origin.js").AskClass[];
1755
1764
  };
1756
1765
  /**
1757
1766
  * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
@@ -7,7 +7,7 @@ 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";
@@ -239,9 +239,11 @@ export function askCarryRowMembers(carry) {
239
239
  if (carry === undefined)
240
240
  return {};
241
241
  const origin = carry.origin;
242
+ const classed = screenAskClassMembers(carry.askClass, carry.classes);
242
243
  return {
243
244
  ...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
244
245
  ...(isAskOrigin(origin) ? { origin } : {}),
246
+ ...(classed !== undefined ? classed : {}),
245
247
  };
246
248
  }
247
249
  export async function runToolGate(input) {
@@ -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
  },
@@ -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">;
@@ -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;
@@ -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.
@@ -173,11 +173,7 @@ export function prepareParkAsk(input) {
173
173
  if (reprojectedRaced.tag === "threw")
174
174
  throw reprojectedRaced.error;
175
175
  const reprojected = refuseOutOfContractDecision(reprojectedRaced.value);
176
- const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
177
- const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
178
- !(demanded?.ok === true &&
179
- samePlainValue(demanded.value, parkedArgs) &&
180
- samePlainValue(reprojected.updatedInput, parkedArgs));
176
+ const rewroteTheFiledValue = reprojected.updatedInput !== undefined && !samePlainValue(reprojected.updatedInput, parkedArgs);
181
177
  if (reprojected.action === "deny" || rewroteTheFiledValue) {
182
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` +
183
179
  `${reprojected.action === "deny" && reprojected.message ? `: ${reprojected.message}` : ""} — ` +
@@ -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;
@@ -186,13 +187,7 @@ export interface PreparePolicyChainResult {
186
187
  }>>;
187
188
  /** borrowed-mutable — the grant record. Writers: the inherited arms here (record a clean allow at a live approver);
188
189
  * the gate station (consume on reuse, sweep at settlement). */
189
- inheritedAskGrants: Map<string, {
190
- approver: unknown;
191
- argsJson: string;
192
- presented: unknown;
193
- waitMs: number;
194
- settled?: object;
195
- }>;
190
+ inheritedAskGrants: Map<string, InheritedAskGrant>;
196
191
  /** owned — the SINGLE AskRequest source-identity factory every resolveAsk construction site spreads. */
197
192
  askSourceIdentity: () => {
198
193
  principal?: string;
@@ -6,12 +6,12 @@ import { cloneObserverInput, resolveHookTimeoutMs } from "../hooks.js";
6
6
  import { assertHookSeatsCallable } from "../hook-seats.js";
7
7
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "../permission-rules.js";
8
8
  import { isIsolated } from "../remote-env.js";
9
- import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, durableParkOptInDeclared, isLiveApproverSeat, resolveAsk, toolPolicyNameSets, wrapToolPolicy, attestAskUnresolvableCause } from "../tool-policy.js";
9
+ import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, durableParkOptInDeclared, isLiveApproverSeat, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, wrapToolPolicy, attestAskUnresolvableCause } from "../tool-policy.js";
10
10
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
11
11
  import { NAMESPACED_NAME_SHAPES, protocolOf } from "../protocol-table.js";
12
12
  import { emitTrace } from "../trace.js";
13
13
  import { createActiveSkillScopePolicy } from "./active-skill-scope.js";
14
- import { arbitrateByMode, askMandateMember, classifyAsk } from "../ask-origin.js";
14
+ import { arbitrateByMode, askClassMembersOf, askMandateMember } from "../ask-origin.js";
15
15
  import { createModeGatePolicy, mountedToolDeclaresReadOnly } from "../mode-policy.js";
16
16
  const ancestorFacts = (ask) => ({ ...(ask ?? {}), ancestorResolved: true });
17
17
  import { headlessDenyAtFold, headlessDenyAtRecheck, INHERITED_STATION_FACTS, inheritedAskCarry, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
@@ -20,12 +20,12 @@ import { askGrantShapeOf } from "./inherited-ask-grants.js";
20
20
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
21
21
  import { createLiveSessionRules, createSessionRulePolicy } from "./session-rule-policy.js";
22
22
  import { thrownText } from "../thrown-value.js";
23
- import { heldBy, seatOf, wireOf } from "../tool-name-readings.js";
23
+ import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
24
24
  export async function preparePolicyChain(input) {
25
25
  const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
26
26
  const ancestorArbitrations = new Map();
27
- const arbitrateInherited = (ask, toolCallId) => {
28
- const verdict = arbitrateByMode(permissionMode.current, classifyAsk(ask, INHERITED_STATION_FACTS), { ...(ask.origin !== undefined ? { origin: ask.origin } : {}), ...(ask.mandate !== undefined ? { mandate: ask.mandate } : {}) });
27
+ const arbitrateInherited = (ask, toolCallId, classification) => {
28
+ const verdict = arbitrateByMode(permissionMode.current, classification, { ...(ask.origin !== undefined ? { origin: ask.origin } : {}), ...(ask.mandate !== undefined ? { mandate: ask.mandate } : {}) });
29
29
  if (verdict.kind === "ask" || verdict.kind === "accepted")
30
30
  return undefined;
31
31
  ancestorArbitrations.set(toolCallId, verdict);
@@ -188,7 +188,7 @@ export async function preparePolicyChain(input) {
188
188
  }, "mandate_unreconstructible", creq);
189
189
  const markerChannelFullDeny = (creq) => attestAskUnresolvableCause({ action: "deny", message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed` }, "approver_unavailable", creq);
190
190
  const inheritedAskGrants = new Map();
191
- const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs, settled) => {
191
+ const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs, settled, classes) => {
192
192
  if (typeof approver !== "function")
193
193
  return;
194
194
  if (presented === undefined)
@@ -198,7 +198,7 @@ export async function preparePolicyChain(input) {
198
198
  const argsJson = askGrantShapeOf(presented);
199
199
  if (argsJson === undefined)
200
200
  return;
201
- inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs, settled });
201
+ inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs, settled, classes });
202
202
  };
203
203
  const sandboxDeclared = isIsolated(executionEnv);
204
204
  const sandboxAdmissionArmed = (() => {
@@ -259,7 +259,7 @@ export async function preparePolicyChain(input) {
259
259
  };
260
260
  const callerPolicyLayer = policy !== undefined && sameInstanceAncestorCount > 0
261
261
  ? wrapToolPolicy(policy, async (creq, csignal) => {
262
- const decision = await policy.check(creq, csignal ?? abortController.signal);
262
+ const decision = refuseOutOfContractDecision(await policy.check(creq, csignal ?? abortController.signal));
263
263
  sharedFirstDecision.set(creq.toolCallId, decision);
264
264
  if (sharedFirstDecision.size > SHARED_FIRST_DECISION_CAP) {
265
265
  const oldest = sharedFirstDecision.keys().next().value;
@@ -278,15 +278,33 @@ export async function preparePolicyChain(input) {
278
278
  : {}),
279
279
  ...(delegation.isDelegatedChild ? { isDelegatedChild: true } : {}),
280
280
  });
281
+ const seatMarksOf = (call) => {
282
+ const tierSeat = seatOf("report.riskAxes", irreversibilityTier, call);
283
+ return {
284
+ tier: tierSeat?.value,
285
+ egress: heldBy("report.riskAxes.egress", egressTools, call),
286
+ shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
287
+ sources: new Set(heldValues("report.markSources", toolMarkSources, call).flatMap((s) => [...s])),
288
+ };
289
+ };
281
290
  const riskAxesOf = (call) => {
282
- const tier = seatOf("report.riskAxes", irreversibilityTier, call)?.value;
291
+ const seat = seatMarksOf(call);
283
292
  const negatives = seatOf("report.riskAxes.negatives", axisExplicitNegatives, call)?.value;
284
- const irreversible = tier === "always" || tier === "maybe" ? true : tier === "never" ? false : negatives?.irreversible;
285
- const egress = heldBy("report.riskAxes.egress", egressTools, call) ? true : negatives?.egress;
293
+ const irreversible = seat.tier === "always" || seat.tier === "maybe" ? true : seat.tier === "never" ? false : negatives?.irreversible;
294
+ const egress = seat.egress ? true : negatives?.egress;
286
295
  if (irreversible === undefined && egress === undefined)
287
296
  return {};
288
297
  return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
289
298
  };
299
+ const inheritedStationFacts = (call) => {
300
+ const seat = seatMarksOf(call);
301
+ return {
302
+ ...INHERITED_STATION_FACTS,
303
+ mandateMarks: { egress: seat.egress, shellGated: seat.shellGated, ...(seat.tier !== undefined ? { irreversibility: seat.tier } : {}) },
304
+ ...(seat.sources.size > 0 ? { toolMarkSources: seat.sources } : {}),
305
+ ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}),
306
+ };
307
+ };
290
308
  const inheritedAskEvidence = inheritedAskRuleEvidence(deps.permissionRuleStore);
291
309
  const { personal: permissionRuleLane, org: permissionRuleOrgLane } = createPermissionRuleLanes({
292
310
  provider: deps.permissionRuleStore,
@@ -335,7 +353,7 @@ export async function preparePolicyChain(input) {
335
353
  }
336
354
  let re;
337
355
  try {
338
- re = await pol.check({ ...creq, args: editArgs }, csignal ?? abortController.signal);
356
+ re = refuseOutOfContractDecision(await pol.check({ ...creq, args: editArgs }, csignal ?? abortController.signal));
339
357
  }
340
358
  catch (err) {
341
359
  return vetoed({
@@ -353,7 +371,7 @@ export async function preparePolicyChain(input) {
353
371
  }
354
372
  if (re.updatedInput !== undefined)
355
373
  editArgs = re.updatedInput;
356
- const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: ancestorAutoMode, ask: re, req: { ...creq, args: editArgs }, signal: csignal ?? abortController.signal, subject: "the approved edit", allowRewrite: editArgs, unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(ancestorDecider?.timeoutMs !== undefined ? { budgetMs: ancestorDecider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(onAskOf), onNotice: deps.onNotice, sessionId, runId }) });
374
+ const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: ancestorAutoMode, ask: re, req: { ...creq, args: editArgs }, signal: csignal ?? abortController.signal, subject: "the approved edit", allowRewrite: editArgs, facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(ancestorDecider?.timeoutMs !== undefined ? { budgetMs: ancestorDecider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(onAskOf), onNotice: deps.onNotice, sessionId, runId }) });
357
375
  if (judged.kind === "allow") {
358
376
  const cleared = { ...judged.result };
359
377
  carryEngineAttestations(judged.result, cleared);
@@ -364,7 +382,7 @@ export async function preparePolicyChain(input) {
364
382
  return vetoed(judged.result);
365
383
  const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere } = judged;
366
384
  re = editAsk;
367
- const editArbitrated = arbitrateInherited(re, creq.toolCallId);
385
+ const editArbitrated = arbitrateInherited(re, creq.toolCallId, judged.classification);
368
386
  if (editArbitrated !== undefined) {
369
387
  const out = editArbitrated.action === "allow" ? { action: "allow", updatedInput: editArgs } : editArbitrated;
370
388
  carryPrecedingSettlement(settledBy, out);
@@ -435,17 +453,17 @@ export async function preparePolicyChain(input) {
435
453
  return first;
436
454
  return mandateUnreconstructibleDeny(creq);
437
455
  }
438
- const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: first, req: creq, signal: csignal ?? abortController.signal, subject: "this call", unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
456
+ const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: first, req: creq, signal: csignal ?? abortController.signal, subject: "this call", facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
439
457
  if (judged.kind === "allow")
440
458
  return judged.result;
441
459
  if (judged.kind === "deny")
442
460
  return judged.result;
443
461
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
444
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && classifyAsk(inheritedAsk, INHERITED_STATION_FACTS).row.sandboxAdmission) {
462
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
445
463
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
446
464
  return { action: "allow" };
447
465
  }
448
- const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId);
466
+ const arbitrated = arbitrateInherited(inheritedAsk, creq.toolCallId, judged.classification);
449
467
  if (arbitrated !== undefined)
450
468
  return arbitrated;
451
469
  const presentedArgs = creq.args;
@@ -477,14 +495,14 @@ export async function preparePolicyChain(input) {
477
495
  if (resolved.updatedInput !== undefined) {
478
496
  return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode);
479
497
  }
480
- recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
498
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved, askClassMembersOf(judged.classification).classes);
481
499
  return { action: "allow" };
482
500
  },
483
501
  }
484
502
  : wrapToolPolicy(pc.policy, async (creq, csignal) => {
485
503
  let decision;
486
504
  try {
487
- decision = await pc.policy.check(creq, csignal ?? abortController.signal);
505
+ decision = refuseOutOfContractDecision(await pc.policy.check(creq, csignal ?? abortController.signal));
488
506
  }
489
507
  catch (err) {
490
508
  return {
@@ -509,17 +527,17 @@ export async function preparePolicyChain(input) {
509
527
  return mandateUnreconstructibleDeny(creq);
510
528
  }
511
529
  const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
512
- const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: decision, req: { ...creq, args: presentedArgs }, signal: csignal ?? abortController.signal, subject: "this call", allowRewrite: decision.updatedInput, unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
530
+ const judged = await judgeInheritedClassifier({ liveMode: () => permissionMode.current, autoMode: pc.autoMode, ask: decision, req: { ...creq, args: presentedArgs }, signal: csignal ?? abortController.signal, subject: "this call", allowRewrite: decision.updatedInput, facts: inheritedStationFacts(creq), unavailableSeat: classifierUnavailableSeat({ station: "ancestor", ...(pc.autoMode?.decider.timeoutMs !== undefined ? { budgetMs: pc.autoMode.decider.timeoutMs } : {}), approvalSeat: isLiveApproverSeat(pc.onAsk), onNotice: deps.onNotice, sessionId, runId }) });
513
531
  if (judged.kind === "allow")
514
532
  return judged.result;
515
533
  if (judged.kind === "deny")
516
534
  return judged.result;
517
535
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
518
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && classifyAsk(inheritedAsk, INHERITED_STATION_FACTS).row.sandboxAdmission) {
536
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq) && judged.classification.row.sandboxAdmission) {
519
537
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
520
538
  return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
521
539
  }
522
- const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId);
540
+ const arbitrated = arbitrateInherited(inheritedAsk.action === "ask" ? { ...inheritedAsk, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) } : inheritedAsk, creq.toolCallId, judged.classification);
523
541
  if (arbitrated !== undefined)
524
542
  return arbitrated;
525
543
  const askT0 = now();
@@ -551,7 +569,7 @@ export async function preparePolicyChain(input) {
551
569
  return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode);
552
570
  }
553
571
  if (pc.preToolUse === undefined) {
554
- recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
572
+ recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved, askClassMembersOf(judged.classification).classes);
555
573
  }
556
574
  if (decision.updatedInput === undefined)
557
575
  return resolved;
@@ -572,7 +590,7 @@ export async function preparePolicyChain(input) {
572
590
  sameInstanceAdapterIndices.add(j + callerSlotOffset);
573
591
  });
574
592
  const instrumentedChain = rewriteCapableChain.map((layer, idx) => wrapToolPolicy(layer, async (creq, csignal) => {
575
- const d = await layer.check(creq, csignal);
593
+ const d = refuseOutOfContractDecision(await layer.check(creq, csignal));
576
594
  if (d.updatedInput !== undefined)
577
595
  rewriteEmitterIndex.set(creq.toolCallId, idx);
578
596
  if (d.action === "ask") {
@@ -619,7 +637,7 @@ export async function preparePolicyChain(input) {
619
637
  const revalidated = {};
620
638
  try {
621
639
  for (const layer of recheckLayers) {
622
- const d = await layer.check(finalReq, signal);
640
+ const d = refuseOutOfContractDecision(await layer.check(finalReq, signal));
623
641
  if (d.action === "deny") {
624
642
  const carried = { ...d, updatedInput: decision.updatedInput };
625
643
  carryEngineAttestations(d, carried);