@tangle-network/agent-runtime 0.188.1 → 0.190.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 (102) hide show
  1. package/dist/{activation-C39nGToq.js → activation-BgRWFKZi.js} +3 -3
  2. package/dist/{activation-C39nGToq.js.map → activation-BgRWFKZi.js.map} +1 -1
  3. package/dist/agent.d.ts +1 -1
  4. package/dist/agent.js +3 -3
  5. package/dist/{authoring-fsUwd9hg.js → authoring-dQQ2z1fT.js} +2 -2
  6. package/dist/{authoring-fsUwd9hg.js.map → authoring-dQQ2z1fT.js.map} +1 -1
  7. package/dist/candidate-execution/index.d.ts +4 -4
  8. package/dist/candidate-execution/index.js +5 -5
  9. package/dist/{candidate-execution-CehLeZmx.js → candidate-execution-BSkZErxD.js} +255 -9
  10. package/dist/candidate-execution-BSkZErxD.js.map +1 -0
  11. package/dist/conversation.d.ts +1 -1
  12. package/dist/{coordination-driver-CtbyZv5d.js → coordination-driver-Da1IGdTP.js} +6 -6
  13. package/dist/coordination-driver-Da1IGdTP.js.map +1 -0
  14. package/dist/{durable-file-g3YdfEh4.js → durable-file-D24y9zg7.js} +2 -2
  15. package/dist/{durable-file-g3YdfEh4.js.map → durable-file-D24y9zg7.js.map} +1 -1
  16. package/dist/durable.d.ts +2 -2
  17. package/dist/durable.js +3 -3
  18. package/dist/{environment-provider-C4AQiUxj.d.ts → environment-provider-Bf-hgR6p.d.ts} +2 -2
  19. package/dist/{environment-provider-DZxuH3E4.js → environment-provider-DcelIawX.js} +2 -2
  20. package/dist/{environment-provider-DZxuH3E4.js.map → environment-provider-DcelIawX.js.map} +1 -1
  21. package/dist/environment-provider.d.ts +1 -1
  22. package/dist/environment-provider.js +1 -1
  23. package/dist/{graph-Cox_7pht.js → graph-Bd1elhdL.js} +5 -3
  24. package/dist/graph-Bd1elhdL.js.map +1 -0
  25. package/dist/graph.d.ts +3 -3
  26. package/dist/graph.js +5 -5
  27. package/dist/{improvement-cycle-CnHGNT5r.js → improvement-cycle-BanlFMyL.js} +7 -7
  28. package/dist/{improvement-cycle-CnHGNT5r.js.map → improvement-cycle-BanlFMyL.js.map} +1 -1
  29. package/dist/{index-DdmozvB8.d.ts → index-C-gBk5M9.d.ts} +5 -5
  30. package/dist/{index-DQv7iCeE.d.ts → index-CUosKU4N.d.ts} +145 -5
  31. package/dist/{index-DA9jKuJc.d.ts → index-Clb7_qML.d.ts} +2 -2
  32. package/dist/{index-DVob1IA1.d.ts → index-DpNil9jh.d.ts} +130 -56
  33. package/dist/index.d.ts +11 -11
  34. package/dist/index.js +12 -12
  35. package/dist/intelligence.d.ts +6 -6
  36. package/dist/intelligence.js +6 -6
  37. package/dist/kernel.d.ts +6 -6
  38. package/dist/kernel.js +12 -12
  39. package/dist/{knowledge-BZDVXRCl.js → knowledge-BXdXlv48.js} +5 -5
  40. package/dist/{knowledge-BZDVXRCl.js.map → knowledge-BXdXlv48.js.map} +1 -1
  41. package/dist/knowledge.d.ts +1 -1
  42. package/dist/knowledge.js +1 -1
  43. package/dist/{loop-runner-bin-hXqgNrPN.js → loop-runner-bin-CGQzzWAc.js} +3 -3
  44. package/dist/{loop-runner-bin-hXqgNrPN.js.map → loop-runner-bin-CGQzzWAc.js.map} +1 -1
  45. package/dist/{loop-runner-bin-g9fWDuOh.d.ts → loop-runner-bin-Cy4f9M6p.d.ts} +3 -3
  46. package/dist/loop-runner-bin.d.ts +1 -1
  47. package/dist/loop-runner-bin.js +1 -1
  48. package/dist/mcp/bin.js +3 -3
  49. package/dist/mcp/index.d.ts +3 -3
  50. package/dist/mcp/index.js +7 -7
  51. package/dist/mcp/memory-bin.js +1 -1
  52. package/dist/{memory-server-DsKOhzGf.js → memory-server-Crkevawz.js} +2 -2
  53. package/dist/{memory-server-DsKOhzGf.js.map → memory-server-Crkevawz.js.map} +1 -1
  54. package/dist/{openai-tools-Cz04RKfD.js → openai-tools-BSDk_umV.js} +2 -2
  55. package/dist/{openai-tools-Cz04RKfD.js.map → openai-tools-BSDk_umV.js.map} +1 -1
  56. package/dist/{prepare-BpIGs5Tc.js → prepare-B5fJ7mLS.js} +336 -246
  57. package/dist/prepare-B5fJ7mLS.js.map +1 -0
  58. package/dist/primeintellect/index.d.ts +1 -1
  59. package/dist/primeintellect/index.js +1 -1
  60. package/dist/profiles.js +1 -1
  61. package/dist/{protected-model-port-YclORl0x.js → protected-model-port-B0xFSAjt.js} +19 -7
  62. package/dist/protected-model-port-B0xFSAjt.js.map +1 -0
  63. package/dist/{protected-model-port-B8ssMyUg.d.ts → protected-model-port-BtXp2dUY.d.ts} +2 -2
  64. package/dist/{provision-supervisor-B-_4I1In.js → provision-supervisor-bwpzajiz.js} +8 -8
  65. package/dist/{provision-supervisor-B-_4I1In.js.map → provision-supervisor-bwpzajiz.js.map} +1 -1
  66. package/dist/{redact-C9EWk1FT.d.ts → redact-Cv1GfOFO.d.ts} +3 -3
  67. package/dist/{run-layout-Cd_l2XWV.js → run-layout-B8I_LXN-.js} +3 -3
  68. package/dist/{run-layout-Cd_l2XWV.js.map → run-layout-B8I_LXN-.js.map} +1 -1
  69. package/dist/{runtime-EQgE3942.d.ts → runtime-BMWmSu4A.d.ts} +202 -4
  70. package/dist/{runtime-CFltmoiL.js → runtime-Ddh7PyCy.js} +15 -15
  71. package/dist/{runtime-CFltmoiL.js.map → runtime-Ddh7PyCy.js.map} +1 -1
  72. package/dist/{spawn-journal-CxRa-NJ2.js → spawn-journal-B9eTmVAn.js} +2 -2
  73. package/dist/{spawn-journal-CxRa-NJ2.js.map → spawn-journal-B9eTmVAn.js.map} +1 -1
  74. package/dist/{stream-agent-turn-BomJ21AK.d.ts → stream-agent-turn-uqzf9ZEh.d.ts} +2 -2
  75. package/dist/{structural-rollout-C8sB6B4q.js → structural-rollout-0NKvKuFm.js} +3 -3
  76. package/dist/{structural-rollout-C8sB6B4q.js.map → structural-rollout-0NKvKuFm.js.map} +1 -1
  77. package/dist/{supervise-BUy1cr2L.js → supervise-yDiZAqlN.js} +372 -26
  78. package/dist/supervise-yDiZAqlN.js.map +1 -0
  79. package/dist/{supervisor-BSNkaM2a.js → supervisor-CqJW4qap.js} +489 -11
  80. package/dist/supervisor-CqJW4qap.js.map +1 -0
  81. package/dist/testing.d.ts +2 -2
  82. package/dist/testing.js +13 -13
  83. package/dist/{top-app-DIBApkt1.js → top-app-KgIFpD9H.js} +3 -3
  84. package/dist/{top-app-DIBApkt1.js.map → top-app-KgIFpD9H.js.map} +1 -1
  85. package/dist/tui/bin.js +1 -1
  86. package/dist/tui/index.d.ts +1 -1
  87. package/dist/tui/index.js +2 -2
  88. package/dist/{types-BbkiJR1D.d.ts → types-02hXqnSP.d.ts} +37 -2
  89. package/dist/{types-19ZOddS3.d.ts → types-1s_29INY.d.ts} +15 -10
  90. package/dist/{util-BfoW6RU7.js → util-mCNUeboW.js} +21 -2
  91. package/dist/util-mCNUeboW.js.map +1 -0
  92. package/dist/{workspace-archive-5K72EnlT.js → workspace-archive-yAPDklLV.js} +4 -4
  93. package/dist/{workspace-archive-5K72EnlT.js.map → workspace-archive-yAPDklLV.js.map} +1 -1
  94. package/package.json +9 -9
  95. package/dist/candidate-execution-CehLeZmx.js.map +0 -1
  96. package/dist/coordination-driver-CtbyZv5d.js.map +0 -1
  97. package/dist/graph-Cox_7pht.js.map +0 -1
  98. package/dist/prepare-BpIGs5Tc.js.map +0 -1
  99. package/dist/protected-model-port-YclORl0x.js.map +0 -1
  100. package/dist/supervise-BUy1cr2L.js.map +0 -1
  101. package/dist/supervisor-BSNkaM2a.js.map +0 -1
  102. package/dist/util-BfoW6RU7.js.map +0 -1
