@odla-ai/harness 0.10.0 → 0.10.2

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 (39) hide show
  1. package/dist/{chunk-U324RQ4N.js → chunk-CPV7ZVHH.js} +1 -1
  2. package/dist/{chunk-U324RQ4N.js.map → chunk-CPV7ZVHH.js.map} +1 -1
  3. package/dist/{chunk-OZBJNTML.js → chunk-ESLFS3FY.js} +4 -4
  4. package/dist/{chunk-5LRYJKUI.js → chunk-FUDTSAOZ.js} +3 -3
  5. package/dist/{chunk-VDY5V7ZG.js → chunk-JV45JAAW.js} +3 -3
  6. package/dist/chunk-JV45JAAW.js.map +1 -0
  7. package/dist/{chunk-FAN2R3GW.js → chunk-KXFI3WM2.js} +5 -1
  8. package/dist/chunk-KXFI3WM2.js.map +1 -0
  9. package/dist/{chunk-5MDYIJXC.js → chunk-VIZALA6O.js} +643 -67
  10. package/dist/chunk-VIZALA6O.js.map +1 -0
  11. package/dist/cli.cjs +1 -1
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.js +4 -4
  14. package/dist/code-runtime-cli.cjs +706 -125
  15. package/dist/code-runtime-cli.cjs.map +1 -1
  16. package/dist/code-runtime-cli.js +5 -5
  17. package/dist/index.cjs +5 -1
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +2 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +3 -3
  22. package/dist/node.cjs +713 -130
  23. package/dist/node.cjs.map +1 -1
  24. package/dist/node.d.cts +41 -6
  25. package/dist/node.d.ts +41 -6
  26. package/dist/node.js +8 -6
  27. package/dist/node.js.map +1 -1
  28. package/dist/testing.cjs.map +1 -1
  29. package/dist/testing.d.cts +1 -1
  30. package/dist/testing.d.ts +1 -1
  31. package/dist/testing.js +1 -1
  32. package/dist/{types-_8y8vBDI.d.ts → types-ocy70g_p.d.cts} +1 -1
  33. package/dist/{types-_8y8vBDI.d.cts → types-ocy70g_p.d.ts} +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-5MDYIJXC.js.map +0 -1
  36. package/dist/chunk-FAN2R3GW.js.map +0 -1
  37. package/dist/chunk-VDY5V7ZG.js.map +0 -1
  38. /package/dist/{chunk-OZBJNTML.js.map → chunk-ESLFS3FY.js.map} +0 -0
  39. /package/dist/{chunk-5LRYJKUI.js.map → chunk-FUDTSAOZ.js.map} +0 -0
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  observedBroker
3
- } from "./chunk-FAN2R3GW.js";
3
+ } from "./chunk-KXFI3WM2.js";
4
4
  import {
5
5
  SKIP_WORKSPACE_DIRS,
6
6
  assertPinnedImage,
7
7
  stageWorkspace,
8
8
  stageWorkspacePair,
9
9
  verifyContainerEngineBoundary
10
- } from "./chunk-5LRYJKUI.js";
10
+ } from "./chunk-FUDTSAOZ.js";
11
11
  import {
12
12
  HARNESS_PROTOCOL_VERSION
13
- } from "./chunk-U324RQ4N.js";
13
+ } from "./chunk-CPV7ZVHH.js";
14
14
 
15
15
  // src/workspace-digest.ts
16
16
  import { createHash } from "crypto";
@@ -87,6 +87,34 @@ function createCodeRuntimeControlClient(options) {
87
87
  }
88
88
  return value;
89
89
  };
