@sema-agent/core 7.5.1 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/protocol-table.d.ts +5 -0
  31. package/dist/core/protocol-table.js +1 -0
  32. package/dist/core/runner/abort-race.d.ts +41 -0
  33. package/dist/core/runner/abort-race.js +38 -0
  34. package/dist/core/runner/assemble-result.d.ts +32 -41
  35. package/dist/core/runner/assemble-result.js +55 -74
  36. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  37. package/dist/core/runner/checkpoint-scope.js +3 -0
  38. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  39. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  40. package/dist/core/runner/content-ask-bindings.js +1 -0
  41. package/dist/core/runner/contracts.d.ts +144 -50
  42. package/dist/core/runner/denial-limit-arms.d.ts +24 -1
  43. package/dist/core/runner/denial-limit-arms.js +24 -3
  44. package/dist/core/runner/gate-exit.d.ts +74 -0
  45. package/dist/core/runner/gate-exit.js +55 -0
  46. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  47. package/dist/core/runner/inherited-ask-grants.js +29 -0
  48. package/dist/core/runner/park-commit.d.ts +102 -0
  49. package/dist/core/runner/park-commit.js +31 -0
  50. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  51. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  52. package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
  53. package/dist/core/runner/prepare-ask-lane.js +131 -0
  54. package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
  55. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  56. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  57. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  58. package/dist/core/runner/prepare-context-lane.js +230 -0
  59. package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
  60. package/dist/core/runner/prepare-gate-stations.js +265 -0
  61. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  63. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  64. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  65. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  66. package/dist/core/runner/prepare-memory.d.ts +44 -26
  67. package/dist/core/runner/prepare-park-ask.d.ts +162 -0
  68. package/dist/core/runner/prepare-park-ask.js +377 -0
  69. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  70. package/dist/core/runner/prepare-policy-chain.js +584 -0
  71. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  72. package/dist/core/runner/prepare-project-context.js +1 -3
  73. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  74. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  75. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  76. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  77. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  78. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  79. package/dist/core/runner/prepare-question-face.js +2 -1
  80. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  81. package/dist/core/runner/prepare-safety-scan.js +1 -2
  82. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  83. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  84. package/dist/core/runner/prepare-task.d.ts +9 -136
  85. package/dist/core/runner/prepare-task.js +47 -2745
  86. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  87. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  88. package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
  89. package/dist/core/runner/prepare-wiring-manifest.js +16 -10
  90. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  91. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  92. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  93. package/dist/core/runner/prompt-hash-salt.js +2 -0
  94. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  95. package/dist/core/runner/remote-env-retry.js +16 -0
  96. package/dist/core/runner/runtask.d.ts +16 -31
  97. package/dist/core/runner/runtask.js +110 -121
  98. package/dist/core/runner/terminal-projection.d.ts +22 -0
  99. package/dist/core/runner/terminal-projection.js +28 -0
  100. package/dist/core/session.d.ts +12 -0
  101. package/dist/core/session.js +3 -0
  102. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  103. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  104. package/dist/core/terminal-cause.d.ts +137 -0
  105. package/dist/core/terminal-cause.js +9 -0
  106. package/dist/core/tool-policy.d.ts +43 -139
  107. package/dist/core/tool-policy.js +79 -112
  108. package/dist/core/types.d.ts +67 -164
  109. package/dist/core/wiring-manifest.d.ts +6 -3
  110. package/dist/core/workflow-journal-store.js +3 -4
  111. package/dist/engine/harness/agent-harness.d.ts +1 -1
  112. package/dist/index.d.ts +10 -7
  113. package/dist/index.js +8 -5
  114. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  115. package/dist/orchestration/builtin-workflows.js +1 -1
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.js +5 -3
  118. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  119. package/dist/orchestration/run-workflow-tool.js +4 -4
  120. package/dist/orchestration/workflow-governance.d.ts +4 -4
  121. package/dist/orchestration/workflow-governance.js +4 -2
  122. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  123. package/dist/orchestration/workflow-primitives.js +1 -1
  124. package/dist/orchestration/workflow.d.ts +11 -0
  125. package/dist/orchestration/workflow.js +64 -39
  126. package/dist/prompts/supervisor.d.ts +1 -1
  127. package/dist/prompts/supervisor.js +3 -3
  128. package/dist/scenarios/scenario-registry.js +1 -1
  129. package/package.json +3 -1
  130. package/test/export-surface.snapshot.json +74 -22
  131. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  132. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -0,0 +1,377 @@