@@ -1,12 +1,12 @@
1
- import { x as contentAddress } from "./spawn-journal-CxRa-NJ2.js";
1
+ import { x as contentAddress } from "./spawn-journal-B9eTmVAn.js";
2
2
  import { i as redactProtectedValue, n as createProtectedRecordRedactor, r as redactProtectedReason, t as assertNoProtectedBytes } from "./protected-redaction--F3v1oo8.js";
3
3
  import { agentCandidateArtifactRefSchema, agentCandidateBenchmarkResultEvidenceSchema, agentCandidateBenchmarkSuiteSchema, agentCandidateBenchmarkTaskSchema, agentCandidateBundleSchema, agentCandidateContainerSchema, agentCandidateExecutionLimitsSchema, agentCandidateExecutionPlanEvidenceSchema, agentCandidateExecutionPlanMaterialSchema, agentCandidateMaterializationReceiptSchema, agentCandidateModelAccessNetworkSchema, agentCandidateModelSettlementEvidenceSchema, agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfileSchema, agentCandidateResolvedModelSchema, agentCandidateRunCellSchema, agentCandidateRunReceiptSchema, agentCandidateTaskOutcomeEvidenceSchema, agentCandidateTaskOutcomeSpecSchema, agentCandidateTerminationSchema, agentCandidateWorkspaceManifestMaterialSchema, agentCandidateWorkspaceSnapshotEvidenceSchema, agentProfileDiffSchema, agentProfileSchema, applyAgentProfileDiff, sha256DigestSchema } from "@tangle-network/agent-interface";
4
4
  import { REDACTION_VERSION, canonicalJson, isLlmSpan } from "@tangle-network/agent-eval";
5
5
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
6
- import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
7
- import { applyAgentCandidateWorkspacePlan, isMaterializerHarness, materializeCandidateProfile } from "@tangle-network/agent-profile-materialize";
8
- import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat } from "node:fs/promises";
9
6
  import { constants } from "node:fs";
7
+ import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat } from "node:fs/promises";
8
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
9
+ import { applyAgentCandidateWorkspacePlan, isMaterializerHarness, materializeCandidateProfile, resolvePiRuntimePaths } from "@tangle-network/agent-profile-materialize";
10
10
  import { spawn } from "node:child_process";
11
11
  import { tmpdir } from "node:os";
12
12
  import { hashKnowledgeBase } from "@tangle-network/agent-knowledge";
@@ -64,6 +64,240 @@ function deepFreezeCandidate(value, seen = /* @__PURE__ */ new Set()) {
64
64
  return Object.freeze(value);
65
65
  }
66
66
  //#endregion
