@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.
- package/dist/{chunk-U324RQ4N.js → chunk-CPV7ZVHH.js} +1 -1
- package/dist/{chunk-U324RQ4N.js.map → chunk-CPV7ZVHH.js.map} +1 -1
- package/dist/{chunk-OZBJNTML.js → chunk-ESLFS3FY.js} +4 -4
- package/dist/{chunk-5LRYJKUI.js → chunk-FUDTSAOZ.js} +3 -3
- package/dist/{chunk-VDY5V7ZG.js → chunk-JV45JAAW.js} +3 -3
- package/dist/chunk-JV45JAAW.js.map +1 -0
- package/dist/{chunk-FAN2R3GW.js → chunk-KXFI3WM2.js} +5 -1
- package/dist/chunk-KXFI3WM2.js.map +1 -0
- package/dist/{chunk-5MDYIJXC.js → chunk-VIZALA6O.js} +643 -67
- package/dist/chunk-VIZALA6O.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/code-runtime-cli.cjs +706 -125
- package/dist/code-runtime-cli.cjs.map +1 -1
- package/dist/code-runtime-cli.js +5 -5
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/node.cjs +713 -130
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +41 -6
- package/dist/node.d.ts +41 -6
- package/dist/node.js +8 -6
- package/dist/node.js.map +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-_8y8vBDI.d.ts → types-ocy70g_p.d.cts} +1 -1
- package/dist/{types-_8y8vBDI.d.cts → types-ocy70g_p.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-5MDYIJXC.js.map +0 -1
- package/dist/chunk-FAN2R3GW.js.map +0 -1
- package/dist/chunk-VDY5V7ZG.js.map +0 -1
- /package/dist/{chunk-OZBJNTML.js.map → chunk-ESLFS3FY.js.map} +0 -0
- /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
|
|
6
|
-
var
|
|
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 =
|
|
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((
|
|
399
|
-
if (signal?.aborted) return
|
|
400
|
-
const timer = setTimeout(
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
1458
|
-
const sourceDir = (0,
|
|
1459
|
-
await (0,
|
|
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,
|
|
1470
|
-
if (!target.startsWith(`${(0,
|
|
1471
|
-
await (0,
|
|
1472
|
-
await (0,
|
|
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
|
-
|
|
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,
|
|
1485
|
-
if (!target.startsWith(`${(0,
|
|
1486
|
-
await (0,
|
|
1487
|
-
await (0,
|
|
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,
|
|
1889
|
+
return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
|
|
1491
1890
|
} catch (cause) {
|
|
1492
|
-
await (0,
|
|
1891
|
+
await (0, import_promises8.rm)(root, { recursive: true, force: true });
|
|
1493
1892
|
throw cause;
|
|
1494
1893
|
}
|
|
1495
1894
|
}
|
|
1496
|
-
function
|
|
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
|
-
|
|
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,
|
|
1512
|
-
if (!target.startsWith(`${(0,
|
|
1513
|
-
await (0,
|
|
1514
|
-
await (0,
|
|
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 === ".." ||
|
|
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
|
|
1550
|
-
if (
|
|
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,
|
|
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
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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-
|
|
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
|
|
2376
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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((
|
|
3159
|
-
settle =
|
|
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 {
|
|
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,
|
|
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,
|
|
3562
|
-
memoryBytes: (0,
|
|
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));
|