@sema-agent/core 7.5.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 (62) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/core/auto-mode.d.ts +9 -0
  3. package/dist/core/auto-mode.js +11 -0
  4. package/dist/core/checkpoint-store.js +5 -1
  5. package/dist/core/runner/checkpoint-scope.d.ts +32 -0
  6. package/dist/core/runner/checkpoint-scope.js +4 -0
  7. package/dist/core/runner/contracts.d.ts +1878 -0
  8. package/dist/core/runner/contracts.js +1 -0
  9. package/dist/core/runner/denial-limit-arms.d.ts +1 -1
  10. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  11. package/dist/core/runner/derived-route-fallback.js +16 -0
  12. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  13. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  14. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  15. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  16. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  17. package/dist/core/runner/prepare-defer-classify.js +107 -0
  18. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  19. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  20. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  21. package/dist/core/runner/prepare-execution-env.js +86 -0
  22. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  23. package/dist/core/runner/prepare-file-history.js +383 -0
  24. package/dist/core/runner/prepare-hands-readface.d.ts +5 -7
  25. package/dist/core/runner/prepare-hands-readface.js +1 -1
  26. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  27. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  28. package/dist/core/runner/prepare-listings.d.ts +77 -0
  29. package/dist/core/runner/prepare-listings.js +76 -0
  30. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  31. package/dist/core/runner/prepare-lsp.js +27 -0
  32. package/dist/core/runner/prepare-memory.d.ts +1 -1
  33. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  34. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  35. package/dist/core/runner/prepare-project-context.d.ts +131 -0
  36. package/dist/core/runner/prepare-project-context.js +150 -0
  37. package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
  38. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  39. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  40. package/dist/core/runner/prepare-protocol-tools.js +182 -0
  41. package/dist/core/runner/prepare-question-face.d.ts +119 -0
  42. package/dist/core/runner/prepare-question-face.js +83 -0
  43. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  44. package/dist/core/runner/prepare-run-refs.js +39 -0
  45. package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
  46. package/dist/core/runner/prepare-task.d.ts +11 -1846
  47. package/dist/core/runner/prepare-task.js +83 -2366
  48. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  49. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  50. package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
  51. package/dist/core/runner/prepare-wiring-manifest.js +240 -0
  52. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
  53. package/dist/core/runner/prepare-workspace-restore.js +1 -22
  54. package/dist/core/runner/rollback-stack.d.ts +32 -0
  55. package/dist/core/runner/rollback-stack.js +30 -0
  56. package/dist/core/runner/workspace-path.d.ts +33 -0
  57. package/dist/core/runner/workspace-path.js +22 -0
  58. package/dist/core/tool-policy.d.ts +16 -0
  59. package/dist/core/tool-policy.js +3 -0
  60. package/dist/core/types.d.ts +2 -2
  61. package/dist/core/write-protect.js +3 -2
  62. package/package.json +6 -2
@@ -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
+ }
@@ -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
+ }
@@ -1415,6 +1415,22 @@ export declare function withDelegationProvenance(onAsk: OnAsk, delegation: AskDe
1415
1415
  * consultation". A non-wrapper value (including `"allow"`/`"deny"`/undefined) is its own identity.
1416
1416
  */
1417
1417
  export declare function askApproverIdentity(onAsk: unknown): unknown;
1418
+ /**
1419
+ * Is this `onAsk` seat a LIVE APPROVER — a person the run can actually reach?
1420
+ *
1421
+ * Only a function is. A blanket `"allow"`/`"deny"` string is a policy setting (the same reading the
1422
+ * interaction-posture door gives it), and an absent seat is nobody at all; neither can answer an ask, so
1423
+ * neither may outrank a durable park.
1424
+ *
1425
+ * ONE definition on purpose. Two sites must agree on this — the run's own sync-first decline, and the
1426
+ * mandate frozen into the chain its children inherit — and when they were two hand-written booleans they
1427
+ * drifted: the decline narrowed to the function test while the freeze kept testing mere presence, and
1428
+ * each still read correctly on its own, so nothing pointed at the pair. A blanket seat was therefore not
1429
+ * an approver for the parent's asks and an approver for its children's. Declared here, in the vocabulary
1430
+ * beside {@link askApproverIdentity}, because the two sites are now two prepare phases (the inherited-gate
1431
+ * fold and the wiring station) plus the orchestrator, and each reaches DOWN for the one definition.
1432
+ */
1433
+ export declare function isLiveApproverSeat(onAsk: OnAsk | undefined): boolean;
1418
1434
  /** EXACT structural clone for the approval boundary (alias isolation) — fail-closed, never lossy,
1419
1435
  * never identity: a JSON round-trip would silently DROP unclonable members (presented ≠ executing =
1420
1436
  * consent misrepresentation) and an identity fallback would hand the approver the live executing
@@ -847,6 +847,9 @@ export function withDelegationProvenance(onAsk, delegation) {
847
847
  export function askApproverIdentity(onAsk) {
848
848
  return typeof onAsk === "function" ? (delegatedApproverRoot.get(onAsk) ?? onAsk) : onAsk;
849
849
  }
850
+ export function isLiveApproverSeat(onAsk) {
851
+ return typeof onAsk === "function";
852
+ }
850
853
  export function tryCloneArgs(v) {
851
854
  try {
852
855
  const value = structuredClone(v);
@@ -662,7 +662,7 @@ export interface ToolExecuteContext {
662
662
  * at all; a park under a screening entry announces the requirement on `onError`
663
663
  * (classification `screening-constraint-in-durable-chain`) at park time.
664
664
  */
665
- inheritedGate?: import("./runner/prepare-task.js").InheritedGate;
665
+ inheritedGate?: import("./runner/contracts.js").InheritedGate;
666
666
  };