90
+ const callRaw = async (path, body) => {
91
+ const timeout = AbortSignal.timeout(modelRequestTimeoutMs);
92
+ const signals = [options.signal, timeout].filter((item) => Boolean(item));
93
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
94
+ let response2;
95
+ try {
96
+ response2 = await request(`${endpoint}${path}`, {
97
+ method: "POST",
98
+ headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
99
+ body: JSON.stringify(body),
100
+ redirect: "error",
101
+ signal
102
+ });
103
+ } catch (cause) {
104
+ if (options.signal?.aborted) throw cause;
105
+ throw new CodeRuntimeControlError("Code source archive is unavailable", 503, "transport_unavailable");
106
+ }
107
+ if (!response2.ok) {
108
+ const value = await response2.json().catch(() => null);
109
+ const problem = record(record(value)?.error);
110
+ throw new CodeRuntimeControlError(
111
+ typeof problem?.message === "string" ? problem.message : `Code runtime request failed (${response2.status})`,
112
+ response2.status,
113
+ typeof problem?.code === "string" ? problem.code : void 0
114
+ );
115
+ }
116
+ return response2;
117
+ };
90
118
  return {
91
119
  heartbeat: async (version, capabilities) => {
92
120
  validateHeartbeat(version, capabilities);
@@ -98,6 +126,14 @@ function createCodeRuntimeControlClient(options) {
98
126
  source: async (sessionId) => parseSource(
99
127
  await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
100
128
  ),
129
+ sourceArchive: async (sessionId, alias) => {
130
+ if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias)) throw new TypeError("invalid Code source alias");
131
+ const response2 = await callRaw(
132
+ `/registry/code/runtime/sessions/${validSessionId(sessionId)}/source/archive`,
133
+ { alias }
134
+ );
135
+ return parseSourceArchiveResponse(response2, alias);
136
+ },
101
137
  infer: async (sessionId, inference) => {
102
138
  const value = record(await call(
103
139
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
@@ -162,9 +198,70 @@ function createCodeRuntimeControlClient(options) {
162
198
  }
163
199
  };
164
200
  }
201
+ var ARCHIVE_LIMIT_MAXIMA = {
202
+ maxCompressedBytes: 64 * 1024 * 1024,
203
+ maxDecompressedBytes: 96 * 1024 * 1024,
204
+ maxEntries: 2e5,
205
+ maxFiles: 1e5,
206
+ maxFileBytes: 16 * 1024 * 1024,
207
+ maxTotalFileBytes: 80 * 1024 * 1024,
208
+ maxPathBytes: 4096,
209
+ maxExtendedHeaderBytes: 32 * 1024
210
+ };
211
+ async function parseSourceArchiveResponse(response2, expectedAlias) {
212
+ const alias = response2.headers.get("x-odla-source-alias") ?? "";
213
+ const repository = response2.headers.get("x-odla-source-repository") ?? "";
214
+ const commitSha = response2.headers.get("x-odla-source-commit") ?? "";
215
+ if (alias !== expectedAlias || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || !/^[0-9a-f]{40}$/.test(commitSha)) {
216
+ await response2.body?.cancel().catch(() => void 0);
217
+ throw new CodeRuntimeControlError("invalid Code source archive identity", 502, "invalid_response");
218
+ }
219
+ const limits = Object.fromEntries(Object.entries(ARCHIVE_LIMIT_MAXIMA).map(([key, maximum]) => {
220
+ const header = `x-odla-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
221
+ const value = response2.headers.get(header) ?? "";
222
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1 || Number(value) > maximum) {
223
+ throw new CodeRuntimeControlError("invalid Code source archive limits", 502, "invalid_response");
224
+ }
225
+ return [key, Number(value)];
226
+ }));
227
+ const length = response2.headers.get("content-length");
228
+ if (length && (!/^\d+$/.test(length) || Number(length) > limits.maxCompressedBytes)) {
229
+ await response2.body?.cancel().catch(() => void 0);
230
+ throw new CodeRuntimeControlError("Code source archive exceeds its byte bound", 502, "invalid_response");
231
+ }
232
+ const compressed = await readBoundedResponse(response2.body, limits.maxCompressedBytes);
233
+ return { alias, repository, commitSha, compressed, limits };
234
+ }
235
+ async function readBoundedResponse(body, maximum) {
236
+ if (!body) throw new CodeRuntimeControlError("Code source archive body is missing", 502, "invalid_response");
237
+ const reader = body.getReader();
238
+ const chunks = [];
239
+ let size = 0;
240
+ try {
241
+ while (true) {
242
+ const { done, value } = await reader.read();
243
+ if (done) break;
244
+ size += value.byteLength;
245
+ if (size > maximum) {
246
+ await reader.cancel().catch(() => void 0);
247
+ throw new CodeRuntimeControlError("Code source archive exceeds its byte bound", 502, "invalid_response");
248
+ }
249
+ chunks.push(value);
250
+ }
251
+ } finally {
252
+ reader.releaseLock();
253
+ }
254
+ const result = new Uint8Array(size);
255
+ let offset = 0;
256
+ for (const chunk of chunks) {
257
+ result.set(chunk, offset);
258
+ offset += chunk.byteLength;
259
+ }
260
+ return result;
261
+ }
165
262
 
166
263
  // src/code-runtime.ts
167
- var CODE_RUNTIME_PROTOCOL_VERSION = 2;
264
+ var CODE_RUNTIME_PROTOCOL_VERSION = 3;
168
265
  async function runCodeRuntimeHeartbeatLoop(options) {
169
266
  const heartbeatMs = options.heartbeatMs ?? 15e3;
170
267
  if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
@@ -233,12 +330,12 @@ function retryableControlFailure(value) {
233
330
  return failure.status === 408 || failure.status === 425 || failure.status === 429 || failure.status >= 500;
234
331
  }
235
332
  function wait(ms, signal) {
236
- return new Promise((resolve5) => {
237
- if (signal?.aborted) return resolve5();
238
- const timer = setTimeout(resolve5, ms);
333
+ return new Promise((resolve6) => {
334
+ if (signal?.aborted) return resolve6();
335
+ const timer = setTimeout(resolve6, ms);
239
336
  signal?.addEventListener("abort", () => {
240
337
  clearTimeout(timer);
241
- resolve5();
338
+ resolve6();
242
339
  }, { once: true });
243
340
  });
244
341
  }
@@ -1080,6 +1177,246 @@ var CodeRuntimeCheckpointManager = class {
1080
1177
  }
1081
1178
  };
1082
1179
 
1180
+ // src/code-runtime-archive.ts
1181
+ import { gunzipSync } from "zlib";
1182
+ import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
1183
+ import { tmpdir } from "os";
1184
+ import { dirname, join as join2, resolve as resolve3, sep as sep2 } from "path";
1185
+ var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
1186
+ var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
1187
+ var MAX_NUL_SHARE = 0.1;
1188
+ async function materializeCodeRuntimeArchive(archive, visiblePaths, tempRoot = tmpdir()) {
1189
+ if (!archive.compressed.byteLength || archive.compressed.byteLength > archive.limits.maxCompressedBytes) {
1190
+ throw new TypeError("Code source archive exceeds its compressed byte bound");
1191
+ }
1192
+ let bytes;
1193
+ try {
1194
+ bytes = archive.compressed[0] === 31 && archive.compressed[1] === 139 ? gunzipSync(archive.compressed, { maxOutputLength: archive.limits.maxDecompressedBytes }) : archive.compressed;
1195
+ } catch {
1196
+ throw new TypeError("Code source archive is not a valid bounded gzip stream");
1197
+ }
1198
+ if (bytes.byteLength > archive.limits.maxDecompressedBytes) {
1199
+ throw new TypeError("Code source archive exceeds its decompressed byte bound");
1200
+ }
1201
+ const entries = parseTar(bytes, archive.limits);
1202
+ const root = await mkdtemp(join2(tempRoot, "odla-code-archive-"));
1203
+ const sourceDir = join2(root, "source");
1204
+ await mkdir(sourceDir);
1205
+ let visible = 0;
1206
+ try {
1207
+ for (const entry of entries) {
1208
+ if (entry.directory || visiblePaths && !visiblePaths.has(entry.path)) continue;
1209
+ if (filteredPath(entry.path)) continue;
1210
+ let content;
1211
+ try {
1212
+ content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(entry.bytes);
1213
+ } catch {
1214
+ continue;
1215
+ }
1216
+ if (nulShare(content) > MAX_NUL_SHARE) continue;
1217
+ const target = resolve3(sourceDir, entry.path);
1218
+ if (!target.startsWith(`${resolve3(sourceDir)}${sep2}`)) throw new TypeError("Code source path escapes its root");
1219
+ await mkdir(dirname(target), { recursive: true });
1220
+ await writeFile(target, content, { flag: "wx", mode: 420 });
1221
+ visible += 1;
1222
+ }
1223
+ if (!visible && !visiblePaths) throw new TypeError("GitHub commit has no Code-visible text source");
1224
+ return { sourceDir, cleanup: () => rm(root, { recursive: true, force: true }) };
1225
+ } catch (cause) {
1226
+ await rm(root, { recursive: true, force: true });
1227
+ throw cause;
1228
+ }
1229
+ }
1230
+ function parseTar(bytes, limits) {
1231
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
1232
+ const entries = [];
1233
+ const archivePaths = /* @__PURE__ */ new Set();
1234
+ let entryCount = 0;
1235
+ let fileCount = 0;
1236
+ let totalFileBytes = 0;
1237
+ let pendingPath;
1238
+ let pendingPaxPath;
1239
+ let offset = 0;
1240
+ let ended = false;
1241
+ while (offset + 512 <= bytes.byteLength) {
1242
+ const header = bytes.subarray(offset, offset + 512);
1243
+ offset += 512;
1244
+ if (zeroBlock(header)) {
1245
+ ended = true;
1246
+ assertZeroTail(bytes, offset);
1247
+ break;
1248
+ }
1249
+ entryCount += 1;
1250
+ if (entryCount > limits.maxEntries) throw new TypeError("Code source archive has too many entries");
1251
+ validateChecksum(header);
1252
+ const type = header[156] ?? 0;
1253
+ const size = tarNumber(header.subarray(124, 136));
1254
+ if (size > limits.maxDecompressedBytes || offset + aligned(size) > bytes.byteLength) {
1255
+ throw new TypeError("Code source archive entry is invalid or too large");
1256
+ }
1257
+ const payload = bytes.subarray(offset, offset + size);
1258
+ offset += aligned(size);
1259
+ if (type === 120 || type === 103) {
1260
+ if (size > limits.maxExtendedHeaderBytes) throw new TypeError("Code source archive extended header is too large");
1261
+ const pax = parsePax(payload, decoder, limits.maxPathBytes);
1262
+ if (pax.linkpath !== void 0 || pax.size !== void 0 || Object.keys(pax).some((key) => key.toLowerCase().includes("sparse"))) {
1263
+ throw new TypeError("Code source archive contains an unsupported entry");
1264
+ }
1265
+ if (type === 103 && pax.path !== void 0) throw new TypeError("Code source archive path is unsafe");
1266
+ if (type === 120 && pax.path !== void 0) {
1267
+ if (pendingPaxPath !== void 0 || pendingPath !== void 0) throw new TypeError("Code source archive is invalid");
1268
+ pendingPaxPath = pax.path;
1269
+ }
1270
+ continue;
1271
+ }
1272
+ if (type === 76) {
1273
+ if (size > limits.maxExtendedHeaderBytes || pendingPath !== void 0 || pendingPaxPath !== void 0) {
1274
+ throw new TypeError("Code source archive long path is invalid");
1275
+ }
1276
+ pendingPath = validateArchivePath(decode(payload, decoder).replace(/\0+$/, "").replace(/\n$/, ""), limits.maxPathBytes);
1277
+ continue;
1278
+ }
1279
+ if (type === 75) throw new TypeError("Code source archive contains an unsupported link");
1280
+ const path = validateArchivePath(pendingPaxPath ?? pendingPath ?? tarPath(header, decoder), limits.maxPathBytes);
1281
+ pendingPaxPath = void 0;
1282
+ pendingPath = void 0;
1283
+ const directory = type === 53;
1284
+ const regular = type === 0 || type === 48;
1285
+ if (!directory && !regular || directory && size !== 0 || archivePaths.has(path)) {
1286
+ throw new TypeError("Code source archive contains an invalid or duplicate entry");
1287
+ }
1288
+ archivePaths.add(path);
1289
+ if (regular) {
1290
+ fileCount += 1;
1291
+ totalFileBytes += size;
1292
+ if (fileCount > limits.maxFiles || size > limits.maxFileBytes || totalFileBytes > limits.maxTotalFileBytes) {
1293
+ throw new TypeError("Code source archive exceeds its file bounds");
1294
+ }
1295
+ }
1296
+ entries.push({ path, directory, ...regular ? { bytes: payload } : {} });
1297
+ }
1298
+ if (!ended || pendingPath !== void 0 || pendingPaxPath !== void 0 || !entries.length || !fileCount) {
1299
+ throw new TypeError("Code source archive is incomplete");
1300
+ }
1301
+ return unwrapRepository(entries, limits.maxPathBytes);
1302
+ }
1303
+ function unwrapRepository(entries, maxPathBytes) {
1304
+ const root = entries[0].path.split("/")[0];
1305
+ if (!root || entries.some((entry) => entry.path !== root && !entry.path.startsWith(`${root}/`))) {
1306
+ throw new TypeError("Code source archive has no single repository root");
1307
+ }
1308
+ const output = [];
1309
+ const kinds = /* @__PURE__ */ new Map();
1310
+ for (const entry of entries) {
1311
+ if (entry.path === root) {
1312
+ if (!entry.directory) throw new TypeError("Code source archive root is not a directory");
1313
+ continue;
1314
+ }
1315
+ const path = entry.path.slice(root.length + 1);
1316
+ validateRepositoryPath(path, maxPathBytes);
1317
+ const parts = path.split("/");
1318
+ for (let index = 1; index < parts.length; index += 1) {
1319
+ if (kinds.get(parts.slice(0, index).join("/")) === "file") throw new TypeError("Code source archive path conflicts");
1320
+ }
1321
+ if (!entry.directory) {
1322
+ for (const existing of kinds.keys()) {
1323
+ if (existing.startsWith(`${path}/`)) throw new TypeError("Code source archive path conflicts");
1324
+ }
1325
+ }
1326
+ if (kinds.has(path)) throw new TypeError("Code source archive repeats a path");
1327
+ kinds.set(path, entry.directory ? "directory" : "file");
1328
+ output.push({ ...entry, path });
1329
+ }
1330
+ return output.sort((left, right) => left.path.localeCompare(right.path));
1331
+ }
1332
+ function parsePax(bytes, decoder, maxPathBytes) {
1333
+ const result = {};
1334
+ let offset = 0;
1335
+ while (offset < bytes.byteLength) {
1336
+ const space = bytes.indexOf(32, offset);
1337
+ if (space < 0) throw new TypeError("Code source archive PAX header is invalid");
1338
+ const lengthText = ascii(bytes.subarray(offset, space));
1339
+ if (!/^[1-9][0-9]{0,8}$/.test(lengthText)) throw new TypeError("Code source archive PAX length is invalid");
1340
+ const length = Number(lengthText);
1341
+ const end = offset + length;
1342
+ if (!Number.isSafeInteger(length) || end > bytes.byteLength || bytes[end - 1] !== 10) {
1343
+ throw new TypeError("Code source archive PAX record is invalid");
1344
+ }
1345
+ const record3 = decode(bytes.subarray(space + 1, end - 1), decoder);
1346
+ const equals = record3.indexOf("=");
1347
+ if (equals < 1) throw new TypeError("Code source archive PAX field is invalid");
1348
+ const key = record3.slice(0, equals);
1349
+ if (Object.hasOwn(result, key)) throw new TypeError("Code source archive PAX field repeats");
1350
+ result[key] = record3.slice(equals + 1);
1351
+ offset = end;
1352
+ }
1353
+ if (result.path !== void 0) result.path = validateArchivePath(result.path, maxPathBytes);
1354
+ return result;
1355
+ }
1356
+ var aligned = (size) => Math.ceil(size / 512) * 512;
1357
+ var zeroBlock = (block) => block.every((value) => value === 0);
1358
+ function validateChecksum(header) {
1359
+ const expected = tarNumber(header.subarray(148, 156));
1360
+ let actual = 0;
1361
+ for (let index = 0; index < header.length; index += 1) actual += index >= 148 && index < 156 ? 32 : header[index];
1362
+ if (actual !== expected) throw new TypeError("Code source archive checksum is invalid");
1363
+ }
1364
+ function tarNumber(field) {
1365
+ if ((field[0] ?? 0) & 128) throw new TypeError("Code source archive numeric format is unsupported");
1366
+ const value = ascii(field).replaceAll("\0", "").trim();
1367
+ if (!value) return 0;
1368
+ if (!/^[0-7]+$/.test(value)) throw new TypeError("Code source archive number is invalid");
1369
+ const parsed = Number.parseInt(value, 8);
1370
+ if (!Number.isSafeInteger(parsed) || parsed < 0) throw new TypeError("Code source archive number is too large");
1371
+ return parsed;
1372
+ }
1373
+ function tarPath(header, decoder) {
1374
+ const name = decodeNul(header.subarray(0, 100), decoder);
1375
+ const prefix = decodeNul(header.subarray(345, 500), decoder);
1376
+ return prefix ? `${prefix}/${name}` : name;
1377
+ }
1378
+ function decodeNul(value, decoder) {
1379
+ const end = value.indexOf(0);
1380
+ return decode(end < 0 ? value : value.subarray(0, end), decoder);
1381
+ }
1382
+ function decode(value, decoder) {
1383
+ try {
1384
+ return decoder.decode(value);
1385
+ } catch {
1386
+ throw new TypeError("Code source archive text is invalid UTF-8");
1387
+ }
1388
+ }
1389
+ function ascii(value) {
1390
+ let output = "";
1391
+ for (const byte of value) output += String.fromCharCode(byte);
1392
+ return output;
1393
+ }
1394
+ function validateArchivePath(input, maximum) {
1395
+ const path = input.endsWith("/") ? input.slice(0, -1) : input;
1396
+ validateRepositoryPath(path, maximum);
1397
+ return path;
1398
+ }
1399
+ function validateRepositoryPath(path, maximum) {
1400
+ if (!path || path.startsWith("/") || path.includes("\\") || /[\u0000-\u001f\u007f]/.test(path) || new TextEncoder().encode(path).byteLength > maximum || path.split("/").some((part) => !part || part === "." || part === "..")) {
1401
+ throw new TypeError("Code source archive path is unsafe");
1402
+ }
1403
+ }
1404
+ function assertZeroTail(bytes, offset) {
1405
+ for (let index = offset; index < bytes.byteLength; index += 1) {
1406
+ if (bytes[index] !== 0) throw new TypeError("Code source archive has data after its end marker");
1407
+ }
1408
+ }
1409
+ function filteredPath(path) {
1410
+ const parts = path.split("/");
1411
+ return parts.some((part) => RESERVED2.has(part) || SECRET2.test(part));
1412
+ }
1413
+ function nulShare(content) {
1414
+ if (!content.length) return 0;
1415
+ let count = 0;
1416
+ for (let index = 0; index < content.length; index += 1) if (content.charCodeAt(index) === 0) count += 1;
1417
+ return count / content.length;
1418
+ }
1419
+
1083
1420
  // src/code-runtime-task.ts
1084
1421
  function codeCommandMetadata(payload, resume) {
1085
1422
  const trusted = record2(payload.trustedBase);
@@ -1099,7 +1436,8 @@ function codeCommandMetadata(payload, resume) {
1099
1436
  const repository = trusted?.repository;
1100
1437
  const baseCommitSha = trusted?.commitSha;
1101
1438
  const sourceTreeDigest = trusted?.treeDigest;
1102
- if (typeof repository !== "string" || !repository.includes("/") || typeof baseCommitSha !== "string" || !/^[0-9a-f]{40}$/.test(baseCommitSha) || typeof sourceTreeDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(sourceTreeDigest)) {
1439
+ const hostMaterialized = record2(payload.sourceSet) !== null && sourceTreeDigest === void 0;
1440
+ if (typeof repository !== "string" || !repository.includes("/") || typeof baseCommitSha !== "string" || !/^[0-9a-f]{40}$/.test(baseCommitSha) || !hostMaterialized && (typeof sourceTreeDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(sourceTreeDigest))) {
1103
1441
  throw new TypeError(`invalid Code ${resume ? "resume" : "start"} trusted base`);
1104
1442
  }
1105
1443
  if (!Number.isSafeInteger(maxTokensPerInteraction) || Number(maxTokensPerInteraction) < 4e3 || Number(maxTokensPerInteraction) > 2e5) {
@@ -1115,7 +1453,7 @@ function codeCommandMetadata(payload, resume) {
1115
1453
  attestationDigest: typeof attestation === "string" ? attestation : "resume",
1116
1454
  repository,
1117
1455
  baseCommitSha,
1118
- sourceTreeDigest
1456
+ sourceTreeDigest: typeof sourceTreeDigest === "string" ? sourceTreeDigest : null
1119
1457
  };
1120
1458
  }
1121
1459
  function codeLocalSource(payload) {
@@ -1180,19 +1518,80 @@ async function prepareRuntimeLocalSource(input) {
1180
1518
  }
1181
1519
 
1182
1520
  // src/code-runtime-source.ts
1183
- import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
1184
- import { tmpdir } from "os";
1185
- import { dirname, join as join2, resolve as resolve3, sep as sep2 } from "path";
1186
- var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
1187
- var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
1521
+ import { mkdir as mkdir3, mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1522
+ import { tmpdir as tmpdir2 } from "os";
1523
+ import { dirname as dirname3, join as join4, resolve as resolve4, sep as sep3 } from "path";
1524
+
1525
+ // src/code-runtime-selected-source.ts
1526
+ import { chmod, cp, mkdir as mkdir2, readdir as readdir2 } from "fs/promises";
1527
+ import { dirname as dirname2, join as join3 } from "path";
1528
+ function selectedSourceSet(payload) {
1529
+ if (!payload.sourceSet) return null;
1530
+ const set = payload.sourceSet && typeof payload.sourceSet === "object" && !Array.isArray(payload.sourceSet) ? payload.sourceSet : null;
1531
+ if (!set) throw new TypeError("Code selected source set is invalid");
1532
+ const parse = (value, primary2) => {
1533
+ const item = value && typeof value === "object" && !Array.isArray(value) ? value : null;
1534
+ const alias = item?.alias;
1535
+ const repository = item?.repository;
1536
+ const commitSha = item?.commitSha;
1537
+ const materialization = item?.materialization ?? "registry_snapshot";
1538
+ if (typeof alias !== "string" || alias !== (primary2 ? "primary" : alias) || !/^[a-z][a-z0-9-]{0,39}$/.test(alias) || typeof repository !== "string" || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || typeof commitSha !== "string" || !/^[0-9a-f]{40}$/.test(commitSha) || !["registry_snapshot", "local_checkout", "host_archive"].includes(String(materialization))) {
1539
+ throw new TypeError("Code selected source identity is invalid");
1540
+ }
1541
+ const visiblePaths = item?.visiblePaths;
1542
+ if (visiblePaths !== void 0 && (!Array.isArray(visiblePaths) || visiblePaths.length > 2e3 || visiblePaths.some((path) => typeof path !== "string" || path.length > 4096))) {
1543
+ throw new TypeError("Code selected source visibility slice is invalid");
1544
+ }
1545
+ return {
1546
+ alias,
1547
+ repository,
1548
+ commitSha,
1549
+ materialization,
1550
+ ...visiblePaths ? { visiblePaths } : {}
1551
+ };
1552
+ };
1553
+ if (!Array.isArray(set.references)) throw new TypeError("Code selected source references are invalid");
1554
+ const primary = parse(set.primary, true);
1555
+ const references = set.references.map((item) => parse(item, false));
1556
+ const aliases = /* @__PURE__ */ new Set([primary.alias, ...references.map((item) => item.alias)]);
1557
+ if (aliases.size !== references.length + 1) throw new TypeError("Code selected source aliases repeat");
1558
+ return { primary, references };
1559
+ }
1560
+ async function attachReferenceDirectories(workspace, references) {
1561
+ for (const reference of references) {
1562
+ validateAlias(reference.alias);
1563
+ for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
1564
+ const target = join3(root, ".odla-references", reference.alias);
1565
+ await mkdir2(dirname2(target), { recursive: true });
1566
+ await cp(reference.sourceDir, target, { recursive: true, errorOnExist: true, force: false });
1567
+ await makeTreeReadOnly(target);
1568
+ }
1569
+ }
1570
+ }
1571
+ function validateAlias(alias) {
1572
+ if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
1573
+ throw new TypeError("Code reference alias is invalid");
1574
+ }
1575
+ }
1576
+ async function makeTreeReadOnly(root) {
1577
+ for (const entry of await readdir2(root, { withFileTypes: true })) {
1578
+ const target = join3(root, entry.name);
1579
+ if (entry.isDirectory()) await makeTreeReadOnly(target);
1580
+ else if (entry.isFile()) await chmod(target, 292);
1581
+ }
1582
+ }
1583
+
1584
+ // src/code-runtime-source.ts
1585
+ var RESERVED3 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
1586
+ var SECRET3 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
1188
1587
  var SOURCE_MAX_FILES = 1e5;
1189
1588
  var SOURCE_MAX_BYTES = 80 * 1024 * 1024;
1190
1589
  var SOURCE_SET_MAX_BYTES = 480 * 1024 * 1024;
1191
- async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir()) {
1590
+ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir2()) {
1192
1591
  if (!snapshot.files.length || snapshot.files.length > SOURCE_MAX_FILES) throw new TypeError("Code source file count is invalid");
1193
- const root = await mkdtemp(join2(tempRoot, "odla-code-source-"));
1194
- const sourceDir = join2(root, "source");
1195
- await mkdir(sourceDir);
1592
+ const root = await mkdtemp2(join4(tempRoot, "odla-code-source-"));
1593
+ const sourceDir = join4(root, "source");
1594
+ await mkdir3(sourceDir);
1196
1595
  const seen = /* @__PURE__ */ new Set();
1197
1596
  let bytes = 0;
1198
1597
  try {
@@ -1202,13 +1601,13 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir()) {
1202
1601
  seen.add(file.path);
1203
1602
  bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
1204
1603
  if (bytes > SOURCE_MAX_BYTES) throw new TypeError("Code source exceeds its byte bound");
1205
- const target = resolve3(sourceDir, file.path);
1206
- if (!target.startsWith(`${resolve3(sourceDir)}${sep2}`)) throw new TypeError("Code source path escapes its root");
1207
- await mkdir(dirname(target), { recursive: true });
1208
- await writeFile(target, file.content, { flag: "wx", mode: 420 });
1604
+ const target = resolve4(sourceDir, file.path);
1605
+ if (!target.startsWith(`${resolve4(sourceDir)}${sep3}`)) throw new TypeError("Code source path escapes its root");
1606
+ await mkdir3(dirname3(target), { recursive: true });
1607
+ await writeFile2(target, file.content, { flag: "wx", mode: 420 });
1209
1608
  }
1210
1609
  for (const reference of snapshot.references ?? []) {
1211
- validateAlias(reference.alias);
1610
+ validateAlias2(reference.alias);
1212
1611
  if (!reference.files.length || reference.files.length > SOURCE_MAX_FILES) throw new TypeError("Code reference file count is invalid");
1213
1612
  for (const file of reference.files) {
1214
1613
  validatePath(file.path);
@@ -1217,19 +1616,19 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir()) {
1217
1616
  seen.add(path);
1218
1617
  bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
1219
1618
  if (bytes > SOURCE_SET_MAX_BYTES) throw new TypeError("Code source set exceeds its byte bound");
1220
- const target = resolve3(sourceDir, path);
1221
- if (!target.startsWith(`${resolve3(sourceDir)}${sep2}`)) throw new TypeError("Code reference path escapes its root");
1222
- await mkdir(dirname(target), { recursive: true });
1223
- await writeFile(target, file.content, { flag: "wx", mode: 292 });
1619
+ const target = resolve4(sourceDir, path);
1620
+ if (!target.startsWith(`${resolve4(sourceDir)}${sep3}`)) throw new TypeError("Code reference path escapes its root");
1621
+ await mkdir3(dirname3(target), { recursive: true });
1622
+ await writeFile2(target, file.content, { flag: "wx", mode: 292 });
1224
1623
  }
1225
1624
  }
1226
- return { sourceDir, cleanup: () => rm(root, { recursive: true, force: true }) };
1625
+ return { sourceDir, cleanup: () => rm2(root, { recursive: true, force: true }) };
1227
1626
  } catch (cause) {
1228
- await rm(root, { recursive: true, force: true });
1627
+ await rm2(root, { recursive: true, force: true });
1229
1628
  throw cause;
1230
1629
  }
1231
1630
  }
1232
- function validateAlias(alias) {
1631
+ function validateAlias2(alias) {
1233
1632
  if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
1234
1633
  throw new TypeError("Code reference alias is invalid");
1235
1634
  }
@@ -1237,24 +1636,24 @@ function validateAlias(alias) {
1237
1636
  async function attachCodeRuntimeReferences(workspace, references) {
1238
1637
  let bytes = 0;
1239
1638
  for (const reference of references) {
1240
- validateAlias(reference.alias);
1639
+ validateAlias2(reference.alias);
1241
1640
  for (const file of reference.files) {
1242
1641
  validatePath(file.path);
1243
1642
  const path = `.odla-references/${reference.alias}/${file.path}`;
1244
1643
  bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
1245
1644
  if (bytes > SOURCE_SET_MAX_BYTES - SOURCE_MAX_BYTES) throw new TypeError("Code reference set exceeds its byte bound");
1246
1645
  for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
1247
- const target = resolve3(root, path);
1248
- if (!target.startsWith(`${resolve3(root)}${sep2}`)) throw new TypeError("Code reference path escapes its root");
1249
- await mkdir(dirname(target), { recursive: true });
1250
- await writeFile(target, file.content, { flag: "wx", mode: 292 });
1646
+ const target = resolve4(root, path);
1647
+ if (!target.startsWith(`${resolve4(root)}${sep3}`)) throw new TypeError("Code reference path escapes its root");
1648
+ await mkdir3(dirname3(target), { recursive: true });
1649
+ await writeFile2(target, file.content, { flag: "wx", mode: 292 });
1251
1650
  }
1252
1651
  }
1253
1652
  }
1254
1653
  }
1255
1654
  function validatePath(path) {
1256
1655
  const parts = path.split("/");
1257
- if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED2.has(part) || SECRET2.test(part))) {
1656
+ if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED3.has(part) || SECRET3.test(part))) {
1258
1657
  throw new TypeError("Code source contains an unsafe path");
1259
1658
  }
1260
1659
  }
@@ -1282,12 +1681,12 @@ async function materializeCommandWorkspace(input) {
1282
1681
  throw new TypeError("Code selected source set is invalid");
1283
1682
  }
1284
1683
  if (references.length) {
1285
- const selected = await input.control.source(command.sessionId);
1286
- if (selected.repository !== metadata.repository || selected.commitSha !== metadata.baseCommitSha || selected.treeDigest !== metadata.sourceTreeDigest) {
1684
+ const selected2 = await input.control.source(command.sessionId);
1685
+ if (selected2.repository !== metadata.repository || selected2.commitSha !== metadata.baseCommitSha || selected2.treeDigest !== metadata.sourceTreeDigest) {
1287
1686
  await prepared.workspace.cleanup();
1288
1687
  throw new TypeError("Code local source does not match the selected GitHub primary source");
1289
1688
  }
1290
- await attachCodeRuntimeReferences(prepared.workspace, selected.references ?? []);
1689
+ await attachCodeRuntimeReferences(prepared.workspace, selected2.references ?? []);
1291
1690
  }
1292
1691
  }
1293
1692
  return {
@@ -1297,6 +1696,63 @@ async function materializeCommandWorkspace(input) {
1297
1696
  requestedLocal
1298
1697
  };
1299
1698
  }
1699
+ const selected = selectedSourceSet(command.payload);
1700
+ if (selected?.primary.materialization === "host_archive") {
1701
+ if (!input.control.sourceArchive) {
1702
+ throw new TypeError("Code runtime protocol does not support host source materialization");
1703
+ }
1704
+ const sources = [selected.primary, ...selected.references];
1705
+ const materialized2 = [];
1706
+ try {
1707
+ for (const descriptor2 of sources) {
1708
+ if (descriptor2.materialization !== "host_archive") {
1709
+ throw new TypeError("Code selected source set mixes incompatible materialization modes");
1710
+ }
1711
+ const archive = await input.control.sourceArchive(command.sessionId, descriptor2.alias);
1712
+ if (archive.repository !== descriptor2.repository || archive.commitSha !== descriptor2.commitSha) {
1713
+ throw new TypeError("Code source archive does not match its selected repository and commit");
1714
+ }
1715
+ const source2 = await materializeCodeRuntimeArchive(
1716
+ archive,
1717
+ descriptor2.visiblePaths ? new Set(descriptor2.visiblePaths) : void 0
1718
+ );
1719
+ const treeDigest = await digestStagedWorkspace(source2.sourceDir, {
1720
+ maxFiles: archive.limits.maxFiles,
1721
+ maxBytes: archive.limits.maxTotalFileBytes
1722
+ });
1723
+ materialized2.push({ descriptor: descriptor2, source: source2, treeDigest });
1724
+ }
1725
+ const primary = materialized2[0];
1726
+ const workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
1727
+ trustedBaseDir: primary.source.sourceDir,
1728
+ trustedBaseCommitSha: primary.descriptor.commitSha,
1729
+ checkpoint: codeCheckpointPayload(command.payload)
1730
+ })).workspace : await stageWorkspace(primary.source.sourceDir, {
1731
+ maxFiles: SOURCE_MAX_FILES,
1732
+ maxBytes: SOURCE_SET_MAX_BYTES
1733
+ });
1734
+ try {
1735
+ await attachReferenceDirectories(workspace, materialized2.slice(1).map((item) => ({
1736
+ alias: item.descriptor.alias,
1737
+ sourceDir: item.source.sourceDir
1738
+ })));
1739
+ } catch (cause) {
1740
+ await workspace.cleanup();
1741
+ throw cause;
1742
+ }
1743
+ return {
1744
+ workspace,
1745
+ sourceDigest: primary.treeDigest,
1746
+ requestedLocal: null,
1747
+ sourceDigests: materialized2.map((item) => ({
1748
+ alias: item.descriptor.alias,
1749
+ treeDigest: item.treeDigest
1750
+ }))
1751
+ };
1752
+ } finally {
1753
+ await Promise.allSettled(materialized2.map((item) => item.source.cleanup()));
1754
+ }
1755
+ }
1300
1756
  const source = await input.control.source(command.sessionId);
1301
1757
  const materialized = await materializeCodeRuntimeSource(source);
1302
1758
  try {
@@ -1345,10 +1801,12 @@ Prefer these over listing the tree \u2014 a full listing of a real repository is
1345
1801
  of thousands of tokens and you will carry it for the rest of the session.
1346
1802
 
1347
1803
  Then odla_search for a literal string, odla_read for a bounded range, and
1348
- odla_apply_git_diff to change something. A patch must start with
1349
- "diff --git a/<path> b/<path>", include matching "---" and "+++" headers and
1350
- numbered "@@" hunks with at least one line of surrounding context, and must never
1351
- use "*** Begin Patch" wrappers.
1804
+ odla_edit_file to change an existing file: give it the exact current text
1805
+ (copied verbatim from odla_read) and the text that replaces it; it must match
1806
+ once. Use odla_apply_git_diff only to create or delete whole files. A patch must
1807
+ start with "diff --git a/<path> b/<path>", include matching "---" and "+++"
1808
+ headers and numbered "@@" hunks with at least one line of surrounding context,
1809
+ and must never use "*** Begin Patch" wrappers.
1352
1810
 
1353
1811
  The workspace, model, and tool effects are controlled by the host broker.
1354
1812
  Never claim a build or test passed unless odla_run_recipe returned that result.`;
@@ -1408,6 +1866,21 @@ function codeSkill(opts) {
1408
1866
  },
1409
1867
  handler: (input, ctx) => call("sandbox.apply_patch", input, ctx.signal)
1410
1868
  };
1869
+ const editFile = {
1870
+ name: "odla_edit_file",
1871
+ description: "Change an existing file by replacing one exact stretch of its current text with new text. oldText must match the file exactly once (copy it verbatim from odla_read, including indentation and blank lines; widen it if it would match more than once). Prefer this over odla_apply_git_diff for every change to an existing file; use odla_apply_git_diff only to create or delete whole files.",
1872
+ inputSchema: {
1873
+ type: "object",
1874
+ required: ["path", "oldText", "newText"],
1875
+ properties: {
1876
+ path: { type: "string", minLength: 1, maxLength: 1024 },
1877
+ oldText: { type: "string", minLength: 1, maxLength: 262144 },
1878
+ newText: { type: "string", maxLength: 262144 }
1879
+ },
1880
+ additionalProperties: false
1881
+ },
1882
+ handler: (input, ctx) => call("sandbox.edit", input, ctx.signal)
1883
+ };
1411
1884
  const runRecipe = {
1412
1885
  name: "odla_run_recipe",
1413
1886
  description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
@@ -1495,7 +1968,7 @@ function codeSkill(opts) {
1495
1968
  )
1496
1969
  ];
1497
1970
  const effects = opts.readOnly ? [] : [applyPatch, runRecipe];
1498
- const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
1971
+ const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
1499
1972
  return { name: "code", tools };
1500
1973
  }
1501
1974
 
@@ -1633,14 +2106,40 @@ async function sessionSkillsFor(options, command) {
1633
2106
  }
1634
2107
 
1635
2108
  // src/code-runtime-inference.ts
2109
+ var OVERLOAD_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3];
2110
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["control_plane_overloaded", "registry_overloaded", "transport_unavailable"]);
2111
+ function overloadedControlFailure(cause) {
2112
+ return cause instanceof CodeRuntimeControlError && cause.status === 503 && RETRYABLE_CODES.has(cause.code);
2113
+ }
2114
+ async function inferWithBackoff(infer, wait2, onRetry) {
2115
+ for (let attempt = 0; ; attempt += 1) {
2116
+ try {
2117
+ return await infer();
2118
+ } catch (cause) {
2119
+ const delayMs = OVERLOAD_RETRY_DELAYS_MS[attempt];
2120
+ if (delayMs === void 0 || !overloadedControlFailure(cause)) throw cause;
2121
+ await onRetry(cause, delayMs);
2122
+ await wait2(delayMs);
2123
+ }
2124
+ }
2125
+ }
1636
2126
  async function handleCodeRuntimeInference(input) {
1637
2127
  const { command, request, state } = input;
1638
2128
  const startedAt = Date.now();
1639
- const response2 = await input.control.infer(command.sessionId, {
1640
- requestId: request.requestId,
1641
- interactionId: command.commandId,
1642
- call: request.call
1643
- });
2129
+ const wait2 = input.wait ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
2130
+ const response2 = await inferWithBackoff(
2131
+ () => input.control.infer(command.sessionId, {
2132
+ requestId: request.requestId,
2133
+ interactionId: command.commandId,
2134
+ call: request.call
2135
+ }),
2136
+ wait2,
2137
+ (cause, delayMs) => input.event({
2138
+ type: "diagnostic",
2139
+ level: "error",
2140
+ message: `Code control plane overloaded (${cause.code}); retrying the model call in ${delayMs / 1e3}s`
2141
+ }).catch(() => void 0)
2142
+ );
1644
2143
  state.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
1645
2144
  const { costUsd } = response2.receipt;
1646
2145
  if (costUsd === void 0) state.costKnown = false;
@@ -1696,8 +2195,8 @@ function createCodeRuntimeInference(options) {
1696
2195
 
1697
2196
  // src/code-tool-discovery.ts
1698
2197
  import { spawn as spawn3 } from "child_process";
1699
- import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
1700
- import { relative as relative2, resolve as resolve4 } from "path";
2198
+ import { readFile as readFile2, readdir as readdir3 } from "fs/promises";
2199
+ import { relative as relative2, resolve as resolve5 } from "path";
1701
2200
  var DEFAULT_MAX_FILES = 2e4;
1702
2201
  var DEFAULT_MAX_RESULTS = 100;
1703
2202
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
@@ -1722,10 +2221,10 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
1722
2221
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1723
2222
  const paths = [];
1724
2223
  const walk = async (directory) => {
1725
- for (const entry of await readdir2(directory, { withFileTypes: true })) {
2224
+ for (const entry of await readdir3(directory, { withFileTypes: true })) {
1726
2225
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
1727
2226
  if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
1728
- const target = resolve4(directory, entry.name);
2227
+ const target = resolve5(directory, entry.name);
1729
2228
  if (entry.isDirectory()) await walk(target);
1730
2229
  else if (entry.isFile()) {
1731
2230
  const path = relative2(root, target).split("\\").join("/");
@@ -1739,7 +2238,7 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1739
2238
  }
1740
2239
  }
1741
2240
  };
1742
- await walk(resolve4(root));
2241
+ await walk(resolve5(root));
1743
2242
  return paths.sort();
1744
2243
  }
1745
2244
  function listWorkspace(paths, options = {}) {
@@ -1853,7 +2352,7 @@ async function fallbackSearch(root, scoped, options) {
1853
2352
  if (matches.length >= options.maxResults) break;
1854
2353
  let source;
1855
2354
  try {
1856
- source = await readFile2(resolve4(root, path));
2355
+ source = await readFile2(resolve5(root, path));
1857
2356
  } catch {
1858
2357
  continue;
1859
2358
  }
@@ -1915,6 +2414,11 @@ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
1915
2414
  authority: "authority",
1916
2415
  patch: "payload"
1917
2416
  });
2417
+ var EDIT = descriptor("sandbox.edit", "reversible_mutation", {
2418
+ workspace: "destination",
2419
+ authority: "authority",
2420
+ patch: "payload"
2421
+ });
1918
2422
  var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
1919
2423
  workspace: "destination",
1920
2424
  authority: "authority",
@@ -1982,6 +2486,14 @@ function createCodePolicyGate(options) {
1982
2486
  patch: { role: "payload", value: patch2 }
1983
2487
  }, []);
1984
2488
  },
2489
+ edit: async (input) => {
2490
+ const base = await environment(input, options, "sandbox.edit");
2491
+ const patch2 = unsafe(base, input.patch, "patch");
2492
+ return authorize(input, options, base, EDIT, {
2493
+ ...base.fixedArgs,
2494
+ patch: { role: "payload", value: patch2 }
2495
+ }, []);
2496
+ },
1985
2497
  recipe: async (input) => {
1986
2498
  const base = await environment(input, options, "sandbox.run_recipe");
1987
2499
  const conversions = await conversionRegistry([
@@ -2126,12 +2638,12 @@ function response(request, ok, content, details) {
2126
2638
  return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
2127
2639
  }
2128
2640
 
2129
- // src/code-tool-reads.ts
2130
- import { readFile as readFile4, stat } from "fs/promises";
2641
+ // src/code-tool-edit.ts
2642
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
2131
2643
 
2132
2644
  // src/code-tool-graph.ts
2133
2645
  import { readFile as readFile3 } from "fs/promises";
2134
- import { join as join3 } from "path";
2646
+ import { join as join5 } from "path";
2135
2647
  import {
2136
2648
  hubs,
2137
2649
  incident,
@@ -2145,7 +2657,7 @@ var cache = /* @__PURE__ */ new Map();
2145
2657
  function workspaceGraphs(workspaceDir, paths) {
2146
2658
  const existing = cache.get(workspaceDir);
2147
2659
  if (existing) return existing;
2148
- const read2 = (path) => readFile3(join3(workspaceDir, path), "utf8");
2660
+ const read2 = (path) => readFile3(join5(workspaceDir, path), "utf8");
2149
2661
  const built = (async () => ({
2150
2662
  // No knownTables: a staged workspace may not carry migrations, and a filter
2151
2663
  // that silently drops every table is worse than an unfiltered one. Callers
@@ -2197,7 +2709,65 @@ function renderWhoTouches(graphs, query) {
2197
2709
  }).join("\n\n");
2198
2710
  }
2199
2711
 
2712
+ // src/code-tool-edit.ts
2713
+ var MAX_TEXT_BYTES = 256 * 1024;
2714
+ function editAsDiff(path, oldText, newText) {
2715
+ const removed = oldText.split("\n").map((line) => `-${line}`);
2716
+ const added = newText.split("\n").map((line) => `+${line}`);
2717
+ return `diff --git a/${path} b/${path}
2718
+ --- a/${path}
2719
+ +++ b/${path}
2720
+ @@ -1,${removed.length} +1,${added.length} @@
2721
+ ${[...removed, ...added].join("\n")}
2722
+ `;
2723
+ }
2724
+ function occurrences(haystack, needle) {
2725
+ let count = 0;
2726
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count += 1;
2727
+ return count;
2728
+ }
2729
+ async function editCodeFile(workspaceDir, path, oldText, newText) {
2730
+ if (!oldText) throw new TypeError("oldText must not be empty; to create a file use sandbox.apply_patch with a new-file diff");
2731
+ if (Buffer.byteLength(oldText) > MAX_TEXT_BYTES || Buffer.byteLength(newText) > MAX_TEXT_BYTES) {
2732
+ throw new TypeError(`oldText and newText must each stay under ${MAX_TEXT_BYTES} bytes; split the change`);
2733
+ }
2734
+ if (oldText.includes("\0") || newText.includes("\0")) throw new TypeError("edit text contains NUL bytes; use plain text");
2735
+ const target = resolveCodePath(workspaceDir, path);
2736
+ const current = await readFile4(target, "utf8");
2737
+ const found = occurrences(current, oldText);
2738
+ if (found === 0) {
2739
+ throw new TypeError(`oldText was not found in "${path}"; sandbox.read the current lines and copy them exactly, including indentation and blank lines`);
2740
+ }
2741
+ if (found > 1) {
2742
+ throw new TypeError(`oldText occurs ${found} times in "${path}"; include more of the surrounding lines so it matches exactly once`);
2743
+ }
2744
+ const at = current.indexOf(oldText);
2745
+ await writeFile3(target, `${current.slice(0, at)}${newText}${current.slice(at + oldText.length)}`, "utf8");
2746
+ return { deletions: oldText.split("\n").length, additions: newText.split("\n").length };
2747
+ }
2748
+ async function edit(context, request, options, policy, registry) {
2749
+ exactKeys(request.input, ["path", "oldText", "newText"]);
2750
+ const path = stringField(request.input, "path");
2751
+ const oldText = stringField(request.input, "oldText");
2752
+ const newText = typeof request.input.newText === "string" ? request.input.newText : stringField(request.input, "newText");
2753
+ const paths = await registry.files(context.workspaceDir);
2754
+ if (!paths.includes(path)) {
2755
+ throw new TypeError(`no such file in the staged workspace: "${path}". Use sandbox.overview, sandbox.where_is or sandbox.search to find the correct path.`);
2756
+ }
2757
+ if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
2758
+ throw new TypeError("edit targets a read-only reference source");
2759
+ }
2760
+ const patch2 = editAsDiff(path, oldText, newText);
2761
+ const allowed = await policy.edit(policyContext(context, request, options, { patch: patch2 }));
2762
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2763
+ const changed = await editCodeFile(context.workspaceDir, path, oldText, newText);
2764
+ registry.invalidate(context.workspaceDir);
2765
+ forgetWorkspaceGraphs(context.workspaceDir);
2766
+ return response(request, true, `Edited ${path}: -${changed.deletions} +${changed.additions} line(s).`, { paths: [path], ...changed });
2767
+ }
2768
+
2200
2769
  // src/code-tool-reads.ts
2770
+ import { readFile as readFile5, stat } from "fs/promises";
2201
2771
  var GRAPH_TOOLS = /* @__PURE__ */ new Set([
2202
2772
  "sandbox.overview",
2203
2773
  "sandbox.where_is",
@@ -2223,7 +2793,7 @@ async function read(context, request, options, policy, registry) {
2223
2793
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
2224
2794
  throw new TypeError("file is not a bounded regular source file");
2225
2795
  }
2226
- const source = await readFile4(target);
2796
+ const source = await readFile5(target);
2227
2797
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
2228
2798
  const lines = source.toString("utf8").split("\n");
2229
2799
  const content = lines.slice(startLine - 1, endLine).join("\n");
@@ -2338,6 +2908,7 @@ async function route(context, request, options, recipes, policy, registry) {
2338
2908
  if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
2339
2909
  if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
2340
2910
  if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
2911
+ if (request.tool === "sandbox.edit") return await edit(context, request, options, policy, registry);
2341
2912
  return await recipe(context, request, options, recipes, policy);
2342
2913
  } catch (reason) {
2343
2914
  return response(request, false, toolFailureMessage(reason));
@@ -2768,8 +3339,8 @@ var runtimeErrorMessage = (value) => value instanceof Error ? value.message : St
2768
3339
  function codeRuntimeAcknowledgementGate(signal) {
2769
3340
  let settle;
2770
3341
  let settled = false;
2771
- const ready = new Promise((resolve5) => {
2772
- settle = resolve5;
3342
+ const ready = new Promise((resolve6) => {
3343
+ settle = resolve6;
2773
3344
  });
2774
3345
  const release = (run) => {
2775
3346
  if (settled) return;
@@ -2878,7 +3449,7 @@ var TheseusRuntimeEngine = class {
2878
3449
  async #start(command, resume) {
2879
3450
  if (this.#active.has(command.sessionId)) throw new TypeError("Code session is already active on this runtime");
2880
3451
  const metadata = codeCommandMetadata(command.payload, resume);
2881
- const { workspace, sourceDigest, localTrustedBaseDigest, requestedLocal } = await materializeCommandWorkspace({
3452
+ const { workspace, sourceDigest, sourceDigests, localTrustedBaseDigest, requestedLocal } = await materializeCommandWorkspace({
2882
3453
  command,
2883
3454
  metadata,
2884
3455
  resume,
@@ -2899,7 +3470,7 @@ var TheseusRuntimeEngine = class {
2899
3470
  maxTokensPerInteraction: metadata.maxTokensPerInteraction,
2900
3471
  baseCommitSha: metadata.baseCommitSha,
2901
3472
  repository: metadata.repository,
2902
- sourceTreeDigest: metadata.sourceTreeDigest,
3473
+ sourceTreeDigest: metadata.sourceTreeDigest ?? sourceDigest,
2903
3474
  trustedBaseDigest: requestedLocal ? localTrustedBaseDigest : await digestStagedWorkspace(workspace.baselineDir, {
2904
3475
  maxFiles: 2e4,
2905
3476
  maxBytes: 512 * 1024 * 1024
@@ -2925,7 +3496,11 @@ var TheseusRuntimeEngine = class {
2925
3496
  await this.#failure(command, active, detail);
2926
3497
  return null;
2927
3498
  });
2928
- return { status: "running", message: resume ? "Theseus resumed from a portable checkpoint" : "Theseus started" };
3499
+ return {
3500
+ status: "running",
3501
+ message: resume ? "Theseus resumed from a portable checkpoint" : "Theseus started",
3502
+ ...sourceDigests ? { sourceDigests } : {}
3503
+ };
2929
3504
  }
2930
3505
  /**
2931
3506
  * Pursue a goal: attempt, judge with the clean verifier, re-prompt from what
@@ -3131,6 +3706,7 @@ export {
3131
3706
  verifyCodeCandidate,
3132
3707
  prepareRuntimeCheckpoint,
3133
3708
  CodeRuntimeCheckpointManager,
3709
+ materializeCodeRuntimeArchive,
3134
3710
  materializeCodeRuntimeSource,
3135
3711
  attachCodeRuntimeReferences,
3136
3712
  materializeCommandWorkspace,
@@ -3154,4 +3730,4 @@ export {
3154
3730
  runGoal,
3155
3731
  TheseusRuntimeEngine
3156
3732
  };
3157
- //# sourceMappingURL=chunk-5MDYIJXC.js.map
3733
+ //# sourceMappingURL=chunk-VIZALA6O.js.map