@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
@@ -2,8 +2,8 @@
2
2
  "use strict";
3
3
 
4
4
  // src/code-runtime-cli.ts
5
- var import_node_os3 = require("os");
6
- var import_promises10 = require("fs/promises");
5
+ var import_node_os4 = require("os");
6
+ var import_promises13 = require("fs/promises");
7
7
 
8
8
  // src/code-runtime-client-validation.ts
9
9
  var import_code = require("@odla-ai/camel/code");
@@ -249,6 +249,34 @@ function createCodeRuntimeControlClient(options) {
249
249
  }
250
250
  return value;
251
251
  };
252
+ const callRaw = async (path, body) => {
253
+ const timeout = AbortSignal.timeout(modelRequestTimeoutMs);
254
+ const signals = [options.signal, timeout].filter((item) => Boolean(item));
255
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
256
+ let response2;
257
+ try {
258
+ response2 = await request(`${endpoint}${path}`, {
259
+ method: "POST",
260
+ headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
261
+ body: JSON.stringify(body),
262
+ redirect: "error",
263
+ signal
264
+ });
265
+ } catch (cause) {
266
+ if (options.signal?.aborted) throw cause;
267
+ throw new CodeRuntimeControlError("Code source archive is unavailable", 503, "transport_unavailable");
268
+ }
269
+ if (!response2.ok) {
270
+ const value = await response2.json().catch(() => null);
271
+ const problem = record(record(value)?.error);
272
+ throw new CodeRuntimeControlError(
273
+ typeof problem?.message === "string" ? problem.message : `Code runtime request failed (${response2.status})`,
274
+ response2.status,
275
+ typeof problem?.code === "string" ? problem.code : void 0
276
+ );
277
+ }
278
+ return response2;
279
+ };
252
280
  return {
253
281
  heartbeat: async (version, capabilities) => {
254
282
  validateHeartbeat(version, capabilities);
@@ -260,6 +288,14 @@ function createCodeRuntimeControlClient(options) {
260
288
  source: async (sessionId) => parseSource(
261
289
  await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
262
290
  ),
291
+ sourceArchive: async (sessionId, alias) => {
292
+ if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias)) throw new TypeError("invalid Code source alias");
293
+ const response2 = await callRaw(
294
+ `/registry/code/runtime/sessions/${validSessionId(sessionId)}/source/archive`,
295
+ { alias }
296
+ );
297
+ return parseSourceArchiveResponse(response2, alias);
298
+ },
263
299
  infer: async (sessionId, inference) => {
264
300
  const value = record(await call(
265
301
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
@@ -324,9 +360,70 @@ function createCodeRuntimeControlClient(options) {
324
360
  }
325
361
  };
326
362
  }
363
+ var ARCHIVE_LIMIT_MAXIMA = {
364
+ maxCompressedBytes: 64 * 1024 * 1024,
365
+ maxDecompressedBytes: 96 * 1024 * 1024,
366
+ maxEntries: 2e5,
367
+ maxFiles: 1e5,
368
+ maxFileBytes: 16 * 1024 * 1024,
369
+ maxTotalFileBytes: 80 * 1024 * 1024,
370
+ maxPathBytes: 4096,
371
+ maxExtendedHeaderBytes: 32 * 1024
372
+ };
373
+ async function parseSourceArchiveResponse(response2, expectedAlias) {
374
+ const alias = response2.headers.get("x-odla-source-alias") ?? "";
375
+ const repository = response2.headers.get("x-odla-source-repository") ?? "";
376
+ const commitSha = response2.headers.get("x-odla-source-commit") ?? "";
377
+ if (alias !== expectedAlias || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || !/^[0-9a-f]{40}$/.test(commitSha)) {
378
+ await response2.body?.cancel().catch(() => void 0);
379
+ throw new CodeRuntimeControlError("invalid Code source archive identity", 502, "invalid_response");
380
+ }
381
+ const limits = Object.fromEntries(Object.entries(ARCHIVE_LIMIT_MAXIMA).map(([key, maximum]) => {
382
+ const header = `x-odla-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
383
+ const value = response2.headers.get(header) ?? "";
384
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1 || Number(value) > maximum) {
385
+ throw new CodeRuntimeControlError("invalid Code source archive limits", 502, "invalid_response");
386
+ }
387
+ return [key, Number(value)];
388
+ }));
389
+ const length = response2.headers.get("content-length");
390
+ if (length && (!/^\d+$/.test(length) || Number(length) > limits.maxCompressedBytes)) {
391
+ await response2.body?.cancel().catch(() => void 0);
392
+ throw new CodeRuntimeControlError("Code source archive exceeds its byte bound", 502, "invalid_response");
393
+ }
394
+ const compressed = await readBoundedResponse(response2.body, limits.maxCompressedBytes);
395
+ return { alias, repository, commitSha, compressed, limits };
396
+ }
397
+ async function readBoundedResponse(body, maximum) {
398
+ if (!body) throw new CodeRuntimeControlError("Code source archive body is missing", 502, "invalid_response");
399
+ const reader = body.getReader();
400
+ const chunks = [];
401
+ let size = 0;
402
+ try {
403
+ while (true) {
404
+ const { done, value } = await reader.read();
405
+ if (done) break;
406
+ size += value.byteLength;
407
+ if (size > maximum) {
408
+ await reader.cancel().catch(() => void 0);
409
+ throw new CodeRuntimeControlError("Code source archive exceeds its byte bound", 502, "invalid_response");
410
+ }
411
+ chunks.push(value);
412
+ }
413
+ } finally {
414
+ reader.releaseLock();
415
+ }
416
+ const result = new Uint8Array(size);
417
+ let offset = 0;
418
+ for (const chunk of chunks) {
419
+ result.set(chunk, offset);
420
+ offset += chunk.byteLength;
421
+ }
422
+ return result;
423
+ }
327
424
 
328
425
  // src/code-runtime.ts
329
- var CODE_RUNTIME_PROTOCOL_VERSION = 2;
426
+ var CODE_RUNTIME_PROTOCOL_VERSION = 3;
330
427
  async function runCodeRuntimeHeartbeatLoop(options) {
331
428
  const heartbeatMs = options.heartbeatMs ?? 15e3;
332
429
  if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
@@ -395,12 +492,12 @@ function retryableControlFailure(value) {
395
492
  return failure.status === 408 || failure.status === 425 || failure.status === 429 || failure.status >= 500;
396
493
  }
397
494
  function wait(ms, signal) {
398
- return new Promise((resolve6) => {
399
- if (signal?.aborted) return resolve6();
400
- const timer = setTimeout(resolve6, ms);
495
+ return new Promise((resolve7) => {
496
+ if (signal?.aborted) return resolve7();
497
+ const timer = setTimeout(resolve7, ms);
401
498
  signal?.addEventListener("abort", () => {
402
499
  clearTimeout(timer);
403
- resolve6();
500
+ resolve7();
404
501
  }, { once: true });
405
502
  });
406
503
  }
@@ -846,7 +943,7 @@ async function selectContainerEngine(requested = "auto", options = {}) {
846
943
  throw new TypeError("no supported container engine found");
847
944
  }
848
945
  function inspectRootlessPodman() {
849
- return new Promise((resolve6, reject) => {
946
+ return new Promise((resolve7, reject) => {
850
947
  (0, import_node_child_process3.execFile)(
851
948
  "podman",
852
949
  ["info", "--format", "{{.Host.Security.Rootless}}"],
@@ -856,7 +953,7 @@ function inspectRootlessPodman() {
856
953
  reject(new TypeError("could not verify that the active Podman service is rootless"));
857
954
  return;
858
955
  }
859
- resolve6(stdout.trim() === "true");
956
+ resolve7(stdout.trim() === "true");
860
957
  }
861
958
  );
862
959
  });
@@ -1363,7 +1460,8 @@ function codeCommandMetadata(payload, resume) {
1363
1460
  const repository = trusted?.repository;
1364
1461
  const baseCommitSha = trusted?.commitSha;
1365
1462
  const sourceTreeDigest = trusted?.treeDigest;
1366
- 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)) {
1463
+ const hostMaterialized = record2(payload.sourceSet) !== null && sourceTreeDigest === void 0;
1464
+ 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))) {
1367
1465
  throw new TypeError(`invalid Code ${resume ? "resume" : "start"} trusted base`);
1368
1466
  }
1369
1467
  if (!Number.isSafeInteger(maxTokensPerInteraction) || Number(maxTokensPerInteraction) < 4e3 || Number(maxTokensPerInteraction) > 2e5) {
@@ -1379,7 +1477,7 @@ function codeCommandMetadata(payload, resume) {
1379
1477
  attestationDigest: typeof attestation === "string" ? attestation : "resume",
1380
1478
  repository,
1381
1479
  baseCommitSha,
1382
- sourceTreeDigest
1480
+ sourceTreeDigest: typeof sourceTreeDigest === "string" ? sourceTreeDigest : null
1383
1481
  };
1384
1482
  }
1385
1483
  function codeLocalSource(payload) {
@@ -1444,19 +1542,320 @@ async function prepareRuntimeLocalSource(input) {
1444
1542
  }
1445
1543
 
1446
1544
  // src/code-runtime-source.ts
1545
+ var import_promises8 = require("fs/promises");
1546
+ var import_node_os3 = require("os");
1547
+ var import_node_path9 = require("path");
1548
+
1549
+ // src/code-runtime-archive.ts
1550
+ var import_node_zlib = require("zlib");
1447
1551
  var import_promises6 = require("fs/promises");
1448
1552
  var import_node_os2 = require("os");
1449
1553
  var import_node_path7 = require("path");
1450
1554
  var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
1451
1555
  var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
1556
+ var MAX_NUL_SHARE = 0.1;
1557
+ async function materializeCodeRuntimeArchive(archive, visiblePaths, tempRoot = (0, import_node_os2.tmpdir)()) {
1558
+ if (!archive.compressed.byteLength || archive.compressed.byteLength > archive.limits.maxCompressedBytes) {
1559
+ throw new TypeError("Code source archive exceeds its compressed byte bound");
1560
+ }
1561
+ let bytes;
1562
+ try {
1563
+ bytes = archive.compressed[0] === 31 && archive.compressed[1] === 139 ? (0, import_node_zlib.gunzipSync)(archive.compressed, { maxOutputLength: archive.limits.maxDecompressedBytes }) : archive.compressed;
1564
+ } catch {
1565
+ throw new TypeError("Code source archive is not a valid bounded gzip stream");
1566
+ }
1567
+ if (bytes.byteLength > archive.limits.maxDecompressedBytes) {
1568
+ throw new TypeError("Code source archive exceeds its decompressed byte bound");
1569
+ }
1570
+ const entries = parseTar(bytes, archive.limits);
1571
+ const root = await (0, import_promises6.mkdtemp)((0, import_node_path7.join)(tempRoot, "odla-code-archive-"));
1572
+ const sourceDir = (0, import_node_path7.join)(root, "source");
1573
+ await (0, import_promises6.mkdir)(sourceDir);
1574
+ let visible = 0;
1575
+ try {
1576
+ for (const entry of entries) {
1577
+ if (entry.directory || visiblePaths && !visiblePaths.has(entry.path)) continue;
1578
+ if (filteredPath(entry.path)) continue;
1579
+ let content;
1580
+ try {
1581
+ content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(entry.bytes);
1582
+ } catch {
1583
+ continue;
1584
+ }
1585
+ if (nulShare(content) > MAX_NUL_SHARE) continue;
1586
+ const target = (0, import_node_path7.resolve)(sourceDir, entry.path);
1587
+ if (!target.startsWith(`${(0, import_node_path7.resolve)(sourceDir)}${import_node_path7.sep}`)) throw new TypeError("Code source path escapes its root");
1588
+ await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
1589
+ await (0, import_promises6.writeFile)(target, content, { flag: "wx", mode: 420 });
1590
+ visible += 1;
1591
+ }
1592
+ if (!visible && !visiblePaths) throw new TypeError("GitHub commit has no Code-visible text source");
1593
+ return { sourceDir, cleanup: () => (0, import_promises6.rm)(root, { recursive: true, force: true }) };
1594
+ } catch (cause) {
1595
+ await (0, import_promises6.rm)(root, { recursive: true, force: true });
1596
+ throw cause;
1597
+ }
1598
+ }
1599
+ function parseTar(bytes, limits) {
1600
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
1601
+ const entries = [];
1602
+ const archivePaths = /* @__PURE__ */ new Set();
1603
+ let entryCount = 0;
1604
+ let fileCount = 0;
1605
+ let totalFileBytes = 0;
1606
+ let pendingPath;
1607
+ let pendingPaxPath;
1608
+ let offset = 0;
1609
+ let ended = false;
1610
+ while (offset + 512 <= bytes.byteLength) {
1611
+ const header = bytes.subarray(offset, offset + 512);
1612
+ offset += 512;
1613
+ if (zeroBlock(header)) {
1614
+ ended = true;
1615
+ assertZeroTail(bytes, offset);
1616
+ break;
1617
+ }
1618
+ entryCount += 1;
1619
+ if (entryCount > limits.maxEntries) throw new TypeError("Code source archive has too many entries");
1620
+ validateChecksum(header);
1621
+ const type = header[156] ?? 0;
1622
+ const size = tarNumber(header.subarray(124, 136));
1623
+ if (size > limits.maxDecompressedBytes || offset + aligned(size) > bytes.byteLength) {
1624
+ throw new TypeError("Code source archive entry is invalid or too large");
1625
+ }
1626
+ const payload = bytes.subarray(offset, offset + size);
1627
+ offset += aligned(size);
1628
+ if (type === 120 || type === 103) {
1629
+ if (size > limits.maxExtendedHeaderBytes) throw new TypeError("Code source archive extended header is too large");
1630
+ const pax = parsePax(payload, decoder, limits.maxPathBytes);
1631
+ if (pax.linkpath !== void 0 || pax.size !== void 0 || Object.keys(pax).some((key) => key.toLowerCase().includes("sparse"))) {
1632
+ throw new TypeError("Code source archive contains an unsupported entry");
1633
+ }
1634
+ if (type === 103 && pax.path !== void 0) throw new TypeError("Code source archive path is unsafe");
1635
+ if (type === 120 && pax.path !== void 0) {
1636
+ if (pendingPaxPath !== void 0 || pendingPath !== void 0) throw new TypeError("Code source archive is invalid");
1637
+ pendingPaxPath = pax.path;
1638
+ }
1639
+ continue;
1640
+ }
1641
+ if (type === 76) {
1642
+ if (size > limits.maxExtendedHeaderBytes || pendingPath !== void 0 || pendingPaxPath !== void 0) {
1643
+ throw new TypeError("Code source archive long path is invalid");
1644
+ }
1645
+ pendingPath = validateArchivePath(decode(payload, decoder).replace(/\0+$/, "").replace(/\n$/, ""), limits.maxPathBytes);
1646
+ continue;
1647
+ }
1648
+ if (type === 75) throw new TypeError("Code source archive contains an unsupported link");
1649
+ const path = validateArchivePath(pendingPaxPath ?? pendingPath ?? tarPath(header, decoder), limits.maxPathBytes);
1650
+ pendingPaxPath = void 0;
1651
+ pendingPath = void 0;
1652
+ const directory = type === 53;
1653
+ const regular = type === 0 || type === 48;
1654
+ if (!directory && !regular || directory && size !== 0 || archivePaths.has(path)) {
1655
+ throw new TypeError("Code source archive contains an invalid or duplicate entry");
1656
+ }
1657
+ archivePaths.add(path);
1658
+ if (regular) {
1659
+ fileCount += 1;
1660
+ totalFileBytes += size;
1661
+ if (fileCount > limits.maxFiles || size > limits.maxFileBytes || totalFileBytes > limits.maxTotalFileBytes) {
1662
+ throw new TypeError("Code source archive exceeds its file bounds");
1663
+ }
1664
+ }
1665
+ entries.push({ path, directory, ...regular ? { bytes: payload } : {} });
1666
+ }
1667
+ if (!ended || pendingPath !== void 0 || pendingPaxPath !== void 0 || !entries.length || !fileCount) {
1668
+ throw new TypeError("Code source archive is incomplete");
1669
+ }
1670
+ return unwrapRepository(entries, limits.maxPathBytes);
1671
+ }
1672
+ function unwrapRepository(entries, maxPathBytes) {
1673
+ const root = entries[0].path.split("/")[0];
1674
+ if (!root || entries.some((entry) => entry.path !== root && !entry.path.startsWith(`${root}/`))) {
1675
+ throw new TypeError("Code source archive has no single repository root");
1676
+ }
1677
+ const output = [];
1678
+ const kinds = /* @__PURE__ */ new Map();
1679
+ for (const entry of entries) {
1680
+ if (entry.path === root) {
1681
+ if (!entry.directory) throw new TypeError("Code source archive root is not a directory");
1682
+ continue;
1683
+ }
1684
+ const path = entry.path.slice(root.length + 1);
1685
+ validateRepositoryPath(path, maxPathBytes);
1686
+ const parts = path.split("/");
1687
+ for (let index = 1; index < parts.length; index += 1) {
1688
+ if (kinds.get(parts.slice(0, index).join("/")) === "file") throw new TypeError("Code source archive path conflicts");
1689
+ }
1690
+ if (!entry.directory) {
1691
+ for (const existing of kinds.keys()) {
1692
+ if (existing.startsWith(`${path}/`)) throw new TypeError("Code source archive path conflicts");
1693
+ }
1694
+ }
1695
+ if (kinds.has(path)) throw new TypeError("Code source archive repeats a path");
1696
+ kinds.set(path, entry.directory ? "directory" : "file");
1697
+ output.push({ ...entry, path });
1698
+ }
1699
+ return output.sort((left, right) => left.path.localeCompare(right.path));
1700
+ }
1701
+ function parsePax(bytes, decoder, maxPathBytes) {
1702
+ const result = {};
1703
+ let offset = 0;
1704
+ while (offset < bytes.byteLength) {
1705
+ const space = bytes.indexOf(32, offset);
1706
+ if (space < 0) throw new TypeError("Code source archive PAX header is invalid");
1707
+ const lengthText = ascii(bytes.subarray(offset, space));
1708
+ if (!/^[1-9][0-9]{0,8}$/.test(lengthText)) throw new TypeError("Code source archive PAX length is invalid");
1709
+ const length = Number(lengthText);
1710
+ const end = offset + length;
1711
+ if (!Number.isSafeInteger(length) || end > bytes.byteLength || bytes[end - 1] !== 10) {
1712
+ throw new TypeError("Code source archive PAX record is invalid");
1713
+ }
1714
+ const record4 = decode(bytes.subarray(space + 1, end - 1), decoder);
1715
+ const equals = record4.indexOf("=");
1716
+ if (equals < 1) throw new TypeError("Code source archive PAX field is invalid");
1717
+ const key = record4.slice(0, equals);
1718
+ if (Object.hasOwn(result, key)) throw new TypeError("Code source archive PAX field repeats");
1719
+ result[key] = record4.slice(equals + 1);
1720
+ offset = end;
1721
+ }
1722
+ if (result.path !== void 0) result.path = validateArchivePath(result.path, maxPathBytes);
1723
+ return result;
1724
+ }
1725
+ var aligned = (size) => Math.ceil(size / 512) * 512;
1726
+ var zeroBlock = (block) => block.every((value) => value === 0);
1727
+ function validateChecksum(header) {
1728
+ const expected = tarNumber(header.subarray(148, 156));
1729
+ let actual = 0;
1730
+ for (let index = 0; index < header.length; index += 1) actual += index >= 148 && index < 156 ? 32 : header[index];
1731
+ if (actual !== expected) throw new TypeError("Code source archive checksum is invalid");
1732
+ }
1733
+ function tarNumber(field) {
1734
+ if ((field[0] ?? 0) & 128) throw new TypeError("Code source archive numeric format is unsupported");
1735
+ const value = ascii(field).replaceAll("\0", "").trim();
1736
+ if (!value) return 0;
1737
+ if (!/^[0-7]+$/.test(value)) throw new TypeError("Code source archive number is invalid");
1738
+ const parsed = Number.parseInt(value, 8);
1739
+ if (!Number.isSafeInteger(parsed) || parsed < 0) throw new TypeError("Code source archive number is too large");
1740
+ return parsed;
1741
+ }
1742
+ function tarPath(header, decoder) {
1743
+ const name = decodeNul(header.subarray(0, 100), decoder);
1744
+ const prefix = decodeNul(header.subarray(345, 500), decoder);
1745
+ return prefix ? `${prefix}/${name}` : name;
1746
+ }
1747
+ function decodeNul(value, decoder) {
1748
+ const end = value.indexOf(0);
1749
+ return decode(end < 0 ? value : value.subarray(0, end), decoder);
1750
+ }
1751
+ function decode(value, decoder) {
1752
+ try {
1753
+ return decoder.decode(value);
1754
+ } catch {
1755
+ throw new TypeError("Code source archive text is invalid UTF-8");
1756
+ }
1757
+ }
1758
+ function ascii(value) {
1759
+ let output = "";
1760
+ for (const byte of value) output += String.fromCharCode(byte);
1761
+ return output;
1762
+ }
1763
+ function validateArchivePath(input, maximum) {
1764
+ const path = input.endsWith("/") ? input.slice(0, -1) : input;
1765
+ validateRepositoryPath(path, maximum);
1766
+ return path;
1767
+ }
1768
+ function validateRepositoryPath(path, maximum) {
1769
+ if (!path || path.startsWith("/") || path.includes("\\") || /[\u0000-\u001f\u007f]/.test(path) || new TextEncoder().encode(path).byteLength > maximum || path.split("/").some((part) => !part || part === "." || part === "..")) {
1770
+ throw new TypeError("Code source archive path is unsafe");
1771
+ }
1772
+ }
1773
+ function assertZeroTail(bytes, offset) {
1774
+ for (let index = offset; index < bytes.byteLength; index += 1) {
1775
+ if (bytes[index] !== 0) throw new TypeError("Code source archive has data after its end marker");
1776
+ }
1777
+ }
1778
+ function filteredPath(path) {
1779
+ const parts = path.split("/");
1780
+ return parts.some((part) => RESERVED2.has(part) || SECRET2.test(part));
1781
+ }
1782
+ function nulShare(content) {
1783
+ if (!content.length) return 0;
1784
+ let count = 0;
1785
+ for (let index = 0; index < content.length; index += 1) if (content.charCodeAt(index) === 0) count += 1;
1786
+ return count / content.length;
1787
+ }
1788
+
1789
+ // src/code-runtime-selected-source.ts
1790
+ var import_promises7 = require("fs/promises");
1791
+ var import_node_path8 = require("path");
1792
+ function selectedSourceSet(payload) {
1793
+ if (!payload.sourceSet) return null;
1794
+ const set = payload.sourceSet && typeof payload.sourceSet === "object" && !Array.isArray(payload.sourceSet) ? payload.sourceSet : null;
1795
+ if (!set) throw new TypeError("Code selected source set is invalid");
1796
+ const parse2 = (value, primary2) => {
1797
+ const item = value && typeof value === "object" && !Array.isArray(value) ? value : null;
1798
+ const alias = item?.alias;
1799
+ const repository = item?.repository;
1800
+ const commitSha = item?.commitSha;
1801
+ const materialization = item?.materialization ?? "registry_snapshot";
1802
+ 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))) {
1803
+ throw new TypeError("Code selected source identity is invalid");
1804
+ }
1805
+ const visiblePaths = item?.visiblePaths;
1806
+ if (visiblePaths !== void 0 && (!Array.isArray(visiblePaths) || visiblePaths.length > 2e3 || visiblePaths.some((path) => typeof path !== "string" || path.length > 4096))) {
1807
+ throw new TypeError("Code selected source visibility slice is invalid");
1808
+ }
1809
+ return {
1810
+ alias,
1811
+ repository,
1812
+ commitSha,
1813
+ materialization,
1814
+ ...visiblePaths ? { visiblePaths } : {}
1815
+ };
1816
+ };
1817
+ if (!Array.isArray(set.references)) throw new TypeError("Code selected source references are invalid");
1818
+ const primary = parse2(set.primary, true);
1819
+ const references = set.references.map((item) => parse2(item, false));
1820
+ const aliases = /* @__PURE__ */ new Set([primary.alias, ...references.map((item) => item.alias)]);
1821
+ if (aliases.size !== references.length + 1) throw new TypeError("Code selected source aliases repeat");
1822
+ return { primary, references };
1823
+ }
1824
+ async function attachReferenceDirectories(workspace, references) {
1825
+ for (const reference of references) {
1826
+ validateAlias(reference.alias);
1827
+ for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
1828
+ const target = (0, import_node_path8.join)(root, ".odla-references", reference.alias);
1829
+ await (0, import_promises7.mkdir)((0, import_node_path8.dirname)(target), { recursive: true });
1830
+ await (0, import_promises7.cp)(reference.sourceDir, target, { recursive: true, errorOnExist: true, force: false });
1831
+ await makeTreeReadOnly(target);
1832
+ }
1833
+ }
1834
+ }
1835
+ function validateAlias(alias) {
1836
+ if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
1837
+ throw new TypeError("Code reference alias is invalid");
1838
+ }
1839
+ }
1840
+ async function makeTreeReadOnly(root) {
1841
+ for (const entry of await (0, import_promises7.readdir)(root, { withFileTypes: true })) {
1842
+ const target = (0, import_node_path8.join)(root, entry.name);
1843
+ if (entry.isDirectory()) await makeTreeReadOnly(target);
1844
+ else if (entry.isFile()) await (0, import_promises7.chmod)(target, 292);
1845
+ }
1846
+ }
1847
+
1848
+ // src/code-runtime-source.ts
1849
+ var RESERVED3 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
1850
+ var SECRET3 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
1452
1851
  var SOURCE_MAX_FILES = 1e5;
1453
1852
  var SOURCE_MAX_BYTES = 80 * 1024 * 1024;
1454
1853
  var SOURCE_SET_MAX_BYTES = 480 * 1024 * 1024;
1455
- async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_node_os2.tmpdir)()) {
1854
+ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_node_os3.tmpdir)()) {
1456
1855
  if (!snapshot.files.length || snapshot.files.length > SOURCE_MAX_FILES) throw new TypeError("Code source file count is invalid");
1457
- const root = await (0, import_promises6.mkdtemp)((0, import_node_path7.join)(tempRoot, "odla-code-source-"));
1458
- const sourceDir = (0, import_node_path7.join)(root, "source");
1459
- await (0, import_promises6.mkdir)(sourceDir);
1856
+ const root = await (0, import_promises8.mkdtemp)((0, import_node_path9.join)(tempRoot, "odla-code-source-"));
1857
+ const sourceDir = (0, import_node_path9.join)(root, "source");
1858
+ await (0, import_promises8.mkdir)(sourceDir);
1460
1859
  const seen = /* @__PURE__ */ new Set();
1461
1860
  let bytes = 0;
1462
1861
  try {
@@ -1466,13 +1865,13 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_node
1466
1865
  seen.add(file.path);
1467
1866
  bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
1468
1867
  if (bytes > SOURCE_MAX_BYTES) throw new TypeError("Code source exceeds its byte bound");
1469
- const target = (0, import_node_path7.resolve)(sourceDir, file.path);
1470
- if (!target.startsWith(`${(0, import_node_path7.resolve)(sourceDir)}${import_node_path7.sep}`)) throw new TypeError("Code source path escapes its root");
1471
- await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
1472
- await (0, import_promises6.writeFile)(target, file.content, { flag: "wx", mode: 420 });
1868
+ const target = (0, import_node_path9.resolve)(sourceDir, file.path);
1869
+ if (!target.startsWith(`${(0, import_node_path9.resolve)(sourceDir)}${import_node_path9.sep}`)) throw new TypeError("Code source path escapes its root");
1870
+ await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
1871
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
1473
1872
  }
1474
1873
  for (const reference of snapshot.references ?? []) {
1475
- validateAlias(reference.alias);
1874
+ validateAlias2(reference.alias);
1476
1875
  if (!reference.files.length || reference.files.length > SOURCE_MAX_FILES) throw new TypeError("Code reference file count is invalid");
1477
1876
  for (const file of reference.files) {
1478
1877
  validatePath(file.path);
@@ -1481,19 +1880,19 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_node
1481
1880
  seen.add(path);
1482
1881
  bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
1483
1882
  if (bytes > SOURCE_SET_MAX_BYTES) throw new TypeError("Code source set exceeds its byte bound");
1484
- const target = (0, import_node_path7.resolve)(sourceDir, path);
1485
- if (!target.startsWith(`${(0, import_node_path7.resolve)(sourceDir)}${import_node_path7.sep}`)) throw new TypeError("Code reference path escapes its root");
1486
- await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
1487
- await (0, import_promises6.writeFile)(target, file.content, { flag: "wx", mode: 292 });
1883
+ const target = (0, import_node_path9.resolve)(sourceDir, path);
1884
+ if (!target.startsWith(`${(0, import_node_path9.resolve)(sourceDir)}${import_node_path9.sep}`)) throw new TypeError("Code reference path escapes its root");
1885
+ await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
1886
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
1488
1887
  }
1489
1888
  }
1490
- return { sourceDir, cleanup: () => (0, import_promises6.rm)(root, { recursive: true, force: true }) };
1889
+ return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
1491
1890
  } catch (cause) {
1492
- await (0, import_promises6.rm)(root, { recursive: true, force: true });
1891
+ await (0, import_promises8.rm)(root, { recursive: true, force: true });
1493
1892
  throw cause;
1494
1893
  }
1495
1894
  }
1496
- function validateAlias(alias) {
1895
+ function validateAlias2(alias) {
1497
1896
  if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
1498
1897
  throw new TypeError("Code reference alias is invalid");
1499
1898
  }
@@ -1501,24 +1900,24 @@ function validateAlias(alias) {
1501
1900
  async function attachCodeRuntimeReferences(workspace, references) {
1502
1901
  let bytes = 0;
1503
1902
  for (const reference of references) {
1504
- validateAlias(reference.alias);
1903
+ validateAlias2(reference.alias);
1505
1904
  for (const file of reference.files) {
1506
1905
  validatePath(file.path);
1507
1906
  const path = `.odla-references/${reference.alias}/${file.path}`;
1508
1907
  bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
1509
1908
  if (bytes > SOURCE_SET_MAX_BYTES - SOURCE_MAX_BYTES) throw new TypeError("Code reference set exceeds its byte bound");
1510
1909
  for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
1511
- const target = (0, import_node_path7.resolve)(root, path);
1512
- if (!target.startsWith(`${(0, import_node_path7.resolve)(root)}${import_node_path7.sep}`)) throw new TypeError("Code reference path escapes its root");
1513
- await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
1514
- await (0, import_promises6.writeFile)(target, file.content, { flag: "wx", mode: 292 });
1910
+ const target = (0, import_node_path9.resolve)(root, path);
1911
+ if (!target.startsWith(`${(0, import_node_path9.resolve)(root)}${import_node_path9.sep}`)) throw new TypeError("Code reference path escapes its root");
1912
+ await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
1913
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
1515
1914
  }
1516
1915
  }
1517
1916
  }
1518
1917
  }
1519
1918
  function validatePath(path) {
1520
1919
  const parts = path.split("/");
1521
- if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED2.has(part) || SECRET2.test(part))) {
1920
+ if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED3.has(part) || SECRET3.test(part))) {
1522
1921
  throw new TypeError("Code source contains an unsafe path");
1523
1922
  }
1524
1923
  }
@@ -1546,12 +1945,12 @@ async function materializeCommandWorkspace(input) {
1546
1945
  throw new TypeError("Code selected source set is invalid");
1547
1946
  }
1548
1947
  if (references.length) {
1549
- const selected = await input.control.source(command.sessionId);
1550
- if (selected.repository !== metadata.repository || selected.commitSha !== metadata.baseCommitSha || selected.treeDigest !== metadata.sourceTreeDigest) {
1948
+ const selected2 = await input.control.source(command.sessionId);
1949
+ if (selected2.repository !== metadata.repository || selected2.commitSha !== metadata.baseCommitSha || selected2.treeDigest !== metadata.sourceTreeDigest) {
1551
1950
  await prepared.workspace.cleanup();
1552
1951
  throw new TypeError("Code local source does not match the selected GitHub primary source");
1553
1952
  }
1554
- await attachCodeRuntimeReferences(prepared.workspace, selected.references ?? []);
1953
+ await attachCodeRuntimeReferences(prepared.workspace, selected2.references ?? []);
1555
1954
  }
1556
1955
  }
1557
1956
  return {
@@ -1561,6 +1960,63 @@ async function materializeCommandWorkspace(input) {
1561
1960
  requestedLocal
1562
1961
  };
1563
1962
  }
1963
+ const selected = selectedSourceSet(command.payload);
1964
+ if (selected?.primary.materialization === "host_archive") {
1965
+ if (!input.control.sourceArchive) {
1966
+ throw new TypeError("Code runtime protocol does not support host source materialization");
1967
+ }
1968
+ const sources = [selected.primary, ...selected.references];
1969
+ const materialized2 = [];
1970
+ try {
1971
+ for (const descriptor2 of sources) {
1972
+ if (descriptor2.materialization !== "host_archive") {
1973
+ throw new TypeError("Code selected source set mixes incompatible materialization modes");
1974
+ }
1975
+ const archive = await input.control.sourceArchive(command.sessionId, descriptor2.alias);
1976
+ if (archive.repository !== descriptor2.repository || archive.commitSha !== descriptor2.commitSha) {
1977
+ throw new TypeError("Code source archive does not match its selected repository and commit");
1978
+ }
1979
+ const source2 = await materializeCodeRuntimeArchive(
1980
+ archive,
1981
+ descriptor2.visiblePaths ? new Set(descriptor2.visiblePaths) : void 0
1982
+ );
1983
+ const treeDigest = await digestStagedWorkspace(source2.sourceDir, {
1984
+ maxFiles: archive.limits.maxFiles,
1985
+ maxBytes: archive.limits.maxTotalFileBytes
1986
+ });
1987
+ materialized2.push({ descriptor: descriptor2, source: source2, treeDigest });
1988
+ }
1989
+ const primary = materialized2[0];
1990
+ const workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
1991
+ trustedBaseDir: primary.source.sourceDir,
1992
+ trustedBaseCommitSha: primary.descriptor.commitSha,
1993
+ checkpoint: codeCheckpointPayload(command.payload)
1994
+ })).workspace : await stageWorkspace(primary.source.sourceDir, {
1995
+ maxFiles: SOURCE_MAX_FILES,
1996
+ maxBytes: SOURCE_SET_MAX_BYTES
1997
+ });
1998
+ try {
1999
+ await attachReferenceDirectories(workspace, materialized2.slice(1).map((item) => ({
2000
+ alias: item.descriptor.alias,
2001
+ sourceDir: item.source.sourceDir
2002
+ })));
2003
+ } catch (cause) {
2004
+ await workspace.cleanup();
2005
+ throw cause;
2006
+ }
2007
+ return {
2008
+ workspace,
2009
+ sourceDigest: primary.treeDigest,
2010
+ requestedLocal: null,
2011
+ sourceDigests: materialized2.map((item) => ({
2012
+ alias: item.descriptor.alias,
2013
+ treeDigest: item.treeDigest
2014
+ }))
2015
+ };
2016
+ } finally {
2017
+ await Promise.allSettled(materialized2.map((item) => item.source.cleanup()));
2018
+ }
2019
+ }
1564
2020
  const source = await input.control.source(command.sessionId);
1565
2021
  const materialized = await materializeCodeRuntimeSource(source);
1566
2022
  try {
@@ -1615,10 +2071,12 @@ Prefer these over listing the tree \u2014 a full listing of a real repository is
1615
2071
  of thousands of tokens and you will carry it for the rest of the session.
1616
2072
 
1617
2073
  Then odla_search for a literal string, odla_read for a bounded range, and
1618
- odla_apply_git_diff to change something. A patch must start with
1619
- "diff --git a/<path> b/<path>", include matching "---" and "+++" headers and
1620
- numbered "@@" hunks with at least one line of surrounding context, and must never
1621
- use "*** Begin Patch" wrappers.
2074
+ odla_edit_file to change an existing file: give it the exact current text
2075
+ (copied verbatim from odla_read) and the text that replaces it; it must match
2076
+ once. Use odla_apply_git_diff only to create or delete whole files. A patch must
2077
+ start with "diff --git a/<path> b/<path>", include matching "---" and "+++"
2078
+ headers and numbered "@@" hunks with at least one line of surrounding context,
2079
+ and must never use "*** Begin Patch" wrappers.
1622
2080
 
1623
2081
  The workspace, model, and tool effects are controlled by the host broker.
1624
2082
  Never claim a build or test passed unless odla_run_recipe returned that result.`;
@@ -1678,6 +2136,21 @@ function codeSkill(opts) {
1678
2136
  },
1679
2137
  handler: (input, ctx) => call("sandbox.apply_patch", input, ctx.signal)
1680
2138
  };
2139
+ const editFile = {
2140
+ name: "odla_edit_file",
2141
+ 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.",
2142
+ inputSchema: {
2143
+ type: "object",
2144
+ required: ["path", "oldText", "newText"],
2145
+ properties: {
2146
+ path: { type: "string", minLength: 1, maxLength: 1024 },
2147
+ oldText: { type: "string", minLength: 1, maxLength: 262144 },
2148
+ newText: { type: "string", maxLength: 262144 }
2149
+ },
2150
+ additionalProperties: false
2151
+ },
2152
+ handler: (input, ctx) => call("sandbox.edit", input, ctx.signal)
2153
+ };
1681
2154
  const runRecipe = {
1682
2155
  name: "odla_run_recipe",
1683
2156
  description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
@@ -1765,7 +2238,7 @@ function codeSkill(opts) {
1765
2238
  )
1766
2239
  ];
1767
2240
  const effects = opts.readOnly ? [] : [applyPatch, runRecipe];
1768
- const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2241
+ const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
1769
2242
  return { name: "code", tools };
1770
2243
  }
1771
2244
 
@@ -1898,14 +2371,40 @@ async function sessionSkillsFor(options, command) {
1898
2371
  }
1899
2372
 
1900
2373
  // src/code-runtime-inference.ts
2374
+ var OVERLOAD_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3];
2375
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["control_plane_overloaded", "registry_overloaded", "transport_unavailable"]);
2376
+ function overloadedControlFailure(cause) {
2377
+ return cause instanceof CodeRuntimeControlError && cause.status === 503 && RETRYABLE_CODES.has(cause.code);
2378
+ }
2379
+ async function inferWithBackoff(infer, wait2, onRetry) {
2380
+ for (let attempt = 0; ; attempt += 1) {
2381
+ try {
2382
+ return await infer();
2383
+ } catch (cause) {
2384
+ const delayMs = OVERLOAD_RETRY_DELAYS_MS[attempt];
2385
+ if (delayMs === void 0 || !overloadedControlFailure(cause)) throw cause;
2386
+ await onRetry(cause, delayMs);
2387
+ await wait2(delayMs);
2388
+ }
2389
+ }
2390
+ }
1901
2391
  async function handleCodeRuntimeInference(input) {
1902
2392
  const { command, request, state } = input;
1903
2393
  const startedAt = Date.now();
1904
- const response2 = await input.control.infer(command.sessionId, {
1905
- requestId: request.requestId,
1906
- interactionId: command.commandId,
1907
- call: request.call
1908
- });
2394
+ const wait2 = input.wait ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
2395
+ const response2 = await inferWithBackoff(
2396
+ () => input.control.infer(command.sessionId, {
2397
+ requestId: request.requestId,
2398
+ interactionId: command.commandId,
2399
+ call: request.call
2400
+ }),
2401
+ wait2,
2402
+ (cause, delayMs) => input.event({
2403
+ type: "diagnostic",
2404
+ level: "error",
2405
+ message: `Code control plane overloaded (${cause.code}); retrying the model call in ${delayMs / 1e3}s`
2406
+ }).catch(() => void 0)
2407
+ );
1909
2408
  state.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
1910
2409
  const { costUsd } = response2.receipt;
1911
2410
  if (costUsd === void 0) state.costKnown = false;
@@ -2029,6 +2528,10 @@ function codeToolRequestPresentation(request) {
2029
2528
  if (request.tool === "sandbox.apply_patch") {
2030
2529
  return { kind: "patch", ...patchStats(input.patch) };
2031
2530
  }
2531
+ if (request.tool === "sandbox.edit") {
2532
+ const lines = (value) => typeof value === "string" ? value.slice(0, 262144).split("\n").length : 0;
2533
+ return { kind: "patch", additions: lines(input.newText), deletions: lines(input.oldText) };
2534
+ }
2032
2535
  const recipeId = text(input.recipeId, 120);
2033
2536
  return recipeId ? { kind: "recipe", recipeId } : void 0;
2034
2537
  }
@@ -2156,6 +2659,11 @@ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
2156
2659
  authority: "authority",
2157
2660
  patch: "payload"
2158
2661
  });
2662
+ var EDIT = descriptor("sandbox.edit", "reversible_mutation", {
2663
+ workspace: "destination",
2664
+ authority: "authority",
2665
+ patch: "payload"
2666
+ });
2159
2667
  var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
2160
2668
  workspace: "destination",
2161
2669
  authority: "authority",
@@ -2223,6 +2731,14 @@ function createCodePolicyGate(options) {
2223
2731
  patch: { role: "payload", value: patch2 }
2224
2732
  }, []);
