@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.
- package/CHANGELOG.md +81 -0
- package/dist/core/ask-origin.d.ts +35 -5
- package/dist/core/ask-origin.js +23 -4
- package/dist/core/checkpoint-store.d.ts +13 -3
- package/dist/core/decision-receipt.d.ts +21 -0
- package/dist/core/decision-receipt.js +63 -0
- package/dist/core/engine-notice.d.ts +8 -5
- package/dist/core/gate-fold.js +2 -1
- package/dist/core/gate-lanes.js +8 -13
- package/dist/core/gate-outcome.d.ts +7 -5
- package/dist/core/gate-outcome.js +5 -1
- package/dist/core/hooks.d.ts +11 -2
- package/dist/core/hooks.js +3 -1
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/runner/denial-limit-arms.d.ts +28 -18
- package/dist/core/runner/denial-limit-arms.js +11 -7
- package/dist/core/runner/inherited-ask-grants.d.ts +32 -31
- package/dist/core/runner/inherited-ask-grants.js +5 -0
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +1 -5
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -8
- package/dist/core/runner/prepare-policy-chain.js +43 -25
- package/dist/core/store-contracts/checkpoint-store-contract.js +18 -0
- package/dist/core/tool-name-readings.d.ts +4 -0
- package/dist/core/tool-name-readings.js +1 -0
- package/dist/core/tool-policy.d.ts +74 -84
- package/dist/core/tool-policy.js +71 -36
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
- package/wire-changes/core@7.37.0.json +405 -0
package/dist/core/hooks.d.ts
CHANGED
|
@@ -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)
|
|
1751
|
-
* row is the boundary a host reads back through —
|
|
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
|
package/dist/core/hooks.js
CHANGED
|
@@ -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
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
|
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:
|
|
117
|
-
* OWN snapshot (its stamp point spreads the policy's object and stamps the word onto the copy),
|
|
118
|
-
* origin
|
|
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
|
|
122
|
-
* {@link judgeInheritedClassifier} handed back — the word it derived under the station
|
|
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
|
|
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
|
-
|
|
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
|
|
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() || !
|
|
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,
|
|
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
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
|
13
|
-
* carry onto the row
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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 =
|
|
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) &&
|
|
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) &&
|
|
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);
|