@sema-agent/core 7.4.0 → 7.5.1
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 +69 -0
- package/dist/agents/cascade.d.ts +2 -1
- package/dist/agents/peer-notices.d.ts +11 -1
- package/dist/agents/peer-session-drain.js +2 -0
- package/dist/agents/verify.d.ts +2 -1
- package/dist/core/ask-origin.d.ts +130 -0
- package/dist/core/ask-origin.js +35 -0
- package/dist/core/auto-mode-arming.d.ts +40 -1
- package/dist/core/auto-mode-arming.js +51 -3
- package/dist/core/auto-mode.d.ts +87 -10
- package/dist/core/auto-mode.js +34 -6
- package/dist/core/checkpoint-store.d.ts +41 -6
- package/dist/core/checkpoint-store.js +8 -0
- package/dist/core/hooks.d.ts +63 -19
- package/dist/core/hooks.js +37 -16
- package/dist/core/mcp.d.ts +47 -55
- package/dist/core/mcp.js +89 -31
- package/dist/core/park-selfcheck.js +3 -0
- package/dist/core/permission-rule-consent.d.ts +2 -11
- package/dist/core/permission-rule-consent.js +13 -62
- package/dist/core/permission-rule-org.d.ts +70 -54
- package/dist/core/permission-rule-org.js +47 -61
- package/dist/core/permission-rule-provider.d.ts +162 -0
- package/dist/core/permission-rule-provider.js +148 -0
- package/dist/core/permission-rule-session.d.ts +23 -19
- package/dist/core/permission-rule-session.js +5 -5
- package/dist/core/permission-rule-store.d.ts +46 -21
- package/dist/core/permission-rule-store.js +13 -6
- package/dist/core/permission-rule-sync.d.ts +2 -1
- package/dist/core/permission-rule-sync.js +11 -0
- package/dist/core/runner/assemble-result.d.ts +3 -2
- package/dist/core/runner/checkpoint-scope.d.ts +32 -0
- package/dist/core/runner/checkpoint-scope.js +4 -0
- package/dist/core/runner/contracts.d.ts +1878 -0
- package/dist/core/runner/contracts.js +1 -0
- package/dist/core/runner/denial-limit-arms.d.ts +57 -31
- package/dist/core/runner/denial-limit-arms.js +42 -17
- package/dist/core/runner/derived-route-fallback.d.ts +34 -0
- package/dist/core/runner/derived-route-fallback.js +16 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
- package/dist/core/runner/prepare-announce-once.d.ts +83 -0
- package/dist/core/runner/prepare-announce-once.js +105 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
- package/dist/core/runner/prepare-config-doors.d.ts +2 -10
- package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
- package/dist/core/runner/prepare-defer-classify.js +107 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
- package/dist/core/runner/prepare-delegation-surface.js +144 -0
- package/dist/core/runner/prepare-execution-env.d.ts +54 -0
- package/dist/core/runner/prepare-execution-env.js +86 -0
- package/dist/core/runner/prepare-file-history.d.ts +95 -0
- package/dist/core/runner/prepare-file-history.js +383 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
- package/dist/core/runner/prepare-hands-readface.js +3 -3
- package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
- package/dist/core/runner/prepare-inherited-gate.js +266 -0
- package/dist/core/runner/prepare-listings.d.ts +77 -0
- package/dist/core/runner/prepare-listings.js +76 -0
- package/dist/core/runner/prepare-lsp.d.ts +55 -0
- package/dist/core/runner/prepare-lsp.js +27 -0
- package/dist/core/runner/prepare-memory.d.ts +1 -1
- package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
- package/dist/core/runner/prepare-offload-wrappers.js +45 -0
- package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
- package/dist/core/runner/prepare-permission-rules.js +140 -0
- package/dist/core/runner/prepare-project-context.d.ts +131 -0
- package/dist/core/runner/prepare-project-context.js +150 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
- package/dist/core/runner/prepare-prompt-inputs.js +141 -0
- package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
- package/dist/core/runner/prepare-protocol-tools.js +182 -0
- package/dist/core/runner/prepare-question-face.d.ts +119 -0
- package/dist/core/runner/prepare-question-face.js +83 -0
- package/dist/core/runner/prepare-run-refs.d.ts +89 -0
- package/dist/core/runner/prepare-run-refs.js +39 -0
- package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
- package/dist/core/runner/prepare-task.d.ts +11 -1815
- package/dist/core/runner/prepare-task.js +138 -2542
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
- package/dist/core/runner/prepare-wiring-manifest.js +240 -0
- package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
- package/dist/core/runner/prepare-workspace-restore.js +1 -22
- package/dist/core/runner/rollback-stack.d.ts +32 -0
- package/dist/core/runner/rollback-stack.js +30 -0
- package/dist/core/runner/runtask.d.ts +11 -2
- package/dist/core/runner/runtask.js +27 -9
- package/dist/core/runner/workspace-path.d.ts +33 -0
- package/dist/core/runner/workspace-path.js +22 -0
- package/dist/core/sensitive-path-policy.d.ts +16 -0
- package/dist/core/sensitive-path-policy.js +1 -1
- package/dist/core/tool-policy.d.ts +57 -9
- package/dist/core/tool-policy.js +11 -0
- package/dist/core/types.d.ts +63 -51
- package/dist/core/wiring-manifest.d.ts +40 -3
- package/dist/core/wiring-manifest.js +4 -3
- package/dist/core/write-protect.d.ts +13 -2
- package/dist/core/write-protect.js +58 -29
- package/dist/engine/harness/types.d.ts +38 -16
- package/dist/engine/harness/types.js +25 -1
- package/dist/engine/session/session.d.ts +3 -11
- package/dist/index.d.ts +10 -6
- package/dist/index.js +9 -5
- package/dist/internal/harness.d.ts +1 -0
- package/dist/stores/file/adoption/adopt.d.ts +1 -1
- package/dist/stores/file/adoption/marker.d.ts +1 -1
- package/dist/stores/file/permission-rule-adopt.js +4 -3
- package/dist/stores/file/permission-rule-store.d.ts +65 -25
- package/dist/stores/file/permission-rule-store.js +215 -37
- package/dist/stores/file/task-list-store.d.ts +1 -1
- package/dist/tools/fs/read-face.d.ts +1 -1
- package/package.json +8 -2
- package/test/export-surface.snapshot.json +76 -28
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { bindPeerLaneDrain } from "../../agents/peer-session-drain.js";
|
|
2
|
+
import { HAND_TOOL_EFFECTS } from "../../tools/fs/index.js";
|
|
3
|
+
import { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
4
|
+
import { createHookEnvCapabilities, mintHookInvocationIdentity } from "../hooks.js";
|
|
5
|
+
import { isRemoteExecutionEnv, missingRestoreSurface } from "../remote-env.js";
|
|
6
|
+
import { isVolatileOffloadStore } from "../tool-result-store.js";
|
|
7
|
+
import { isLiveApproverSeat } from "../tool-policy.js";
|
|
8
|
+
import { deliverEngineNotice } from "../types.js";
|
|
9
|
+
import { inlineUntrusted } from "../untrusted-text.js";
|
|
10
|
+
import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
|
|
11
|
+
import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
|
|
12
|
+
import { DEFAULT_IRREVERSIBLE_SCOPE } from "./checkpoint-scope.js";
|
|
13
|
+
export function mcpManifestEntries(entries, statuses) {
|
|
14
|
+
return (entries ?? []).map((entry) => {
|
|
15
|
+
const status = statuses.find((s) => s.name === entry.name);
|
|
16
|
+
const source = typeof entry.source === "string" && entry.source.length > 0 ? { source: inlineUntrusted(entry.source, 80) } : {};
|
|
17
|
+
if (status === undefined)
|
|
18
|
+
return { name: entry.name, ...source, status: "skipped", toolCount: 0 };
|
|
19
|
+
if (status.status === "connected")
|
|
20
|
+
return { name: entry.name, ...source, status: "connected", toolCount: status.toolNames?.length ?? 0 };
|
|
21
|
+
return {
|
|
22
|
+
name: entry.name,
|
|
23
|
+
...source,
|
|
24
|
+
status: "failed",
|
|
25
|
+
...(status.errorCode !== undefined ? { errorCode: status.errorCode } : {}),
|
|
26
|
+
...(status.error !== undefined ? { error: inlineUntrusted(status.error, 240) } : {}),
|
|
27
|
+
toolCount: 0,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function buildUsageGovernance(windows, deps, principal, sessionId) {
|
|
32
|
+
if (windows === undefined || windows.length === 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
const store = deps.usageWindowStore;
|
|
35
|
+
if (store === undefined) {
|
|
36
|
+
deps.onError?.(new Error("RunnerDeps.usageWindows is set but INACTIVE: no `usageWindowStore` is wired, so cross-task usage cannot be counted and no window can be enforced. Wire InMemoryUsageWindowStore (process-local) or FileUsageWindowStore (restart-surviving)."), { phase: "config", sessionId });
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const key = principal || GLOBAL_USAGE_KEY;
|
|
40
|
+
const governsCost = windowsGovernCost(windows);
|
|
41
|
+
let chargedTokens = 0;
|
|
42
|
+
let chargedCostMicroUsd = 0;
|
|
43
|
+
let announcedCostGap = false;
|
|
44
|
+
let recordedUnpricedGap = false;
|
|
45
|
+
return {
|
|
46
|
+
key,
|
|
47
|
+
governsCost,
|
|
48
|
+
async check(now) {
|
|
49
|
+
const readings = await store.read(key, windows, now);
|
|
50
|
+
if (!announcedCostGap && readings.some((r) => r.costUnknown === true)) {
|
|
51
|
+
announcedCostGap = true;
|
|
52
|
+
deps.onError?.(new Error(`the usage window for ledger key ${JSON.stringify(key)} holds spend that nothing could price, so its maxCostUsd ceiling is being evaluated against a LOWER BOUND until that charge ages out of the window. Price every model this deployment can reach (including degrade targets and the compaction model).`), { phase: "config", sessionId });
|
|
53
|
+
}
|
|
54
|
+
return usageRetryAfterMs(readings, windows);
|
|
55
|
+
},
|
|
56
|
+
async commit(cumulativeTokens, cumulativeCostMicroUsd, now) {
|
|
57
|
+
if (!governsCost) {
|
|
58
|
+
const delta = cumulativeTokens - chargedTokens;
|
|
59
|
+
if (delta <= 0)
|
|
60
|
+
return;
|
|
61
|
+
await store.charge(key, delta, now, windows);
|
|
62
|
+
chargedTokens = cumulativeTokens;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (cumulativeCostMicroUsd === undefined) {
|
|
66
|
+
const tokenDelta = cumulativeTokens - chargedTokens;
|
|
67
|
+
if (tokenDelta > 0 || !recordedUnpricedGap) {
|
|
68
|
+
await store.charge(key, Math.max(0, tokenDelta), now, windows, null);
|
|
69
|
+
chargedTokens = Math.max(chargedTokens, cumulativeTokens);
|
|
70
|
+
recordedUnpricedGap = true;
|
|
71
|
+
}
|
|
72
|
+
const e = new Error("a deployment usage window declares maxCostUsd, but this run's spend has NO cost figure (RB-368 unpriced: a model served without a RunnerDeps.pricing entry or a Model.cost declaration). Refused rather than charged a fabricated 0 — the money ceiling would have silently stopped applying. Price every model this run can reach, or drop maxCostUsd from the window.");
|
|
73
|
+
e.code = "config.usage_window_unpriced";
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
const tokenDelta = cumulativeTokens - chargedTokens;
|
|
77
|
+
const costDelta = cumulativeCostMicroUsd - chargedCostMicroUsd;
|
|
78
|
+
if (tokenDelta <= 0 && costDelta <= 0)
|
|
79
|
+
return;
|
|
80
|
+
await store.charge(key, Math.max(0, tokenDelta), now, windows, Math.max(0, costDelta));
|
|
81
|
+
chargedTokens = cumulativeTokens;
|
|
82
|
+
chargedCostMicroUsd = cumulativeCostMicroUsd;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const ungatedWarnedShapes = new WeakMap();
|
|
87
|
+
function announceDurableGateUnavailable(args) {
|
|
88
|
+
if (!args.forceDurableGate || args.storeWired)
|
|
89
|
+
return;
|
|
90
|
+
const cause = args.taskStoreNull ? "task_store_null" : "no_deployment_store";
|
|
91
|
+
const inheritedContentMandate = (args.parentConstraints ?? []).some((pc) => pc.contentMandate === true);
|
|
92
|
+
const liveQuestionFace = args.liveQuestionFace && !inheritedContentMandate;
|
|
93
|
+
deliverEngineNotice(args.onNotice, {
|
|
94
|
+
code: "config.durable_gate_unavailable",
|
|
95
|
+
message: `Durable approval gate mandated but unavailable: the runtime entitlement forceDurableGate is in force for this ` +
|
|
96
|
+
`principal, but ${cause === "task_store_null" ? "this task disabled the checkpoint store (checkpointStore: null)" : "this deployment wired no checkpoint store"}, ` +
|
|
97
|
+
`so no interactive ask of this task can be parked for a durable answer. Asks resolve on the live chain instead ` +
|
|
98
|
+
`(${args.liveApprover ? "a live approver answers permission asks in-stream" : "permission asks are denied fail-closed — no live approver is wired"}; ` +
|
|
99
|
+
`${liveQuestionFace ? "a live question face answers AskUserQuestion in-stream" : args.liveQuestionFace ? "AskUserQuestion is withheld from the live face by an inherited durable mandate and refused" : "AskUserQuestion has no live face and is refused"}) ` +
|
|
100
|
+
`and NO durable approval record is written. ${cause === "task_store_null" ? "Run this task with its store, or withdraw the entitlement for principals whose runs are machine-started." : "Wire a checkpoint store on this deployment, or withdraw the entitlement for principals served here."}`,
|
|
101
|
+
detail: {
|
|
102
|
+
sessionId: args.sessionId,
|
|
103
|
+
runId: args.runId,
|
|
104
|
+
...(args.principal ? { principal: args.principal } : {}),
|
|
105
|
+
cause,
|
|
106
|
+
liveApprover: args.liveApprover,
|
|
107
|
+
liveQuestionFace,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
export function prepareWiringManifest(input) {
|
|
112
|
+
const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, handsEnabled, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, lockedPreflight, peerLaneRefs, peerLaneActive, harness, suspendRef, reviewRef, frozenOnAsk, liveInheritedGate, delegation } = input;
|
|
113
|
+
const onAsk = spec.onAsk ?? deps.onAsk;
|
|
114
|
+
const handWriteTools = handsEnabled && spec.handsReadOnly !== true
|
|
115
|
+
? Object.keys(HAND_TOOL_EFFECTS).filter((name) => {
|
|
116
|
+
const eff = HAND_TOOL_EFFECTS[name];
|
|
117
|
+
return eff === "write" || eff === "idempotent";
|
|
118
|
+
})
|
|
119
|
+
: [];
|
|
120
|
+
const hasEffectAwareGate = Boolean(policyLayers.length > 0 || (hooks?.preToolUse !== undefined && !preToolUseObservational));
|
|
121
|
+
const destructiveMcpUngated = mcp.tools.some((t) => !irreversibleTools.has(t.name) && !egressTools.has(t.name) && (toolEffects.get(t.name) ?? "write") !== "read");
|
|
122
|
+
const firstPartyWriteUngated = (spec.tools ?? [])
|
|
123
|
+
.map((t) => t.name)
|
|
124
|
+
.filter((name) => !irreversibleTools.has(name) && !egressTools.has(name) && toolEffects.get(name) === "write");
|
|
125
|
+
if (!hasEffectAwareGate && (handWriteTools.length > 0 || destructiveMcpUngated || firstPartyWriteUngated.length > 0)) {
|
|
126
|
+
const what = [
|
|
127
|
+
handWriteTools.length > 0 ? `hand tools [${handWriteTools.join(", ")}]` : undefined,
|
|
128
|
+
destructiveMcpUngated ? "MCP write tools" : undefined,
|
|
129
|
+
firstPartyWriteUngated.length > 0 ? `write tools [${firstPartyWriteUngated.join(", ")}]` : undefined,
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(" + ");
|
|
133
|
+
if (!ungatedWarnedShapes.get(deps)?.has(what)) {
|
|
134
|
+
let shapes = ungatedWarnedShapes.get(deps);
|
|
135
|
+
if (!shapes) {
|
|
136
|
+
shapes = new Set();
|
|
137
|
+
ungatedWarnedShapes.set(deps, shapes);
|
|
138
|
+
}
|
|
139
|
+
shapes.add(what);
|
|
140
|
+
deps.onError?.(new Error(`write-capable ${what} are present but UNGATED — wire an effect-aware tool policy (or a PreToolUse hook). ` +
|
|
141
|
+
`bash/MCP irreversible actions are otherwise unadjudicated; the deployment sandbox/egress boundary is the only protection (design/77 §4, design/53 §2.H). ` +
|
|
142
|
+
`(This warning fires once per deployment per tool-set shape.)`), { phase: "config", sessionId });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const preToolContexts = new Map();
|
|
146
|
+
const blockedToolCalls = new Set();
|
|
147
|
+
const approvalSettlement = new Map();
|
|
148
|
+
const humanBareRejections = new Set();
|
|
149
|
+
const batchHaltRef = {};
|
|
150
|
+
const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
|
|
151
|
+
const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
|
|
152
|
+
const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
|
|
153
|
+
const durableSuspendInfraReady = resolveCheckpointStore(spec, deps) !== undefined &&
|
|
154
|
+
!(offloadStore !== undefined && isVolatileOffloadStore(offloadStore)) &&
|
|
155
|
+
(ownedEnv === undefined || isRemoteExecutionEnv(ownedEnv)) &&
|
|
156
|
+
incompleteSuspendAdapter === undefined;
|
|
157
|
+
const resourceSuspendEligible = spec.resourceSuspend !== undefined && durableSuspendInfraReady;
|
|
158
|
+
if (spec.resourceSuspend !== undefined && !resourceSuspendEligible) {
|
|
159
|
+
const why = resolveCheckpointStore(spec, deps) === undefined
|
|
160
|
+
? "no CheckpointStore is wired"
|
|
161
|
+
: offloadStore !== undefined && isVolatileOffloadStore(offloadStore)
|
|
162
|
+
? "tool-result offload uses the in-memory store (a resume needs durable results)"
|
|
163
|
+
: incompleteSuspendAdapter !== undefined
|
|
164
|
+
? `the per-task execution env declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")} (a snapshot nothing can restore is worse than no snapshot)`
|
|
165
|
+
: "the per-task execution env is not a RemoteExecutionEnv (it would be destroyed on suspend)";
|
|
166
|
+
onceLedger.onError(new Error(`resourceSuspend is set but INACTIVE: ${why}; resource limits will hard-fail, not suspend`), {
|
|
167
|
+
phase: "config",
|
|
168
|
+
sessionId,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const usageGovernance = buildUsageGovernance(usageWindows, deps, spec.principal, sessionId);
|
|
172
|
+
const platformSuspendArmed = durableSuspendInfraReady && (envLifetimeSuspendAt !== undefined || usageGovernance !== undefined);
|
|
173
|
+
const durableApproval = spec.durableApproval ??
|
|
174
|
+
(runtimeCaps?.forceDurableGate ? { scope: spec.principal || DEFAULT_IRREVERSIBLE_SCOPE } : undefined);
|
|
175
|
+
const wiringQuestionSeam = resolveQuestionSeam(spec, deps);
|
|
176
|
+
const wiringQuestionStripped = internals?.questionFaceStripped === true;
|
|
177
|
+
const wiringAskSeam = resolveAskSeamForm(spec, deps);
|
|
178
|
+
const wiringManifest = deriveWiringManifest({
|
|
179
|
+
half: "effective",
|
|
180
|
+
leg: resume !== undefined ? "resume" : internals?.isDelegatedChild === true ? "child" : "root",
|
|
181
|
+
askForm: wiringAskSeam.form,
|
|
182
|
+
...(wiringAskSeam.provenance !== undefined ? { askProvenance: wiringAskSeam.provenance } : {}),
|
|
183
|
+
questionWired: wiringQuestionSeam.wired,
|
|
184
|
+
...(wiringQuestionSeam.provenance !== undefined ? { questionProvenance: wiringQuestionSeam.provenance } : {}),
|
|
185
|
+
...(wiringQuestionStripped ? { questionStrippedByEngine: true } : {}),
|
|
186
|
+
...(resolvedInteractionPosture !== undefined ? { interactionPosture: resolvedInteractionPosture } : {}),
|
|
187
|
+
...(spec.interactiveTools === true && !wiringQuestionSeam.wired && !durableQuestionFace && !wiringQuestionStripped
|
|
188
|
+
? { interactiveToolsWithoutDeliveryFace: true }
|
|
189
|
+
: {}),
|
|
190
|
+
elicitSeamWired: resolveElicitSeam(deps),
|
|
191
|
+
elicitServersOptedIn: countElicitOptIns(spec),
|
|
192
|
+
parkCapable: checkpointStore !== undefined,
|
|
193
|
+
parkDurableApprovalOptIn: spec.durableApproval !== undefined,
|
|
194
|
+
parkForceDurableGate: runtimeCaps?.forceDurableGate === true,
|
|
195
|
+
parkSafetyVocabularyArmed: irreversibleTools.size > 0 || egressTools.size > 0,
|
|
196
|
+
...(checkpointStore !== undefined ? { checkpointDurability: resolveDeclaredDurability(checkpointStore, "checkpointStore") } : {}),
|
|
197
|
+
sessionDurability: resolveDeclaredDurability(sessions, "sessionStore"),
|
|
198
|
+
backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
|
|
199
|
+
subagentTranscriptTier: resolveSubagentTranscriptTier(deps.backgroundAgentStore !== undefined, sessions),
|
|
200
|
+
permissionRuleStoreWired: deps.permissionRuleStore?.partitions.durable === true,
|
|
201
|
+
permissionRuleSyncWired: deps.permissionRuleSyncWired === true,
|
|
202
|
+
permissionRuleOrgGoverned: deps.permissionRuleStore?.partitions.org === true,
|
|
203
|
+
hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
|
|
204
|
+
lockedConfigWired: deps.lockedConfig !== undefined,
|
|
205
|
+
complianceWired: deps.compliancePostureResolver !== undefined,
|
|
206
|
+
memoryAdmissionWired: deps.memoryScopeAdmission !== undefined,
|
|
207
|
+
retentionPolicyWired: deps.retentionPolicy !== undefined,
|
|
208
|
+
...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
|
|
209
|
+
autoMode: { armed: autoModeArmReason === "armed", reason: autoModeArmReason },
|
|
210
|
+
mcp: mcpManifestEntries(lockedPreflight.mcp, mcp.statuses),
|
|
211
|
+
});
|
|
212
|
+
peerLaneRefs.askEffective = wiringManifest.ask.effective;
|
|
213
|
+
if (peerLaneActive && internals?.onTaskNotification !== undefined) {
|
|
214
|
+
bindPeerLaneDrain(harness, {
|
|
215
|
+
deps,
|
|
216
|
+
sessionId,
|
|
217
|
+
runId,
|
|
218
|
+
scope: taskScope,
|
|
219
|
+
inject: internals.onTaskNotification,
|
|
220
|
+
ownTokens: () => internals?.peerSelfRef?.current.ownTokens ?? [],
|
|
221
|
+
refs: peerLaneRefs,
|
|
222
|
+
selfName: () => internals?.explicitAgentName,
|
|
223
|
+
parked: () => suspendRef.token !== undefined || reviewRef.token !== undefined,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const parkLaneArmed = wiringManifest.parkLane.effective === true;
|
|
227
|
+
announceDurableGateUnavailable({ onNotice: deps.onNotice, forceDurableGate: runtimeCaps?.forceDurableGate === true, storeWired: checkpointStore !== undefined, taskStoreNull: spec.checkpointStore === null, liveApprover: isLiveApproverSeat(frozenOnAsk), liveQuestionFace: liveQuestionFace !== undefined, parentConstraints: liveInheritedGate?.parentConstraints, sessionId, runId, principal: spec.principal });
|
|
228
|
+
const hookIdentity = mintHookInvocationIdentity({
|
|
229
|
+
sessionId,
|
|
230
|
+
taskId: hostTaskId,
|
|
231
|
+
legKind: wiringManifest.leg.kind,
|
|
232
|
+
isDelegatedChild: delegation.isDelegatedChild,
|
|
233
|
+
...(internals?.insideFork === true ? { insideFork: true } : {}),
|
|
234
|
+
...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
|
|
235
|
+
...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
|
|
236
|
+
});
|
|
237
|
+
const hookContextConsumerWired = hooks?.preToolUse !== undefined || hooks?.postToolUse !== undefined || hooks?.postToolUseFailure !== undefined;
|
|
238
|
+
const hookEnvFace = hookContextConsumerWired && (ownedEnv ?? deps.executionEnv) != null ? createHookEnvCapabilities(executionEnv) : undefined;
|
|
239
|
+
return { onAsk, preToolContexts, blockedToolCalls, approvalSettlement, humanBareRejections, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, platformSuspendArmed, durableApproval, wiringManifest, parkLaneArmed, hookIdentity, hookEnvFace };
|
|
240
|
+
}
|
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import type { RemoteExecutionEnv, RemoteExecutionError, SnapshotId, VmLifecycleOptions } from "../remote-env.js";
|
|
2
2
|
import type { ExecutionEnv } from "../../internal/harness.js";
|
|
3
3
|
import type { RemoteEnvFailureNote, RunnerDeps, TaskSpec } from "../types.js";
|
|
4
|
-
import type { PrepareResume, RunInternals } from "./
|
|
5
|
-
/**
|
|
6
|
-
* rebase one checkpointed absolute path from the OLD workspace root onto the RESTORED
|
|
7
|
-
* one (divergent `resumeVM`). POSIX-ONLY by contract: every remote lane's `mountPath` is a container
|
|
8
|
-
* path (e2b/k8s/ssh/adb/local-docker are all Linux targets), so a backslash ANYWHERE in the inputs marks
|
|
9
|
-
* the value outside this function's domain and it returns `p` UNCHANGED — an un-rebased path is honestly
|
|
10
|
-
* observable (the divergence observation already fired) while a WRONGLY-rebased one silently corrupts
|
|
11
|
-
* cwd/read-state (r4: drive-root and cross-family recomposition are not implementable without a Windows
|
|
12
|
-
* path model no lane needs). Trailing slashes are tolerated (from="/app/" must not weld the suffix);
|
|
13
|
-
* bare "/" keeps filesystem-root semantics; a path outside `from` (incl. the prefix-sibling
|
|
14
|
-
* "/application" vs "/app") returns unchanged. Exported for direct unit pinning.
|
|
15
|
-
*/
|
|
16
|
-
export declare function rebaseWorkspacePath(p: string, fromRaw: string, toRaw: string): string;
|
|
4
|
+
import type { PrepareResume, RunInternals } from "./contracts.js";
|
|
17
5
|
/**
|
|
18
6
|
* RB-439-a — project one failed remote lifecycle call onto the caller-facing note. ONE builder for all
|
|
19
7
|
* three ops so the `retryable` classification can never drift between the lane that reports a `suspendVM`
|
|
@@ -41,20 +29,6 @@ onAttempt?: (attempt: number) => void): Promise<{
|
|
|
41
29
|
outcome: Awaited<ReturnType<RemoteExecutionEnv["resumeVM"]>>;
|
|
42
30
|
attempts: number;
|
|
43
31
|
}>;
|
|
44
|
-
/**
|
|
45
|
-
* RB-439-c — {@link rebaseWorkspacePath} over a SET of accepted spellings of the old root: the first
|
|
46
|
-
* prefix that actually matches wins, and a path under none of them is returned unchanged.
|
|
47
|
-
*
|
|
48
|
-
* The set exists because "the old root" has no single spelling. `WorkspaceHandle.mountPath` is whatever the
|
|
49
|
-
* adapter called the mount, while the persisted paths being migrated were spelled by whoever produced them
|
|
50
|
-
* (a `cd` the env canonicalized, a Read key resolved through realpath). On a target where the root has an
|
|
51
|
-
* equivalent alias (`/var` ↔ `/private/var`) those disagree while naming the same directory, and a
|
|
52
|
-
* single-prefix rebase then matched nothing and silently left the resumed shell in the pre-suspend tree —
|
|
53
|
-
* under a disclosure that said the task follows the restored root. Order is caller-chosen (the checkpointed
|
|
54
|
-
* spelling first, its canonical form second) and only matters if one prefix is a prefix of another, in
|
|
55
|
-
* which case the earlier — more specific — spelling is the intended one. Exported for direct unit pinning.
|
|
56
|
-
*/
|
|
57
|
-
export declare function rebaseWorkspacePathAcross(p: string, froms: readonly string[], to: string): string;
|
|
58
32
|
export interface PrepareWorkspaceRestoreInput {
|
|
59
33
|
/** borrowed-readonly (the ownership-ref PROTOCOL stays with the driver — D-8 案①): the env this
|
|
60
34
|
* task owns, when the factory minted one. This phase reads and drives it (`resumeVM`,
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import { isRemoteExecutionEnv, isRetryableRemoteErrorCode, missingRestoreSurface, RETRYABLE_REMOTE_ERROR_CODES } from "../remote-env.js";
|
|
2
2
|
import { withRetry } from "../with-retry.js";
|
|
3
|
-
|
|
4
|
-
if (p.includes("\\") || fromRaw.includes("\\") || toRaw.includes("\\"))
|
|
5
|
-
return p;
|
|
6
|
-
const stripTrail = (s) => (s.length > 1 && s.endsWith("/") ? stripTrail(s.slice(0, -1)) : s);
|
|
7
|
-
const from = stripTrail(fromRaw);
|
|
8
|
-
const to = stripTrail(toRaw);
|
|
9
|
-
if (p === from || stripTrail(p) === from)
|
|
10
|
-
return to;
|
|
11
|
-
const fromPrefix = from === "/" ? from : `${from}/`;
|
|
12
|
-
if (!p.startsWith(fromPrefix))
|
|
13
|
-
return p;
|
|
14
|
-
const suffix = p.slice(fromPrefix.length).replace(/^\/+/, "");
|
|
15
|
-
return to === "/" ? `${to}${suffix}` : `${to}/${suffix}`;
|
|
16
|
-
}
|
|
3
|
+
import { rebaseWorkspacePathAcross } from "./workspace-path.js";
|
|
17
4
|
export function remoteEnvFailureNote(op, error, attempts) {
|
|
18
5
|
return { op, code: error.code, retryable: isRetryableRemoteErrorCode(error.code), attempts, message: error.message };
|
|
19
6
|
}
|
|
@@ -28,14 +15,6 @@ export async function restoreWorkspaceWithRetry(env, snapshotId, options, onAtte
|
|
|
28
15
|
}, { retryableCodes: RETRYABLE_REMOTE_ERROR_CODES, maxAttempts: REMOTE_RESTORE_MAX_ATTEMPTS, backoffMs: () => REMOTE_RESTORE_BACKOFF_MS }, { ...(options.abortSignal !== undefined ? { signal: options.abortSignal } : {}) });
|
|
29
16
|
return { outcome, attempts };
|
|
30
17
|
}
|
|
31
|
-
export function rebaseWorkspacePathAcross(p, froms, to) {
|
|
32
|
-
for (const from of froms) {
|
|
33
|
-
const out = rebaseWorkspacePath(p, from, to);
|
|
34
|
-
if (out !== p)
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
return p;
|
|
38
|
-
}
|
|
39
18
|
export async function prepareWorkspaceRestore(input) {
|
|
40
19
|
const { ownedEnv, deps, spec, internals, resume, abortController, sessionId } = input;
|
|
41
20
|
let taskRootPath = input.taskRootInitial;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prepare-FAILURE rollback stack — the implementation of {@link RollbackStack} (declared beside the
|
|
3
|
+
* other prepare contracts in ./contracts.ts, whose JSDoc carries the contract this file keeps).
|
|
4
|
+
*
|
|
5
|
+
* Why a stack and not a `finally`: prepare acquires resources in sequence (an execution env, MCP
|
|
6
|
+
* clients, A2A peers) and every one of them belongs to the RUN once prepare succeeds — the Runner's
|
|
7
|
+
* finish/teardown releases them. Only a prepare that THROWS must release what it already took, in
|
|
8
|
+
* reverse acquisition order. Today the orchestrator sees those handles because they are hoisted
|
|
9
|
+
* `let`s at function scope; a phase module that acquires a handle inside its own body has no such
|
|
10
|
+
* hoist, so it registers the cleanup HERE, immediately after the acquisition, and the driver unwinds
|
|
11
|
+
* the whole stack from its one catch.
|
|
12
|
+
*
|
|
13
|
+
* Posture (identical to the teardown leg the catch already uses — {@link settleTeardownLeg}):
|
|
14
|
+
* · reverse order, each cleanup exactly once — the entry is removed before it runs, so a second
|
|
15
|
+
* `unwindAll` (or a re-entrant one) finds nothing;
|
|
16
|
+
* · no cleanup can stop another — a throw is routed to `report`, a hang is BOUNDED and reported as a
|
|
17
|
+
* `TeardownLegTimeoutError` naming the leg; the unwind proceeds either way;
|
|
18
|
+
* · `unwindAll` NEVER throws — it runs inside the driver's catch, where a throw would replace the
|
|
19
|
+
* prepare error the caller is owed.
|
|
20
|
+
*
|
|
21
|
+
* States: `armed` (accepting pushes) → `committed` (prepare succeeded; the stack is emptied and
|
|
22
|
+
* ownership has passed to `Prepared`) or → `unwinding`/`unwound` (the failure path ran). A `push`
|
|
23
|
+
* outside `armed` is a PROGRAMMING error and is refused loudly: a resource acquired after commit
|
|
24
|
+
* would have no owner on the failure path, and one pushed during an unwind would never run.
|
|
25
|
+
*/
|
|
26
|
+
import type { RollbackStack } from "./contracts.js";
|
|
27
|
+
/**
|
|
28
|
+
* Mint one stack per prepare. `report` is the incident channel for a cleanup that throws or times out
|
|
29
|
+
* (wire it to `deps.onError`); with none, failures of the failure path are dropped — the same
|
|
30
|
+
* contract `settleTeardownLeg` states for its own `report`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createRollbackStack(report?: (err: Error) => void): RollbackStack;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
2
|
+
export function createRollbackStack(report) {
|
|
3
|
+
const cleanups = [];
|
|
4
|
+
let state = "armed";
|
|
5
|
+
return {
|
|
6
|
+
push(cleanup) {
|
|
7
|
+
if (state !== "armed") {
|
|
8
|
+
throw new Error(`RollbackStack.push after ${state}: a cleanup registered once prepare has ${state === "committed" ? "succeeded" : "started unwinding"} would ${state === "committed" ? "never be owned by the failure path" : "never run"} — acquire resources only while the stack is armed.`);
|
|
9
|
+
}
|
|
10
|
+
cleanups.push(cleanup);
|
|
11
|
+
},
|
|
12
|
+
commit() {
|
|
13
|
+
if (state !== "armed")
|
|
14
|
+
throw new Error(`RollbackStack.commit after ${state}: commit is the success path's one disarm and cannot follow an unwind or a second commit.`);
|
|
15
|
+
state = "committed";
|
|
16
|
+
cleanups.length = 0;
|
|
17
|
+
},
|
|
18
|
+
async unwindAll() {
|
|
19
|
+
if (state !== "armed")
|
|
20
|
+
return;
|
|
21
|
+
state = "unwinding";
|
|
22
|
+
const total = cleanups.length;
|
|
23
|
+
let leg = total;
|
|
24
|
+
for (let cleanup = cleanups.pop(); cleanup !== undefined; cleanup = cleanups.pop(), leg--) {
|
|
25
|
+
await settleTeardownLeg(cleanup, `${cleanup.name !== "" ? cleanup.name : "anonymous cleanup"} (rollback leg ${leg} of ${total}, prepare-throw)`, report);
|
|
26
|
+
}
|
|
27
|
+
state = "unwound";
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -86,7 +86,9 @@ declare function toolEndBodyFrom(result: unknown, isError: boolean,
|
|
|
86
86
|
* live loop reads it off the gate's per-call sideband, the resumed leg off the decide. Deliberately
|
|
87
87
|
* a parameter and never derived from `result`: a tool's own `details` (which post-tool hooks may
|
|
88
88
|
* also replace) is writable by layers that adjudicate nothing, so reading provenance out of it would
|
|
89
|
-
* let a failing tool claim a person approved it. Omitted ⇒
|
|
89
|
+
* let a failing tool claim a person approved it. Omitted ⇒ the caller supplied no source: the call
|
|
90
|
+
* settled no approval, OR an approval settled whose decider named none (a caller predating the
|
|
91
|
+
* field) — absence is not a fact about the call (see the `tool_end.settledBy` doc, the one home). */
|
|
90
92
|
settledBy?: ApprovalSettledBy,
|
|
91
93
|
/** design/252 G-7 — WHOSE settlement, from the same caller and the same channel as `settledBy`, and
|
|
92
94
|
* for the same reason it is a parameter: an attribution read out of a tool's own result would let a
|
|
@@ -100,7 +102,13 @@ resolution?: import("../tool-policy.js").AskDenyResolution,
|
|
|
100
102
|
* sharpest version of that reason: this one is an assertion about a DIFFERENT call, so a tool able to
|
|
101
103
|
* author it could point an approval UI at a call nobody is waiting on. Omitted ⇒ no park is holding a
|
|
102
104
|
* call (nothing parked, or the park that did holds none), and the frame then carries no id at all. */
|
|
103
|
-
gatedCallIdOfRun?: string
|
|
105
|
+
gatedCallIdOfRun?: string,
|
|
106
|
+
/** #557 — the classifier denial-limit fallback's own auto-deny marker, from the same engine-owned
|
|
107
|
+
* settlement sideband as `resolution` (same never-derived-from-`result` posture). Omitted ⇒ absent.
|
|
108
|
+
* The pair `settledBy:"timeout"` + `resolution:"window_expired"` WITHOUT this bit is the approval
|
|
109
|
+
* factory's own `approvalTimeoutMs` window; a durable park's expiry is neither — the decide lane's
|
|
110
|
+
* emitter passes no `resolution` at all (see the `tool_end.autoDenied` doc on the event type). */
|
|
111
|
+
autoDenied?: true): {
|
|
104
112
|
output?: unknown;
|
|
105
113
|
truncated?: boolean;
|
|
106
114
|
totalChars?: number;
|
|
@@ -110,6 +118,7 @@ gatedCallIdOfRun?: string): {
|
|
|
110
118
|
settledBy?: ApprovalSettledBy;
|
|
111
119
|
approver?: string;
|
|
112
120
|
resolution?: import("../tool-policy.js").AskDenyResolution;
|
|
121
|
+
autoDenied?: true;
|
|
113
122
|
};
|
|
114
123
|
/**
|
|
115
124
|
* scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
|
|
@@ -47,6 +47,7 @@ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.j
|
|
|
47
47
|
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, MAX_HOOK_TIMEOUT_MS, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
48
48
|
import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
|
|
49
49
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
50
|
+
import { droppedToolDigest, withAnnounceDigests } from "./prepare-announce-once.js";
|
|
50
51
|
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
51
52
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
52
53
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
@@ -146,7 +147,7 @@ function resumeDecisionWasNegative(resume) {
|
|
|
146
147
|
}
|
|
147
148
|
const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
|
|
148
149
|
"NOT executed on resume. If you still need it, issue it again now.";
|
|
149
|
-
function toolEndBodyFrom(result, isError, settledBy, approver, resolution, gatedCallIdOfRun) {
|
|
150
|
+
function toolEndBodyFrom(result, isError, settledBy, approver, resolution, gatedCallIdOfRun, autoDenied) {
|
|
150
151
|
const o = toolOutputFrom(result);
|
|
151
152
|
const st = structuredFrom(result);
|
|
152
153
|
const det = isError ? result?.details : undefined;
|
|
@@ -162,6 +163,7 @@ function toolEndBodyFrom(result, isError, settledBy, approver, resolution, gated
|
|
|
162
163
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
163
164
|
...(approver !== undefined ? { approver } : {}),
|
|
164
165
|
...(resolution !== undefined ? { resolution } : {}),
|
|
166
|
+
...(autoDenied !== undefined ? { autoDenied } : {}),
|
|
165
167
|
};
|
|
166
168
|
}
|
|
167
169
|
export function reconciledToolEndBody(orphan) {
|
|
@@ -381,6 +383,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
381
383
|
let finalVerifySeenAtLastBoundary = 0;
|
|
382
384
|
const onTurnBoundary = async (event) => {
|
|
383
385
|
let boundarySteered = false;
|
|
386
|
+
const noNextRequest = () => prepared.stopRequestedRef.current || deps.loopLatch.userHalted;
|
|
384
387
|
const walltimeHit = walltimeMonotonicDeadline !== undefined && performance.now() >= walltimeMonotonicDeadline;
|
|
385
388
|
const preemptHit = spec.preemptSignal?.aborted === true;
|
|
386
389
|
if (preemptHit && !prepared.suspendForResource && !rs.counters.preemptIgnoredReported) {
|
|
@@ -565,6 +568,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
565
568
|
rs.attach.attachState !== undefined) &&
|
|
566
569
|
!boundarySteered &&
|
|
567
570
|
prepared.batchHaltRef.current === undefined &&
|
|
571
|
+
!noNextRequest() &&
|
|
568
572
|
rs.counters.finalVerifyInjections === 0 &&
|
|
569
573
|
!prepared.abortController.signal.aborted) {
|
|
570
574
|
const due = [];
|
|
@@ -589,6 +593,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
589
593
|
const backgroundTasksOn = (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true;
|
|
590
594
|
const bgTasks = backgroundTasksOn && rs.attach.attachState.postCompactPending ? prepared.listBackgroundTasks() : undefined;
|
|
591
595
|
const toolsDeltaOn = rs.attach.attachmentsCfg?.toolsDelta === true;
|
|
596
|
+
let announcedOnceDirty = false;
|
|
592
597
|
const tdRef = toolsDeltaOn ? prepared.toolsDeltaRef : undefined;
|
|
593
598
|
const pendingTools = tdRef !== undefined && tdRef.pending.length > 0 ? [...tdRef.pending] : undefined;
|
|
594
599
|
const pendingRemovedTools = tdRef !== undefined && tdRef.pendingRemoved.length > 0 ? [...tdRef.pendingRemoved] : undefined;
|
|
@@ -694,8 +699,13 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
694
699
|
ref.pendingRemoved.splice(0, pendingRemovedTools.length);
|
|
695
700
|
if (pendingReaddedTools !== undefined)
|
|
696
701
|
ref.pendingReadded.splice(0, pendingReaddedTools.length);
|
|
697
|
-
if (pendingFailedMcp !== undefined)
|
|
702
|
+
if (pendingFailedMcp !== undefined) {
|
|
698
703
|
ref.pendingFailed.splice(0, pendingFailedMcp.length);
|
|
704
|
+
if (!noNextRequest()) {
|
|
705
|
+
prepared.announcedListingsRef.mcpFailed = [...(prepared.announcedListingsRef.mcpFailed ?? []), ...pendingFailedMcp];
|
|
706
|
+
announcedOnceDirty = true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
699
709
|
}
|
|
700
710
|
}
|
|
701
711
|
if ((mcpAdds !== undefined || mcpRemovals !== undefined) &&
|
|
@@ -708,6 +718,10 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
708
718
|
if (mcpDropped !== undefined &&
|
|
709
719
|
due.some((a) => a.source === "mcp_dropped_tools" && a.body === renderMcpDroppedTools(mcpDropped))) {
|
|
710
720
|
prepared.mcp.droppedTools.splice(0, mcpDropped.length);
|
|
721
|
+
if (!noNextRequest()) {
|
|
722
|
+
prepared.announcedListingsRef.advisories = withAnnounceDigests(prepared.announcedListingsRef.advisories, mcpDropped.map(droppedToolDigest));
|
|
723
|
+
announcedOnceDirty = true;
|
|
724
|
+
}
|
|
711
725
|
}
|
|
712
726
|
if (rs.attach.agentListingOn && prepared.agentListing !== undefined && due.some((a) => a.source === "agent_listing")) {
|
|
713
727
|
const listing = prepared.agentListing;
|
|
@@ -717,7 +731,8 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
717
731
|
prepared.announcedListingsRef.agents = listing.entries.map((e) => e.name);
|
|
718
732
|
if (listing.models !== undefined)
|
|
719
733
|
prepared.announcedListingsRef.models = [...listing.models];
|
|
720
|
-
|
|
734
|
+
if (prepared.announcedSnapshotRecovered)
|
|
735
|
+
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
721
736
|
}
|
|
722
737
|
}
|
|
723
738
|
if (rs.attach.skillsListingOn && prepared.skillsListing !== undefined && due.some((a) => a.source === "skills_listing")) {
|
|
@@ -726,9 +741,12 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
726
741
|
if (intactBody !== undefined && due.some((a) => a.source === "skills_listing" && a.body === intactBody)) {
|
|
727
742
|
commitSkillsListing(rs.attach.attachState, listing.entries);
|
|
728
743
|
prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
|
|
729
|
-
|
|
744
|
+
if (prepared.announcedSnapshotRecovered)
|
|
745
|
+
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
730
746
|
}
|
|
731
747
|
}
|
|
748
|
+
if (announcedOnceDirty && prepared.announcedSnapshotRecovered)
|
|
749
|
+
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
732
750
|
}
|
|
733
751
|
if (rs.attach.dateState !== undefined && rs.turn.lastTurnHadToolCalls) {
|
|
734
752
|
const today = prepared.dateChange.today();
|
|
@@ -1377,7 +1395,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1377
1395
|
toolName: event.toolName,
|
|
1378
1396
|
...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
|
|
1379
1397
|
isError: event.isError,
|
|
1380
|
-
...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver, settlement?.resolution, prepared.abortController.signal.aborted ? gatedCallIdOf(prepared) : undefined),
|
|
1398
|
+
...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver, settlement?.resolution, prepared.abortController.signal.aborted ? gatedCallIdOf(prepared) : undefined, settlement?.autoDenied),
|
|
1381
1399
|
...ident(),
|
|
1382
1400
|
});
|
|
1383
1401
|
announceWorkspaceMove();
|
|
@@ -3346,7 +3364,7 @@ export class Runner {
|
|
|
3346
3364
|
w.resolve(outcome);
|
|
3347
3365
|
};
|
|
3348
3366
|
const onTurnBoundary = makeTurnBoundary(prepared, stats, rs, {
|
|
3349
|
-
spec, queue, manualCompactRef, todoToolMounted, taskToolsMounted,
|
|
3367
|
+
spec, queue, loopLatch, manualCompactRef, todoToolMounted, taskToolsMounted,
|
|
3350
3368
|
walltimeMonotonicDeadline,
|
|
3351
3369
|
timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction,
|
|
3352
3370
|
compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact,
|
|
@@ -3833,7 +3851,7 @@ export class Runner {
|
|
|
3833
3851
|
queue.push({ type: "steering_injected", source: f.source, preview: redactThenCut(f.body, 220), ...ident() });
|
|
3834
3852
|
}
|
|
3835
3853
|
rs.attach.attachmentsInjected += firstFrames.length;
|
|
3836
|
-
if (firstFrames.length > 0)
|
|
3854
|
+
if (firstFrames.length > 0 && prepared.announcedSnapshotRecovered)
|
|
3837
3855
|
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
3838
3856
|
const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
|
|
3839
3857
|
queue.push({
|
|
@@ -4719,8 +4737,8 @@ export class Runner {
|
|
|
4719
4737
|
}
|
|
4720
4738
|
}
|
|
4721
4739
|
if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
|
|
4722
|
-
this.deps.
|
|
4723
|
-
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (
|
|
4740
|
+
this.deps.permissionRuleStore?.partitions.org !== true) {
|
|
4741
|
+
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (its permission-rule store carries no org partition) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
|
|
4724
4742
|
}
|
|
4725
4743
|
const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
|
|
4726
4744
|
if (retiredWalltimeTotal !== undefined) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure path arithmetic for a workspace whose ROOT moved: rebase an absolute path from the old root
|
|
3
|
+
* onto the new one. No I/O, no state, no dependency on anything — which is why it lives here rather
|
|
4
|
+
* than inside the restore phase that used to own it. The read-face phase needs the same arithmetic,
|
|
5
|
+
* and a phase reaching sideways into a sibling phase for a helper is the shape the dependency
|
|
6
|
+
* gate exists to stop; the answer is to put the helper where both can reach DOWN to it.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* rebase one checkpointed absolute path from the OLD workspace root onto the RESTORED
|
|
10
|
+
* one (divergent `resumeVM`). POSIX-ONLY by contract: every remote lane's `mountPath` is a container
|
|
11
|
+
* path (e2b/k8s/ssh/adb/local-docker are all Linux targets), so a backslash ANYWHERE in the inputs marks
|
|
12
|
+
* the value outside this function's domain and it returns `p` UNCHANGED — an un-rebased path is honestly
|
|
13
|
+
* observable (the divergence observation already fired) while a WRONGLY-rebased one silently corrupts
|
|
14
|
+
* cwd/read-state (r4: drive-root and cross-family recomposition are not implementable without a Windows
|
|
15
|
+
* path model no lane needs). Trailing slashes are tolerated (from="/app/" must not weld the suffix);
|
|
16
|
+
* bare "/" keeps filesystem-root semantics; a path outside `from` (incl. the prefix-sibling
|
|
17
|
+
* "/application" vs "/app") returns unchanged. Exported for direct unit pinning.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rebaseWorkspacePath(p: string, fromRaw: string, toRaw: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* RB-439-c — {@link rebaseWorkspacePath} over a SET of accepted spellings of the old root: the first
|
|
22
|
+
* prefix that actually matches wins, and a path under none of them is returned unchanged.
|
|
23
|
+
*
|
|
24
|
+
* The set exists because "the old root" has no single spelling. `WorkspaceHandle.mountPath` is whatever the
|
|
25
|
+
* adapter called the mount, while the persisted paths being migrated were spelled by whoever produced them
|
|
26
|
+
* (a `cd` the env canonicalized, a Read key resolved through realpath). On a target where the root has an
|
|
27
|
+
* equivalent alias (`/var` ↔ `/private/var`) those disagree while naming the same directory, and a
|
|
28
|
+
* single-prefix rebase then matched nothing and silently left the resumed shell in the pre-suspend tree —
|
|
29
|
+
* under a disclosure that said the task follows the restored root. Order is caller-chosen (the checkpointed
|
|
30
|
+
* spelling first, its canonical form second) and only matters if one prefix is a prefix of another, in
|
|
31
|
+
* which case the earlier — more specific — spelling is the intended one. Exported for direct unit pinning.
|
|
32
|
+
*/
|
|
33
|
+
export declare function rebaseWorkspacePathAcross(p: string, froms: readonly string[], to: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function rebaseWorkspacePath(p, fromRaw, toRaw) {
|
|
2
|
+
if (p.includes("\\") || fromRaw.includes("\\") || toRaw.includes("\\"))
|
|
3
|
+
return p;
|
|
4
|
+
const stripTrail = (s) => (s.length > 1 && s.endsWith("/") ? stripTrail(s.slice(0, -1)) : s);
|
|
5
|
+
const from = stripTrail(fromRaw);
|
|
6
|
+
const to = stripTrail(toRaw);
|
|
7
|
+
if (p === from || stripTrail(p) === from)
|
|
8
|
+
return to;
|
|
9
|
+
const fromPrefix = from === "/" ? from : `${from}/`;
|
|
10
|
+
if (!p.startsWith(fromPrefix))
|
|
11
|
+
return p;
|
|
12
|
+
const suffix = p.slice(fromPrefix.length).replace(/^\/+/, "");
|
|
13
|
+
return to === "/" ? `${to}${suffix}` : `${to}/${suffix}`;
|
|
14
|
+
}
|
|
15
|
+
export function rebaseWorkspacePathAcross(p, froms, to) {
|
|
16
|
+
for (const from of froms) {
|
|
17
|
+
const out = rebaseWorkspacePath(p, from, to);
|
|
18
|
+
if (out !== p)
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
@@ -33,6 +33,22 @@ import type { ToolPolicy } from "./tool-policy.js";
|
|
|
33
33
|
* hard refusal for every deployment that wires this list.
|
|
34
34
|
*/
|
|
35
35
|
export declare const RECOMMENDED_SENSITIVE_PATTERNS: readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* HRD-PRM-10 — the engine data root, read from the same knob its two consumers read
|
|
38
|
+
* (`$AGENT_DATA_DIR ?? ~/.ai-agent`, off the HOST at wiring time, NFC-normalized) and returned in
|
|
39
|
+
* EVERY spelling those consumers resolve it to. There are two, and they disagree for exactly two
|
|
40
|
+
* inputs (a leading `~`, and a relative value) — the transcript gate expands the tilde and leaves a
|
|
41
|
+
* relative value to the environment, the memory engine does neither and resolves against
|
|
42
|
+
* `process.cwd()`. Accepting only one caliber would exempt a directory nobody uses while the other
|
|
43
|
+
* consumer's real directory stayed guarded, which is the refusal this whole passthrough exists to
|
|
44
|
+
* remove. Every returned spelling is ANCHORED (see the loop) — that is not a detail.
|
|
45
|
+
*
|
|
46
|
+
* Exported as an ENGINE-INTERNAL seam (deliberately not re-exported from `src/index.ts`): the ask-side
|
|
47
|
+
* write-protection table carries the same passthrough law for the same reason, and a second reading of
|
|
48
|
+
* "where is the data root" is exactly how the two guards would come to disagree about which directory
|
|
49
|
+
* the engine owns. One resolver, two judges.
|
|
50
|
+
*/
|
|
51
|
+
export declare function dataRootSpellings(explicit: string | undefined): string[];
|
|
36
52
|
/**
|
|
37
53
|
* Build a composable policy that DENIES writes (via the guarded fs tools) whose canonical target
|
|
38
54
|
* lands on a sensitive path. `patterns` are deployment-supplied (no default list is shipped — the
|