@sema-agent/core 7.8.0 → 7.9.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 +52 -1
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/core/ask-origin.d.ts +12 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/checkpoint-store.d.ts +34 -28
- package/dist/core/gate-lanes.js +49 -23
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/hooks.d.ts +56 -31
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +254 -55
- package/dist/core/permission-rule-model.js +323 -43
- package/dist/core/permission-rule-org.d.ts +9 -8
- package/dist/core/permission-rule-org.js +6 -15
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +55 -33
- package/dist/core/permission-rules.js +65 -55
- package/dist/core/persisted-rule-arms.d.ts +56 -0
- package/dist/core/persisted-rule-arms.js +48 -0
- package/dist/core/runner/contracts.d.ts +21 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
- package/dist/core/runner/permission-rule-lanes.js +27 -21
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-safety-scan.js +8 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +85 -734
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +5 -5
- package/dist/core/runner/tool-output-projection.d.ts +9 -6
- package/dist/core/runner/tool-output-projection.js +1 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/tool-roster.d.ts +9 -0
- package/dist/core/tool-roster.js +15 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +43 -11
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { persistedRuleMandateOf } from "../hooks.js";
|
|
2
|
-
import {
|
|
2
|
+
import { adjudicatePersistedPathRules, adjudicatePersistedRules, lexicalNormalAbsolutePathOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
3
|
+
import { declaredPathTargetOf, pathTargetValue, protectivePathTargetOf } from "../tool-registry.js";
|
|
3
4
|
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
4
5
|
export const PERSISTED_RULE_TOOL = "Bash";
|
|
5
|
-
export const DIRECTORY_RULE_TOOL = "Read";
|
|
6
6
|
const HANDOFF_BOUND = 256;
|
|
7
7
|
export function inheritedAskRuleEvidence(provider) {
|
|
8
8
|
const org = provider?.partitions.org === true ? "not_adjudicated" : "not_wired";
|
|
@@ -19,21 +19,26 @@ export function orgRevisionEvidenceOf(resolution, onDefect) {
|
|
|
19
19
|
`the adjudication stands, but the ask carries no revision evidence for this call`);
|
|
20
20
|
return {};
|
|
21
21
|
}
|
|
22
|
-
export function persistedRuleHitOf(
|
|
23
|
-
return
|
|
22
|
+
export function persistedRuleHitOf(verdict) {
|
|
23
|
+
return verdict === undefined
|
|
24
24
|
? undefined
|
|
25
|
-
: { rules:
|
|
25
|
+
: { behavior: verdict.behavior, rules: verdict.rules.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
|
|
26
26
|
}
|
|
27
|
-
export function
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
export function pathRuleLaneAnswer(table, req, ctx) {
|
|
28
|
+
const base = (ctx.liveCwd ?? ctx.root ?? "").replace(/\/+$/, "");
|
|
29
|
+
const resolve = (pt) => {
|
|
30
|
+
if (pt === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const spelled = pathTargetValue(req.args, pt);
|
|
33
|
+
if (spelled === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
return lexicalNormalAbsolutePathOf(spelled.startsWith("/") ? spelled : `${base}/${spelled}`);
|
|
36
|
+
};
|
|
37
|
+
const tighten = resolve(protectivePathTargetOf(req));
|
|
38
|
+
const allow = resolve(declaredPathTargetOf(req));
|
|
39
|
+
if (tighten === undefined && allow === undefined)
|
|
34
40
|
return undefined;
|
|
35
|
-
|
|
36
|
-
return hit !== undefined ? persistedRuleHitOf([hit]) : undefined;
|
|
41
|
+
return persistedRuleHitOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}) }));
|
|
37
42
|
}
|
|
38
43
|
export function createPermissionRuleLanes(cfg) {
|
|
39
44
|
const provider = cfg.provider;
|
|
@@ -103,10 +108,11 @@ export function createPermissionRuleLanes(cfg) {
|
|
|
103
108
|
: undefined;
|
|
104
109
|
const personal = provider.partitions.durable
|
|
105
110
|
? {
|
|
106
|
-
|
|
111
|
+
adjudicate: async (req) => {
|
|
107
112
|
if (anonymous && !cfg.localOwnerDeclared)
|
|
108
113
|
return undefined;
|
|
109
|
-
|
|
114
|
+
const grammar = ruleToolGrammarOf(req.toolName);
|
|
115
|
+
if (grammar === undefined)
|
|
110
116
|
return undefined;
|
|
111
117
|
let view;
|
|
112
118
|
try {
|
|
@@ -122,16 +128,16 @@ export function createPermissionRuleLanes(cfg) {
|
|
|
122
128
|
}
|
|
123
129
|
const table = view.rules;
|
|
124
130
|
const liveCwd = cfg.liveCwd();
|
|
125
|
-
if (
|
|
126
|
-
return
|
|
131
|
+
if (grammar === "path") {
|
|
132
|
+
return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd });
|
|
127
133
|
}
|
|
128
134
|
const command = req.args?.command;
|
|
129
135
|
if (typeof command !== "string")
|
|
130
136
|
return undefined;
|
|
131
137
|
const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
return persistedRuleHitOf(
|
|
138
|
+
const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
|
|
139
|
+
if (verdict !== undefined)
|
|
140
|
+
return persistedRuleHitOf(verdict);
|
|
135
141
|
const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
|
|
136
142
|
return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
|
|
137
143
|
},
|
|
@@ -123,7 +123,7 @@ export function prepareGateStations(input) {
|
|
|
123
123
|
...(permissionRuleLane
|
|
124
124
|
? {
|
|
125
125
|
persistedRules: {
|
|
126
|
-
|
|
126
|
+
adjudicate: permissionRuleLane.adjudicate,
|
|
127
127
|
onResolved: (info) => emitTrace(deps.tracer, () => ({
|
|
128
128
|
kind: "permission.persisted_rule_allowed",
|
|
129
129
|
version: 2,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NAMESPACED_NAME_SHAPES } from "../protocol-table.js";
|
|
2
|
-
import { handBandEffects } from "../tool-registry.js";
|
|
2
|
+
import { engineCardTypes, handBandEffects } from "../tool-registry.js";
|
|
3
3
|
import { TOOL_CONTRACT_MAX_CHARS, TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js";
|
|
4
|
-
import { schemaKeyBoundProblem } from "../tool-roster.js";
|
|
4
|
+
import { callerCardIdOf, offendingResultCard, schemaKeyBoundProblem } from "../tool-roster.js";
|
|
5
5
|
import { toolFaceProblem } from "./tool-face-overlay.js";
|
|
6
6
|
import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
|
|
7
7
|
import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
|
|
@@ -52,6 +52,12 @@ export function prepareSafetyScan(input) {
|
|
|
52
52
|
e.code = "config.tool_face_invalid";
|
|
53
53
|
throw e;
|
|
54
54
|
}
|
|
55
|
+
const foreignCard = offendingResultCard(t.renderHints, callerCardIdOf(t), engineCardTypes());
|
|
56
|
+
if (foreignCard !== undefined) {
|
|
57
|
+
const e = new Error(`Tool "${t.name}" declares a result card outside its own namespace: ${JSON.stringify(foreignCard)} (a caller card is "${callerCardIdOf(t)}/<card>"; engine cards and other contracts' namespaces are reserved).`);
|
|
58
|
+
e.code = "config.tool_face_invalid";
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
55
61
|
}
|
|
56
62
|
if (t.name.includes("__")) {
|
|
57
63
|
const e = new Error(`Tool name "${t.name}" is invalid: "__" is reserved for the engine's protocol tool namespaces (${NAMESPACED_NAME_SHAPES}) and must not appear in a caller tool name.`);
|
|
@@ -686,7 +686,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
686
686
|
const preparedHolder = {};
|
|
687
687
|
const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveReadDenyObserved } = prepareTurnWiring({ deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, readDenyAdditionsNormalized, preparedHolder });
|
|
688
688
|
await onceLedger.settle(session, announcedListingsRef);
|
|
689
|
-
const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
689
|
+
const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
690
690
|
rollback.commit();
|
|
691
691
|
const prepared = buildPrepared();
|
|
692
692
|
preparedHolder.current = prepared;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/393 S3 — the resume ladder's ADMISSION rung (E1) and the wake's shaping (E2), verbatim from
|
|
3
|
+
* `Runner.resumeStream`: the outcome container's domain, the one-read snapshots of the caller's config and
|
|
4
|
+
* the trusted internals bag, the store resolution, the row read and its terminal-status confirm, the
|
|
5
|
+
* same-process parent-constraint re-supply, then the wake pre-ladder (gate purity, pendingAction purity,
|
|
6
|
+
* the message's persist-time validation) and the one read of the outcome discriminant and the signal slot.
|
|
7
|
+
*
|
|
8
|
+
* Async (two store reads); every refusal throws a typed `CheckpointError` pre-CAS. It ends by handing what it
|
|
9
|
+
* admitted to the rung after it (`next`) in its LAST continuation — no suspension point between the
|
|
10
|
+
* registry read and the wake pre-ladder's first read of the row — and returns what that rung returns.
|
|
11
|
+
* The same seat shape the tool gate's fold uses to enter its lanes (gate-fold.ts `afterFold`).
|
|
12
|
+
*/
|
|
13
|
+
import { type Checkpoint, type CheckpointStore, type CheckpointToken, type PendingSteerEntry, type ResumeOutcome } from "../checkpoint-store.js";
|
|
14
|
+
import type { InheritedGate, ResumeTaskConfig, RunInternals, RunnerDepsSeat } from "./contracts.js";
|
|
15
|
+
/** What the admission rung was called with. */
|
|
16
|
+
export interface ResumeAdmissionInput<R> {
|
|
17
|
+
/** borrowed-readonly — the checkpoint token the caller is redeeming. */
|
|
18
|
+
token: CheckpointToken;
|
|
19
|
+
/** borrowed-readonly — the caller's outcome object, read exactly where the driver read it (the container test,
|
|
20
|
+
* the discriminant once, the wake message once); never stored, never handed on as itself. */
|
|
21
|
+
outcome: ResumeOutcome;
|
|
22
|
+
/** borrowed-readonly — the caller's config bag; snapshotted ONCE here (own enumerable fields), the snapshot is what
|
|
23
|
+
* every later rung and the resumed leg read. */
|
|
24
|
+
taskConfig: ResumeTaskConfig;
|
|
25
|
+
/** borrowed-readonly — the trusted run-internals bag as the caller supplied it; shallow-snapshotted here, the
|
|
26
|
+
* snapshot (with the parked chain re-supplied when the registry holds it) is what the ladder reads. */
|
|
27
|
+
internals: RunInternals | undefined;
|
|
28
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE through the Runner (the store resolution reads `checkpointStore`); the one home of why it is a
|
|
29
|
+
* seat and not a captured object is {@link RunnerDepsSeat}. */
|
|
30
|
+
runner: RunnerDepsSeat;
|
|
31
|
+
/** borrowed-readonly — the Runner's same-process parked-constraint registry; read for this token (never written here). */
|
|
32
|
+
parentConstraintRegistry: ReadonlyMap<CheckpointToken, NonNullable<InheritedGate["parentConstraints"]>>;
|
|
33
|
+
/** borrowed-readonly — the rung after admission, entered in this rung's last continuation with what it admitted. */
|
|
34
|
+
next: (admitted: ResumeAdmissionResult) => Promise<R>;
|
|
35
|
+
}
|
|
36
|
+
/** What admission established — the constants every later rung reads. */
|
|
37
|
+
export interface ResumeAdmissionResult {
|
|
38
|
+
/** The caller's config, snapshotted once (own enumerable fields). */
|
|
39
|
+
config: ResumeTaskConfig;
|
|
40
|
+
/** The trusted internals snapshot, with the parked parent chain re-supplied when this Runner spawned the leg. */
|
|
41
|
+
internals: RunInternals | undefined;
|
|
42
|
+
/** The checkpoint store the row lives in. */
|
|
43
|
+
store: CheckpointStore;
|
|
44
|
+
/** The pending row, read once; a non-pending snapshot was refused before this result existed. */
|
|
45
|
+
cp: Checkpoint;
|
|
46
|
+
/** The ONE read of the outcome's discriminant. */
|
|
47
|
+
outcomeGate: ResumeOutcome["gate"];
|
|
48
|
+
/** The ONE read of the config's signal slot (`.aborted` stays a live read on this object). */
|
|
49
|
+
resumeSignal: AbortSignal | undefined;
|
|
50
|
+
/** The validated wake message (wake outcomes that carried one), never the caller's object. */
|
|
51
|
+
wakeMessage: Omit<PendingSteerEntry, "seq"> | undefined;
|
|
52
|
+
}
|
|
53
|
+
export declare function resumeAdmission<R>(input: ResumeAdmissionInput<R>): Promise<R>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { CheckpointError, readPendingSteerQueue, resolveCheckpointStore, validatePendingSteer } from "../checkpoint-store.js";
|
|
2
|
+
import { describeSuppliedValue } from "./decide-continuation.js";
|
|
3
|
+
export async function resumeAdmission(input) {
|
|
4
|
+
const { token, outcome, taskConfig, runner, parentConstraintRegistry } = input;
|
|
5
|
+
let internals = input.internals;
|
|
6
|
+
if (outcome === null || typeof outcome !== "object") {
|
|
7
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
|
|
8
|
+
}
|
|
9
|
+
const config = typeof taskConfig === "object" && taskConfig !== null ? { ...taskConfig } : taskConfig;
|
|
10
|
+
internals = internals !== undefined ? { ...internals } : undefined;
|
|
11
|
+
const store = resolveCheckpointStore(config, runner.deps);
|
|
12
|
+
if (!store) {
|
|
13
|
+
throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
|
|
14
|
+
}
|
|
15
|
+
let cp = await store.get(token);
|
|
16
|
+
if (!cp) {
|
|
17
|
+
throw new CheckpointError("checkpoint.not_found", "no checkpoint found for the supplied token");
|
|
18
|
+
}
|
|
19
|
+
if (cp.status !== "pending") {
|
|
20
|
+
const live = await store.get(token);
|
|
21
|
+
if (live?.status === "pending") {
|
|
22
|
+
throw new CheckpointError("checkpoint.reopened_concurrently", "checkpoint changed concurrently (a resolve/reopen cycle landed between this resume's read of the row and its state check) — nothing was validated or executed; re-resume against the current state");
|
|
23
|
+
}
|
|
24
|
+
throw new CheckpointError("checkpoint.already_resolved", `checkpoint is ${describeSuppliedValue(live?.status ?? cp.status)} — the token is consumed, so this resume was neither validated nor executed (idempotent); a different outcome cannot redeem it`);
|
|
25
|
+
}
|
|
26
|
+
if ((internals?.inheritedGate?.parentConstraints?.length ?? 0) === 0) {
|
|
27
|
+
const parked = parentConstraintRegistry.get(token);
|
|
28
|
+
if (parked !== undefined) {
|
|
29
|
+
internals = {
|
|
30
|
+
...(internals ?? {}),
|
|
31
|
+
inheritedGate: { ...(internals?.inheritedGate ?? {}), parentConstraints: parked },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let wakeMessage;
|
|
36
|
+
const outcomeGate = outcome.gate;
|
|
37
|
+
const resumeSignal = config.signal;
|
|
38
|
+
let suppliedMessage;
|
|
39
|
+
if (outcomeGate === "wake") {
|
|
40
|
+
suppliedMessage = outcome.message;
|
|
41
|
+
const gk = cp.gate.kind;
|
|
42
|
+
const decideEntry = gk === "human" || gk === "irreversible_ask"
|
|
43
|
+
? 'a `policy_ask` outcome (decision allow/deny bound to the pending tool call)'
|
|
44
|
+
: gk === "needs_review"
|
|
45
|
+
? 'a `dry_run_review` outcome (decision approve/reject)'
|
|
46
|
+
: gk === "plan_review"
|
|
47
|
+
? 'a `plan_review` outcome (decision approve/edit/reject)'
|
|
48
|
+
: gk === "resource_limit"
|
|
49
|
+
? 'a `resource_limit` outcome (decision "continue")'
|
|
50
|
+
: undefined;
|
|
51
|
+
if (gk !== "task_done") {
|
|
52
|
+
throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint awaits a pending gate decision (gate "${gk}") — wake is not a decision ` +
|
|
53
|
+
`channel; resume it with ${decideEntry ?? "its own gate outcome"} instead`);
|
|
54
|
+
}
|
|
55
|
+
const pa = cp.pendingAction;
|
|
56
|
+
switch (pa.kind) {
|
|
57
|
+
case "task_done":
|
|
58
|
+
break;
|
|
59
|
+
case "tool_approval":
|
|
60
|
+
case "resource_limit":
|
|
61
|
+
case "review":
|
|
62
|
+
case "plan_review": {
|
|
63
|
+
throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint still carries a pending gate-typed action (pendingAction "${pa.kind}" ` +
|
|
64
|
+
`on a "${gk}" gate — inconsistent row) — wake is not a decision channel; resolve it through its ` +
|
|
65
|
+
`own decide entry instead`);
|
|
66
|
+
}
|
|
67
|
+
default: {
|
|
68
|
+
const _exhaustive = pa;
|
|
69
|
+
void _exhaustive;
|
|
70
|
+
throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint carries an unrecognized pendingAction kind "${pa.kind}" ` +
|
|
71
|
+
`(newer-version row?) — fail-closed: wake only serves a pure park (pendingAction "task_done")`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (suppliedMessage !== undefined) {
|
|
75
|
+
wakeMessage = validatePendingSteer(suppliedMessage);
|
|
76
|
+
}
|
|
77
|
+
else if (readPendingSteerQueue(cp.state).length === 0) {
|
|
78
|
+
throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
|
|
79
|
+
"wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return await input.next({ config, internals, store, cp, outcomeGate, resumeSignal, wakeMessage });
|
|
83
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Checkpoint, type PendingSteerEntry, type ResumeOutcome } from "../checkpoint-store.js";
|
|
2
|
+
import type { ResumeRun, ResumeTaskConfig, RunInternals, RunnerDepsSeat } from "./contracts.js";
|
|
3
|
+
export interface ResumeApplyInput<R> {
|
|
4
|
+
/** borrowed-readonly — the pending row (session id, pending call, inherited chain, ledger, hands cwd). */
|
|
5
|
+
cp: Checkpoint;
|
|
6
|
+
/** borrowed-readonly — the config snapshot (`hooks`, `taskId`, `signal`, the policy seats). */
|
|
7
|
+
config: ResumeTaskConfig;
|
|
8
|
+
/** borrowed-readonly — the internals snapshot (delegation identity facts for the screen's envelope). */
|
|
9
|
+
internals: RunInternals | undefined;
|
|
10
|
+
/** borrowed-readonly — the ONE read of the outcome discriminant. */
|
|
11
|
+
outcomeGate: ResumeOutcome["gate"];
|
|
12
|
+
/** borrowed-readonly — the validated wake message the screen judges. */
|
|
13
|
+
wakeMessage: Omit<PendingSteerEntry, "seq"> | undefined;
|
|
14
|
+
/** borrowed-readonly — the ONE signal read from admission; the screen and the edit legs are bounded by it. */
|
|
15
|
+
resumeSignal: AbortSignal | undefined;
|
|
16
|
+
/** borrowed-readonly — the window verdict to re-assert after the screen's await (undefined when unarmed). */
|
|
17
|
+
recheckGovernanceWindow: (() => Promise<void>) | undefined;
|
|
18
|
+
/** borrowed-readonly — the approval lane's twin (its edit is what the rechecks judge; the settlement is minted onto a copy). */
|
|
19
|
+
plainPolicyOutcome: Extract<ResumeOutcome, {
|
|
20
|
+
gate: "policy_ask";
|
|
21
|
+
}> | undefined;
|
|
22
|
+
/** borrowed-readonly — the review lane's twin. */
|
|
23
|
+
plainReviewOutcome: Extract<ResumeOutcome, {
|
|
24
|
+
gate: "plan_review" | "dry_run_review";
|
|
25
|
+
}> | undefined;
|
|
26
|
+
/** borrowed-readonly — the tool-less park lanes' twin. */
|
|
27
|
+
plainParkOutcome: Extract<ResumeOutcome, {
|
|
28
|
+
gate: "wake" | "resource_limit";
|
|
29
|
+
}> | undefined;
|
|
30
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE through the Runner (reads `hooks`, `onError`, `lockedConfig`, `basePolicyForResumeEdit`, `toolPolicy` — the last three AFTER this rung's awaits); the one home of why it is a
|
|
31
|
+
* seat and not a captured object is {@link RunnerDepsSeat}. */
|
|
32
|
+
runner: RunnerDepsSeat;
|
|
33
|
+
/** borrowed-readonly — the orchestrator's delegation-facts fold (a lane may not name the orchestrator; the driver hands it in). */
|
|
34
|
+
effectiveDelegationFacts: (internals: Pick<RunInternals, "isDelegatedChild" | "insideFork"> | undefined, seedIsDelegatedChild: boolean | undefined) => {
|
|
35
|
+
isDelegatedChild: boolean;
|
|
36
|
+
};
|
|
37
|
+
/** borrowed-readonly — the rung after apply, entered in this rung's last continuation. */
|
|
38
|
+
next: (applied: ResumeApplyResult) => Promise<R>;
|
|
39
|
+
}
|
|
40
|
+
export interface ResumeApplyResult {
|
|
41
|
+
/** The screen's `additionalContext` for the wake message, when a screen ran and supplied one. */
|
|
42
|
+
wakeHookContext: string | undefined;
|
|
43
|
+
/** The approval lane's twin WITH its minted settlement record (undefined on every other lane). */
|
|
44
|
+
plainPolicyOutcome: Extract<ResumeOutcome, {
|
|
45
|
+
gate: "policy_ask";
|
|
46
|
+
}> | undefined;
|
|
47
|
+
/** The twin the store persists and the resumed leg reads — exactly one lane captured it. */
|
|
48
|
+
outcomeForStore: ResumeRun["outcome"];
|
|
49
|
+
}
|
|
50
|
+
export declare function resumeApply<R>(input: ResumeApplyInput<R>): Promise<R>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
2
|
+
import { isAskOrigin } from "../ask-origin.js";
|
|
3
|
+
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
|
|
4
|
+
import { CheckpointError, remainingBudgetMicroUsd } from "../checkpoint-store.js";
|
|
5
|
+
import { cloneObserverInput, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
6
|
+
import { checkToolPolicyProjection, refuseOutOfContractDecision } from "../tool-policy.js";
|
|
7
|
+
import { callFaceSeat, parkedCallFaceOf } from "../tool-roster.js";
|
|
8
|
+
import { inlineUntrusted } from "../untrusted-text.js";
|
|
9
|
+
import { deepJsonEqual, describeSuppliedValue } from "./decide-continuation.js";
|
|
10
|
+
import { mintGateOutcome } from "./gate-exit.js";
|
|
11
|
+
export async function resumeApply(input) {
|
|
12
|
+
const { cp, config, internals, outcomeGate, wakeMessage, resumeSignal, recheckGovernanceWindow, plainReviewOutcome, plainParkOutcome, runner, effectiveDelegationFacts } = input;
|
|
13
|
+
let plainPolicyOutcome = input.plainPolicyOutcome;
|
|
14
|
+
let wakeHookContext;
|
|
15
|
+
if (outcomeGate === "wake" && wakeMessage !== undefined) {
|
|
16
|
+
const resumeScreenHooks = config.hooks ?? runner.deps.hooks;
|
|
17
|
+
const screen = resumeScreenHooks?.userPromptSubmit;
|
|
18
|
+
if (screen !== undefined && resumeSignal?.aborted !== true) {
|
|
19
|
+
const screenedMessage = wakeMessage;
|
|
20
|
+
const identity = mintHookInvocationIdentity({
|
|
21
|
+
sessionId: cp.sessionId,
|
|
22
|
+
taskId: config.taskId ?? cp.sessionId,
|
|
23
|
+
legKind: "resume",
|
|
24
|
+
isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
|
|
25
|
+
...(internals?.insideFork === true ? { insideFork: true } : {}),
|
|
26
|
+
...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
|
|
27
|
+
...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
|
|
28
|
+
});
|
|
29
|
+
const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
|
|
30
|
+
try {
|
|
31
|
+
runner.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
}
|
|
35
|
+
}, resumeScreenHooks);
|
|
36
|
+
let decision;
|
|
37
|
+
try {
|
|
38
|
+
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
|
|
39
|
+
identity,
|
|
40
|
+
signal: sig,
|
|
41
|
+
source: "resume_message",
|
|
42
|
+
...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
|
|
43
|
+
...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
|
|
44
|
+
}));
|
|
45
|
+
if (seat.expired) {
|
|
46
|
+
throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
|
|
47
|
+
? `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`
|
|
48
|
+
: `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
|
|
49
|
+
}
|
|
50
|
+
decision = seat.value;
|
|
51
|
+
}
|
|
52
|
+
catch (hookErr) {
|
|
53
|
+
if (hookErr instanceof CheckpointError)
|
|
54
|
+
throw hookErr;
|
|
55
|
+
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
56
|
+
try {
|
|
57
|
+
runner.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
}
|
|
61
|
+
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook crashed while screening this wake message (${inlineUntrusted(err.message)}); the resume was refused (fail-closed) and the checkpoint stays pending`);
|
|
62
|
+
}
|
|
63
|
+
if (decision?.block !== undefined && decision.block !== "") {
|
|
64
|
+
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
|
|
65
|
+
}
|
|
66
|
+
if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
|
|
67
|
+
wakeHookContext = decision.additionalContext;
|
|
68
|
+
}
|
|
69
|
+
if (recheckGovernanceWindow !== undefined)
|
|
70
|
+
await recheckGovernanceWindow();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (plainPolicyOutcome !== undefined &&
|
|
74
|
+
plainPolicyOutcome.decision === "allow" &&
|
|
75
|
+
plainPolicyOutcome.updatedInput !== undefined &&
|
|
76
|
+
cp.pendingAction.kind === "tool_approval" &&
|
|
77
|
+
cp.pendingAction.toolName !== ASK_USER_QUESTION_TOOL_NAME) {
|
|
78
|
+
const editedArgs = plainPolicyOutcome.updatedInput;
|
|
79
|
+
const editBudget = {
|
|
80
|
+
resourceRemainingMicroUsd: remainingBudgetMicroUsd(cp.resourceLedger),
|
|
81
|
+
resourceSpentMicroUsd: cp.resourceLedger?.spentMicroUsd ?? 0,
|
|
82
|
+
suspendCount: cp.suspendCount ?? 0,
|
|
83
|
+
};
|
|
84
|
+
const editedReq = {
|
|
85
|
+
toolName: cp.pendingAction.toolName,
|
|
86
|
+
args: editedArgs,
|
|
87
|
+
toolCallId: cp.pendingAction.toolCallId,
|
|
88
|
+
budget: editBudget,
|
|
89
|
+
...callFaceSeat(parkedCallFaceOf(cp.pendingAction)),
|
|
90
|
+
...(cp.state.handsCwd !== undefined ? { cwd: cp.state.handsCwd } : {}),
|
|
91
|
+
};
|
|
92
|
+
const editSignal = config.signal;
|
|
93
|
+
const raceEditAbort = async (p) => {
|
|
94
|
+
if (editSignal === undefined)
|
|
95
|
+
return p;
|
|
96
|
+
if (editSignal.aborted) {
|
|
97
|
+
throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while re-adjudicating the approver's edit — the checkpoint stays pending and is resumable with a live signal");
|
|
98
|
+
}
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const onAbort = () => reject(new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while re-adjudicating the approver's edit — the checkpoint stays pending and is resumable with a live signal"));
|
|
101
|
+
editSignal.addEventListener("abort", onAbort, { once: true });
|
|
102
|
+
p.then((v) => { editSignal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { editSignal.removeEventListener("abort", onAbort); reject(e); });
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const ig = cp.state.inheritedGate;
|
|
106
|
+
if (ig?.requiresParentConstraint === true) {
|
|
107
|
+
const chain = ig.constraintChain;
|
|
108
|
+
const unprojectable = chain === undefined
|
|
109
|
+
? "the checkpoint predates the frozen projection chain (no constraintChain recorded)"
|
|
110
|
+
: chain.some((e) => "opaque" in e)
|
|
111
|
+
? "the inherited chain carries an OPAQUE layer (no projection exported)"
|
|
112
|
+
: chain.some((e) => !("opaque" in e) && e.requiresLiveRemainder)
|
|
113
|
+
? "an inherited chain layer declares a live remainder beyond its projection"
|
|
114
|
+
: undefined;
|
|
115
|
+
if (unprojectable !== undefined) {
|
|
116
|
+
throw new CheckpointError("resume.constraint_unprojectable", `the approver EDITED this call's input, but the inherited ancestor chain cannot re-adjudicate the edit statically: ${unprojectable}. ` +
|
|
117
|
+
"Refusing the edited resume pre-CAS (the checkpoint stays pending and remains decidable) — approve the shown input as-is, deny it, " +
|
|
118
|
+
"or re-issue the edited action as a fresh call so the full live chain adjudicates it");
|
|
119
|
+
}
|
|
120
|
+
for (const entry of chain) {
|
|
121
|
+
const denied = checkToolPolicyProjection(entry, editedReq);
|
|
122
|
+
if (denied !== undefined) {
|
|
123
|
+
throw new CheckpointError("resume.constraint_rejected", `the approver EDITED this call's input and a FROZEN inherited ancestor constraint denies the edited args${denied.message ? `: ${denied.message}` : ""}. ` +
|
|
124
|
+
"Refused pre-CAS (fresh-redecision semantics: the checkpoint stays pending and the same token remains decidable — approve as shown, deny, or edit differently)");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const toolPolicyLocked = runner.deps.lockedConfig?.keys?.includes("toolPolicy") === true;
|
|
129
|
+
const editBasePolicy = toolPolicyLocked
|
|
130
|
+
? (runner.deps.basePolicyForResumeEdit ?? runner.deps.toolPolicy)
|
|
131
|
+
: (config.basePolicyForResumeEdit ?? runner.deps.basePolicyForResumeEdit ?? config.toolPolicy ?? runner.deps.toolPolicy);
|
|
132
|
+
if (editBasePolicy !== undefined) {
|
|
133
|
+
let rechecked;
|
|
134
|
+
try {
|
|
135
|
+
rechecked = refuseOutOfContractDecision(await raceEditAbort(Promise.resolve(editBasePolicy.check(editedReq, editSignal))));
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err instanceof CheckpointError)
|
|
139
|
+
throw err;
|
|
140
|
+
rechecked = { action: "deny", message: `the resume-edit policy re-check errored: ${err instanceof Error ? err.message : String(err)}` };
|
|
141
|
+
}
|
|
142
|
+
if (rechecked.action === "deny") {
|
|
143
|
+
throw new CheckpointError("resume.constraint_rejected", `the approver EDITED this call's input and the deployment's resume-edit policy denies the edited args${rechecked.message ? `: ${rechecked.message}` : ""}. ` +
|
|
144
|
+
"Refused pre-CAS (the checkpoint stays pending and remains decidable)");
|
|
145
|
+
}
|
|
146
|
+
if (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, editedArgs)) {
|
|
147
|
+
throw new CheckpointError("resume.constraint_rejected", `the deployment's resume-edit policy REWROTE the approver's edited args to a different value than was approved — the durable resume cannot execute either the un-adjudicated edit or an unapproved rewrite; ` +
|
|
148
|
+
`refused pre-CAS (the checkpoint stays pending); re-submit the edited action so the rewrite is adjudicated and approved synchronously`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const resumeHooks = config.hooks ?? runner.deps.hooks;
|
|
152
|
+
if (resumeHooks?.preToolUse !== undefined && resumeHooks.preToolUseObservational === true) {
|
|
153
|
+
try {
|
|
154
|
+
await raceEditAbort(Promise.resolve(resumeHooks.preToolUse(cp.pendingAction.toolName, cloneObserverInput(editedArgs), {
|
|
155
|
+
toolCallId: cp.pendingAction.toolCallId,
|
|
156
|
+
toolName: cp.pendingAction.toolName,
|
|
157
|
+
})));
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
if (err instanceof CheckpointError)
|
|
161
|
+
throw err;
|
|
162
|
+
throw new CheckpointError("resume.constraint_rejected", `the declared-observational PreToolUse face threw while observing the approver's edited args (${err instanceof Error ? err.message : String(err)}) — ` +
|
|
163
|
+
"refused fail-closed pre-CAS (the checkpoint stays pending and remains decidable)");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (plainPolicyOutcome !== undefined) {
|
|
168
|
+
const parkedOrigin = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.origin : undefined;
|
|
169
|
+
if (!isAskOrigin(parkedOrigin)) {
|
|
170
|
+
throw new CheckpointError("checkpoint.unsupported_version", "this checkpoint's pending approval carries no origin word (a row minted before the ask's origin was persisted on the park) — its settlement cannot be recorded; the checkpoint stays pending", { reason: "origin_missing" });
|
|
171
|
+
}
|
|
172
|
+
const facts = plainPolicyOutcome.hostDecision;
|
|
173
|
+
const approverCell = facts.approver !== undefined ? { approver: facts.approver } : {};
|
|
174
|
+
const settlement = facts.decidedBy === "sla_timeout"
|
|
175
|
+
? { kind: "park_sla_expired", who: { party: "host", ...approverCell }, when: Date.now() }
|
|
176
|
+
: { kind: plainPolicyOutcome.decision === "allow" ? "human_allowed" : "human_refused", who: { party: "person", ...approverCell }, when: Date.now(), ...(plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason !== undefined ? { note: plainPolicyOutcome.reason } : {}) };
|
|
177
|
+
plainPolicyOutcome = { ...plainPolicyOutcome, gateOutcome: mintGateOutcome({ ...(plainPolicyOutcome.decision === "deny" ? { deniedBy: "ask_resolution" } : {}), settled: { settlement, origin: parkedOrigin } }) };
|
|
178
|
+
}
|
|
179
|
+
const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
|
|
180
|
+
if (outcomeForStore === undefined) {
|
|
181
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") matched the checkpoint gate but no lane captured it — refusing pre-CAS rather than passing the caller's live object to the store and the resumed run`);
|
|
182
|
+
}
|
|
183
|
+
return await input.next({ wakeHookContext, plainPolicyOutcome, outcomeForStore });
|
|
184
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Checkpoint } from "../checkpoint-store.js";
|
|
2
|
+
import type { RunInternals, RunnerDepsSeat } from "./contracts.js";
|
|
3
|
+
export interface ResumeCheckpointScreenInput {
|
|
4
|
+
/** borrowed-readonly — the pending row; every invariant is judged on its persisted fields. */
|
|
5
|
+
cp: Checkpoint;
|
|
6
|
+
/** borrowed-readonly — the ONE signal read from admission; `.aborted` is read live here. */
|
|
7
|
+
resumeSignal: AbortSignal | undefined;
|
|
8
|
+
/** borrowed-readonly — the internals snapshot (the re-supplied chain, the requested cwd). */
|
|
9
|
+
internals: RunInternals | undefined;
|
|
10
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE through the Runner (reads `permissionRuleStore` and `executionEnvFactory`); the one home of why it is a
|
|
11
|
+
* seat and not a captured object is {@link RunnerDepsSeat}. */
|
|
12
|
+
runner: RunnerDepsSeat;
|
|
13
|
+
}
|
|
14
|
+
export interface ResumeCheckpointScreenResult {
|
|
15
|
+
/** The internals the resumed leg is handed: the validated chain frozen at the array level (entries by reference). */
|
|
16
|
+
resumeInternals: RunInternals | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare function resumeCheckpointScreen(input: ResumeCheckpointScreenInput): ResumeCheckpointScreenResult;
|