@tangle-network/agent-runtime 7.0.0 → 8.0.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 (124) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +0 -3
  3. package/dist/{activation-Ba9IAgQ3.js → activation-8AdN-3r4.js} +4 -4
  4. package/dist/{activation-Ba9IAgQ3.js.map → activation-8AdN-3r4.js.map} +1 -1
  5. package/dist/{activation-DIkTEBWF.d.ts → activation-xxvLF1-1.d.ts} +2 -2
  6. package/dist/agent.d.ts +1 -1
  7. package/dist/agent.js +3 -3
  8. package/dist/{bundle-Cy5igJdv.js → bundle-CcpL2lfr.js} +2 -2
  9. package/dist/{bundle-Cy5igJdv.js.map → bundle-CcpL2lfr.js.map} +1 -1
  10. package/dist/candidate-execution/index.js +4 -4
  11. package/dist/continuation-CIyKnS1Q.js +451 -0
  12. package/dist/continuation-CIyKnS1Q.js.map +1 -0
  13. package/dist/{coordination-manager-DQZ2u6L-.js → coordination-manager-DAGPerJy.js} +194 -148
  14. package/dist/coordination-manager-DAGPerJy.js.map +1 -0
  15. package/dist/{coordination-map-routing-CKRNZSaV.js → coordination-map-routing-XZXTlCmn.js} +2 -4
  16. package/dist/coordination-map-routing-XZXTlCmn.js.map +1 -0
  17. package/dist/{delegate-DFLSVjFG.js → delegate-CoaNr0hO.js} +3 -3
  18. package/dist/{delegate-DFLSVjFG.js.map → delegate-CoaNr0hO.js.map} +1 -1
  19. package/dist/durable.d.ts +1 -1
  20. package/dist/durable.js +13 -12
  21. package/dist/durable.js.map +1 -1
  22. package/dist/{errors-AzSuGI23.js → errors-DG8yufqX.js} +8 -1
  23. package/dist/errors-DG8yufqX.js.map +1 -0
  24. package/dist/{failure-cause-rJTm_Tx8.js → failure-cause-CVB-YE88.js} +12 -8
  25. package/dist/{failure-cause-rJTm_Tx8.js.map → failure-cause-CVB-YE88.js.map} +1 -1
  26. package/dist/graph-pFTeNZ-G.js +1793 -0
  27. package/dist/graph-pFTeNZ-G.js.map +1 -0
  28. package/dist/{graph-registry-7X2nrreR.js → graph-registry-czAwYwNj.js} +133 -9
  29. package/dist/graph-registry-czAwYwNj.js.map +1 -0
  30. package/dist/{graph-registry-gYbq4L5Z.js → graph-registry-uTIyyy7A.js} +1 -1
  31. package/dist/{improve-BolbYegO.js → improve-I43jex8v.js} +19 -48
  32. package/dist/improve-I43jex8v.js.map +1 -0
  33. package/dist/index.d.ts +3 -3
  34. package/dist/index.js +16 -16
  35. package/dist/intelligence.d.ts +2 -2
  36. package/dist/intelligence.js +8 -8
  37. package/dist/intelligence.js.map +1 -1
  38. package/dist/kernel.d.ts +2 -2
  39. package/dist/kernel.js +16 -15
  40. package/dist/{loop-runner-Cs9IJ6Nk.d.ts → loop-runner-BGwhcn1p.d.ts} +2 -2
  41. package/dist/{loop-runner-C9W3T_3T.js → loop-runner-BN4KozR0.js} +3 -3
  42. package/dist/{loop-runner-C9W3T_3T.js.map → loop-runner-BN4KozR0.js.map} +1 -1
  43. package/dist/loop-runner-bin.d.ts +1 -1
  44. package/dist/loop-runner-bin.js +1 -1
  45. package/dist/mcp/bin.js +7 -5
  46. package/dist/mcp/bin.js.map +1 -1
  47. package/dist/mcp/index.d.ts +1 -1
  48. package/dist/mcp/index.js +9 -9
  49. package/dist/mcp/memory-bin.js +2 -2
  50. package/dist/{model-policy-JOcg3NRs.js → model-policy-CZwhXp8O.js} +15 -10
  51. package/dist/model-policy-CZwhXp8O.js.map +1 -0
  52. package/dist/{prepare-Dtfk_Wi_.js → prepare-BnF8nGIg.js} +4 -4
  53. package/dist/{prepare-Dtfk_Wi_.js.map → prepare-BnF8nGIg.js.map} +1 -1
  54. package/dist/{profile-CNSQjOes.js → profile-CD25rTJF.js} +4 -4
  55. package/dist/{profile-CNSQjOes.js.map → profile-CD25rTJF.js.map} +1 -1
  56. package/dist/profiles.d.ts +1 -1
  57. package/dist/profiles.js +2 -2
  58. package/dist/{provision-supervisor-BQFHjCtN.js → provision-supervisor-DvzayGpe.js} +8 -8
  59. package/dist/{provision-supervisor-BQFHjCtN.js.map → provision-supervisor-DvzayGpe.js.map} +1 -1
  60. package/dist/{run-cancellation-B84X8ZAP.js → run-cancellation-C5__aBKW.js} +4 -4
  61. package/dist/{run-cancellation-B84X8ZAP.js.map → run-cancellation-C5__aBKW.js.map} +1 -1
  62. package/dist/{run-state-Dh1bGERI.js → run-state-CJAaxqfp.js} +5 -5
  63. package/dist/{run-state-Dh1bGERI.js.map → run-state-CJAaxqfp.js.map} +1 -1
  64. package/dist/{runtime-C9F1PG2A.js → runtime-CuwHe8_K.js} +76 -987
  65. package/dist/runtime-CuwHe8_K.js.map +1 -0
  66. package/dist/{sandbox-profile-compat-oFGWESLn.js → sandbox-profile-compat-l-SLQnGb.js} +2 -2
  67. package/dist/{sandbox-profile-compat-oFGWESLn.js.map → sandbox-profile-compat-l-SLQnGb.js.map} +1 -1
  68. package/dist/{search-executor-CKR9_6NR.js → search-executor-DAjm-NlG.js} +17 -46
  69. package/dist/search-executor-DAjm-NlG.js.map +1 -0
  70. package/dist/{server-Dh3SQGQT.js → server-BUpNUn5D.js} +32 -8
  71. package/dist/{server-Dh3SQGQT.js.map → server-BUpNUn5D.js.map} +1 -1
  72. package/dist/{settle-record-C6LdVvok.js → settle-record-CeBdBuxP.js} +2 -2
  73. package/dist/{settle-record-C6LdVvok.js.map → settle-record-CeBdBuxP.js.map} +1 -1
  74. package/dist/{snapshot-Cgj2Wzi6.js → snapshot-BKI_w3JD.js} +19 -5
  75. package/dist/snapshot-BKI_w3JD.js.map +1 -0
  76. package/dist/{structural-rollout-Dapf2aZY.js → structural-rollout-CqxBKxKY.js} +18 -54
  77. package/dist/structural-rollout-CqxBKxKY.js.map +1 -0
  78. package/dist/{supervise-xwl2D6q3.js → supervise-Cdqj_oe4.js} +101 -111
  79. package/dist/supervise-Cdqj_oe4.js.map +1 -0
  80. package/dist/{supervision-discovery-B47U8TH5.js → supervision-discovery-CW-ildZ-.js} +4 -4
  81. package/dist/{supervision-discovery-B47U8TH5.js.map → supervision-discovery-CW-ildZ-.js.map} +1 -1
  82. package/dist/{supervisor-gC-u_PRk.js → supervisor-D-eT-6Tu.js} +206 -693
  83. package/dist/supervisor-D-eT-6Tu.js.map +1 -0
  84. package/dist/{supervisor-accounting-DxLQ_TQe.js → supervisor-accounting-Ow6CChpB.js} +434 -423
  85. package/dist/supervisor-accounting-Ow6CChpB.js.map +1 -0
  86. package/dist/testing.d.ts +1 -1
  87. package/dist/testing.js +61 -61
  88. package/dist/{tool-server-Dg350Bgz.js → tool-server-DYwRbi0I.js} +3 -3
  89. package/dist/{tool-server-Dg350Bgz.js.map → tool-server-DYwRbi0I.js.map} +1 -1
  90. package/dist/{top-web-BJLnFQdr.js → top-web-BuYMkJN4.js} +9 -7
  91. package/dist/{top-web-BJLnFQdr.js.map → top-web-BuYMkJN4.js.map} +1 -1
  92. package/dist/tui/bin.js +3 -3
  93. package/dist/tui/index.d.ts +1 -1
  94. package/dist/tui/index.js +2 -2
  95. package/dist/{types-Jg274chm.d.ts → types-C_qbnw9N.d.ts} +1335 -1413
  96. package/dist/{util-C_cdPNC4.js → util-fcckrkKp.js} +39 -113
  97. package/dist/util-fcckrkKp.js.map +1 -0
  98. package/dist/viewer/report-viewer.js +90 -90
  99. package/dist/viewer/runtime-viewer/AgentGraphCanvas-P_PFS2Ko-B7S6Fwwy.js +563 -0
  100. package/dist/viewer/runtime-viewer/latest-events-wofY6ifW-B8_bfLwI.js +17870 -0
  101. package/dist/viewer/styles.css +1592 -7027
  102. package/dist/viewer/workspace-viewer.js +10926 -10522
  103. package/dist/{workspace-streams-B7yNMahl.js → workspace-streams-DA9R3j-p.js} +2 -2
  104. package/dist/{workspace-streams-B7yNMahl.js.map → workspace-streams-DA9R3j-p.js.map} +1 -1
  105. package/package.json +9 -9
  106. package/dist/coordination-manager-DQZ2u6L-.js.map +0 -1
  107. package/dist/coordination-map-routing-CKRNZSaV.js.map +0 -1
  108. package/dist/errors-AzSuGI23.js.map +0 -1
  109. package/dist/graph-BjQF2syl.js +0 -579
  110. package/dist/graph-BjQF2syl.js.map +0 -1
  111. package/dist/graph-registry-7X2nrreR.js.map +0 -1
  112. package/dist/improve-BolbYegO.js.map +0 -1
  113. package/dist/model-policy-JOcg3NRs.js.map +0 -1
  114. package/dist/runtime-C9F1PG2A.js.map +0 -1
  115. package/dist/search-executor-CKR9_6NR.js.map +0 -1
  116. package/dist/snapshot-Cgj2Wzi6.js.map +0 -1
  117. package/dist/structural-rollout-Dapf2aZY.js.map +0 -1
  118. package/dist/supervise-xwl2D6q3.js.map +0 -1
  119. package/dist/supervisor-accounting-DxLQ_TQe.js.map +0 -1
  120. package/dist/supervisor-gC-u_PRk.js.map +0 -1
  121. package/dist/util-C_cdPNC4.js.map +0 -1
  122. package/dist/viewer/runtime-viewer/AgentGraphCanvas-DGq7_u8--C6yDfIUT.js +0 -8555
  123. package/dist/viewer/runtime-viewer/latest-events-CRq9sGkK-CfoBl1dP.js +0 -9592
  124. package/dist/viewer/runtime-viewer/sandbox-ui-BEziPi7D.css +0 -2
