@sema-agent/core 7.35.2 → 7.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/dist/brain/anthropic.js +6 -6
- package/dist/brain/errors.d.ts +1 -14
- package/dist/brain/errors.js +2 -6
- package/dist/brain/open-responses.js +7 -7
- package/dist/brain/openai.js +7 -7
- package/dist/brain/provider-echo-shape.d.ts +26 -0
- package/dist/brain/provider-echo-shape.js +12 -0
- package/dist/brain/tool-call-id.d.ts +27 -15
- package/dist/brain/tool-call-id.js +43 -0
- package/dist/core/ask-origin.d.ts +35 -5
- package/dist/core/ask-origin.js +23 -4
- package/dist/core/ask-question.d.ts +0 -30
- package/dist/core/ask-question.js +3 -2
- package/dist/core/checkpoint-store.d.ts +18 -6
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/context-edit.d.ts +4 -0
- package/dist/core/context-edit.js +5 -2
- package/dist/core/decision-receipt.d.ts +21 -0
- package/dist/core/decision-receipt.js +63 -0
- package/dist/core/engine-notice.d.ts +15 -5
- package/dist/core/gate-fold.js +2 -1
- package/dist/core/gate-lanes.js +20 -23
- package/dist/core/gate-outcome.d.ts +7 -5
- package/dist/core/gate-outcome.js +5 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +14 -2
- package/dist/core/hooks.js +7 -3
- package/dist/core/mode-policy.js +2 -1
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/permission-rule-model.d.ts +2 -2
- package/dist/core/permission-rule-model.js +2 -2
- package/dist/core/permission-rules.js +2 -5
- package/dist/core/runner/active-skill-scope.js +5 -5
- package/dist/core/runner/decide-continuation.js +2 -1
- 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/permission-rule-lanes.d.ts +3 -0
- package/dist/core/runner/permission-rule-lanes.js +7 -6
- package/dist/core/runner/prepare-context-lane.d.ts +4 -0
- package/dist/core/runner/prepare-context-lane.js +7 -5
- package/dist/core/runner/prepare-gate-stations.js +17 -12
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-park-ask.d.ts +4 -2
- package/dist/core/runner/prepare-park-ask.js +8 -11
- package/dist/core/runner/prepare-policy-chain.d.ts +5 -8
- package/dist/core/runner/prepare-policy-chain.js +50 -31
- package/dist/core/runner/prepare-safety-scan.js +9 -2
- package/dist/core/runner/prepare-task.js +3 -3
- package/dist/core/runner/resume-apply.js +2 -1
- package/dist/core/runner/run-attachment-seats.js +3 -1
- package/dist/core/runner/run-harness-handlers.js +6 -1
- package/dist/core/runner/run-leg.js +11 -9
- package/dist/core/runner/run-recovery-lanes.js +2 -0
- package/dist/core/runner/session-rule-policy.js +5 -4
- package/dist/core/runner/tool-call-id-fold-seat.d.ts +13 -0
- package/dist/core/runner/tool-call-id-fold-seat.js +31 -0
- package/dist/core/sensitive-path-policy.js +2 -2
- package/dist/core/side-query.d.ts +5 -1
- package/dist/core/side-query.js +5 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +18 -0
- package/dist/core/tool-name-readings.d.ts +370 -0
- package/dist/core/tool-name-readings.js +137 -0
- package/dist/core/tool-policy.d.ts +79 -89
- package/dist/core/tool-policy.js +86 -49
- package/dist/core/tool-registry.d.ts +1 -31
- package/dist/core/tool-registry.js +2 -22
- package/dist/core/tool-result-budget.d.ts +3 -0
- package/dist/core/tool-result-budget.js +5 -3
- package/dist/core/tool-roster.d.ts +30 -9
- package/dist/core/tool-roster.js +45 -5
- package/dist/engine/llm/types.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/file-read-check.d.ts +1 -1
- package/dist/tools/fs/file-read-check.js +4 -3
- package/dist/tools/fs/shell-read-check.d.ts +15 -1
- package/dist/tools/fs/shell-read-check.js +5 -2
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +7 -1
- package/wire-changes/core@7.36.0.json +288 -0
- package/wire-changes/core@7.37.0.json +405 -0
|
@@ -107,8 +107,9 @@ export interface AskOriginFacts {
|
|
|
107
107
|
/** The MANDATE mark inputs of this call — the tool SEAT's marks plus the gate's own per-call records
|
|
108
108
|
* ({@link MandateMarks}), read from the same two places the lane's own derivation reads them. The
|
|
109
109
|
* per-call PROBE member is deliberately not here: it is engine-stamped on the DECISION, and
|
|
110
|
-
* {@link ruleClearanceOf} reads it there, so one fact cannot arrive twice with two values.
|
|
111
|
-
*
|
|
110
|
+
* {@link ruleClearanceOf} reads it there, so one fact cannot arrive twice with two values. An inherited
|
|
111
|
+
* station passes the tool SEAT's marks of the task it runs in (its per-call records are the gate's, absent
|
|
112
|
+
* there); a station holding no seat at all passes `{}` — an honest empty, since the decision's own
|
|
112
113
|
* members still answer. */
|
|
113
114
|
readonly mandateMarks: Omit<MandateMarks, "probeMandated">;
|
|
114
115
|
}
|
|
@@ -133,12 +134,15 @@ export declare function askOriginOf(decision: Extract<PermissionResult, {
|
|
|
133
134
|
* <out-of-root>` under a standing `Bash(cat:*)` ask rule offered the directory and kept asking after it).
|
|
134
135
|
* It lives at the one derivation rather than the three stamp sites: the withdrawal's input IS the origin
|
|
135
136
|
* word, so a site that stamped without it would be a second, quieter answer to one question.
|
|
137
|
+
* The same stamp writes the ask's CLASS members (`askClass` / `classes`, {@link askClassMembersOf} over the
|
|
138
|
+
* `classification` the caller derived from this decision under these facts): the word and the class leave the
|
|
139
|
+
* gate on one object, and a value a policy put on either is replaced.
|
|
136
140
|
*/
|
|
137
141
|
export declare function stampAskOrigin<T extends Extract<PermissionResult, {
|
|
138
142
|
action: "ask";
|
|
139
|
-
}>>(decision: T, facts: AskOriginFacts): T & {
|
|
143
|
+
}>>(decision: T, facts: AskOriginFacts, classification: AskClassification): T & {
|
|
140
144
|
origin: AskOrigin;
|
|
141
|
-
};
|
|
145
|
+
} & AskClassMembers;
|
|
142
146
|
/**
|
|
143
147
|
* CAN a persisted allow rule clear this ask — and, if not, which door closed? ONE derivation with TWO
|
|
144
148
|
* readers, which is the whole point of the shape: the approval card's `ruleOffersAbsence` word and the
|
|
@@ -610,6 +614,18 @@ export declare const RRA_EXPLAINERS: ReadonlySet<AskClass>;
|
|
|
610
614
|
/** The classes a permission mode can RELEASE (the `bypassImmune: false` rows) — the only words that can appear on
|
|
611
615
|
* `GateDisposition.released.askClass`; every other class reaches a person under every mode. */
|
|
612
616
|
export declare const RELEASABLE_ASK_CLASSES: readonly AskClass[];
|
|
617
|
+
/**
|
|
618
|
+
* The closed set of `reason` words a mode release carries (`GateDisposition.released.reason`, the `released` arm of
|
|
619
|
+
* {@link Arbitration}): each is a releasable class the release record names on its own — `rule_unreadable`, a release
|
|
620
|
+
* over a person's rule the call could not be read against. A release carries the first member (array order) its class
|
|
621
|
+
* set holds, and none when it holds no member. {@link arbitrateByMode} and {@link isModeReleaseReason} read one copy
|
|
622
|
+
* of this list taken at load (a caller that writes into the exported array changes neither), so a new reason is one
|
|
623
|
+
* word here.
|
|
624
|
+
*/
|
|
625
|
+
export declare const MODE_RELEASE_REASONS: readonly ["rule_unreadable"];
|
|
626
|
+
export type ModeReleaseReason = (typeof MODE_RELEASE_REASONS)[number];
|
|
627
|
+
/** Membership test for {@link ModeReleaseReason} — the word crosses the wire on `tool_end.gate.disposition.released.reason`. */
|
|
628
|
+
export declare function isModeReleaseReason(v: unknown): v is ModeReleaseReason;
|
|
613
629
|
/**
|
|
614
630
|
* The facts the class derivation reads beside the decision: the origin facts the gate holds, plus the tool
|
|
615
631
|
* seat's mark PROVENANCE ({@link ToolMarkSource}; a mark with no source reads as the deployment's, fail-closed),
|
|
@@ -628,6 +644,20 @@ export interface AskClassification {
|
|
|
628
644
|
readonly askClass: AskClass;
|
|
629
645
|
readonly row: ModeArbitrationRow;
|
|
630
646
|
}
|
|
647
|
+
/** The two members an ask's classification puts on the approval request and the park row: the disclosed word and
|
|
648
|
+
* the whole set, the set as an array in {@link ASK_CLASSES} order (so `classes[0] === askClass`). */
|
|
649
|
+
export interface AskClassMembers {
|
|
650
|
+
readonly askClass: AskClass;
|
|
651
|
+
readonly classes: readonly AskClass[];
|
|
652
|
+
}
|
|
653
|
+
/** The ONE projection of a classification onto {@link AskClassMembers} — every station that carries an ask's class
|
|
654
|
+
* spells it through this, so the word and the set cannot be read from two derivations. The array is a fresh frozen
|
|
655
|
+
* copy per call: an approver that holds it cannot reach the park row's copy or the next card's. */
|
|
656
|
+
export declare function askClassMembersOf(c: AskClassification): AskClassMembers;
|
|
657
|
+
/** The screen a boundary applies before it writes the pair where a host reads it back (the park row): both or
|
|
658
|
+
* neither, and only a well-formed pair — every member of `classes` in the closed set, no repeat, and `askClass` its
|
|
659
|
+
* first member in {@link ASK_CLASSES} order. The copy returned is fresh and frozen, never the caller's array. */
|
|
660
|
+
export declare function screenAskClassMembers(askClass: unknown, classes: unknown): AskClassMembers | undefined;
|
|
631
661
|
/**
|
|
632
662
|
* The DECISION half of the class derivation — what ONE ask says about itself through its own members, read
|
|
633
663
|
* per contributing ask at the policy fold so a member the fold does not copy (a guard mark, the unreadable-rule
|
|
@@ -699,7 +729,7 @@ export type Arbitration = {
|
|
|
699
729
|
classes: readonly AskClass[];
|
|
700
730
|
origin?: AskOrigin;
|
|
701
731
|
mandate?: PersistedRuleMandate;
|
|
702
|
-
reason?:
|
|
732
|
+
reason?: ModeReleaseReason;
|
|
703
733
|
} | {
|
|
704
734
|
kind: "denied_by_mode";
|
|
705
735
|
mode: "bypassPermissions" | "dontAsk";
|
package/dist/core/ask-origin.js
CHANGED
|
@@ -11,15 +11,16 @@ export function askOriginOf(decision, facts) {
|
|
|
11
11
|
return origin;
|
|
12
12
|
}
|
|
13
13
|
const TIGHTEN_ONLY_ORIGINS = new Set(["shell_gate_tighten", "safety_tighten"]);
|
|
14
|
-
export function stampAskOrigin(decision, facts) {
|
|
14
|
+
export function stampAskOrigin(decision, facts, classification) {
|
|
15
15
|
const origin = askOriginOf(decision, facts);
|
|
16
|
+
const classed = askClassMembersOf(classification);
|
|
16
17
|
const { mandate: _selfDeclared, ...unstamped } = decision;
|
|
17
18
|
const clearance = ruleClearanceOf(unstamped, facts.mandateMarks);
|
|
18
19
|
const mandated = { ...unstamped, ...(clearance.mandated ? { mandated: true } : {}), ...(clearance.mandate !== undefined ? { mandate: clearance.mandate } : {}) };
|
|
19
20
|
if (mandated.probeReadRootCandidate === undefined || TIGHTEN_ONLY_ORIGINS.has(origin))
|
|
20
|
-
return { ...mandated, origin };
|
|
21
|
+
return { ...mandated, origin, ...classed };
|
|
21
22
|
const { probeReadRootCandidate: _withdrawn, ...kept } = mandated;
|
|
22
|
-
return { ...kept, origin };
|
|
23
|
+
return { ...kept, origin, ...classed };
|
|
23
24
|
}
|
|
24
25
|
export function ruleClearanceOf(ask, marks) {
|
|
25
26
|
const marksWord = persistedRuleMandateOf({ ...marks, probeMandated: ask?.probeMandated === true });
|
|
@@ -215,6 +216,23 @@ export const MODE_ARBITRATION = {
|
|
|
215
216
|
};
|
|
216
217
|
export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read"]);
|
|
217
218
|
export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
|
|
219
|
+
export const MODE_RELEASE_REASONS = ["rule_unreadable"];
|
|
220
|
+
const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
|
|
221
|
+
export function isModeReleaseReason(v) {
|
|
222
|
+
return MODE_RELEASE_REASON_SET.has(v);
|
|
223
|
+
}
|
|
224
|
+
export function askClassMembersOf(c) {
|
|
225
|
+
return { askClass: c.askClass, classes: Object.freeze(ASK_CLASSES.filter((k) => c.classes.has(k))) };
|
|
226
|
+
}
|
|
227
|
+
export function screenAskClassMembers(askClass, classes) {
|
|
228
|
+
if (!isAskClass(askClass) || !Array.isArray(classes) || classes.length === 0 || !classes.every(isAskClass))
|
|
229
|
+
return undefined;
|
|
230
|
+
const held = new Set(classes);
|
|
231
|
+
const ordered = ASK_CLASSES.filter((k) => held.has(k));
|
|
232
|
+
if (ordered.length !== classes.length || ordered[0] !== askClass)
|
|
233
|
+
return undefined;
|
|
234
|
+
return { askClass, classes: Object.freeze(ordered) };
|
|
235
|
+
}
|
|
218
236
|
export function decisionAskClassesOf(d) {
|
|
219
237
|
const out = new Set();
|
|
220
238
|
if (d.guard === "unverifiable_delete")
|
|
@@ -339,6 +357,7 @@ export function arbitrateByMode(mode, c, disclosure, call) {
|
|
|
339
357
|
return { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
|
|
340
358
|
if (c.row.bypassImmune)
|
|
341
359
|
return { kind: "ask" };
|
|
360
|
+
const reason = [...MODE_RELEASE_REASON_SET].find((r) => has(r));
|
|
342
361
|
return {
|
|
343
362
|
kind: "released",
|
|
344
363
|
mode,
|
|
@@ -346,7 +365,7 @@ export function arbitrateByMode(mode, c, disclosure, call) {
|
|
|
346
365
|
classes,
|
|
347
366
|
...(disclosure?.origin !== undefined ? { origin: disclosure.origin } : {}),
|
|
348
367
|
...(disclosure?.mandate !== undefined ? { mandate: disclosure.mandate } : {}),
|
|
349
|
-
...(
|
|
368
|
+
...(reason !== undefined ? { reason } : {}),
|
|
350
369
|
};
|
|
351
370
|
case "acceptEdits":
|
|
352
371
|
return call?.fileCommand === true && c.classes.size === 1 && c.classes.has("ordinary") ? { kind: "accepted", mode } : { kind: "ask" };
|
|
@@ -279,36 +279,6 @@ export interface AskUserQuestionToolOptions {
|
|
|
279
279
|
* would let a LATER question inherit the claim that an operator approved it. */
|
|
280
280
|
redeemedApprovalQuestionsHash?: string;
|
|
281
281
|
}
|
|
282
|
-
/**
|
|
283
|
-
* Durable ask (design/64 TC-5.4 — ABOVE CC, the disconnected-human case): instead of the headless
|
|
284
|
-
* "proceed with your best judgment" default, an AskUserQuestion **suspends the task durably** and the
|
|
285
|
-
* answer resumes it — built ENTIRELY from existing primitives, no new gate kind.
|
|
286
|
-
*
|
|
287
|
-
* **The park is the fallback, not the mandate.** An adjudicated question parks durably UNLESS this run
|
|
288
|
-
* has a live question face (see {@link isLiveQuestionFace}), in which case the engine routes the question
|
|
289
|
-
* to that face and the answer arrives in-stream — same turn, no checkpoint, no rebuild. The park is what
|
|
290
|
-
* happens when nobody is there: no live face, the face affirming nobody is reachable, or a control-plane
|
|
291
|
-
* `forceDurableGate` that mandates the durable breakpoint regardless. A deployment that wired a live face
|
|
292
|
-
* AND wants every question parked anyway states that with `forceDurableGate` (or by not wiring the face).
|
|
293
|
-
*
|
|
294
|
-
* 1. Run the task with `durableApproval` + a durable `CheckpointStore` (+ durable session/offload
|
|
295
|
-
* stores) and `combinePolicies(createDurableQuestionPolicy(), yourPolicy)`. The policy adjudicates
|
|
296
|
-
* the AskUserQuestion call `ask`; in durable mode that suspends BEFORE the tool executes —
|
|
297
|
-
* `onQuestion` is never called on the suspend side, so mount the tool with
|
|
298
|
-
* {@link QUESTION_AWAITS_RESUME} when no live human is connected.
|
|
299
|
-
* 2. The HITL surface reads the questions from `checkpoint.pendingAction.args.questions` and collects
|
|
300
|
-
* the human's {@link QuestionAnswer} out-of-band (hours/days later, any replica).
|
|
301
|
-
* 3. `runner.resume(token, { gate: "policy_ask", decision: "allow", boundCallId:
|
|
302
|
-
* checkpoint.pendingAction.toolCallId }, { ...taskConfig, onQuestion: async () => answer })` — the resume
|
|
303
|
-
* re-mounts the tool with the answer closure; the pending call executes against it and the model
|
|
304
|
-
* continues with "The user answered: …". `boundCallId` (design/80 D-1) must name the pending call the
|
|
305
|
-
* human answered, or the resume is rejected (`checkpoint.invalid_outcome`).
|
|
306
|
-
*
|
|
307
|
-
* A deployment that wants the CC-style threshold form ("wait N minutes for a live human, then go
|
|
308
|
-
* durable") implements it inside its own policy: return `allow` while a human is connected (the live
|
|
309
|
-
* `onQuestion` answers synchronously) and `ask` when none is — the sync-vs-durable choice is exactly
|
|
310
|
-
* one policy decision.
|
|
311
|
-
*/
|
|
312
282
|
export declare function createDurableQuestionPolicy(): ToolPolicy;
|
|
313
283
|
/**
|
|
314
284
|
* Placeholder {@link OnQuestion} that mounts the AskUserQuestion tool on a worker with NO live human
|
|
@@ -5,7 +5,7 @@ import { defineTool, errorResult } from "./tools.js";
|
|
|
5
5
|
import { delimitUntrusted, inlineUntrusted } from "./untrusted-text.js";
|
|
6
6
|
import { toolFace } from "./tool-catalog-entries.js";
|
|
7
7
|
import { thrownText } from "./thrown-value.js";
|
|
8
|
-
import {
|
|
8
|
+
import { heldBy } from "./tool-name-readings.js";
|
|
9
9
|
export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
|
|
10
10
|
export function classifyQuestionOutcome(outcome) {
|
|
11
11
|
let unavailable;
|
|
@@ -132,10 +132,11 @@ export function validateAskQuestions(questions) {
|
|
|
132
132
|
}
|
|
133
133
|
return undefined;
|
|
134
134
|
}
|
|
135
|
+
const QUESTION_TOOL_NAMES = new Set([ASK_USER_QUESTION_TOOL_NAME]);
|
|
135
136
|
export function createDurableQuestionPolicy() {
|
|
136
137
|
return {
|
|
137
138
|
check(req) {
|
|
138
|
-
if (!
|
|
139
|
+
if (!heldBy("policy.durableQuestion", QUESTION_TOOL_NAMES, req)) {
|
|
139
140
|
return { action: "allow" };
|
|
140
141
|
}
|
|
141
142
|
return { action: "ask", message: "AskUserQuestion: awaiting a human answer (durable)" };
|
|
@@ -834,6 +834,16 @@ export type PendingAction = {
|
|
|
834
834
|
* Written only when it is a member of the set; a pre-existing row reads absent (unreported).
|
|
835
835
|
* Display metadata — the resume belts keep reading the gate's own bit. */
|
|
836
836
|
origin?: import("./ask-origin.js").AskOrigin;
|
|
837
|
+
/** (additive, same precedent) — the PARK twin of `AskRequest.askClass`: the parked ask's class, the first member of
|
|
838
|
+
* {@link classes}, from the same classification the gate arbitrated it by — so a durable approval seat can answer by
|
|
839
|
+
* class exactly as a synchronous `onAsk` does. Written by the park mint only as a well-formed pair with
|
|
840
|
+
* {@link classes} (both or neither). A row parked by an engine before this member existed reads ABSENT
|
|
841
|
+
* (unclassified), and a resume does not fill it in: a seat answering by class treats absence as a class it will
|
|
842
|
+
* not answer. Display / seat metadata — no gate, CAS or resume path reads it. */
|
|
843
|
+
askClass?: import("./ask-origin.js").AskClass;
|
|
844
|
+
/** The PARK twin of `AskRequest.classes`: every class of the parked ask, in `ASK_CLASSES` order
|
|
845
|
+
* (`classes[0] === askClass`). Present exactly when {@link askClass} is. */
|
|
846
|
+
classes?: readonly import("./ask-origin.js").AskClass[];
|
|
837
847
|
/**
|
|
838
848
|
* design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
|
|
839
849
|
* SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
|
|
@@ -868,9 +878,11 @@ export type PendingAction = {
|
|
|
868
878
|
*/
|
|
869
879
|
family?: import("./tool-face.js").ToolFamily;
|
|
870
880
|
pathTarget?: import("./tool-face.js").ToolRosterPathTarget;
|
|
871
|
-
/** The names
|
|
872
|
-
*
|
|
873
|
-
*
|
|
881
|
+
/** The names the parked call answered to, copied from the face the gate judged it under (`ToolCallFace.names`: the
|
|
882
|
+
* model's spelling first, then the wire name and the other aliases that resolve to it; a row minted before the
|
|
883
|
+
* spelling led carries the wire name first) — the pre-CAS recheck judges an edit against rules written on any of
|
|
884
|
+
* them, as the live gate judged the original, and the resume re-mints its face under the spelling while it still
|
|
885
|
+
* resolves to the row's tool. Absent on a row minted before it ⇒ the row's `toolName` alone. */
|
|
874
886
|
names?: readonly string[];
|
|
875
887
|
/**
|
|
876
888
|
* #438 (additive; no checkpoint-version bump — the `reminderMark` precedent: an optional field an
|
|
@@ -1123,7 +1135,7 @@ export interface CheckpointState {
|
|
|
1123
1135
|
* revive the mode where the redeeming deployment denies it, and an `auto` row arms only where a face is
|
|
1124
1136
|
* wired. Absent on a task that declared nothing and inherited nothing (`default`). Rows written before this
|
|
1125
1137
|
* member carried `autoModeRequested` / `approverPosture` / a doctrine `shellGate`; the resume leg migrates
|
|
1126
|
-
* them once, announced (`resume.legacy_mode_seed_migrated`), and the next suspend writes this member. */
|
|
1138
|
+
* them once, announced (`resume.legacy_mode_seed_migrated`), and the next suspend writes this member. The migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys. */
|
|
1127
1139
|
permissionMode?: PermissionMode;
|
|
1128
1140
|
/** Beside a recorded `plan`: the declared word the leg had when a steer or the model's `EnterPlanMode` moved it into
|
|
1129
1141
|
* plan — the mode an approved or edited `plan_review` resumes under ({@link import("./mode-policy.js").modeRequestAfterResume}). */
|
|
@@ -3042,7 +3054,7 @@ export interface LegacyModeSeedMigration {
|
|
|
3042
3054
|
* bypass);
|
|
3043
3055
|
* - `shellGate`: absent ⇒ none; `"always"` ⇒ kept (a live key, not a retired encoding); `"off"` / `"classify"` ⇒
|
|
3044
3056
|
* dropped (no floor); anything else ⇒ refused (`resume.legacy_mode_seed`).
|
|
3045
|
-
* Idempotent: a row the resume leg writes back carries `permissionMode` and the floor only, so a second read migrates
|
|
3046
|
-
*
|
|
3057
|
+
* Idempotent: a row the resume leg writes back carries `permissionMode` and the floor only, so a second read migrates nothing ("once" is per row).
|
|
3058
|
+
* Migration precedes any retired-key door, so a window row is read rather than refused. The window: 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 a row parked before 7.34.0 before upgrading to 7.38.0.
|
|
3047
3059
|
*/
|
|
3048
3060
|
export declare function migrateLegacyModeSeed(gate: object | undefined): LegacyModeSeedMigration;
|
|
@@ -9,6 +9,7 @@ import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-
|
|
|
9
9
|
import { redactSecrets } from "./untrusted-egress.js";
|
|
10
10
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
11
11
|
import { isPermissionMode } from "./ask-origin.js";
|
|
12
|
+
import { wireOf } from "./tool-name-readings.js";
|
|
12
13
|
export function mintCheckpointToken() {
|
|
13
14
|
return randomBytes(16).toString("hex");
|
|
14
15
|
}
|
|
@@ -290,7 +291,7 @@ export function summarizeCheckpoint(cp) {
|
|
|
290
291
|
...(cp.sourceTaskId !== undefined ? { sourceTaskId: cp.sourceTaskId } : {}),
|
|
291
292
|
...(cp.principal !== undefined ? { principal: cp.principal } : {}),
|
|
292
293
|
...(tool !== undefined ? { toolCallId: tool.toolCallId, toolName: tool.toolName } : {}),
|
|
293
|
-
...(tool
|
|
294
|
+
...(tool !== undefined && wireOf("park.contentKindProjection", tool) === ASK_USER_QUESTION_TOOL_NAME ? { contentKind: "content_ask" } : {}),
|
|
294
295
|
...(toolInput !== undefined ? { toolInput } : {}),
|
|
295
296
|
...(cp.state.workspaceHandle !== undefined
|
|
296
297
|
? { restoreMode: cp.state.workspaceHandle.restoreMode === "park_only" ? "park_only" : "snapshot" }
|
|
@@ -171,6 +171,9 @@ export interface ContextEditOptions {
|
|
|
171
171
|
* registry state are compaction's job, not a blanking marker's.
|
|
172
172
|
*/
|
|
173
173
|
compactableTools?: ReadonlySet<string>;
|
|
174
|
+
/** The wire name a result's recorded tool name resolves to on the leg's roster (a result is clearable by the tool the
|
|
175
|
+
* call ran, not the spelling the model used). Absent ⇒ the recorded name. */
|
|
176
|
+
toolNameOf?: (recorded: string) => string;
|
|
174
177
|
/**
|
|
175
178
|
* design/374 (RB-212 durable half): the PRE-CAP counterpart array of `messages`, position-aligned
|
|
176
179
|
* (the pipeline's aggregate caps replace in place, never drop). When present, the machine
|
|
@@ -316,6 +319,7 @@ export declare function planRejectionClears(messages: AgentMessage[], opts: {
|
|
|
316
319
|
keyOf: OccurrenceIndex["keyOf"];
|
|
317
320
|
offload?: ContextEditOptions["offload"];
|
|
318
321
|
compactableTools?: ReadonlySet<string>;
|
|
322
|
+
toolNameOf?: (recorded: string) => string;
|
|
319
323
|
keepRecentToolResults?: number;
|
|
320
324
|
}): RejectionClearPlan;
|
|
321
325
|
/**
|
|
@@ -5,6 +5,7 @@ import { physicalWindowOf } from "./context-settings.js";
|
|
|
5
5
|
import { PERSISTED_OUTPUT_PREFIX, offloadPagebackHint } from "./tool-result-store.js";
|
|
6
6
|
import { assertNoRetiredKeys } from "./retired-keys.js";
|
|
7
7
|
import { compactableToolNames } from "./tool-registry.js";
|
|
8
|
+
import { wireOf } from "./tool-name-readings.js";
|
|
8
9
|
const CC_CLEARED_MARKER = "[Old tool result content cleared]";
|
|
9
10
|
const CC_CLEARED_EXTENDED_PREFIX = "[Old tool result content cleared —";
|
|
10
11
|
const refNote = (ref) => offloadPagebackHint(ref, "cleared");
|
|
@@ -47,6 +48,7 @@ function resolveKeepRecentToolResults(value, defaultValue) {
|
|
|
47
48
|
return Math.max(MIN_KEEP_RECENT_TOOL_RESULTS, value);
|
|
48
49
|
}
|
|
49
50
|
const COMPACTABLE_TOOLS = compactableToolNames();
|
|
51
|
+
const compactableResult = (compactable, recorded, toolNameOf) => compactable.has(wireOf("compaction.compactableResult", { toolName: toolNameOf !== undefined ? toolNameOf(recorded) : recorded }));
|
|
50
52
|
function soleMarkerText(m) {
|
|
51
53
|
const content = m.content;
|
|
52
54
|
if (!Array.isArray(content) || content.length !== 1)
|
|
@@ -127,6 +129,7 @@ const CONTEXT_EDIT_OPTION_KEYS = new Set([
|
|
|
127
129
|
"anchoredTotalTokens",
|
|
128
130
|
"charsPerToken",
|
|
129
131
|
"compactableTools",
|
|
132
|
+
"toolNameOf",
|
|
130
133
|
"clearSource",
|
|
131
134
|
]);
|
|
132
135
|
const RETIRED_CONTEXT_EDIT_OPTION_KEYS = {
|
|
@@ -159,7 +162,7 @@ export function clearStaleToolResults(messages, opts) {
|
|
|
159
162
|
}
|
|
160
163
|
const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS);
|
|
161
164
|
const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
|
|
162
|
-
const candidates = messages.flatMap((m, i) => isToolResult(m) && compactable
|
|
165
|
+
const candidates = messages.flatMap((m, i) => isToolResult(m) && compactableResult(compactable, m.toolName, opts.toolNameOf) ? [{ idx: i, target: m }] : []);
|
|
163
166
|
const beyondKeep = candidates.slice(0, Math.max(0, candidates.length - keep));
|
|
164
167
|
const clearable = beyondKeep.filter(({ target }) => !isCleared(target));
|
|
165
168
|
if (clearable.length === 0) {
|
|
@@ -223,7 +226,7 @@ function occurrenceGroupId(m) {
|
|
|
223
226
|
export function planRejectionClears(messages, opts) {
|
|
224
227
|
const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS);
|
|
225
228
|
const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
|
|
226
|
-
const candidates = messages.flatMap((m) => (isToolResult(m) && compactable
|
|
229
|
+
const candidates = messages.flatMap((m) => (isToolResult(m) && compactableResult(compactable, m.toolName, opts.toolNameOf) ? [m] : []));
|
|
227
230
|
const beyondKeep = candidates.slice(0, Math.max(0, candidates.length - keep));
|
|
228
231
|
const clearable = beyondKeep.flatMap((m) => {
|
|
229
232
|
if (isCleared(m))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PermissionResult } from "./tool-policy.js";
|
|
2
|
+
/** The screen's one refusal — every arm's deny, so an out-of-contract decision is one code whichever arm caught it. */
|
|
3
|
+
export declare const refusedOutOfContract: (message: string) => PermissionResult;
|
|
4
|
+
/** One level of the receipt, the same for the decision and each record it nests. The carrier is an ORDINARY object — it and
|
|
5
|
+
* every object on its prototype chain are looked at for being a Proxy before any descriptor is asked for (a Proxy's traps are
|
|
6
|
+
* the caller's code; the look invokes none of them) — so the receipt runs none of the carrier's code: a contract member is
|
|
7
|
+
* absent or an own enumerable DATA property, its value taken off the descriptor (no read at all). Anything else refuses the
|
|
8
|
+
* carrier, the member named by `path`: an accessor (never invoked), and a member the prototype or a non-enumerable property
|
|
9
|
+
* supplies, unless the nearest descriptor holding it spells absence (an `undefined` data property). Descriptors are looked up
|
|
10
|
+
* as OWN entries of the descriptor map, so a polluted `Object.prototype` names no member. Members outside the contract are
|
|
11
|
+
* not looked at. */
|
|
12
|
+
/** The receipt's own answers carry a `kind` discriminant as an own property: narrowing never asks the prototype chain (`in`) for a
|
|
13
|
+
* word, so a polluted `Object.prototype` cannot answer for the receipt either. */
|
|
14
|
+
export type Received = {
|
|
15
|
+
kind: "held";
|
|
16
|
+
held: Record<string, unknown>;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "refused";
|
|
19
|
+
refused: PermissionResult;
|
|
20
|
+
};
|
|
21
|
+
export declare function receiveContract(carrier: object, members: readonly string[], path: string): Received;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { types } from "node:util";
|
|
2
|
+
import { thrownText } from "./thrown-value.js";
|
|
3
|
+
export const refusedOutOfContract = (message) => ({ action: "deny", message, decisionReason: "rule" });
|
|
4
|
+
const spellsAbsence = (d) => Object.hasOwn(d, "value") && d.value === undefined;
|
|
5
|
+
function ordinaryChain(carrier) {
|
|
6
|
+
const chain = [];
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
try {
|
|
9
|
+
for (let o = carrier; o !== null; o = Object.getPrototypeOf(o)) {
|
|
10
|
+
if (types.isProxy(o))
|
|
11
|
+
return { kind: "not", why: o === carrier ? "is a Proxy" : "has a Proxy on its prototype chain" };
|
|
12
|
+
if (seen.has(o))
|
|
13
|
+
return { kind: "not", why: "has a cyclic prototype chain" };
|
|
14
|
+
seen.add(o);
|
|
15
|
+
chain.push(o);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { kind: "not", why: `threw while its prototype chain was looked at (${thrownText(e)})` };
|
|
20
|
+
}
|
|
21
|
+
return { kind: "chain", chain };
|
|
22
|
+
}
|
|
23
|
+
export function receiveContract(carrier, members, path) {
|
|
24
|
+
const what = path === "" ? "a permission decision" : `a permission decision's "${path.slice(0, -1)}" record`;
|
|
25
|
+
const ordinary = ordinaryChain(carrier);
|
|
26
|
+
if (ordinary.kind === "not") {
|
|
27
|
+
return { kind: "refused", refused: refusedOutOfContract(`${what} ${ordinary.why} — the engine reads a decision off an ordinary object's own property descriptors and runs none of the carrier's code; denied fail-closed`) };
|
|
28
|
+
}
|
|
29
|
+
let own;
|
|
30
|
+
try {
|
|
31
|
+
own = Object.getOwnPropertyDescriptors(carrier);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return { kind: "refused", refused: refusedOutOfContract(`${what} threw while its property descriptors were looked at (${thrownText(e)}) — not an ordinary object; denied fail-closed`) };
|
|
35
|
+
}
|
|
36
|
+
const held = Object.create(null);
|
|
37
|
+
for (const m of members) {
|
|
38
|
+
const d = Object.hasOwn(own, m) ? own[m] : undefined;
|
|
39
|
+
if (d !== undefined && !Object.hasOwn(d, "value")) {
|
|
40
|
+
return { kind: "refused", refused: refusedOutOfContract(`${what}'s contract member "${path}${m}" is held by an accessor — contract members must be own data properties, not values supplied through an accessor; denied fail-closed`) };
|
|
41
|
+
}
|
|
42
|
+
if (d !== undefined && d.enumerable === true) {
|
|
43
|
+
held[m] = d.value;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let reached = d;
|
|
47
|
+
try {
|
|
48
|
+
for (let i = 1; reached === undefined && i < ordinary.chain.length; i++)
|
|
49
|
+
reached = Object.getOwnPropertyDescriptor(ordinary.chain[i], m);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return { kind: "refused", refused: refusedOutOfContract(`${what} threw while "${path}${m}" was looked for on its prototype chain (${thrownText(e)}) — not an ordinary object; denied fail-closed`) };
|
|
53
|
+
}
|
|
54
|
+
if (reached !== undefined && !spellsAbsence(reached)) {
|
|
55
|
+
return {
|
|
56
|
+
kind: "refused",
|
|
57
|
+
refused: refusedOutOfContract(`${what} supplies "${path}${m}" other than as its own enumerable property (its prototype or a non-enumerable property) — ` +
|
|
58
|
+
`contract fields must be own properties, so every reader sees the one value the engine read; denied fail-closed`),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { kind: "held", held };
|
|
63
|
+
}
|
|
@@ -251,6 +251,13 @@ export interface EngineNotice {
|
|
|
251
251
|
* (requested, reported) PAIR — a catalog fact, not a turn fact. Audience `operator`;
|
|
252
252
|
* `detail: { requested, reported, sessionId, runId }`. Absent ⇒ the provider echoed the requested id
|
|
253
253
|
* or named nothing — one reading, not two.
|
|
254
|
+
* - `"route.tool_call_id_folded"` (#1207) — the provider named a tool call with an id outside the shape the
|
|
255
|
+
* engine keys calls under (visible ASCII, 1–128, no `~`), so the brain keyed it under a deterministic fold
|
|
256
|
+
* (`ToolCall.id`) and keeps the provider's spelling beside it (`ToolCall.wireId`), handed back only to the
|
|
257
|
+
* provider. Advisory: the run is unchanged. ONCE PER RUN, on the first folded call — a gateway fact, not a
|
|
258
|
+
* call fact. Audience `operator`; `detail: { toolCallId, rawLength, reason, sessionId, runId }` —
|
|
259
|
+
* `toolCallId` the engine id, `reason` `"charset"` (a control byte, a space, a non-ASCII unit or `~`) or
|
|
260
|
+
* `"length"` (admitted characters, over 128); never the raw bytes. Absent ⇒ no call of the run was folded.
|
|
254
261
|
* - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
|
|
255
262
|
* in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
|
|
256
263
|
* (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
|
|
@@ -330,7 +337,8 @@ export interface EngineNotice {
|
|
|
330
337
|
* `permissionMode` (`autoModeRequested: true` ⇒ `auto`; a posture is dropped — it never authorized anything;
|
|
331
338
|
* `shellGate` `"off"` / `"classify"` are dropped, `"always"` kept) and the next suspend writes the new member.
|
|
332
339
|
* `detail: { migrated: { permissionMode?, droppedKeys }, sessionId, runId }`. Audience `operator`. A row the
|
|
333
|
-
* migration cannot read refuses the resume instead (`resume.legacy_mode_seed`, the error-code row above).
|
|
340
|
+
* migration cannot read refuses the resume instead (`resume.legacy_mode_seed`, the error-code row above). The
|
|
341
|
+
* migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys.
|
|
334
342
|
* - `"config.durable_gate_unavailable"` — a durable-approval MANDATE is in force on a leg with NO
|
|
335
343
|
* checkpoint store: the per-principal `forceDurableGate` entitlement, or the task's own
|
|
336
344
|
* `durableApproval` opt-in with no live approver seat (`durableMandateSourceOf`, the predicate the
|
|
@@ -705,10 +713,12 @@ export interface EngineNotice {
|
|
|
705
713
|
* `detail.settlement` names how: `"delivered"` (a re-judgment or an approval released it into
|
|
706
714
|
* this session's model context), `"refused"` (the setting moved to refuse, or an approval met a
|
|
707
715
|
* refusing policy), `"expired"` (the `dialogExpiry` deadline, an eviction when the 100-entry queue
|
|
708
|
-
* was full, a cancelled review,
|
|
709
|
-
* (the injection lane refused the released frame).
|
|
710
|
-
* (`deadline | evicted | shutdown | rejudge |
|
|
711
|
-
*
|
|
716
|
+
* was full, a cancelled review, a graceful shutdown, or a record another lease holder already settled),
|
|
717
|
+
* `"denied"` (the review face), `"dropped"` (the injection lane refused the released frame).
|
|
718
|
+
* `detail.reason` is the trigger (`PeerHeldSettleReason`: `deadline | evicted | shutdown | rejudge |
|
|
719
|
+
* approved | denied | cancelled | inject_failed | superseded`); the sender was sent the matching receipt
|
|
720
|
+
* whenever the record carried a reply address — except on `superseded`, where the lease holder that
|
|
721
|
+
* settled the record sent it.
|
|
712
722
|
* `detail: { settlement, reason, seq, box, fromSession?, heldCount, sessionId, runId }`. Audience
|
|
713
723
|
* `"user"`: it is this session's user who held (or was holding) the message.
|
|
714
724
|
* - `"peer.idle_subscription"` (design/385 §5.2, slice 4) — a peer session asked to be told when
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -6,11 +6,12 @@ import { traceHookCrash } from "./gate-crash-trace.js";
|
|
|
6
6
|
import { writeProtectionMandated, writeProtectionTightenDecision, writeProtectionUnresolved } from "./write-protect.js";
|
|
7
7
|
import { createGateDeadline, awaitWriteProtectionJudgment, gateWaitFailureOf, withGateTimeout } from "./gate-bounded-await.js";
|
|
8
8
|
import { thrownText } from "./thrown-value.js";
|
|
9
|
+
import { refuseOutOfContractDecision } from "./tool-policy.js";
|
|
9
10
|
export async function runGateFold(pass) {
|
|
10
11
|
const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
|
|
11
12
|
const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
|
|
12
13
|
pass.req = { toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
13
|
-
pass.decision = adjudicate ? await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req)) : { action: "allow" };
|
|
14
|
+
pass.decision = adjudicate ? refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req))) : { action: "allow" };
|
|
14
15
|
{
|
|
15
16
|
const refusedBy = ledger.fromDecision(pass.decision);
|
|
16
17
|
if (refusedBy !== undefined)
|