1
+ import { uuidv7 } from "../../engine/session/uuid.js";
2
+ import { Value } from "typebox/value";
3
+ import { ASK_USER_QUESTION_TOOL_NAME, classifyQuestionOutcome, validateAskQuestions } from "../ask-question.js";
4
+ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
5
+ import { boundInputHashOf } from "../canonical-json.js";
6
+ import { BINDING_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, mintCheckpointId, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, resolveDeclaredFidelity, samePlainValue, TOKEN_CHECKPOINT_VERSION, } from "../checkpoint-store.js";
7
+ import { pauseOf } from "../pause-registry.js";
8
+ import { askCarryRowMembers } from "../hooks.js";
9
+ import { defaultTaskRegistry } from "../task-registry.js";
10
+ import { isLiveApproverSeat, carriesBidiControls, describeThrown, refuseOutOfContractDecision, tryCloneArgs } from "../tool-policy.js";
11
+ import { InMemoryToolResultStore, ScopedToolResultStore, isVolatileOffloadStore } from "../tool-result-store.js";
12
+ import { raceAbort, raceSettlementAgainstSignal } from "./abort-race.js";
13
+ import { checkpointScopeOf } from "./checkpoint-scope.js";
14
+ import { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
15
+ import { DEFAULT_UNATTENDED_APPROVAL_TTL_MS, publishCommittedSuspend, sanitizedTtlMs } from "./park-commit.js";
16
+ import { remoteEnvFailureNote } from "./remote-env-retry.js";
17
+ class ParkRefusal extends Error {
18
+ constructor(message, options) {
19
+ super(message, options);
20
+ this.name = "ParkRefusal";
21
+ }
22
+ }
23
+ export function prepareParkAsk(input) {
24
+ const { askLane, saga, spec, deps, sessionId, checkpointStore, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedBash, shellGatedMonitor, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
25
+ if (askLane === undefined || saga === undefined)
26
+ return { parkAsk: undefined };
27
+ const { composedCallSignal, approvalPreviewOf } = askLane;
28
+ const { inFlightSpendMicroUsd, serializeCheckpointState, compensatePausedVM, commitSuspendSaga, suspendLoopCapHit, suspendableEnv, parkOnlyRemoteEnv, parkOnlyHandle } = saga;
29
+ const resolveContentAsk = async (req, callSignal) => {
30
+ const contentAskSignal = composedCallSignal(callSignal);
31
+ if (!contentAskRoutable(req.toolCallId) || liveQuestionFace === undefined || mountedQuestionTool === undefined) {
32
+ return { kind: "unavailable", parkDeclined: false };
33
+ }
34
+ const deliveryId = uuidv7();
35
+ const bindOutcome = (entry, questionsHash) => {
36
+ if (contentAskBindings.size >= CONTENT_ASK_BINDING_CAP && !contentAskBindings.has(req.toolCallId))
37
+ return;
38
+ contentAskBindings.set(req.toolCallId, { ...entry, questionsHash, deliveryId });
39
+ };
40
+ const refuseBeforeDelivery = (code, presentedInput) => ({
41
+ kind: "delivery_failure",
42
+ code,
43
+ ...(presentedInput !== undefined ? { presentedInput } : {}),
44
+ });
45
+ let retained;
46
+ let presented;
47
+ try {
48
+ retained = structuredClone(req.args);
49
+ presented = structuredClone(retained.questions);
50
+ }
51
+ catch {
52
+ return refuseBeforeDelivery("question.malformed_request");
53
+ }
54
+ const retainedQuestionsHash = boundInputHashOf(retained.questions);
55
+ if (!Value.Check(mountedQuestionTool.parameters, retained) || validateAskQuestions(presented) !== undefined) {
56
+ return refuseBeforeDelivery("question.malformed_request", retained);
57
+ }
58
+ if (contentAskBindings.size >= CONTENT_ASK_BINDING_CAP && !contentAskBindings.has(req.toolCallId)) {
59
+ return { kind: "unavailable", parkDeclined: true };
60
+ }
61
+ if (contentAskSignal.aborted) {
62
+ return { kind: "delivery_failure", code: "question.aborted", presentedInput: retained };
63
+ }
64
+ try {
65
+ const facePromise = (async () => liveQuestionFace({
66
+ toolCallId: req.toolCallId,
67
+ questions: presented,
68
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
69
+ sourceTaskId: sessionId,
70
+ boundInputHash: boundInputHashOf(retained),
71
+ deliveryId,
72
+ }, contentAskSignal))();
73
+ const settlement = await raceAbort(facePromise.then((outcome) => ({ tag: "outcome", outcome }), (error) => ({ tag: "threw", error })), contentAskSignal, () => ({ tag: "aborted" }));
74
+ if (settlement.tag === "aborted") {
75
+ void facePromise.then((late) => {
76
+ if (classifyQuestionOutcome(late).shape !== "answered")
77
+ return;
78
+ lateStrandedAnswers.push({ deliveryId, toolCallId: req.toolCallId });
79
+ discloseStrandedAnswers([{ deliveryId, toolCallId: req.toolCallId }], "the task had already ended when the answer arrived");
80
+ }, () => undefined).catch(() => undefined);
81
+ }
82
+ if (settlement.tag === "outcome") {
83
+ const outcome = settlement.outcome;
84
+ const reading = classifyQuestionOutcome(outcome);
85
+ if (reading.shape === "contradictory") {
86
+ bindOutcome({ kind: "failed", error: new Error("the question channel returned a contradictory outcome (an unavailable answer)") }, retainedQuestionsHash);
87
+ return { kind: "delivery_failure", code: "question.human_channel_failed", presentedInput: retained };
88
+ }
89
+ if (reading.shape === "unavailable")
90
+ return { kind: "unavailable", parkDeclined: true, presentedInput: retained };
91
+ const answerSnapshot = reading.answer;
92
+ bindOutcome({ kind: "answered", answer: answerSnapshot }, retainedQuestionsHash);
93
+ return { kind: "answered", presentedInput: retained };
94
+ }
95
+ const error = settlement.tag === "threw"
96
+ ? settlement.error
97
+ : Object.assign(new Error("the task ended while this question was still out"), { name: "AbortError" });
98
+ bindOutcome({ kind: "failed", error }, retainedQuestionsHash);
99
+ return {
100
+ kind: "delivery_failure",
101
+ code: settlement.tag === "aborted" || contentAskSignal.aborted ? "question.aborted" : "question.human_channel_failed",
102
+ presentedInput: retained,
103
+ };
104
+ }
105
+ catch (err) {
106
+ bindOutcome({ kind: "failed", error: err }, retainedQuestionsHash);
107
+ return { kind: "delivery_failure", code: "question.human_channel_failed", presentedInput: retained };
108
+ }
109
+ };
110
+ const suspendAsk = parkLaneArmed && checkpointStore !== undefined
111
+ ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated, carry) => {
112
+ const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
113
+ if (syncFirstEligible &&
114
+ runtimeCaps?.forceDurableGate !== true &&
115
+ liveFaceUnavailable !== true &&
116
+ !inheritedUnavailableAsks.has(req.toolCallId)) {
117
+ return undefined;
118
+ }
119
+ const cutSignal = composedCallSignal(carry?.signal);
120
+ let token;
121
+ let gate;
122
+ let cp;
123
+ let remoteHandle;
124
+ const remoteEnv = suspendableEnv;
125
+ try {
126
+ const parked = tryCloneArgs(postHookArgs);
127
+ if (!parked.ok) {
128
+ throw new ParkRefusal(`the arguments of "${req.toolName}" cannot be captured onto an approval checkpoint (unclonable value: ${parked.reason}) — ` +
129
+ `a durable approval row must hold an inert snapshot of exactly what a human approves and a resume executes`, { cause: parked.cause });
130
+ }
131
+ let backendFidelity;
132
+ try {
133
+ backendFidelity = resolveDeclaredFidelity(checkpointStore, "checkpointStore");
134
+ }
135
+ catch (err) {
136
+ throw new ParkRefusal(`the checkpoint backend declares an unrecognized encoding fidelity, so the width of the snapshot this row may hold is unknown (${describeThrown(err)}) — ` +
137
+ `refusing to park rather than guess which values would survive it`, { cause: err });
138
+ }
139
+ const captured = encodeAtFidelity(backendFidelity, parked.value);
140
+ if (!captured.ok) {
141
+ throw new ParkRefusal(`the arguments of "${req.toolName}" cannot be encoded by this checkpoint backend (${describeThrown(captured.cause)}) — ` +
142
+ `a durable approval row must hold an inert snapshot of exactly what a human approves and a resume executes`, { cause: captured.cause });
143
+ }
144
+ const parkedArgs = captured.value;
145
+ const projectionMovedArgs = backendFidelity !== "structured-clone" && !samePlainValue(parked.value, parkedArgs);
146
+ if (projectionMovedArgs && basePolicyForResumeEdit === undefined) {
147
+ throw new ParkRefusal(`the arguments of "${req.toolName}" change shape when stored by this checkpoint backend, and no policy is wired to re-adjudicate the stored shape ` +
148
+ `(no basePolicyForResumeEdit override and no caller toolPolicy) — refusing to file a row whose executable form nothing adjudicated`);
149
+ }
150
+ if (projectionMovedArgs && basePolicyForResumeEdit !== undefined) {
151
+ const presented = encodeAtFidelity(backendFidelity, parkedArgs);
152
+ if (!presented.ok) {
153
+ throw new ParkRefusal(`the stored form of "${req.toolName}"'s arguments could not be presented for re-adjudication (${describeThrown(presented.cause)})`, { cause: presented.cause });
154
+ }
155
+ const reprojectedRaced = await raceSettlementAgainstSignal(Promise.resolve(basePolicyForResumeEdit.check({
156
+ toolName: req.toolName,
157
+ args: presented.value,
158
+ toolCallId: req.toolCallId,
159
+ budget: budgetSnapshot,
160
+ ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
161
+ }, cutSignal)), cutSignal);
162
+ if (reprojectedRaced.tag === "aborted")
163
+ return undefined;
164
+ if (reprojectedRaced.tag === "threw")
165
+ throw reprojectedRaced.error;
166
+ const reprojected = refuseOutOfContractDecision(reprojectedRaced.value);
167
+ const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
168
+ const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
169
+ !(demanded?.ok === true &&
170
+ samePlainValue(demanded.value, parkedArgs) &&
171
+ samePlainValue(reprojected.updatedInput, parkedArgs));
172
+ if (reprojected.action === "deny" || rewroteTheFiledValue) {
173
+ throw new ParkRefusal(`the arguments of "${req.toolName}" change shape when stored by this checkpoint backend, and the deployment's tool policy does not accept the stored shape` +
174
+ `${reprojected.action === "deny" && reprojected.message ? `: ${reprojected.message}` : ""} — ` +
175
+ `refusing to file a row whose executable form nothing adjudicated`);
176
+ }
177
+ }
178
+ if (offloadStore && isVolatileOffloadStore(offloadStore) && !offloadStore.isEmpty()) {
179
+ throw new ParkRefusal("durable suspend requires a durable toolResultStore (the default InMemoryToolResultStore " +
180
+ "holds offloaded results that are lost across a cross-replica resume); inject " +
181
+ "RunnerDeps.toolResultStore or disable offload.");
182
+ }
183
+ if (ownedEnv !== undefined && remoteEnv === undefined && parkOnlyRemoteEnv === undefined) {
184
+ throw new ParkRefusal(incompleteSuspendAdapter !== undefined
185
+ ? `durable suspend is not supported with this per-task executionEnvFactory env: it declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")}, so a snapshot taken now could never be restored. Implement the full RemoteExecutionEnv restore surface, or declare capabilities.suspendable:false if the workspace is externally durable (the park-only lane).`
186
+ : "durable suspend is not supported with a non-remote per-task executionEnvFactory env: the " +
187
+ "minted env is destroyed on suspend, so a resumed file/shell tool would act on a fresh " +
188
+ "(empty) env. Use a RemoteExecutionEnv factory (it is paused, not destroyed) or a static, " +
189
+ "caller-owned RunnerDeps.executionEnv.");
190
+ }
191
+ const leafId = await session.getLeafId();
192
+ if (!leafId) {
193
+ throw new ParkRefusal("cannot suspend: session has no committed leaf to resume from");
194
+ }
195
+ const { messages } = await session.buildContext();
196
+ const { batchToolCallIds, completedCallIds } = batchContextAt(messages, req.toolCallId);
197
+ if (cutSignal.aborted) {
198
+ return undefined;
199
+ }
200
+ if (suspendLoopCapHit(suspendChainBase(), maxSuspends, ` for tool "${req.toolName}" — likely a resume/restart loop.`))
201
+ return undefined;
202
+ if (remoteEnv !== undefined) {
203
+ if (hasBackgroundShell(remoteEnv))
204
+ await sweepBackgroundShells(remoteEnv, defaultTaskRegistry);
205
+ const snap = await remoteEnv.suspendVM({ abortSignal: cutSignal });
206
+ if (!snap.ok) {
207
+ remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
208
+ throw new Error(`suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error });
209
+ }
210
+ remoteHandle = { ...remoteEnv.workspaceHandle(), snapshotId: snap.value };
211
+ if (cutSignal.aborted) {
212
+ const back = await compensatePausedVM(remoteEnv, snap.value);
213
+ if (!back.ok) {
214
+ abortController.abort();
215
+ void harness.abort();
216
+ }
217
+ return undefined;
218
+ }
219
+ }
220
+ else if (parkOnlyRemoteEnv !== undefined) {
221
+ if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
222
+ await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
223
+ remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
224
+ }
225
+ token = mintCheckpointToken();
226
+ const riskDescriptor = buildRiskDescriptor({
227
+ toolName: req.toolName,
228
+ args: parkedArgs,
229
+ safety,
230
+ ...(shadowedRule !== undefined ? { shadowedRule } : {}),
231
+ ...(probeReason !== undefined ? { probeReason } : {}),
232
+ ...(probeCause !== undefined ? { probeCause } : {}),
233
+ shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
234
+ ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
235
+ });
236
+ gate =
237
+ safety !== undefined || realApproval !== undefined
238
+ ? {
239
+ kind: "irreversible_ask",
240
+ reason: safety !== undefined
241
+ ? `human approval required before safety-tightened tool "${req.toolName}"`
242
+ : `real human approval required for tool "${req.toolName}" (non-budgetable: ${realApproval.origin})`,
243
+ toolName: req.toolName,
244
+ ...(safety !== undefined ? { safetyAxis: safety } : {}),
245
+ ...(realApproval !== undefined ? { realApproval } : {}),
246
+ riskDescriptor,
247
+ }
248
+ : durableApproval
249
+ ? {
250
+ kind: "human",
251
+ reason: `human approval required for tool "${req.toolName}"`,
252
+ toolName: req.toolName,
253
+ riskDescriptor,
254
+ }
255
+ : {
256
+ kind: "irreversible_ask",
257
+ reason: `human approval required before irreversible tool "${req.toolName}"`,
258
+ toolName: req.toolName,
259
+ riskDescriptor,
260
+ };
261
+ const scope = durableApproval?.scope || checkpointScopeOf({ principal: spec.principal });
262
+ const ttlMs = sanitizedTtlMs(durableApproval?.ttlMs);
263
+ const checkpointState = serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd());
264
+ if (realApproval !== undefined && checkpointState.readFace !== undefined) {
265
+ checkpointState.readFace = { ...checkpointState.readFace, realApproval: true };
266
+ }
267
+ const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
268
+ const mintedAt = Date.now();
269
+ cp = {
270
+ token,
271
+ checkpointId: mintCheckpointId(),
272
+ version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
273
+ scope,
274
+ sessionId,
275
+ leafId,
276
+ ...pauseOf(gate, {
277
+ kind: "tool_approval",
278
+ toolCallId: req.toolCallId,
279
+ toolName: req.toolName,
280
+ args: parkedArgs,
281
+ ...(() => {
282
+ const p = approvalPreviewOf(req.toolName, parkedArgs);
283
+ const bidi = carriesBidiControls(parkedArgs) || carriesBidiControls(p.preview);
284
+ return {
285
+ ...(p.preview !== undefined ? { preview: p.preview } : {}),
286
+ ...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}),
287
+ ...(bidi ? { hasBidiControls: true } : {}),
288
+ };
289
+ })(),
290
+ ...(realApproval !== undefined ? { requiresRealApproval: true } : {}),
291
+ ...askCarryRowMembers(carry),
292
+ ...ruleOffersOf(req.toolName, parkedArgs, {
293
+ ...(realApproval !== undefined ? { requiresRealApproval: true } : {}),
294
+ ...(shadowedRule !== undefined ? { persistedRuleShadowed: shadowedRule } : {}),
295
+ ...(askDecisionReason !== undefined ? { decisionReason: askDecisionReason } : {}),
296
+ ...(matchedAskRule !== undefined ? { matchedAskRule } : {}),
297
+ ...(probeMandated === true ? { probeMandated: true } : {}),
298
+ ...(inheritedUnavailableAsks.has(req.toolCallId) ? { inheritedUnresolved: true } : {}),
299
+ ...(segmentCoverage !== undefined ? { segmentCoverage } : {}),
300
+ }),
301
+ boundInputHash: boundInputHashOf(parkedArgs),
302
+ batchToolCallIds,
303
+ completedCallIds,
304
+ }),
305
+ state: checkpointState,
306
+ deadline: ttlMs !== undefined
307
+ ? mintedAt + ttlMs
308
+ : durableApproval !== undefined
309
+ ? undefined
310
+ : mintedAt + DEFAULT_UNATTENDED_APPROVAL_TTL_MS,
311
+ status: "pending",
312
+ createdAt: mintedAt,
313
+ suspendedAt: now(),
314
+ humanReview: humanReviewRef.count > 0
315
+ ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
316
+ : undefined,
317
+ suspendCount: suspendChainBase() + 1,
318
+ resourceLedger: approvalLedger,
319
+ sourceTaskId: sessionId,
320
+ ...(spec.principal ? { principal: spec.principal } : {}),
321
+ ...(durableApproval !== undefined ? { durableApproval: { ...durableApproval } } : {}),
322
+ };
323
+ }
324
+ catch (err) {
325
+ const cutWon = cutSignal.aborted;
326
+ const preselected = cutWon
327
+ ? undefined
328
+ : {
329
+ parkFailed: err instanceof ParkRefusal
330
+ ? err.message
331
+ : "the durable approval park could not be prepared (the deployment's error face carries the exception)",
332
+ };
333
+ try {
334
+ deps.onError?.(err, { phase: "config", sessionId });
335
+ }
336
+ catch {
337
+ }
338
+ return preselected;
339
+ }
340
+ const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle, cutSignal);
341
+ if (committed.tag !== "committed") {
342
+ if (committed.tag === "cut")
343
+ return undefined;
344
+ if (committed.tag === "absent") {
345
+ if (cutSignal.aborted)
346
+ return undefined;
347
+ return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
348
+ }
349
+ return { parkFailed: committed.reason };
350
+ }
351
+ publishCommittedSuspend({ pausedRef }, token, gate, cp.scope, remoteHandle, cp.checkpointId, cp.pendingAction);
352
+ try {
353
+ await sessions.pin?.(sessionId);
354
+ }
355
+ catch (pinErr) {
356
+ deps.onError?.(pinErr, { phase: "config", sessionId });
357
+ }
358
+ abortController.abort();
359
+ void harness.abort();
360
+ return { token, gate };
361
+ }
362
+ : undefined;
363
+ return { parkAsk: { resolveContentAsk, suspendAsk } };
364
+ }
365
+ export function batchContextAt(messages, currentId) {
366
+ let batch = [];
367
+ for (const m of messages) {
368
+ if (m.role === "assistant") {
369
+ const ids = [...new Set(m.content.filter((c) => c.type === "toolCall").map((c) => c.id))];
370
+ if (ids.includes(currentId))
371
+ batch = ids;
372
+ }
373
+ }
374
+ const resolved = new Set(messages.filter((m) => m.role === "toolResult").map((m) => m.toolCallId));
375
+ const completedCallIds = batch.filter((id) => id !== currentId && resolved.has(id));
376
+ return { batchToolCallIds: batch, completedCallIds };
377
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * design/390 §1.2 M17 (design/238 P21) — prepareTask's POLICY-CHAIN phase, verbatim from the driver: the caller policy
3
+ * and its name audit (advisory once per deployment; the two hard refusals), the session / ancestor / frozen-projection /
4
+ * skill-scope deny-narrowing layers and their fold, the resume-edit base policy, the hook record with its timeout seat
5
+ * and observational half, the same-instance arbitration machinery (the caller slot's first-decision record), the
6
+ * inherited-unavailable marker channel, the RB-40 grant record, the sandbox-admission mode's arming and its three
7
+ * sideband maps, the source-identity and risk-axes factories, the two permission-rule lanes and the rule-offer
8
+ * factory, the approved-edit re-check, the inherited-constraint wrappers (the same-instance adapter and the true
9
+ * wrapper), the rewrite-emitter index and the instrumented chain, the integrity guards, the fold, and the final-form
10
+ * re-check that is the effective policy. The advised-names dedupe pair only this section reads moved with it.
11
+ * The interface is the dependency list the segment had implicitly (design/238 R-1).
12
+ *
13
+ * ASYNC PHASE: the stretch awaits the session-policy store (the task's own rules and each ancestor's live re-read)
14
+ * exactly where the segment did; the closures it builds await at tool-call time. The phase boundary adds one
15
+ * continuation between its last await and the driver's next read — the accepted class under {@link RunInternals}
16
+ * `@contract prepare.deps-read-stable`.
17
+ *
18
+ * NOT CHANGED HERE: the ask-origin vocabulary. This phase is a mint site for `AskOrigin` words (through the inherited
19
+ * arms' `judgeInheritedClassifier` / `inheritedAskCarry`), and it carries them verbatim — the closed set, its fence and
20
+ * every spelling are exactly the driver's.
21
+ */
22
+ import type { AgentTool } from "../../internal/harness.js";
23
+ import type { ExecutionEnv } from "../../internal/harness.js";
24
+ import { type AskClass } from "../ask-class.js";
25
+ import type { AutoModeDecider } from "../auto-mode.js";
26
+ import { type Hooks } from "../hooks.js";
27
+ import type { LockedPreflight } from "../locked-config.js";
28
+ import type { SessionPermissionRules } from "../session-policy-store.js";
29
+ import { type OnAsk, type ToolPolicy } from "../tool-policy.js";
30
+ import type { RunnerDeps, RuntimeCaps, TaskSpec, ToolEffect } from "../types.js";
31
+ import type { CwdRef } from "../../tools/fs/fs-shared.js";
32
+ import type { ReadDenyMatcher } from "../../tools/fs/read-deny.js";
33
+ import { type ActiveSkillScope } from "./active-skill-scope.js";
34
+ import type { InheritedGate, IrreversibilityTier, PrepareResume, RunInternals } from "./contracts.js";
35
+ import { type StopForDenialLimit } from "./denial-limit-arms.js";
36
+ import { createRuleOffersOf, type PermissionRuleLanes } from "./permission-rule-lanes.js";
37
+ export interface PreparePolicyChainInput {
38
+ /** borrowed-readonly — the REBOUND spec. Read: `sessionId` / `principal` (the session-rule read and the lanes' identity),
39
+ * `hooks` (spec over deps), `durableApproval` / `checkpointStore` (the mandate-park facility test and the sandbox arming's park prerequisites), `taskId` (the late-ask observer's detail).
40
+ * Never mutated. */
41
+ spec: Pick<TaskSpec, "sessionId" | "principal" | "hooks" | "durableApproval" | "taskId" | "checkpointStore">;
42
+ /** borrowed-readonly — the deployment seats: `sessionPolicyStore` (the rule reads), `hooks` (the fallback record),
43
+ * `permissionRuleStore` / `localOwnerRules` (the lanes and the offer factory), `onError` / `onNotice` (the advisories,
44
+ * the refusal disclosures, the late-ask observer), `tracer` (the admission and rule-store frames), `checkpointStore` (the park facility). Receiver preserved;
45
+ * the advisory dedupe is KEYED on this object's identity. */
46
+ deps: Pick<RunnerDeps, "sessionPolicyStore" | "hooks" | "permissionRuleStore" | "localOwnerRules" | "onError" | "onNotice" | "tracer" | "checkpointStore">;
47
+ /** borrowed-readonly — the trusted spawn-side channel: `parentTaskId` (the from-subagent bit), `explicitAgentName` /
48
+ * `agentName` (the source agent name) — the ask source identity's local, trusted facts. */
49
+ internals: Pick<RunInternals, "parentTaskId" | "explicitAgentName" | "agentName"> | undefined;
50
+ /** borrowed-readonly — the acquired session id (every operator line's attribution; the ask source's task id). */
51
+ sessionId: string;
52
+ /** borrowed-readonly — the run's invocation id (the late-ask observer's detail). */
53
+ runId: string;
54
+ /** borrowed-readonly — the per-task identity (the trace frames' taskId; the rule-store disclosure). */
55
+ hostTaskId: string;
56
+ /** borrowed-readonly — the FINAL task root (path rules resolve against it; the lanes' and the offers' root). */
57
+ taskRootFinal: string;
58
+ /** borrowed-readonly — the run's resolved env (path rules resolve in it; the sandbox declaration is read off it). */
59
+ executionEnv: ExecutionEnv;
60
+ /** borrowed-readonly — the frozen preflight: `toolPolicy` (the caller slot) and `basePolicyForResumeEdit` (the lock-aware
61
+ * resume-edit override). */
62
+ lockedPreflight: Pick<LockedPreflight, "toolPolicy" | "basePolicyForResumeEdit">;
63
+ /** borrowed-readonly — the run's ONE tool roster, read only here (the name audit's universe: names + aliases). */
64
+ tools: ReadonlyArray<AgentTool>;
65
+ /** borrowed-readonly — the harness list (placeholders keep their deferred names), the audit universe's second half. */
66
+ harnessTools: ReadonlyArray<AgentTool>;
67
+ /** borrowed-readonly — the effect collection, read by the narrowing layers (path-confinable write tools). */
68
+ toolEffects: ReadonlyMap<string, ToolEffect>;
69
+ /** borrowed-readonly — the egress mark set (the risk axes, the boundary predicate, the offer factory). */
70
+ egressTools: ReadonlySet<string>;
71
+ /** borrowed-readonly — the folded irreversibility tiers (the risk axes, the offer factory). */
72
+ irreversibilityTier: IrreversibilityTier;
73
+ /** borrowed-readonly — the explicit negative declarations per tool (the risk axes' "engine judged it false"). */
74
+ axisExplicitNegatives: ReadonlyMap<string, {
75
+ irreversible?: false;
76
+ egress?: false;
77
+ }>;
78
+ /** borrowed-readonly — the caller tools' names, the first-await snapshot (the sandbox boundary predicate: a caller tool
79
+ * is never contained by the hand tools' env). */
80
+ ownToolNames: ReadonlySet<string>;
81
+ /** borrowed-readonly — whether the shell tool is under an operator `shellGate:"always"` (the offer factory's mandate term). */
82
+ shellGatedBash: boolean;
83
+ /** borrowed-readonly — the tracked-cwd seat, read per call by the lanes and the offer factory (never written here). */
84
+ handsCwdRef: CwdRef | undefined;
85
+ /** borrowed-readonly — the compiled read-deny judge, or undefined (the offer factory's directory negative control). */
86
+ readDenyMatcher: ReadDenyMatcher | undefined;
87
+ /** borrowed-readonly — whether the question tool mounted (the lanes' question-tool exclusion). */
88
+ questionToolMounted: boolean;
89
+ /** borrowed-readonly — the resolved per-principal entitlements; only `forceDurableGate` (the mandate-park facility and
90
+ * the sandbox arming's prerequisite). */
91
+ runtimeCaps: Pick<RuntimeCaps, "forceDurableGate"> | undefined;
92
+ /** borrowed-readonly — the ancestors' session-rule snapshots (each re-read live-preferring, floor kept). */
93
+ inheritedAncestorRules: InheritedGate["ancestorRules"];
94
+ /** borrowed-readonly — the ancestors' caller-policy constraints, ancestors first (one wrapper each). */
95
+ inheritedParentConstraints: NonNullable<InheritedGate["parentConstraints"]>[number][] | undefined;
96
+ /** borrowed-readonly — the resumed leg's SEED half; only `constraintChain` (the frozen deny projections). */
97
+ seedInheritedGate: Pick<NonNullable<PrepareResume["seed"]["inheritedGate"]>, "constraintChain"> | undefined;
98
+ /** borrowed-readonly — whether any skill carries a manifest or the task inherited a frame (the skill-scope layer). */
99
+ hasSkillManifest: boolean;
100
+ /** borrowed-readonly — the live skill scope the Gate-3 layer judges over. */
101
+ skillScope: ActiveSkillScope;
102
+ /** borrowed-readonly — the FROZEN approver seat (the pass-through test's approver identity). */
103
+ frozenOnAsk: OnAsk | undefined;
104
+ /** borrowed-readonly — the identity of this task's hook-env source (the pass-through test's environment term). */
105
+ hookEnvSource: unknown;
106
+ /** borrowed-readonly — the own-hook crash reporter (the timeout seat's bad-value disclosure). */
107
+ notifyOwnHookCrash: (err: unknown) => void;
108
+ /** borrowed-readonly — this task's own classifier instance, or undefined (the pass-through test's decider term). */
109
+ autoModeDecider: AutoModeDecider | undefined;
110
+ /** borrowed-readonly — the run's ONE typed stop for a denial-limit bound (the fallback settlements call it). */
111
+ stopForDenialLimit: StopForDenialLimit;
112
+ /** borrowed-readonly — the effective delegation facts; only `isDelegatedChild` (the ask source identity). */
113
+ delegation: {
114
+ isDelegatedChild: boolean;
115
+ };
116
+ /** borrowed-readonly — the run's abort controller; only `signal` (the default call signal of every closure here). */
117
+ abortController: Pick<AbortController, "signal">;
118
+ /** borrowed-readonly — the injectable wall-clock (the inherited arms' human-wait timing). */
119
+ now: () => number;
120
+ /** borrowed-mutable — the own session-rules snapshot seat (the inherited-gate phase mints it; `inheritedGateForChildren`
121
+ * reads it late). Writer: THIS phase, once, when the store returns this task's own rules. */
122
+ ownSessionRulesRef: {
123
+ current?: {
124
+ sessionId: string;
125
+ principal?: string;
126
+ rev: number;
127
+ rules: SessionPermissionRules;
128
+ };
129
+ };
130
+ }
131
+ export interface PreparePolicyChainResult {
132
+ /** owned — the resolved hook record (`spec.hooks ?? deps.hooks`), or undefined. */
133
+ hooks: Hooks | undefined;
134
+ /** owned — the per-leg hook timeout, resolved and validated ONCE (`Prepared.hookTimeoutMs`). */
135
+ hookTimeoutMs: number;
136
+ /** owned — whether the deployment declared its screening face observational. */
137
+ preToolUseObservational: boolean;
138
+ /** owned — the screening face this task's OWN gate runs in phase 1 (the observational wrapper when declared). */
139
+ ownGatePreToolUse: Hooks["preToolUse"] | undefined;
140
+ /** owned — the resume-edit base policy (`Prepared.basePolicyForResumeEdit`), or undefined. */
141
+ basePolicyForResumeEdit: ToolPolicy | undefined;
142
+ /** owned — the deny-narrowing fold (`Prepared.denyNarrowingPolicy`), or undefined when no layer exists. */
143
+ denyNarrowingPolicy: ToolPolicy | undefined;
144
+ /** owned — the caller-facing policy layers (the wiring manifest reads their COUNT). */
145
+ policyLayers: ToolPolicy[];
146
+ /** owned — THE effective policy the gate station adjudicates with (the fold plus its final-form re-check), or undefined. */
147
+ effectivePolicy: ToolPolicy | undefined;
148
+ /** borrowed-mutable — the inherited-unavailable marker channel. Writers: the inherited arms here (mark); the gate station
149
+ * (the lifecycle-end sweep) and the park lane's consumers (delete on consumption). */
150
+ inheritedUnavailableAsks: Set<string>;
151
+ /** borrowed-mutable — the RB-40 grant record. Writers: the inherited arms here (record a clean allow at a live approver);
152
+ * the gate station (consume on reuse, sweep at settlement). */
153
+ inheritedAskGrants: Map<string, {
154
+ approver: unknown;
155
+ argsJson: string;
156
+ presented: unknown;
157
+ waitMs: number;
158
+ }>;
159
+ /** owned — the SINGLE AskRequest source-identity factory every resolveAsk construction site spreads. */
160
+ askSourceIdentity: () => {
161
+ principal?: string;
162
+ sourceTaskId?: string;
163
+ fromSubagent?: true;
164
+ sourceAgentName?: string;
165
+ isDelegatedChild?: true;
166
+ };
167
+ /** owned — the engine-judged risk axes for an ask, from the same resolved sources the safety tightens use. */
168
+ riskAxesOf: (toolName: string) => {
169
+ riskAxes?: {
170
+ irreversible?: boolean;
171
+ egress?: boolean;
172
+ };
173
+ };
174
+ /** owned — the ONE rule-offer factory for all five ask-mint sites. */
175
+ ruleOffersOf: ReturnType<typeof createRuleOffersOf>;
176
+ /** owned — the personal permission-rule lane, or undefined. */
177
+ permissionRuleLane: PermissionRuleLanes["personal"];
178
+ /** owned — the org permission-rule lane (`Prepared.permissionRuleOrg`), or undefined. */
179
+ permissionRuleOrgLane: PermissionRuleLanes["org"];
180
+ /** owned — whether the sandbox-admission mode armed (declared isolation with its park prerequisites). */
181
+ sandboxAdmissionArmed: boolean;
182
+ /** owned — the NOT-CONTAINED classification: a call the declared sandbox does not contain. */
183
+ sandboxBoundaryCapable: (toolName: string) => boolean;
184
+ /** owned — the admission disclosure's trace emitter (the gate station emits it once the fold settles). */
185
+ emitSandboxAdmitted: (info: {
186
+ toolCallId: string;
187
+ toolName: string;
188
+ askClasses: ReadonlyArray<{
189
+ cls: AskClass;
190
+ layer: string;
191
+ }>;
192
+ boundary: "sandbox_internal";
193
+ }) => void;
194
+ /** borrowed-mutable — the per-call fold ask-class record. Writers: the instrumented chain here (record); the gate station
195
+ * (read for strict dominance, then sweep). */
196
+ foldAskClasses: Map<string, Array<{
197
+ cls: AskClass;
198
+ layer: string;
199
+ }>>;
200
+ /** borrowed-mutable — the ancestor arms' deferred sandbox admissions. Writers: the inherited arms here (record); the gate
201
+ * station (emit once the fold settles, then sweep). */
202
+ ancestorSandboxAdmissions: Map<string, Array<{
203
+ cls: AskClass;
204
+ layer: string;
205
+ }>>;
206
+ }
207
+ /** The M17 phase body — prepareTask's policy-chain stretch, verbatim (see the module header). */
208
+ export declare function preparePolicyChain(input: PreparePolicyChainInput): Promise<PreparePolicyChainResult>;