67
+ //#region src/candidate-execution/artifacts.ts
68
+ function artifactCacheKey(artifact) {
69
+ return `${artifact.sha256}:${artifact.byteLength}`;
70
+ }
71
+ async function readVerifiedArtifact(artifact, port) {
72
+ const bytes = "content" in artifact ? Buffer.from(artifact.content, "base64") : await port.read(artifact);
73
+ verifyBytes(bytes, artifact.sha256, artifact.byteLength, "candidate artifact");
74
+ return Uint8Array.from(bytes);
75
+ }
76
+ function verifyBytes(bytes, digest, byteLength, label) {
77
+ if (bytes.byteLength !== byteLength) throw new Error(`${label} byte length ${bytes.byteLength} does not match ${byteLength}`);
78
+ const actual = sha256Bytes(bytes);
79
+ if (actual !== digest) throw new Error(`${label} digest ${actual} does not match ${digest}`);
80
+ }
81
+ async function verifyWorkspaceSnapshotArtifacts(snapshot, port) {
82
+ const [manifest, archive] = await Promise.all([readVerifiedArtifact(snapshot.manifest, port), readVerifiedArtifact(snapshot.archive, port)]);
83
+ const canonicalManifest = canonicalCandidateBytes(snapshot.material);
84
+ if (!Buffer.from(manifest).equals(Buffer.from(canonicalManifest))) throw new Error("workspace manifest artifact is not the exact canonical manifest material");
85
+ if (sha256Bytes(canonicalManifest) !== snapshot.digest) throw new Error("workspace snapshot digest does not match its canonical manifest material");
86
+ return {
87
+ manifest,
88
+ archive
89
+ };
90
+ }
91
+ /**
92
+ * Refuse a materialized workspace whose files, modes, or bytes are not the signed manifest.
93
+ *
94
+ * The scan streams, so the size of the largest file does not decide whether the check can run, and
95
+ * the refusal names the one mismatch a caller can produce on its own: a capture and a verify that
96
+ * disagree about `portableTree`.
97
+ */
98
+ async function verifyMaterializedWorkspace(root, expected, options = {}) {
99
+ assertWorkspaceManifest(await scanMaterializedWorkspaceManifest(root, options), expected);
100
+ }
101
+ /**
102
+ * The canonical manifest of one materialized workspace, read without holding any file.
103
+ *
104
+ * Every file is digested by streaming, so the size of the largest file does not decide whether the
105
+ * workspace can be described. `FileHandle.readFile` refuses anything above 2 GiB with
106
+ * `ERR_FS_FILE_TOO_LARGE`, which made a workspace holding one such artifact impossible to verify
107
+ * against a manifest it already matched. The digest is sha-256 over the same bytes either way, so
108
+ * a manifest a buffered read produced is reproduced exactly.
109
+ */
110
+ async function scanMaterializedWorkspaceManifest(root, options = {}) {
111
+ return workspaceManifestFromEntries(await walkWorkspace(root, options, false));
112
+ }
113
+ async function captureMaterializedWorkspace(root, options = {}) {
114
+ const entries = await walkWorkspace(root, options, true);
115
+ return {
116
+ manifest: workspaceManifestFromEntries(entries),
117
+ files: entries.map((entry) => ({
118
+ path: entry.path,
119
+ mode: entry.mode,
120
+ bytes: Uint8Array.from(capturedBytes(entry))
121
+ }))
122
+ };
123
+ }
124
+ /** Capture exact verified regular-file bytes for fresh isolated materialization. */
125
+ async function readMaterializedWorkspaceFiles(root, expected, options = {}) {
126
+ const observed = await captureMaterializedWorkspace(root, options);
127
+ assertWorkspaceManifest(observed.manifest, expected);
128
+ return observed.files.map((file) => Object.freeze({
129
+ path: file.path,
130
+ mode: file.mode,
131
+ bytes: Uint8Array.from(file.bytes)
132
+ }));
133
+ }
134
+ /**
135
+ * Build the canonical manifest for files a caller already holds — the shape a remote executor
136
+ * returns. Pass `portableTree` to record Git's two file modes instead of exact permission bits, and
137
+ * pass the same flag to every verify that reads the result.
138
+ */
139
+ function candidateWorkspaceManifest(files, options = {}) {
140
+ return workspaceManifestFromEntries(files.map((file) => ({
141
+ path: file.path,
142
+ mode: options.portableTree === true ? portableFileMode(file.mode) : file.mode,
143
+ sha256: sha256Bytes(file.bytes),
144
+ byteLength: file.bytes.byteLength
145
+ })));
146
+ }
147
+ function workspaceManifestFromEntries(entries) {
148
+ return {
149
+ kind: "agent-candidate-workspace-manifest",
150
+ files: entries.map((entry) => ({
151
+ path: entry.path,
152
+ mode: entry.mode,
153
+ sha256: entry.sha256,
154
+ byteLength: entry.byteLength
155
+ })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
156
+ };
157
+ }
158
+ /** Git records exactly two file modes, and a portable-tree scan records the same two. */
159
+ function portableFileMode(mode) {
160
+ return (mode & 73) !== 0 ? 493 : 420;
161
+ }
162
+ function capturedBytes(entry) {
163
+ if (!entry.bytes) throw new Error(`workspace file was digested without its bytes: ${entry.path}`);
164
+ return entry.bytes;
165
+ }
166
+ function assertWorkspaceManifest(observed, expected) {
167
+ if (Buffer.from(canonicalCandidateBytes(observed)).equals(canonicalCandidateBytes(expected))) return;
168
+ if (Buffer.from(canonicalCandidateBytes(withPortableModes(observed))).equals(canonicalCandidateBytes(withPortableModes(expected)))) throw new Error("materialized workspace files do not match the signed manifest: only the file modes differ, so the capture and this verify disagree about portableTree");
169
+ throw new Error("materialized workspace files, modes, or bytes do not match the signed manifest");
170
+ }
171
+ function withPortableModes(material) {
172
+ return {
173
+ ...material,
174
+ files: material.files.map((file) => ({
175
+ ...file,
176
+ mode: portableFileMode(file.mode)
177
+ }))
178
+ };
179
+ }
180
+ async function verifyMaterializedProfileWorkspace(root, expected, fileRoot = "workspace") {
181
+ const observedProfile = (await scanMaterializedWorkspaceManifest(root)).files.map(({ path, mode, sha256 }) => ({
182
+ relPath: path,
183
+ mode,
184
+ contentSha256: sha256
185
+ }));
186
+ const expectedFiles = expected.files.filter((file) => (file.root ?? "workspace") === fileRoot).map(({ root: _root, ...file }) => file);
187
+ if (!Buffer.from(canonicalCandidateBytes(observedProfile)).equals(canonicalCandidateBytes(expectedFiles))) throw new Error(`profile ${fileRoot} staging files, modes, or bytes do not match the signed profile plan`);
188
+ }
189
+ async function walkWorkspace(root, options, keepBytes) {
190
+ const ignoredProtectedRootEntries = new Set(options.ignoredProtectedRootEntries ?? []);
191
+ const limits = options.limits;
192
+ const requestedRoot = resolve(root);
193
+ const rootStats = await lstat(requestedRoot);
194
+ if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) throw new Error("workspace root must be a real directory");
195
+ const absoluteRoot = await realpath(requestedRoot);
196
+ const resolvedStats = await lstat(absoluteRoot);
197
+ if (!resolvedStats.isDirectory() || resolvedStats.isSymbolicLink()) throw new Error("workspace root must resolve to a real directory");
198
+ const scanned = [];
199
+ let totalBytes = 0;
200
+ async function visit(directory) {
201
+ const entries = await readdir(directory, { withFileTypes: true });
202
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
203
+ for (const entry of entries) {
204
+ if (directory === absoluteRoot && ignoredProtectedRootEntries.has(entry.name)) continue;
205
+ const absolute = resolve(directory, entry.name);
206
+ const relPath = relative(absoluteRoot, absolute).split(sep).join("/");
207
+ if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) throw new Error(`workspace entry escapes root: ${relPath}`);
208
+ const stats = await lstat(absolute);
209
+ if (stats.isSymbolicLink()) throw new Error(await symlinkRefusal(absolute, relPath, absoluteRoot));
210
+ if (stats.isDirectory()) {
211
+ await visit(absolute);
212
+ continue;
213
+ }
214
+ if (!stats.isFile()) throw new Error(`workspace contains a non-regular entry: ${relPath}`);
215
+ if (limits && scanned.length >= limits.maxFiles) throw new Error("workspace exceeds maxFiles");
216
+ const descriptor = await open(absolute, constants.O_RDONLY | (typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0));
217
+ try {
218
+ const openedStats = await descriptor.stat();
219
+ if (!openedStats.isFile()) throw new Error(`workspace contains a non-regular entry: ${relPath}`);
220
+ if (openedStats.nlink !== 1) throw new Error(`workspace contains a hard-linked file: ${relPath}`);
221
+ const mode = options.portableTree === true ? portableFileMode(openedStats.mode) : openedStats.mode & 511;
222
+ if (limits && openedStats.size > limits.maxFileBytes) throw new Error(`workspace file exceeds maxFileBytes: ${relPath}`);
223
+ const remainingBytes = limits ? limits.maxTotalFileBytes - totalBytes : void 0;
224
+ if (remainingBytes !== void 0 && openedStats.size > remainingBytes) throw new Error("workspace exceeds maxTotalFileBytes");
225
+ const read = await readWorkspaceFile(descriptor, limits ? Math.min(limits.maxFileBytes, remainingBytes ?? limits.maxFileBytes) : void 0, keepBytes, relPath);
226
+ totalBytes += read.byteLength;
227
+ scanned.push({
228
+ path: relPath,
229
+ mode,
230
+ sha256: read.sha256,
231
+ byteLength: read.byteLength,
232
+ ...read.bytes === void 0 ? {} : { bytes: read.bytes }
233
+ });
234
+ } finally {
235
+ await descriptor.close();
236
+ }
237
+ }
238
+ }
239
+ await visit(absoluteRoot);
240
+ return scanned;
241
+ }
242
+ /**
243
+ * Why one symbolic link is refused, in the terms that separate a portable link from a link that
244
+ * reaches outside the tree.
245
+ *
246
+ * Every link is refused, in both mode policies. `AgentCandidateWorkspaceManifestMaterial` has no
247
+ * representation for one — `mode`, `sha256` and `byteLength` describe a regular file — so a link
248
+ * recorded as a file would let two different trees share one digest. The reason says which kind of
249
+ * link it was, so a caller can tell a link it can rewrite from one it cannot.
250
+ */
251
+ async function symlinkRefusal(absolute, relPath, absoluteRoot) {
252
+ const target = await readlink(absolute);
253
+ if (isAbsolute(target)) return `workspace contains an absolute symlink: ${relPath} -> ${target}`;
254
+ if (!isWithin(absoluteRoot, resolve(dirname(absolute), target))) return `workspace contains a symlink that escapes its tree: ${relPath} -> ${target}`;
255
+ let physical;
256
+ try {
257
+ physical = await realpath(absolute);
258
+ } catch {
259
+ return `workspace contains an unresolved symlink: ${relPath} -> ${target}`;
260
+ }
261
+ if (!isWithin(absoluteRoot, physical)) return `workspace contains a symlink that resolves outside its tree: ${relPath} -> ${target}`;
262
+ return `workspace contains a symlink: ${relPath} -> ${target}`;
263
+ }
264
+ function isWithin(root, path) {
265
+ return path === root || path.startsWith(`${root}${sep}`);
266
+ }
267
+ /**
268
+ * Read one open regular file into its digest, keeping the bytes only when the caller needs them.
269
+ *
270
+ * The content reaches the hash in 1 MiB chunks and is never held whole for a digest-only walk, so
271
+ * the size of the largest file does not decide whether a workspace can be described.
272
+ * `FileHandle.readFile` refuses anything above 2 GiB with `ERR_FS_FILE_TOO_LARGE`. Hashing the
273
+ * chunks in order equals hashing the whole buffer, so a manifest a buffered read produced is
274
+ * reproduced byte for byte.
275
+ */
276
+ async function readWorkspaceFile(descriptor, maxBytes, keepBytes, path) {
277
+ const hash = createHash("sha256");
278
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
279
+ const chunks = [];
280
+ let total = 0;
281
+ while (true) {
282
+ const { bytesRead } = await descriptor.read(buffer, 0, buffer.byteLength, null);
283
+ if (bytesRead === 0) break;
284
+ total += bytesRead;
285
+ if (maxBytes !== void 0 && total > maxBytes) throw new Error(`workspace file exceeds its capture limit: ${path}`);
286
+ const chunk = buffer.subarray(0, bytesRead);
287
+ hash.update(chunk);
288
+ if (keepBytes) chunks.push(Buffer.from(chunk));
289
+ }
290
+ const sha256 = `sha256:${hash.digest("hex")}`;
291
+ return keepBytes ? {
292
+ sha256,
293
+ byteLength: total,
294
+ bytes: Buffer.concat(chunks, total)
295
+ } : {
296
+ sha256,
297
+ byteLength: total
298
+ };
299
+ }
300
+ //#endregion
67
301
  //#region src/candidate-execution/system-prompt.ts
