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