2225
2733
  },
2734
+ edit: async (input) => {
2735
+ const base = await environment(input, options, "sandbox.edit");
2736
+ const patch2 = unsafe(base, input.patch, "patch");
2737
+ return authorize(input, options, base, EDIT, {
2738
+ ...base.fixedArgs,
2739
+ patch: { role: "payload", value: patch2 }
2740
+ }, []);
2741
+ },
2226
2742
  recipe: async (input) => {
2227
2743
  const base = await environment(input, options, "sandbox.run_recipe");
2228
2744
  const conversions = await conversionRegistry([
@@ -2367,13 +2883,134 @@ function response(request, ok, content, details) {
2367
2883
  return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
2368
2884
  }
2369
2885
 
2370
- // src/code-tool-reads.ts
2886
+ // src/code-tool-edit.ts
2887
+ var import_promises10 = require("fs/promises");
2888
+
2889
+ // src/code-tool-graph.ts
2371
2890
  var import_promises9 = require("fs/promises");
2891
+ var import_node_path10 = require("path");
2892
+ var import_graph = require("@odla-ai/graph");
2893
+ var import_code4 = require("@odla-ai/graph/code");
2894
+ var cache = /* @__PURE__ */ new Map();
2895
+ function workspaceGraphs(workspaceDir, paths2) {
2896
+ const existing = cache.get(workspaceDir);
2897
+ if (existing) return existing;
2898
+ const read2 = (path) => (0, import_promises9.readFile)((0, import_node_path10.join)(workspaceDir, path), "utf8");
2899
+ const built = (async () => ({
2900
+ // No knownTables: a staged workspace may not carry migrations, and a filter
2901
+ // that silently drops every table is worse than an unfiltered one. Callers
2902
+ // with ground truth should build the graph themselves.
2903
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2904
+ }))();
2905
+ cache.set(workspaceDir, built);
2906
+ return built;
2907
+ }
2908
+ function forgetWorkspaceGraphs(workspaceDir) {
2909
+ cache.delete(workspaceDir);
2910
+ }
2911
+ var shortId = (id) => id.slice(id.indexOf(":") + 1);
2912
+ function renderOverview(graphs, prefix) {
2913
+ const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
2914
+ if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
2915
+ const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
2916
+ const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
2917
+ return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
2918
+ }
2919
+ function renderWhereIs(graphs, symbol) {
2920
+ const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
2921
+ path: shortId(id),
2922
+ pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
2923
+ dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
2924
+ })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
2925
+ if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
2926
+ return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
2927
+ }
2928
+ function renderWhoImports(graphs, path) {
2929
+ const id = (0, import_graph.nodeId)(import_code4.FILE, path);
2930
+ const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
2931
+ if (importers.length === 0) {
2932
+ return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
2933
+ }
2934
+ return importers.slice(0, 40).map(shortId).sort().join("\n");
2935
+ }
2936
+ function renderWhoTouches(graphs, query) {
2937
+ const needle = query.toLowerCase();
2938
+ const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
2939
+ if (hits.length === 0) return `No table or namespace matching "${query}".`;
2940
+ return hits.map((hit) => {
2941
+ const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
2942
+ return [
2943
+ `${hit.name} (${hit.kind})`,
2944
+ ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
2945
+ ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
2946
+ ].join("\n");
2947
+ }).join("\n\n");
2948
+ }
2949
+
2950
+ // src/code-tool-edit.ts
2951
+ var MAX_TEXT_BYTES = 256 * 1024;
2952
+ function editAsDiff(path, oldText, newText) {
2953
+ const removed = oldText.split("\n").map((line) => `-${line}`);
2954
+ const added = newText.split("\n").map((line) => `+${line}`);
2955
+ return `diff --git a/${path} b/${path}
2956
+ --- a/${path}
2957
+ +++ b/${path}
2958
+ @@ -1,${removed.length} +1,${added.length} @@
2959
+ ${[...removed, ...added].join("\n")}
2960
+ `;
2961
+ }
2962
+ function occurrences(haystack, needle) {
2963
+ let count = 0;
2964
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count += 1;
2965
+ return count;
2966
+ }
2967
+ async function editCodeFile(workspaceDir, path, oldText, newText) {
2968
+ if (!oldText) throw new TypeError("oldText must not be empty; to create a file use sandbox.apply_patch with a new-file diff");
2969
+ if (Buffer.byteLength(oldText) > MAX_TEXT_BYTES || Buffer.byteLength(newText) > MAX_TEXT_BYTES) {
2970
+ throw new TypeError(`oldText and newText must each stay under ${MAX_TEXT_BYTES} bytes; split the change`);
2971
+ }
2972
+ if (oldText.includes("\0") || newText.includes("\0")) throw new TypeError("edit text contains NUL bytes; use plain text");
2973
+ const target = resolveCodePath(workspaceDir, path);
2974
+ const current = await (0, import_promises10.readFile)(target, "utf8");
2975
+ const found = occurrences(current, oldText);
2976
+ if (found === 0) {
2977
+ throw new TypeError(`oldText was not found in "${path}"; sandbox.read the current lines and copy them exactly, including indentation and blank lines`);
2978
+ }
2979
+ if (found > 1) {
2980
+ throw new TypeError(`oldText occurs ${found} times in "${path}"; include more of the surrounding lines so it matches exactly once`);
2981
+ }
2982
+ const at = current.indexOf(oldText);
2983
+ await (0, import_promises10.writeFile)(target, `${current.slice(0, at)}${newText}${current.slice(at + oldText.length)}`, "utf8");
2984
+ return { deletions: oldText.split("\n").length, additions: newText.split("\n").length };
2985
+ }
2986
+ async function edit(context, request, options, policy, registry) {
2987
+ exactKeys(request.input, ["path", "oldText", "newText"]);
2988
+ const path = stringField(request.input, "path");
2989
+ const oldText = stringField(request.input, "oldText");
2990
+ const newText = typeof request.input.newText === "string" ? request.input.newText : stringField(request.input, "newText");
2991
+ const paths2 = await registry.files(context.workspaceDir);
2992
+ if (!paths2.includes(path)) {
2993
+ 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.`);
2994
+ }
2995
+ if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
2996
+ throw new TypeError("edit targets a read-only reference source");
2997
+ }
2998
+ const patch2 = editAsDiff(path, oldText, newText);
2999
+ const allowed = await policy.edit(policyContext(context, request, options, { patch: patch2 }));
3000
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3001
+ const changed = await editCodeFile(context.workspaceDir, path, oldText, newText);
3002
+ registry.invalidate(context.workspaceDir);
3003
+ forgetWorkspaceGraphs(context.workspaceDir);
3004
+ return response(request, true, `Edited ${path}: -${changed.deletions} +${changed.additions} line(s).`, { paths: [path], ...changed });
3005
+ }
3006
+
3007
+ // src/code-tool-reads.ts
3008
+ var import_promises12 = require("fs/promises");
2372
3009
 
2373
3010
  // src/code-tool-discovery.ts
2374
3011
  var import_node_child_process5 = require("child_process");
2375
- var import_promises7 = require("fs/promises");
2376
- var import_node_path8 = require("path");
3012
+ var import_promises11 = require("fs/promises");
3013
+ var import_node_path11 = require("path");
2377
3014
  var DEFAULT_MAX_FILES = 2e4;
2378
3015
  var DEFAULT_MAX_RESULTS = 100;
2379
3016
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
@@ -2398,13 +3035,13 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
2398
3035
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2399
3036
  const paths2 = [];
2400
3037
  const walk = async (directory) => {
2401
- for (const entry of await (0, import_promises7.readdir)(directory, { withFileTypes: true })) {
3038
+ for (const entry of await (0, import_promises11.readdir)(directory, { withFileTypes: true })) {
2402
3039
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
2403
3040
  if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
2404
- const target = (0, import_node_path8.resolve)(directory, entry.name);
3041
+ const target = (0, import_node_path11.resolve)(directory, entry.name);
2405
3042
  if (entry.isDirectory()) await walk(target);
2406
3043
  else if (entry.isFile()) {
2407
- const path = (0, import_node_path8.relative)(root, target).split("\\").join("/");
3044
+ const path = (0, import_node_path11.relative)(root, target).split("\\").join("/");
2408
3045
  try {
2409
3046
  validateRelativePath(path);
2410
3047
  } catch {
@@ -2415,7 +3052,7 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2415
3052
  }
2416
3053
  }
2417
3054
  };
2418
- await walk((0, import_node_path8.resolve)(root));
3055
+ await walk((0, import_node_path11.resolve)(root));
2419
3056
  return paths2.sort();
2420
3057
  }
2421
3058
  function listWorkspace(paths2, options = {}) {
@@ -2529,7 +3166,7 @@ async function fallbackSearch(root, scoped, options) {
2529
3166
  if (matches.length >= options.maxResults) break;
2530
3167
  let source;
2531
3168
  try {
2532
- source = await (0, import_promises7.readFile)((0, import_node_path8.resolve)(root, path));
3169
+ source = await (0, import_promises11.readFile)((0, import_node_path11.resolve)(root, path));
2533
3170
  } catch {
2534
3171
  continue;
2535
3172
  }
@@ -2546,67 +3183,6 @@ async function fallbackSearch(root, scoped, options) {
2546
3183
  return matches;
2547
3184
  }
2548
3185
 
2549
- // src/code-tool-graph.ts
2550
- var import_promises8 = require("fs/promises");
2551
- var import_node_path9 = require("path");
2552
- var import_graph = require("@odla-ai/graph");
2553
- var import_code4 = require("@odla-ai/graph/code");
2554
- var cache = /* @__PURE__ */ new Map();
2555
- function workspaceGraphs(workspaceDir, paths2) {
2556
- const existing = cache.get(workspaceDir);
2557
- if (existing) return existing;
2558
- const read2 = (path) => (0, import_promises8.readFile)((0, import_node_path9.join)(workspaceDir, path), "utf8");
2559
- const built = (async () => ({
2560
- // No knownTables: a staged workspace may not carry migrations, and a filter
2561
- // that silently drops every table is worse than an unfiltered one. Callers
2562
- // with ground truth should build the graph themselves.
2563
- graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2564
- }))();
2565
- cache.set(workspaceDir, built);
2566
- return built;
2567
- }
2568
- function forgetWorkspaceGraphs(workspaceDir) {
2569
- cache.delete(workspaceDir);
2570
- }
2571
- var shortId = (id) => id.slice(id.indexOf(":") + 1);
2572
- function renderOverview(graphs, prefix) {
2573
- const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
2574
- if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
2575
- const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
2576
- const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
2577
- return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
2578
- }
2579
- function renderWhereIs(graphs, symbol) {
2580
- const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
2581
- path: shortId(id),
2582
- pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
2583
- dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
2584
- })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
2585
- if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
2586
- return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
2587
- }
2588
- function renderWhoImports(graphs, path) {
2589
- const id = (0, import_graph.nodeId)(import_code4.FILE, path);
2590
- const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
2591
- if (importers.length === 0) {
2592
- return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
2593
- }
2594
- return importers.slice(0, 40).map(shortId).sort().join("\n");
2595
- }
2596
- function renderWhoTouches(graphs, query) {
2597
- const needle = query.toLowerCase();
2598
- const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
2599
- if (hits.length === 0) return `No table or namespace matching "${query}".`;
2600
- return hits.map((hit) => {
2601
- const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
2602
- return [
2603
- `${hit.name} (${hit.kind})`,
2604
- ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
2605
- ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
2606
- ].join("\n");
2607
- }).join("\n\n");
2608
- }
2609
-
2610
3186
  // src/code-tool-reads.ts
2611
3187
  var GRAPH_TOOLS = /* @__PURE__ */ new Set([
2612
3188
  "sandbox.overview",
@@ -2629,11 +3205,11 @@ async function read(context, request, options, policy, registry) {
2629
3205
  const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
2630
3206
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2631
3207
  const target = resolveCodePath(context.workspaceDir, path);
2632
- const info = await (0, import_promises9.stat)(target);
3208
+ const info = await (0, import_promises12.stat)(target);
2633
3209
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
2634
3210
  throw new TypeError("file is not a bounded regular source file");
2635
3211
  }
2636
- const source = await (0, import_promises9.readFile)(target);
3212
+ const source = await (0, import_promises12.readFile)(target);
2637
3213
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
2638
3214
  const lines = source.toString("utf8").split("\n");
2639
3215
  const content = lines.slice(startLine - 1, endLine).join("\n");
@@ -2748,6 +3324,7 @@ async function route(context, request, options, recipes, policy, registry) {
2748
3324
  if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
2749
3325
  if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
2750
3326
  if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
3327
+ if (request.tool === "sandbox.edit") return await edit(context, request, options, policy, registry);
2751
3328
  return await recipe(context, request, options, recipes, policy);
2752
3329
  } catch (reason) {
2753
3330
  return response(request, false, toolFailureMessage(reason));
@@ -3155,8 +3732,8 @@ var runtimeErrorMessage = (value) => value instanceof Error ? value.message : St
3155
3732
  function codeRuntimeAcknowledgementGate(signal) {
3156
3733
  let settle;
3157
3734
  let settled = false;
3158
- const ready = new Promise((resolve6) => {
3159
- settle = resolve6;
3735
+ const ready = new Promise((resolve7) => {
3736
+ settle = resolve7;
3160
3737
  });
3161
3738
  const release = (run) => {
3162
3739
  if (settled) return;
@@ -3265,7 +3842,7 @@ var TheseusRuntimeEngine = class {
3265
3842
  async #start(command, resume) {
3266
3843
  if (this.#active.has(command.sessionId)) throw new TypeError("Code session is already active on this runtime");
3267
3844
  const metadata = codeCommandMetadata(command.payload, resume);
3268
- const { workspace, sourceDigest, localTrustedBaseDigest, requestedLocal } = await materializeCommandWorkspace({
3845
+ const { workspace, sourceDigest, sourceDigests, localTrustedBaseDigest, requestedLocal } = await materializeCommandWorkspace({
3269
3846
  command,
3270
3847
  metadata,
3271
3848
  resume,
@@ -3286,7 +3863,7 @@ var TheseusRuntimeEngine = class {
3286
3863
  maxTokensPerInteraction: metadata.maxTokensPerInteraction,
3287
3864
  baseCommitSha: metadata.baseCommitSha,
3288
3865
  repository: metadata.repository,
3289
- sourceTreeDigest: metadata.sourceTreeDigest,
3866
+ sourceTreeDigest: metadata.sourceTreeDigest ?? sourceDigest,
3290
3867
  trustedBaseDigest: requestedLocal ? localTrustedBaseDigest : await digestStagedWorkspace(workspace.baselineDir, {
3291
3868
  maxFiles: 2e4,
3292
3869
  maxBytes: 512 * 1024 * 1024
@@ -3312,7 +3889,11 @@ var TheseusRuntimeEngine = class {
3312
3889
  await this.#failure(command, active, detail);
3313
3890
  return null;
3314
3891
  });
3315
- return { status: "running", message: resume ? "Theseus resumed from a portable checkpoint" : "Theseus started" };
3892
+ return {
3893
+ status: "running",
3894
+ message: resume ? "Theseus resumed from a portable checkpoint" : "Theseus started",
3895
+ ...sourceDigests ? { sourceDigests } : {}
3896
+ };
3316
3897
  }
3317
3898
  /**
3318
3899
  * Pursue a goal: attempt, judge with the clean verifier, re-prompt from what
@@ -3539,7 +4120,7 @@ function parse(argv) {
3539
4120
  };
3540
4121
  }
3541
4122
  async function readPolicy(path) {
3542
- const value = JSON.parse(await (0, import_promises10.readFile)(path, "utf8"));
4123
+ const value = JSON.parse(await (0, import_promises13.readFile)(path, "utf8"));
3543
4124
  if (!value || Object.keys(value).some((key) => !["recipes", "recipeAuthorization"].includes(key)) || !Array.isArray(value.recipes) || !value.recipes.length || value.recipeAuthorization !== void 0 && value.recipeAuthorization !== "registered_recipe" && value.recipeAuthorization !== "exact_approval") throw new TypeError("invalid Code build policy file");
3544
4125
  const recipes = value.recipes;
3545
4126
  for (const recipe2 of recipes) assertCodeBuildRecipe(recipe2);
@@ -3558,8 +4139,8 @@ async function main() {
3558
4139
  platform: process.platform === "darwin" ? "macos" : "linux",
3559
4140
  arch: process.arch,
3560
4141
  engines: [engine],
3561
- cpuCount: (0, import_node_os3.cpus)().length,
3562
- memoryBytes: (0, import_node_os3.totalmem)()
4142
+ cpuCount: (0, import_node_os4.cpus)().length,
4143
+ memoryBytes: (0, import_node_os4.totalmem)()
3563
4144
  };
3564
4145
  const controller = new AbortController();
3565
4146
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => controller.abort(signal));