68
302
  const SYSTEM_PROMPT_FILE = ".tangle/system-prompt.md";
69
303
  const APPEND_SYSTEM_PROMPT_FILE = ".tangle/append-system-prompt.md";
@@ -215,6 +449,8 @@ function candidateMaterializerHarness(harness) {
215
449
  }
216
450
  /** Runtime applies the materializer's launch flags to the candidate process. */
217
451
  const CANDIDATE_PROFILE_MATERIALIZER_BINDS = ["systemPrompt"];
452
+ const PI_CANDIDATE_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
453
+ const PI_CANDIDATE_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
218
454
  /** Derive the exact native profile plan used by preparation and later evidence checks. */
219
455
  function materializeAgentCandidateProfilePlan(options) {
220
456
  const root = options.workspace === "task" ? options.workspaces.taskRoot : options.workspaces.candidateRoot;
@@ -224,15 +460,22 @@ function materializeAgentCandidateProfilePlan(options) {
224
460
  resolvedResources: options.resolvedResources
225
461
  }), options.launch, posix.join(root, ".tangle/system-prompt.md"), options.profile.prompt?.systemPrompt, options.profile.prompt?.appendSystemPrompt);
226
462
  }
463
+ /** Resolve the private Pi roots needed by an agent-root profile plan. */
464
+ function candidateProfileAgentPaths(plan, runtimeHome) {
465
+ if (!plan.files.some((file) => file.root === "agent")) return void 0;
466
+ if (plan.harness !== "pi") throw new Error(`candidate profile agent-root files are unsupported for ${plan.harness}`);
467
+ if (runtimeHome === void 0) throw new Error("candidate profile agent-root files require executionRoots.profileRoot");
468
+ return resolvePiRuntimePaths(runtimeHome);
469
+ }
227
470
  /** Bind exact native profile text to the canonical plan captured during preparation. */