@@ -0,0 +1,1793 @@
1
+ import { Jr as runtimeCheckFactory, Xr as runtimeFlowBrain, si as runAbortable } from "./supervisor-accounting-Ow6CChpB.js";
2
+ import { i as ConfigError, m as ValidationError } from "./errors-DG8yufqX.js";
3
+ import { I as armDeadlineTimer } from "./util-fcckrkKp.js";
4
+ import { M as contentAddress, v as retainedExecutorContext } from "./failure-cause-CVB-YE88.js";
5
+ import { Mt as defaultRunCommand, Nt as runSettledCommand } from "./supervisor-D-eT-6Tu.js";
6
+ import { v as harnessRunsAgent } from "./model-policy-CZwhXp8O.js";
7
+ import { a as readCommittedJsonLines, c as appendCommittedJsonLine, s as withJsonlFileLock } from "./jsonl-file-DsxVI0E_.js";
8
+ import { S as captureMaterializedWorkspace, T as scanMaterializedWorkspaceManifest, g as persistCandidateOutputArtifact, y as WorkspaceScanLimitError, z as sha256Bytes$1 } from "./workspace-streams-DA9R3j-p.js";
9
+ import { t as composeRuntimeHooks } from "./runtime-hooks-CsW2pfFk.js";
10
+ import { f as createFileRunContext, m as withRunContext, p as createInMemoryRunContext } from "./coordination-manager-DAGPerJy.js";
11
+ import { M as kernelPromptRegistry, S as validateFlowGraph, T as validateGraphTopology, _ as GraphRouteError, b as journaledGateDecider, g as createGraphOverlay, i as profileGuidanceComposer, r as superviseWithTestBrain, t as supervise, v as createFlowRun, w as graphRootProfile, x as routeFlowOf, y as flowRootProfile } from "./supervise-Cdqj_oe4.js";
12
+ import { S as portableContinuationPolicySchema } from "./graph-registry-czAwYwNj.js";
13
+ import { canonicalCandidateDigest } from "@tangle-network/agent-interface";
14
+ import { randomUUID } from "node:crypto";
15
+ import { constants, mkdirSync, realpathSync } from "node:fs";
16
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
+ import { execFile, spawn } from "node:child_process";
18
+ import { chmod, cp, lstat, mkdir, mkdtemp, open, opendir, readlink, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
19
+ import { promisify } from "node:util";
20
+ import { tmpdir } from "node:os";
21
+ // src/runtime/isolated-checker-outputs.ts
22
+ const MAX_FILES = 16;
23
+ const MAX_BYTES = 67108864;
24
+ const MAX_TIMEOUT_MS = 12e4;
25
+ /** Validate authored declarations before starting a check. The returned paths are detached and frozen. */
26
+ function validateIsolatedCheckOutputs(value) {
27
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new ValidationError("check outputs must be a declaration object");
28
+ const item = value;
29
+ if (Object.keys(item).some((key) => ![
30
+ "paths",
31
+ "maxFileBytes",
32
+ "maxTotalBytes",
33
+ "timeoutMs"
34
+ ].includes(key))) throw new ValidationError("check outputs has an unknown field");
35
+ if (!Array.isArray(item.paths) || item.paths.length < 1 || item.paths.length > MAX_FILES) throw new ValidationError("check outputs must name between 1 and 16 files");
36
+ const paths = [];
37
+ for (const path of item.paths) {
38
+ if (typeof path !== "string" || path.length > 512 || !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/u.test(path) || path.split("/").some((part) => part === "." || part === ".." || part === ".git" || part === ".sidecar") || paths.some((prior) => prior === path || prior.startsWith(`${path}/`) || path.startsWith(`${prior}/`))) throw new ValidationError("check outputs paths must be distinct portable relative files");
39
+ paths.push(path);
40
+ }
41
+ for (const key of ["maxFileBytes", "maxTotalBytes"]) if (typeof item[key] !== "number" || !Number.isSafeInteger(item[key]) || item[key] < 1 || item[key] > MAX_BYTES) throw new ValidationError("check output byte limits must be positive integers at most 64 MiB");
42
+ const maxFileBytes = item.maxFileBytes;
43
+ const maxTotalBytes = item.maxTotalBytes;
44
+ if (maxFileBytes > maxTotalBytes) throw new ValidationError("check outputs maxFileBytes must not exceed maxTotalBytes");
45
+ if (item.timeoutMs !== void 0 && (typeof item.timeoutMs !== "number" || !Number.isSafeInteger(item.timeoutMs) || item.timeoutMs < 1 || item.timeoutMs > MAX_TIMEOUT_MS)) throw new ValidationError("check outputs timeoutMs must be a positive integer at most 120000");
46
+ return Object.freeze({
47
+ paths: Object.freeze(paths),
48
+ maxFileBytes,
49
+ maxTotalBytes,
50
+ ...item.timeoutMs === void 0 ? {} : { timeoutMs: item.timeoutMs }
51
+ });
52
+ }
53
+ /** Resolve and validate trusted ports before any isolated work starts. */
54
+ function prepareIsolatedCheckOutputs(options) {
55
+ if (options === void 0) return void 0;
56
+ const { executionId, artifacts, ...declaration } = options;
57
+ const validated = validateIsolatedCheckOutputs(declaration);
58
+ if (typeof executionId !== "string" || executionId.trim() === "" || executionId.length > 200 || /\p{Cc}/u.test(executionId)) throw new ValidationError("check outputs executionId must be a nonempty bounded identifier");
59
+ if (typeof artifacts?.put !== "function" || typeof artifacts?.read !== "function") throw new ValidationError("check outputs requires a readable output artifact port");
60
+ return Object.freeze({
61
+ ...validated,
62
+ executionId,
63
+ artifacts
64
+ });
65
+ }
66
+ var OutputCaptureError = class extends Error {
67
+ reason;
68
+ constructor(reason) {
69
+ super(reason);
70
+ this.reason = reason;
71
+ }
72
+ };
73
+ /** Capture runs after command settlement, even on command failure or cancellation. There are no retries. */
74
+ async function retainIsolatedCheckOutputs(options, inputDigest, read) {
75
+ const controller = new AbortController();
76
+ const clearTimer = armDeadlineTimer(options.timeoutMs ?? 3e4, () => controller.abort(), true);
77
+ const files = [];
78
+ const missing = [];
79
+ let totalBytes = 0;
80
+ try {
81
+ for (const path of options.paths) {
82
+ let phase = "capture-failed";
83
+ try {
84
+ const bytes = await runAbortable(() => read(path, Math.min(options.maxFileBytes, options.maxTotalBytes - totalBytes), controller.signal), controller.signal, "output capture deadline exceeded");
85
+ totalBytes += bytes.byteLength;
86
+ phase = "store-failed";
87
+ const artifact = await runAbortable(() => persistCandidateOutputArtifact(options.artifacts, {
88
+ executionId: options.executionId,
89
+ purpose: "grader-evidence",
90
+ bytes,
91
+ signal: controller.signal
92
+ }), controller.signal, "output persistence deadline exceeded");
93
+ files.push({
94
+ path,
95
+ artifact
96
+ });
97
+ } catch (error) {
98
+ missing.push({
99
+ path,
100
+ reason: controller.signal.aborted ? "timeout" : error instanceof OutputCaptureError ? error.reason : phase
101
+ });
102
+ }
103
+ }
104
+ } finally {
105
+ clearTimer();
106
+ }
107
+ return Object.freeze({
108
+ executionId: options.executionId,
109
+ inputDigest,
110
+ status: missing.length === 0 ? "retained" : "incomplete",
111
+ files: Object.freeze(files.map((file) => Object.freeze(file))),
112
+ missing: Object.freeze(missing.map((file) => Object.freeze(file)))
113
+ });
114
+ }
115
+ /** The local namespace has exited; the trusted host must keep its private copy stable. */
116
+ function localCheckOutputReader(root) {
117
+ return async (path, maxBytes, signal) => {
118
+ try {
119
+ const rootInfo = await lstat(root);
120
+ if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) throw new OutputCaptureError("unsafe-path");
121
+ let current = root;
122
+ const parts = path.split("/");
123
+ let expected;
124
+ for (let index = 0; index < parts.length; index++) {
125
+ current = join(current, parts[index]);
126
+ const info = await lstat(current, { bigint: true });
127
+ if (info.isSymbolicLink() || (index < parts.length - 1 ? !info.isDirectory() : !info.isFile() || info.nlink !== 1n)) throw new OutputCaptureError("unsafe-path");
128
+ expected = info;
129
+ }
130
+ signal.throwIfAborted();
131
+ const file = await open(current, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
132
+ try {
133
+ const before = await file.stat({ bigint: true });
134
+ if (!before.isFile() || before.nlink !== 1n || before.dev !== expected?.dev || before.ino !== expected?.ino) throw new OutputCaptureError("unsafe-path");
135
+ if (before.size > BigInt(maxBytes)) throw new OutputCaptureError("size-limit");
136
+ const chunks = [];
137
+ let total = 0;
138
+ const buffer = Buffer.allocUnsafe(Math.min(65536, maxBytes + 1));
139
+ while (true) {
140
+ signal.throwIfAborted();
141
+ const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, maxBytes - total + 1), null);
142
+ if (bytesRead === 0) break;
143
+ total += bytesRead;
144
+ if (total > maxBytes) throw new OutputCaptureError("size-limit");
145
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
146
+ }
147
+ const after = await file.stat({ bigint: true });
148
+ if (before.size !== BigInt(total) || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || after.nlink !== 1n) throw new OutputCaptureError("capture-failed");
149
+ return Buffer.concat(chunks, total);
150
+ } finally {
151
+ await file.close();
152
+ }
153
+ } catch (error) {
154
+ if (error?.code === "ENOENT") throw new OutputCaptureError("missing");
155
+ if (["ELOOP", "ENOTDIR"].includes(error?.code ?? "")) throw new OutputCaptureError("unsafe-path");
156
+ throw error;
157
+ }
158
+ };
159
+ }
160
+ /** Preserve a whole box only after its native lifecycle owner confirms stopped state. */
161
+ async function stopCheckBoxForRecovery(box) {
162
+ const controller = new AbortController();
163
+ const clearTimer = armDeadlineTimer(3e4, () => controller.abort(), true);
164
+ try {
165
+ await runAbortable(() => box.stop({
166
+ wait: true,
167
+ signal: controller.signal
168
+ }), controller.signal, "check recovery stop deadline exceeded");
169
+ return true;
170
+ } catch {
171
+ return false;
172
+ } finally {
173
+ clearTimer();
174
+ }
175
+ }
176
+ /** Whether kill and process exit were confirmed; recovery custody still needs a whole-box stop. */
177
+ async function stopCheckProcess(child) {
178
+ const controller = new AbortController();
179
+ const clearTimer = armDeadlineTimer(5e3, () => controller.abort(), true);
180
+ try {
181
+ await runAbortable(async () => {
182
+ await child.kill("SIGKILL", { tree: true });
183
+ await child.wait();
184
+ }, controller.signal, "check process stop deadline exceeded");
185
+ return true;
186
+ } catch {
187
+ return false;
188
+ } finally {
189
+ clearTimer();
190
+ }
191
+ }
192
+ const isolatedCheckOutputCaptureProgram = `import base64, errno, hashlib, json, os, stat, sys
193
+ fds=[]
194
+ def fail(reason):
195
+ print(json.dumps({'error':reason})); sys.exit(0)
196
+ try:
197
+ root,path,limit=sys.argv[1],sys.argv[2],int(sys.argv[3])
198
+ directory=os.open(root,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW); fds.append(directory)
199
+ parts=path.split('/')
200
+ for part in parts[:-1]:
201
+ directory=os.open(part,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=directory); fds.append(directory)
202
+ before=os.stat(parts[-1],dir_fd=directory,follow_symlinks=False)
203
+ if not stat.S_ISREG(before.st_mode) or before.st_nlink!=1: fail('unsafe-path')
204
+ fd=os.open(parts[-1],os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=directory); fds.append(fd)
205
+ opened=os.fstat(fd)
206
+ if not stat.S_ISREG(opened.st_mode) or opened.st_nlink!=1 or (before.st_dev,before.st_ino)!=(opened.st_dev,opened.st_ino): fail('unsafe-path')
207
+ if opened.st_size>limit: fail('size-limit')
208
+ chunks=[]; total=0
209
+ while True:
210
+ chunk=os.read(fd,min(1048576,limit-total+1))
211
+ if not chunk: break
212
+ total+=len(chunk)
213
+ if total>limit: fail('size-limit')
214
+ chunks.append(chunk)
215
+ after=os.fstat(fd)
216
+ if (opened.st_size,opened.st_mtime_ns,opened.st_ctime_ns,opened.st_nlink)!=(after.st_size,after.st_mtime_ns,after.st_ctime_ns,after.st_nlink) or total!=opened.st_size: fail('capture-failed')
217
+ data=b''.join(chunks)
218
+ print(json.dumps({'content':base64.b64encode(data).decode('ascii'),'sha256':'sha256:'+hashlib.sha256(data).hexdigest()}))
219
+ except OSError as error:
220
+ fail('missing' if error.errno==errno.ENOENT else 'unsafe-path' if error.errno in (errno.ELOOP,errno.ENOTDIR) else 'capture-failed')
221
+ finally:
222
+ for fd in reversed(fds): os.close(fd)
223
+ `;
224
+ /** A box with declared outputs needs python3; missing capture tooling fails closed and keeps custody. */
225
+ function boxCheckOutputReader(box, root, timeoutMs) {
226
+ return async (path, maxBytes, signal) => {
227
+ const child = await box.process.spawnExact("/usr/bin/python3", [
228
+ "-I",
229
+ "-S",
230
+ "-c",
231
+ isolatedCheckOutputCaptureProgram,
232
+ root,
233
+ path,
234
+ String(maxBytes)
235
+ ], {
236
+ env: {
237
+ PATH: "/usr/bin:/bin",
238
+ LANG: "C"
239
+ },
240
+ inheritEnv: false,
241
+ timeoutMs
242
+ });
243
+ let stopping;
244
+ const stop = () => {
245
+ stopping ??= stopCheckProcess(child);
246
+ };
247
+ signal.addEventListener("abort", stop, { once: true });
248
+ if (signal.aborted) stop();
249
+ const chunks = [];
250
+ const limit = Math.ceil(maxBytes / 3) * 4 + 2048;
251
+ let length = 0;
252
+ let overflow = false;
253
+ const collect = async (stream, keep) => {
254
+ for await (const text of stream) {
255
+ length += Buffer.byteLength(text);
256
+ if (length > limit) {
257
+ overflow = true;
258
+ stop();
259
+ } else if (keep) chunks.push(text);
260
+ }
261
+ };
262
+ try {
263
+ await Promise.all([collect(child.stdout(), true), collect(child.stderr(), false)]);
264
+ if (overflow || await child.wait() !== 0) throw new OutputCaptureError("capture-failed");
265
+ signal.throwIfAborted();
266
+ const value = JSON.parse(chunks.join(""));
267
+ if (value.error === "missing" || value.error === "unsafe-path" || value.error === "size-limit" || value.error === "capture-failed") throw new OutputCaptureError(value.error);
268
+ if (typeof value.content !== "string" || typeof value.sha256 !== "string") throw new OutputCaptureError("capture-failed");
269
+ const bytes = Buffer.from(value.content, "base64");
270
+ if (bytes.byteLength > maxBytes) throw new OutputCaptureError("size-limit");
271
+ if (sha256Bytes$1(bytes) !== value.sha256) throw new OutputCaptureError("capture-failed");
272
+ return bytes;
273
+ } catch (error) {
274
+ stop();
275
+ throw error;
276
+ } finally {
277
+ signal.removeEventListener("abort", stop);
278
+ await stopping;
279
+ }
280
+ };
281
+ }
282
+ // src/runtime/isolated-checker.ts
283
+ const toolchains = [
284
+ "/usr",
285
+ "/bin",
286
+ "/lib",
287
+ "/lib64"
288
+ ];
289
+ const contains = (parent, child) => {
290
+ const path = relative(parent, child);
291
+ return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !path.startsWith(sep);
292
+ };
293
+ /**
294
+ * Run an untrusted check in Linux Bubblewrap, or in its own Sandbox box when `box` is set.
295
+ * Never falls back to host execution.
296
+ *
297
+ * Bubblewrap requires /usr/bin/bwrap and permission to create Linux namespaces.
298
+ * Only trusted system toolchains, private proc/dev/tmp, and the writable copy are mounted.
299
+ * The canonical input path remains the working directory. Requested output files are retained
300
+ * before cleanup; otherwise copy writes are discarded.
301
+ * Time and output are bounded; input copy bytes and entries are bounded when requested.
302
+ * The command's memory and generated files require a resource-limited placement.
303
+ * Callers must keep the input and trusted toolchains stable while preparing the check.
304
+ *
305
+ * A box check creates one fresh box with no owner secrets and blocked egress (or strict egress to
306
+ * the placement's named domains), delivers the tree's regular files verified by sha256, runs the
307
+ * command there with only the environment `env` names, and deletes the box after successful
308
+ * requested retention. Incomplete retention returns private recovery custody only after the
309
+ * native owner confirms the box stopped. Box file capture requires /usr/bin/python3. Local recovery scratch is the
310
+ * caller's responsibility; the resource-limited placement requirement still applies.
311
+ */
312
+ async function runIsolatedCheck(options) {
313
+ if (options.box) return runInBox(options, options.box);
314
+ let scratch;
315
+ let outputs;
316
+ let result;
317
+ try {
318
+ const retention = prepareIsolatedCheckOutputs(options.outputs);
319
+ if (process.platform !== "linux") throw new Error("Linux Bubblewrap namespaces are required");
320
+ const { timeoutMs, maxOutputBytes, maxInputBytes, maxInputEntries } = checkLimits(options);
321
+ if (options.signal?.aborted) return {
322
+ succeeded: false,
323
+ reason: "cancelled",
324
+ diagnostic: "Check cancelled"
325
+ };
326
+ const { workspace, tree } = await protectedTree(options);
327
+ const binds = [];
328
+ for (const path of toolchains) {
329
+ let actual;
330
+ try {
331
+ actual = await realpath(path);
332
+ } catch (error) {
333
+ if (error.code === "ENOENT") continue;
334
+ throw error;
335
+ }
336
+ for (const mount of [path, actual]) if (contains(mount, workspace) || contains(workspace, mount) || contains(mount, tree) || contains(tree, mount)) throw new Error(`Toolchain overlaps protected workspace: ${path}`);
337
+ binds.push("--ro-bind", actual, path);
338
+ }
339
+ if (["/proc", "/dev"].some((path) => contains(path, tree) || contains(tree, path))) throw new Error("Tree overlaps a reserved namespace mount");
340
+ scratch = await mkdtemp(join(tmpdir(), "runtime-check-"));
341
+ if (contains(tree, scratch)) throw new Error("Temporary storage must be outside the input tree");
342
+ const copy = join(scratch, "tree");
343
+ await copyBoundedTree(tree, copy, maxInputBytes, maxInputEntries);
344
+ const input = retention ? await scanMaterializedWorkspaceManifest(copy) : void 0;
345
+ if (retention && input) assertOutputPathsUnoccupied(retention.paths, input);
346
+ result = await execute([
347
+ "--unshare-all",
348
+ "--die-with-parent",
349
+ "--new-session",
350
+ "--cap-drop",
351
+ "ALL",
352
+ "--clearenv",
353
+ "--setenv",
354
+ "PATH",
355
+ "/usr/bin:/bin",
356
+ "--setenv",
357
+ "HOME",
358
+ "/tmp",
359
+ "--setenv",
360
+ "LANG",
361
+ "C",
362
+ ...Object.entries(options.env ?? {}).flatMap(([name, value]) => [
363
+ "--setenv",
364
+ name,
365
+ value
366
+ ]),
367
+ ...binds,
368
+ "--proc",
369
+ "/proc",
370
+ "--dev",
371
+ "/dev",
372
+ "--tmpfs",
373
+ "/tmp",
374
+ "--bind",
375
+ copy,
376
+ tree,
377
+ "--chdir",
378
+ tree,
379
+ "--",
380
+ ...options.command
381
+ ], timeoutMs, maxOutputBytes, options.signal);
382
+ if (retention && input && (result.succeeded || result.reason !== "refused")) {
383
+ outputs = await retainIsolatedCheckOutputs(retention, canonicalCandidateDigest(input), localCheckOutputReader(copy));
384
+ if (outputs.status === "incomplete") outputs = {
385
+ ...outputs,
386
+ source: {
387
+ kind: "local",
388
+ path: scratch
389
+ }
390
+ };
391
+ result = withRetainedOutputs(result, outputs);
392
+ }
393
+ } catch (error) {
394
+ result = {
395
+ succeeded: false,
396
+ reason: error instanceof InputLimitError ? "input-limit" : "refused",
397
+ diagnostic: String(error)
398
+ };
399
+ }
400
+ if (scratch && outputs?.status !== "incomplete") try {
401
+ await removeScratch(scratch);
402
+ } catch (error) {
403
+ const cleanupDiagnostic = String(error);
404
+ result = result.succeeded ? {
405
+ succeeded: false,
406
+ reason: "cleanup-failed",
407
+ diagnostic: cleanupDiagnostic,
408
+ stdout: result.value.stdout,
409
+ stderr: result.value.stderr,
410
+ exitCode: 0
411
+ } : {
412
+ ...result,
413
+ cleanupDiagnostic
414
+ };
415
+ }
416
+ return outputs ? {
417
+ ...result,
418
+ outputs
419
+ } : result;
420
+ }
421
+ /** A preexisting input file must never masquerade as newly produced evaluator evidence. */
422
+ function assertOutputPathsUnoccupied(paths, input) {
423
+ for (const path of paths) if (input.files.some((file) => file.path === path || file.path.startsWith(`${path}/`) || path.startsWith(`${file.path}/`))) throw new Error("Declared check output path overlaps an input file");
424
+ }
425
+ function withRetainedOutputs(result, outputs) {
426
+ if (!result.succeeded || outputs.status === "retained") return {
427
+ ...result,
428
+ outputs
429
+ };
430
+ return {
431
+ succeeded: false,
432
+ reason: "failed",
433
+ diagnostic: "Check outputs were not completely retained",
434
+ stdout: result.value.stdout,
435
+ stderr: result.value.stderr,
436
+ exitCode: 0,
437
+ outputs
438
+ };
439
+ }
440
+ function checkLimits(options) {
441
+ const timeoutMs = options.timeoutMs ?? 3e4;
442
+ const maxOutputBytes = options.maxOutputBytes ?? 1048576;
443
+ const maxInputBytes = options.maxInputBytes;
444
+ const maxInputEntries = options.maxInputEntries;
445
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647 || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxInputBytes !== void 0 && (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1) || maxInputEntries !== void 0 && (!Number.isSafeInteger(maxInputEntries) || maxInputEntries < 1)) throw new Error("Invalid execution limits");
446
+ if (!options.command.length || !options.command[0]) throw new Error("A command is required");
447
+ return {
448
+ timeoutMs,
449
+ maxOutputBytes,
450
+ maxInputBytes,
451
+ maxInputEntries
452
+ };
453
+ }
454
+ var InputLimitError = class extends Error {};
455
+ /** Copy in bounded chunks, preserving symlink text without reading its target. */
456
+ async function copyBoundedTree(source, destination, maxBytes, maxEntries) {
457
+ let bytes = 0;
458
+ let entries = 0;
459
+ const buffer = Buffer.allocUnsafe(1048576);
460
+ const visit = async (from, to, isRoot = false) => {
461
+ if (!isRoot && ++entries > (maxEntries ?? Number.MAX_SAFE_INTEGER)) throw new InputLimitError("Input tree exceeds maxInputEntries");
462
+ const info = await lstat(from);
463
+ if (info.isSymbolicLink()) await symlink(await readlink(from), to);
464
+ else if (info.isDirectory()) {
465
+ await mkdir(to, { mode: 448 });
466
+ for await (const entry of await opendir(from)) await visit(join(from, entry.name), join(to, entry.name));
467
+ await chmod(to, info.mode & 511);
468
+ } else if (info.isFile()) {
469
+ if (maxBytes !== void 0 && info.size > maxBytes - bytes) throw new InputLimitError("Input tree exceeds maxInputBytes");
470
+ const input = await open(from, constants.O_RDONLY | constants.O_NOFOLLOW);
471
+ try {
472
+ const output = await open(to, "wx", info.mode & 511);
473
+ try {
474
+ while (true) {
475
+ const { bytesRead } = await input.read(buffer, 0, maxBytes === void 0 ? buffer.length : Math.min(buffer.length, maxBytes - bytes + 1), null);
476
+ if (bytesRead === 0) break;
477
+ bytes += bytesRead;
478
+ if (maxBytes !== void 0 && bytes > maxBytes) throw new InputLimitError("Input tree exceeds maxInputBytes");
479
+ let written = 0;
480
+ while (written < bytesRead) {
481
+ const result = await output.write(buffer, written, bytesRead - written);
482
+ if (result.bytesWritten === 0) throw new Error("Input copy made no write progress");
483
+ written += result.bytesWritten;
484
+ }
485
+ }
486
+ } finally {
487
+ await output.close();
488
+ }
489
+ await chmod(to, info.mode & 511);
490
+ } finally {
491
+ await input.close();
492
+ }
493
+ } else throw new Error(`Input tree contains an unsupported entry: ${from}`);
494
+ };
495
+ await visit(source, destination, true);
496
+ }
497
+ async function checkInputEntries(root, maxEntries) {
498
+ let count = 0;
499
+ const visit = async (path, isRoot = false) => {
500
+ if (!isRoot && ++count > maxEntries) throw new InputLimitError("Input tree exceeds maxInputEntries");
501
+ if (!(await lstat(path)).isDirectory()) return;
502
+ for await (const entry of await opendir(path)) await visit(join(path, entry.name));
503
+ };
504
+ await visit(root, true);
505
+ }
506
+ async function protectedTree(options) {
507
+ const workspace = await realpath(options.workspaceRoot);
508
+ const tree = await realpath(options.tree);
509
+ if (!contains(workspace, tree) || !(await stat(tree)).isDirectory()) throw new Error("Tree must be a directory inside the workspace");
510
+ return {
511
+ workspace,
512
+ tree
513
+ };
514
+ }
515
+ /** Box-workspace-relative directory that receives the tree and is the command's cwd. */
516
+ const BOX_TREE = "tree";
517
+ /** Setup headroom in the requested native lifetime policy; this is not a deletion guarantee. */
518
+ const BOX_SETUP_SECONDS = 900;
519
+ /** The box's own process timeout is a backstop behind this module's deadline. */
520
+ const BOX_PROCESS_BACKSTOP_MS = 3e4;
521
+ /** The box's process supervisor prefixes this to stderr when it cannot start the executable. */
522
+ const BOX_EXEC_FAILURE = "process-supervisor: exec failed";
523
+ async function runInBox(options, placement) {
524
+ let box;
525
+ let evidence;
526
+ let outputs;
527
+ let retention;
528
+ let maxLifetimeSeconds;
529
+ let commandAttempted = false;
530
+ let commandSettled = false;
531
+ let result;
532
+ try {
533
+ retention = prepareIsolatedCheckOutputs(options.outputs);
534
+ const { timeoutMs, maxOutputBytes, maxInputBytes, maxInputEntries } = checkLimits(options);
535
+ maxLifetimeSeconds = Math.ceil(timeoutMs / 1e3) + BOX_SETUP_SECONDS;
536
+ if (options.signal?.aborted) return {
537
+ succeeded: false,
538
+ reason: "cancelled",
539
+ diagnostic: "Check cancelled"
540
+ };
541
+ const { tree } = await protectedTree(options);
542
+ if (maxInputEntries !== void 0) await checkInputEntries(tree, maxInputEntries);
543
+ const captured = await captureMaterializedWorkspace(tree, { limits: {
544
+ maxFiles: maxInputEntries ?? Number.MAX_SAFE_INTEGER,
545
+ maxFileBytes: maxInputBytes ?? Number.MAX_SAFE_INTEGER,
546
+ maxTotalFileBytes: maxInputBytes ?? Number.MAX_SAFE_INTEGER
547
+ } });
548
+ if (retention) assertOutputPathsUnoccupied(retention.paths, captured.manifest);
549
+ const judge = placement.ownerStep !== true;
550
+ if (judge && (placement.builderAccounts.length === 0 || placement.builderAccounts.some((id) => !id))) throw new Error("builderAccounts must name the account of every key the judged run holds");
551
+ const memoryMB = placement.resources?.memoryMB;
552
+ if (memoryMB !== void 0 && (!Number.isSafeInteger(memoryMB) || memoryMB < 1 || placement.resources?.advisory === true || placement.resources?.advisoryFields?.includes("memoryMB"))) throw new Error("Check box requires an explicit finite resources.memoryMB limit");
553
+ const account = (await placement.client.getIdentity()).customerId;
554
+ if (!account) throw new Error("Sandbox returned no account for the check key");
555
+ if (judge && placement.builderAccounts.includes(account)) throw new Error(`Check account ${account} is an account the judged run holds a key to, so the run can reach the check box`);
556
+ options.signal?.throwIfAborted();
557
+ box = await placement.client.createIsolated({
558
+ environment: placement.environment,
559
+ agent: placement.containers === true,
560
+ bare: false,
561
+ ephemeral: placement.containers !== true && judge,
562
+ egressPolicy: egressFor(placement),
563
+ ...placement.resources ? { resources: placement.resources } : {},
564
+ maxLifetimeSeconds,
565
+ idempotencyKey: `agent-runtime-check-${randomUUID()}`
566
+ }, options.signal ? { signal: options.signal } : void 0);
567
+ const receipt = box.createReceipt();
568
+ if (receipt?.outcome !== "created" || receipt.ownerContext !== "isolated" || !Array.isArray(receipt.injectedSecrets) || receipt.injectedSecrets.length !== 0) throw new Error("Sandbox did not confirm a fresh box with no owner secrets");
569
+ const grantedMemoryMB = box.resources?.memoryMB;
570
+ if (memoryMB !== void 0 && (typeof grantedMemoryMB !== "number" || !Number.isSafeInteger(grantedMemoryMB) || grantedMemoryMB < 1 || grantedMemoryMB > memoryMB)) throw new Error("Sandbox did not confirm the requested finite memory cgroup limit");
571
+ const egress = (await box.egress.get()).policy;
572
+ const wanted = egressFor(placement);
573
+ if (egress.mode !== wanted.mode || wanted.mode === "strict" && (egress.includeImplicitDomains === true || [...egress.allowDomains ?? []].sort().join(",") !== [...wanted.allowDomains ?? []].sort().join(","))) throw new Error(`Check box egress is ${egress.mode} ${JSON.stringify(egress.allowDomains ?? [])}, not ${wanted.mode} ${JSON.stringify(wanted.allowDomains ?? [])}`);
574
+ const staging = await mkdtemp(join(tmpdir(), "runtime-check-box-"));
575
+ try {
576
+ for (const file of captured.files) {
577
+ const path = join(staging, file.path);
578
+ await mkdir(dirname(path), { recursive: true });
579
+ await writeFile(path, file.bytes, { mode: file.mode });
580
+ }
581
+ options.signal?.throwIfAborted();
582
+ await box.fs.uploadDir(staging, BOX_TREE);
583
+ } finally {
584
+ await rm(staging, {
585
+ recursive: true,
586
+ force: true
587
+ });
588
+ }
589
+ await verifyBoxTree(box, captured.manifest.files);
590
+ evidence = {
591
+ sandboxId: box.id,
592
+ account,
593
+ ...memoryMB === void 0 ? {} : { memoryMB: grantedMemoryMB },
594
+ input: captured.manifest,
595
+ inputDigest: canonicalCandidateDigest(captured.manifest)
596
+ };
597
+ commandAttempted = true;
598
+ const execution = await executeInBox(box, options.command, timeoutMs, maxOutputBytes, options.env ?? {}, options.signal);
599
+ result = execution.result;
600
+ commandSettled = execution.commandSettled;
601
+ } catch (error) {
602
+ result = {
603
+ succeeded: false,
604
+ reason: options.signal?.aborted ? "cancelled" : error instanceof InputLimitError || error instanceof WorkspaceScanLimitError ? "input-limit" : "refused",
605
+ diagnostic: String(error)
606
+ };
607
+ }
608
+ if (box && evidence && retention && maxLifetimeSeconds !== void 0 && commandAttempted && (!commandSettled || result.succeeded || result.reason !== "refused")) {
609
+ outputs = commandSettled ? await retainIsolatedCheckOutputs(retention, evidence.inputDigest, boxCheckOutputReader(box, BOX_TREE, retention.timeoutMs ?? 3e4)) : {
610
+ executionId: retention.executionId,
611
+ inputDigest: evidence.inputDigest,
612
+ status: "incomplete",
613
+ files: [],
614
+ missing: retention.paths.map((path) => ({
615
+ path,
616
+ reason: "capture-failed"
617
+ }))
618
+ };
619
+ result = withRetainedOutputs(result, outputs);
620
+ if (outputs.status === "incomplete") {
621
+ if (await stopCheckBoxForRecovery(box)) outputs = {
622
+ ...outputs,
623
+ source: {
624
+ kind: "sandbox",
625
+ sandboxId: box.id,
626
+ state: "stopped"
627
+ }
628
+ };
629
+ else if (!result.succeeded) result = {
630
+ ...result,
631
+ cleanupDiagnostic: "Recovery source stop was not confirmed; box deletion was requested"
632
+ };
633
+ }
634
+ }
635
+ if (box && outputs?.source?.kind !== "sandbox") try {
636
+ await box.delete();
637
+ } catch (error) {
638
+ const cleanupDiagnostic = `Check box ${box.id} was not deleted: ${String(error)}`;
639
+ result = result.succeeded ? {
640
+ succeeded: false,
641
+ reason: "cleanup-failed",
642
+ diagnostic: cleanupDiagnostic,
643
+ stdout: result.value.stdout,
644
+ stderr: result.value.stderr,
645
+ exitCode: 0
646
+ } : {
647
+ ...result,
648
+ cleanupDiagnostic: [result.cleanupDiagnostic, cleanupDiagnostic].filter(Boolean).join("; ")
649
+ };
650
+ }
651
+ return {
652
+ ...result,
653
+ ...evidence ? { box: evidence } : {},
654
+ ...outputs ? { outputs } : {}
655
+ };
656
+ }
657
+ /** Refuse unless the box reads each file with its manifest digest. `find` names the files: a box
658
+ * process takes 256 arguments at most. A name holding a newline or backslash, which coreutils
659
+ * escapes, does not compare. */
660
+ async function verifyBoxTree(box, files) {
661
+ const child = await box.process.spawnExact("find", [
662
+ ".",
663
+ "-type",
664
+ "f",
665
+ "-exec",
666
+ "sha256sum",
667
+ "{}",
668
+ "+"
669
+ ], {
670
+ cwd: BOX_TREE,
671
+ env: {
672
+ PATH: "/usr/bin:/bin",
673
+ LANG: "C"
674
+ },
675
+ inheritEnv: false,
676
+ timeoutMs: BOX_SETUP_SECONDS * 1e3
677
+ });
678
+ let listing = "";
679
+ for await (const text of child.stdout()) listing += text;
680
+ if (await child.wait() !== 0) throw new Error("Check box could not hash the tree it received");
681
+ const read = new Set(listing.split("\n").map((line) => `sha256:${line.slice(0, 64)} ${line.slice(68)}`));
682
+ for (const file of files) if (!read.has(`${file.sha256} ${file.path}`)) throw new Error(`Check box received different bytes for ${file.path}`);
683
+ }
684
+ /** Blocked unless the placement names domains; then strict, with no implicit domain list. */
685
+ function egressFor(placement) {
686
+ const domains = [...new Set(placement.egress ?? [])].filter((domain) => domain.trim() !== "");
687
+ return domains.length === 0 ? { mode: "blocked" } : {
688
+ mode: "strict",
689
+ allowDomains: domains,
690
+ includeImplicitDomains: false
691
+ };
692
+ }
693
+ async function executeInBox(box, command, timeoutMs, limit, env, signal) {
694
+ const [executable, ...args] = command;
695
+ let child;
696
+ const deadline = new AbortController();
697
+ const stdout = [];
698
+ const stderr = [];
699
+ let bytes = 0;
700
+ let failure;
701
+ let stopping;
702
+ let stopped = false;
703
+ const stop = (reason) => {
704
+ failure ??= reason;
705
+ deadline.abort();
706
+ if (child) stopping ??= stopCheckProcess(child);
707
+ };
708
+ const clearTimer = armDeadlineTimer(timeoutMs, () => stop("timeout"), true);
709
+ const cancel = () => stop("cancelled");
710
+ signal?.addEventListener("abort", cancel, { once: true });
711
+ if (signal?.aborted) cancel();
712
+ const capture = async (stream, target) => {
713
+ for await (const text of stream) {
714
+ if (deadline.signal.aborted) break;
715
+ const chunk = Buffer.from(text);
716
+ const remaining = Math.max(0, limit - bytes);
717
+ bytes += chunk.length;
718
+ if (remaining) target.push(chunk.subarray(0, remaining));
719
+ if (bytes > limit) stop("output-limit");
720
+ }
721
+ };
722
+ let completion;
723
+ try {
724
+ child = await runAbortable(() => {
725
+ const launching = box.process.spawnExact(executable, args, {
726
+ cwd: BOX_TREE,
727
+ env: {
728
+ PATH: "/usr/bin:/bin",
729
+ HOME: "/tmp",
730
+ LANG: "C",
731
+ ...env
732
+ },
733
+ inheritEnv: false,
734
+ timeoutMs: timeoutMs + BOX_PROCESS_BACKSTOP_MS
735
+ });
736
+ launching.then((late) => {
737
+ if (deadline.signal.aborted) stopCheckProcess(late);
738
+ }, () => void 0);
739
+ return launching;
740
+ }, deadline.signal, "Check interrupted");
741
+ const running = child;
742
+ completion = await runAbortable(async () => {
743
+ await Promise.all([capture(running.stdout(), stdout), capture(running.stderr(), stderr)]);
744
+ return {
745
+ exitCode: await running.wait(),
746
+ exitSignal: (await running.status()).exitSignal
747
+ };
748
+ }, deadline.signal, "Check interrupted");
749
+ } catch (error) {
750
+ if (child) stopping ??= stopCheckProcess(child);
751
+ if (!failure) throw error;
752
+ } finally {
753
+ clearTimer();
754
+ signal?.removeEventListener("abort", cancel);
755
+ stopped = await stopping === true;
756
+ }
757
+ const out = Buffer.concat(stdout).toString();
758
+ const err = Buffer.concat(stderr).toString();
759
+ const exitCode = completion?.exitCode ?? null;
760
+ const exitSignal = completion?.exitSignal;
761
+ const evidence = {
762
+ stdout: out,
763
+ stderr: err,
764
+ exitCode: exitSignal ? null : exitCode
765
+ };
766
+ const finish = (result) => ({
767
+ result,
768
+ commandSettled: completion !== void 0 || stopped
769
+ });
770
+ if (failure) return finish({
771
+ succeeded: false,
772
+ reason: failure,
773
+ diagnostic: err || failure,
774
+ ...evidence
775
+ });
776
+ if (exitCode === 127 && err.startsWith(BOX_EXEC_FAILURE)) return finish({
777
+ succeeded: false,
778
+ reason: "refused",
779
+ diagnostic: err,
780
+ ...evidence
781
+ });
782
+ if (exitSignal) return finish({
783
+ succeeded: false,
784
+ reason: "failed",
785
+ diagnostic: err || `Check killed by ${exitSignal}`,
786
+ ...evidence
787
+ });
788
+ if (exitCode !== 0) return finish({
789
+ succeeded: false,
790
+ reason: "failed",
791
+ diagnostic: err || out || `Check exited ${exitCode}`,
792
+ ...evidence
793
+ });
794
+ return finish({
795
+ succeeded: true,
796
+ value: {
797
+ stdout: out,
798
+ stderr: err
799
+ }
800
+ });
801
+ }
802
+ function execute(args, timeoutMs, limit, signal) {
803
+ return new Promise((done) => {
804
+ const child = spawn("/usr/bin/bwrap", [
805
+ "--json-status-fd",
806
+ "3",
807
+ ...args
808
+ ], {
809
+ env: {},
810
+ stdio: [
811
+ "ignore",
812
+ "pipe",
813
+ "pipe",
814
+ "pipe"
815
+ ],
816
+ detached: true
817
+ });
818
+ const stdout = [];
819
+ const stderr = [];
820
+ let bytes = 0;
821
+ let status = "";
822
+ let failure;
823
+ const stop = (reason) => {
824
+ failure ??= reason;
825
+ if (child.pid) try {
826
+ process.kill(-child.pid, "SIGKILL");
827
+ } catch (error) {
828
+ if (error.code !== "ESRCH") child.kill("SIGKILL");
829
+ }
830
+ };
831
+ const clearTimer = armDeadlineTimer(timeoutMs, () => stop("timeout"), true);
832
+ const cancel = () => stop("cancelled");
833
+ signal?.addEventListener("abort", cancel, { once: true });
834
+ if (signal?.aborted) cancel();
835
+ const capture = (target) => (chunk) => {
836
+ const remaining = Math.max(0, limit - bytes);
837
+ bytes += chunk.length;
838
+ if (remaining) target.push(chunk.subarray(0, remaining));
839
+ if (bytes > limit) stop("output-limit");
840
+ };
841
+ child.stdio[3]?.on("data", (chunk) => {
842
+ if (status.length + chunk.length > 16384) {
843
+ stop("output-limit");
844
+ return;
845
+ }
846
+ status += chunk.toString();
847
+ });
848
+ child.stdout.on("data", capture(stdout));
849
+ child.stderr.on("data", capture(stderr));
850
+ let spawnError;
851
+ child.on("error", (error) => {
852
+ spawnError = error;
853
+ });
854
+ child.on("close", (code) => {
855
+ clearTimer();
856
+ signal?.removeEventListener("abort", cancel);
857
+ const out = Buffer.concat(stdout).toString();
858
+ const err = Buffer.concat(stderr).toString();
859
+ const evidence = {
860
+ stdout: out,
861
+ stderr: err,
862
+ exitCode: code
863
+ };
864
+ const executed = status.split("\n").some((line) => {
865
+ try {
866
+ const record = JSON.parse(line);
867
+ return record !== null && typeof record === "object" && "exit-code" in record && typeof record["exit-code"] === "number" && record["exit-code"] === code;
868
+ } catch {
869
+ return false;
870
+ }
871
+ });
872
+ if (failure) done({
873
+ succeeded: false,
874
+ reason: failure,
875
+ diagnostic: err || failure,
876
+ ...evidence
877
+ });
878
+ else if (spawnError) done({
879
+ succeeded: false,
880
+ reason: "refused",
881
+ diagnostic: String(spawnError),
882
+ ...evidence
883
+ });
884
+ else if (!executed) done({
885
+ succeeded: false,
886
+ reason: "refused",
887
+ diagnostic: err || "Bubblewrap did not confirm command execution",
888
+ ...evidence
889
+ });
890
+ else if (code !== 0) done({
891
+ succeeded: false,
892
+ reason: "failed",
893
+ diagnostic: err || out || `Check exited ${code}`,
894
+ ...evidence
895
+ });
896
+ else done({
897
+ succeeded: true,
898
+ value: {
899
+ stdout: out,
900
+ stderr: err
901
+ }
902
+ });
903
+ });
904
+ });
905
+ }
906
+ const executeFile = promisify(execFile);
907
+ /** GNU tools traverse relative to open directories, including trees deeper than PATH_MAX.
908
+ * Recursive chmod ignores encountered symlinks; rm never follows their targets. */
909
+ async function removeScratch(path) {
910
+ const options = {
911
+ env: {},
912
+ timeout: 1e4,
913
+ maxBuffer: 16384
914
+ };
915
+ let permissionError;
916
+ try {
917
+ await executeFile("/bin/chmod", [
918
+ "--recursive",
919
+ "--preserve-root",
920
+ "u+rwX",
921
+ "--",
922
+ path
923
+ ], options);
924
+ } catch (error) {
925
+ permissionError = error;
926
+ }
927
+ try {
928
+ await executeFile("/bin/rm", [
929
+ "--recursive",
930
+ "--force",
931
+ "--one-file-system",
932
+ "--",
933
+ path
934
+ ], options);
935
+ } catch (error) {
936
+ throw new Error([permissionError, error].filter((value) => value !== void 0).map(String).join("; "));
937
+ }
938
+ }
939
+ // src/runtime/supervise/graph-backend.ts
940
+ /** Graph nodes expose JSON outputs through Runtime's resumable execution transports. */
941
+ function graphExecutionOptions(options) {
942
+ if (options.makeLeafAgent) return options;
943
+ const backend = options.backend;
944
+ if (backend) return backend.backend === "bridge" || backend.backend === "provider" ? {
945
+ ...options,
946
+ backend: {
947
+ ...backend,
948
+ outputFormat: "json"
949
+ }
950
+ } : options;
951
+ const bridgeUrl = process.env.CLI_BRIDGE_URL;
952
+ const bridgeBearer = process.env.BRIDGE_BEARER ?? process.env.CLI_BRIDGE_BEARER;
953
+ if (!bridgeUrl || !bridgeBearer) throw new ValidationError("runGraph: start CLI Bridge and supply backend: { backend: \"bridge\", bridgeUrl, bridgeBearer, cwd }, or configure CLI_BRIDGE_URL and BRIDGE_BEARER");
954
+ return {
955
+ ...options,
956
+ backend: {
957
+ backend: "bridge",
958
+ bridgeUrl,
959
+ bridgeBearer,
960
+ cwd: process.cwd(),
961
+ outputFormat: "json"
962
+ }
963
+ };
964
+ }
965
+ // src/runtime/supervise/stored-graph-check.ts
966
+ /** Driver-owned journal identity. Worker output never supplies any part of this authority. */
967
+ function receiptKey(receipt) {
968
+ return contentAddress({
969
+ binding: receipt.binding,
970
+ checkDigest: receipt.checkDigest,
971
+ nodeName: receipt.nodeName,
972
+ nodeId: receipt.nodeId,
973
+ attemptId: receipt.attemptId,
974
+ outputDigest: receipt.outputDigest
975
+ });
976
+ }
977
+ function isReceipt(value) {
978
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
979
+ const row = value;
980
+ if (row.version !== 1 || ![
981
+ "key",
982
+ "binding",
983
+ "checkDigest",
984
+ "nodeName",
985
+ "nodeId",
986
+ "attemptId",
987
+ "outputDigest",
988
+ "output"
989
+ ].every((key) => typeof row[key] === "string") || !row.nodeName || !row.nodeId || !row.attemptId || !(row.exitCode === null || Number.isSafeInteger(row.exitCode)) || typeof row.outputTruncated !== "boolean" || typeof row.nativeValid !== "boolean" || typeof row.valid !== "boolean") return false;
990
+ const receipt = value;
991
+ return receipt.key === receiptKey(receipt) && receipt.valid === (receipt.exitCode === 0 && receipt.nativeValid);
992
+ }
993
+ async function appendReceipt(file, workspace, receipt) {
994
+ await withJsonlFileLock(file, async (canonicalPath) => {
995
+ if (workspace !== void 0 && within(workspace, canonicalPath)) throw new ValidationError("runGraph: command receipt journal resolves inside worker workspace");
996
+ await appendCommittedJsonLine(canonicalPath, {
997
+ ...receipt,
998
+ key: receiptKey(receipt)
999
+ });
1000
+ });
1001
+ }
1002
+ function within(directory, path) {
1003
+ const offset = relative(directory, path);
1004
+ return offset === "" || !isAbsolute(offset) && offset !== ".." && !offset.startsWith("../");
1005
+ }
1006
+ /** The run's working directory (a loopback Bridge cwd or local CLI workspace) and its driver-owned
1007
+ * run directory, which must lie outside it. */
1008
+ function localCheckDirectories(options) {
1009
+ const cwd = backendWorkspace(options);
1010
+ if (!cwd) throw new ValidationError("runGraph: command checks require a loopback Bridge or local CLI workspace; provider node binding is unsupported");
1011
+ return {
1012
+ cwd,
1013
+ runDir: driverRunDir(options, cwd)
1014
+ };
1015
+ }
1016
+ function backendWorkspace(options) {
1017
+ const backend = options.backend;
1018
+ if (!backend) return void 0;
1019
+ const workspace = backend.backend === "bridge" ? backend.cwd : backend.backend === "cli-in-place" ? backend.workspacePath : void 0;
1020
+ return !workspace || backend.backend === "bridge" && ![
1021
+ "localhost",
1022
+ "127.0.0.1",
1023
+ "[::1]"
1024
+ ].includes(new URL(backend.bridgeUrl).hostname) ? void 0 : realpathSync(workspace);
1025
+ }
1026
+ function driverRunDir(options, workspace) {
1027
+ if (!options.runDir) throw new ValidationError("runGraph: command checks require a driver-owned runDir outside the workspace");
1028
+ mkdirSync(options.runDir, { recursive: true });
1029
+ const runDir = realpathSync(options.runDir);
1030
+ if (workspace !== void 0 && within(workspace, runDir)) throw new ValidationError("runGraph: command check runDir must be outside the worker workspace");
1031
+ return runDir;
1032
+ }
1033
+ /** Lower a named check onto the local workspace, with evidence in the driver's durable state.
1034
+ * The run directory has the same trust boundary as RunContext journals: keep it outside the
1035
+ * worker workspace and do not mount it into the worker. A remote Bridge cannot run a host check.
1036
+ * Provider validators are refused because their context does not identify the admitted node.
1037
+ */
1038
+ function bindGraphCommand(graph, options) {
1039
+ const { command: definition, ...deliverable } = graph.deliverable;
1040
+ if (!definition) return {
1041
+ graph,
1042
+ options
1043
+ };
1044
+ const { command, timeoutMs = 6e4 } = definition;
1045
+ if (!command.trim() || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new ValidationError("graphFromStored: command check needs a command and positive timeoutMs");
1046
+ if (options.makeLeafAgent || graph.nodes.some((node) => node.check)) throw new ValidationError("runGraph: command checks need a backend and no check nodes");
1047
+ const { cwd, runDir } = localCheckDirectories(options);
1048
+ const file = join(runDir, "command-checks.jsonl");
1049
+ const checkDigest = contentAddress({
1050
+ command,
1051
+ timeoutMs,
1052
+ cwd
1053
+ });
1054
+ const binding = contentAddress({
1055
+ graph: {
1056
+ ...graph,
1057
+ deliverable: {
1058
+ ...deliverable,
1059
+ check: void 0
1060
+ }
1061
+ },
1062
+ runId: options.runId ?? options.runContext?.runId,
1063
+ checkDigest
1064
+ });
1065
+ const nodeNames = new Set(graph.nodes.map((node) => node.id));
1066
+ const checkFinal = async (out) => {
1067
+ if (!out || typeof out !== "object" || Array.isArray(out)) return false;
1068
+ const entries = Object.entries(out);
1069
+ if (entries.length === 0 || entries.some(([name]) => !nodeNames.has(name))) return false;
1070
+ return withJsonlFileLock(file, async (canonicalPath) => {
1071
+ if (within(cwd, canonicalPath)) return false;
1072
+ const latest = /* @__PURE__ */ new Map();
1073
+ for await (const raw of readCommittedJsonLines(canonicalPath, { allowMissing: true })) {
1074
+ if (!isReceipt(raw)) return false;
1075
+ if (raw.binding !== binding || raw.checkDigest !== checkDigest) continue;
1076
+ latest.set(contentAddress([raw.nodeName, raw.outputDigest]), raw);
1077
+ }
1078
+ return entries.every(([name, output]) => latest.get(contentAddress([name, contentAddress(output)]))?.valid === true);
1079
+ });
1080
+ };
1081
+ const checkLeaf = async (out, context) => {
1082
+ const node = context.node;
1083
+ const nodeName = context.profile.name;
1084
+ if (!node || !node.nodeId || !node.attemptId || !nodeName || !nodeNames.has(nodeName)) return false;
1085
+ const outputDigest = contentAddress(out);
1086
+ let result;
1087
+ try {
1088
+ result = await defaultRunCommand({
1089
+ command,
1090
+ cwd,
1091
+ timeoutMs,
1092
+ signal: context.signal
1093
+ });
1094
+ } catch (error) {
1095
+ result = {
1096
+ exitCode: null,
1097
+ output: error instanceof Error ? error.message : String(error)
1098
+ };
1099
+ }
1100
+ const bytes = Buffer.from(result.output);
1101
+ const limit = 65536;
1102
+ const receipt = {
1103
+ version: 1,
1104
+ binding,
1105
+ checkDigest,
1106
+ nodeName,
1107
+ nodeId: node.nodeId,
1108
+ attemptId: node.attemptId,
1109
+ outputDigest,
1110
+ exitCode: result.exitCode,
1111
+ output: bytes.subarray(0, limit).toString("utf8"),
1112
+ outputTruncated: bytes.length > limit,
1113
+ nativeValid: context.originalVerdict?.valid !== false,
1114
+ valid: result.exitCode === 0 && context.originalVerdict?.valid !== false
1115
+ };
1116
+ await appendReceipt(file, cwd, receipt);
1117
+ return receipt.valid;
1118
+ };
1119
+ return {
1120
+ graph: {
1121
+ ...graph,
1122
+ deliverable: {
1123
+ ...deliverable,
1124
+ check: (out, context) => context === void 0 ? checkFinal(out) : checkLeaf(out, context)
1125
+ }
1126
+ },
1127
+ options
1128
+ };
1129
+ }
1130
+ const CHECK_OUTPUT_LIMIT = 65536;
1131
+ const CHECK_TIMEOUT_MS = 9e5;
1132
+ const safeNodeFile = /^(?!\.\.?$)[A-Za-z0-9._-]+$/;
1133
+ function tail(text) {
1134
+ const bytes = Buffer.from(text);
1135
+ return bytes.length <= CHECK_OUTPUT_LIMIT ? {
1136
+ output: text,
1137
+ truncated: false
1138
+ } : {
1139
+ output: bytes.subarray(bytes.length - CHECK_OUTPUT_LIMIT).toString("utf8"),
1140
+ truncated: true
1141
+ };
1142
+ }
1143
+ function lastJsonObject(stdout) {
1144
+ const line = stdout.trimEnd().split("\n").at(-1)?.trim();
1145
+ if (!line?.startsWith("{")) return void 0;
1146
+ try {
1147
+ const value = JSON.parse(line);
1148
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1149
+ } catch {
1150
+ return;
1151
+ }
1152
+ }
1153
+ /** Each settled result a routes spawn carries: its source and its evidence, by node id. */
1154
+ function evidenceOf(task) {
1155
+ if (!task || typeof task !== "object") return [];
1156
+ const { from, evidence } = task;
1157
+ const entries = /* @__PURE__ */ new Map();
1158
+ if (from && typeof from === "object" && "node" in from && "result" in from) entries.set(String(from.node), from.result);
1159
+ if (evidence && typeof evidence === "object" && !Array.isArray(evidence)) for (const [id, result] of Object.entries(evidence)) entries.set(id, result);
1160
+ return [...entries];
1161
+ }
1162
+ async function writeEvidence(directory, task) {
1163
+ await mkdir(directory, { recursive: true });
1164
+ for (const [id, result] of evidenceOf(task)) {
1165
+ if (!safeNodeFile.test(id)) throw new ValidationError(`runGraph: evidence node id ${id}`);
1166
+ await writeFile(join(directory, `${id}.json`), `${JSON.stringify(result)}\n`);
1167
+ }
1168
+ }
1169
+ async function readSecrets(names, environment, node) {
1170
+ const secrets = {};
1171
+ for (const name of names ?? []) {
1172
+ const value = environment[name];
1173
+ if (!value) throw new ValidationError(`runGraph: check node ${node} needs secret ${name}`);
1174
+ secrets[name] = value;
1175
+ }
1176
+ return secrets;
1177
+ }
1178
+ /** Run one check node's command locally, in the run's working directory. */
1179
+ async function runLocalCheck(check, cwd, evidence, signal, secrets) {
1180
+ const timeoutMs = check.timeoutMs ?? CHECK_TIMEOUT_MS;
1181
+ const result = await runSettledCommand({
1182
+ command: "/bin/sh",
1183
+ args: ["-c", check.command],
1184
+ cwd,
1185
+ timeoutMs,
1186
+ signal,
1187
+ env: {
1188
+ ...secrets,
1189
+ GRAPH_EVIDENCE: evidence
1190
+ }
1191
+ });
1192
+ const timedOut = result.timedOut ? `\n[check timed out after ${timeoutMs} ms]` : "";
1193
+ return {
1194
+ exitCode: result.timedOut ? null : result.exitCode,
1195
+ stdout: result.stdout,
1196
+ output: result.stdout + result.stderr + timedOut
1197
+ };
1198
+ }
1199
+ /** Run one check node's command in a fresh box on the check account, as a declared check does. */
1200
+ async function runBoxCheck(check, node, placement, evidence, signal, secrets) {
1201
+ const workspaceRoot = await mkdtemp(join(evidence, "..", "box-"));
1202
+ try {
1203
+ const tree = join(workspaceRoot, "tree");
1204
+ await mkdir(tree);
1205
+ await cp(evidence, join(tree, "_evidence"), { recursive: true });
1206
+ const result = await runIsolatedCheck({
1207
+ workspaceRoot,
1208
+ tree,
1209
+ command: [
1210
+ "/bin/sh",
1211
+ "-c",
1212
+ `GRAPH_EVIDENCE="$PWD/_evidence"; export GRAPH_EVIDENCE; ${check.command}`
1213
+ ],
1214
+ timeoutMs: check.timeoutMs ?? CHECK_TIMEOUT_MS,
1215
+ maxOutputBytes: 16 * CHECK_OUTPUT_LIMIT,
1216
+ env: secrets,
1217
+ signal,
1218
+ box: {
1219
+ client: placement.client,
1220
+ builderAccounts: placement.builderAccounts ?? ["run-owner"],
1221
+ ownerStep: true,
1222
+ environment: check.environment,
1223
+ ...check.egress === void 0 ? {} : { egress: check.egress },
1224
+ ...check.resources === void 0 ? {} : { resources: check.resources }
1225
+ }
1226
+ });
1227
+ if (result.succeeded) return {
1228
+ exitCode: 0,
1229
+ stdout: result.value.stdout,
1230
+ output: result.value.stdout + result.value.stderr
1231
+ };
1232
+ const stdout = result.stdout ?? "";
1233
+ const output = stdout + (result.stderr ?? "");
1234
+ if (result.reason === "failed" && typeof result.exitCode === "number") return {
1235
+ exitCode: result.exitCode,
1236
+ stdout,
1237
+ output
1238
+ };
1239
+ if (result.reason === "timeout") return {
1240
+ exitCode: null,
1241
+ stdout,
1242
+ output: `${output}\n[check timed out]`
1243
+ };
1244
+ throw new Error(redact(`runGraph: check node ${node} box ${result.reason}: ${result.diagnostic}`, secrets));
1245
+ } finally {
1246
+ await rm(workspaceRoot, {
1247
+ recursive: true,
1248
+ force: true
1249
+ });
1250
+ }
1251
+ }
1252
+ /** Every resolved secret value becomes `[redacted:NAME]` before output leaves the check. */
1253
+ function redact(text, secrets) {
1254
+ return Object.entries(secrets).sort(([, a], [, b]) => b.length - a.length).reduce((out, [name, value]) => out.replaceAll(value, `[redacted:${name}]`), text);
1255
+ }
1256
+ function checkOutput(receipt) {
1257
+ return {
1258
+ pass: receipt.valid,
1259
+ exitCode: receipt.exitCode,
1260
+ output: receipt.output,
1261
+ ...receipt.json === void 0 ? {} : { json: receipt.json }
1262
+ };
1263
+ }
1264
+ /** The receipt a run context's journal holds for this binding, as executor evidence. */
1265
+ async function replayContextReceipt(context, runId, binding, checkDigest) {
1266
+ for (const event of await context.journal.loadTree(runId) ?? []) {
1267
+ if (event.kind !== "execution-evidence") continue;
1268
+ const row = (await context.blobs.get(event.outRef))?.graphCheckReceipt;
1269
+ if (isReceipt(row) && row.binding === binding && row.checkDigest === checkDigest) return checkOutput(row);
1270
+ }
1271
+ }
1272
+ /** A node attempt's journaled settle, so a resumed run never runs a settled command again. */
1273
+ async function replayCheckReceipt(file, binding, checkDigest) {
1274
+ return withJsonlFileLock(file, async (canonicalPath) => {
1275
+ for await (const row of readCommittedJsonLines(canonicalPath, { allowMissing: true })) if (isReceipt(row) && row.binding === binding && row.checkDigest === checkDigest) return checkOutput(row);
1276
+ });
1277
+ }
1278
+ /**
1279
+ * Bind a flow graph's check nodes. Each spawn of a check node runs its command once and settles
1280
+ * `{ pass, exitCode, output, json }` as a usable result, so routes branch on `/pass`. The receipt
1281
+ * rides the run's `command-checks.jsonl` beside the command deliverable's, and a node attempt that
1282
+ * already holds a receipt replays it instead of running the command again.
1283
+ */
1284
+ function bindGraphCheckNodes(graph, options) {
1285
+ const checks = new Map(graph.nodes.flatMap((node) => node.check === void 0 ? [] : [[node.id, node.check]]));
1286
+ if (checks.size === 0) return void 0;
1287
+ if (graph.entry === void 0) throw new ValidationError("runGraph: check nodes run only in a flow graph (one with entry)");
1288
+ for (const node of graph.nodes) if (!safeNodeFile.test(node.id)) throw new ValidationError(`runGraph: node id ${JSON.stringify(node.id)} cannot name an evidence file; use letters, digits, '.', '_' and '-'`);
1289
+ for (const edge of graph.edges) if (edge.kind === "maps" && [edge.to, edge.join].some((id) => id && checks.has(id))) throw new ValidationError("runGraph: a check node runs from entry or routes, not a maps edge");
1290
+ const placement = options.checkPlacement;
1291
+ for (const [id, check] of checks) {
1292
+ const timeoutMs = check.timeoutMs ?? CHECK_TIMEOUT_MS;
1293
+ if (!check.command.trim() || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new ValidationError(`runGraph: check node ${id} needs a command and positive timeoutMs`);
1294
+ if (placement && !check.environment?.trim()) throw new ValidationError(`runGraph: check node ${id} needs environment for box placement`);
1295
+ }
1296
+ const local = placement === void 0 ? localCheckDirectories(options) : void 0;
1297
+ const workspace = local?.cwd ?? backendWorkspace(options);
1298
+ const context = local || options.runDir ? void 0 : options.runContext;
1299
+ const runDir = context ? void 0 : local?.runDir ?? driverRunDir(options, workspace);
1300
+ const file = runDir === void 0 ? void 0 : join(runDir, "command-checks.jsonl");
1301
+ const runId = options.runId ?? options.runContext?.runId;
1302
+ const leaf = (profile, spawn) => {
1303
+ const id = profile.name;
1304
+ const check = checks.get(id);
1305
+ if (check === void 0) throw new ValidationError(`runGraph: ${id} is not a check node`);
1306
+ const checkDigest = contentAddress({
1307
+ ...check,
1308
+ timeoutMs: check.timeoutMs ?? CHECK_TIMEOUT_MS,
1309
+ at: local?.cwd ?? "box"
1310
+ });
1311
+ const binding = contentAddress({
1312
+ runId,
1313
+ node: id,
1314
+ checkDigest,
1315
+ at: spawn?.assignmentId
1316
+ });
1317
+ const executorFactory = (_spec, context_) => {
1318
+ let artifact;
1319
+ const settle = (out, ms) => {
1320
+ artifact = {
1321
+ out,
1322
+ outRef: contentAddress(out),
1323
+ verdict: {
1324
+ valid: true,
1325
+ score: out.pass ? 1 : 0
1326
+ },
1327
+ spent: {
1328
+ iterations: 1,
1329
+ tokens: {
1330
+ input: 0,
1331
+ output: 0
1332
+ },
1333
+ tokensKnown: true,
1334
+ usd: 0,
1335
+ usdKnown: true,
1336
+ ms
1337
+ }
1338
+ };
1339
+ return artifact;
1340
+ };
1341
+ const execute = async (task, signal) => {
1342
+ const node = context_.node;
1343
+ if (!node?.nodeId || !node.attemptId || !spawn) throw new ValidationError(`runGraph: check node ${id} ran outside a supervised node`);
1344
+ const attempt = {
1345
+ binding,
1346
+ checkDigest,
1347
+ nodeId: node.nodeId,
1348
+ attemptId: node.attemptId
1349
+ };
1350
+ const prior = file ? await replayCheckReceipt(file, binding, checkDigest) : await replayContextReceipt(context, runId, binding, checkDigest);
1351
+ if (prior) return settle(prior, 0);
1352
+ const started = Date.now();
1353
+ const staged = runDir ? void 0 : await mkdtemp(join(tmpdir(), "graph-check-"));
1354
+ const evidence = join(runDir ?? staged, "graph-evidence", contentAddress(attempt).slice(7, 31));
1355
+ const box = placement;
1356
+ const secrets = await readSecrets(check.secrets, local ? process.env : typeof box.env === "function" ? await box.env({
1357
+ node: id,
1358
+ secrets: check.secrets ?? [],
1359
+ signal
1360
+ }) : box.env ?? {}, id);
1361
+ let ran;
1362
+ try {
1363
+ await writeEvidence(evidence, task);
1364
+ ran = local ? await runLocalCheck(check, local.cwd, evidence, signal, secrets) : await runBoxCheck(check, id, placement, evidence, signal, secrets);
1365
+ } finally {
1366
+ if (staged) await rm(staged, {
1367
+ recursive: true,
1368
+ force: true
1369
+ });
1370
+ }
1371
+ const { output, truncated } = tail(redact(ran.output, secrets));
1372
+ const json = lastJsonObject(redact(ran.stdout, secrets));
1373
+ const receipt = {
1374
+ version: 1,
1375
+ ...attempt,
1376
+ nodeName: id,
1377
+ outputDigest: contentAddress(task),
1378
+ exitCode: ran.exitCode,
1379
+ output,
1380
+ outputTruncated: truncated,
1381
+ nativeValid: true,
1382
+ valid: ran.exitCode === 0,
1383
+ ...json === void 0 ? {} : { json }
1384
+ };
1385
+ if (file) await appendReceipt(file, workspace, receipt);
1386
+ else await retainedExecutorContext(context_)?.onEvidence?.({ graphCheckReceipt: {
1387
+ ...receipt,
1388
+ key: receiptKey(receipt)
1389
+ } });
1390
+ return settle(checkOutput(receipt), Date.now() - started);
1391
+ };
1392
+ return {
1393
+ runtime: "graph-check",
1394
+ execute,
1395
+ recover: execute,
1396
+ async teardown() {
1397
+ return { destroyed: true };
1398
+ },
1399
+ resultArtifact() {
1400
+ if (!artifact) throw new ValidationError(`runGraph: check node ${id} has not settled`);
1401
+ return artifact;
1402
+ }
1403
+ };
1404
+ };
1405
+ return {
1406
+ name: id,
1407
+ act: async () => "",
1408
+ executorSpec: {
1409
+ profile,
1410
+ harness: null,
1411
+ executorFactory: runtimeCheckFactory(executorFactory)
1412
+ }
1413
+ };
1414
+ };
1415
+ return {
1416
+ names: [...checks.keys()],
1417
+ leaf
1418
+ };
1419
+ }
1420
+ // src/runtime/supervise/graph.ts
1421
+ /**
1422
+ * `runGraph` interprets a topology on `supervise()`. Nodes are canonical `AgentProfile`s, edges
1423
+ * carry versioned {@link PromptHandle} directives, and `deliverable` and one conserved `budget`
1424
+ * are mandatory. It does not schedule: `runAgentRounds` is not the substrate. A spawn's
1425
+ * `profile.name` is the node id, so a driver cannot add capabilities the node did not define.
1426
+ * Every delegates, analyzes, maps, or routes hop is an edge-ledger row (`delivered`, `stripped`,
1427
+ * `empty`, `unpropagated`, `failed`) on the result and in the journal. Edge text lives in the
1428
+ * prompt registry. A delegates edge at its traversal cap refuses. `continuity: 'resume'`
1429
+ * re-attaches to the node's latest settled session; ledger hops say `fresh`, `resume`, or `steer`.
1430
+ * A `maps` edge spawns one authorized item per selected array element and then the join, on the
1431
+ * same pool, with no model turn. With `entry` set, Runtime owns the root and fires `routes` on
1432
+ * settle (`graph-flow.ts`). An `analyzes` edge names a registry lens or a graph node, never a
1433
+ * delegates target, and an id in both places is refused.
1434
+ *
1435
+ * @experimental
1436
+ */
1437
+ /** A delegates edge exhausted its traversal cap and the run produced no winner: the cap, not the
1438
+ * task, ended it. Carries the full evidence so failing loud loses nothing. */
1439
+ var GraphEdgeCapError = class extends Error {
1440
+ exhaustedEdges;
1441
+ ledger;
1442
+ result;
1443
+ constructor(exhaustedEdges, ledger, result) {
1444
+ super(`runGraph: edge traversal cap exhausted on ${exhaustedEdges.join(", ")} and the run delivered no winner — the cap (the cyclic-graph backstop), not the task, ended this run. Raise maxTraversals on the edge or fix the cycle; the full edge ledger and the supervised result ride on this error.`);
1445
+ this.name = "GraphEdgeCapError";
1446
+ this.exhaustedEdges = exhaustedEdges;
1447
+ this.ledger = ledger;
1448
+ this.result = result;
1449
+ }
1450
+ };
1451
+ /**
1452
+ * Every `SuperviseOptions` key, partitioned by what `runGraph` does with it.
1453
+ *
1454
+ * `runGraph` starts one `supervise()` run, so every option that run honors is a graph option too
1455
+ * unless the graph itself owns the value. This used to be an opt-in list written by hand, and the
1456
+ * hand lost: 25 of 49 keys never reached `supervise()` from a graph, including `childSettleGraceMs`
1457
+ * (a root-driver failure tore down children that had ALREADY computed the deliverable) and
1458
+ * `driverRetry` (agent-runtime#963). Each absence was discovered by losing a run.
1459
+ *
1460
+ * The four lists below must cover `keyof SuperviseOptions` exactly. `everySuperviseOptionIsClassified`
1461
+ * below fails to COMPILE, naming the offender, when a key belongs to none of them — so the next
1462
+ * option added to `supervise()` cannot go missing here silently. It has to be classified, and
1463
+ * classifying it as forwarded is one word.
1464
+ */
1465
+ /** The graph derives these from the `AgentGraph` itself; a caller value would be overwritten. */
1466
+ const GRAPH_OWNED_SUPERVISE_OPTIONS = [
1467
+ "rootDriverFromBackend",
1468
+ "resolveSpawnProfile",
1469
+ "budget",
1470
+ "deliverable",
1471
+ "makeWorkerAgent",
1472
+ "onCoordinationEvent",
1473
+ "analyzeOnSettle",
1474
+ "mapOnSettle",
1475
+ "routeFlow",
1476
+ "checkNodes",
1477
+ "continuityByProfile",
1478
+ "inheritSpawnRights",
1479
+ "promptRegistry"
1480
+ ];
1481
+ /** Caller-facing on `RunGraphOptions`, but the graph wraps or defaults the value before it goes in:
1482
+ * `hooks` composes with the graph's own spawn-binding hook, `authorizeMessage` is wrapped so a
1483
+ * narrowed instruction ledgers `stripped`, `authorizeSpawn` runs AFTER the graph has pinned the
1484
+ * node (so a product sees the canonical profile, never the driver's stub), `makeLeafAgent` is the
1485
+ * caller's leaf override slotted under the graph's pinning, `analysts` rides only with analyze
1486
+ * routes, and `journal`/`blobs`/`runId` get graph defaults. */
1487
+ const GRAPH_TRANSFORMED_SUPERVISE_OPTIONS = [
1488
+ "hooks",
1489
+ "authorizeMessage",
1490
+ "authorizeSpawn",
1491
+ "makeLeafAgent",
1492
+ "analysts",
1493
+ "journal",
1494
+ "blobs",
1495
+ "runId",
1496
+ "decideGate"
1497
+ ];
1498
+ /** Not reachable from a graph, with the reason. `registry` is a NAME COLLISION, not a policy:
1499
+ * `RunGraphOptions.registry` is the directive `PromptRegistry` and `SuperviseOptions.registry` is
1500
+ * the `SuperviseRegistry` name→value table. Two different types under one name; the graph's wins.
1501
+ * Giving the supervise one a graph channel means renaming a public option, so it is recorded here
1502
+ * rather than silently dropped. */
1503
+ const GRAPH_REFUSED_SUPERVISE_OPTIONS = ["registry"];
1504
+ /**
1505
+ * Forwarded to the root `supervise()` VERBATIM. Everything not owned, transformed, or refused
1506
+ * above belongs here — the default is "a graph honors it", not "someone remembered to add it".
1507
+ */
1508
+ const GRAPH_FORWARDED_SUPERVISE_OPTIONS = [
1509
+ "parking",
1510
+ "backend",
1511
+ "profileGuidance",
1512
+ "escalateQuestion",
1513
+ "rootHandle",
1514
+ "signal",
1515
+ "execution",
1516
+ "resolveDeliverable",
1517
+ "coordination",
1518
+ "peerMail",
1519
+ "peerAudit",
1520
+ "traceTools",
1521
+ "driverBackend",
1522
+ "profileSecurity",
1523
+ "authorizeSpawn",
1524
+ "router",
1525
+ "driveHarness",
1526
+ "selectSeat",
1527
+ "driverRetry",
1528
+ "onDriverAttempt",
1529
+ "continuation",
1530
+ "childSettleGraceMs",
1531
+ "teardownConfirmMs",
1532
+ "retainedAtSettlement",
1533
+ "resolveDriveHarness",
1534
+ "driveHarnessMaterialization",
1535
+ "resolveSupervisorTools",
1536
+ "extraTools",
1537
+ "executeExtraTool",
1538
+ "recoverExecutor",
1539
+ "perWorker",
1540
+ "reservationPolicy",
1541
+ "deadlineCheckIn",
1542
+ "budgetStopHoldMs",
1543
+ "workerSlots",
1544
+ "watchWorkers",
1545
+ "stallAfterMs",
1546
+ "wake",
1547
+ "runDir",
1548
+ "resume",
1549
+ "segment",
1550
+ "runContext",
1551
+ "steerDir",
1552
+ "stopRule",
1553
+ "onProgressStop",
1554
+ "maxDepth",
1555
+ "maxTurns",
1556
+ "compaction",
1557
+ "now",
1558
+ "allowedModels",
1559
+ "finalizer",
1560
+ "otel",
1561
+ "graphRegistry"
1562
+ ];
1563
+ /**
1564
+ * Every key `RunGraphOptions` accepts, as data: the forwarded and transformed `SuperviseOptions`
1565
+ * keys, the name-colliding `registry`, and the graph's own `brain`. `GRAPH_OWNED_SUPERVISE_OPTIONS`
1566
+ * is deliberately absent — a caller value for one of those is overwritten by the graph, so it is
1567
+ * refused by name rather than discarded.
1568
+ *
1569
+ * The assignment below fails to COMPILE, naming the key, when `RunGraphOptions` declares something
1570
+ * this list does not carry.
1571
+ */
1572
+ const runGraphOptionKeys = [
1573
+ ...GRAPH_FORWARDED_SUPERVISE_OPTIONS,
1574
+ ...GRAPH_TRANSFORMED_SUPERVISE_OPTIONS,
1575
+ ...GRAPH_REFUSED_SUPERVISE_OPTIONS,
1576
+ "brain",
1577
+ "checkPlacement"
1578
+ ];
1579
+ const runGraphOptionKeySet = new Set(runGraphOptionKeys);
1580
+ const graphOwnedOptionKeySet = new Set(GRAPH_OWNED_SUPERVISE_OPTIONS);
1581
+ /**
1582
+ * Refuse an option key the graph would not read, naming it and saying which kind it is.
1583
+ *
1584
+ * A graph-owned key is the sharper case: it type-checks against a widened `SuperviseOptions`
1585
+ * value, the graph derives its own value and overwrites the caller's, the run succeeds, and the
1586
+ * capability the caller asked for is not there. The rename of the leaf seam from `makeWorkerAgent`
1587
+ * to `makeLeafAgent` is the shape of the failure — the old name stayed a valid `SuperviseOptions`
1588
+ * key AND a graph-owned one, so a caller that kept passing it got a run with no leaf override and
1589
+ * no complaint.
1590
+ */
1591
+ function assertRunGraphOptionKeys(opts) {
1592
+ const graphOwned = [];
1593
+ const unknown = [];
1594
+ for (const key of Object.keys(opts)) {
1595
+ if (runGraphOptionKeySet.has(key)) continue;
1596
+ if (graphOwnedOptionKeySet.has(key)) graphOwned.push(key);
1597
+ else unknown.push(key);
1598
+ }
1599
+ if (graphOwned.length > 0) throw new ConfigError(`runGraph: ${graphOwned.sort().join(", ")} — the graph derives this from the AgentGraph itself, so a caller value is overwritten and the capability it asks for would silently be absent`);
1600
+ if (unknown.length > 0) throw new ConfigError(`runGraph: unknown option ${unknown.sort().join(", ")} — no reader consults that name, so the value would be discarded and the capability it asks for would silently be absent`);
1601
+ }
1602
+ /** Copy every forwarded option the caller actually set. Absent stays absent: `supervise()` and the
1603
+ * graph must not disagree about what "unset" means. */
1604
+ function forwardedSuperviseOptions(opts) {
1605
+ const forwarded = {};
1606
+ for (const key of GRAPH_FORWARDED_SUPERVISE_OPTIONS) {
1607
+ const value = opts[key];
1608
+ if (value !== void 0) forwarded[key] = value;
1609
+ }
1610
+ return forwarded;
1611
+ }
1612
+ /** Validate the graph and resolve every directive BEFORE any compute is spent — an invalid
1613
+ * topology or an unknown directive is a configuration fault, never a mid-run surprise. */
1614
+ function validateGraph(graph, registry, analysts) {
1615
+ if (graph.entry === void 0) {
1616
+ if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) throw new ValidationError("runGraph: graph.nodes must be a non-empty array");
1617
+ if (!Array.isArray(graph.edges) || graph.edges.length === 0) throw new ValidationError("runGraph: graph.edges must be a non-empty array");
1618
+ }
1619
+ if (typeof graph.deliverable?.check !== "function") throw new ValidationError("runGraph: graph.deliverable is mandatory (termination oracle)");
1620
+ if (typeof graph.budget !== "object" || graph.budget === null) throw new ValidationError("runGraph: graph.budget is mandatory (the conserved pool)");
1621
+ if (graph.entry !== void 0) return validateFlowGraph(graph, registry, analysts);
1622
+ if (graph.gates !== void 0) throw new ValidationError("runGraph: graph.gates needs graph.entry");
1623
+ return validateGraphTopology(graph.nodes, graph.edges, registry, analysts);
1624
+ }
1625
+ /**
1626
+ * Execute an {@link AgentGraph}. A flow graph (`entry` set) runs from its entries and routes edges
1627
+ * under a runtime-owned root that places no model call. Otherwise the root node becomes the
1628
+ * supervisor (`supervise()` — the execution core), each worker node is spawnable BY NODE ID
1629
+ * (`spawn_worker` with `profile: { name: '<node id>' }`; the node's canonical profile is pinned by
1630
+ * the graph), each
1631
+ * delegates directive is appended to the worker profile's `prompt.instructions` per traversal,
1632
+ * and each analyzes edge becomes an analyst-on-settle route with a real DESTINATION. Every
1633
+ * traversal is ledgered and journaled.
1634
+ */
1635
+ function runGraph(graph, opts) {
1636
+ const { brain, ...runtimeOptions } = graphExecutionOptions(opts);
1637
+ return superviseAgentGraph(graph, runtimeOptions, brain);
1638
+ }
1639
+ /** Alias for graph tests written before `RunGraphOptions.brain` was production. The production
1640
+ * entry accepts the same shape; this wrapper only keeps the `/testing` import path working. */
1641
+ function runGraphWithTestBrain(graph, opts) {
1642
+ const { brain, ...runtimeOptions } = opts;
1643
+ return superviseAgentGraph(graph, runtimeOptions, brain);
1644
+ }
1645
+ /**
1646
+ * The graph supervise run, as its own entry: the engine's `run-graph` preset node executes exactly
1647
+ * this, so the preset and `runGraph` are the same code path by construction (agent-runtime#982).
1648
+ */
1649
+ /**
1650
+ * Every refusal a graph earns before any compute, in one place: the authoring contract. `runGraph`
1651
+ * calls it FIRST so a malformed graph throws synchronously, exactly as it did before the engine
1652
+ * preset (agent-runtime#982) moved execution behind a promise.
1653
+ */
1654
+ function assertRunGraphAuthoring(graph, opts, brain) {
1655
+ assertRunGraphOptionKeys(opts);
1656
+ if (graph.continuation !== void 0) {
1657
+ portableContinuationPolicySchema.parse(graph.continuation);
1658
+ if (opts.continuation !== void 0) throw new ValidationError("runGraph: graph.continuation and opts.continuation have two policy owners");
1659
+ if (graph.entry !== void 0) throw new ValidationError("runGraph: graph.continuation with graph.entry has no root manager");
1660
+ }
1661
+ const validated = validateGraph(graph, opts.registry ?? kernelPromptRegistry(), opts.analysts);
1662
+ const requireLeaves = () => {
1663
+ if (!opts.backend && !opts.makeLeafAgent) throw new ValidationError("runGraph: provide opts.backend (where nodes run) or opts.makeLeafAgent");
1664
+ };
1665
+ if (validated.flow !== void 0) {
1666
+ const rootOptions = [...brain ? ["brain"] : [], ...[
1667
+ "driverBackend",
1668
+ "finalizer",
1669
+ "maxTurns",
1670
+ "compaction"
1671
+ ].filter((key) => opts[key] !== void 0)];
1672
+ if (rootOptions.length > 0) throw new ValidationError(`runGraph: ${rootOptions.join(", ")} with graph.entry — a flow graph has no root manager: Runtime spawns its entries and traverses its routes edges, and the graph owns the run's output`);
1673
+ if ((graph.gates?.length ?? 0) > 0 && opts.decideGate === void 0) throw new ValidationError("runGraph: graph.gates needs opts.decideGate to answer them");
1674
+ requireLeaves();
1675
+ return validated;
1676
+ }
1677
+ const { root } = validated;
1678
+ if (graph.continuation !== void 0 && !harnessRunsAgent(root.profile.harness)) throw new ValidationError("runGraph: graph.continuation requires an external-harness root");
1679
+ if (brain && opts.driverBackend) throw new ValidationError("runGraph: brain and driverBackend are mutually exclusive — a caller brain makes the root model calls, a driverBackend places a harness that makes its own");
1680
+ if (brain && harnessRunsAgent(root.profile.harness)) throw new ValidationError(`runGraph: root node '${root.id}' declares harness '${root.profile.harness}', so the harness drives it — a caller brain applies only to a router-brained root (profile.harness omitted or 'cli-base')`);
1681
+ requireLeaves();
1682
+ return validated;
1683
+ }
1684
+ function superviseAgentGraph(graph, opts, brain) {
1685
+ const { continuation, ...topology } = graph;
1686
+ graph = {
1687
+ ...topology,
1688
+ ...continuation === void 0 ? {} : { continuation: portableContinuationPolicySchema.parse(continuation) }
1689
+ };
1690
+ if (graph.deliverable.command !== void 0) {
1691
+ const bound = bindGraphCommand(graph, opts);
1692
+ return superviseAgentGraph(bound.graph, bound.options, brain);
1693
+ }
1694
+ const registry = opts.registry ?? kernelPromptRegistry();
1695
+ if (opts.runContext?.acquire !== void 0) return withRunContext(opts.runContext, opts.signal, (runContext, signal) => superviseAgentGraph(graph, {
1696
+ ...opts,
1697
+ runContext,
1698
+ ...signal === void 0 ? {} : { signal }
1699
+ }, brain));
1700
+ const validated = assertRunGraphAuthoring(graph, opts, brain);
1701
+ const context = opts.runContext ?? (opts.runDir === void 0 ? createInMemoryRunContext() : createFileRunContext(resolve(opts.runDir)));
1702
+ const journal = opts.journal ?? context.journal;
1703
+ const blobs = opts.blobs ?? context.blobs;
1704
+ const runId = opts.runId ?? opts.runContext?.runId ?? `graph-${canonicalCandidateDigest(graph.nodes.map((n) => n.id)).slice(7, 19)}`;
1705
+ const now = opts.now ?? Date.now;
1706
+ const checkNodes = bindGraphCheckNodes(graph, {
1707
+ ...opts,
1708
+ runId
1709
+ });
1710
+ const guidance = profileGuidanceComposer(opts.profileGuidance);
1711
+ const overlay = createGraphOverlay(validated, {
1712
+ registry,
1713
+ journal,
1714
+ runId,
1715
+ now,
1716
+ ...guidance ? { guidance } : {},
1717
+ ...opts.authorizeSpawn ? { authorizeSpawn: opts.authorizeSpawn } : {}
1718
+ });
1719
+ const flow = validated.flow === void 0 ? void 0 : createFlowRun(overlay.nodeOfWorker);
1720
+ const callerAuthorizeMessage = opts.authorizeMessage;
1721
+ const authorizeMessage = overlay.observeMessages((input) => callerAuthorizeMessage ? callerAuthorizeMessage(input) : { instruction: input.instruction });
1722
+ const hooks = composeRuntimeHooks({ onEvent: (event) => {
1723
+ if (event.target !== "agent.spawn" || event.phase !== "after") return;
1724
+ const payload = event.payload;
1725
+ if (typeof payload?.childId !== "string" || typeof payload.assignmentId !== "string") return;
1726
+ return overlay.bindSpawn(event.parentId, payload.assignmentId, payload.childId);
1727
+ } }, opts.hooks);
1728
+ const rootProfile = flow === void 0 ? graphRootProfile(validated, registry) : flowRootProfile;
1729
+ const rootBrain = flow === void 0 ? brain : runtimeFlowBrain();
1730
+ const task = flow === void 0 ? graph.deliverable.describe ?? `Deliver the graph's deliverable by driving your worker nodes (root: '${validated.root.id}').` : graph.deliverable.describe ?? "Runtime runs this flow graph from its entries.";
1731
+ const start = async () => {
1732
+ const superviseOptions = {
1733
+ ...forwardedSuperviseOptions(opts),
1734
+ rootDriverFromBackend: flow !== void 0,
1735
+ budget: graph.budget,
1736
+ ...graph.continuation === void 0 ? {} : { continuation: graph.continuation },
1737
+ deliverable: flow?.routeCheck(graph.deliverable) ?? graph.deliverable,
1738
+ authorizeSpawn: overlay.authorizeSpawn,
1739
+ resolveSpawnProfile: overlay.resolveSpawnProfile,
1740
+ inheritSpawnRights: false,
1741
+ promptRegistry: registry,
1742
+ ...opts.makeLeafAgent ? { makeLeafAgent: opts.makeLeafAgent } : {},
1743
+ journal,
1744
+ blobs,
1745
+ runId,
1746
+ hooks,
1747
+ onCoordinationEvent: async (context, _eventId, record) => {
1748
+ if (record.event.type === "route" && context.depth !== 0) return;
1749
+ overlay.onCoordinationEvent(record.event);
1750
+ flow?.observe(record.event);
1751
+ },
1752
+ ...overlay.routes.length > 0 ? {
1753
+ analyzeOnSettle: overlay.routes,
1754
+ ...opts.analysts ? { analysts: opts.analysts } : {}
1755
+ } : {},
1756
+ ...overlay.maps.length > 0 ? { mapOnSettle: overlay.maps } : {},
1757
+ ...Object.keys(overlay.continuityByProfile).length > 0 ? { continuityByProfile: overlay.continuityByProfile } : {},
1758
+ ...flow === void 0 ? {} : {
1759
+ routeFlow: routeFlowOf(validated),
1760
+ ...checkNodes && { checkNodes },
1761
+ ...opts.decideGate && { decideGate: journaledGateDecider(opts.decideGate, journal, runId) },
1762
+ finalizer: flow.finalizer,
1763
+ wake: {
1764
+ debounceMs: 0,
1765
+ ...opts.wake
1766
+ }
1767
+ },
1768
+ authorizeMessage
1769
+ };
1770
+ const supervised = rootBrain === void 0 ? await supervise(rootProfile, task, superviseOptions) : await superviseWithTestBrain(rootProfile, task, {
1771
+ ...superviseOptions,
1772
+ brain: rootBrain
1773
+ });
1774
+ const { ledger, exhaustedEdges, exhaustedDelegates } = await overlay.settle();
1775
+ const concluded = flow?.conclude(supervised);
1776
+ if (concluded?.kind === "unrouted") throw new GraphRouteError(concluded.unrouted, ledger, supervised);
1777
+ const result = concluded?.result ?? supervised;
1778
+ const stopped = concluded?.stopped;
1779
+ const lifecycleEnded = result.kind === "no-winner" && (result.reason === "aborted" || result.reason === "cancelled" || result.reason === "budget-exhausted" || result.reason === "deadline");
1780
+ if (result.kind !== "winner" && !lifecycleEnded && exhaustedDelegates.length > 0) throw new GraphEdgeCapError(exhaustedDelegates, ledger, result);
1781
+ return {
1782
+ result,
1783
+ ledger,
1784
+ exhaustedEdges,
1785
+ runId,
1786
+ ...stopped ? { stopped } : {}
1787
+ };
1788
+ };
1789
+ return start();
1790
+ }
1791
+ export { graphExecutionOptions as a, superviseAgentGraph as i, runGraph as n, runIsolatedCheck as o, runGraphWithTestBrain as r, validateIsolatedCheckOutputs as s, GraphEdgeCapError as t };
1792
+
1793
+ //# sourceMappingURL=graph-pFTeNZ-G.js.map