@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/peer-notices.d.ts +11 -1
  4. package/dist/agents/peer-session-drain.js +2 -0
  5. package/dist/agents/verify.d.ts +2 -1
  6. package/dist/core/ask-origin.d.ts +130 -0
  7. package/dist/core/ask-origin.js +35 -0
  8. package/dist/core/auto-mode-arming.d.ts +40 -1
  9. package/dist/core/auto-mode-arming.js +51 -3
  10. package/dist/core/auto-mode.d.ts +87 -10
  11. package/dist/core/auto-mode.js +34 -6
  12. package/dist/core/checkpoint-store.d.ts +41 -6
  13. package/dist/core/checkpoint-store.js +8 -0
  14. package/dist/core/hooks.d.ts +63 -19
  15. package/dist/core/hooks.js +37 -16
  16. package/dist/core/mcp.d.ts +47 -55
  17. package/dist/core/mcp.js +89 -31
  18. package/dist/core/park-selfcheck.js +3 -0
  19. package/dist/core/permission-rule-consent.d.ts +2 -11
  20. package/dist/core/permission-rule-consent.js +13 -62
  21. package/dist/core/permission-rule-org.d.ts +70 -54
  22. package/dist/core/permission-rule-org.js +47 -61
  23. package/dist/core/permission-rule-provider.d.ts +162 -0
  24. package/dist/core/permission-rule-provider.js +148 -0
  25. package/dist/core/permission-rule-session.d.ts +23 -19
  26. package/dist/core/permission-rule-session.js +5 -5
  27. package/dist/core/permission-rule-store.d.ts +46 -21
  28. package/dist/core/permission-rule-store.js +13 -6
  29. package/dist/core/permission-rule-sync.d.ts +2 -1
  30. package/dist/core/permission-rule-sync.js +11 -0
  31. package/dist/core/runner/assemble-result.d.ts +3 -2
  32. package/dist/core/runner/checkpoint-scope.d.ts +32 -0
  33. package/dist/core/runner/checkpoint-scope.js +4 -0
  34. package/dist/core/runner/contracts.d.ts +1878 -0
  35. package/dist/core/runner/contracts.js +1 -0
  36. package/dist/core/runner/denial-limit-arms.d.ts +57 -31
  37. package/dist/core/runner/denial-limit-arms.js +42 -17
  38. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  39. package/dist/core/runner/derived-route-fallback.js +16 -0
  40. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  41. package/dist/core/runner/prepare-announce-once.d.ts +83 -0
  42. package/dist/core/runner/prepare-announce-once.js +105 -0
  43. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  44. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  45. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  46. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  47. package/dist/core/runner/prepare-defer-classify.js +107 -0
  48. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  49. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  50. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  51. package/dist/core/runner/prepare-execution-env.js +86 -0
  52. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  53. package/dist/core/runner/prepare-file-history.js +383 -0
  54. package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
  55. package/dist/core/runner/prepare-hands-readface.js +3 -3
  56. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  57. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  58. package/dist/core/runner/prepare-listings.d.ts +77 -0
  59. package/dist/core/runner/prepare-listings.js +76 -0
  60. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  61. package/dist/core/runner/prepare-lsp.js +27 -0
  62. package/dist/core/runner/prepare-memory.d.ts +1 -1
  63. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  64. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  65. package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
  66. package/dist/core/runner/prepare-permission-rules.js +140 -0
  67. package/dist/core/runner/prepare-project-context.d.ts +131 -0
  68. package/dist/core/runner/prepare-project-context.js +150 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  71. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  72. package/dist/core/runner/prepare-protocol-tools.js +182 -0
  73. package/dist/core/runner/prepare-question-face.d.ts +119 -0
  74. package/dist/core/runner/prepare-question-face.js +83 -0
  75. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  76. package/dist/core/runner/prepare-run-refs.js +39 -0
  77. package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
  78. package/dist/core/runner/prepare-task.d.ts +11 -1815
  79. package/dist/core/runner/prepare-task.js +138 -2542
  80. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  81. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  82. package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
  83. package/dist/core/runner/prepare-wiring-manifest.js +240 -0
  84. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
  85. package/dist/core/runner/prepare-workspace-restore.js +1 -22
  86. package/dist/core/runner/rollback-stack.d.ts +32 -0
  87. package/dist/core/runner/rollback-stack.js +30 -0
  88. package/dist/core/runner/runtask.d.ts +11 -2
  89. package/dist/core/runner/runtask.js +27 -9
  90. package/dist/core/runner/workspace-path.d.ts +33 -0
  91. package/dist/core/runner/workspace-path.js +22 -0
  92. package/dist/core/sensitive-path-policy.d.ts +16 -0
  93. package/dist/core/sensitive-path-policy.js +1 -1
  94. package/dist/core/tool-policy.d.ts +57 -9
  95. package/dist/core/tool-policy.js +11 -0
  96. package/dist/core/types.d.ts +63 -51
  97. package/dist/core/wiring-manifest.d.ts +40 -3
  98. package/dist/core/wiring-manifest.js +4 -3
  99. package/dist/core/write-protect.d.ts +13 -2
  100. package/dist/core/write-protect.js +58 -29
  101. package/dist/engine/harness/types.d.ts +38 -16
  102. package/dist/engine/harness/types.js +25 -1
  103. package/dist/engine/session/session.d.ts +3 -11
  104. package/dist/index.d.ts +10 -6
  105. package/dist/index.js +9 -5
  106. package/dist/internal/harness.d.ts +1 -0
  107. package/dist/stores/file/adoption/adopt.d.ts +1 -1
  108. package/dist/stores/file/adoption/marker.d.ts +1 -1
  109. package/dist/stores/file/permission-rule-adopt.js +4 -3
  110. package/dist/stores/file/permission-rule-store.d.ts +65 -25
  111. package/dist/stores/file/permission-rule-store.js +215 -37
  112. package/dist/stores/file/task-list-store.d.ts +1 -1
  113. package/dist/tools/fs/read-face.d.ts +1 -1
  114. package/package.json +8 -2
  115. 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 "./prepare-task.js";
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
- export function rebaseWorkspacePath(p, fromRaw, toRaw) {
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 ⇒ this call settled no approval. */
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
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
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
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
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.permissionRuleOrg === undefined) {
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 (permissionRuleOrg) — 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" });
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