@odla-ai/cli 0.14.1 → 0.16.0
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/README.md +22 -15
- package/dist/bin.cjs +740 -280
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-AN6KZMR5.js → chunk-S3OYXHDG.js} +745 -285
- package/dist/chunk-S3OYXHDG.js.map +1 -0
- package/dist/index.cjs +740 -280
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -8
- package/dist/index.d.ts +53 -8
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla-migrate/references/phase-2-db.md +2 -2
- package/dist/chunk-AN6KZMR5.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -509,12 +509,13 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
509
509
|
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
510
510
|
endpoint: audience,
|
|
511
511
|
email,
|
|
512
|
-
label: options.label ?? `odla CLI
|
|
512
|
+
label: options.label ?? `odla CLI (${scope})`,
|
|
513
513
|
scopes: [scope],
|
|
514
514
|
fetch: doFetch,
|
|
515
515
|
onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
|
|
516
516
|
const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
|
|
517
|
-
|
|
517
|
+
const requestName = scope === "app:code:host:connect" ? "terminal connection" : "scoped request";
|
|
518
|
+
out.log(`Review, then approve ${requestName} ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
518
519
|
if (approvalBrowser({ open: options.open }).open) {
|
|
519
520
|
await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
520
521
|
}
|
|
@@ -576,8 +577,8 @@ async function responseBody(response2) {
|
|
|
576
577
|
}
|
|
577
578
|
function apiError(status, body) {
|
|
578
579
|
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
579
|
-
const
|
|
580
|
-
return `read System AI admin changes failed (${status}): ${
|
|
580
|
+
const message2 = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
581
|
+
return `read System AI admin changes failed (${status}): ${message2}`;
|
|
581
582
|
}
|
|
582
583
|
function timestamp(value) {
|
|
583
584
|
if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
|
|
@@ -678,8 +679,8 @@ async function responseBody2(res) {
|
|
|
678
679
|
}
|
|
679
680
|
function apiError2(action, status, body) {
|
|
680
681
|
const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
|
|
681
|
-
const
|
|
682
|
-
return `${action} failed (${status}): ${
|
|
682
|
+
const message2 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
683
|
+
return `${action} failed (${status}): ${message2}`;
|
|
683
684
|
}
|
|
684
685
|
function isRecord2(value) {
|
|
685
686
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -859,8 +860,8 @@ async function responseBody3(res) {
|
|
|
859
860
|
}
|
|
860
861
|
function apiError3(action, status, body) {
|
|
861
862
|
const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
|
|
862
|
-
const
|
|
863
|
-
return `${action} failed (${status}): ${
|
|
863
|
+
const message2 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
864
|
+
return `${action} failed (${status}): ${message2}`;
|
|
864
865
|
}
|
|
865
866
|
function isRecord3(value) {
|
|
866
867
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -1540,8 +1541,8 @@ var CAPABILITIES = {
|
|
|
1540
1541
|
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
1541
1542
|
]
|
|
1542
1543
|
};
|
|
1543
|
-
function printCapabilities(
|
|
1544
|
-
if (
|
|
1544
|
+
function printCapabilities(json = false, out = console) {
|
|
1545
|
+
if (json) {
|
|
1545
1546
|
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
1546
1547
|
return;
|
|
1547
1548
|
}
|
|
@@ -1571,8 +1572,8 @@ var CalendarRequestError = class extends Error {
|
|
|
1571
1572
|
status;
|
|
1572
1573
|
/** Machine-readable `error.code` from the response body, when present. */
|
|
1573
1574
|
code;
|
|
1574
|
-
constructor(
|
|
1575
|
-
super(
|
|
1575
|
+
constructor(message2, status, code) {
|
|
1576
|
+
super(message2);
|
|
1576
1577
|
this.name = "CalendarRequestError";
|
|
1577
1578
|
this.status = status;
|
|
1578
1579
|
if (code !== void 0) this.code = code;
|
|
@@ -1739,8 +1740,8 @@ async function calendarJson(ctx, suffix, init) {
|
|
|
1739
1740
|
const body = await response2.json().catch(() => ({}));
|
|
1740
1741
|
if (!response2.ok) {
|
|
1741
1742
|
const code = textField(body.error?.code, 128);
|
|
1742
|
-
const
|
|
1743
|
-
const detail = `${code ? ` ${code}` : ""}${
|
|
1743
|
+
const message2 = textField(body.error?.message, 500);
|
|
1744
|
+
const detail = `${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`;
|
|
1744
1745
|
throw new CalendarRequestError(redactSecrets(`calendar request failed (${response2.status})${detail}`), response2.status, code);
|
|
1745
1746
|
}
|
|
1746
1747
|
return body;
|
|
@@ -1976,8 +1977,8 @@ function pollTimeout(value = 10 * 6e4) {
|
|
|
1976
1977
|
function productionConsent(env, yes, action) {
|
|
1977
1978
|
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action} for "${env}" without --yes`);
|
|
1978
1979
|
}
|
|
1979
|
-
function printStatus(status,
|
|
1980
|
-
if (
|
|
1980
|
+
function printStatus(status, json, out) {
|
|
1981
|
+
if (json) {
|
|
1981
1982
|
out.log(JSON.stringify(status, null, 2));
|
|
1982
1983
|
return;
|
|
1983
1984
|
}
|
|
@@ -2019,63 +2020,63 @@ function parseAgentOutput(line) {
|
|
|
2019
2020
|
} catch {
|
|
2020
2021
|
throw new HarnessProtocolError("agent emitted invalid JSON");
|
|
2021
2022
|
}
|
|
2022
|
-
const
|
|
2023
|
-
if (!
|
|
2023
|
+
const message2 = record2(value);
|
|
2024
|
+
if (!message2 || message2.protocolVersion !== HARNESS_PROTOCOL_VERSION) {
|
|
2024
2025
|
throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);
|
|
2025
2026
|
}
|
|
2026
|
-
if (
|
|
2027
|
+
if (message2.type === "event") {
|
|
2027
2028
|
return {
|
|
2028
2029
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
2029
2030
|
type: "event",
|
|
2030
|
-
kind: boundedText(
|
|
2031
|
-
...
|
|
2031
|
+
kind: boundedText(message2.kind, "event.kind", 120),
|
|
2032
|
+
...message2.payload === void 0 ? {} : { payload: message2.payload }
|
|
2032
2033
|
};
|
|
2033
2034
|
}
|
|
2034
|
-
if (
|
|
2035
|
-
const call = record2(
|
|
2035
|
+
if (message2.type === "inference.request") {
|
|
2036
|
+
const call = record2(message2.call);
|
|
2036
2037
|
if (!call || !Array.isArray(call.messages) || !Number.isSafeInteger(call.maxTokens)) {
|
|
2037
2038
|
throw new HarnessProtocolError("inference.request.call requires messages and maxTokens");
|
|
2038
2039
|
}
|
|
2039
2040
|
return {
|
|
2040
2041
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
2041
2042
|
type: "inference.request",
|
|
2042
|
-
requestId: boundedText(
|
|
2043
|
+
requestId: boundedText(message2.requestId, "requestId", 180),
|
|
2043
2044
|
call
|
|
2044
2045
|
};
|
|
2045
2046
|
}
|
|
2046
|
-
if (
|
|
2047
|
-
const input = record2(
|
|
2048
|
-
const tool = String(
|
|
2047
|
+
if (message2.type === "tool.request") {
|
|
2048
|
+
const input = record2(message2.input);
|
|
2049
|
+
const tool = String(message2.tool);
|
|
2049
2050
|
if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
|
|
2050
2051
|
throw new HarnessProtocolError("tool.request requires a registered tool and object input");
|
|
2051
2052
|
}
|
|
2052
2053
|
return {
|
|
2053
2054
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
2054
2055
|
type: "tool.request",
|
|
2055
|
-
requestId: boundedText(
|
|
2056
|
+
requestId: boundedText(message2.requestId, "requestId", 180),
|
|
2056
2057
|
tool,
|
|
2057
2058
|
input
|
|
2058
2059
|
};
|
|
2059
2060
|
}
|
|
2060
|
-
if (
|
|
2061
|
-
if (!(/* @__PURE__ */ new Set(["completed", "failed", "cancelled"])).has(String(
|
|
2061
|
+
if (message2.type === "attempt.complete") {
|
|
2062
|
+
if (!(/* @__PURE__ */ new Set(["completed", "failed", "cancelled"])).has(String(message2.status))) {
|
|
2062
2063
|
throw new HarnessProtocolError("attempt.complete.status is invalid");
|
|
2063
2064
|
}
|
|
2064
2065
|
return {
|
|
2065
2066
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
2066
2067
|
type: "attempt.complete",
|
|
2067
|
-
status:
|
|
2068
|
-
...
|
|
2068
|
+
status: message2.status,
|
|
2069
|
+
...message2.result === void 0 ? {} : { result: message2.result }
|
|
2069
2070
|
};
|
|
2070
2071
|
}
|
|
2071
2072
|
throw new HarnessProtocolError("agent message type is unsupported");
|
|
2072
2073
|
}
|
|
2073
|
-
function encodeAgentInput(
|
|
2074
|
-
return `${JSON.stringify(
|
|
2074
|
+
function encodeAgentInput(message2) {
|
|
2075
|
+
return `${JSON.stringify(message2)}
|
|
2075
2076
|
`;
|
|
2076
2077
|
}
|
|
2077
2078
|
|
|
2078
|
-
// ../harness/dist/chunk-
|
|
2079
|
+
// ../harness/dist/chunk-TA2FKK27.js
|
|
2079
2080
|
var import_child_process = require("child_process");
|
|
2080
2081
|
var import_fs = require("fs");
|
|
2081
2082
|
var import_promises2 = require("fs/promises");
|
|
@@ -2085,6 +2086,11 @@ var import_promises3 = require("fs/promises");
|
|
|
2085
2086
|
var import_os = require("os");
|
|
2086
2087
|
var import_path2 = require("path");
|
|
2087
2088
|
var import_child_process2 = require("child_process");
|
|
2089
|
+
var import_path3 = require("path");
|
|
2090
|
+
var import_promises4 = require("fs/promises");
|
|
2091
|
+
var import_os2 = require("os");
|
|
2092
|
+
var import_path4 = require("path");
|
|
2093
|
+
var import_child_process3 = require("child_process");
|
|
2088
2094
|
var DIGEST_IMAGE = /^[a-z0-9][a-z0-9._/-]*(?::[a-zA-Z0-9._-]+)?@sha256:[0-9a-f]{64}$/;
|
|
2089
2095
|
function assertPinnedImage(image) {
|
|
2090
2096
|
if (!DIGEST_IMAGE.test(image)) throw new TypeError("container image must be pinned by sha256 digest");
|
|
@@ -2128,7 +2134,7 @@ async function selectContainerEngine(requested = "auto", options = {}) {
|
|
|
2128
2134
|
throw new TypeError("no supported container engine found");
|
|
2129
2135
|
}
|
|
2130
2136
|
function inspectRootlessPodman() {
|
|
2131
|
-
return new Promise((
|
|
2137
|
+
return new Promise((resolve33, reject) => {
|
|
2132
2138
|
(0, import_child_process.execFile)(
|
|
2133
2139
|
"podman",
|
|
2134
2140
|
["info", "--format", "{{.Host.Security.Rootless}}"],
|
|
@@ -2138,7 +2144,7 @@ function inspectRootlessPodman() {
|
|
|
2138
2144
|
reject(new TypeError("could not verify that the active Podman service is rootless"));
|
|
2139
2145
|
return;
|
|
2140
2146
|
}
|
|
2141
|
-
|
|
2147
|
+
resolve33(stdout.trim() === "true");
|
|
2142
2148
|
}
|
|
2143
2149
|
);
|
|
2144
2150
|
});
|
|
@@ -2249,9 +2255,9 @@ async function runContainerAttempt(options) {
|
|
|
2249
2255
|
if (bytes.byteLength > 1e6) throw new Error("agent message exceeds 1 MB");
|
|
2250
2256
|
const line = bytes.toString("utf8");
|
|
2251
2257
|
if (!line.trim()) return;
|
|
2252
|
-
const
|
|
2253
|
-
if (
|
|
2254
|
-
const response2 = await options.onMessage(
|
|
2258
|
+
const message2 = parseAgentOutput(line);
|
|
2259
|
+
if (message2.type === "attempt.complete") complete = message2;
|
|
2260
|
+
const response2 = await options.onMessage(message2);
|
|
2255
2261
|
if (response2 && !child.stdin.destroyed) child.stdin.write(encodeAgentInput(response2));
|
|
2256
2262
|
};
|
|
2257
2263
|
try {
|
|
@@ -2299,29 +2305,142 @@ async function runContainerAttempt(options) {
|
|
|
2299
2305
|
options.signal?.removeEventListener("abort", abort);
|
|
2300
2306
|
}
|
|
2301
2307
|
}
|
|
2302
|
-
var
|
|
2303
|
-
|
|
2308
|
+
var SKIP_WORKSPACE_DIRS = /* @__PURE__ */ new Set([
|
|
2309
|
+
".git",
|
|
2310
|
+
".odla",
|
|
2311
|
+
".wrangler",
|
|
2312
|
+
"node_modules",
|
|
2313
|
+
"dist",
|
|
2314
|
+
"coverage"
|
|
2315
|
+
]);
|
|
2316
|
+
var SECRET_WORKSPACE_FILE = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
2317
|
+
function allowedWorkspacePath(relativePath) {
|
|
2318
|
+
const parts = relativePath.split("/");
|
|
2319
|
+
return !(0, import_path3.isAbsolute)(relativePath) && !relativePath.includes("\\") && !relativePath.includes("\0") && !parts.some((part) => !part || part === "." || part === ".." || SKIP_WORKSPACE_DIRS.has(part)) && !SECRET_WORKSPACE_FILE.test(parts.at(-1) ?? "");
|
|
2320
|
+
}
|
|
2321
|
+
async function gitOutput(cwd, args, maxBytes) {
|
|
2322
|
+
const child = (0, import_child_process2.spawn)("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
2323
|
+
const stdout = [];
|
|
2324
|
+
const stderr = [];
|
|
2325
|
+
let bytes = 0;
|
|
2326
|
+
child.stdout.on("data", (chunk) => {
|
|
2327
|
+
bytes += chunk.byteLength;
|
|
2328
|
+
if (bytes > maxBytes) child.kill("SIGKILL");
|
|
2329
|
+
else stdout.push(chunk);
|
|
2330
|
+
});
|
|
2331
|
+
child.stderr.on("data", (chunk) => {
|
|
2332
|
+
if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
|
|
2333
|
+
});
|
|
2334
|
+
const code = await new Promise((accept, reject) => {
|
|
2335
|
+
child.once("error", reject);
|
|
2336
|
+
child.once("exit", accept);
|
|
2337
|
+
});
|
|
2338
|
+
if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
|
|
2339
|
+
if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
|
|
2340
|
+
return Buffer.concat(stdout);
|
|
2341
|
+
}
|
|
2342
|
+
async function gitBlobs(cwd, entries, maxBytes) {
|
|
2343
|
+
const child = (0, import_child_process2.spawn)("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
2344
|
+
const stdout = [];
|
|
2345
|
+
const stderr = [];
|
|
2346
|
+
let bytes = 0;
|
|
2347
|
+
child.stdout.on("data", (chunk) => {
|
|
2348
|
+
bytes += chunk.byteLength;
|
|
2349
|
+
if (bytes > maxBytes + entries.length * 100) child.kill("SIGKILL");
|
|
2350
|
+
else stdout.push(chunk);
|
|
2351
|
+
});
|
|
2352
|
+
child.stderr.on("data", (chunk) => {
|
|
2353
|
+
if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
|
|
2354
|
+
});
|
|
2355
|
+
child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
|
|
2356
|
+
`);
|
|
2357
|
+
const code = await new Promise((accept, reject) => {
|
|
2358
|
+
child.once("error", reject);
|
|
2359
|
+
child.once("exit", accept);
|
|
2360
|
+
});
|
|
2361
|
+
if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
|
|
2362
|
+
if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
|
|
2363
|
+
const output = Buffer.concat(stdout);
|
|
2364
|
+
const blobs = [];
|
|
2365
|
+
let offset = 0;
|
|
2366
|
+
let total = 0;
|
|
2367
|
+
for (const entry of entries) {
|
|
2368
|
+
const newline = output.indexOf(10, offset);
|
|
2369
|
+
if (newline < 0) throw new Error("git object output is truncated");
|
|
2370
|
+
const match = /^([0-9a-f]{40,64}) blob ([0-9]+)$/.exec(output.subarray(offset, newline).toString("utf8"));
|
|
2371
|
+
if (!match || match[1] !== entry.hash) throw new Error("git object output does not match its inventory");
|
|
2372
|
+
const length = Number(match[2]);
|
|
2373
|
+
if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
|
|
2374
|
+
throw new Error("git object output has an invalid length");
|
|
2375
|
+
}
|
|
2376
|
+
const content = output.subarray(newline + 1, newline + 1 + length);
|
|
2377
|
+
if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
|
|
2378
|
+
total += content.byteLength;
|
|
2379
|
+
if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2380
|
+
blobs.push(content);
|
|
2381
|
+
offset = newline + 1 + length + 1;
|
|
2382
|
+
}
|
|
2383
|
+
return blobs;
|
|
2384
|
+
}
|
|
2385
|
+
async function materializeGitTree(source, commitSha, options = {}) {
|
|
2386
|
+
if (!/^[0-9a-f]{40}$/.test(commitSha)) throw new TypeError("Git tree requires an exact commit SHA");
|
|
2387
|
+
const sourceDir = await (0, import_promises3.realpath)((0, import_path2.resolve)(source));
|
|
2388
|
+
const maxFiles = options.maxFiles ?? 2e4;
|
|
2389
|
+
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
2390
|
+
const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
|
|
2391
|
+
const entries = inventory.flatMap((record5) => {
|
|
2392
|
+
const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record5);
|
|
2393
|
+
return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
|
|
2394
|
+
});
|
|
2395
|
+
if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
2396
|
+
const root = await (0, import_promises3.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
|
|
2397
|
+
const targetRoot = (0, import_path2.join)(root, "source");
|
|
2398
|
+
await (0, import_promises3.mkdir)(targetRoot);
|
|
2399
|
+
let byteCount = 0;
|
|
2400
|
+
try {
|
|
2401
|
+
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
2402
|
+
for (const [index, entry] of entries.entries()) {
|
|
2403
|
+
const content = blobs[index];
|
|
2404
|
+
byteCount += content.byteLength;
|
|
2405
|
+
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2406
|
+
const target = (0, import_path2.resolve)(targetRoot, entry.path);
|
|
2407
|
+
if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
|
|
2408
|
+
await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
|
|
2409
|
+
await (0, import_promises3.writeFile)(target, content, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
2410
|
+
}
|
|
2411
|
+
return {
|
|
2412
|
+
root,
|
|
2413
|
+
sourceDir: targetRoot,
|
|
2414
|
+
fileCount: entries.length,
|
|
2415
|
+
byteCount,
|
|
2416
|
+
cleanup: () => (0, import_promises3.rm)(root, { recursive: true, force: true })
|
|
2417
|
+
};
|
|
2418
|
+
} catch (error) {
|
|
2419
|
+
await (0, import_promises3.rm)(root, { recursive: true, force: true });
|
|
2420
|
+
throw error;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2304
2423
|
async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
2305
2424
|
const files = [];
|
|
2306
2425
|
let bytes = 0;
|
|
2307
2426
|
const walk = async (dir) => {
|
|
2308
|
-
for (const entry of await (0,
|
|
2309
|
-
if (entry.isDirectory() &&
|
|
2310
|
-
if (!entry.isDirectory() &&
|
|
2311
|
-
const path = (0,
|
|
2427
|
+
for (const entry of await (0, import_promises4.readdir)(dir, { withFileTypes: true })) {
|
|
2428
|
+
if (entry.isDirectory() && SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
2429
|
+
if (!entry.isDirectory() && SECRET_WORKSPACE_FILE.test(entry.name)) continue;
|
|
2430
|
+
const path = (0, import_path4.join)(dir, entry.name);
|
|
2312
2431
|
if (entry.isSymbolicLink()) continue;
|
|
2313
2432
|
if (entry.isDirectory()) {
|
|
2314
2433
|
await walk(path);
|
|
2315
2434
|
continue;
|
|
2316
2435
|
}
|
|
2317
2436
|
if (!entry.isFile()) continue;
|
|
2318
|
-
const metadata2 = await (0,
|
|
2437
|
+
const metadata2 = await (0, import_promises4.stat)(path);
|
|
2319
2438
|
bytes += metadata2.size;
|
|
2320
2439
|
if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
2321
2440
|
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2322
2441
|
files.push({
|
|
2323
2442
|
source: path,
|
|
2324
|
-
relativePath: (0,
|
|
2443
|
+
relativePath: (0, import_path4.relative)(sourceDir, path),
|
|
2325
2444
|
mode: metadata2.mode & 511,
|
|
2326
2445
|
bytes: metadata2.size
|
|
2327
2446
|
});
|
|
@@ -2330,16 +2449,62 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
2330
2449
|
await walk(sourceDir);
|
|
2331
2450
|
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
2332
2451
|
}
|
|
2452
|
+
async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
2453
|
+
const child = (0, import_child_process3.spawn)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
2454
|
+
cwd: sourceDir,
|
|
2455
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2456
|
+
shell: false
|
|
2457
|
+
});
|
|
2458
|
+
const stdout = [];
|
|
2459
|
+
const stderr = [];
|
|
2460
|
+
let outputBytes = 0;
|
|
2461
|
+
child.stdout.on("data", (chunk) => {
|
|
2462
|
+
outputBytes += chunk.byteLength;
|
|
2463
|
+
if (outputBytes > 8 * 1024 * 1024) child.kill("SIGKILL");
|
|
2464
|
+
else stdout.push(chunk);
|
|
2465
|
+
});
|
|
2466
|
+
child.stderr.on("data", (chunk) => {
|
|
2467
|
+
if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
|
|
2468
|
+
});
|
|
2469
|
+
const code = await new Promise((accept, reject) => {
|
|
2470
|
+
child.once("error", reject);
|
|
2471
|
+
child.once("exit", accept);
|
|
2472
|
+
});
|
|
2473
|
+
if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
|
|
2474
|
+
if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
|
|
2475
|
+
const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
|
|
2476
|
+
if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
2477
|
+
const root = (0, import_path4.resolve)(sourceDir);
|
|
2478
|
+
const files = [];
|
|
2479
|
+
let bytes = 0;
|
|
2480
|
+
for (const relativePath of paths) {
|
|
2481
|
+
if (!allowedWorkspacePath(relativePath)) continue;
|
|
2482
|
+
const source = (0, import_path4.resolve)(root, relativePath);
|
|
2483
|
+
if (!source.startsWith(`${root}${import_path4.sep}`)) throw new TypeError("git file path escapes workspace");
|
|
2484
|
+
let metadata2;
|
|
2485
|
+
try {
|
|
2486
|
+
metadata2 = await (0, import_promises4.lstat)(source);
|
|
2487
|
+
} catch (error) {
|
|
2488
|
+
if (error.code === "ENOENT") continue;
|
|
2489
|
+
throw error;
|
|
2490
|
+
}
|
|
2491
|
+
if (metadata2.isSymbolicLink() || !metadata2.isFile()) continue;
|
|
2492
|
+
bytes += metadata2.size;
|
|
2493
|
+
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2494
|
+
files.push({ source, relativePath, mode: metadata2.mode & 511, bytes: metadata2.size });
|
|
2495
|
+
}
|
|
2496
|
+
return files;
|
|
2497
|
+
}
|
|
2333
2498
|
async function copyTree(files, destination) {
|
|
2334
2499
|
for (const file of files) {
|
|
2335
|
-
const target = (0,
|
|
2336
|
-
await (0,
|
|
2337
|
-
await (0,
|
|
2338
|
-
await (0,
|
|
2500
|
+
const target = (0, import_path4.join)(destination, file.relativePath);
|
|
2501
|
+
await (0, import_promises4.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
|
|
2502
|
+
await (0, import_promises4.copyFile)(file.source, target);
|
|
2503
|
+
await (0, import_promises4.chmod)(target, file.mode);
|
|
2339
2504
|
}
|
|
2340
2505
|
}
|
|
2341
2506
|
async function captureGitDiff(root, maxBytes) {
|
|
2342
|
-
const child = (0,
|
|
2507
|
+
const child = (0, import_child_process3.spawn)("git", [
|
|
2343
2508
|
"diff",
|
|
2344
2509
|
"--no-index",
|
|
2345
2510
|
"--binary",
|
|
@@ -2372,15 +2537,17 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
2372
2537
|
return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
|
|
2373
2538
|
}
|
|
2374
2539
|
async function stageWorkspace(source, options = {}) {
|
|
2375
|
-
const sourceDir = await (0,
|
|
2376
|
-
const sourceStat = await (0,
|
|
2540
|
+
const sourceDir = await (0, import_promises4.realpath)((0, import_path4.resolve)(source));
|
|
2541
|
+
const sourceStat = await (0, import_promises4.stat)(sourceDir);
|
|
2377
2542
|
if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
|
|
2378
|
-
const root = await (0,
|
|
2379
|
-
const baselineDir = (0,
|
|
2380
|
-
const workspaceDir = (0,
|
|
2381
|
-
await Promise.all([(0,
|
|
2543
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
2544
|
+
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
2545
|
+
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
2546
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
2382
2547
|
try {
|
|
2383
|
-
const
|
|
2548
|
+
const maxFiles = options.maxFiles ?? 2e4;
|
|
2549
|
+
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
2550
|
+
const files = options.gitTrackedAndUnignored ? await gitSourceFiles(sourceDir, maxFiles, maxBytes) : await sourceFiles(sourceDir, maxFiles, maxBytes);
|
|
2384
2551
|
await Promise.all([copyTree(files, baselineDir), copyTree(files, workspaceDir)]);
|
|
2385
2552
|
return {
|
|
2386
2553
|
root,
|
|
@@ -2388,15 +2555,49 @@ async function stageWorkspace(source, options = {}) {
|
|
|
2388
2555
|
workspaceDir,
|
|
2389
2556
|
fileCount: files.length,
|
|
2390
2557
|
byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
|
|
2391
|
-
patch: (
|
|
2392
|
-
cleanup: () => (0,
|
|
2558
|
+
patch: (maxBytes2) => captureGitDiff(root, maxBytes2),
|
|
2559
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
2393
2560
|
};
|
|
2394
2561
|
} catch (error) {
|
|
2395
|
-
await (0,
|
|
2562
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
2563
|
+
throw error;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
async function stageWorkspacePair(baselineSource, workspaceSource, options = {}) {
|
|
2567
|
+
const baselineDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(baselineSource));
|
|
2568
|
+
const workspaceDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(workspaceSource));
|
|
2569
|
+
const maxFiles = options.maxFiles ?? 2e4;
|
|
2570
|
+
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
2571
|
+
const [baselineFiles, workspaceFiles] = await Promise.all([
|
|
2572
|
+
sourceFiles(baselineDirSource, maxFiles, maxBytes),
|
|
2573
|
+
sourceFiles(workspaceDirSource, maxFiles, maxBytes)
|
|
2574
|
+
]);
|
|
2575
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
2576
|
+
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
2577
|
+
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
2578
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
2579
|
+
try {
|
|
2580
|
+
await Promise.all([copyTree(baselineFiles, baselineDir), copyTree(workspaceFiles, workspaceDir)]);
|
|
2581
|
+
return {
|
|
2582
|
+
root,
|
|
2583
|
+
baselineDir,
|
|
2584
|
+
workspaceDir,
|
|
2585
|
+
fileCount: workspaceFiles.length,
|
|
2586
|
+
byteCount: workspaceFiles.reduce((sum, file) => sum + file.bytes, 0),
|
|
2587
|
+
patch: (maxPatchBytes) => captureGitDiff(root, maxPatchBytes),
|
|
2588
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
2589
|
+
};
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
2396
2592
|
throw error;
|
|
2397
2593
|
}
|
|
2398
2594
|
}
|
|
2399
2595
|
|
|
2596
|
+
// ../harness/dist/chunk-4EM6NMET.js
|
|
2597
|
+
var import_crypto = require("crypto");
|
|
2598
|
+
var import_promises5 = require("fs/promises");
|
|
2599
|
+
var import_path5 = require("path");
|
|
2600
|
+
|
|
2400
2601
|
// ../camel/dist/chunk-7FHPOQVP.js
|
|
2401
2602
|
var CamelError = class extends Error {
|
|
2402
2603
|
code;
|
|
@@ -2415,8 +2616,8 @@ function canonicalJson(value) {
|
|
|
2415
2616
|
}
|
|
2416
2617
|
async function sha256Hex(value) {
|
|
2417
2618
|
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
2418
|
-
const
|
|
2419
|
-
return [...new Uint8Array(
|
|
2619
|
+
const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer);
|
|
2620
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2420
2621
|
}
|
|
2421
2622
|
function utf8Length(value) {
|
|
2422
2623
|
if (typeof value === "string") return new TextEncoder().encode(value).byteLength;
|
|
@@ -2693,8 +2894,8 @@ function freeze(value) {
|
|
|
2693
2894
|
})
|
|
2694
2895
|
});
|
|
2695
2896
|
}
|
|
2696
|
-
function invalid2(
|
|
2697
|
-
return new CamelError("state_conflict",
|
|
2897
|
+
function invalid2(message2) {
|
|
2898
|
+
return new CamelError("state_conflict", message2);
|
|
2698
2899
|
}
|
|
2699
2900
|
var SHA4 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
2700
2901
|
var REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
@@ -2729,25 +2930,22 @@ function validateSnapshot(snapshot, limits) {
|
|
|
2729
2930
|
}
|
|
2730
2931
|
}
|
|
2731
2932
|
|
|
2732
|
-
// ../harness/dist/chunk-
|
|
2733
|
-
var import_child_process3 = require("child_process");
|
|
2734
|
-
var import_promises4 = require("fs/promises");
|
|
2735
|
-
var import_path3 = require("path");
|
|
2933
|
+
// ../harness/dist/chunk-4EM6NMET.js
|
|
2736
2934
|
var import_child_process4 = require("child_process");
|
|
2935
|
+
var import_promises6 = require("fs/promises");
|
|
2936
|
+
var import_path6 = require("path");
|
|
2937
|
+
var import_child_process5 = require("child_process");
|
|
2737
2938
|
var import_process2 = require("process");
|
|
2738
|
-
var import_crypto = require("crypto");
|
|
2739
2939
|
var import_crypto2 = require("crypto");
|
|
2740
|
-
var import_fs2 = require("fs");
|
|
2741
|
-
var import_promises5 = require("fs/promises");
|
|
2742
|
-
var import_path4 = require("path");
|
|
2743
2940
|
var import_crypto3 = require("crypto");
|
|
2744
|
-
var
|
|
2745
|
-
var import_path5 = require("path");
|
|
2941
|
+
var import_fs2 = require("fs");
|
|
2746
2942
|
var import_promises7 = require("fs/promises");
|
|
2747
|
-
var import_os2 = require("os");
|
|
2748
|
-
var import_path6 = require("path");
|
|
2749
|
-
var import_promises8 = require("fs/promises");
|
|
2750
2943
|
var import_path7 = require("path");
|
|
2944
|
+
var import_promises8 = require("fs/promises");
|
|
2945
|
+
var import_os3 = require("os");
|
|
2946
|
+
var import_path8 = require("path");
|
|
2947
|
+
var import_promises9 = require("fs/promises");
|
|
2948
|
+
var import_path9 = require("path");
|
|
2751
2949
|
|
|
2752
2950
|
// ../camel/dist/chunk-LAXU2AVK.js
|
|
2753
2951
|
function conversionPolicyDigest(policy) {
|
|
@@ -3025,8 +3223,34 @@ function looksLikeDestination(value) {
|
|
|
3025
3223
|
return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
|
|
3026
3224
|
}
|
|
3027
3225
|
|
|
3028
|
-
// ../harness/dist/chunk-
|
|
3226
|
+
// ../harness/dist/chunk-4EM6NMET.js
|
|
3029
3227
|
var import_crypto4 = require("crypto");
|
|
3228
|
+
async function digestStagedWorkspace(root, limits) {
|
|
3229
|
+
const files = [];
|
|
3230
|
+
const walk = async (directory) => {
|
|
3231
|
+
const entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
|
|
3232
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
3233
|
+
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
3234
|
+
const target = (0, import_path5.resolve)(directory, entry.name);
|
|
3235
|
+
if (entry.isDirectory()) await walk(target);
|
|
3236
|
+
else if (entry.isFile()) {
|
|
3237
|
+
files.push({ path: (0, import_path5.relative)(root, target).split("\\").join("/"), target });
|
|
3238
|
+
if (files.length > limits.maxFiles) throw new TypeError("workspace digest exceeds its file bound");
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
await walk((0, import_path5.resolve)(root));
|
|
3243
|
+
const hash = (0, import_crypto.createHash)("sha256");
|
|
3244
|
+
let bytes = 0;
|
|
3245
|
+
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
3246
|
+
const content = await (0, import_promises5.readFile)(file.target);
|
|
3247
|
+
bytes += Buffer.byteLength(file.path) + content.byteLength;
|
|
3248
|
+
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
3249
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
|
|
3250
|
+
hash.update(content);
|
|
3251
|
+
}
|
|
3252
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3253
|
+
}
|
|
3030
3254
|
var CODE_RUNTIME_PROTOCOL_VERSION = 1;
|
|
3031
3255
|
async function runCodeRuntimeHeartbeatLoop(options) {
|
|
3032
3256
|
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
@@ -3098,8 +3322,8 @@ function wait(ms, signal) {
|
|
|
3098
3322
|
});
|
|
3099
3323
|
}
|
|
3100
3324
|
var CodeRuntimeControlError = class extends Error {
|
|
3101
|
-
constructor(
|
|
3102
|
-
super(
|
|
3325
|
+
constructor(message2, status, code = "control_error") {
|
|
3326
|
+
super(message2);
|
|
3103
3327
|
this.status = status;
|
|
3104
3328
|
this.code = code;
|
|
3105
3329
|
}
|
|
@@ -3178,9 +3402,9 @@ function createCodeRuntimeControlClient(options) {
|
|
|
3178
3402
|
}
|
|
3179
3403
|
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
|
|
3180
3404
|
},
|
|
3181
|
-
reportSessionFailure: async (sessionId,
|
|
3182
|
-
if (!
|
|
3183
|
-
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message:
|
|
3405
|
+
reportSessionFailure: async (sessionId, message2) => {
|
|
3406
|
+
if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
|
|
3407
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
|
|
3184
3408
|
}
|
|
3185
3409
|
};
|
|
3186
3410
|
}
|
|
@@ -3247,10 +3471,28 @@ async function parseSource(value) {
|
|
|
3247
3471
|
if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("source file");
|
|
3248
3472
|
return { path: file.path, content: file.content };
|
|
3249
3473
|
});
|
|
3474
|
+
const referencesValue = snapshot.references === void 0 ? [] : snapshot.references;
|
|
3475
|
+
if (!Array.isArray(referencesValue) || referencesValue.length > 5) throw invalid("reference sources");
|
|
3476
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
3477
|
+
const references = [];
|
|
3478
|
+
for (const item of referencesValue) {
|
|
3479
|
+
const reference = record3(item);
|
|
3480
|
+
if (!reference || typeof reference.alias !== "string" || !/^[a-z][a-z0-9-]{0,39}$/.test(reference.alias) || aliases.has(reference.alias) || reference.alias === "primary" || typeof reference.repository !== "string" || typeof reference.commitSha !== "string" || typeof reference.treeDigest !== "string" || !Array.isArray(reference.files)) throw invalid("reference source");
|
|
3481
|
+
aliases.add(reference.alias);
|
|
3482
|
+
const referenceFiles = reference.files.map((entry) => {
|
|
3483
|
+
const file = record3(entry);
|
|
3484
|
+
if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("reference source file");
|
|
3485
|
+
return { path: file.path, content: file.content };
|
|
3486
|
+
});
|
|
3487
|
+
const source2 = { repository: reference.repository, commitSha: reference.commitSha, files: referenceFiles };
|
|
3488
|
+
const referenceDigest = await digestCodeRepositorySnapshot(source2, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
|
|
3489
|
+
if (referenceDigest !== reference.treeDigest) throw invalid("reference source digest");
|
|
3490
|
+
references.push({ alias: reference.alias, ...source2, treeDigest: referenceDigest });
|
|
3491
|
+
}
|
|
3250
3492
|
const source = { repository: snapshot.repository, commitSha: snapshot.commitSha, files };
|
|
3251
|
-
const
|
|
3252
|
-
if (
|
|
3253
|
-
return { ...source, treeDigest:
|
|
3493
|
+
const digest = await digestCodeRepositorySnapshot(source, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
|
|
3494
|
+
if (digest !== snapshot.treeDigest) throw invalid("source digest");
|
|
3495
|
+
return { ...source, treeDigest: digest, ...references.length ? { references } : {} };
|
|
3254
3496
|
}
|
|
3255
3497
|
function parseReview(value) {
|
|
3256
3498
|
const review = record3(record3(value)?.review);
|
|
@@ -3309,9 +3551,9 @@ function validateRelativePath(path) {
|
|
|
3309
3551
|
}
|
|
3310
3552
|
function resolveCodePath(workspaceDir, path) {
|
|
3311
3553
|
validateRelativePath(path);
|
|
3312
|
-
const root = (0,
|
|
3313
|
-
const target = (0,
|
|
3314
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
3554
|
+
const root = (0, import_path6.resolve)(workspaceDir);
|
|
3555
|
+
const target = (0, import_path6.resolve)(root, path);
|
|
3556
|
+
if (target !== root && !target.startsWith(`${root}${import_path6.sep}`)) throw new TypeError("path escapes the staged workspace");
|
|
3315
3557
|
return target;
|
|
3316
3558
|
}
|
|
3317
3559
|
async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
@@ -3319,7 +3561,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
|
3319
3561
|
await gitApply(workspaceDir, patch2, false);
|
|
3320
3562
|
for (const path of paths) {
|
|
3321
3563
|
try {
|
|
3322
|
-
const info = await (0,
|
|
3564
|
+
const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
|
|
3323
3565
|
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
3324
3566
|
throw new TypeError("patch created a non-regular workspace entry");
|
|
3325
3567
|
}
|
|
@@ -3331,7 +3573,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
|
3331
3573
|
function gitApply(cwd, patch2, check) {
|
|
3332
3574
|
return new Promise((accept, reject) => {
|
|
3333
3575
|
const args = ["apply", "--recount", "--whitespace=nowarn", ...check ? ["--check"] : [], "-"];
|
|
3334
|
-
const child = (0,
|
|
3576
|
+
const child = (0, import_child_process4.spawn)("git", args, {
|
|
3335
3577
|
cwd,
|
|
3336
3578
|
shell: false,
|
|
3337
3579
|
stdio: ["pipe", "ignore", "pipe"],
|
|
@@ -3375,7 +3617,7 @@ async function restoreCodeWorkspaceCheckpoint(input) {
|
|
|
3375
3617
|
}
|
|
3376
3618
|
var ARTIFACT_PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
3377
3619
|
var PRIVATE_ARTIFACT_PART = /^(?:\.git|\.odla|\.wrangler|\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json)$/i;
|
|
3378
|
-
function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${(0,
|
|
3620
|
+
function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${(0, import_crypto2.randomUUID)().slice(0, 12)}`) {
|
|
3379
3621
|
assertCodeBuildRecipe(recipe2);
|
|
3380
3622
|
if (/[,\r\n]/.test(workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
|
|
3381
3623
|
const uid = typeof import_process2.getuid === "function" ? (0, import_process2.getuid)() : 1e3;
|
|
@@ -3430,7 +3672,7 @@ function createContainerRecipeExecutor(engine) {
|
|
|
3430
3672
|
return {
|
|
3431
3673
|
async run(input) {
|
|
3432
3674
|
await verifyContainerEngineBoundary(engine);
|
|
3433
|
-
const name = `odla-recipe-${(0,
|
|
3675
|
+
const name = `odla-recipe-${(0, import_crypto2.randomUUID)().slice(0, 12)}`;
|
|
3434
3676
|
const args = buildRecipeContainerArgs(engine, input.workspaceDir, input.recipe, name);
|
|
3435
3677
|
return execute(engine, args, name, input.recipe, input.signal);
|
|
3436
3678
|
}
|
|
@@ -3454,7 +3696,7 @@ function parseMemory(value) {
|
|
|
3454
3696
|
function execute(engine, args, name, recipe2, signal) {
|
|
3455
3697
|
return new Promise((accept, reject) => {
|
|
3456
3698
|
const started = Date.now();
|
|
3457
|
-
const child = (0,
|
|
3699
|
+
const child = (0, import_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
3458
3700
|
const stdout = [];
|
|
3459
3701
|
const stderr = [];
|
|
3460
3702
|
let bytes = 0;
|
|
@@ -3467,7 +3709,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
3467
3709
|
timedOut = reason === "timeout";
|
|
3468
3710
|
outputLimitExceeded = reason === "output";
|
|
3469
3711
|
const remove = engine === "container" ? ["delete", "--force", name] : ["rm", "-f", name];
|
|
3470
|
-
const killer = (0,
|
|
3712
|
+
const killer = (0, import_child_process5.spawn)(engine, remove, { shell: false, stdio: "ignore" });
|
|
3471
3713
|
killer.unref();
|
|
3472
3714
|
child.kill("SIGTERM");
|
|
3473
3715
|
};
|
|
@@ -3501,32 +3743,6 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
3501
3743
|
});
|
|
3502
3744
|
});
|
|
3503
3745
|
}
|
|
3504
|
-
async function digestStagedWorkspace(root, limits) {
|
|
3505
|
-
const files = [];
|
|
3506
|
-
const walk = async (directory) => {
|
|
3507
|
-
const entries = await (0, import_promises6.readdir)(directory, { withFileTypes: true });
|
|
3508
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
3509
|
-
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
3510
|
-
const target = (0, import_path5.resolve)(directory, entry.name);
|
|
3511
|
-
if (entry.isDirectory()) await walk(target);
|
|
3512
|
-
else if (entry.isFile()) {
|
|
3513
|
-
files.push({ path: (0, import_path5.relative)(root, target).split("\\").join("/"), target });
|
|
3514
|
-
if (files.length > limits.maxFiles) throw new TypeError("workspace digest exceeds its file bound");
|
|
3515
|
-
}
|
|
3516
|
-
}
|
|
3517
|
-
};
|
|
3518
|
-
await walk((0, import_path5.resolve)(root));
|
|
3519
|
-
const hash = (0, import_crypto3.createHash)("sha256");
|
|
3520
|
-
let bytes = 0;
|
|
3521
|
-
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
3522
|
-
const content = await (0, import_promises6.readFile)(file.target);
|
|
3523
|
-
bytes += Buffer.byteLength(file.path) + content.byteLength;
|
|
3524
|
-
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
3525
|
-
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
|
|
3526
|
-
hash.update(content);
|
|
3527
|
-
}
|
|
3528
|
-
return `sha256:${hash.digest("hex")}`;
|
|
3529
|
-
}
|
|
3530
3746
|
var SHA3 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
3531
3747
|
var DIGEST3 = /^sha256:[0-9a-f]{64}$/;
|
|
3532
3748
|
var ID3 = /^[A-Za-z0-9._:-]{1,160}$/;
|
|
@@ -3574,7 +3790,7 @@ async function verifyCodeCandidate(input) {
|
|
|
3574
3790
|
}
|
|
3575
3791
|
const fields = {
|
|
3576
3792
|
schemaVersion: 1,
|
|
3577
|
-
verificationId: input.verificationId ?? `verify-${(0,
|
|
3793
|
+
verificationId: input.verificationId ?? `verify-${(0, import_crypto3.randomUUID)()}`,
|
|
3578
3794
|
trustedBaseCommitSha: input.trustedBaseCommitSha,
|
|
3579
3795
|
trustedBaseDigest: input.trustedBaseDigest,
|
|
3580
3796
|
patchDigest,
|
|
@@ -3635,8 +3851,8 @@ async function inspectArtifacts(workspaceDir, recipe2) {
|
|
|
3635
3851
|
const receipts = [];
|
|
3636
3852
|
for (const artifact of recipe2.expectedArtifacts ?? []) {
|
|
3637
3853
|
try {
|
|
3638
|
-
const path = (0,
|
|
3639
|
-
const info = await (0,
|
|
3854
|
+
const path = (0, import_path7.join)(workspaceDir, artifact.path);
|
|
3855
|
+
const info = await (0, import_promises7.lstat)(path);
|
|
3640
3856
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
3641
3857
|
receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
|
|
3642
3858
|
} else if (info.size > artifact.maximumBytes) {
|
|
@@ -3653,7 +3869,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
|
|
|
3653
3869
|
}
|
|
3654
3870
|
function hashFile(path) {
|
|
3655
3871
|
return new Promise((accept, reject) => {
|
|
3656
|
-
const hash = (0,
|
|
3872
|
+
const hash = (0, import_crypto3.createHash)("sha256");
|
|
3657
3873
|
const stream = (0, import_fs2.createReadStream)(path);
|
|
3658
3874
|
stream.on("data", (chunk) => {
|
|
3659
3875
|
hash.update(chunk);
|
|
@@ -3711,7 +3927,7 @@ function digestJson(value) {
|
|
|
3711
3927
|
return digestBytes(JSON.stringify(value));
|
|
3712
3928
|
}
|
|
3713
3929
|
function digestBytes(value) {
|
|
3714
|
-
return `sha256:${(0,
|
|
3930
|
+
return `sha256:${(0, import_crypto3.createHash)("sha256").update(value).digest("hex")}`;
|
|
3715
3931
|
}
|
|
3716
3932
|
function integer(value, minimum, maximum) {
|
|
3717
3933
|
return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
|
|
@@ -3732,8 +3948,8 @@ async function prepareRuntimeCheckpoint(input) {
|
|
|
3732
3948
|
policy: {
|
|
3733
3949
|
policyId: "code.runtime",
|
|
3734
3950
|
recipes: input.recipes,
|
|
3735
|
-
maximumFiles:
|
|
3736
|
-
maximumBytes:
|
|
3951
|
+
maximumFiles: 2e4,
|
|
3952
|
+
maximumBytes: 512 * 1024 * 1024
|
|
3737
3953
|
},
|
|
3738
3954
|
recipeExecutor: input.recipeExecutor
|
|
3739
3955
|
});
|
|
@@ -3807,7 +4023,7 @@ var CodeRuntimeCheckpointManager = class {
|
|
|
3807
4023
|
await this.options.event(command, {
|
|
3808
4024
|
type: "message",
|
|
3809
4025
|
actor: "system",
|
|
3810
|
-
body: `Candidate ${candidate.candidateId} is ready for
|
|
4026
|
+
body: command.payload.sourceSet ? `Candidate ${candidate.candidateId} was verified and delivered to the session PR branch` : `Candidate ${candidate.candidateId} is ready for legacy owner publication approval`
|
|
3811
4027
|
}, pending.refs);
|
|
3812
4028
|
this.#pending.delete(command.commandId);
|
|
3813
4029
|
return true;
|
|
@@ -3815,11 +4031,11 @@ var CodeRuntimeCheckpointManager = class {
|
|
|
3815
4031
|
};
|
|
3816
4032
|
var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
3817
4033
|
var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
3818
|
-
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0,
|
|
4034
|
+
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.tmpdir)()) {
|
|
3819
4035
|
if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
|
|
3820
|
-
const root = await (0,
|
|
3821
|
-
const sourceDir = (0,
|
|
3822
|
-
await (0,
|
|
4036
|
+
const root = await (0, import_promises8.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
|
|
4037
|
+
const sourceDir = (0, import_path8.join)(root, "source");
|
|
4038
|
+
await (0, import_promises8.mkdir)(sourceDir);
|
|
3823
4039
|
const seen = /* @__PURE__ */ new Set();
|
|
3824
4040
|
let bytes = 0;
|
|
3825
4041
|
try {
|
|
@@ -3829,17 +4045,56 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os2.
|
|
|
3829
4045
|
seen.add(file.path);
|
|
3830
4046
|
bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
|
|
3831
4047
|
if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
|
|
3832
|
-
const target = (0,
|
|
3833
|
-
if (!target.startsWith(`${(0,
|
|
3834
|
-
await (0,
|
|
3835
|
-
await (0,
|
|
4048
|
+
const target = (0, import_path8.resolve)(sourceDir, file.path);
|
|
4049
|
+
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code source path escapes its root");
|
|
4050
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
4051
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
|
|
4052
|
+
}
|
|
4053
|
+
for (const reference of snapshot.references ?? []) {
|
|
4054
|
+
validateAlias(reference.alias);
|
|
4055
|
+
if (!reference.files.length || reference.files.length > 1e4) throw new TypeError("Code reference file count is invalid");
|
|
4056
|
+
for (const file of reference.files) {
|
|
4057
|
+
validatePath(file.path);
|
|
4058
|
+
const path = `.odla-references/${reference.alias}/${file.path}`;
|
|
4059
|
+
if (seen.has(path)) throw new TypeError("Code reference repeats a path");
|
|
4060
|
+
seen.add(path);
|
|
4061
|
+
bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
|
|
4062
|
+
if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
|
|
4063
|
+
const target = (0, import_path8.resolve)(sourceDir, path);
|
|
4064
|
+
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
4065
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
4066
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
4067
|
+
}
|
|
3836
4068
|
}
|
|
3837
|
-
return { sourceDir, cleanup: () => (0,
|
|
4069
|
+
return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
|
|
3838
4070
|
} catch (cause) {
|
|
3839
|
-
await (0,
|
|
4071
|
+
await (0, import_promises8.rm)(root, { recursive: true, force: true });
|
|
3840
4072
|
throw cause;
|
|
3841
4073
|
}
|
|
3842
4074
|
}
|
|
4075
|
+
function validateAlias(alias) {
|
|
4076
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
|
|
4077
|
+
throw new TypeError("Code reference alias is invalid");
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
async function attachCodeRuntimeReferences(workspace, references) {
|
|
4081
|
+
let bytes = 0;
|
|
4082
|
+
for (const reference of references) {
|
|
4083
|
+
validateAlias(reference.alias);
|
|
4084
|
+
for (const file of reference.files) {
|
|
4085
|
+
validatePath(file.path);
|
|
4086
|
+
const path = `.odla-references/${reference.alias}/${file.path}`;
|
|
4087
|
+
bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
|
|
4088
|
+
if (bytes > 64 * 1024 * 1024) throw new TypeError("Code reference set exceeds its byte bound");
|
|
4089
|
+
for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
|
|
4090
|
+
const target = (0, import_path8.resolve)(root, path);
|
|
4091
|
+
if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
4092
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
4093
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
3843
4098
|
function validatePath(path) {
|
|
3844
4099
|
const parts = path.split("/");
|
|
3845
4100
|
if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED2.has(part) || SECRET2.test(part))) {
|
|
@@ -3942,15 +4197,15 @@ async function environment(input, options, tool) {
|
|
|
3942
4197
|
{ id: "authority", value: `lease:${input.lease.leaseId}`, readers: input.readers },
|
|
3943
4198
|
{ id: "reader", value: options.readerId, readers: input.readers }
|
|
3944
4199
|
]);
|
|
3945
|
-
const
|
|
4200
|
+
const digest = await destinationRegistryDigest([input.workspaceId]);
|
|
3946
4201
|
const approvals = ["irreversible_mutation", "external_send", "financial", "secret_read"];
|
|
3947
4202
|
if (options.recipeAuthorization === "exact_approval") approvals.push("code_execution");
|
|
3948
4203
|
const policy = createEffectPolicy({
|
|
3949
|
-
destinationRegistries: { [DESTINATIONS]: { digest
|
|
4204
|
+
destinationRegistries: { [DESTINATIONS]: { digest, values: [input.workspaceId] } },
|
|
3950
4205
|
approvalEffects: approvals
|
|
3951
4206
|
});
|
|
3952
4207
|
const fixedArgs = {
|
|
3953
|
-
workspace: { role: "destination", value: ingress.control("workspace"), registryId: DESTINATIONS, registryDigest:
|
|
4208
|
+
workspace: { role: "destination", value: ingress.control("workspace"), registryId: DESTINATIONS, registryDigest: digest },
|
|
3954
4209
|
authority: { role: "authority", value: ingress.control("authority") }
|
|
3955
4210
|
};
|
|
3956
4211
|
return { ingress, policy, fixedArgs, reader: ingress.control("reader"), runId: `${input.request.requestId}:${tool}` };
|
|
@@ -4021,11 +4276,11 @@ async function read(context, request, options, policy) {
|
|
|
4021
4276
|
const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
|
|
4022
4277
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
4023
4278
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
4024
|
-
const info = await (0,
|
|
4279
|
+
const info = await (0, import_promises9.stat)(target);
|
|
4025
4280
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
4026
4281
|
throw new TypeError("file is not a bounded regular source file");
|
|
4027
4282
|
}
|
|
4028
|
-
const source = await (0,
|
|
4283
|
+
const source = await (0, import_promises9.readFile)(target);
|
|
4029
4284
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
4030
4285
|
const lines = source.toString("utf8").split("\n");
|
|
4031
4286
|
const content = lines.slice(startLine - 1, endLine).join("\n");
|
|
@@ -4038,6 +4293,9 @@ async function patch(context, request, options, policy) {
|
|
|
4038
4293
|
exactKeys(request.input, ["patch"]);
|
|
4039
4294
|
const value = stringField(request.input, "patch");
|
|
4040
4295
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
4296
|
+
if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
|
|
4297
|
+
throw new TypeError("patch targets a read-only reference source");
|
|
4298
|
+
}
|
|
4041
4299
|
const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
|
|
4042
4300
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
4043
4301
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
@@ -4099,12 +4357,12 @@ function policyContext(context, request, options, extra) {
|
|
|
4099
4357
|
async function registeredFiles(root, limit) {
|
|
4100
4358
|
const paths = [];
|
|
4101
4359
|
const walk = async (directory) => {
|
|
4102
|
-
for (const entry of await (0,
|
|
4360
|
+
for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
|
|
4103
4361
|
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
4104
|
-
const target = (0,
|
|
4362
|
+
const target = (0, import_path9.resolve)(directory, entry.name);
|
|
4105
4363
|
if (entry.isDirectory()) await walk(target);
|
|
4106
4364
|
else if (entry.isFile()) {
|
|
4107
|
-
const path = (0,
|
|
4365
|
+
const path = (0, import_path9.relative)(root, target).split("\\").join("/");
|
|
4108
4366
|
try {
|
|
4109
4367
|
validateRelativePath(path);
|
|
4110
4368
|
} catch {
|
|
@@ -4115,7 +4373,7 @@ async function registeredFiles(root, limit) {
|
|
|
4115
4373
|
}
|
|
4116
4374
|
}
|
|
4117
4375
|
};
|
|
4118
|
-
await walk((0,
|
|
4376
|
+
await walk((0, import_path9.resolve)(root));
|
|
4119
4377
|
return paths.sort();
|
|
4120
4378
|
}
|
|
4121
4379
|
function validateOptions(options) {
|
|
@@ -4123,6 +4381,9 @@ function validateOptions(options) {
|
|
|
4123
4381
|
throw new TypeError("Code tool broker requires a reader and unique registered recipes");
|
|
4124
4382
|
}
|
|
4125
4383
|
for (const recipe2 of options.recipes) assertCodeBuildRecipe(recipe2);
|
|
4384
|
+
if (options.readOnlyPrefixes?.some((prefix) => !/^[A-Za-z0-9_.-]+$/.test(prefix) || prefix === "." || prefix === "..")) {
|
|
4385
|
+
throw new TypeError("Code tool broker read-only prefix is invalid");
|
|
4386
|
+
}
|
|
4126
4387
|
}
|
|
4127
4388
|
function exactKeys(input, allowed) {
|
|
4128
4389
|
if (Object.keys(input).some((key) => !allowed.includes(key))) throw new TypeError("tool input contains an unsupported field");
|
|
@@ -4150,14 +4411,31 @@ function codeCommandMetadata(payload, resume) {
|
|
|
4150
4411
|
}
|
|
4151
4412
|
const planning = trusted?.planningInputDigest;
|
|
4152
4413
|
const attestation = trusted?.attestationDigest;
|
|
4414
|
+
const repository = trusted?.repository;
|
|
4415
|
+
const baseCommitSha = trusted?.commitSha;
|
|
4416
|
+
const sourceTreeDigest = trusted?.treeDigest;
|
|
4417
|
+
if (typeof repository !== "string" || !repository.includes("/") || typeof baseCommitSha !== "string" || !/^[0-9a-f]{40}$/.test(baseCommitSha) || typeof sourceTreeDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(sourceTreeDigest)) {
|
|
4418
|
+
throw new TypeError(`invalid Code ${resume ? "resume" : "start"} trusted base`);
|
|
4419
|
+
}
|
|
4153
4420
|
return {
|
|
4154
4421
|
role,
|
|
4155
4422
|
title,
|
|
4156
4423
|
prompt,
|
|
4157
4424
|
planningInputDigest: typeof planning === "string" && /^sha256:[0-9a-f]{64}$/.test(planning) ? planning : null,
|
|
4158
|
-
attestationDigest: typeof attestation === "string" ? attestation : "resume"
|
|
4425
|
+
attestationDigest: typeof attestation === "string" ? attestation : "resume",
|
|
4426
|
+
repository,
|
|
4427
|
+
baseCommitSha,
|
|
4428
|
+
sourceTreeDigest
|
|
4159
4429
|
};
|
|
4160
4430
|
}
|
|
4431
|
+
function codeLocalSource(payload) {
|
|
4432
|
+
const source = record22(payload.source);
|
|
4433
|
+
if (!source) return null;
|
|
4434
|
+
if (source.kind !== "local_checkout" || typeof source.repository !== "string" || typeof source.headCommitSha !== "string" || !/^[0-9a-f]{40}$/.test(source.headCommitSha) || typeof source.trustedBaseDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(source.trustedBaseDigest) || typeof source.developerPatchDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(source.developerPatchDigest) || typeof source.snapshotDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(source.snapshotDigest) || typeof source.modified !== "boolean" || !Number.isSafeInteger(source.fileCount) || Number(source.fileCount) < 1 || Number(source.fileCount) > 2e4 || !Number.isSafeInteger(source.byteCount) || Number(source.byteCount) < 1 || Number(source.byteCount) > 512 * 1024 * 1024 || !Number.isSafeInteger(source.capturedAt) || Number(source.capturedAt) < 1) {
|
|
4435
|
+
throw new TypeError("invalid local checkout source descriptor");
|
|
4436
|
+
}
|
|
4437
|
+
return source;
|
|
4438
|
+
}
|
|
4161
4439
|
function codeCheckpointPayload(payload) {
|
|
4162
4440
|
const value = payload.checkpoint;
|
|
4163
4441
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("resume checkpoint is missing");
|
|
@@ -4186,11 +4464,63 @@ function fakeCodeLease(command, metadata2) {
|
|
|
4186
4464
|
};
|
|
4187
4465
|
}
|
|
4188
4466
|
var record22 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4467
|
+
var SOURCE_LIMITS = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
4468
|
+
async function prepareRuntimeLocalSource(input) {
|
|
4469
|
+
const { command, descriptor: descriptor2, available, repository, baseCommitSha, resume } = input;
|
|
4470
|
+
if (!available || JSON.stringify(available.descriptor) !== JSON.stringify(descriptor2) || descriptor2.repository.toLowerCase() !== repository.toLowerCase() || descriptor2.headCommitSha !== baseCommitSha) {
|
|
4471
|
+
throw new TypeError("the session's local checkout snapshot is not available on this terminal");
|
|
4472
|
+
}
|
|
4473
|
+
const workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
|
|
4474
|
+
trustedBaseDir: available.trustedBaseDir,
|
|
4475
|
+
trustedBaseCommitSha: baseCommitSha,
|
|
4476
|
+
checkpoint: codeCheckpointPayload(command.payload)
|
|
4477
|
+
})).workspace : await stageWorkspacePair(available.trustedBaseDir, available.sourceDir, SOURCE_LIMITS);
|
|
4478
|
+
const trustedBaseDigest = await digestStagedWorkspace(workspace.baselineDir, SOURCE_LIMITS);
|
|
4479
|
+
if (trustedBaseDigest !== descriptor2.trustedBaseDigest) {
|
|
4480
|
+
await workspace.cleanup();
|
|
4481
|
+
throw new TypeError("trusted Git base digest changed after connection");
|
|
4482
|
+
}
|
|
4483
|
+
if (!resume && await digestStagedWorkspace(workspace.workspaceDir, SOURCE_LIMITS) !== descriptor2.snapshotDigest) {
|
|
4484
|
+
await workspace.cleanup();
|
|
4485
|
+
throw new TypeError("local checkout snapshot digest changed after connection");
|
|
4486
|
+
}
|
|
4487
|
+
return { workspace, sourceDigest: descriptor2.snapshotDigest, trustedBaseDigest };
|
|
4488
|
+
}
|
|
4489
|
+
function createCodeRuntimeToolBroker(input, lease, role) {
|
|
4490
|
+
const broker = createCodeToolBroker({
|
|
4491
|
+
recipes: input.recipes,
|
|
4492
|
+
recipeExecutor: createContainerRecipeExecutor(input.engine),
|
|
4493
|
+
recipeAuthorization: input.recipeAuthorization ?? "registered_recipe",
|
|
4494
|
+
readerId: `code-session:${lease.task.taskId}`,
|
|
4495
|
+
readOnlyPrefixes: [".odla-references"]
|
|
4496
|
+
});
|
|
4497
|
+
return role === "coding" ? broker : { execute: (context, request) => request.tool === "sandbox.read" ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
|
|
4498
|
+
}
|
|
4499
|
+
async function appendCodeRuntimeEvent(control, command, event, refs) {
|
|
4500
|
+
const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
|
|
4501
|
+
refs.push(eventId);
|
|
4502
|
+
const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
|
|
4503
|
+
await control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
4504
|
+
}
|
|
4505
|
+
var digestRuntimeValue = (value) => `sha256:${(0, import_crypto4.createHash)("sha256").update(value).digest("hex")}`;
|
|
4506
|
+
var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
|
|
4507
|
+
var runtimeRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4508
|
+
var safeRuntimeJson = (value) => {
|
|
4509
|
+
try {
|
|
4510
|
+
return JSON.stringify(value).slice(0, 1e4);
|
|
4511
|
+
} catch {
|
|
4512
|
+
return "[event]";
|
|
4513
|
+
}
|
|
4514
|
+
};
|
|
4515
|
+
function runtimeResultText(value) {
|
|
4516
|
+
const record32 = runtimeRecord(value);
|
|
4517
|
+
return record32 && typeof record32.text === "string" ? record32.text.slice(0, 2e4) : null;
|
|
4518
|
+
}
|
|
4189
4519
|
var CodePiRuntimeEngine = class {
|
|
4190
4520
|
constructor(options) {
|
|
4191
4521
|
this.options = options;
|
|
4192
4522
|
this.#run = options.runAttempt ?? runContainerAttempt;
|
|
4193
|
-
this.#buildPolicyDigest =
|
|
4523
|
+
this.#buildPolicyDigest = digestRuntimeValue(JSON.stringify(options.recipes));
|
|
4194
4524
|
this.#checkpoints = new CodeRuntimeCheckpointManager({
|
|
4195
4525
|
control: options.control,
|
|
4196
4526
|
recipes: options.recipes,
|
|
@@ -4225,19 +4555,43 @@ var CodePiRuntimeEngine = class {
|
|
|
4225
4555
|
}
|
|
4226
4556
|
async #start(command, resume) {
|
|
4227
4557
|
if (this.#active.has(command.sessionId)) throw new TypeError("Code session is already active on this runtime");
|
|
4228
|
-
const
|
|
4229
|
-
const
|
|
4558
|
+
const metadata2 = codeCommandMetadata(command.payload, resume);
|
|
4559
|
+
const requestedLocal = codeLocalSource(command.payload);
|
|
4230
4560
|
let workspace;
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4561
|
+
let sourceDigest;
|
|
4562
|
+
let localTrustedBaseDigest;
|
|
4563
|
+
if (requestedLocal) {
|
|
4564
|
+
const prepared = await prepareRuntimeLocalSource({
|
|
4565
|
+
command,
|
|
4566
|
+
descriptor: requestedLocal,
|
|
4567
|
+
available: this.options.localSource,
|
|
4568
|
+
repository: metadata2.repository,
|
|
4569
|
+
baseCommitSha: metadata2.baseCommitSha,
|
|
4570
|
+
resume
|
|
4571
|
+
});
|
|
4572
|
+
({ workspace, sourceDigest, trustedBaseDigest: localTrustedBaseDigest } = prepared);
|
|
4573
|
+
if (command.payload.sourceSet) {
|
|
4574
|
+
const selected = await this.options.control.source(command.sessionId);
|
|
4575
|
+
if (selected.repository !== metadata2.repository || selected.commitSha !== metadata2.baseCommitSha || selected.treeDigest !== metadata2.sourceTreeDigest) {
|
|
4576
|
+
await workspace.cleanup();
|
|
4577
|
+
throw new TypeError("Code local source does not match the selected GitHub primary source");
|
|
4578
|
+
}
|
|
4579
|
+
await attachCodeRuntimeReferences(workspace, selected.references ?? []);
|
|
4580
|
+
}
|
|
4581
|
+
} else {
|
|
4582
|
+
const source = await this.options.control.source(command.sessionId);
|
|
4583
|
+
const materialized = await materializeCodeRuntimeSource(source);
|
|
4584
|
+
try {
|
|
4585
|
+
workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
|
|
4586
|
+
trustedBaseDir: materialized.sourceDir,
|
|
4587
|
+
trustedBaseCommitSha: source.commitSha,
|
|
4588
|
+
checkpoint: codeCheckpointPayload(command.payload)
|
|
4589
|
+
})).workspace : await stageWorkspace(materialized.sourceDir);
|
|
4590
|
+
} finally {
|
|
4591
|
+
await materialized.cleanup();
|
|
4592
|
+
}
|
|
4593
|
+
sourceDigest = source.treeDigest;
|
|
4239
4594
|
}
|
|
4240
|
-
const metadata2 = codeCommandMetadata(command.payload, resume);
|
|
4241
4595
|
const abort = new AbortController();
|
|
4242
4596
|
const conversationRefs = [];
|
|
4243
4597
|
const active = {
|
|
@@ -4247,21 +4601,30 @@ var CodePiRuntimeEngine = class {
|
|
|
4247
4601
|
acknowledged: false,
|
|
4248
4602
|
role: metadata2.role,
|
|
4249
4603
|
title: metadata2.title,
|
|
4250
|
-
baseCommitSha:
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4604
|
+
baseCommitSha: metadata2.baseCommitSha,
|
|
4605
|
+
repository: metadata2.repository,
|
|
4606
|
+
sourceTreeDigest: metadata2.sourceTreeDigest,
|
|
4607
|
+
trustedBaseDigest: requestedLocal ? localTrustedBaseDigest : await digestStagedWorkspace(workspace.baselineDir, {
|
|
4608
|
+
maxFiles: 2e4,
|
|
4609
|
+
maxBytes: 512 * 1024 * 1024
|
|
4254
4610
|
}),
|
|
4255
|
-
planningInputDigest: metadata2.planningInputDigest ??
|
|
4256
|
-
JSON.stringify({ attestation: metadata2.attestationDigest, prompt: metadata2.prompt, tree:
|
|
4611
|
+
planningInputDigest: metadata2.planningInputDigest ?? digestRuntimeValue(
|
|
4612
|
+
JSON.stringify({ attestation: metadata2.attestationDigest, prompt: metadata2.prompt, tree: sourceDigest })
|
|
4257
4613
|
),
|
|
4258
4614
|
done: Promise.resolve(null)
|
|
4259
4615
|
};
|
|
4260
4616
|
this.#active.set(command.sessionId, active);
|
|
4617
|
+
if (requestedLocal) {
|
|
4618
|
+
await this.#event(command, {
|
|
4619
|
+
type: "message",
|
|
4620
|
+
actor: "system",
|
|
4621
|
+
body: `Source snapshot: local checkout ${requestedLocal.snapshotDigest} \xB7 ${requestedLocal.modified ? "modified" : "clean"} \xB7 Git ${requestedLocal.headCommitSha}`
|
|
4622
|
+
}, conversationRefs);
|
|
4623
|
+
}
|
|
4261
4624
|
active.done = this.#runAttempt(command, metadata2, active).catch(async (cause) => {
|
|
4262
|
-
await this.#event(command, { type: "message", actor: "system", body: `Pi failed: ${
|
|
4625
|
+
await this.#event(command, { type: "message", actor: "system", body: `Pi failed: ${runtimeErrorMessage(cause)}` }, conversationRefs).catch(() => void 0);
|
|
4263
4626
|
await this.#event(command, { type: "status", status: "failed" }, conversationRefs).catch(() => void 0);
|
|
4264
|
-
await this.#failure(command, active,
|
|
4627
|
+
await this.#failure(command, active, runtimeErrorMessage(cause));
|
|
4265
4628
|
return null;
|
|
4266
4629
|
});
|
|
4267
4630
|
return { status: "running", message: resume ? "Pi resumed from a portable checkpoint" : "Pi started" };
|
|
@@ -4281,22 +4644,29 @@ var CodePiRuntimeEngine = class {
|
|
|
4281
4644
|
title: active.title,
|
|
4282
4645
|
prompt,
|
|
4283
4646
|
planningInputDigest: active.planningInputDigest,
|
|
4284
|
-
attestationDigest: "follow-up"
|
|
4647
|
+
attestationDigest: "follow-up",
|
|
4648
|
+
repository: active.repository,
|
|
4649
|
+
baseCommitSha: active.baseCommitSha,
|
|
4650
|
+
sourceTreeDigest: active.sourceTreeDigest
|
|
4285
4651
|
}, active).catch(async (cause) => {
|
|
4286
4652
|
await this.#event(
|
|
4287
4653
|
command,
|
|
4288
|
-
{ type: "message", actor: "system", body: `Pi failed: ${
|
|
4654
|
+
{ type: "message", actor: "system", body: `Pi failed: ${runtimeErrorMessage(cause)}` },
|
|
4289
4655
|
active.conversationRefs
|
|
4290
4656
|
).catch(() => void 0);
|
|
4291
4657
|
await this.#event(command, { type: "status", status: "failed" }, active.conversationRefs).catch(() => void 0);
|
|
4292
|
-
await this.#failure(command, active,
|
|
4658
|
+
await this.#failure(command, active, runtimeErrorMessage(cause));
|
|
4293
4659
|
return null;
|
|
4294
4660
|
});
|
|
4295
4661
|
return { status: "running", message: "Pi accepted the owner prompt" };
|
|
4296
4662
|
}
|
|
4297
4663
|
async #runAttempt(command, metadata2, active) {
|
|
4298
4664
|
const lease = fakeCodeLease(command, metadata2);
|
|
4299
|
-
const broker =
|
|
4665
|
+
const broker = createCodeRuntimeToolBroker({
|
|
4666
|
+
recipes: this.options.recipes,
|
|
4667
|
+
engine: this.options.engine,
|
|
4668
|
+
recipeAuthorization: this.options.recipeAuthorization
|
|
4669
|
+
}, lease, metadata2.role);
|
|
4300
4670
|
const startedAt = Date.now();
|
|
4301
4671
|
let completionSeen = false;
|
|
4302
4672
|
const result = await this.#run({
|
|
@@ -4356,7 +4726,7 @@ var CodePiRuntimeEngine = class {
|
|
|
4356
4726
|
return { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "tool.response", ...response2 };
|
|
4357
4727
|
}
|
|
4358
4728
|
if (output.type === "event") {
|
|
4359
|
-
const payload =
|
|
4729
|
+
const payload = runtimeRecord(output.payload);
|
|
4360
4730
|
if (output.kind === "pi.started") {
|
|
4361
4731
|
await this.#event(command, { type: "status", status: "running" }, active.conversationRefs);
|
|
4362
4732
|
} else if (output.kind === "pi.thinking" && payload?.available === true && Number.isSafeInteger(payload.durationMs) && Number(payload.durationMs) >= 0) {
|
|
@@ -4369,12 +4739,12 @@ var CodePiRuntimeEngine = class {
|
|
|
4369
4739
|
await this.#event(command, {
|
|
4370
4740
|
type: "message",
|
|
4371
4741
|
actor: "system",
|
|
4372
|
-
body: `${output.kind}${output.payload === void 0 ? "" : ` ${
|
|
4742
|
+
body: `${output.kind}${output.payload === void 0 ? "" : ` ${safeRuntimeJson(output.payload)}`}`
|
|
4373
4743
|
}, active.conversationRefs);
|
|
4374
4744
|
}
|
|
4375
4745
|
} else if (output.type === "attempt.complete") {
|
|
4376
4746
|
completionSeen = true;
|
|
4377
|
-
const body =
|
|
4747
|
+
const body = runtimeResultText(output.result) ?? `Pi ${output.status}.`;
|
|
4378
4748
|
await this.#event(command, {
|
|
4379
4749
|
type: "message",
|
|
4380
4750
|
actor: output.status === "completed" ? "agent" : "system",
|
|
@@ -4401,15 +4771,6 @@ var CodePiRuntimeEngine = class {
|
|
|
4401
4771
|
}
|
|
4402
4772
|
return result;
|
|
4403
4773
|
}
|
|
4404
|
-
#toolBroker(lease, role) {
|
|
4405
|
-
const broker = createCodeToolBroker({
|
|
4406
|
-
recipes: this.options.recipes,
|
|
4407
|
-
recipeExecutor: createContainerRecipeExecutor(this.options.engine),
|
|
4408
|
-
recipeAuthorization: this.options.recipeAuthorization ?? "registered_recipe",
|
|
4409
|
-
readerId: `code-session:${lease.task.taskId}`
|
|
4410
|
-
});
|
|
4411
|
-
return role === "coding" ? broker : { execute: (context, request) => request.tool === "sandbox.read" ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
|
|
4412
|
-
}
|
|
4413
4774
|
async #checkpoint(command) {
|
|
4414
4775
|
const active = this.#active.get(command.sessionId);
|
|
4415
4776
|
if (!active) throw new TypeError("Code session workspace is not active on this runtime");
|
|
@@ -4424,26 +4785,9 @@ var CodePiRuntimeEngine = class {
|
|
|
4424
4785
|
}
|
|
4425
4786
|
}
|
|
4426
4787
|
async #event(command, event, refs) {
|
|
4427
|
-
|
|
4428
|
-
refs.push(eventId);
|
|
4429
|
-
const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
|
|
4430
|
-
await this.options.control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
4431
|
-
}
|
|
4432
|
-
};
|
|
4433
|
-
var object2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4434
|
-
var digest = (value) => `sha256:${(0, import_crypto4.createHash)("sha256").update(value).digest("hex")}`;
|
|
4435
|
-
var message2 = (value) => value instanceof Error ? value.message : String(value);
|
|
4436
|
-
var json = (value) => {
|
|
4437
|
-
try {
|
|
4438
|
-
return JSON.stringify(value).slice(0, 1e4);
|
|
4439
|
-
} catch {
|
|
4440
|
-
return "[event]";
|
|
4788
|
+
await appendCodeRuntimeEvent(this.options.control, command, event, refs);
|
|
4441
4789
|
}
|
|
4442
4790
|
};
|
|
4443
|
-
function resultText(value) {
|
|
4444
|
-
const record32 = object2(value);
|
|
4445
|
-
return record32 && typeof record32.text === "string" ? record32.text.slice(0, 2e4) : null;
|
|
4446
|
-
}
|
|
4447
4791
|
|
|
4448
4792
|
// src/help.ts
|
|
4449
4793
|
var import_node_fs6 = require("fs");
|
|
@@ -4585,8 +4929,8 @@ async function requestHostedSecurityJson(options, path, init, action) {
|
|
|
4585
4929
|
const body = await response2.json().catch(() => ({}));
|
|
4586
4930
|
if (!response2.ok) {
|
|
4587
4931
|
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
4588
|
-
const
|
|
4589
|
-
throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${
|
|
4932
|
+
const message2 = optionalHostedText(body.error?.message, "error message", 300);
|
|
4933
|
+
throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
|
|
4590
4934
|
}
|
|
4591
4935
|
return body;
|
|
4592
4936
|
}
|
|
@@ -4775,24 +5119,111 @@ async function defaultReadOrigin(cwd) {
|
|
|
4775
5119
|
return result.stdout;
|
|
4776
5120
|
}
|
|
4777
5121
|
|
|
4778
|
-
// src/code-
|
|
4779
|
-
var
|
|
5122
|
+
// src/code-local-source.ts
|
|
5123
|
+
var import_node_child_process3 = require("child_process");
|
|
5124
|
+
var import_node_crypto = require("crypto");
|
|
5125
|
+
var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
5126
|
+
async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
|
|
5127
|
+
const headCommitSha = await readHead(cwd);
|
|
5128
|
+
const staged = await stageWorkspace(cwd, { ...SOURCE_LIMITS2, gitTrackedAndUnignored: true });
|
|
5129
|
+
let trusted;
|
|
5130
|
+
try {
|
|
5131
|
+
trusted = await materializeGitTree(cwd, headCommitSha, SOURCE_LIMITS2);
|
|
5132
|
+
const comparison = await stageWorkspacePair(trusted.sourceDir, staged.baselineDir, SOURCE_LIMITS2);
|
|
5133
|
+
let developerPatch;
|
|
5134
|
+
try {
|
|
5135
|
+
developerPatch = await comparison.patch(4 * 1024 * 1024);
|
|
5136
|
+
} finally {
|
|
5137
|
+
await comparison.cleanup();
|
|
5138
|
+
}
|
|
5139
|
+
const descriptor2 = {
|
|
5140
|
+
kind: "local_checkout",
|
|
5141
|
+
repository,
|
|
5142
|
+
headCommitSha,
|
|
5143
|
+
trustedBaseDigest: await digestStagedWorkspace(trusted.sourceDir, SOURCE_LIMITS2),
|
|
5144
|
+
developerPatchDigest: digestText(developerPatch),
|
|
5145
|
+
snapshotDigest: await digestStagedWorkspace(staged.baselineDir, SOURCE_LIMITS2),
|
|
5146
|
+
modified: developerPatch.length > 0,
|
|
5147
|
+
fileCount: staged.fileCount,
|
|
5148
|
+
byteCount: staged.byteCount,
|
|
5149
|
+
capturedAt: Date.now()
|
|
5150
|
+
};
|
|
5151
|
+
return {
|
|
5152
|
+
descriptor: descriptor2,
|
|
5153
|
+
trustedBaseDir: trusted.sourceDir,
|
|
5154
|
+
sourceDir: staged.baselineDir,
|
|
5155
|
+
cleanup: async () => {
|
|
5156
|
+
await Promise.all([staged.cleanup(), trusted.cleanup()]);
|
|
5157
|
+
}
|
|
5158
|
+
};
|
|
5159
|
+
} catch (error) {
|
|
5160
|
+
await Promise.all([staged.cleanup(), trusted?.cleanup()]);
|
|
5161
|
+
throw error;
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
async function readGitHead(cwd) {
|
|
5165
|
+
const value = await new Promise((accept, reject) => {
|
|
5166
|
+
(0, import_node_child_process3.execFile)("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", maxBuffer: 16384 }, (error, stdout) => {
|
|
5167
|
+
if (error) reject(new Error("code connect requires a Git checkout with an initial commit"));
|
|
5168
|
+
else accept(stdout.trim());
|
|
5169
|
+
});
|
|
5170
|
+
});
|
|
5171
|
+
if (!/^[0-9a-f]{40}$/.test(value)) throw new Error("code connect could not resolve the checkout HEAD commit");
|
|
5172
|
+
return value;
|
|
5173
|
+
}
|
|
5174
|
+
function digestText(value) {
|
|
5175
|
+
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(value).digest("hex")}`;
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
// src/code-images.ts
|
|
5179
|
+
var import_node_child_process4 = require("child_process");
|
|
5180
|
+
async function prepareCodeImages(engine, images) {
|
|
5181
|
+
for (const image of images) {
|
|
5182
|
+
await new Promise((accept, reject) => {
|
|
5183
|
+
const args = engine === "container" ? ["image", "pull", image] : ["pull", image];
|
|
5184
|
+
(0, import_node_child_process4.execFile)(engine, args, { encoding: "utf8", maxBuffer: 2 * 1024 * 1024, timeout: 10 * 6e4 }, (error) => {
|
|
5185
|
+
if (error) reject(new Error(`could not prepare pinned Code image ${image}`));
|
|
5186
|
+
else accept();
|
|
5187
|
+
});
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
// src/code-runtime-config.ts
|
|
5193
|
+
var CODE_PI_IMAGE = "ghcr.io/cory/odla-pi-agent@sha256:e7858b4f0291543171b2659b47c5d00f97acc4767cafd9b74ae5228899e0cde4";
|
|
4780
5194
|
var CODE_BUILD_RECIPES = Object.freeze([{
|
|
4781
|
-
id: "
|
|
5195
|
+
id: "odla-code-contracts",
|
|
4782
5196
|
image: "node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd",
|
|
4783
|
-
command: [
|
|
5197
|
+
command: [
|
|
5198
|
+
"node",
|
|
5199
|
+
"--test",
|
|
5200
|
+
"scripts/lib/directories.test.mjs",
|
|
5201
|
+
"scripts/lib/internal-deps.test.mjs",
|
|
5202
|
+
"scripts/lib/npm-audit.test.mjs",
|
|
5203
|
+
"scripts/lib/npm-release-contract.test.mjs",
|
|
5204
|
+
"scripts/lib/release-surfaces.test.mjs"
|
|
5205
|
+
],
|
|
4784
5206
|
timeoutMs: 12e4,
|
|
4785
5207
|
maxOutputBytes: 1024 * 1024,
|
|
4786
5208
|
cpus: 1,
|
|
4787
5209
|
memory: "512m",
|
|
4788
5210
|
pids: 128
|
|
4789
5211
|
}]);
|
|
5212
|
+
|
|
5213
|
+
// src/code-connect.ts
|
|
4790
5214
|
async function codeConnect(options) {
|
|
4791
5215
|
const cwd = options.cwd ?? process.cwd();
|
|
4792
5216
|
const configPath = (0, import_node_path4.resolve)(cwd, options.configPath);
|
|
4793
5217
|
const cfg = (0, import_node_fs7.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
4794
|
-
const
|
|
4795
|
-
|
|
5218
|
+
const requestedAppId = options.appId?.trim();
|
|
5219
|
+
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
5220
|
+
throw new Error("--app-id must be a valid odla app id");
|
|
5221
|
+
}
|
|
5222
|
+
if (requestedAppId && cfg && requestedAppId !== cfg.app.id) {
|
|
5223
|
+
throw new Error(`--app-id ${requestedAppId} does not match ${options.configPath} (${cfg.app.id})`);
|
|
5224
|
+
}
|
|
5225
|
+
const appId = requestedAppId ?? cfg?.app.id;
|
|
5226
|
+
const platform = (options.platform ?? cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai").replace(/\/+$/, "");
|
|
4796
5227
|
const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : "dev");
|
|
4797
5228
|
if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
|
|
4798
5229
|
throw new Error(cfg ? `Code env "${appEnv ?? ""}" must be dev or prod and declared in ${options.configPath}` : `Code env "${appEnv ?? ""}" must be dev or prod`);
|
|
@@ -4811,53 +5242,77 @@ async function codeConnect(options) {
|
|
|
4811
5242
|
const out = options.stdout ?? console;
|
|
4812
5243
|
const doFetch = options.fetch ?? fetch;
|
|
4813
5244
|
const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
|
|
5245
|
+
await (options.prepareImages ?? prepareCodeImages)(engine, [
|
|
5246
|
+
CODE_PI_IMAGE,
|
|
5247
|
+
...new Set(CODE_BUILD_RECIPES.map((recipe2) => recipe2.image))
|
|
5248
|
+
]);
|
|
4814
5249
|
const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
|
|
4815
5250
|
const hostName = (options.name ?? (0, import_node_os.hostname)()).trim();
|
|
4816
5251
|
if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
|
|
4817
|
-
const
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
5252
|
+
const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
|
|
5253
|
+
const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
|
|
5254
|
+
cwd,
|
|
5255
|
+
repository,
|
|
5256
|
+
options.readGitHead
|
|
5257
|
+
);
|
|
5258
|
+
try {
|
|
5259
|
+
const descriptor2 = localSource.descriptor;
|
|
5260
|
+
const approval = await (options.getToken ?? getScopedPlatformToken)({
|
|
5261
|
+
platform,
|
|
5262
|
+
scope: "app:code:host:connect",
|
|
5263
|
+
email: options.email,
|
|
5264
|
+
open: options.open,
|
|
5265
|
+
fetch: doFetch,
|
|
5266
|
+
stdout: out,
|
|
5267
|
+
openApprovalUrl: options.openApprovalUrl,
|
|
5268
|
+
cache: false,
|
|
5269
|
+
label: `Terminal connection for ${appId ?? repository}/${appEnv}`
|
|
5270
|
+
});
|
|
5271
|
+
const target = appId ? { appId } : { repository };
|
|
5272
|
+
const response2 = await doFetch(`${platform}/registry/code/hosts/connect`, {
|
|
5273
|
+
method: "POST",
|
|
5274
|
+
headers: { authorization: `Bearer ${approval}`, "content-type": "application/json" },
|
|
5275
|
+
body: JSON.stringify({ ...target, env: appEnv, name: hostName, platform: hostPlatform, slots }),
|
|
5276
|
+
redirect: "error",
|
|
5277
|
+
signal: options.signal
|
|
5278
|
+
});
|
|
5279
|
+
const raw = await response2.json().catch(() => null);
|
|
5280
|
+
if (!response2.ok) throw new Error(apiFailure("connect Code host", response2.status, raw));
|
|
5281
|
+
const connection = parseConnection(raw, appId, appEnv);
|
|
5282
|
+
const capabilities = {
|
|
5283
|
+
protocolVersion: CODE_RUNTIME_PROTOCOL_VERSION,
|
|
5284
|
+
platform: hostPlatform,
|
|
5285
|
+
arch: process.arch,
|
|
5286
|
+
engines: [engine],
|
|
5287
|
+
cpuCount: (0, import_node_os.cpus)().length,
|
|
5288
|
+
memoryBytes: (0, import_node_os.totalmem)(),
|
|
5289
|
+
source: descriptor2,
|
|
5290
|
+
images: {
|
|
5291
|
+
ready: true,
|
|
5292
|
+
pi: CODE_PI_IMAGE,
|
|
5293
|
+
recipes: CODE_BUILD_RECIPES.map((recipe2) => ({ id: recipe2.id, image: recipe2.image }))
|
|
5294
|
+
}
|
|
5295
|
+
};
|
|
5296
|
+
out.log(`${connection.resumed ? "reconnected" : "enrolled"}: ${connection.host.name} (${connection.host.hostId})`);
|
|
5297
|
+
out.log(`binding: ${connection.binding.appId}/${connection.binding.env} \xB7 ${connection.offer.slots} slot(s) \xB7 ${engine}`);
|
|
5298
|
+
out.log(`source: local checkout \xB7 ${descriptor2.headCommitSha.slice(0, 10)} \xB7 ${descriptor2.fileCount} files \xB7 staged copy`);
|
|
5299
|
+
out.log("credentials: stored in odla-ai/db; host plaintext is memory-only");
|
|
5300
|
+
out.log(`Studio: ${platform}/studio/apps/${encodeURIComponent(connection.binding.appId)}/${connection.binding.env}/code/sessions`);
|
|
5301
|
+
await (options.runRuntime ?? runCodeRuntime)({
|
|
5302
|
+
endpoint: platform,
|
|
5303
|
+
token: connection.token,
|
|
5304
|
+
engine,
|
|
5305
|
+
capabilities,
|
|
5306
|
+
localSource,
|
|
5307
|
+
heartbeatMs,
|
|
5308
|
+
once: options.once === true,
|
|
5309
|
+
signal: options.signal,
|
|
5310
|
+
stdout: out,
|
|
5311
|
+
fetch: doFetch
|
|
5312
|
+
});
|
|
5313
|
+
} finally {
|
|
5314
|
+
await localSource.cleanup();
|
|
5315
|
+
}
|
|
4861
5316
|
}
|
|
4862
5317
|
async function runCodeRuntime(input) {
|
|
4863
5318
|
const controller = new AbortController();
|
|
@@ -4880,7 +5335,8 @@ async function runCodeRuntime(input) {
|
|
|
4880
5335
|
engine: input.engine,
|
|
4881
5336
|
image: CODE_PI_IMAGE,
|
|
4882
5337
|
recipes: CODE_BUILD_RECIPES,
|
|
4883
|
-
recipeAuthorization: "registered_recipe"
|
|
5338
|
+
recipeAuthorization: "registered_recipe",
|
|
5339
|
+
localSource: input.localSource
|
|
4884
5340
|
});
|
|
4885
5341
|
const reconciler = new CodeRuntimeReconciler(control, commandEngine);
|
|
4886
5342
|
try {
|
|
@@ -4921,8 +5377,8 @@ function parseConnection(value, appId, appEnv) {
|
|
|
4921
5377
|
return root;
|
|
4922
5378
|
}
|
|
4923
5379
|
function apiFailure(action, status, value) {
|
|
4924
|
-
const
|
|
4925
|
-
return `${action} failed (${status})${typeof
|
|
5380
|
+
const message2 = record4(record4(value)?.error)?.message;
|
|
5381
|
+
return `${action} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
|
|
4926
5382
|
}
|
|
4927
5383
|
function record4(value) {
|
|
4928
5384
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -4937,6 +5393,8 @@ async function codeCommand(parsed, dependencies) {
|
|
|
4937
5393
|
assertArgs(parsed, [
|
|
4938
5394
|
"config",
|
|
4939
5395
|
"env",
|
|
5396
|
+
"platform",
|
|
5397
|
+
"app-id",
|
|
4940
5398
|
"email",
|
|
4941
5399
|
"open",
|
|
4942
5400
|
"name",
|
|
@@ -4952,6 +5410,8 @@ async function codeCommand(parsed, dependencies) {
|
|
|
4952
5410
|
await (dependencies.codeConnect ?? codeConnect)({
|
|
4953
5411
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
4954
5412
|
env: stringOpt(parsed.options.env),
|
|
5413
|
+
platform: stringOpt(parsed.options.platform),
|
|
5414
|
+
appId: stringOpt(parsed.options["app-id"]),
|
|
4955
5415
|
email: stringOpt(parsed.options.email),
|
|
4956
5416
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
4957
5417
|
name: stringOpt(parsed.options.name),
|
|
@@ -4967,16 +5427,16 @@ async function codeCommand(parsed, dependencies) {
|
|
|
4967
5427
|
}
|
|
4968
5428
|
|
|
4969
5429
|
// src/doctor-checks.ts
|
|
4970
|
-
var
|
|
5430
|
+
var import_node_child_process6 = require("child_process");
|
|
4971
5431
|
var import_node_fs9 = require("fs");
|
|
4972
5432
|
var import_node_path6 = require("path");
|
|
4973
5433
|
|
|
4974
5434
|
// src/wrangler.ts
|
|
4975
|
-
var
|
|
5435
|
+
var import_node_child_process5 = require("child_process");
|
|
4976
5436
|
var import_node_fs8 = require("fs");
|
|
4977
5437
|
var import_node_path5 = require("path");
|
|
4978
5438
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
4979
|
-
const child = (0,
|
|
5439
|
+
const child = (0, import_node_child_process5.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
4980
5440
|
let stdout = "";
|
|
4981
5441
|
let stderr = "";
|
|
4982
5442
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
@@ -5067,7 +5527,7 @@ function lintRules(rules, entities, publicRead) {
|
|
|
5067
5527
|
}
|
|
5068
5528
|
return warnings;
|
|
5069
5529
|
}
|
|
5070
|
-
var defaultExec = (cmd, args, cwd) => (0,
|
|
5530
|
+
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process6.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
5071
5531
|
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
5072
5532
|
let output;
|
|
5073
5533
|
try {
|
|
@@ -5675,8 +6135,8 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
5675
6135
|
});
|
|
5676
6136
|
if (res.status === 404) return null;
|
|
5677
6137
|
if (!res.ok) return null;
|
|
5678
|
-
const
|
|
5679
|
-
return
|
|
6138
|
+
const json = await res.json();
|
|
6139
|
+
return json.app ?? null;
|
|
5680
6140
|
}
|
|
5681
6141
|
async function postJson2(doFetch, url, bearer, body) {
|
|
5682
6142
|
const res = await doFetch(url, {
|
|
@@ -5915,10 +6375,10 @@ async function provision(options) {
|
|
|
5915
6375
|
stdout: out
|
|
5916
6376
|
});
|
|
5917
6377
|
} catch (error) {
|
|
5918
|
-
const
|
|
6378
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
5919
6379
|
const consent = env === "prod" || env === "production" ? " --yes" : "";
|
|
5920
6380
|
throw new Error(
|
|
5921
|
-
`${
|
|
6381
|
+
`${message2}
|
|
5922
6382
|
${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
|
|
5923
6383
|
{ cause: error }
|
|
5924
6384
|
);
|
|
@@ -6034,7 +6494,7 @@ async function resolveVaultWrite(options) {
|
|
|
6034
6494
|
}
|
|
6035
6495
|
|
|
6036
6496
|
// src/security-command-context.ts
|
|
6037
|
-
var
|
|
6497
|
+
var import_promises10 = require("readline/promises");
|
|
6038
6498
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
6039
6499
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
6040
6500
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -6057,12 +6517,12 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
6057
6517
|
);
|
|
6058
6518
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
6059
6519
|
}
|
|
6060
|
-
async function interactiveConfirmation(
|
|
6061
|
-
if (dependencies.confirm) return dependencies.confirm(
|
|
6520
|
+
async function interactiveConfirmation(message2, dependencies) {
|
|
6521
|
+
if (dependencies.confirm) return dependencies.confirm(message2);
|
|
6062
6522
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
6063
|
-
const prompt = (0,
|
|
6523
|
+
const prompt = (0, import_promises10.createInterface)({ input: process.stdin, output: process.stdout });
|
|
6064
6524
|
try {
|
|
6065
|
-
const answer = await prompt.question(`${
|
|
6525
|
+
const answer = await prompt.question(`${message2} [y/N] `);
|
|
6066
6526
|
return /^y(?:es)?$/i.test(answer.trim());
|
|
6067
6527
|
} finally {
|
|
6068
6528
|
prompt.close();
|
|
@@ -6189,7 +6649,7 @@ var import_security2 = require("@odla-ai/security");
|
|
|
6189
6649
|
// src/security.ts
|
|
6190
6650
|
var import_node_path8 = require("path");
|
|
6191
6651
|
var import_security = require("@odla-ai/security");
|
|
6192
|
-
var
|
|
6652
|
+
var import_node3 = require("@odla-ai/security/node");
|
|
6193
6653
|
async function runHostedSecurity(options) {
|
|
6194
6654
|
if (options.sourceDisclosureAck !== "redacted") {
|
|
6195
6655
|
throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
|
|
@@ -6212,7 +6672,7 @@ async function runHostedSecurity(options) {
|
|
|
6212
6672
|
scope: selfAudit ? "platform:security:self" : null
|
|
6213
6673
|
};
|
|
6214
6674
|
const token = await injectedToken(options, tokenRequest);
|
|
6215
|
-
const snapshot = await (0,
|
|
6675
|
+
const snapshot = await (0, import_node3.snapshotDirectory)(target, {
|
|
6216
6676
|
exclude: !outputRelative.startsWith("../") && !(0, import_node_path8.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
6217
6677
|
});
|
|
6218
6678
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
@@ -6231,7 +6691,7 @@ async function runHostedSecurity(options) {
|
|
|
6231
6691
|
});
|
|
6232
6692
|
const harness = (0, import_security.createSecurityHarness)({
|
|
6233
6693
|
profile,
|
|
6234
|
-
store: new
|
|
6694
|
+
store: new import_node3.FileRunStore((0, import_node_path8.resolve)(output, "state")),
|
|
6235
6695
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
6236
6696
|
validationReasoner: hosted.validationReasoner,
|
|
6237
6697
|
policy: {
|
|
@@ -6241,7 +6701,7 @@ async function runHostedSecurity(options) {
|
|
|
6241
6701
|
}
|
|
6242
6702
|
});
|
|
6243
6703
|
const report2 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
6244
|
-
await (0,
|
|
6704
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report2);
|
|
6245
6705
|
const reportDigest = await (0, import_security.securityFingerprint)(report2);
|
|
6246
6706
|
await hosted.complete({
|
|
6247
6707
|
reportDigest,
|
|
@@ -6374,8 +6834,8 @@ async function startHostedSecurityJob(options) {
|
|
|
6374
6834
|
"start hosted security job"
|
|
6375
6835
|
);
|
|
6376
6836
|
} catch (error) {
|
|
6377
|
-
const
|
|
6378
|
-
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(
|
|
6837
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
6838
|
+
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(message2)) {
|
|
6379
6839
|
throw new Error("Hosted security plan or execution intent changed or became unavailable after review; fetch a fresh intent and explicitly acknowledge its digest before enqueueing again", { cause: error });
|
|
6380
6840
|
}
|
|
6381
6841
|
throw error;
|