@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
package/dist/node.cjs
CHANGED
|
@@ -56,6 +56,7 @@ __export(node_exports, {
|
|
|
56
56
|
installedDependencies: () => installedDependencies,
|
|
57
57
|
integrateSubGoals: () => integrateSubGoals,
|
|
58
58
|
isCheckpointEffectCompleted: () => isCheckpointEffectCompleted,
|
|
59
|
+
materializeCodeRuntimeArchive: () => materializeCodeRuntimeArchive,
|
|
59
60
|
materializeCodeRuntimeSource: () => materializeCodeRuntimeSource,
|
|
60
61
|
materializeCommandWorkspace: () => materializeCommandWorkspace,
|
|
61
62
|
materializeGitTree: () => materializeGitTree,
|
|
@@ -155,7 +156,7 @@ function parseAgentOutput(line) {
|
|
|
155
156
|
if (message2.type === "tool.request") {
|
|
156
157
|
const input = record(message2.input);
|
|
157
158
|
const tool = String(message2.tool);
|
|
158
|
-
if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
|
|
159
|
+
if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.edit", "sandbox.run_recipe"].includes(tool)) {
|
|
159
160
|
throw new HarnessProtocolError("tool.request requires a registered tool and object input");
|
|
160
161
|
}
|
|
161
162
|
return {
|
|
@@ -232,7 +233,7 @@ async function selectContainerEngine(requested = "auto", options = {}) {
|
|
|
232
233
|
throw new TypeError("no supported container engine found");
|
|
233
234
|
}
|
|
234
235
|
function inspectRootlessPodman() {
|
|
235
|
-
return new Promise((
|
|
236
|
+
return new Promise((resolve8, reject) => {
|
|
236
237
|
(0, import_node_child_process.execFile)(
|
|
237
238
|
"podman",
|
|
238
239
|
["info", "--format", "{{.Host.Security.Rootless}}"],
|
|
@@ -242,7 +243,7 @@ function inspectRootlessPodman() {
|
|
|
242
243
|
reject(new TypeError("could not verify that the active Podman service is rootless"));
|
|
243
244
|
return;
|
|
244
245
|
}
|
|
245
|
-
|
|
246
|
+
resolve8(stdout.trim() === "true");
|
|
246
247
|
}
|
|
247
248
|
);
|
|
248
249
|
});
|
|
@@ -1140,6 +1141,34 @@ function createCodeRuntimeControlClient(options) {
|
|
|
1140
1141
|
}
|
|
1141
1142
|
return value;
|
|
1142
1143
|
};
|
|
1144
|
+
const callRaw = async (path, body) => {
|
|
1145
|
+
const timeout = AbortSignal.timeout(modelRequestTimeoutMs);
|
|
1146
|
+
const signals = [options.signal, timeout].filter((item) => Boolean(item));
|
|
1147
|
+
const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
1148
|
+
let response2;
|
|
1149
|
+
try {
|
|
1150
|
+
response2 = await request(`${endpoint}${path}`, {
|
|
1151
|
+
method: "POST",
|
|
1152
|
+
headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
|
|
1153
|
+
body: JSON.stringify(body),
|
|
1154
|
+
redirect: "error",
|
|
1155
|
+
signal
|
|
1156
|
+
});
|
|
1157
|
+
} catch (cause) {
|
|
1158
|
+
if (options.signal?.aborted) throw cause;
|
|
1159
|
+
throw new CodeRuntimeControlError("Code source archive is unavailable", 503, "transport_unavailable");
|
|
1160
|
+
}
|
|
1161
|
+
if (!response2.ok) {
|
|
1162
|
+
const value = await response2.json().catch(() => null);
|
|
1163
|
+
const problem = record2(record2(value)?.error);
|
|
1164
|
+
throw new CodeRuntimeControlError(
|
|
1165
|
+
typeof problem?.message === "string" ? problem.message : `Code runtime request failed (${response2.status})`,
|
|
1166
|
+
response2.status,
|
|
1167
|
+
typeof problem?.code === "string" ? problem.code : void 0
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
return response2;
|
|
1171
|
+
};
|
|
1143
1172
|
return {
|
|
1144
1173
|
heartbeat: async (version, capabilities) => {
|
|
1145
1174
|
validateHeartbeat(version, capabilities);
|
|
@@ -1151,6 +1180,14 @@ function createCodeRuntimeControlClient(options) {
|
|
|
1151
1180
|
source: async (sessionId) => parseSource(
|
|
1152
1181
|
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
|
|
1153
1182
|
),
|
|
1183
|
+
sourceArchive: async (sessionId, alias) => {
|
|
1184
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias)) throw new TypeError("invalid Code source alias");
|
|
1185
|
+
const response2 = await callRaw(
|
|
1186
|
+
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source/archive`,
|
|
1187
|
+
{ alias }
|
|
1188
|
+
);
|
|
1189
|
+
return parseSourceArchiveResponse(response2, alias);
|
|
1190
|
+
},
|
|
1154
1191
|
infer: async (sessionId, inference) => {
|
|
1155
1192
|
const value = record2(await call(
|
|
1156
1193
|
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
|
|
@@ -1215,9 +1252,70 @@ function createCodeRuntimeControlClient(options) {
|
|
|
1215
1252
|
}
|
|
1216
1253
|
};
|
|
1217
1254
|
}
|
|
1255
|
+
var ARCHIVE_LIMIT_MAXIMA = {
|
|
1256
|
+
maxCompressedBytes: 64 * 1024 * 1024,
|
|
1257
|
+
maxDecompressedBytes: 96 * 1024 * 1024,
|
|
1258
|
+
maxEntries: 2e5,
|
|
1259
|
+
maxFiles: 1e5,
|
|
1260
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
1261
|
+
maxTotalFileBytes: 80 * 1024 * 1024,
|
|
1262
|
+
maxPathBytes: 4096,
|
|
1263
|
+
maxExtendedHeaderBytes: 32 * 1024
|
|
1264
|
+
};
|
|
1265
|
+
async function parseSourceArchiveResponse(response2, expectedAlias) {
|
|
1266
|
+
const alias = response2.headers.get("x-odla-source-alias") ?? "";
|
|
1267
|
+
const repository = response2.headers.get("x-odla-source-repository") ?? "";
|
|
1268
|
+
const commitSha = response2.headers.get("x-odla-source-commit") ?? "";
|
|
1269
|
+
if (alias !== expectedAlias || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || !/^[0-9a-f]{40}$/.test(commitSha)) {
|
|
1270
|
+
await response2.body?.cancel().catch(() => void 0);
|
|
1271
|
+
throw new CodeRuntimeControlError("invalid Code source archive identity", 502, "invalid_response");
|
|
1272
|
+
}
|
|
1273
|
+
const limits = Object.fromEntries(Object.entries(ARCHIVE_LIMIT_MAXIMA).map(([key, maximum]) => {
|
|
1274
|
+
const header = `x-odla-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
|
|
1275
|
+
const value = response2.headers.get(header) ?? "";
|
|
1276
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1 || Number(value) > maximum) {
|
|
1277
|
+
throw new CodeRuntimeControlError("invalid Code source archive limits", 502, "invalid_response");
|
|
1278
|
+
}
|
|
1279
|
+
return [key, Number(value)];
|
|
1280
|
+
}));
|
|
1281
|
+
const length = response2.headers.get("content-length");
|
|
1282
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > limits.maxCompressedBytes)) {
|
|
1283
|
+
await response2.body?.cancel().catch(() => void 0);
|
|
1284
|
+
throw new CodeRuntimeControlError("Code source archive exceeds its byte bound", 502, "invalid_response");
|
|
1285
|
+
}
|
|
1286
|
+
const compressed = await readBoundedResponse(response2.body, limits.maxCompressedBytes);
|
|
1287
|
+
return { alias, repository, commitSha, compressed, limits };
|
|
1288
|
+
}
|
|
1289
|
+
async function readBoundedResponse(body, maximum) {
|
|
1290
|
+
if (!body) throw new CodeRuntimeControlError("Code source archive body is missing", 502, "invalid_response");
|
|
1291
|
+
const reader = body.getReader();
|
|
1292
|
+
const chunks = [];
|
|
1293
|
+
let size = 0;
|
|
1294
|
+
try {
|
|
1295
|
+
while (true) {
|
|
1296
|
+
const { done, value } = await reader.read();
|
|
1297
|
+
if (done) break;
|
|
1298
|
+
size += value.byteLength;
|
|
1299
|
+
if (size > maximum) {
|
|
1300
|
+
await reader.cancel().catch(() => void 0);
|
|
1301
|
+
throw new CodeRuntimeControlError("Code source archive exceeds its byte bound", 502, "invalid_response");
|
|
1302
|
+
}
|
|
1303
|
+
chunks.push(value);
|
|
1304
|
+
}
|
|
1305
|
+
} finally {
|
|
1306
|
+
reader.releaseLock();
|
|
1307
|
+
}
|
|
1308
|
+
const result = new Uint8Array(size);
|
|
1309
|
+
let offset = 0;
|
|
1310
|
+
for (const chunk of chunks) {
|
|
1311
|
+
result.set(chunk, offset);
|
|
1312
|
+
offset += chunk.byteLength;
|
|
1313
|
+
}
|
|
1314
|
+
return result;
|
|
1315
|
+
}
|
|
1218
1316
|
|
|
1219
1317
|
// src/code-runtime.ts
|
|
1220
|
-
var CODE_RUNTIME_PROTOCOL_VERSION =
|
|
1318
|
+
var CODE_RUNTIME_PROTOCOL_VERSION = 3;
|
|
1221
1319
|
async function runCodeRuntimeHeartbeatLoop(options) {
|
|
1222
1320
|
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
1223
1321
|
if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
|
|
@@ -1286,12 +1384,12 @@ function retryableControlFailure(value) {
|
|
|
1286
1384
|
return failure.status === 408 || failure.status === 425 || failure.status === 429 || failure.status >= 500;
|
|
1287
1385
|
}
|
|
1288
1386
|
function wait(ms, signal) {
|
|
1289
|
-
return new Promise((
|
|
1290
|
-
if (signal?.aborted) return
|
|
1291
|
-
const timer = setTimeout(
|
|
1387
|
+
return new Promise((resolve8) => {
|
|
1388
|
+
if (signal?.aborted) return resolve8();
|
|
1389
|
+
const timer = setTimeout(resolve8, ms);
|
|
1292
1390
|
signal?.addEventListener("abort", () => {
|
|
1293
1391
|
clearTimeout(timer);
|
|
1294
|
-
|
|
1392
|
+
resolve8();
|
|
1295
1393
|
}, { once: true });
|
|
1296
1394
|
});
|
|
1297
1395
|
}
|
|
@@ -1949,7 +2047,8 @@ function codeCommandMetadata(payload, resume) {
|
|
|
1949
2047
|
const repository = trusted?.repository;
|
|
1950
2048
|
const baseCommitSha = trusted?.commitSha;
|
|
1951
2049
|
const sourceTreeDigest = trusted?.treeDigest;
|
|
1952
|
-
|
|
2050
|
+
const hostMaterialized = record3(payload.sourceSet) !== null && sourceTreeDigest === void 0;
|
|
2051
|
+
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))) {
|
|
1953
2052
|
throw new TypeError(`invalid Code ${resume ? "resume" : "start"} trusted base`);
|
|
1954
2053
|
}
|
|
1955
2054
|
if (!Number.isSafeInteger(maxTokensPerInteraction) || Number(maxTokensPerInteraction) < 4e3 || Number(maxTokensPerInteraction) > 2e5) {
|
|
@@ -1965,7 +2064,7 @@ function codeCommandMetadata(payload, resume) {
|
|
|
1965
2064
|
attestationDigest: typeof attestation === "string" ? attestation : "resume",
|
|
1966
2065
|
repository,
|
|
1967
2066
|
baseCommitSha,
|
|
1968
|
-
sourceTreeDigest
|
|
2067
|
+
sourceTreeDigest: typeof sourceTreeDigest === "string" ? sourceTreeDigest : null
|
|
1969
2068
|
};
|
|
1970
2069
|
}
|
|
1971
2070
|
function codeLocalSource(payload) {
|
|
@@ -2030,19 +2129,320 @@ async function prepareRuntimeLocalSource(input) {
|
|
|
2030
2129
|
}
|
|
2031
2130
|
|
|
2032
2131
|
// src/code-runtime-source.ts
|
|
2132
|
+
var import_promises10 = require("fs/promises");
|
|
2133
|
+
var import_node_os4 = require("os");
|
|
2134
|
+
var import_node_path10 = require("path");
|
|
2135
|
+
|
|
2136
|
+
// src/code-runtime-archive.ts
|
|
2137
|
+
var import_node_zlib = require("zlib");
|
|
2033
2138
|
var import_promises8 = require("fs/promises");
|
|
2034
2139
|
var import_node_os3 = require("os");
|
|
2035
2140
|
var import_node_path8 = require("path");
|
|
2036
2141
|
var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
2037
2142
|
var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
2143
|
+
var MAX_NUL_SHARE = 0.1;
|
|
2144
|
+
async function materializeCodeRuntimeArchive(archive, visiblePaths, tempRoot = (0, import_node_os3.tmpdir)()) {
|
|
2145
|
+
if (!archive.compressed.byteLength || archive.compressed.byteLength > archive.limits.maxCompressedBytes) {
|
|
2146
|
+
throw new TypeError("Code source archive exceeds its compressed byte bound");
|
|
2147
|
+
}
|
|
2148
|
+
let bytes;
|
|
2149
|
+
try {
|
|
2150
|
+
bytes = archive.compressed[0] === 31 && archive.compressed[1] === 139 ? (0, import_node_zlib.gunzipSync)(archive.compressed, { maxOutputLength: archive.limits.maxDecompressedBytes }) : archive.compressed;
|
|
2151
|
+
} catch {
|
|
2152
|
+
throw new TypeError("Code source archive is not a valid bounded gzip stream");
|
|
2153
|
+
}
|
|
2154
|
+
if (bytes.byteLength > archive.limits.maxDecompressedBytes) {
|
|
2155
|
+
throw new TypeError("Code source archive exceeds its decompressed byte bound");
|
|
2156
|
+
}
|
|
2157
|
+
const entries = parseTar(bytes, archive.limits);
|
|
2158
|
+
const root = await (0, import_promises8.mkdtemp)((0, import_node_path8.join)(tempRoot, "odla-code-archive-"));
|
|
2159
|
+
const sourceDir = (0, import_node_path8.join)(root, "source");
|
|
2160
|
+
await (0, import_promises8.mkdir)(sourceDir);
|
|
2161
|
+
let visible = 0;
|
|
2162
|
+
try {
|
|
2163
|
+
for (const entry of entries) {
|
|
2164
|
+
if (entry.directory || visiblePaths && !visiblePaths.has(entry.path)) continue;
|
|
2165
|
+
if (filteredPath(entry.path)) continue;
|
|
2166
|
+
let content;
|
|
2167
|
+
try {
|
|
2168
|
+
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(entry.bytes);
|
|
2169
|
+
} catch {
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
if (nulShare(content) > MAX_NUL_SHARE) continue;
|
|
2173
|
+
const target = (0, import_node_path8.resolve)(sourceDir, entry.path);
|
|
2174
|
+
if (!target.startsWith(`${(0, import_node_path8.resolve)(sourceDir)}${import_node_path8.sep}`)) throw new TypeError("Code source path escapes its root");
|
|
2175
|
+
await (0, import_promises8.mkdir)((0, import_node_path8.dirname)(target), { recursive: true });
|
|
2176
|
+
await (0, import_promises8.writeFile)(target, content, { flag: "wx", mode: 420 });
|
|
2177
|
+
visible += 1;
|
|
2178
|
+
}
|
|
2179
|
+
if (!visible && !visiblePaths) throw new TypeError("GitHub commit has no Code-visible text source");
|
|
2180
|
+
return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
|
|
2181
|
+
} catch (cause) {
|
|
2182
|
+
await (0, import_promises8.rm)(root, { recursive: true, force: true });
|
|
2183
|
+
throw cause;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function parseTar(bytes, limits) {
|
|
2187
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
|
|
2188
|
+
const entries = [];
|
|
2189
|
+
const archivePaths = /* @__PURE__ */ new Set();
|
|
2190
|
+
let entryCount = 0;
|
|
2191
|
+
let fileCount = 0;
|
|
2192
|
+
let totalFileBytes = 0;
|
|
2193
|
+
let pendingPath;
|
|
2194
|
+
let pendingPaxPath;
|
|
2195
|
+
let offset = 0;
|
|
2196
|
+
let ended = false;
|
|
2197
|
+
while (offset + 512 <= bytes.byteLength) {
|
|
2198
|
+
const header = bytes.subarray(offset, offset + 512);
|
|
2199
|
+
offset += 512;
|
|
2200
|
+
if (zeroBlock(header)) {
|
|
2201
|
+
ended = true;
|
|
2202
|
+
assertZeroTail(bytes, offset);
|
|
2203
|
+
break;
|
|
2204
|
+
}
|
|
2205
|
+
entryCount += 1;
|
|
2206
|
+
if (entryCount > limits.maxEntries) throw new TypeError("Code source archive has too many entries");
|
|
2207
|
+
validateChecksum(header);
|
|
2208
|
+
const type = header[156] ?? 0;
|
|
2209
|
+
const size = tarNumber(header.subarray(124, 136));
|
|
2210
|
+
if (size > limits.maxDecompressedBytes || offset + aligned(size) > bytes.byteLength) {
|
|
2211
|
+
throw new TypeError("Code source archive entry is invalid or too large");
|
|
2212
|
+
}
|
|
2213
|
+
const payload = bytes.subarray(offset, offset + size);
|
|
2214
|
+
offset += aligned(size);
|
|
2215
|
+
if (type === 120 || type === 103) {
|
|
2216
|
+
if (size > limits.maxExtendedHeaderBytes) throw new TypeError("Code source archive extended header is too large");
|
|
2217
|
+
const pax = parsePax(payload, decoder, limits.maxPathBytes);
|
|
2218
|
+
if (pax.linkpath !== void 0 || pax.size !== void 0 || Object.keys(pax).some((key) => key.toLowerCase().includes("sparse"))) {
|
|
2219
|
+
throw new TypeError("Code source archive contains an unsupported entry");
|
|
2220
|
+
}
|
|
2221
|
+
if (type === 103 && pax.path !== void 0) throw new TypeError("Code source archive path is unsafe");
|
|
2222
|
+
if (type === 120 && pax.path !== void 0) {
|
|
2223
|
+
if (pendingPaxPath !== void 0 || pendingPath !== void 0) throw new TypeError("Code source archive is invalid");
|
|
2224
|
+
pendingPaxPath = pax.path;
|
|
2225
|
+
}
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
if (type === 76) {
|
|
2229
|
+
if (size > limits.maxExtendedHeaderBytes || pendingPath !== void 0 || pendingPaxPath !== void 0) {
|
|
2230
|
+
throw new TypeError("Code source archive long path is invalid");
|
|
2231
|
+
}
|
|
2232
|
+
pendingPath = validateArchivePath(decode(payload, decoder).replace(/\0+$/, "").replace(/\n$/, ""), limits.maxPathBytes);
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
if (type === 75) throw new TypeError("Code source archive contains an unsupported link");
|
|
2236
|
+
const path = validateArchivePath(pendingPaxPath ?? pendingPath ?? tarPath(header, decoder), limits.maxPathBytes);
|
|
2237
|
+
pendingPaxPath = void 0;
|
|
2238
|
+
pendingPath = void 0;
|
|
2239
|
+
const directory = type === 53;
|
|
2240
|
+
const regular = type === 0 || type === 48;
|
|
2241
|
+
if (!directory && !regular || directory && size !== 0 || archivePaths.has(path)) {
|
|
2242
|
+
throw new TypeError("Code source archive contains an invalid or duplicate entry");
|
|
2243
|
+
}
|
|
2244
|
+
archivePaths.add(path);
|
|
2245
|
+
if (regular) {
|
|
2246
|
+
fileCount += 1;
|
|
2247
|
+
totalFileBytes += size;
|
|
2248
|
+
if (fileCount > limits.maxFiles || size > limits.maxFileBytes || totalFileBytes > limits.maxTotalFileBytes) {
|
|
2249
|
+
throw new TypeError("Code source archive exceeds its file bounds");
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
entries.push({ path, directory, ...regular ? { bytes: payload } : {} });
|
|
2253
|
+
}
|
|
2254
|
+
if (!ended || pendingPath !== void 0 || pendingPaxPath !== void 0 || !entries.length || !fileCount) {
|
|
2255
|
+
throw new TypeError("Code source archive is incomplete");
|
|
2256
|
+
}
|
|
2257
|
+
return unwrapRepository(entries, limits.maxPathBytes);
|
|
2258
|
+
}
|
|
2259
|
+
function unwrapRepository(entries, maxPathBytes) {
|
|
2260
|
+
const root = entries[0].path.split("/")[0];
|
|
2261
|
+
if (!root || entries.some((entry) => entry.path !== root && !entry.path.startsWith(`${root}/`))) {
|
|
2262
|
+
throw new TypeError("Code source archive has no single repository root");
|
|
2263
|
+
}
|
|
2264
|
+
const output = [];
|
|
2265
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
2266
|
+
for (const entry of entries) {
|
|
2267
|
+
if (entry.path === root) {
|
|
2268
|
+
if (!entry.directory) throw new TypeError("Code source archive root is not a directory");
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
const path = entry.path.slice(root.length + 1);
|
|
2272
|
+
validateRepositoryPath(path, maxPathBytes);
|
|
2273
|
+
const parts = path.split("/");
|
|
2274
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
2275
|
+
if (kinds.get(parts.slice(0, index).join("/")) === "file") throw new TypeError("Code source archive path conflicts");
|
|
2276
|
+
}
|
|
2277
|
+
if (!entry.directory) {
|
|
2278
|
+
for (const existing of kinds.keys()) {
|
|
2279
|
+
if (existing.startsWith(`${path}/`)) throw new TypeError("Code source archive path conflicts");
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (kinds.has(path)) throw new TypeError("Code source archive repeats a path");
|
|
2283
|
+
kinds.set(path, entry.directory ? "directory" : "file");
|
|
2284
|
+
output.push({ ...entry, path });
|
|
2285
|
+
}
|
|
2286
|
+
return output.sort((left, right) => left.path.localeCompare(right.path));
|
|
2287
|
+
}
|
|
2288
|
+
function parsePax(bytes, decoder, maxPathBytes) {
|
|
2289
|
+
const result = {};
|
|
2290
|
+
let offset = 0;
|
|
2291
|
+
while (offset < bytes.byteLength) {
|
|
2292
|
+
const space = bytes.indexOf(32, offset);
|
|
2293
|
+
if (space < 0) throw new TypeError("Code source archive PAX header is invalid");
|
|
2294
|
+
const lengthText = ascii(bytes.subarray(offset, space));
|
|
2295
|
+
if (!/^[1-9][0-9]{0,8}$/.test(lengthText)) throw new TypeError("Code source archive PAX length is invalid");
|
|
2296
|
+
const length = Number(lengthText);
|
|
2297
|
+
const end = offset + length;
|
|
2298
|
+
if (!Number.isSafeInteger(length) || end > bytes.byteLength || bytes[end - 1] !== 10) {
|
|
2299
|
+
throw new TypeError("Code source archive PAX record is invalid");
|
|
2300
|
+
}
|
|
2301
|
+
const record5 = decode(bytes.subarray(space + 1, end - 1), decoder);
|
|
2302
|
+
const equals = record5.indexOf("=");
|
|
2303
|
+
if (equals < 1) throw new TypeError("Code source archive PAX field is invalid");
|
|
2304
|
+
const key = record5.slice(0, equals);
|
|
2305
|
+
if (Object.hasOwn(result, key)) throw new TypeError("Code source archive PAX field repeats");
|
|
2306
|
+
result[key] = record5.slice(equals + 1);
|
|
2307
|
+
offset = end;
|
|
2308
|
+
}
|
|
2309
|
+
if (result.path !== void 0) result.path = validateArchivePath(result.path, maxPathBytes);
|
|
2310
|
+
return result;
|
|
2311
|
+
}
|
|
2312
|
+
var aligned = (size) => Math.ceil(size / 512) * 512;
|
|
2313
|
+
var zeroBlock = (block) => block.every((value) => value === 0);
|
|
2314
|
+
function validateChecksum(header) {
|
|
2315
|
+
const expected = tarNumber(header.subarray(148, 156));
|
|
2316
|
+
let actual = 0;
|
|
2317
|
+
for (let index = 0; index < header.length; index += 1) actual += index >= 148 && index < 156 ? 32 : header[index];
|
|
2318
|
+
if (actual !== expected) throw new TypeError("Code source archive checksum is invalid");
|
|
2319
|
+
}
|
|
2320
|
+
function tarNumber(field) {
|
|
2321
|
+
if ((field[0] ?? 0) & 128) throw new TypeError("Code source archive numeric format is unsupported");
|
|
2322
|
+
const value = ascii(field).replaceAll("\0", "").trim();
|
|
2323
|
+
if (!value) return 0;
|
|
2324
|
+
if (!/^[0-7]+$/.test(value)) throw new TypeError("Code source archive number is invalid");
|
|
2325
|
+
const parsed = Number.parseInt(value, 8);
|
|
2326
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) throw new TypeError("Code source archive number is too large");
|
|
2327
|
+
return parsed;
|
|
2328
|
+
}
|
|
2329
|
+
function tarPath(header, decoder) {
|
|
2330
|
+
const name = decodeNul(header.subarray(0, 100), decoder);
|
|
2331
|
+
const prefix = decodeNul(header.subarray(345, 500), decoder);
|
|
2332
|
+
return prefix ? `${prefix}/${name}` : name;
|
|
2333
|
+
}
|
|
2334
|
+
function decodeNul(value, decoder) {
|
|
2335
|
+
const end = value.indexOf(0);
|
|
2336
|
+
return decode(end < 0 ? value : value.subarray(0, end), decoder);
|
|
2337
|
+
}
|
|
2338
|
+
function decode(value, decoder) {
|
|
2339
|
+
try {
|
|
2340
|
+
return decoder.decode(value);
|
|
2341
|
+
} catch {
|
|
2342
|
+
throw new TypeError("Code source archive text is invalid UTF-8");
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
function ascii(value) {
|
|
2346
|
+
let output = "";
|
|
2347
|
+
for (const byte of value) output += String.fromCharCode(byte);
|
|
2348
|
+
return output;
|
|
2349
|
+
}
|
|
2350
|
+
function validateArchivePath(input, maximum) {
|
|
2351
|
+
const path = input.endsWith("/") ? input.slice(0, -1) : input;
|
|
2352
|
+
validateRepositoryPath(path, maximum);
|
|
2353
|
+
return path;
|
|
2354
|
+
}
|
|
2355
|
+
function validateRepositoryPath(path, maximum) {
|
|
2356
|
+
if (!path || path.startsWith("/") || path.includes("\\") || /[\u0000-\u001f\u007f]/.test(path) || new TextEncoder().encode(path).byteLength > maximum || path.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
2357
|
+
throw new TypeError("Code source archive path is unsafe");
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
function assertZeroTail(bytes, offset) {
|
|
2361
|
+
for (let index = offset; index < bytes.byteLength; index += 1) {
|
|
2362
|
+
if (bytes[index] !== 0) throw new TypeError("Code source archive has data after its end marker");
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
function filteredPath(path) {
|
|
2366
|
+
const parts = path.split("/");
|
|
2367
|
+
return parts.some((part) => RESERVED2.has(part) || SECRET2.test(part));
|
|
2368
|
+
}
|
|
2369
|
+
function nulShare(content) {
|
|
2370
|
+
if (!content.length) return 0;
|
|
2371
|
+
let count = 0;
|
|
2372
|
+
for (let index = 0; index < content.length; index += 1) if (content.charCodeAt(index) === 0) count += 1;
|
|
2373
|
+
return count / content.length;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// src/code-runtime-selected-source.ts
|
|
2377
|
+
var import_promises9 = require("fs/promises");
|
|
2378
|
+
var import_node_path9 = require("path");
|
|
2379
|
+
function selectedSourceSet(payload) {
|
|
2380
|
+
if (!payload.sourceSet) return null;
|
|
2381
|
+
const set = payload.sourceSet && typeof payload.sourceSet === "object" && !Array.isArray(payload.sourceSet) ? payload.sourceSet : null;
|
|
2382
|
+
if (!set) throw new TypeError("Code selected source set is invalid");
|
|
2383
|
+
const parse = (value, primary2) => {
|
|
2384
|
+
const item = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2385
|
+
const alias = item?.alias;
|
|
2386
|
+
const repository = item?.repository;
|
|
2387
|
+
const commitSha = item?.commitSha;
|
|
2388
|
+
const materialization = item?.materialization ?? "registry_snapshot";
|
|
2389
|
+
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))) {
|
|
2390
|
+
throw new TypeError("Code selected source identity is invalid");
|
|
2391
|
+
}
|
|
2392
|
+
const visiblePaths = item?.visiblePaths;
|
|
2393
|
+
if (visiblePaths !== void 0 && (!Array.isArray(visiblePaths) || visiblePaths.length > 2e3 || visiblePaths.some((path) => typeof path !== "string" || path.length > 4096))) {
|
|
2394
|
+
throw new TypeError("Code selected source visibility slice is invalid");
|
|
2395
|
+
}
|
|
2396
|
+
return {
|
|
2397
|
+
alias,
|
|
2398
|
+
repository,
|
|
2399
|
+
commitSha,
|
|
2400
|
+
materialization,
|
|
2401
|
+
...visiblePaths ? { visiblePaths } : {}
|
|
2402
|
+
};
|
|
2403
|
+
};
|
|
2404
|
+
if (!Array.isArray(set.references)) throw new TypeError("Code selected source references are invalid");
|
|
2405
|
+
const primary = parse(set.primary, true);
|
|
2406
|
+
const references = set.references.map((item) => parse(item, false));
|
|
2407
|
+
const aliases = /* @__PURE__ */ new Set([primary.alias, ...references.map((item) => item.alias)]);
|
|
2408
|
+
if (aliases.size !== references.length + 1) throw new TypeError("Code selected source aliases repeat");
|
|
2409
|
+
return { primary, references };
|
|
2410
|
+
}
|
|
2411
|
+
async function attachReferenceDirectories(workspace, references) {
|
|
2412
|
+
for (const reference of references) {
|
|
2413
|
+
validateAlias(reference.alias);
|
|
2414
|
+
for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
|
|
2415
|
+
const target = (0, import_node_path9.join)(root, ".odla-references", reference.alias);
|
|
2416
|
+
await (0, import_promises9.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
|
|
2417
|
+
await (0, import_promises9.cp)(reference.sourceDir, target, { recursive: true, errorOnExist: true, force: false });
|
|
2418
|
+
await makeTreeReadOnly(target);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
function validateAlias(alias) {
|
|
2423
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
|
|
2424
|
+
throw new TypeError("Code reference alias is invalid");
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
async function makeTreeReadOnly(root) {
|
|
2428
|
+
for (const entry of await (0, import_promises9.readdir)(root, { withFileTypes: true })) {
|
|
2429
|
+
const target = (0, import_node_path9.join)(root, entry.name);
|
|
2430
|
+
if (entry.isDirectory()) await makeTreeReadOnly(target);
|
|
2431
|
+
else if (entry.isFile()) await (0, import_promises9.chmod)(target, 292);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// src/code-runtime-source.ts
|
|
2436
|
+
var RESERVED3 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
2437
|
+
var SECRET3 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
2038
2438
|
var SOURCE_MAX_FILES = 1e5;
|
|
2039
2439
|
var SOURCE_MAX_BYTES = 80 * 1024 * 1024;
|
|
2040
2440
|
var SOURCE_SET_MAX_BYTES = 480 * 1024 * 1024;
|
|
2041
|
-
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0,
|
|
2441
|
+
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_node_os4.tmpdir)()) {
|
|
2042
2442
|
if (!snapshot.files.length || snapshot.files.length > SOURCE_MAX_FILES) throw new TypeError("Code source file count is invalid");
|
|
2043
|
-
const root = await (0,
|
|
2044
|
-
const sourceDir = (0,
|
|
2045
|
-
await (0,
|
|
2443
|
+
const root = await (0, import_promises10.mkdtemp)((0, import_node_path10.join)(tempRoot, "odla-code-source-"));
|
|
2444
|
+
const sourceDir = (0, import_node_path10.join)(root, "source");
|
|
2445
|
+
await (0, import_promises10.mkdir)(sourceDir);
|
|
2046
2446
|
const seen = /* @__PURE__ */ new Set();
|
|
2047
2447
|
let bytes = 0;
|
|
2048
2448
|
try {
|
|
@@ -2052,13 +2452,13 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_node
|
|
|
2052
2452
|
seen.add(file.path);
|
|
2053
2453
|
bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
|
|
2054
2454
|
if (bytes > SOURCE_MAX_BYTES) throw new TypeError("Code source exceeds its byte bound");
|
|
2055
|
-
const target = (0,
|
|
2056
|
-
if (!target.startsWith(`${(0,
|
|
2057
|
-
await (0,
|
|
2058
|
-
await (0,
|
|
2455
|
+
const target = (0, import_node_path10.resolve)(sourceDir, file.path);
|
|
2456
|
+
if (!target.startsWith(`${(0, import_node_path10.resolve)(sourceDir)}${import_node_path10.sep}`)) throw new TypeError("Code source path escapes its root");
|
|
2457
|
+
await (0, import_promises10.mkdir)((0, import_node_path10.dirname)(target), { recursive: true });
|
|
2458
|
+
await (0, import_promises10.writeFile)(target, file.content, { flag: "wx", mode: 420 });
|
|
2059
2459
|
}
|
|
2060
2460
|
for (const reference of snapshot.references ?? []) {
|
|
2061
|
-
|
|
2461
|
+
validateAlias2(reference.alias);
|
|
2062
2462
|
if (!reference.files.length || reference.files.length > SOURCE_MAX_FILES) throw new TypeError("Code reference file count is invalid");
|
|
2063
2463
|
for (const file of reference.files) {
|
|
2064
2464
|
validatePath(file.path);
|
|
@@ -2067,19 +2467,19 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_node
|
|
|
2067
2467
|
seen.add(path);
|
|
2068
2468
|
bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
|
|
2069
2469
|
if (bytes > SOURCE_SET_MAX_BYTES) throw new TypeError("Code source set exceeds its byte bound");
|
|
2070
|
-
const target = (0,
|
|
2071
|
-
if (!target.startsWith(`${(0,
|
|
2072
|
-
await (0,
|
|
2073
|
-
await (0,
|
|
2470
|
+
const target = (0, import_node_path10.resolve)(sourceDir, path);
|
|
2471
|
+
if (!target.startsWith(`${(0, import_node_path10.resolve)(sourceDir)}${import_node_path10.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
2472
|
+
await (0, import_promises10.mkdir)((0, import_node_path10.dirname)(target), { recursive: true });
|
|
2473
|
+
await (0, import_promises10.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
2074
2474
|
}
|
|
2075
2475
|
}
|
|
2076
|
-
return { sourceDir, cleanup: () => (0,
|
|
2476
|
+
return { sourceDir, cleanup: () => (0, import_promises10.rm)(root, { recursive: true, force: true }) };
|
|
2077
2477
|
} catch (cause) {
|
|
2078
|
-
await (0,
|
|
2478
|
+
await (0, import_promises10.rm)(root, { recursive: true, force: true });
|
|
2079
2479
|
throw cause;
|
|
2080
2480
|
}
|
|
2081
2481
|
}
|
|
2082
|
-
function
|
|
2482
|
+
function validateAlias2(alias) {
|
|
2083
2483
|
if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
|
|
2084
2484
|
throw new TypeError("Code reference alias is invalid");
|
|
2085
2485
|
}
|
|
@@ -2087,24 +2487,24 @@ function validateAlias(alias) {
|
|
|
2087
2487
|
async function attachCodeRuntimeReferences(workspace, references) {
|
|
2088
2488
|
let bytes = 0;
|
|
2089
2489
|
for (const reference of references) {
|
|
2090
|
-
|
|
2490
|
+
validateAlias2(reference.alias);
|
|
2091
2491
|
for (const file of reference.files) {
|
|
2092
2492
|
validatePath(file.path);
|
|
2093
2493
|
const path = `.odla-references/${reference.alias}/${file.path}`;
|
|
2094
2494
|
bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
|
|
2095
2495
|
if (bytes > SOURCE_SET_MAX_BYTES - SOURCE_MAX_BYTES) throw new TypeError("Code reference set exceeds its byte bound");
|
|
2096
2496
|
for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
|
|
2097
|
-
const target = (0,
|
|
2098
|
-
if (!target.startsWith(`${(0,
|
|
2099
|
-
await (0,
|
|
2100
|
-
await (0,
|
|
2497
|
+
const target = (0, import_node_path10.resolve)(root, path);
|
|
2498
|
+
if (!target.startsWith(`${(0, import_node_path10.resolve)(root)}${import_node_path10.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
2499
|
+
await (0, import_promises10.mkdir)((0, import_node_path10.dirname)(target), { recursive: true });
|
|
2500
|
+
await (0, import_promises10.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
2101
2501
|
}
|
|
2102
2502
|
}
|
|
2103
2503
|
}
|
|
2104
2504
|
}
|
|
2105
2505
|
function validatePath(path) {
|
|
2106
2506
|
const parts = path.split("/");
|
|
2107
|
-
if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." ||
|
|
2507
|
+
if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED3.has(part) || SECRET3.test(part))) {
|
|
2108
2508
|
throw new TypeError("Code source contains an unsafe path");
|
|
2109
2509
|
}
|
|
2110
2510
|
}
|
|
@@ -2132,12 +2532,12 @@ async function materializeCommandWorkspace(input) {
|
|
|
2132
2532
|
throw new TypeError("Code selected source set is invalid");
|
|
2133
2533
|
}
|
|
2134
2534
|
if (references.length) {
|
|
2135
|
-
const
|
|
2136
|
-
if (
|
|
2535
|
+
const selected2 = await input.control.source(command.sessionId);
|
|
2536
|
+
if (selected2.repository !== metadata.repository || selected2.commitSha !== metadata.baseCommitSha || selected2.treeDigest !== metadata.sourceTreeDigest) {
|
|
2137
2537
|
await prepared.workspace.cleanup();
|
|
2138
2538
|
throw new TypeError("Code local source does not match the selected GitHub primary source");
|
|
2139
2539
|
}
|
|
2140
|
-
await attachCodeRuntimeReferences(prepared.workspace,
|
|
2540
|
+
await attachCodeRuntimeReferences(prepared.workspace, selected2.references ?? []);
|
|
2141
2541
|
}
|
|
2142
2542
|
}
|
|
2143
2543
|
return {
|
|
@@ -2147,6 +2547,63 @@ async function materializeCommandWorkspace(input) {
|
|
|
2147
2547
|
requestedLocal
|
|
2148
2548
|
};
|
|
2149
2549
|
}
|
|
2550
|
+
const selected = selectedSourceSet(command.payload);
|
|
2551
|
+
if (selected?.primary.materialization === "host_archive") {
|
|
2552
|
+
if (!input.control.sourceArchive) {
|
|
2553
|
+
throw new TypeError("Code runtime protocol does not support host source materialization");
|
|
2554
|
+
}
|
|
2555
|
+
const sources = [selected.primary, ...selected.references];
|
|
2556
|
+
const materialized2 = [];
|
|
2557
|
+
try {
|
|
2558
|
+
for (const descriptor2 of sources) {
|
|
2559
|
+
if (descriptor2.materialization !== "host_archive") {
|
|
2560
|
+
throw new TypeError("Code selected source set mixes incompatible materialization modes");
|
|
2561
|
+
}
|
|
2562
|
+
const archive = await input.control.sourceArchive(command.sessionId, descriptor2.alias);
|
|
2563
|
+
if (archive.repository !== descriptor2.repository || archive.commitSha !== descriptor2.commitSha) {
|
|
2564
|
+
throw new TypeError("Code source archive does not match its selected repository and commit");
|
|
2565
|
+
}
|
|
2566
|
+
const source2 = await materializeCodeRuntimeArchive(
|
|
2567
|
+
archive,
|
|
2568
|
+
descriptor2.visiblePaths ? new Set(descriptor2.visiblePaths) : void 0
|
|
2569
|
+
);
|
|
2570
|
+
const treeDigest = await digestStagedWorkspace(source2.sourceDir, {
|
|
2571
|
+
maxFiles: archive.limits.maxFiles,
|
|
2572
|
+
maxBytes: archive.limits.maxTotalFileBytes
|
|
2573
|
+
});
|
|
2574
|
+
materialized2.push({ descriptor: descriptor2, source: source2, treeDigest });
|
|
2575
|
+
}
|
|
2576
|
+
const primary = materialized2[0];
|
|
2577
|
+
const workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
|
|
2578
|
+
trustedBaseDir: primary.source.sourceDir,
|
|
2579
|
+
trustedBaseCommitSha: primary.descriptor.commitSha,
|
|
2580
|
+
checkpoint: codeCheckpointPayload(command.payload)
|
|
2581
|
+
})).workspace : await stageWorkspace(primary.source.sourceDir, {
|
|
2582
|
+
maxFiles: SOURCE_MAX_FILES,
|
|
2583
|
+
maxBytes: SOURCE_SET_MAX_BYTES
|
|
2584
|
+
});
|
|
2585
|
+
try {
|
|
2586
|
+
await attachReferenceDirectories(workspace, materialized2.slice(1).map((item) => ({
|
|
2587
|
+
alias: item.descriptor.alias,
|
|
2588
|
+
sourceDir: item.source.sourceDir
|
|
2589
|
+
})));
|
|
2590
|
+
} catch (cause) {
|
|
2591
|
+
await workspace.cleanup();
|
|
2592
|
+
throw cause;
|
|
2593
|
+
}
|
|
2594
|
+
return {
|
|
2595
|
+
workspace,
|
|
2596
|
+
sourceDigest: primary.treeDigest,
|
|
2597
|
+
requestedLocal: null,
|
|
2598
|
+
sourceDigests: materialized2.map((item) => ({
|
|
2599
|
+
alias: item.descriptor.alias,
|
|
2600
|
+
treeDigest: item.treeDigest
|
|
2601
|
+
}))
|
|
2602
|
+
};
|
|
2603
|
+
} finally {
|
|
2604
|
+
await Promise.allSettled(materialized2.map((item) => item.source.cleanup()));
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2150
2607
|
const source = await input.control.source(command.sessionId);
|
|
2151
2608
|
const materialized = await materializeCodeRuntimeSource(source);
|
|
2152
2609
|
try {
|
|
@@ -2201,10 +2658,12 @@ Prefer these over listing the tree \u2014 a full listing of a real repository is
|
|
|
2201
2658
|
of thousands of tokens and you will carry it for the rest of the session.
|
|
2202
2659
|
|
|
2203
2660
|
Then odla_search for a literal string, odla_read for a bounded range, and
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2661
|
+
odla_edit_file to change an existing file: give it the exact current text
|
|
2662
|
+
(copied verbatim from odla_read) and the text that replaces it; it must match
|
|
2663
|
+
once. Use odla_apply_git_diff only to create or delete whole files. A patch must
|
|
2664
|
+
start with "diff --git a/<path> b/<path>", include matching "---" and "+++"
|
|
2665
|
+
headers and numbered "@@" hunks with at least one line of surrounding context,
|
|
2666
|
+
and must never use "*** Begin Patch" wrappers.
|
|
2208
2667
|
|
|
2209
2668
|
The workspace, model, and tool effects are controlled by the host broker.
|
|
2210
2669
|
Never claim a build or test passed unless odla_run_recipe returned that result.`;
|
|
@@ -2264,6 +2723,21 @@ function codeSkill(opts) {
|
|
|
2264
2723
|
},
|
|
2265
2724
|
handler: (input, ctx) => call("sandbox.apply_patch", input, ctx.signal)
|
|
2266
2725
|
};
|
|
2726
|
+
const editFile = {
|
|
2727
|
+
name: "odla_edit_file",
|
|
2728
|
+
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.",
|
|
2729
|
+
inputSchema: {
|
|
2730
|
+
type: "object",
|
|
2731
|
+
required: ["path", "oldText", "newText"],
|
|
2732
|
+
properties: {
|
|
2733
|
+
path: { type: "string", minLength: 1, maxLength: 1024 },
|
|
2734
|
+
oldText: { type: "string", minLength: 1, maxLength: 262144 },
|
|
2735
|
+
newText: { type: "string", maxLength: 262144 }
|
|
2736
|
+
},
|
|
2737
|
+
additionalProperties: false
|
|
2738
|
+
},
|
|
2739
|
+
handler: (input, ctx) => call("sandbox.edit", input, ctx.signal)
|
|
2740
|
+
};
|
|
2267
2741
|
const runRecipe = {
|
|
2268
2742
|
name: "odla_run_recipe",
|
|
2269
2743
|
description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
|
|
@@ -2351,7 +2825,7 @@ function codeSkill(opts) {
|
|
|
2351
2825
|
)
|
|
2352
2826
|
];
|
|
2353
2827
|
const effects = opts.readOnly ? [] : [applyPatch, runRecipe];
|
|
2354
|
-
const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
|
|
2828
|
+
const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
|
|
2355
2829
|
return { name: "code", tools };
|
|
2356
2830
|
}
|
|
2357
2831
|
|
|
@@ -2484,14 +2958,40 @@ async function sessionSkillsFor(options, command) {
|
|
|
2484
2958
|
}
|
|
2485
2959
|
|
|
2486
2960
|
// src/code-runtime-inference.ts
|
|
2961
|
+
var OVERLOAD_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3];
|
|
2962
|
+
var RETRYABLE_CODES = /* @__PURE__ */ new Set(["control_plane_overloaded", "registry_overloaded", "transport_unavailable"]);
|
|
2963
|
+
function overloadedControlFailure(cause) {
|
|
2964
|
+
return cause instanceof CodeRuntimeControlError && cause.status === 503 && RETRYABLE_CODES.has(cause.code);
|
|
2965
|
+
}
|
|
2966
|
+
async function inferWithBackoff(infer, wait2, onRetry) {
|
|
2967
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
2968
|
+
try {
|
|
2969
|
+
return await infer();
|
|
2970
|
+
} catch (cause) {
|
|
2971
|
+
const delayMs = OVERLOAD_RETRY_DELAYS_MS[attempt];
|
|
2972
|
+
if (delayMs === void 0 || !overloadedControlFailure(cause)) throw cause;
|
|
2973
|
+
await onRetry(cause, delayMs);
|
|
2974
|
+
await wait2(delayMs);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2487
2978
|
async function handleCodeRuntimeInference(input) {
|
|
2488
2979
|
const { command, request, state } = input;
|
|
2489
2980
|
const startedAt = Date.now();
|
|
2490
|
-
const
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2981
|
+
const wait2 = input.wait ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
|
|
2982
|
+
const response2 = await inferWithBackoff(
|
|
2983
|
+
() => input.control.infer(command.sessionId, {
|
|
2984
|
+
requestId: request.requestId,
|
|
2985
|
+
interactionId: command.commandId,
|
|
2986
|
+
call: request.call
|
|
2987
|
+
}),
|
|
2988
|
+
wait2,
|
|
2989
|
+
(cause, delayMs) => input.event({
|
|
2990
|
+
type: "diagnostic",
|
|
2991
|
+
level: "error",
|
|
2992
|
+
message: `Code control plane overloaded (${cause.code}); retrying the model call in ${delayMs / 1e3}s`
|
|
2993
|
+
}).catch(() => void 0)
|
|
2994
|
+
);
|
|
2495
2995
|
state.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
|
|
2496
2996
|
const { costUsd } = response2.receipt;
|
|
2497
2997
|
if (costUsd === void 0) state.costKnown = false;
|
|
@@ -2615,6 +3115,10 @@ function codeToolRequestPresentation(request) {
|
|
|
2615
3115
|
if (request.tool === "sandbox.apply_patch") {
|
|
2616
3116
|
return { kind: "patch", ...patchStats(input.patch) };
|
|
2617
3117
|
}
|
|
3118
|
+
if (request.tool === "sandbox.edit") {
|
|
3119
|
+
const lines = (value) => typeof value === "string" ? value.slice(0, 262144).split("\n").length : 0;
|
|
3120
|
+
return { kind: "patch", additions: lines(input.newText), deletions: lines(input.oldText) };
|
|
3121
|
+
}
|
|
2618
3122
|
const recipeId = text(input.recipeId, 120);
|
|
2619
3123
|
return recipeId ? { kind: "recipe", recipeId } : void 0;
|
|
2620
3124
|
}
|
|
@@ -2742,6 +3246,11 @@ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
|
|
|
2742
3246
|
authority: "authority",
|
|
2743
3247
|
patch: "payload"
|
|
2744
3248
|
});
|
|
3249
|
+
var EDIT = descriptor("sandbox.edit", "reversible_mutation", {
|
|
3250
|
+
workspace: "destination",
|
|
3251
|
+
authority: "authority",
|
|
3252
|
+
patch: "payload"
|
|
3253
|
+
});
|
|
2745
3254
|
var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
|
|
2746
3255
|
workspace: "destination",
|
|
2747
3256
|
authority: "authority",
|
|
@@ -2809,6 +3318,14 @@ function createCodePolicyGate(options) {
|
|
|
2809
3318
|
patch: { role: "payload", value: patch2 }
|
|
2810
3319
|
}, []);
|
|
2811
3320
|
},
|
|
3321
|
+
edit: async (input) => {
|
|
3322
|
+
const base = await environment(input, options, "sandbox.edit");
|
|
3323
|
+
const patch2 = unsafe(base, input.patch, "patch");
|
|
3324
|
+
return authorize(input, options, base, EDIT, {
|
|
3325
|
+
...base.fixedArgs,
|
|
3326
|
+
patch: { role: "payload", value: patch2 }
|
|
3327
|
+
}, []);
|
|
3328
|
+
},
|
|
2812
3329
|
recipe: async (input) => {
|
|
2813
3330
|
const base = await environment(input, options, "sandbox.run_recipe");
|
|
2814
3331
|
const conversions = await conversionRegistry([
|
|
@@ -2953,13 +3470,134 @@ function response(request, ok, content, details) {
|
|
|
2953
3470
|
return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
|
|
2954
3471
|
}
|
|
2955
3472
|
|
|
2956
|
-
// src/code-tool-
|
|
3473
|
+
// src/code-tool-edit.ts
|
|
3474
|
+
var import_promises12 = require("fs/promises");
|
|
3475
|
+
|
|
3476
|
+
// src/code-tool-graph.ts
|
|
2957
3477
|
var import_promises11 = require("fs/promises");
|
|
3478
|
+
var import_node_path11 = require("path");
|
|
3479
|
+
var import_graph = require("@odla-ai/graph");
|
|
3480
|
+
var import_code4 = require("@odla-ai/graph/code");
|
|
3481
|
+
var cache = /* @__PURE__ */ new Map();
|
|
3482
|
+
function workspaceGraphs(workspaceDir, paths2) {
|
|
3483
|
+
const existing = cache.get(workspaceDir);
|
|
3484
|
+
if (existing) return existing;
|
|
3485
|
+
const read2 = (path) => (0, import_promises11.readFile)((0, import_node_path11.join)(workspaceDir, path), "utf8");
|
|
3486
|
+
const built = (async () => ({
|
|
3487
|
+
// No knownTables: a staged workspace may not carry migrations, and a filter
|
|
3488
|
+
// that silently drops every table is worse than an unfiltered one. Callers
|
|
3489
|
+
// with ground truth should build the graph themselves.
|
|
3490
|
+
graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
|
|
3491
|
+
}))();
|
|
3492
|
+
cache.set(workspaceDir, built);
|
|
3493
|
+
return built;
|
|
3494
|
+
}
|
|
3495
|
+
function forgetWorkspaceGraphs(workspaceDir) {
|
|
3496
|
+
cache.delete(workspaceDir);
|
|
3497
|
+
}
|
|
3498
|
+
var shortId = (id) => id.slice(id.indexOf(":") + 1);
|
|
3499
|
+
function renderOverview(graphs, prefix) {
|
|
3500
|
+
const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
|
|
3501
|
+
if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
|
|
3502
|
+
const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
|
|
3503
|
+
const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
|
|
3504
|
+
return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
|
|
3505
|
+
}
|
|
3506
|
+
function renderWhereIs(graphs, symbol) {
|
|
3507
|
+
const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
|
|
3508
|
+
path: shortId(id),
|
|
3509
|
+
pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
|
|
3510
|
+
dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
|
|
3511
|
+
})).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
|
|
3512
|
+
if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
|
|
3513
|
+
return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
|
|
3514
|
+
}
|
|
3515
|
+
function renderWhoImports(graphs, path) {
|
|
3516
|
+
const id = (0, import_graph.nodeId)(import_code4.FILE, path);
|
|
3517
|
+
const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
|
|
3518
|
+
if (importers.length === 0) {
|
|
3519
|
+
return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
|
|
3520
|
+
}
|
|
3521
|
+
return importers.slice(0, 40).map(shortId).sort().join("\n");
|
|
3522
|
+
}
|
|
3523
|
+
function renderWhoTouches(graphs, query) {
|
|
3524
|
+
const needle = query.toLowerCase();
|
|
3525
|
+
const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
|
|
3526
|
+
if (hits.length === 0) return `No table or namespace matching "${query}".`;
|
|
3527
|
+
return hits.map((hit) => {
|
|
3528
|
+
const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
|
|
3529
|
+
return [
|
|
3530
|
+
`${hit.name} (${hit.kind})`,
|
|
3531
|
+
` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
|
|
3532
|
+
` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
|
|
3533
|
+
].join("\n");
|
|
3534
|
+
}).join("\n\n");
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
// src/code-tool-edit.ts
|
|
3538
|
+
var MAX_TEXT_BYTES = 256 * 1024;
|
|
3539
|
+
function editAsDiff(path, oldText, newText) {
|
|
3540
|
+
const removed = oldText.split("\n").map((line) => `-${line}`);
|
|
3541
|
+
const added = newText.split("\n").map((line) => `+${line}`);
|
|
3542
|
+
return `diff --git a/${path} b/${path}
|
|
3543
|
+
--- a/${path}
|
|
3544
|
+
+++ b/${path}
|
|
3545
|
+
@@ -1,${removed.length} +1,${added.length} @@
|
|
3546
|
+
${[...removed, ...added].join("\n")}
|
|
3547
|
+
`;
|
|
3548
|
+
}
|
|
3549
|
+
function occurrences(haystack, needle) {
|
|
3550
|
+
let count = 0;
|
|
3551
|
+
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count += 1;
|
|
3552
|
+
return count;
|
|
3553
|
+
}
|
|
3554
|
+
async function editCodeFile(workspaceDir, path, oldText, newText) {
|
|
3555
|
+
if (!oldText) throw new TypeError("oldText must not be empty; to create a file use sandbox.apply_patch with a new-file diff");
|
|
3556
|
+
if (Buffer.byteLength(oldText) > MAX_TEXT_BYTES || Buffer.byteLength(newText) > MAX_TEXT_BYTES) {
|
|
3557
|
+
throw new TypeError(`oldText and newText must each stay under ${MAX_TEXT_BYTES} bytes; split the change`);
|
|
3558
|
+
}
|
|
3559
|
+
if (oldText.includes("\0") || newText.includes("\0")) throw new TypeError("edit text contains NUL bytes; use plain text");
|
|
3560
|
+
const target = resolveCodePath(workspaceDir, path);
|
|
3561
|
+
const current = await (0, import_promises12.readFile)(target, "utf8");
|
|
3562
|
+
const found = occurrences(current, oldText);
|
|
3563
|
+
if (found === 0) {
|
|
3564
|
+
throw new TypeError(`oldText was not found in "${path}"; sandbox.read the current lines and copy them exactly, including indentation and blank lines`);
|
|
3565
|
+
}
|
|
3566
|
+
if (found > 1) {
|
|
3567
|
+
throw new TypeError(`oldText occurs ${found} times in "${path}"; include more of the surrounding lines so it matches exactly once`);
|
|
3568
|
+
}
|
|
3569
|
+
const at = current.indexOf(oldText);
|
|
3570
|
+
await (0, import_promises12.writeFile)(target, `${current.slice(0, at)}${newText}${current.slice(at + oldText.length)}`, "utf8");
|
|
3571
|
+
return { deletions: oldText.split("\n").length, additions: newText.split("\n").length };
|
|
3572
|
+
}
|
|
3573
|
+
async function edit(context, request, options, policy, registry) {
|
|
3574
|
+
exactKeys(request.input, ["path", "oldText", "newText"]);
|
|
3575
|
+
const path = stringField(request.input, "path");
|
|
3576
|
+
const oldText = stringField(request.input, "oldText");
|
|
3577
|
+
const newText = typeof request.input.newText === "string" ? request.input.newText : stringField(request.input, "newText");
|
|
3578
|
+
const paths2 = await registry.files(context.workspaceDir);
|
|
3579
|
+
if (!paths2.includes(path)) {
|
|
3580
|
+
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.`);
|
|
3581
|
+
}
|
|
3582
|
+
if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
|
|
3583
|
+
throw new TypeError("edit targets a read-only reference source");
|
|
3584
|
+
}
|
|
3585
|
+
const patch2 = editAsDiff(path, oldText, newText);
|
|
3586
|
+
const allowed = await policy.edit(policyContext(context, request, options, { patch: patch2 }));
|
|
3587
|
+
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3588
|
+
const changed = await editCodeFile(context.workspaceDir, path, oldText, newText);
|
|
3589
|
+
registry.invalidate(context.workspaceDir);
|
|
3590
|
+
forgetWorkspaceGraphs(context.workspaceDir);
|
|
3591
|
+
return response(request, true, `Edited ${path}: -${changed.deletions} +${changed.additions} line(s).`, { paths: [path], ...changed });
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// src/code-tool-reads.ts
|
|
3595
|
+
var import_promises14 = require("fs/promises");
|
|
2958
3596
|
|
|
2959
3597
|
// src/code-tool-discovery.ts
|
|
2960
3598
|
var import_node_child_process6 = require("child_process");
|
|
2961
|
-
var
|
|
2962
|
-
var
|
|
3599
|
+
var import_promises13 = require("fs/promises");
|
|
3600
|
+
var import_node_path12 = require("path");
|
|
2963
3601
|
var DEFAULT_MAX_FILES = 2e4;
|
|
2964
3602
|
var DEFAULT_MAX_RESULTS = 100;
|
|
2965
3603
|
var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
@@ -2984,13 +3622,13 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
|
|
|
2984
3622
|
async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
2985
3623
|
const paths2 = [];
|
|
2986
3624
|
const walk = async (directory) => {
|
|
2987
|
-
for (const entry of await (0,
|
|
3625
|
+
for (const entry of await (0, import_promises13.readdir)(directory, { withFileTypes: true })) {
|
|
2988
3626
|
if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
2989
3627
|
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
2990
|
-
const target = (0,
|
|
3628
|
+
const target = (0, import_node_path12.resolve)(directory, entry.name);
|
|
2991
3629
|
if (entry.isDirectory()) await walk(target);
|
|
2992
3630
|
else if (entry.isFile()) {
|
|
2993
|
-
const path = (0,
|
|
3631
|
+
const path = (0, import_node_path12.relative)(root, target).split("\\").join("/");
|
|
2994
3632
|
try {
|
|
2995
3633
|
validateRelativePath(path);
|
|
2996
3634
|
} catch {
|
|
@@ -3001,7 +3639,7 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
|
3001
3639
|
}
|
|
3002
3640
|
}
|
|
3003
3641
|
};
|
|
3004
|
-
await walk((0,
|
|
3642
|
+
await walk((0, import_node_path12.resolve)(root));
|
|
3005
3643
|
return paths2.sort();
|
|
3006
3644
|
}
|
|
3007
3645
|
function listWorkspace(paths2, options = {}) {
|
|
@@ -3115,7 +3753,7 @@ async function fallbackSearch(root, scoped, options) {
|
|
|
3115
3753
|
if (matches.length >= options.maxResults) break;
|
|
3116
3754
|
let source;
|
|
3117
3755
|
try {
|
|
3118
|
-
source = await (0,
|
|
3756
|
+
source = await (0, import_promises13.readFile)((0, import_node_path12.resolve)(root, path));
|
|
3119
3757
|
} catch {
|
|
3120
3758
|
continue;
|
|
3121
3759
|
}
|
|
@@ -3132,67 +3770,6 @@ async function fallbackSearch(root, scoped, options) {
|
|
|
3132
3770
|
return matches;
|
|
3133
3771
|
}
|
|
3134
3772
|
|
|
3135
|
-
// src/code-tool-graph.ts
|
|
3136
|
-
var import_promises10 = require("fs/promises");
|
|
3137
|
-
var import_node_path10 = require("path");
|
|
3138
|
-
var import_graph = require("@odla-ai/graph");
|
|
3139
|
-
var import_code4 = require("@odla-ai/graph/code");
|
|
3140
|
-
var cache = /* @__PURE__ */ new Map();
|
|
3141
|
-
function workspaceGraphs(workspaceDir, paths2) {
|
|
3142
|
-
const existing = cache.get(workspaceDir);
|
|
3143
|
-
if (existing) return existing;
|
|
3144
|
-
const read2 = (path) => (0, import_promises10.readFile)((0, import_node_path10.join)(workspaceDir, path), "utf8");
|
|
3145
|
-
const built = (async () => ({
|
|
3146
|
-
// No knownTables: a staged workspace may not carry migrations, and a filter
|
|
3147
|
-
// that silently drops every table is worse than an unfiltered one. Callers
|
|
3148
|
-
// with ground truth should build the graph themselves.
|
|
3149
|
-
graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
|
|
3150
|
-
}))();
|
|
3151
|
-
cache.set(workspaceDir, built);
|
|
3152
|
-
return built;
|
|
3153
|
-
}
|
|
3154
|
-
function forgetWorkspaceGraphs(workspaceDir) {
|
|
3155
|
-
cache.delete(workspaceDir);
|
|
3156
|
-
}
|
|
3157
|
-
var shortId = (id) => id.slice(id.indexOf(":") + 1);
|
|
3158
|
-
function renderOverview(graphs, prefix) {
|
|
3159
|
-
const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
|
|
3160
|
-
if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
|
|
3161
|
-
const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
|
|
3162
|
-
const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
|
|
3163
|
-
return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
|
|
3164
|
-
}
|
|
3165
|
-
function renderWhereIs(graphs, symbol) {
|
|
3166
|
-
const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
|
|
3167
|
-
path: shortId(id),
|
|
3168
|
-
pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
|
|
3169
|
-
dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
|
|
3170
|
-
})).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
|
|
3171
|
-
if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
|
|
3172
|
-
return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
|
|
3173
|
-
}
|
|
3174
|
-
function renderWhoImports(graphs, path) {
|
|
3175
|
-
const id = (0, import_graph.nodeId)(import_code4.FILE, path);
|
|
3176
|
-
const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
|
|
3177
|
-
if (importers.length === 0) {
|
|
3178
|
-
return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
|
|
3179
|
-
}
|
|
3180
|
-
return importers.slice(0, 40).map(shortId).sort().join("\n");
|
|
3181
|
-
}
|
|
3182
|
-
function renderWhoTouches(graphs, query) {
|
|
3183
|
-
const needle = query.toLowerCase();
|
|
3184
|
-
const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
|
|
3185
|
-
if (hits.length === 0) return `No table or namespace matching "${query}".`;
|
|
3186
|
-
return hits.map((hit) => {
|
|
3187
|
-
const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
|
|
3188
|
-
return [
|
|
3189
|
-
`${hit.name} (${hit.kind})`,
|
|
3190
|
-
` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
|
|
3191
|
-
` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
|
|
3192
|
-
].join("\n");
|
|
3193
|
-
}).join("\n\n");
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
3773
|
// src/code-tool-reads.ts
|
|
3197
3774
|
var GRAPH_TOOLS = /* @__PURE__ */ new Set([
|
|
3198
3775
|
"sandbox.overview",
|
|
@@ -3215,11 +3792,11 @@ async function read(context, request, options, policy, registry) {
|
|
|
3215
3792
|
const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
|
|
3216
3793
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3217
3794
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
3218
|
-
const info = await (0,
|
|
3795
|
+
const info = await (0, import_promises14.stat)(target);
|
|
3219
3796
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
3220
3797
|
throw new TypeError("file is not a bounded regular source file");
|
|
3221
3798
|
}
|
|
3222
|
-
const source = await (0,
|
|
3799
|
+
const source = await (0, import_promises14.readFile)(target);
|
|
3223
3800
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
3224
3801
|
const lines = source.toString("utf8").split("\n");
|
|
3225
3802
|
const content = lines.slice(startLine - 1, endLine).join("\n");
|
|
@@ -3334,6 +3911,7 @@ async function route(context, request, options, recipes, policy, registry) {
|
|
|
3334
3911
|
if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
|
|
3335
3912
|
if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
|
|
3336
3913
|
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
|
|
3914
|
+
if (request.tool === "sandbox.edit") return await edit(context, request, options, policy, registry);
|
|
3337
3915
|
return await recipe(context, request, options, recipes, policy);
|
|
3338
3916
|
} catch (reason) {
|
|
3339
3917
|
return response(request, false, toolFailureMessage(reason));
|
|
@@ -3764,8 +4342,8 @@ var runtimeErrorMessage = (value) => value instanceof Error ? value.message : St
|
|
|
3764
4342
|
function codeRuntimeAcknowledgementGate(signal) {
|
|
3765
4343
|
let settle;
|
|
3766
4344
|
let settled = false;
|
|
3767
|
-
const ready = new Promise((
|
|
3768
|
-
settle =
|
|
4345
|
+
const ready = new Promise((resolve8) => {
|
|
4346
|
+
settle = resolve8;
|
|
3769
4347
|
});
|
|
3770
4348
|
const release = (run) => {
|
|
3771
4349
|
if (settled) return;
|
|
@@ -3874,7 +4452,7 @@ var TheseusRuntimeEngine = class {
|
|
|
3874
4452
|
async #start(command, resume) {
|
|
3875
4453
|
if (this.#active.has(command.sessionId)) throw new TypeError("Code session is already active on this runtime");
|
|
3876
4454
|
const metadata = codeCommandMetadata(command.payload, resume);
|
|
3877
|
-
const { workspace, sourceDigest, localTrustedBaseDigest, requestedLocal } = await materializeCommandWorkspace({
|
|
4455
|
+
const { workspace, sourceDigest, sourceDigests, localTrustedBaseDigest, requestedLocal } = await materializeCommandWorkspace({
|
|
3878
4456
|
command,
|
|
3879
4457
|
metadata,
|
|
3880
4458
|
resume,
|
|
@@ -3895,7 +4473,7 @@ var TheseusRuntimeEngine = class {
|
|
|
3895
4473
|
maxTokensPerInteraction: metadata.maxTokensPerInteraction,
|
|
3896
4474
|
baseCommitSha: metadata.baseCommitSha,
|
|
3897
4475
|
repository: metadata.repository,
|
|
3898
|
-
sourceTreeDigest: metadata.sourceTreeDigest,
|
|
4476
|
+
sourceTreeDigest: metadata.sourceTreeDigest ?? sourceDigest,
|
|
3899
4477
|
trustedBaseDigest: requestedLocal ? localTrustedBaseDigest : await digestStagedWorkspace(workspace.baselineDir, {
|
|
3900
4478
|
maxFiles: 2e4,
|
|
3901
4479
|
maxBytes: 512 * 1024 * 1024
|
|
@@ -3921,7 +4499,11 @@ var TheseusRuntimeEngine = class {
|
|
|
3921
4499
|
await this.#failure(command, active, detail);
|
|
3922
4500
|
return null;
|
|
3923
4501
|
});
|
|
3924
|
-
return {
|
|
4502
|
+
return {
|
|
4503
|
+
status: "running",
|
|
4504
|
+
message: resume ? "Theseus resumed from a portable checkpoint" : "Theseus started",
|
|
4505
|
+
...sourceDigests ? { sourceDigests } : {}
|
|
4506
|
+
};
|
|
3925
4507
|
}
|
|
3926
4508
|
/**
|
|
3927
4509
|
* Pursue a goal: attempt, judge with the clean verifier, re-prompt from what
|
|
@@ -4370,12 +4952,12 @@ function chooseStrategy(signals = {}) {
|
|
|
4370
4952
|
var feedbackIsActionable = actionable;
|
|
4371
4953
|
|
|
4372
4954
|
// src/code-recipe-dependencies.ts
|
|
4373
|
-
var
|
|
4374
|
-
var
|
|
4955
|
+
var import_promises15 = require("fs/promises");
|
|
4956
|
+
var import_node_path13 = require("path");
|
|
4375
4957
|
var RESERVED_MOUNTS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
|
|
4376
4958
|
function withRecipeDependencies(executor, dependencies) {
|
|
4377
4959
|
const mountAs = dependencies.mountAs ?? "node_modules";
|
|
4378
|
-
if (!(0,
|
|
4960
|
+
if (!(0, import_node_path13.isAbsolute)(dependencies.source)) {
|
|
4379
4961
|
throw new TypeError("recipe dependency source must be an absolute path");
|
|
4380
4962
|
}
|
|
4381
4963
|
if (!RESERVED_MOUNTS.has(mountAs)) {
|
|
@@ -4383,24 +4965,24 @@ function withRecipeDependencies(executor, dependencies) {
|
|
|
4383
4965
|
}
|
|
4384
4966
|
return {
|
|
4385
4967
|
run: async (input) => {
|
|
4386
|
-
const target = (0,
|
|
4968
|
+
const target = (0, import_node_path13.join)(input.workspaceDir, mountAs);
|
|
4387
4969
|
let linked = false;
|
|
4388
4970
|
try {
|
|
4389
|
-
const existing = await (0,
|
|
4971
|
+
const existing = await (0, import_promises15.lstat)(target).catch(() => null);
|
|
4390
4972
|
if (!existing) {
|
|
4391
|
-
await (0,
|
|
4973
|
+
await (0, import_promises15.symlink)(dependencies.source, target, "dir");
|
|
4392
4974
|
linked = true;
|
|
4393
4975
|
}
|
|
4394
4976
|
return await executor.run(input);
|
|
4395
4977
|
} finally {
|
|
4396
|
-
if (linked) await (0,
|
|
4978
|
+
if (linked) await (0, import_promises15.rm)(target, { force: true, recursive: false }).catch(() => void 0);
|
|
4397
4979
|
}
|
|
4398
4980
|
}
|
|
4399
4981
|
};
|
|
4400
4982
|
}
|
|
4401
4983
|
async function installedDependencies(repoRoot) {
|
|
4402
|
-
const source = (0,
|
|
4403
|
-
const info = await (0,
|
|
4984
|
+
const source = (0, import_node_path13.join)(repoRoot, "node_modules");
|
|
4985
|
+
const info = await (0, import_promises15.lstat)(source).catch(() => null);
|
|
4404
4986
|
return info?.isDirectory() ? { source } : null;
|
|
4405
4987
|
}
|
|
4406
4988
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -4441,6 +5023,7 @@ async function installedDependencies(repoRoot) {
|
|
|
4441
5023
|
installedDependencies,
|
|
4442
5024
|
integrateSubGoals,
|
|
4443
5025
|
isCheckpointEffectCompleted,
|
|
5026
|
+
materializeCodeRuntimeArchive,
|
|
4444
5027
|
materializeCodeRuntimeSource,
|
|
4445
5028
|
materializeCommandWorkspace,
|
|
4446
5029
|
materializeGitTree,
|