667
667
  /**
668
668
  * design/176 — the peer-chain SEED for a message-driven (tier-3) revival: the hop chain of the
@@ -1046,7 +1046,7 @@ export interface ToolExecuteContext {
1046
1046
  * read-only, NEVER a model/tool argument (same posture as {@link activeSkillScope}). Undefined when the
1047
1047
  * tool runs outside a Runner task.
1048
1048
  */
1049
- inheritedGateForChildren?: () => import("./runner/prepare-task.js").InheritedGate;
1049
+ inheritedGateForChildren?: () => import("./runner/contracts.js").InheritedGate;
1050
1050
  /**
1051
1051
  * design/180 half A — the delegation runtime-provenance ARMING face. Runner-filled; a delegation
1052
1052
  * tool calls it at spawn time: a non-undefined return means this (parent) run is armed (it mounts
@@ -1,4 +1,4 @@
1
- import { isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
1
+ import { isWinFormPath, writeTargetPath, isUncPath } from "../tools/fs/safety.js";
2
2
  import { PATH_CONFINABLE_WRITE_TOOLS } from "./runner/session-rule-policy.js";
3
3
  import { dataRootSpellings } from "./sensitive-path-policy.js";
4
4
  const freezeTable = (rows) => Object.freeze(rows.map((r) => Object.freeze(r)));
@@ -137,7 +137,8 @@ function pathViewSegments(path, win32, seps = BOTH_SEPARATOR_FAMILIES, anchorFlo
137
137
  const BOTH_SEPARATOR_FAMILIES = /[\\/]/;
138
138
  const POSIX_SEPARATOR_FAMILY = /\//;
139
139
  function uncAnchorSegments(path) {
140
- return /^[\\/]{2}[^\\/]/.test(path) ? 2 : 0;
140
+ const third = path[2];
141
+ return isUncPath(path) && third !== undefined && third !== "\\" && third !== "/" ? 2 : 0;
141
142
  }
142
143
  function relativeUnderDataRoot(path, roots) {
143
144
  for (const root of roots) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.5.0",
3
+ "version": "7.5.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -73,7 +73,11 @@
73
73
  "gate:error-surface": "node scripts/verify-error-surface.mjs",
74
74
  "gate:criteria": "node scripts/criteria-lint.mjs",
75
75
  "gate:reminder-literal": "node scripts/verify-reminder-literal.mjs",
76
- "gate:wire": "node scripts/wire-changes.mjs"
76
+ "gate:wire": "node scripts/wire-changes.mjs",
77
+ "gate:layering": "node scripts/verify-layering.mjs",
78
+ "gate:phase-api": "node scripts/verify-phase-api.mjs",
79
+ "gate:agent-returns": "node scripts/verify-agent-returns.mjs",
80
+ "gate:all": "node scripts/gate-all.mjs"
77
81
  },
78
82
  "dependencies": {
79
83
  "@modelcontextprotocol/sdk": "1.30.0",