228
471
  function createAgentCandidateProfileActivation(plan, profilePlan) {
229
- const sourceByPath = new Map(plan.files.map((file) => [file.relPath, file]));
230
- if (sourceByPath.size !== plan.files.length) throw new Error("candidate profile activation contains duplicate native file paths");
472
+ const sourceByIdentity = new Map(plan.files.map((file) => [profileFileIdentity$1(file.root, file.relPath), file]));
473
+ if (sourceByIdentity.size !== plan.files.length) throw new Error("candidate profile activation contains duplicate rooted native file paths");
231
474
  const files = profilePlan.material.files.map((expected) => {
232
- const source = sourceByPath.get(expected.relPath);
233
- if (!source) throw new Error(`candidate profile activation is missing ${expected.relPath}`);
475
+ const source = sourceByIdentity.get(profileFileIdentity$1(expected.root, expected.relPath));
476
+ if (!source) throw new Error(`candidate profile activation is missing ${profileFileLabel(expected.root, expected.relPath)}`);
234
477
  const mode = source.mode ?? 420;
235
- if (!Number.isSafeInteger(mode) || mode < 0 || mode > 511) throw new Error(`candidate profile activation has invalid mode for ${expected.relPath}`);
478
+ if (!Number.isSafeInteger(mode) || mode < 0 || mode > 511) throw new Error(`candidate profile activation has invalid mode for ${profileFileLabel(expected.root, expected.relPath)}`);
236
479
  return {
237
480
  path: expected.relPath,
238
481
  mode,
@@ -246,6 +489,12 @@ function createAgentCandidateProfileActivation(plan, profilePlan) {
246
489
  files
247
490
  }).value, profilePlan.digest);
248
491
  }
492
+ function profileFileIdentity$1(root, relPath) {
493
+ return `${root ?? "workspace"}\0${relPath}`;
494
+ }
495
+ function profileFileLabel(root, relPath) {
496
+ return root === "agent" ? `agent:${relPath}` : relPath;
497
+ }
249
498
  /** Parse and check every native file hash plus both canonical document digests. */
250
499
  function parseAgentCandidateProfileActivation(input, expectedProfilePlanDigest) {
251
500
  const activation = agentCandidateProfileActivationSchema.parse(input);
@@ -885,225 +1134,35 @@ function candidateTerminalWindowMs(cleanupTimeoutMs) {
885
1134
  return windowMs;
886
1135
  }
887
1136
  //#endregion
888
- //#region src/candidate-execution/artifacts.ts
889
- function artifactCacheKey(artifact) {
890
- return `${artifact.sha256}:${artifact.byteLength}`;
891
- }
892
- async function readVerifiedArtifact(artifact, port) {
893
- const bytes = "content" in artifact ? Buffer.from(artifact.content, "base64") : await port.read(artifact);
894
- verifyBytes(bytes, artifact.sha256, artifact.byteLength, "candidate artifact");
895
- return Uint8Array.from(bytes);
896
- }
897
- function verifyBytes(bytes, digest, byteLength, label) {
898
- if (bytes.byteLength !== byteLength) throw new Error(`${label} byte length ${bytes.byteLength} does not match ${byteLength}`);
899
- const actual = sha256Bytes(bytes);
900
- if (actual !== digest) throw new Error(`${label} digest ${actual} does not match ${digest}`);
901
- }
902
- async function verifyWorkspaceSnapshotArtifacts(snapshot, port) {
903
- const [manifest, archive] = await Promise.all([readVerifiedArtifact(snapshot.manifest, port), readVerifiedArtifact(snapshot.archive, port)]);
904
- const canonicalManifest = canonicalCandidateBytes(snapshot.material);
905
- if (!Buffer.from(manifest).equals(Buffer.from(canonicalManifest))) throw new Error("workspace manifest artifact is not the exact canonical manifest material");
906
- if (sha256Bytes(canonicalManifest) !== snapshot.digest) throw new Error("workspace snapshot digest does not match its canonical manifest material");
907
- return {
908
- manifest,
909
- archive
910
- };
911
- }
912
- async function verifyMaterializedWorkspace(root, expected, options = {}) {
913
- assertWorkspaceManifest(await scanMaterializedWorkspaceManifest(root, options), expected);
914
- }
915
- /**
916
- * The canonical manifest of one materialized workspace, read without holding any file.
917
- *
918
- * Every file is digested by streaming, so the size of the largest file does not decide whether the
919
- * workspace can be described. `FileHandle.readFile` refuses anything above 2 GiB with
920
- * `ERR_FS_FILE_TOO_LARGE`, which made a workspace holding one such artifact impossible to verify
921
- * against a manifest it already matched. The digest is sha-256 over the same bytes either way, so
922
- * a manifest a buffered read produced is reproduced exactly.
923
- */
924
- async function scanMaterializedWorkspaceManifest(root, options = {}) {
925
- return workspaceManifestFromEntries(await walkWorkspace(root, options, false));
926
- }
927
- async function captureMaterializedWorkspace(root, options = {}) {
928
- const entries = await walkWorkspace(root, options, true);
929
- return {
930
- manifest: workspaceManifestFromEntries(entries),
931
- files: entries.map((entry) => ({
932
- path: entry.path,
933
- mode: entry.mode,
934
- bytes: Uint8Array.from(capturedBytes(entry))
935
- }))
936
- };
937
- }
938
- /** Capture exact verified regular-file bytes for fresh isolated materialization. */
939
- async function readMaterializedWorkspaceFiles(root, expected, options = {}) {
940
- const observed = await captureMaterializedWorkspace(root, options);
941
- assertWorkspaceManifest(observed.manifest, expected);
942
- return observed.files.map((file) => Object.freeze({
943
- path: file.path,
944
- mode: file.mode,
945
- bytes: Uint8Array.from(file.bytes)
946
- }));
947
- }
948
- function candidateWorkspaceManifest(files, options = {}) {
949
- return workspaceManifestFromEntries(files.map((file) => ({
950
- path: file.path,
951
- mode: options.portableTree === true ? portableFileMode(file.mode) : file.mode,
952
- sha256: sha256Bytes(file.bytes),
953
- byteLength: file.bytes.byteLength
954
- })));
955
- }
956
- function workspaceManifestFromEntries(entries) {
957
- return {
958
- kind: "agent-candidate-workspace-manifest",
959
- files: entries.map((entry) => ({
960
- path: entry.path,
961
- mode: entry.mode,
962
- sha256: entry.sha256,
963
- byteLength: entry.byteLength
964
- })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
965
- };
966
- }
967
- /** Git records exactly two file modes, and a portable-tree scan records the same two. */
968
- function portableFileMode(mode) {
969
- return (mode & 73) !== 0 ? 493 : 420;
970
- }
971
- function capturedBytes(entry) {
972
- if (!entry.bytes) throw new Error(`workspace file was digested without its bytes: ${entry.path}`);
973
- return entry.bytes;
974
- }
975
- function assertWorkspaceManifest(observed, expected) {
976
- if (Buffer.from(canonicalCandidateBytes(observed)).equals(canonicalCandidateBytes(expected))) return;
977
- if (Buffer.from(canonicalCandidateBytes(withPortableModes(observed))).equals(canonicalCandidateBytes(withPortableModes(expected)))) throw new Error("materialized workspace files do not match the signed manifest: only the file modes differ, so the capture and this verify disagree about portableTree");
978
- throw new Error("materialized workspace files, modes, or bytes do not match the signed manifest");
979
- }
980
- function withPortableModes(material) {
981
- return {
982
- ...material,
983
- files: material.files.map((file) => ({
984
- ...file,
985
- mode: portableFileMode(file.mode)
986
- }))
987
- };
988
- }
989
- async function verifyMaterializedProfileWorkspace(root, expected) {
990
- const observedProfile = (await scanMaterializedWorkspaceManifest(root)).files.map(({ path, mode, sha256 }) => ({
991
- relPath: path,
992
- mode,
993
- contentSha256: sha256
994
- }));
995
- if (!Buffer.from(canonicalCandidateBytes(observedProfile)).equals(canonicalCandidateBytes(expected.files))) throw new Error("profile staging files, modes, or bytes do not match the signed profile plan");
996
- }
997
- async function walkWorkspace(root, options, keepBytes) {
998
- const ignoredProtectedRootEntries = new Set(options.ignoredProtectedRootEntries ?? []);
999
- const limits = options.limits;
1000
- const requestedRoot = resolve(root);
1001
- const rootStats = await lstat(requestedRoot);
1002
- if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) throw new Error("workspace root must be a real directory");
1003
- const absoluteRoot = await realpath(requestedRoot);
1004
- const resolvedStats = await lstat(absoluteRoot);
1005
- if (!resolvedStats.isDirectory() || resolvedStats.isSymbolicLink()) throw new Error("workspace root must resolve to a real directory");
1006
- const scanned = [];
1007
- let totalBytes = 0;
1008
- async function visit(directory) {
1009
- const entries = await readdir(directory, { withFileTypes: true });
1010
- entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1011
- for (const entry of entries) {
1012
- if (directory === absoluteRoot && ignoredProtectedRootEntries.has(entry.name)) continue;
1013
- const absolute = resolve(directory, entry.name);
1014
- const relPath = relative(absoluteRoot, absolute).split(sep).join("/");
1015
- if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) throw new Error(`workspace entry escapes root: ${relPath}`);
1016
- const stats = await lstat(absolute);
1017
- if (stats.isSymbolicLink()) throw new Error(await symlinkRefusal(absolute, relPath, absoluteRoot));
1018
- if (stats.isDirectory()) {
1019
- await visit(absolute);
1020
- continue;
1021
- }
1022
- if (!stats.isFile()) throw new Error(`workspace contains a non-regular entry: ${relPath}`);
1023
- if (limits && scanned.length >= limits.maxFiles) throw new Error("workspace exceeds maxFiles");
1024
- const descriptor = await open(absolute, constants.O_RDONLY | (typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0));
1025
- try {
1026
- const openedStats = await descriptor.stat();
1027
- if (!openedStats.isFile()) throw new Error(`workspace contains a non-regular entry: ${relPath}`);
1028
- if (openedStats.nlink !== 1) throw new Error(`workspace contains a hard-linked file: ${relPath}`);
1029
- const mode = options.portableTree === true ? portableFileMode(openedStats.mode) : openedStats.mode & 511;
1030
- if (limits && openedStats.size > limits.maxFileBytes) throw new Error(`workspace file exceeds maxFileBytes: ${relPath}`);
1031
- const remainingBytes = limits ? limits.maxTotalFileBytes - totalBytes : void 0;
1032
- if (remainingBytes !== void 0 && openedStats.size > remainingBytes) throw new Error("workspace exceeds maxTotalFileBytes");
1033
- const read = await readWorkspaceFile(descriptor, limits ? Math.min(limits.maxFileBytes, remainingBytes ?? limits.maxFileBytes) : void 0, keepBytes, relPath);
1034
- totalBytes += read.byteLength;
1035
- scanned.push({
1036
- path: relPath,
1037
- mode,
1038
- sha256: read.sha256,
1039
- byteLength: read.byteLength,
1040
- ...read.bytes === void 0 ? {} : { bytes: read.bytes }
1041
- });
1042
- } finally {
1043
- await descriptor.close();
1044
- }
1137
+ //#region src/candidate-execution/execution-roots.ts
1138
+ /** Validate the canonical identity and isolation boundary for execution roots. */
1139
+ function assertAgentCandidateExecutionRoots(roots) {
1140
+ const entries = [
1141
+ ["task", roots.taskRoot],
1142
+ ["candidate", roots.candidateRoot],
1143
+ ["profile", roots.profileRoot]
1144
+ ];
1145
+ for (const [name, root] of entries) {
1146
+ if (root === void 0) continue;
1147
+ if (!posix.isAbsolute(root) || posix.normalize(root) !== root) throw new Error(`execution ${name} root must be a canonical absolute path`);
1148
+ }
1149
+ for (let left = 0; left < entries.length; left++) {
1150
+ const entry = entries[left];
1151
+ if (!entry) continue;
1152
+ const [leftName, leftRoot] = entry;
1153
+ if (leftRoot === void 0) continue;
1154
+ for (let right = left + 1; right < entries.length; right++) {
1155
+ const rightEntry = entries[right];
1156
+ if (!rightEntry) continue;
1157
+ const [rightName, rightRoot] = rightEntry;
1158
+ if (rightRoot !== void 0 && executionPathsOverlap(leftRoot, rightRoot)) throw new Error(`execution ${leftName} and ${rightName} roots must be distinct and non-overlapping`);
1045
1159
  }
1046
1160
  }
1047
- await visit(absoluteRoot);
1048
- return scanned;
1049
1161
  }
1050
- /**
1051
- * Why one symbolic link is refused, in the terms that separate a portable link from a link that
1052
- * reaches outside the tree.
1053
- *
1054
- * Every link is refused, in both mode policies. `AgentCandidateWorkspaceManifestMaterial` has no
1055
- * representation for one — `mode`, `sha256` and `byteLength` describe a regular file — so a link
1056
- * recorded as a file would let two different trees share one digest. The reason says which kind of
1057
- * link it was, so a caller can tell a link it can rewrite from one it cannot.
1058
- */
1059
- async function symlinkRefusal(absolute, relPath, absoluteRoot) {
1060
- const target = await readlink(absolute);
1061
- if (isAbsolute(target)) return `workspace contains an absolute symlink: ${relPath} -> ${target}`;
1062
- if (!isWithin(absoluteRoot, resolve(dirname(absolute), target))) return `workspace contains a symlink that escapes its tree: ${relPath} -> ${target}`;
1063
- let physical;
1064
- try {
1065
- physical = await realpath(absolute);
1066
- } catch {
1067
- return `workspace contains an unresolved symlink: ${relPath} -> ${target}`;
1068
- }
1069
- if (!isWithin(absoluteRoot, physical)) return `workspace contains a symlink that resolves outside its tree: ${relPath} -> ${target}`;
1070
- return `workspace contains a symlink: ${relPath} -> ${target}`;
1071
- }
1072
- function isWithin(root, path) {
1073
- return path === root || path.startsWith(`${root}${sep}`);
1074
- }
1075
- /**
1076
- * Read one open regular file into its digest, keeping the bytes only when the caller needs them.
1077
- *
1078
- * The content reaches the hash in 1 MiB chunks and is never held whole for a digest-only walk, so
1079
- * the size of the largest file does not decide whether a workspace can be described.
1080
- * `FileHandle.readFile` refuses anything above 2 GiB with `ERR_FS_FILE_TOO_LARGE`. Hashing the
1081
- * chunks in order equals hashing the whole buffer, so a manifest a buffered read produced is
1082
- * reproduced byte for byte.
1083
- */
1084
- async function readWorkspaceFile(descriptor, maxBytes, keepBytes, path) {
1085
- const hash = createHash("sha256");
1086
- const buffer = Buffer.allocUnsafe(1024 * 1024);
1087
- const chunks = [];
1088
- let total = 0;
1089
- while (true) {
1090
- const { bytesRead } = await descriptor.read(buffer, 0, buffer.byteLength, null);
1091
- if (bytesRead === 0) break;
1092
- total += bytesRead;
1093
- if (maxBytes !== void 0 && total > maxBytes) throw new Error(`workspace file exceeds its capture limit: ${path}`);
1094
- const chunk = buffer.subarray(0, bytesRead);
1095
- hash.update(chunk);
1096
- if (keepBytes) chunks.push(Buffer.from(chunk));
1097
- }
1098
- const sha256 = `sha256:${hash.digest("hex")}`;
1099
- return keepBytes ? {
1100
- sha256,
1101
- byteLength: total,
1102
- bytes: Buffer.concat(chunks, total)
1103
- } : {
1104
- sha256,
1105
- byteLength: total
1106
- };
1162
+ function executionPathsOverlap(left, right) {
1163
+ const a = posix.normalize(left);
1164
+ const b = posix.normalize(right);
1165
+ return a === b || b.startsWith(a === "/" ? "/" : `${a}/`) || a.startsWith(b === "/" ? "/" : `${b}/`);
1107
1166
  }
1108
1167
  //#endregion
1109
1168
  //#region src/candidate-execution/git-materialize.ts
@@ -1618,6 +1677,7 @@ function detachPreparedCandidateState(input) {
1618
1677
  });
1619
1678
  }
1620
1679
  function assertPrivateCandidateIntegrity(state) {
1680
+ assertAgentCandidateExecutionRoots(state.roots.execution);
1621
1681
  if (canonicalCandidateDigest$1(omitTopLevelDigest(state.bundle)) !== state.bundle.digest) throw new Error("prepared candidate bundle no longer matches its digest");
1622
1682
  assertPlanEvidence(state.profilePlan.value, state.profilePlan.bytes, "profile plan");
1623
1683
  parseAgentCandidateProfileActivation(state.profileActivation, state.profilePlan.value.digest);
@@ -1694,6 +1754,7 @@ function profileFileView(source) {
1694
1754
  return Object.freeze({
1695
1755
  path: source.path,
1696
1756
  mode: source.mode,
1757
+ ...source.root === "agent" ? { root: "agent" } : {},
1697
1758
  get bytes() {
1698
1759
  return Uint8Array.from(bytes);
1699
1760
  }
@@ -1721,7 +1782,7 @@ function assertExecutorInputs(state) {
1721
1782
  for (let index = 0; index < expectedFiles.length; index++) {
1722
1783
  const expected = expectedFiles[index];
1723
1784
  const actual = state.executorInputs.profileFiles[index];
1724
- if (!expected || !actual || actual.path !== expected.relPath || actual.mode !== expected.mode || sha256Bytes(actual.bytes) !== expected.contentSha256) throw new Error("prepared profile executor files do not match the signed profile plan");
1785
+ if (!expected || !actual || actual.path !== expected.relPath || (actual.root ?? "workspace") !== (expected.root ?? "workspace") || actual.mode !== expected.mode || sha256Bytes(actual.bytes) !== expected.contentSha256) throw new Error("prepared profile executor files do not match the signed profile plan");
1725
1786
  }
1726
1787
  }
1727
1788
  function assertPreparedKnowledge(state) {
@@ -4050,11 +4111,25 @@ async function prepareAgentCandidateExecution(candidate, task, ports, options =
4050
4111
  harness: bundle.execution.harness,
4051
4112
  launch: bundle.execution.launch,
4052
4113
  workspace: bundle.execution.cwd.workspace,
4053
- workspaces: task.executionRoots,
4114
+ workspaces: {
4115
+ taskRoot: task.executionRoots.taskRoot,
4116
+ ...task.executionRoots.candidateRoot ? { candidateRoot: task.executionRoots.candidateRoot } : {}
4117
+ },
4054
4118
  resolvedResources: verifiedResourceTextByDigest(candidate)
4055
4119
  });
4056
- const profileApplication = applyAgentCandidateWorkspacePlan(profileWorkspacePlan, task.stagingRoots.profileRoot, bundle.execution.cwd.workspace);
4057
- await verifyMaterializedProfileWorkspace(task.stagingRoots.profileRoot, profileApplication.profileActivation.profilePlan.material);
4120
+ const profileAgentPaths = candidateProfileAgentPaths(profileWorkspacePlan, task.executionRoots.profileRoot);
4121
+ const profileAgentStagingRoot = profileWorkspacePlan.files.some((file) => file.root === "agent") ? await mkdtemp(join(tmpdir(), "tangle-candidate-profile-agent-")) : void 0;
4122
+ let profileApplication;
4123
+ try {
4124
+ profileApplication = applyAgentCandidateWorkspacePlan(profileWorkspacePlan, task.stagingRoots.profileRoot, bundle.execution.cwd.workspace, profileAgentStagingRoot ? { agentDir: profileAgentStagingRoot } : void 0);
4125
+ await verifyMaterializedProfileWorkspace(task.stagingRoots.profileRoot, profileApplication.profileActivation.profilePlan.material);
4126
+ if (profileAgentStagingRoot) await verifyMaterializedProfileWorkspace(profileAgentStagingRoot, profileApplication.profileActivation.profilePlan.material, "agent");
4127
+ } finally {
4128
+ if (profileAgentStagingRoot) await rm(profileAgentStagingRoot, {
4129
+ recursive: true,
4130
+ force: true
4131
+ });
4132
+ }
4058
4133
  const profilePlanBytes = await readVerifiedArtifact(profileApplication.profileActivation.profilePlan.artifact, ports.artifacts);
4059
4134
  if (!Buffer.from(profilePlanBytes).equals(Buffer.from(canonicalCandidateBytes(profileApplication.profileActivation.profilePlan.material)))) throw new Error("profile materializer did not capture exact canonical plan bytes");
4060
4135
  const profileActivation = profileApplication.profileActivation;
@@ -4079,7 +4154,16 @@ async function prepareAgentCandidateExecution(candidate, task, ports, options =
4079
4154
  const knowledge = await prepareAgentCandidateKnowledge(candidate, ports, task.runCell.arm);
4080
4155
  const knowledgePaths = knowledge ? candidateKnowledgeExecutionPaths(task.executionRoots.taskRoot, knowledge.retrievalConfig !== void 0) : void 0;
4081
4156
  const baseLaunch = buildLaunch(candidate, task, profileApplication.flags);
4082
- const publicEnv = mergePublicEnvironment(bundle.execution.env ?? {}, profileApplication.env, bundle.execution.instructionDelivery.kind === "utf8-file" ? { [bundle.execution.instructionDelivery.env]: {
4157
+ const publicEnv = mergePublicEnvironment(bundle.execution.env ?? {}, profileApplication.env, profileAgentPaths ? {
4158
+ [PI_CANDIDATE_AGENT_DIR_ENV]: {
4159
+ kind: "public",
4160
+ value: profileAgentPaths.agentDir
4161
+ },
4162
+ [PI_CANDIDATE_SESSION_DIR_ENV]: {
4163
+ kind: "public",
4164
+ value: profileAgentPaths.sessionDir
4165
+ }
4166
+ } : {}, bundle.execution.instructionDelivery.kind === "utf8-file" ? { [bundle.execution.instructionDelivery.env]: {
4083
4167
  kind: "public",
4084
4168
  value: bundle.execution.instructionDelivery.path
4085
4169
  } } : {}, knowledgePaths ? {
@@ -4311,15 +4395,14 @@ function assertTaskInput(task, delivery) {
4311
4395
  if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(benchmarkTask.repository.baseTree)) throw new Error("task repository base tree is not a full Git object id");
4312
4396
  if (benchmarkTask.repository.baseCommit.length !== benchmarkTask.repository.baseTree.length) throw new Error("task repository Git object formats disagree");
4313
4397
  }
4398
+ assertAgentCandidateExecutionRoots(task.executionRoots);
4314
4399
  for (const [name, root] of [
4315
- ["execution task root", task.executionRoots.taskRoot],
4316
- ["execution candidate root", task.executionRoots.candidateRoot],
4317
4400
  ["staging task root", task.stagingRoots.taskRoot],
4318
4401
  ["staging candidate root", task.stagingRoots.candidateRoot],
4319
4402
  ["staging profile root", task.stagingRoots.profileRoot]
4320
4403
  ]) {
4321
4404
  if (root === void 0) continue;
4322
- if (!(name.startsWith("execution") ? posix.isAbsolute(root) && posix.normalize(root) === root : isAbsolute(root) && resolve(root) === root)) throw new Error(`${name} must be a canonical absolute path`);
4405
+ if (!(isAbsolute(root) && resolve(root) === root)) throw new Error(`${name} must be a canonical absolute path`);
4323
4406
  }
4324
4407
  if (!Number.isInteger(runCell.attempt) || !Number.isInteger(benchmarkTask.attempt.maxAttempts) || runCell.attempt < 1 || runCell.attempt > benchmarkTask.attempt.maxAttempts || benchmarkTask.attempt.retryPolicy === "none" && benchmarkTask.attempt.maxAttempts !== 1) throw new Error("task attempt policy is invalid");
4325
4408
  const limits = benchmarkTask.limits;
@@ -4339,14 +4422,13 @@ function assertTaskInput(task, delivery) {
4339
4422
  });
4340
4423
  }
4341
4424
  if (delivery.kind === "utf8-file") {
4342
- if (executionPathsOverlap(task.executionRoots.taskRoot, delivery.path) || task.executionRoots.candidateRoot !== void 0 && executionPathsOverlap(task.executionRoots.candidateRoot, delivery.path)) throw new Error("task instruction file must remain outside execution workspaces");
4425
+ if ([
4426
+ task.executionRoots.taskRoot,
4427
+ task.executionRoots.candidateRoot,
4428
+ task.executionRoots.profileRoot
4429
+ ].some((root) => root !== void 0 && executionPathsOverlap(root, delivery.path))) throw new Error("task instruction file must remain outside execution workspaces");
4343
4430
  }
4344
4431
  }
4345
- function executionPathsOverlap(left, right) {
4346
- const a = posix.normalize(left);
4347
- const b = posix.normalize(right);
4348
- return a === b || b.startsWith(a === "/" ? "/" : `${a}/`) || a.startsWith(b === "/" ? "/" : `${b}/`);
4349
- }
4350
4432
  function assertDisjointHostStagingRoots(task) {
4351
4433
  const roots = [
4352
4434
  task.stagingRoots.taskRoot,
@@ -4519,12 +4601,16 @@ function candidateEntrypointReceipt(candidate) {
4519
4601
  };
4520
4602
  }
4521
4603
  function absoluteExecutionCwd(cwd, roots) {
4522
- const root = cwd.workspace === "task" ? roots.taskRoot : roots.candidateRoot;
4523
- if (!root) throw new Error("candidate cwd is missing its execution workspace root");
4604
+ const root = workspaceExecutionRoot(cwd.workspace, roots);
4524
4605
  const absolute = cwd.path === "." ? root : posix.join(root, cwd.path);
4525
4606
  if (absolute !== root && !absolute.startsWith(`${root}/`)) throw new Error("candidate cwd escapes its execution workspace");
4526
4607
  return absolute;
4527
4608
  }
4609
+ function workspaceExecutionRoot(workspace, roots) {
4610
+ const root = workspace === "task" ? roots.taskRoot : roots.candidateRoot;
4611
+ if (!root) throw new Error(`candidate ${workspace} workspace root is missing`);
4612
+ return root;
4613
+ }
4528
4614
  function validateProtectedModelReservation(reservation, expectedLimits, preparationId, expiresAtMs) {
4529
4615
  if (!/^sha256:[a-f0-9]{64}$/.test(reservation.digest)) throw new Error("protected model reservation has an invalid identity digest");
4530
4616
  if (reservation.preparationId !== preparationId || reservation.expiresAtMs !== expiresAtMs) throw new Error("protected model reservation is not scoped to this preparation");
@@ -4549,24 +4635,28 @@ function modelLimits(limits) {
4549
4635
  };
4550
4636
  }
4551
4637
  function exactProfileExecutorFiles(sourceFiles, expectedFiles) {
4552
- const byPath = new Map(sourceFiles.map((file) => [file.relPath, file]));
4553
- if (byPath.size !== sourceFiles.length || sourceFiles.length !== expectedFiles.length) throw new Error("profile source files do not match the signed profile plan");
4638
+ const byIdentity = new Map(sourceFiles.map((file) => [profileFileIdentity(file.root, file.relPath), file]));
4639
+ if (byIdentity.size !== sourceFiles.length || sourceFiles.length !== expectedFiles.length) throw new Error("profile source files do not match the signed profile plan");
4554
4640
  return expectedFiles.map((expected) => {
4555
- const source = byPath.get(expected.relPath);
4641
+ const source = byIdentity.get(profileFileIdentity(expected.root, expected.relPath));
4556
4642
  const mode = source?.mode ?? 420;
4557
4643
  const bytes = Buffer.from(source?.content ?? "", "utf8");
4558
4644
  if (!source || !Number.isInteger(mode) || mode < 0 || mode > 511 || mode !== expected.mode || sha256Bytes(bytes) !== expected.contentSha256) throw new Error("profile source files do not match the signed profile plan");
4559
4645
  return {
4560
4646
  path: expected.relPath,
4561
4647
  mode,
4648
+ ...expected.root === "agent" ? { root: "agent" } : {},
4562
4649
  bytes: Uint8Array.from(bytes)
4563
4650
  };
4564
4651
  });
4565
4652
  }
4653
+ function profileFileIdentity(root, relPath) {
4654
+ return `${root ?? "workspace"}\0${relPath}`;
4655
+ }
4566
4656
  function errorMessage(error) {
4567
4657
  return error instanceof Error ? error.message : String(error);
4568
4658
  }
4569
4659
  //#endregion
4570
- export { parseAgentCandidateProfileActivation as $, verifyMaterializedWorkspace as A, rejectedStage as B, assertNoGitIndirection as C, captureMaterializedWorkspace as D, candidateWorkspaceManifest as E, assertTerminalAllowedInPhase as F, isWellFormedUnicode as G, sealTerminalDigest as H, assertTerminalMatchesClaim as I, assertCandidateProfileBinding as J, agentCandidateProfileAsAgentProfile as K, assertTerminalMatchesStaged as L, candidateCleanupTimeout as M, withinCandidateCleanupDeadline as N, readVerifiedArtifact as O, assertRecoveryMatchesStaged as P, omitUndefinedObjectFields as Q, recoveredTerminalRecord as R, CANDIDATE_TRACE_TAGS as S, runCandidateGit as T, terminalRecord as U, requireStagedTerminal as V, assertExactObjectKeys as W, freezeGenericAgentCandidateProfile as X, createAgentCandidateProfileActivation as Y, materializeAgentCandidateProfilePlan as Z, candidateExecutionClaim as _, persistCandidateOutputArtifact as a, canonicalCandidateDocument as at, consumePreparedCandidateExecution as b, CANDIDATE_KNOWLEDGE_ROOT_ENV as c, immutableCandidateValue as ct, verifiedResourceTextByDigest as d, verifyCanonicalCandidateDocument as dt, parseExactAgentProfile as et, verifyAgentCandidateBundle as f, candidateClaimFileInternals as g, InMemoryAgentCandidateExecutionClaimStore as h, persistCandidateModelSettlementEvidence as i, canonicalCandidateDigest$1 as it, candidateCleanupDeadline as j, verifyBytes as k, candidateKnowledgeExecutionPaths as l, omitTopLevelDigest as lt, usdToNanos as m, executePreparedAgentCandidate as n, parseExactCandidateProfile as nt, sealAgentCandidateExecutorStopAcknowledgement as o, deepFreezeCandidate as ot, sealAgentCandidateModelSettlement as p, applyExactAgentProfileDiff as q, RecoveryAgentCandidateTraceStore as r, canonicalCandidateBytes as rt, CANDIDATE_KNOWLEDGE_RETRIEVAL_CONFIG_ENV as s, embeddedCandidateArtifact as st, prepareAgentCandidateExecution as t, parseExactAgentProfileDiff as tt, AGENT_CANDIDATE_EXECUTION_SUPPORT as u, sha256Bytes as ut, assertPreparedCandidateIntegrity as v, readCandidateGitTreeFiles as w, CANDIDATE_TRACE_ENV as x, beginPreparedCandidateDisposal as y, rejectedFinish as z };
4660
+ export { parseExactAgentProfile as $, assertRecoveryMatchesStaged as A, assertExactObjectKeys as B, assertNoGitIndirection as C, candidateCleanupDeadline as D, assertAgentCandidateExecutionRoots as E, rejectedFinish as F, applyExactAgentProfileDiff as G, PI_CANDIDATE_AGENT_DIR_ENV as H, rejectedStage as I, createAgentCandidateProfileActivation as J, assertCandidateProfileBinding as K, requireStagedTerminal as L, assertTerminalMatchesClaim as M, assertTerminalMatchesStaged as N, candidateCleanupTimeout as O, recoveredTerminalRecord as P, parseAgentCandidateProfileActivation as Q, sealTerminalDigest as R, CANDIDATE_TRACE_TAGS as S, runCandidateGit as T, PI_CANDIDATE_SESSION_DIR_ENV as U, isWellFormedUnicode as V, agentCandidateProfileAsAgentProfile as W, materializeAgentCandidateProfilePlan as X, freezeGenericAgentCandidateProfile as Y, omitUndefinedObjectFields as Z, candidateExecutionClaim as _, persistCandidateOutputArtifact as a, scanMaterializedWorkspaceManifest as at, consumePreparedCandidateExecution as b, CANDIDATE_KNOWLEDGE_ROOT_ENV as c, canonicalCandidateBytes as ct, verifiedResourceTextByDigest as d, deepFreezeCandidate as dt, parseExactAgentProfileDiff as et, verifyAgentCandidateBundle as f, embeddedCandidateArtifact as ft, candidateClaimFileInternals as g, verifyCanonicalCandidateDocument as gt, InMemoryAgentCandidateExecutionClaimStore as h, sha256Bytes as ht, persistCandidateModelSettlementEvidence as i, readVerifiedArtifact as it, assertTerminalAllowedInPhase as j, withinCandidateCleanupDeadline as k, candidateKnowledgeExecutionPaths as l, canonicalCandidateDigest$1 as lt, usdToNanos as m, omitTopLevelDigest as mt, executePreparedAgentCandidate as n, candidateWorkspaceManifest as nt, sealAgentCandidateExecutorStopAcknowledgement as o, verifyBytes as ot, sealAgentCandidateModelSettlement as p, immutableCandidateValue as pt, candidateProfileAgentPaths as q, RecoveryAgentCandidateTraceStore as r, captureMaterializedWorkspace as rt, CANDIDATE_KNOWLEDGE_RETRIEVAL_CONFIG_ENV as s, verifyMaterializedWorkspace as st, prepareAgentCandidateExecution as t, parseExactCandidateProfile as tt, AGENT_CANDIDATE_EXECUTION_SUPPORT as u, canonicalCandidateDocument as ut, assertPreparedCandidateIntegrity as v, readCandidateGitTreeFiles as w, CANDIDATE_TRACE_ENV as x, beginPreparedCandidateDisposal as y, terminalRecord as z };
4571
4661
 
4572
- //# sourceMappingURL=prepare-BpIGs5Tc.js.map
4662
+ //# sourceMappingURL=prepare-B5fJ7mLS.js.map