@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/bin.cjs
CHANGED
|
@@ -456,12 +456,13 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
456
456
|
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
457
457
|
endpoint: audience,
|
|
458
458
|
email,
|
|
459
|
-
label: options.label ?? `odla CLI
|
|
459
|
+
label: options.label ?? `odla CLI (${scope})`,
|
|
460
460
|
scopes: [scope],
|
|
461
461
|
fetch: doFetch,
|
|
462
462
|
onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
|
|
463
463
|
const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
|
|
464
|
-
|
|
464
|
+
const requestName = scope === "app:code:host:connect" ? "terminal connection" : "scoped request";
|
|
465
|
+
out.log(`Review, then approve ${requestName} ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
465
466
|
if (approvalBrowser({ open: options.open }).open) {
|
|
466
467
|
await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
467
468
|
}
|
|
@@ -523,8 +524,8 @@ async function responseBody(response2) {
|
|
|
523
524
|
}
|
|
524
525
|
function apiError(status, body) {
|
|
525
526
|
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
526
|
-
const
|
|
527
|
-
return `read System AI admin changes failed (${status}): ${
|
|
527
|
+
const message2 = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
528
|
+
return `read System AI admin changes failed (${status}): ${message2}`;
|
|
528
529
|
}
|
|
529
530
|
function timestamp(value) {
|
|
530
531
|
if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
|
|
@@ -625,8 +626,8 @@ async function responseBody2(res) {
|
|
|
625
626
|
}
|
|
626
627
|
function apiError2(action, status, body) {
|
|
627
628
|
const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
|
|
628
|
-
const
|
|
629
|
-
return `${action} failed (${status}): ${
|
|
629
|
+
const message2 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
630
|
+
return `${action} failed (${status}): ${message2}`;
|
|
630
631
|
}
|
|
631
632
|
function isRecord2(value) {
|
|
632
633
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -806,8 +807,8 @@ async function responseBody3(res) {
|
|
|
806
807
|
}
|
|
807
808
|
function apiError3(action, status, body) {
|
|
808
809
|
const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
|
|
809
|
-
const
|
|
810
|
-
return `${action} failed (${status}): ${
|
|
810
|
+
const message2 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
811
|
+
return `${action} failed (${status}): ${message2}`;
|
|
811
812
|
}
|
|
812
813
|
function isRecord3(value) {
|
|
813
814
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -1487,8 +1488,8 @@ var CAPABILITIES = {
|
|
|
1487
1488
|
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
1488
1489
|
]
|
|
1489
1490
|
};
|
|
1490
|
-
function printCapabilities(
|
|
1491
|
-
if (
|
|
1491
|
+
function printCapabilities(json = false, out = console) {
|
|
1492
|
+
if (json) {
|
|
1492
1493
|
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
1493
1494
|
return;
|
|
1494
1495
|
}
|
|
@@ -1518,8 +1519,8 @@ var CalendarRequestError = class extends Error {
|
|
|
1518
1519
|
status;
|
|
1519
1520
|
/** Machine-readable `error.code` from the response body, when present. */
|
|
1520
1521
|
code;
|
|
1521
|
-
constructor(
|
|
1522
|
-
super(
|
|
1522
|
+
constructor(message2, status, code) {
|
|
1523
|
+
super(message2);
|
|
1523
1524
|
this.name = "CalendarRequestError";
|
|
1524
1525
|
this.status = status;
|
|
1525
1526
|
if (code !== void 0) this.code = code;
|
|
@@ -1686,8 +1687,8 @@ async function calendarJson(ctx, suffix, init) {
|
|
|
1686
1687
|
const body = await response2.json().catch(() => ({}));
|
|
1687
1688
|
if (!response2.ok) {
|
|
1688
1689
|
const code = textField(body.error?.code, 128);
|
|
1689
|
-
const
|
|
1690
|
-
const detail = `${code ? ` ${code}` : ""}${
|
|
1690
|
+
const message2 = textField(body.error?.message, 500);
|
|
1691
|
+
const detail = `${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`;
|
|
1691
1692
|
throw new CalendarRequestError(redactSecrets(`calendar request failed (${response2.status})${detail}`), response2.status, code);
|
|
1692
1693
|
}
|
|
1693
1694
|
return body;
|
|
@@ -1923,8 +1924,8 @@ function pollTimeout(value = 10 * 6e4) {
|
|
|
1923
1924
|
function productionConsent(env, yes, action) {
|
|
1924
1925
|
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action} for "${env}" without --yes`);
|
|
1925
1926
|
}
|
|
1926
|
-
function printStatus(status,
|
|
1927
|
-
if (
|
|
1927
|
+
function printStatus(status, json, out) {
|
|
1928
|
+
if (json) {
|
|
1928
1929
|
out.log(JSON.stringify(status, null, 2));
|
|
1929
1930
|
return;
|
|
1930
1931
|
}
|
|
@@ -1966,63 +1967,63 @@ function parseAgentOutput(line) {
|
|
|
1966
1967
|
} catch {
|
|
1967
1968
|
throw new HarnessProtocolError("agent emitted invalid JSON");
|
|
1968
1969
|
}
|
|
1969
|
-
const
|
|
1970
|
-
if (!
|
|
1970
|
+
const message2 = record2(value);
|
|
1971
|
+
if (!message2 || message2.protocolVersion !== HARNESS_PROTOCOL_VERSION) {
|
|
1971
1972
|
throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);
|
|
1972
1973
|
}
|
|
1973
|
-
if (
|
|
1974
|
+
if (message2.type === "event") {
|
|
1974
1975
|
return {
|
|
1975
1976
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
1976
1977
|
type: "event",
|
|
1977
|
-
kind: boundedText(
|
|
1978
|
-
...
|
|
1978
|
+
kind: boundedText(message2.kind, "event.kind", 120),
|
|
1979
|
+
...message2.payload === void 0 ? {} : { payload: message2.payload }
|
|
1979
1980
|
};
|
|
1980
1981
|
}
|
|
1981
|
-
if (
|
|
1982
|
-
const call = record2(
|
|
1982
|
+
if (message2.type === "inference.request") {
|
|
1983
|
+
const call = record2(message2.call);
|
|
1983
1984
|
if (!call || !Array.isArray(call.messages) || !Number.isSafeInteger(call.maxTokens)) {
|
|
1984
1985
|
throw new HarnessProtocolError("inference.request.call requires messages and maxTokens");
|
|
1985
1986
|
}
|
|
1986
1987
|
return {
|
|
1987
1988
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
1988
1989
|
type: "inference.request",
|
|
1989
|
-
requestId: boundedText(
|
|
1990
|
+
requestId: boundedText(message2.requestId, "requestId", 180),
|
|
1990
1991
|
call
|
|
1991
1992
|
};
|
|
1992
1993
|
}
|
|
1993
|
-
if (
|
|
1994
|
-
const input = record2(
|
|
1995
|
-
const tool = String(
|
|
1994
|
+
if (message2.type === "tool.request") {
|
|
1995
|
+
const input = record2(message2.input);
|
|
1996
|
+
const tool = String(message2.tool);
|
|
1996
1997
|
if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
|
|
1997
1998
|
throw new HarnessProtocolError("tool.request requires a registered tool and object input");
|
|
1998
1999
|
}
|
|
1999
2000
|
return {
|
|
2000
2001
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
2001
2002
|
type: "tool.request",
|
|
2002
|
-
requestId: boundedText(
|
|
2003
|
+
requestId: boundedText(message2.requestId, "requestId", 180),
|
|
2003
2004
|
tool,
|
|
2004
2005
|
input
|
|
2005
2006
|
};
|
|
2006
2007
|
}
|
|
2007
|
-
if (
|
|
2008
|
-
if (!(/* @__PURE__ */ new Set(["completed", "failed", "cancelled"])).has(String(
|
|
2008
|
+
if (message2.type === "attempt.complete") {
|
|
2009
|
+
if (!(/* @__PURE__ */ new Set(["completed", "failed", "cancelled"])).has(String(message2.status))) {
|
|
2009
2010
|
throw new HarnessProtocolError("attempt.complete.status is invalid");
|
|
2010
2011
|
}
|
|
2011
2012
|
return {
|
|
2012
2013
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
2013
2014
|
type: "attempt.complete",
|
|
2014
|
-
status:
|
|
2015
|
-
...
|
|
2015
|
+
status: message2.status,
|
|
2016
|
+
...message2.result === void 0 ? {} : { result: message2.result }
|
|
2016
2017
|
};
|
|
2017
2018
|
}
|
|
2018
2019
|
throw new HarnessProtocolError("agent message type is unsupported");
|
|
2019
2020
|
}
|
|
2020
|
-
function encodeAgentInput(
|
|
2021
|
-
return `${JSON.stringify(
|
|
2021
|
+
function encodeAgentInput(message2) {
|
|
2022
|
+
return `${JSON.stringify(message2)}
|
|
2022
2023
|
`;
|
|
2023
2024
|
}
|
|
2024
2025
|
|
|
2025
|
-
// ../harness/dist/chunk-
|
|
2026
|
+
// ../harness/dist/chunk-TA2FKK27.js
|
|
2026
2027
|
var import_child_process = require("child_process");
|
|
2027
2028
|
var import_fs = require("fs");
|
|
2028
2029
|
var import_promises2 = require("fs/promises");
|
|
@@ -2032,6 +2033,11 @@ var import_promises3 = require("fs/promises");
|
|
|
2032
2033
|
var import_os = require("os");
|
|
2033
2034
|
var import_path2 = require("path");
|
|
2034
2035
|
var import_child_process2 = require("child_process");
|
|
2036
|
+
var import_path3 = require("path");
|
|
2037
|
+
var import_promises4 = require("fs/promises");
|
|
2038
|
+
var import_os2 = require("os");
|
|
2039
|
+
var import_path4 = require("path");
|
|
2040
|
+
var import_child_process3 = require("child_process");
|
|
2035
2041
|
var DIGEST_IMAGE = /^[a-z0-9][a-z0-9._/-]*(?::[a-zA-Z0-9._-]+)?@sha256:[0-9a-f]{64}$/;
|
|
2036
2042
|
function assertPinnedImage(image) {
|
|
2037
2043
|
if (!DIGEST_IMAGE.test(image)) throw new TypeError("container image must be pinned by sha256 digest");
|
|
@@ -2075,7 +2081,7 @@ async function selectContainerEngine(requested = "auto", options = {}) {
|
|
|
2075
2081
|
throw new TypeError("no supported container engine found");
|
|
2076
2082
|
}
|
|
2077
2083
|
function inspectRootlessPodman() {
|
|
2078
|
-
return new Promise((
|
|
2084
|
+
return new Promise((resolve33, reject) => {
|
|
2079
2085
|
(0, import_child_process.execFile)(
|
|
2080
2086
|
"podman",
|
|
2081
2087
|
["info", "--format", "{{.Host.Security.Rootless}}"],
|
|
@@ -2085,7 +2091,7 @@ function inspectRootlessPodman() {
|
|
|
2085
2091
|
reject(new TypeError("could not verify that the active Podman service is rootless"));
|
|
2086
2092
|
return;
|
|
2087
2093
|
}
|
|
2088
|
-
|
|
2094
|
+
resolve33(stdout.trim() === "true");
|
|
2089
2095
|
}
|
|
2090
2096
|
);
|
|
2091
2097
|
});
|
|
@@ -2196,9 +2202,9 @@ async function runContainerAttempt(options) {
|
|
|
2196
2202
|
if (bytes.byteLength > 1e6) throw new Error("agent message exceeds 1 MB");
|
|
2197
2203
|
const line = bytes.toString("utf8");
|
|
2198
2204
|
if (!line.trim()) return;
|
|
2199
|
-
const
|
|
2200
|
-
if (
|
|
2201
|
-
const response2 = await options.onMessage(
|
|
2205
|
+
const message2 = parseAgentOutput(line);
|
|
2206
|
+
if (message2.type === "attempt.complete") complete = message2;
|
|
2207
|
+
const response2 = await options.onMessage(message2);
|
|
2202
2208
|
if (response2 && !child.stdin.destroyed) child.stdin.write(encodeAgentInput(response2));
|
|
2203
2209
|
};
|
|
2204
2210
|
try {
|
|
@@ -2246,29 +2252,142 @@ async function runContainerAttempt(options) {
|
|
|
2246
2252
|
options.signal?.removeEventListener("abort", abort);
|
|
2247
2253
|
}
|
|
2248
2254
|
}
|
|
2249
|
-
var
|
|
2250
|
-
|
|
2255
|
+
var SKIP_WORKSPACE_DIRS = /* @__PURE__ */ new Set([
|
|
2256
|
+
".git",
|
|
2257
|
+
".odla",
|
|
2258
|
+
".wrangler",
|
|
2259
|
+
"node_modules",
|
|
2260
|
+
"dist",
|
|
2261
|
+
"coverage"
|
|
2262
|
+
]);
|
|
2263
|
+
var SECRET_WORKSPACE_FILE = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
2264
|
+
function allowedWorkspacePath(relativePath) {
|
|
2265
|
+
const parts = relativePath.split("/");
|
|
2266
|
+
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) ?? "");
|
|
2267
|
+
}
|
|
2268
|
+
async function gitOutput(cwd, args, maxBytes) {
|
|
2269
|
+
const child = (0, import_child_process2.spawn)("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
2270
|
+
const stdout = [];
|
|
2271
|
+
const stderr = [];
|
|
2272
|
+
let bytes = 0;
|
|
2273
|
+
child.stdout.on("data", (chunk) => {
|
|
2274
|
+
bytes += chunk.byteLength;
|
|
2275
|
+
if (bytes > maxBytes) child.kill("SIGKILL");
|
|
2276
|
+
else stdout.push(chunk);
|
|
2277
|
+
});
|
|
2278
|
+
child.stderr.on("data", (chunk) => {
|
|
2279
|
+
if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
|
|
2280
|
+
});
|
|
2281
|
+
const code = await new Promise((accept, reject) => {
|
|
2282
|
+
child.once("error", reject);
|
|
2283
|
+
child.once("exit", accept);
|
|
2284
|
+
});
|
|
2285
|
+
if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
|
|
2286
|
+
if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
|
|
2287
|
+
return Buffer.concat(stdout);
|
|
2288
|
+
}
|
|
2289
|
+
async function gitBlobs(cwd, entries, maxBytes) {
|
|
2290
|
+
const child = (0, import_child_process2.spawn)("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
2291
|
+
const stdout = [];
|
|
2292
|
+
const stderr = [];
|
|
2293
|
+
let bytes = 0;
|
|
2294
|
+
child.stdout.on("data", (chunk) => {
|
|
2295
|
+
bytes += chunk.byteLength;
|
|
2296
|
+
if (bytes > maxBytes + entries.length * 100) child.kill("SIGKILL");
|
|
2297
|
+
else stdout.push(chunk);
|
|
2298
|
+
});
|
|
2299
|
+
child.stderr.on("data", (chunk) => {
|
|
2300
|
+
if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
|
|
2301
|
+
});
|
|
2302
|
+
child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
|
|
2303
|
+
`);
|
|
2304
|
+
const code = await new Promise((accept, reject) => {
|
|
2305
|
+
child.once("error", reject);
|
|
2306
|
+
child.once("exit", accept);
|
|
2307
|
+
});
|
|
2308
|
+
if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
|
|
2309
|
+
if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
|
|
2310
|
+
const output = Buffer.concat(stdout);
|
|
2311
|
+
const blobs = [];
|
|
2312
|
+
let offset = 0;
|
|
2313
|
+
let total = 0;
|
|
2314
|
+
for (const entry of entries) {
|
|
2315
|
+
const newline = output.indexOf(10, offset);
|
|
2316
|
+
if (newline < 0) throw new Error("git object output is truncated");
|
|
2317
|
+
const match = /^([0-9a-f]{40,64}) blob ([0-9]+)$/.exec(output.subarray(offset, newline).toString("utf8"));
|
|
2318
|
+
if (!match || match[1] !== entry.hash) throw new Error("git object output does not match its inventory");
|
|
2319
|
+
const length = Number(match[2]);
|
|
2320
|
+
if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
|
|
2321
|
+
throw new Error("git object output has an invalid length");
|
|
2322
|
+
}
|
|
2323
|
+
const content = output.subarray(newline + 1, newline + 1 + length);
|
|
2324
|
+
if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
|
|
2325
|
+
total += content.byteLength;
|
|
2326
|
+
if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2327
|
+
blobs.push(content);
|
|
2328
|
+
offset = newline + 1 + length + 1;
|
|
2329
|
+
}
|
|
2330
|
+
return blobs;
|
|
2331
|
+
}
|
|
2332
|
+
async function materializeGitTree(source, commitSha, options = {}) {
|
|
2333
|
+
if (!/^[0-9a-f]{40}$/.test(commitSha)) throw new TypeError("Git tree requires an exact commit SHA");
|
|
2334
|
+
const sourceDir = await (0, import_promises3.realpath)((0, import_path2.resolve)(source));
|
|
2335
|
+
const maxFiles = options.maxFiles ?? 2e4;
|
|
2336
|
+
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
2337
|
+
const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
|
|
2338
|
+
const entries = inventory.flatMap((record5) => {
|
|
2339
|
+
const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record5);
|
|
2340
|
+
return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
|
|
2341
|
+
});
|
|
2342
|
+
if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
2343
|
+
const root = await (0, import_promises3.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
|
|
2344
|
+
const targetRoot = (0, import_path2.join)(root, "source");
|
|
2345
|
+
await (0, import_promises3.mkdir)(targetRoot);
|
|
2346
|
+
let byteCount = 0;
|
|
2347
|
+
try {
|
|
2348
|
+
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
2349
|
+
for (const [index, entry] of entries.entries()) {
|
|
2350
|
+
const content = blobs[index];
|
|
2351
|
+
byteCount += content.byteLength;
|
|
2352
|
+
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2353
|
+
const target = (0, import_path2.resolve)(targetRoot, entry.path);
|
|
2354
|
+
if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
|
|
2355
|
+
await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
|
|
2356
|
+
await (0, import_promises3.writeFile)(target, content, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
2357
|
+
}
|
|
2358
|
+
return {
|
|
2359
|
+
root,
|
|
2360
|
+
sourceDir: targetRoot,
|
|
2361
|
+
fileCount: entries.length,
|
|
2362
|
+
byteCount,
|
|
2363
|
+
cleanup: () => (0, import_promises3.rm)(root, { recursive: true, force: true })
|
|
2364
|
+
};
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
await (0, import_promises3.rm)(root, { recursive: true, force: true });
|
|
2367
|
+
throw error;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2251
2370
|
async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
2252
2371
|
const files = [];
|
|
2253
2372
|
let bytes = 0;
|
|
2254
2373
|
const walk = async (dir) => {
|
|
2255
|
-
for (const entry of await (0,
|
|
2256
|
-
if (entry.isDirectory() &&
|
|
2257
|
-
if (!entry.isDirectory() &&
|
|
2258
|
-
const path = (0,
|
|
2374
|
+
for (const entry of await (0, import_promises4.readdir)(dir, { withFileTypes: true })) {
|
|
2375
|
+
if (entry.isDirectory() && SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
2376
|
+
if (!entry.isDirectory() && SECRET_WORKSPACE_FILE.test(entry.name)) continue;
|
|
2377
|
+
const path = (0, import_path4.join)(dir, entry.name);
|
|
2259
2378
|
if (entry.isSymbolicLink()) continue;
|
|
2260
2379
|
if (entry.isDirectory()) {
|
|
2261
2380
|
await walk(path);
|
|
2262
2381
|
continue;
|
|
2263
2382
|
}
|
|
2264
2383
|
if (!entry.isFile()) continue;
|
|
2265
|
-
const metadata2 = await (0,
|
|
2384
|
+
const metadata2 = await (0, import_promises4.stat)(path);
|
|
2266
2385
|
bytes += metadata2.size;
|
|
2267
2386
|
if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
2268
2387
|
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2269
2388
|
files.push({
|
|
2270
2389
|
source: path,
|
|
2271
|
-
relativePath: (0,
|
|
2390
|
+
relativePath: (0, import_path4.relative)(sourceDir, path),
|
|
2272
2391
|
mode: metadata2.mode & 511,
|
|
2273
2392
|
bytes: metadata2.size
|
|
2274
2393
|
});
|
|
@@ -2277,16 +2396,62 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
2277
2396
|
await walk(sourceDir);
|
|
2278
2397
|
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
2279
2398
|
}
|
|
2399
|
+
async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
2400
|
+
const child = (0, import_child_process3.spawn)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
2401
|
+
cwd: sourceDir,
|
|
2402
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2403
|
+
shell: false
|
|
2404
|
+
});
|
|
2405
|
+
const stdout = [];
|
|
2406
|
+
const stderr = [];
|
|
2407
|
+
let outputBytes = 0;
|
|
2408
|
+
child.stdout.on("data", (chunk) => {
|
|
2409
|
+
outputBytes += chunk.byteLength;
|
|
2410
|
+
if (outputBytes > 8 * 1024 * 1024) child.kill("SIGKILL");
|
|
2411
|
+
else stdout.push(chunk);
|
|
2412
|
+
});
|
|
2413
|
+
child.stderr.on("data", (chunk) => {
|
|
2414
|
+
if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
|
|
2415
|
+
});
|
|
2416
|
+
const code = await new Promise((accept, reject) => {
|
|
2417
|
+
child.once("error", reject);
|
|
2418
|
+
child.once("exit", accept);
|
|
2419
|
+
});
|
|
2420
|
+
if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
|
|
2421
|
+
if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
|
|
2422
|
+
const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
|
|
2423
|
+
if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
2424
|
+
const root = (0, import_path4.resolve)(sourceDir);
|
|
2425
|
+
const files = [];
|
|
2426
|
+
let bytes = 0;
|
|
2427
|
+
for (const relativePath of paths) {
|
|
2428
|
+
if (!allowedWorkspacePath(relativePath)) continue;
|
|
2429
|
+
const source = (0, import_path4.resolve)(root, relativePath);
|
|
2430
|
+
if (!source.startsWith(`${root}${import_path4.sep}`)) throw new TypeError("git file path escapes workspace");
|
|
2431
|
+
let metadata2;
|
|
2432
|
+
try {
|
|
2433
|
+
metadata2 = await (0, import_promises4.lstat)(source);
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
if (error.code === "ENOENT") continue;
|
|
2436
|
+
throw error;
|
|
2437
|
+
}
|
|
2438
|
+
if (metadata2.isSymbolicLink() || !metadata2.isFile()) continue;
|
|
2439
|
+
bytes += metadata2.size;
|
|
2440
|
+
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2441
|
+
files.push({ source, relativePath, mode: metadata2.mode & 511, bytes: metadata2.size });
|
|
2442
|
+
}
|
|
2443
|
+
return files;
|
|
2444
|
+
}
|
|
2280
2445
|
async function copyTree(files, destination) {
|
|
2281
2446
|
for (const file of files) {
|
|
2282
|
-
const target = (0,
|
|
2283
|
-
await (0,
|
|
2284
|
-
await (0,
|
|
2285
|
-
await (0,
|
|
2447
|
+
const target = (0, import_path4.join)(destination, file.relativePath);
|
|
2448
|
+
await (0, import_promises4.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
|
|
2449
|
+
await (0, import_promises4.copyFile)(file.source, target);
|
|
2450
|
+
await (0, import_promises4.chmod)(target, file.mode);
|
|
2286
2451
|
}
|
|
2287
2452
|
}
|
|
2288
2453
|
async function captureGitDiff(root, maxBytes) {
|
|
2289
|
-
const child = (0,
|
|
2454
|
+
const child = (0, import_child_process3.spawn)("git", [
|
|
2290
2455
|
"diff",
|
|
2291
2456
|
"--no-index",
|
|
2292
2457
|
"--binary",
|
|
@@ -2319,15 +2484,17 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
2319
2484
|
return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
|
|
2320
2485
|
}
|
|
2321
2486
|
async function stageWorkspace(source, options = {}) {
|
|
2322
|
-
const sourceDir = await (0,
|
|
2323
|
-
const sourceStat = await (0,
|
|
2487
|
+
const sourceDir = await (0, import_promises4.realpath)((0, import_path4.resolve)(source));
|
|
2488
|
+
const sourceStat = await (0, import_promises4.stat)(sourceDir);
|
|
2324
2489
|
if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
|
|
2325
|
-
const root = await (0,
|
|
2326
|
-
const baselineDir = (0,
|
|
2327
|
-
const workspaceDir = (0,
|
|
2328
|
-
await Promise.all([(0,
|
|
2490
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
2491
|
+
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
2492
|
+
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
2493
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
2329
2494
|
try {
|
|
2330
|
-
const
|
|
2495
|
+
const maxFiles = options.maxFiles ?? 2e4;
|
|
2496
|
+
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
2497
|
+
const files = options.gitTrackedAndUnignored ? await gitSourceFiles(sourceDir, maxFiles, maxBytes) : await sourceFiles(sourceDir, maxFiles, maxBytes);
|
|
2331
2498
|
await Promise.all([copyTree(files, baselineDir), copyTree(files, workspaceDir)]);
|
|
2332
2499
|
return {
|
|
2333
2500
|
root,
|
|
@@ -2335,15 +2502,49 @@ async function stageWorkspace(source, options = {}) {
|
|
|
2335
2502
|
workspaceDir,
|
|
2336
2503
|
fileCount: files.length,
|
|
2337
2504
|
byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
|
|
2338
|
-
patch: (
|
|
2339
|
-
cleanup: () => (0,
|
|
2505
|
+
patch: (maxBytes2) => captureGitDiff(root, maxBytes2),
|
|
2506
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
2340
2507
|
};
|
|
2341
2508
|
} catch (error) {
|
|
2342
|
-
await (0,
|
|
2509
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
async function stageWorkspacePair(baselineSource, workspaceSource, options = {}) {
|
|
2514
|
+
const baselineDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(baselineSource));
|
|
2515
|
+
const workspaceDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(workspaceSource));
|
|
2516
|
+
const maxFiles = options.maxFiles ?? 2e4;
|
|
2517
|
+
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
2518
|
+
const [baselineFiles, workspaceFiles] = await Promise.all([
|
|
2519
|
+
sourceFiles(baselineDirSource, maxFiles, maxBytes),
|
|
2520
|
+
sourceFiles(workspaceDirSource, maxFiles, maxBytes)
|
|
2521
|
+
]);
|
|
2522
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
2523
|
+
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
2524
|
+
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
2525
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
2526
|
+
try {
|
|
2527
|
+
await Promise.all([copyTree(baselineFiles, baselineDir), copyTree(workspaceFiles, workspaceDir)]);
|
|
2528
|
+
return {
|
|
2529
|
+
root,
|
|
2530
|
+
baselineDir,
|
|
2531
|
+
workspaceDir,
|
|
2532
|
+
fileCount: workspaceFiles.length,
|
|
2533
|
+
byteCount: workspaceFiles.reduce((sum, file) => sum + file.bytes, 0),
|
|
2534
|
+
patch: (maxPatchBytes) => captureGitDiff(root, maxPatchBytes),
|
|
2535
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
2536
|
+
};
|
|
2537
|
+
} catch (error) {
|
|
2538
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
2343
2539
|
throw error;
|
|
2344
2540
|
}
|
|
2345
2541
|
}
|
|
2346
2542
|
|
|
2543
|
+
// ../harness/dist/chunk-4EM6NMET.js
|
|
2544
|
+
var import_crypto = require("crypto");
|
|
2545
|
+
var import_promises5 = require("fs/promises");
|
|
2546
|
+
var import_path5 = require("path");
|
|
2547
|
+
|
|
2347
2548
|
// ../camel/dist/chunk-7FHPOQVP.js
|
|
2348
2549
|
var CamelError = class extends Error {
|
|
2349
2550
|
code;
|
|
@@ -2362,8 +2563,8 @@ function canonicalJson(value) {
|
|
|
2362
2563
|
}
|
|
2363
2564
|
async function sha256Hex(value) {
|
|
2364
2565
|
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
2365
|
-
const
|
|
2366
|
-
return [...new Uint8Array(
|
|
2566
|
+
const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer);
|
|
2567
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2367
2568
|
}
|
|
2368
2569
|
function utf8Length(value) {
|
|
2369
2570
|
if (typeof value === "string") return new TextEncoder().encode(value).byteLength;
|
|
@@ -2640,8 +2841,8 @@ function freeze(value) {
|
|
|
2640
2841
|
})
|
|
2641
2842
|
});
|
|
2642
2843
|
}
|
|
2643
|
-
function invalid2(
|
|
2644
|
-
return new CamelError("state_conflict",
|
|
2844
|
+
function invalid2(message2) {
|
|
2845
|
+
return new CamelError("state_conflict", message2);
|
|
2645
2846
|
}
|
|
2646
2847
|
var SHA4 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
2647
2848
|
var REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
@@ -2676,25 +2877,22 @@ function validateSnapshot(snapshot, limits) {
|
|
|
2676
2877
|
}
|
|
2677
2878
|
}
|
|
2678
2879
|
|
|
2679
|
-
// ../harness/dist/chunk-
|
|
2680
|
-
var import_child_process3 = require("child_process");
|
|
2681
|
-
var import_promises4 = require("fs/promises");
|
|
2682
|
-
var import_path3 = require("path");
|
|
2880
|
+
// ../harness/dist/chunk-4EM6NMET.js
|
|
2683
2881
|
var import_child_process4 = require("child_process");
|
|
2882
|
+
var import_promises6 = require("fs/promises");
|
|
2883
|
+
var import_path6 = require("path");
|
|
2884
|
+
var import_child_process5 = require("child_process");
|
|
2684
2885
|
var import_process2 = require("process");
|
|
2685
|
-
var import_crypto = require("crypto");
|
|
2686
2886
|
var import_crypto2 = require("crypto");
|
|
2687
|
-
var import_fs2 = require("fs");
|
|
2688
|
-
var import_promises5 = require("fs/promises");
|
|
2689
|
-
var import_path4 = require("path");
|
|
2690
2887
|
var import_crypto3 = require("crypto");
|
|
2691
|
-
var
|
|
2692
|
-
var import_path5 = require("path");
|
|
2888
|
+
var import_fs2 = require("fs");
|
|
2693
2889
|
var import_promises7 = require("fs/promises");
|
|
2694
|
-
var import_os2 = require("os");
|
|
2695
|
-
var import_path6 = require("path");
|
|
2696
|
-
var import_promises8 = require("fs/promises");
|
|
2697
2890
|
var import_path7 = require("path");
|
|
2891
|
+
var import_promises8 = require("fs/promises");
|
|
2892
|
+
var import_os3 = require("os");
|
|
2893
|
+
var import_path8 = require("path");
|
|
2894
|
+
var import_promises9 = require("fs/promises");
|
|
2895
|
+
var import_path9 = require("path");
|
|
2698
2896
|
|
|
2699
2897
|
// ../camel/dist/chunk-LAXU2AVK.js
|
|
2700
2898
|
function conversionPolicyDigest(policy) {
|
|
@@ -2972,8 +3170,34 @@ function looksLikeDestination(value) {
|
|
|
2972
3170
|
return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
|
|
2973
3171
|
}
|
|
2974
3172
|
|
|
2975
|
-
// ../harness/dist/chunk-
|
|
3173
|
+
// ../harness/dist/chunk-4EM6NMET.js
|
|
2976
3174
|
var import_crypto4 = require("crypto");
|
|
3175
|
+
async function digestStagedWorkspace(root, limits) {
|
|
3176
|
+
const files = [];
|
|
3177
|
+
const walk = async (directory) => {
|
|
3178
|
+
const entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
|
|
3179
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
3180
|
+
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
3181
|
+
const target = (0, import_path5.resolve)(directory, entry.name);
|
|
3182
|
+
if (entry.isDirectory()) await walk(target);
|
|
3183
|
+
else if (entry.isFile()) {
|
|
3184
|
+
files.push({ path: (0, import_path5.relative)(root, target).split("\\").join("/"), target });
|
|
3185
|
+
if (files.length > limits.maxFiles) throw new TypeError("workspace digest exceeds its file bound");
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
await walk((0, import_path5.resolve)(root));
|
|
3190
|
+
const hash = (0, import_crypto.createHash)("sha256");
|
|
3191
|
+
let bytes = 0;
|
|
3192
|
+
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
3193
|
+
const content = await (0, import_promises5.readFile)(file.target);
|
|
3194
|
+
bytes += Buffer.byteLength(file.path) + content.byteLength;
|
|
3195
|
+
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
3196
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
|
|
3197
|
+
hash.update(content);
|
|
3198
|
+
}
|
|
3199
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3200
|
+
}
|
|
2977
3201
|
var CODE_RUNTIME_PROTOCOL_VERSION = 1;
|
|
2978
3202
|
async function runCodeRuntimeHeartbeatLoop(options) {
|
|
2979
3203
|
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
@@ -3045,8 +3269,8 @@ function wait(ms, signal) {
|
|
|
3045
3269
|
});
|
|
3046
3270
|
}
|
|
3047
3271
|
var CodeRuntimeControlError = class extends Error {
|
|
3048
|
-
constructor(
|
|
3049
|
-
super(
|
|
3272
|
+
constructor(message2, status, code = "control_error") {
|
|
3273
|
+
super(message2);
|
|
3050
3274
|
this.status = status;
|
|
3051
3275
|
this.code = code;
|
|
3052
3276
|
}
|
|
@@ -3125,9 +3349,9 @@ function createCodeRuntimeControlClient(options) {
|
|
|
3125
3349
|
}
|
|
3126
3350
|
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
|
|
3127
3351
|
},
|
|
3128
|
-
reportSessionFailure: async (sessionId,
|
|
3129
|
-
if (!
|
|
3130
|
-
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message:
|
|
3352
|
+
reportSessionFailure: async (sessionId, message2) => {
|
|
3353
|
+
if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
|
|
3354
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
|
|
3131
3355
|
}
|
|
3132
3356
|
};
|
|
3133
3357
|
}
|
|
@@ -3194,10 +3418,28 @@ async function parseSource(value) {
|
|
|
3194
3418
|
if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("source file");
|
|
3195
3419
|
return { path: file.path, content: file.content };
|
|
3196
3420
|
});
|
|
3421
|
+
const referencesValue = snapshot.references === void 0 ? [] : snapshot.references;
|
|
3422
|
+
if (!Array.isArray(referencesValue) || referencesValue.length > 5) throw invalid("reference sources");
|
|
3423
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
3424
|
+
const references = [];
|
|
3425
|
+
for (const item of referencesValue) {
|
|
3426
|
+
const reference = record3(item);
|
|
3427
|
+
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");
|
|
3428
|
+
aliases.add(reference.alias);
|
|
3429
|
+
const referenceFiles = reference.files.map((entry) => {
|
|
3430
|
+
const file = record3(entry);
|
|
3431
|
+
if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("reference source file");
|
|
3432
|
+
return { path: file.path, content: file.content };
|
|
3433
|
+
});
|
|
3434
|
+
const source2 = { repository: reference.repository, commitSha: reference.commitSha, files: referenceFiles };
|
|
3435
|
+
const referenceDigest = await digestCodeRepositorySnapshot(source2, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
|
|
3436
|
+
if (referenceDigest !== reference.treeDigest) throw invalid("reference source digest");
|
|
3437
|
+
references.push({ alias: reference.alias, ...source2, treeDigest: referenceDigest });
|
|
3438
|
+
}
|
|
3197
3439
|
const source = { repository: snapshot.repository, commitSha: snapshot.commitSha, files };
|
|
3198
|
-
const
|
|
3199
|
-
if (
|
|
3200
|
-
return { ...source, treeDigest:
|
|
3440
|
+
const digest = await digestCodeRepositorySnapshot(source, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
|
|
3441
|
+
if (digest !== snapshot.treeDigest) throw invalid("source digest");
|
|
3442
|
+
return { ...source, treeDigest: digest, ...references.length ? { references } : {} };
|
|
3201
3443
|
}
|
|
3202
3444
|
function parseReview(value) {
|
|
3203
3445
|
const review = record3(record3(value)?.review);
|
|
@@ -3256,9 +3498,9 @@ function validateRelativePath(path) {
|
|
|
3256
3498
|
}
|
|
3257
3499
|
function resolveCodePath(workspaceDir, path) {
|
|
3258
3500
|
validateRelativePath(path);
|
|
3259
|
-
const root = (0,
|
|
3260
|
-
const target = (0,
|
|
3261
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
3501
|
+
const root = (0, import_path6.resolve)(workspaceDir);
|
|
3502
|
+
const target = (0, import_path6.resolve)(root, path);
|
|
3503
|
+
if (target !== root && !target.startsWith(`${root}${import_path6.sep}`)) throw new TypeError("path escapes the staged workspace");
|
|
3262
3504
|
return target;
|
|
3263
3505
|
}
|
|
3264
3506
|
async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
@@ -3266,7 +3508,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
|
3266
3508
|
await gitApply(workspaceDir, patch2, false);
|
|
3267
3509
|
for (const path of paths) {
|
|
3268
3510
|
try {
|
|
3269
|
-
const info = await (0,
|
|
3511
|
+
const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
|
|
3270
3512
|
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
3271
3513
|
throw new TypeError("patch created a non-regular workspace entry");
|
|
3272
3514
|
}
|
|
@@ -3278,7 +3520,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
|
3278
3520
|
function gitApply(cwd, patch2, check) {
|
|
3279
3521
|
return new Promise((accept, reject) => {
|
|
3280
3522
|
const args = ["apply", "--recount", "--whitespace=nowarn", ...check ? ["--check"] : [], "-"];
|
|
3281
|
-
const child = (0,
|
|
3523
|
+
const child = (0, import_child_process4.spawn)("git", args, {
|
|
3282
3524
|
cwd,
|
|
3283
3525
|
shell: false,
|
|
3284
3526
|
stdio: ["pipe", "ignore", "pipe"],
|
|
@@ -3322,7 +3564,7 @@ async function restoreCodeWorkspaceCheckpoint(input) {
|
|
|
3322
3564
|
}
|
|
3323
3565
|
var ARTIFACT_PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
3324
3566
|
var PRIVATE_ARTIFACT_PART = /^(?:\.git|\.odla|\.wrangler|\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json)$/i;
|
|
3325
|
-
function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${(0,
|
|
3567
|
+
function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${(0, import_crypto2.randomUUID)().slice(0, 12)}`) {
|
|
3326
3568
|
assertCodeBuildRecipe(recipe2);
|
|
3327
3569
|
if (/[,\r\n]/.test(workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
|
|
3328
3570
|
const uid = typeof import_process2.getuid === "function" ? (0, import_process2.getuid)() : 1e3;
|
|
@@ -3377,7 +3619,7 @@ function createContainerRecipeExecutor(engine) {
|
|
|
3377
3619
|
return {
|
|
3378
3620
|
async run(input) {
|
|
3379
3621
|
await verifyContainerEngineBoundary(engine);
|
|
3380
|
-
const name = `odla-recipe-${(0,
|
|
3622
|
+
const name = `odla-recipe-${(0, import_crypto2.randomUUID)().slice(0, 12)}`;
|
|
3381
3623
|
const args = buildRecipeContainerArgs(engine, input.workspaceDir, input.recipe, name);
|
|
3382
3624
|
return execute(engine, args, name, input.recipe, input.signal);
|
|
3383
3625
|
}
|
|
@@ -3401,7 +3643,7 @@ function parseMemory(value) {
|
|
|
3401
3643
|
function execute(engine, args, name, recipe2, signal) {
|
|
3402
3644
|
return new Promise((accept, reject) => {
|
|
3403
3645
|
const started = Date.now();
|
|
3404
|
-
const child = (0,
|
|
3646
|
+
const child = (0, import_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
3405
3647
|
const stdout = [];
|
|
3406
3648
|
const stderr = [];
|
|
3407
3649
|
let bytes = 0;
|
|
@@ -3414,7 +3656,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
3414
3656
|
timedOut = reason === "timeout";
|
|
3415
3657
|
outputLimitExceeded = reason === "output";
|
|
3416
3658
|
const remove = engine === "container" ? ["delete", "--force", name] : ["rm", "-f", name];
|
|
3417
|
-
const killer = (0,
|
|
3659
|
+
const killer = (0, import_child_process5.spawn)(engine, remove, { shell: false, stdio: "ignore" });
|
|
3418
3660
|
killer.unref();
|
|
3419
3661
|
child.kill("SIGTERM");
|
|
3420
3662
|
};
|
|
@@ -3448,32 +3690,6 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
3448
3690
|
});
|
|
3449
3691
|
});
|
|
3450
3692
|
}
|
|
3451
|
-
async function digestStagedWorkspace(root, limits) {
|
|
3452
|
-
const files = [];
|
|
3453
|
-
const walk = async (directory) => {
|
|
3454
|
-
const entries = await (0, import_promises6.readdir)(directory, { withFileTypes: true });
|
|
3455
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
3456
|
-
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
3457
|
-
const target = (0, import_path5.resolve)(directory, entry.name);
|
|
3458
|
-
if (entry.isDirectory()) await walk(target);
|
|
3459
|
-
else if (entry.isFile()) {
|
|
3460
|
-
files.push({ path: (0, import_path5.relative)(root, target).split("\\").join("/"), target });
|
|
3461
|
-
if (files.length > limits.maxFiles) throw new TypeError("workspace digest exceeds its file bound");
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
|
-
};
|
|
3465
|
-
await walk((0, import_path5.resolve)(root));
|
|
3466
|
-
const hash = (0, import_crypto3.createHash)("sha256");
|
|
3467
|
-
let bytes = 0;
|
|
3468
|
-
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
3469
|
-
const content = await (0, import_promises6.readFile)(file.target);
|
|
3470
|
-
bytes += Buffer.byteLength(file.path) + content.byteLength;
|
|
3471
|
-
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
3472
|
-
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
|
|
3473
|
-
hash.update(content);
|
|
3474
|
-
}
|
|
3475
|
-
return `sha256:${hash.digest("hex")}`;
|
|
3476
|
-
}
|
|
3477
3693
|
var SHA3 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
3478
3694
|
var DIGEST3 = /^sha256:[0-9a-f]{64}$/;
|
|
3479
3695
|
var ID3 = /^[A-Za-z0-9._:-]{1,160}$/;
|
|
@@ -3521,7 +3737,7 @@ async function verifyCodeCandidate(input) {
|
|
|
3521
3737
|
}
|
|
3522
3738
|
const fields = {
|
|
3523
3739
|
schemaVersion: 1,
|
|
3524
|
-
verificationId: input.verificationId ?? `verify-${(0,
|
|
3740
|
+
verificationId: input.verificationId ?? `verify-${(0, import_crypto3.randomUUID)()}`,
|
|
3525
3741
|
trustedBaseCommitSha: input.trustedBaseCommitSha,
|
|
3526
3742
|
trustedBaseDigest: input.trustedBaseDigest,
|
|
3527
3743
|
patchDigest,
|
|
@@ -3582,8 +3798,8 @@ async function inspectArtifacts(workspaceDir, recipe2) {
|
|
|
3582
3798
|
const receipts = [];
|
|
3583
3799
|
for (const artifact of recipe2.expectedArtifacts ?? []) {
|
|
3584
3800
|
try {
|
|
3585
|
-
const path = (0,
|
|
3586
|
-
const info = await (0,
|
|
3801
|
+
const path = (0, import_path7.join)(workspaceDir, artifact.path);
|
|
3802
|
+
const info = await (0, import_promises7.lstat)(path);
|
|
3587
3803
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
3588
3804
|
receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
|
|
3589
3805
|
} else if (info.size > artifact.maximumBytes) {
|
|
@@ -3600,7 +3816,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
|
|
|
3600
3816
|
}
|
|
3601
3817
|
function hashFile(path) {
|
|
3602
3818
|
return new Promise((accept, reject) => {
|
|
3603
|
-
const hash = (0,
|
|
3819
|
+
const hash = (0, import_crypto3.createHash)("sha256");
|
|
3604
3820
|
const stream = (0, import_fs2.createReadStream)(path);
|
|
3605
3821
|
stream.on("data", (chunk) => {
|
|
3606
3822
|
hash.update(chunk);
|
|
@@ -3658,7 +3874,7 @@ function digestJson(value) {
|
|
|
3658
3874
|
return digestBytes(JSON.stringify(value));
|
|
3659
3875
|
}
|
|
3660
3876
|
function digestBytes(value) {
|
|
3661
|
-
return `sha256:${(0,
|
|
3877
|
+
return `sha256:${(0, import_crypto3.createHash)("sha256").update(value).digest("hex")}`;
|
|
3662
3878
|
}
|
|
3663
3879
|
function integer(value, minimum, maximum) {
|
|
3664
3880
|
return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
|
|
@@ -3679,8 +3895,8 @@ async function prepareRuntimeCheckpoint(input) {
|
|
|
3679
3895
|
policy: {
|
|
3680
3896
|
policyId: "code.runtime",
|
|
3681
3897
|
recipes: input.recipes,
|
|
3682
|
-
maximumFiles:
|
|
3683
|
-
maximumBytes:
|
|
3898
|
+
maximumFiles: 2e4,
|
|
3899
|
+
maximumBytes: 512 * 1024 * 1024
|
|
3684
3900
|
},
|
|
3685
3901
|
recipeExecutor: input.recipeExecutor
|
|
3686
3902
|
});
|
|
@@ -3754,7 +3970,7 @@ var CodeRuntimeCheckpointManager = class {
|
|
|
3754
3970
|
await this.options.event(command, {
|
|
3755
3971
|
type: "message",
|
|
3756
3972
|
actor: "system",
|
|
3757
|
-
body: `Candidate ${candidate.candidateId} is ready for
|
|
3973
|
+
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`
|
|
3758
3974
|
}, pending.refs);
|
|
3759
3975
|
this.#pending.delete(command.commandId);
|
|
3760
3976
|
return true;
|
|
@@ -3762,11 +3978,11 @@ var CodeRuntimeCheckpointManager = class {
|
|
|
3762
3978
|
};
|
|
3763
3979
|
var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
3764
3980
|
var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
3765
|
-
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0,
|
|
3981
|
+
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.tmpdir)()) {
|
|
3766
3982
|
if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
|
|
3767
|
-
const root = await (0,
|
|
3768
|
-
const sourceDir = (0,
|
|
3769
|
-
await (0,
|
|
3983
|
+
const root = await (0, import_promises8.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
|
|
3984
|
+
const sourceDir = (0, import_path8.join)(root, "source");
|
|
3985
|
+
await (0, import_promises8.mkdir)(sourceDir);
|
|
3770
3986
|
const seen = /* @__PURE__ */ new Set();
|
|
3771
3987
|
let bytes = 0;
|
|
3772
3988
|
try {
|
|
@@ -3776,17 +3992,56 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os2.
|
|
|
3776
3992
|
seen.add(file.path);
|
|
3777
3993
|
bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
|
|
3778
3994
|
if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
|
|
3779
|
-
const target = (0,
|
|
3780
|
-
if (!target.startsWith(`${(0,
|
|
3781
|
-
await (0,
|
|
3782
|
-
await (0,
|
|
3995
|
+
const target = (0, import_path8.resolve)(sourceDir, file.path);
|
|
3996
|
+
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code source path escapes its root");
|
|
3997
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
3998
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
|
|
3999
|
+
}
|
|
4000
|
+
for (const reference of snapshot.references ?? []) {
|
|
4001
|
+
validateAlias(reference.alias);
|
|
4002
|
+
if (!reference.files.length || reference.files.length > 1e4) throw new TypeError("Code reference file count is invalid");
|
|
4003
|
+
for (const file of reference.files) {
|
|
4004
|
+
validatePath(file.path);
|
|
4005
|
+
const path = `.odla-references/${reference.alias}/${file.path}`;
|
|
4006
|
+
if (seen.has(path)) throw new TypeError("Code reference repeats a path");
|
|
4007
|
+
seen.add(path);
|
|
4008
|
+
bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
|
|
4009
|
+
if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
|
|
4010
|
+
const target = (0, import_path8.resolve)(sourceDir, path);
|
|
4011
|
+
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
4012
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
4013
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
4014
|
+
}
|
|
3783
4015
|
}
|
|
3784
|
-
return { sourceDir, cleanup: () => (0,
|
|
4016
|
+
return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
|
|
3785
4017
|
} catch (cause) {
|
|
3786
|
-
await (0,
|
|
4018
|
+
await (0, import_promises8.rm)(root, { recursive: true, force: true });
|
|
3787
4019
|
throw cause;
|
|
3788
4020
|
}
|
|
3789
4021
|
}
|
|
4022
|
+
function validateAlias(alias) {
|
|
4023
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
|
|
4024
|
+
throw new TypeError("Code reference alias is invalid");
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
async function attachCodeRuntimeReferences(workspace, references) {
|
|
4028
|
+
let bytes = 0;
|
|
4029
|
+
for (const reference of references) {
|
|
4030
|
+
validateAlias(reference.alias);
|
|
4031
|
+
for (const file of reference.files) {
|
|
4032
|
+
validatePath(file.path);
|
|
4033
|
+
const path = `.odla-references/${reference.alias}/${file.path}`;
|
|
4034
|
+
bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
|
|
4035
|
+
if (bytes > 64 * 1024 * 1024) throw new TypeError("Code reference set exceeds its byte bound");
|
|
4036
|
+
for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
|
|
4037
|
+
const target = (0, import_path8.resolve)(root, path);
|
|
4038
|
+
if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
4039
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
4040
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
3790
4045
|
function validatePath(path) {
|
|
3791
4046
|
const parts = path.split("/");
|
|
3792
4047
|
if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED2.has(part) || SECRET2.test(part))) {
|
|
@@ -3889,15 +4144,15 @@ async function environment(input, options, tool) {
|
|
|
3889
4144
|
{ id: "authority", value: `lease:${input.lease.leaseId}`, readers: input.readers },
|
|
3890
4145
|
{ id: "reader", value: options.readerId, readers: input.readers }
|
|
3891
4146
|
]);
|
|
3892
|
-
const
|
|
4147
|
+
const digest = await destinationRegistryDigest([input.workspaceId]);
|
|
3893
4148
|
const approvals = ["irreversible_mutation", "external_send", "financial", "secret_read"];
|
|
3894
4149
|
if (options.recipeAuthorization === "exact_approval") approvals.push("code_execution");
|
|
3895
4150
|
const policy = createEffectPolicy({
|
|
3896
|
-
destinationRegistries: { [DESTINATIONS]: { digest
|
|
4151
|
+
destinationRegistries: { [DESTINATIONS]: { digest, values: [input.workspaceId] } },
|
|
3897
4152
|
approvalEffects: approvals
|
|
3898
4153
|
});
|
|
3899
4154
|
const fixedArgs = {
|
|
3900
|
-
workspace: { role: "destination", value: ingress.control("workspace"), registryId: DESTINATIONS, registryDigest:
|
|
4155
|
+
workspace: { role: "destination", value: ingress.control("workspace"), registryId: DESTINATIONS, registryDigest: digest },
|
|
3901
4156
|
authority: { role: "authority", value: ingress.control("authority") }
|
|
3902
4157
|
};
|
|
3903
4158
|
return { ingress, policy, fixedArgs, reader: ingress.control("reader"), runId: `${input.request.requestId}:${tool}` };
|
|
@@ -3968,11 +4223,11 @@ async function read(context, request, options, policy) {
|
|
|
3968
4223
|
const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
|
|
3969
4224
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3970
4225
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
3971
|
-
const info = await (0,
|
|
4226
|
+
const info = await (0, import_promises9.stat)(target);
|
|
3972
4227
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
3973
4228
|
throw new TypeError("file is not a bounded regular source file");
|
|
3974
4229
|
}
|
|
3975
|
-
const source = await (0,
|
|
4230
|
+
const source = await (0, import_promises9.readFile)(target);
|
|
3976
4231
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
3977
4232
|
const lines = source.toString("utf8").split("\n");
|
|
3978
4233
|
const content = lines.slice(startLine - 1, endLine).join("\n");
|
|
@@ -3985,6 +4240,9 @@ async function patch(context, request, options, policy) {
|
|
|
3985
4240
|
exactKeys(request.input, ["patch"]);
|
|
3986
4241
|
const value = stringField(request.input, "patch");
|
|
3987
4242
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
4243
|
+
if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
|
|
4244
|
+
throw new TypeError("patch targets a read-only reference source");
|
|
4245
|
+
}
|
|
3988
4246
|
const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
|
|
3989
4247
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3990
4248
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
@@ -4046,12 +4304,12 @@ function policyContext(context, request, options, extra) {
|
|
|
4046
4304
|
async function registeredFiles(root, limit) {
|
|
4047
4305
|
const paths = [];
|
|
4048
4306
|
const walk = async (directory) => {
|
|
4049
|
-
for (const entry of await (0,
|
|
4307
|
+
for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
|
|
4050
4308
|
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
4051
|
-
const target = (0,
|
|
4309
|
+
const target = (0, import_path9.resolve)(directory, entry.name);
|
|
4052
4310
|
if (entry.isDirectory()) await walk(target);
|
|
4053
4311
|
else if (entry.isFile()) {
|
|
4054
|
-
const path = (0,
|
|
4312
|
+
const path = (0, import_path9.relative)(root, target).split("\\").join("/");
|
|
4055
4313
|
try {
|
|
4056
4314
|
validateRelativePath(path);
|
|
4057
4315
|
} catch {
|
|
@@ -4062,7 +4320,7 @@ async function registeredFiles(root, limit) {
|
|
|
4062
4320
|
}
|
|
4063
4321
|
}
|
|
4064
4322
|
};
|
|
4065
|
-
await walk((0,
|
|
4323
|
+
await walk((0, import_path9.resolve)(root));
|
|
4066
4324
|
return paths.sort();
|
|
4067
4325
|
}
|
|
4068
4326
|
function validateOptions(options) {
|
|
@@ -4070,6 +4328,9 @@ function validateOptions(options) {
|
|
|
4070
4328
|
throw new TypeError("Code tool broker requires a reader and unique registered recipes");
|
|
4071
4329
|
}
|
|
4072
4330
|
for (const recipe2 of options.recipes) assertCodeBuildRecipe(recipe2);
|
|
4331
|
+
if (options.readOnlyPrefixes?.some((prefix) => !/^[A-Za-z0-9_.-]+$/.test(prefix) || prefix === "." || prefix === "..")) {
|
|
4332
|
+
throw new TypeError("Code tool broker read-only prefix is invalid");
|
|
4333
|
+
}
|
|
4073
4334
|
}
|
|
4074
4335
|
function exactKeys(input, allowed) {
|
|
4075
4336
|
if (Object.keys(input).some((key) => !allowed.includes(key))) throw new TypeError("tool input contains an unsupported field");
|
|
@@ -4097,14 +4358,31 @@ function codeCommandMetadata(payload, resume) {
|
|
|
4097
4358
|
}
|
|
4098
4359
|
const planning = trusted?.planningInputDigest;
|
|
4099
4360
|
const attestation = trusted?.attestationDigest;
|
|
4361
|
+
const repository = trusted?.repository;
|
|
4362
|
+
const baseCommitSha = trusted?.commitSha;
|
|
4363
|
+
const sourceTreeDigest = trusted?.treeDigest;
|
|
4364
|
+
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)) {
|
|
4365
|
+
throw new TypeError(`invalid Code ${resume ? "resume" : "start"} trusted base`);
|
|
4366
|
+
}
|
|
4100
4367
|
return {
|
|
4101
4368
|
role,
|
|
4102
4369
|
title,
|
|
4103
4370
|
prompt,
|
|
4104
4371
|
planningInputDigest: typeof planning === "string" && /^sha256:[0-9a-f]{64}$/.test(planning) ? planning : null,
|
|
4105
|
-
attestationDigest: typeof attestation === "string" ? attestation : "resume"
|
|
4372
|
+
attestationDigest: typeof attestation === "string" ? attestation : "resume",
|
|
4373
|
+
repository,
|
|
4374
|
+
baseCommitSha,
|
|
4375
|
+
sourceTreeDigest
|
|
4106
4376
|
};
|
|
4107
4377
|
}
|
|
4378
|
+
function codeLocalSource(payload) {
|
|
4379
|
+
const source = record22(payload.source);
|
|
4380
|
+
if (!source) return null;
|
|
4381
|
+
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) {
|
|
4382
|
+
throw new TypeError("invalid local checkout source descriptor");
|
|
4383
|
+
}
|
|
4384
|
+
return source;
|
|
4385
|
+
}
|
|
4108
4386
|
function codeCheckpointPayload(payload) {
|
|
4109
4387
|
const value = payload.checkpoint;
|
|
4110
4388
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("resume checkpoint is missing");
|
|
@@ -4133,11 +4411,63 @@ function fakeCodeLease(command, metadata2) {
|
|
|
4133
4411
|
};
|
|
4134
4412
|
}
|
|
4135
4413
|
var record22 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4414
|
+
var SOURCE_LIMITS = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
4415
|
+
async function prepareRuntimeLocalSource(input) {
|
|
4416
|
+
const { command, descriptor: descriptor2, available, repository, baseCommitSha, resume } = input;
|
|
4417
|
+
if (!available || JSON.stringify(available.descriptor) !== JSON.stringify(descriptor2) || descriptor2.repository.toLowerCase() !== repository.toLowerCase() || descriptor2.headCommitSha !== baseCommitSha) {
|
|
4418
|
+
throw new TypeError("the session's local checkout snapshot is not available on this terminal");
|
|
4419
|
+
}
|
|
4420
|
+
const workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
|
|
4421
|
+
trustedBaseDir: available.trustedBaseDir,
|
|
4422
|
+
trustedBaseCommitSha: baseCommitSha,
|
|
4423
|
+
checkpoint: codeCheckpointPayload(command.payload)
|
|
4424
|
+
})).workspace : await stageWorkspacePair(available.trustedBaseDir, available.sourceDir, SOURCE_LIMITS);
|
|
4425
|
+
const trustedBaseDigest = await digestStagedWorkspace(workspace.baselineDir, SOURCE_LIMITS);
|
|
4426
|
+
if (trustedBaseDigest !== descriptor2.trustedBaseDigest) {
|
|
4427
|
+
await workspace.cleanup();
|
|
4428
|
+
throw new TypeError("trusted Git base digest changed after connection");
|
|
4429
|
+
}
|
|
4430
|
+
if (!resume && await digestStagedWorkspace(workspace.workspaceDir, SOURCE_LIMITS) !== descriptor2.snapshotDigest) {
|
|
4431
|
+
await workspace.cleanup();
|
|
4432
|
+
throw new TypeError("local checkout snapshot digest changed after connection");
|
|
4433
|
+
}
|
|
4434
|
+
return { workspace, sourceDigest: descriptor2.snapshotDigest, trustedBaseDigest };
|
|
4435
|
+
}
|
|
4436
|
+
function createCodeRuntimeToolBroker(input, lease, role) {
|
|
4437
|
+
const broker = createCodeToolBroker({
|
|
4438
|
+
recipes: input.recipes,
|
|
4439
|
+
recipeExecutor: createContainerRecipeExecutor(input.engine),
|
|
4440
|
+
recipeAuthorization: input.recipeAuthorization ?? "registered_recipe",
|
|
4441
|
+
readerId: `code-session:${lease.task.taskId}`,
|
|
4442
|
+
readOnlyPrefixes: [".odla-references"]
|
|
4443
|
+
});
|
|
4444
|
+
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" }) };
|
|
4445
|
+
}
|
|
4446
|
+
async function appendCodeRuntimeEvent(control, command, event, refs) {
|
|
4447
|
+
const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
|
|
4448
|
+
refs.push(eventId);
|
|
4449
|
+
const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
|
|
4450
|
+
await control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
4451
|
+
}
|
|
4452
|
+
var digestRuntimeValue = (value) => `sha256:${(0, import_crypto4.createHash)("sha256").update(value).digest("hex")}`;
|
|
4453
|
+
var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
|
|
4454
|
+
var runtimeRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4455
|
+
var safeRuntimeJson = (value) => {
|
|
4456
|
+
try {
|
|
4457
|
+
return JSON.stringify(value).slice(0, 1e4);
|
|
4458
|
+
} catch {
|
|
4459
|
+
return "[event]";
|
|
4460
|
+
}
|
|
4461
|
+
};
|
|
4462
|
+
function runtimeResultText(value) {
|
|
4463
|
+
const record32 = runtimeRecord(value);
|
|
4464
|
+
return record32 && typeof record32.text === "string" ? record32.text.slice(0, 2e4) : null;
|
|
4465
|
+
}
|
|
4136
4466
|
var CodePiRuntimeEngine = class {
|
|
4137
4467
|
constructor(options) {
|
|
4138
4468
|
this.options = options;
|
|
4139
4469
|
this.#run = options.runAttempt ?? runContainerAttempt;
|
|
4140
|
-
this.#buildPolicyDigest =
|
|
4470
|
+
this.#buildPolicyDigest = digestRuntimeValue(JSON.stringify(options.recipes));
|
|
4141
4471
|
this.#checkpoints = new CodeRuntimeCheckpointManager({
|
|
4142
4472
|
control: options.control,
|
|
4143
4473
|
recipes: options.recipes,
|
|
@@ -4172,19 +4502,43 @@ var CodePiRuntimeEngine = class {
|
|
|
4172
4502
|
}
|
|
4173
4503
|
async #start(command, resume) {
|
|
4174
4504
|
if (this.#active.has(command.sessionId)) throw new TypeError("Code session is already active on this runtime");
|
|
4175
|
-
const
|
|
4176
|
-
const
|
|
4505
|
+
const metadata2 = codeCommandMetadata(command.payload, resume);
|
|
4506
|
+
const requestedLocal = codeLocalSource(command.payload);
|
|
4177
4507
|
let workspace;
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4508
|
+
let sourceDigest;
|
|
4509
|
+
let localTrustedBaseDigest;
|
|
4510
|
+
if (requestedLocal) {
|
|
4511
|
+
const prepared = await prepareRuntimeLocalSource({
|
|
4512
|
+
command,
|
|
4513
|
+
descriptor: requestedLocal,
|
|
4514
|
+
available: this.options.localSource,
|
|
4515
|
+
repository: metadata2.repository,
|
|
4516
|
+
baseCommitSha: metadata2.baseCommitSha,
|
|
4517
|
+
resume
|
|
4518
|
+
});
|
|
4519
|
+
({ workspace, sourceDigest, trustedBaseDigest: localTrustedBaseDigest } = prepared);
|
|
4520
|
+
if (command.payload.sourceSet) {
|
|
4521
|
+
const selected = await this.options.control.source(command.sessionId);
|
|
4522
|
+
if (selected.repository !== metadata2.repository || selected.commitSha !== metadata2.baseCommitSha || selected.treeDigest !== metadata2.sourceTreeDigest) {
|
|
4523
|
+
await workspace.cleanup();
|
|
4524
|
+
throw new TypeError("Code local source does not match the selected GitHub primary source");
|
|
4525
|
+
}
|
|
4526
|
+
await attachCodeRuntimeReferences(workspace, selected.references ?? []);
|
|
4527
|
+
}
|
|
4528
|
+
} else {
|
|
4529
|
+
const source = await this.options.control.source(command.sessionId);
|
|
4530
|
+
const materialized = await materializeCodeRuntimeSource(source);
|
|
4531
|
+
try {
|
|
4532
|
+
workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
|
|
4533
|
+
trustedBaseDir: materialized.sourceDir,
|
|
4534
|
+
trustedBaseCommitSha: source.commitSha,
|
|
4535
|
+
checkpoint: codeCheckpointPayload(command.payload)
|
|
4536
|
+
})).workspace : await stageWorkspace(materialized.sourceDir);
|
|
4537
|
+
} finally {
|
|
4538
|
+
await materialized.cleanup();
|
|
4539
|
+
}
|
|
4540
|
+
sourceDigest = source.treeDigest;
|
|
4186
4541
|
}
|
|
4187
|
-
const metadata2 = codeCommandMetadata(command.payload, resume);
|
|
4188
4542
|
const abort = new AbortController();
|
|
4189
4543
|
const conversationRefs = [];
|
|
4190
4544
|
const active = {
|
|
@@ -4194,21 +4548,30 @@ var CodePiRuntimeEngine = class {
|
|
|
4194
4548
|
acknowledged: false,
|
|
4195
4549
|
role: metadata2.role,
|
|
4196
4550
|
title: metadata2.title,
|
|
4197
|
-
baseCommitSha:
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4551
|
+
baseCommitSha: metadata2.baseCommitSha,
|
|
4552
|
+
repository: metadata2.repository,
|
|
4553
|
+
sourceTreeDigest: metadata2.sourceTreeDigest,
|
|
4554
|
+
trustedBaseDigest: requestedLocal ? localTrustedBaseDigest : await digestStagedWorkspace(workspace.baselineDir, {
|
|
4555
|
+
maxFiles: 2e4,
|
|
4556
|
+
maxBytes: 512 * 1024 * 1024
|
|
4201
4557
|
}),
|
|
4202
|
-
planningInputDigest: metadata2.planningInputDigest ??
|
|
4203
|
-
JSON.stringify({ attestation: metadata2.attestationDigest, prompt: metadata2.prompt, tree:
|
|
4558
|
+
planningInputDigest: metadata2.planningInputDigest ?? digestRuntimeValue(
|
|
4559
|
+
JSON.stringify({ attestation: metadata2.attestationDigest, prompt: metadata2.prompt, tree: sourceDigest })
|
|
4204
4560
|
),
|
|
4205
4561
|
done: Promise.resolve(null)
|
|
4206
4562
|
};
|
|
4207
4563
|
this.#active.set(command.sessionId, active);
|
|
4564
|
+
if (requestedLocal) {
|
|
4565
|
+
await this.#event(command, {
|
|
4566
|
+
type: "message",
|
|
4567
|
+
actor: "system",
|
|
4568
|
+
body: `Source snapshot: local checkout ${requestedLocal.snapshotDigest} \xB7 ${requestedLocal.modified ? "modified" : "clean"} \xB7 Git ${requestedLocal.headCommitSha}`
|
|
4569
|
+
}, conversationRefs);
|
|
4570
|
+
}
|
|
4208
4571
|
active.done = this.#runAttempt(command, metadata2, active).catch(async (cause) => {
|
|
4209
|
-
await this.#event(command, { type: "message", actor: "system", body: `Pi failed: ${
|
|
4572
|
+
await this.#event(command, { type: "message", actor: "system", body: `Pi failed: ${runtimeErrorMessage(cause)}` }, conversationRefs).catch(() => void 0);
|
|
4210
4573
|
await this.#event(command, { type: "status", status: "failed" }, conversationRefs).catch(() => void 0);
|
|
4211
|
-
await this.#failure(command, active,
|
|
4574
|
+
await this.#failure(command, active, runtimeErrorMessage(cause));
|
|
4212
4575
|
return null;
|
|
4213
4576
|
});
|
|
4214
4577
|
return { status: "running", message: resume ? "Pi resumed from a portable checkpoint" : "Pi started" };
|
|
@@ -4228,22 +4591,29 @@ var CodePiRuntimeEngine = class {
|
|
|
4228
4591
|
title: active.title,
|
|
4229
4592
|
prompt,
|
|
4230
4593
|
planningInputDigest: active.planningInputDigest,
|
|
4231
|
-
attestationDigest: "follow-up"
|
|
4594
|
+
attestationDigest: "follow-up",
|
|
4595
|
+
repository: active.repository,
|
|
4596
|
+
baseCommitSha: active.baseCommitSha,
|
|
4597
|
+
sourceTreeDigest: active.sourceTreeDigest
|
|
4232
4598
|
}, active).catch(async (cause) => {
|
|
4233
4599
|
await this.#event(
|
|
4234
4600
|
command,
|
|
4235
|
-
{ type: "message", actor: "system", body: `Pi failed: ${
|
|
4601
|
+
{ type: "message", actor: "system", body: `Pi failed: ${runtimeErrorMessage(cause)}` },
|
|
4236
4602
|
active.conversationRefs
|
|
4237
4603
|
).catch(() => void 0);
|
|
4238
4604
|
await this.#event(command, { type: "status", status: "failed" }, active.conversationRefs).catch(() => void 0);
|
|
4239
|
-
await this.#failure(command, active,
|
|
4605
|
+
await this.#failure(command, active, runtimeErrorMessage(cause));
|
|
4240
4606
|
return null;
|
|
4241
4607
|
});
|
|
4242
4608
|
return { status: "running", message: "Pi accepted the owner prompt" };
|
|
4243
4609
|
}
|
|
4244
4610
|
async #runAttempt(command, metadata2, active) {
|
|
4245
4611
|
const lease = fakeCodeLease(command, metadata2);
|
|
4246
|
-
const broker =
|
|
4612
|
+
const broker = createCodeRuntimeToolBroker({
|
|
4613
|
+
recipes: this.options.recipes,
|
|
4614
|
+
engine: this.options.engine,
|
|
4615
|
+
recipeAuthorization: this.options.recipeAuthorization
|
|
4616
|
+
}, lease, metadata2.role);
|
|
4247
4617
|
const startedAt = Date.now();
|
|
4248
4618
|
let completionSeen = false;
|
|
4249
4619
|
const result = await this.#run({
|
|
@@ -4303,7 +4673,7 @@ var CodePiRuntimeEngine = class {
|
|
|
4303
4673
|
return { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "tool.response", ...response2 };
|
|
4304
4674
|
}
|
|
4305
4675
|
if (output.type === "event") {
|
|
4306
|
-
const payload =
|
|
4676
|
+
const payload = runtimeRecord(output.payload);
|
|
4307
4677
|
if (output.kind === "pi.started") {
|
|
4308
4678
|
await this.#event(command, { type: "status", status: "running" }, active.conversationRefs);
|
|
4309
4679
|
} else if (output.kind === "pi.thinking" && payload?.available === true && Number.isSafeInteger(payload.durationMs) && Number(payload.durationMs) >= 0) {
|
|
@@ -4316,12 +4686,12 @@ var CodePiRuntimeEngine = class {
|
|
|
4316
4686
|
await this.#event(command, {
|
|
4317
4687
|
type: "message",
|
|
4318
4688
|
actor: "system",
|
|
4319
|
-
body: `${output.kind}${output.payload === void 0 ? "" : ` ${
|
|
4689
|
+
body: `${output.kind}${output.payload === void 0 ? "" : ` ${safeRuntimeJson(output.payload)}`}`
|
|
4320
4690
|
}, active.conversationRefs);
|
|
4321
4691
|
}
|
|
4322
4692
|
} else if (output.type === "attempt.complete") {
|
|
4323
4693
|
completionSeen = true;
|
|
4324
|
-
const body =
|
|
4694
|
+
const body = runtimeResultText(output.result) ?? `Pi ${output.status}.`;
|
|
4325
4695
|
await this.#event(command, {
|
|
4326
4696
|
type: "message",
|
|
4327
4697
|
actor: output.status === "completed" ? "agent" : "system",
|
|
@@ -4348,15 +4718,6 @@ var CodePiRuntimeEngine = class {
|
|
|
4348
4718
|
}
|
|
4349
4719
|
return result;
|
|
4350
4720
|
}
|
|
4351
|
-
#toolBroker(lease, role) {
|
|
4352
|
-
const broker = createCodeToolBroker({
|
|
4353
|
-
recipes: this.options.recipes,
|
|
4354
|
-
recipeExecutor: createContainerRecipeExecutor(this.options.engine),
|
|
4355
|
-
recipeAuthorization: this.options.recipeAuthorization ?? "registered_recipe",
|
|
4356
|
-
readerId: `code-session:${lease.task.taskId}`
|
|
4357
|
-
});
|
|
4358
|
-
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" }) };
|
|
4359
|
-
}
|
|
4360
4721
|
async #checkpoint(command) {
|
|
4361
4722
|
const active = this.#active.get(command.sessionId);
|
|
4362
4723
|
if (!active) throw new TypeError("Code session workspace is not active on this runtime");
|
|
@@ -4371,26 +4732,9 @@ var CodePiRuntimeEngine = class {
|
|
|
4371
4732
|
}
|
|
4372
4733
|
}
|
|
4373
4734
|
async #event(command, event, refs) {
|
|
4374
|
-
|
|
4375
|
-
refs.push(eventId);
|
|
4376
|
-
const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
|
|
4377
|
-
await this.options.control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
4378
|
-
}
|
|
4379
|
-
};
|
|
4380
|
-
var object2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4381
|
-
var digest = (value) => `sha256:${(0, import_crypto4.createHash)("sha256").update(value).digest("hex")}`;
|
|
4382
|
-
var message2 = (value) => value instanceof Error ? value.message : String(value);
|
|
4383
|
-
var json = (value) => {
|
|
4384
|
-
try {
|
|
4385
|
-
return JSON.stringify(value).slice(0, 1e4);
|
|
4386
|
-
} catch {
|
|
4387
|
-
return "[event]";
|
|
4735
|
+
await appendCodeRuntimeEvent(this.options.control, command, event, refs);
|
|
4388
4736
|
}
|
|
4389
4737
|
};
|
|
4390
|
-
function resultText(value) {
|
|
4391
|
-
const record32 = object2(value);
|
|
4392
|
-
return record32 && typeof record32.text === "string" ? record32.text.slice(0, 2e4) : null;
|
|
4393
|
-
}
|
|
4394
4738
|
|
|
4395
4739
|
// src/help.ts
|
|
4396
4740
|
var import_node_fs6 = require("fs");
|
|
@@ -4532,8 +4876,8 @@ async function requestHostedSecurityJson(options, path, init, action) {
|
|
|
4532
4876
|
const body = await response2.json().catch(() => ({}));
|
|
4533
4877
|
if (!response2.ok) {
|
|
4534
4878
|
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
4535
|
-
const
|
|
4536
|
-
throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${
|
|
4879
|
+
const message2 = optionalHostedText(body.error?.message, "error message", 300);
|
|
4880
|
+
throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
|
|
4537
4881
|
}
|
|
4538
4882
|
return body;
|
|
4539
4883
|
}
|
|
@@ -4722,24 +5066,111 @@ async function defaultReadOrigin(cwd) {
|
|
|
4722
5066
|
return result.stdout;
|
|
4723
5067
|
}
|
|
4724
5068
|
|
|
4725
|
-
// src/code-
|
|
4726
|
-
var
|
|
5069
|
+
// src/code-local-source.ts
|
|
5070
|
+
var import_node_child_process3 = require("child_process");
|
|
5071
|
+
var import_node_crypto = require("crypto");
|
|
5072
|
+
var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
5073
|
+
async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
|
|
5074
|
+
const headCommitSha = await readHead(cwd);
|
|
5075
|
+
const staged = await stageWorkspace(cwd, { ...SOURCE_LIMITS2, gitTrackedAndUnignored: true });
|
|
5076
|
+
let trusted;
|
|
5077
|
+
try {
|
|
5078
|
+
trusted = await materializeGitTree(cwd, headCommitSha, SOURCE_LIMITS2);
|
|
5079
|
+
const comparison = await stageWorkspacePair(trusted.sourceDir, staged.baselineDir, SOURCE_LIMITS2);
|
|
5080
|
+
let developerPatch;
|
|
5081
|
+
try {
|
|
5082
|
+
developerPatch = await comparison.patch(4 * 1024 * 1024);
|
|
5083
|
+
} finally {
|
|
5084
|
+
await comparison.cleanup();
|
|
5085
|
+
}
|
|
5086
|
+
const descriptor2 = {
|
|
5087
|
+
kind: "local_checkout",
|
|
5088
|
+
repository,
|
|
5089
|
+
headCommitSha,
|
|
5090
|
+
trustedBaseDigest: await digestStagedWorkspace(trusted.sourceDir, SOURCE_LIMITS2),
|
|
5091
|
+
developerPatchDigest: digestText(developerPatch),
|
|
5092
|
+
snapshotDigest: await digestStagedWorkspace(staged.baselineDir, SOURCE_LIMITS2),
|
|
5093
|
+
modified: developerPatch.length > 0,
|
|
5094
|
+
fileCount: staged.fileCount,
|
|
5095
|
+
byteCount: staged.byteCount,
|
|
5096
|
+
capturedAt: Date.now()
|
|
5097
|
+
};
|
|
5098
|
+
return {
|
|
5099
|
+
descriptor: descriptor2,
|
|
5100
|
+
trustedBaseDir: trusted.sourceDir,
|
|
5101
|
+
sourceDir: staged.baselineDir,
|
|
5102
|
+
cleanup: async () => {
|
|
5103
|
+
await Promise.all([staged.cleanup(), trusted.cleanup()]);
|
|
5104
|
+
}
|
|
5105
|
+
};
|
|
5106
|
+
} catch (error) {
|
|
5107
|
+
await Promise.all([staged.cleanup(), trusted?.cleanup()]);
|
|
5108
|
+
throw error;
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
async function readGitHead(cwd) {
|
|
5112
|
+
const value = await new Promise((accept, reject) => {
|
|
5113
|
+
(0, import_node_child_process3.execFile)("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", maxBuffer: 16384 }, (error, stdout) => {
|
|
5114
|
+
if (error) reject(new Error("code connect requires a Git checkout with an initial commit"));
|
|
5115
|
+
else accept(stdout.trim());
|
|
5116
|
+
});
|
|
5117
|
+
});
|
|
5118
|
+
if (!/^[0-9a-f]{40}$/.test(value)) throw new Error("code connect could not resolve the checkout HEAD commit");
|
|
5119
|
+
return value;
|
|
5120
|
+
}
|
|
5121
|
+
function digestText(value) {
|
|
5122
|
+
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(value).digest("hex")}`;
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
// src/code-images.ts
|
|
5126
|
+
var import_node_child_process4 = require("child_process");
|
|
5127
|
+
async function prepareCodeImages(engine, images) {
|
|
5128
|
+
for (const image of images) {
|
|
5129
|
+
await new Promise((accept, reject) => {
|
|
5130
|
+
const args = engine === "container" ? ["image", "pull", image] : ["pull", image];
|
|
5131
|
+
(0, import_node_child_process4.execFile)(engine, args, { encoding: "utf8", maxBuffer: 2 * 1024 * 1024, timeout: 10 * 6e4 }, (error) => {
|
|
5132
|
+
if (error) reject(new Error(`could not prepare pinned Code image ${image}`));
|
|
5133
|
+
else accept();
|
|
5134
|
+
});
|
|
5135
|
+
});
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
// src/code-runtime-config.ts
|
|
5140
|
+
var CODE_PI_IMAGE = "ghcr.io/cory/odla-pi-agent@sha256:e7858b4f0291543171b2659b47c5d00f97acc4767cafd9b74ae5228899e0cde4";
|
|
4727
5141
|
var CODE_BUILD_RECIPES = Object.freeze([{
|
|
4728
|
-
id: "
|
|
5142
|
+
id: "odla-code-contracts",
|
|
4729
5143
|
image: "node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd",
|
|
4730
|
-
command: [
|
|
5144
|
+
command: [
|
|
5145
|
+
"node",
|
|
5146
|
+
"--test",
|
|
5147
|
+
"scripts/lib/directories.test.mjs",
|
|
5148
|
+
"scripts/lib/internal-deps.test.mjs",
|
|
5149
|
+
"scripts/lib/npm-audit.test.mjs",
|
|
5150
|
+
"scripts/lib/npm-release-contract.test.mjs",
|
|
5151
|
+
"scripts/lib/release-surfaces.test.mjs"
|
|
5152
|
+
],
|
|
4731
5153
|
timeoutMs: 12e4,
|
|
4732
5154
|
maxOutputBytes: 1024 * 1024,
|
|
4733
5155
|
cpus: 1,
|
|
4734
5156
|
memory: "512m",
|
|
4735
5157
|
pids: 128
|
|
4736
5158
|
}]);
|
|
5159
|
+
|
|
5160
|
+
// src/code-connect.ts
|
|
4737
5161
|
async function codeConnect(options) {
|
|
4738
5162
|
const cwd = options.cwd ?? process.cwd();
|
|
4739
5163
|
const configPath = (0, import_node_path4.resolve)(cwd, options.configPath);
|
|
4740
5164
|
const cfg = (0, import_node_fs7.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
4741
|
-
const
|
|
4742
|
-
|
|
5165
|
+
const requestedAppId = options.appId?.trim();
|
|
5166
|
+
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
5167
|
+
throw new Error("--app-id must be a valid odla app id");
|
|
5168
|
+
}
|
|
5169
|
+
if (requestedAppId && cfg && requestedAppId !== cfg.app.id) {
|
|
5170
|
+
throw new Error(`--app-id ${requestedAppId} does not match ${options.configPath} (${cfg.app.id})`);
|
|
5171
|
+
}
|
|
5172
|
+
const appId = requestedAppId ?? cfg?.app.id;
|
|
5173
|
+
const platform = (options.platform ?? cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai").replace(/\/+$/, "");
|
|
4743
5174
|
const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : "dev");
|
|
4744
5175
|
if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
|
|
4745
5176
|
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`);
|
|
@@ -4758,53 +5189,77 @@ async function codeConnect(options) {
|
|
|
4758
5189
|
const out = options.stdout ?? console;
|
|
4759
5190
|
const doFetch = options.fetch ?? fetch;
|
|
4760
5191
|
const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
|
|
5192
|
+
await (options.prepareImages ?? prepareCodeImages)(engine, [
|
|
5193
|
+
CODE_PI_IMAGE,
|
|
5194
|
+
...new Set(CODE_BUILD_RECIPES.map((recipe2) => recipe2.image))
|
|
5195
|
+
]);
|
|
4761
5196
|
const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
|
|
4762
5197
|
const hostName = (options.name ?? (0, import_node_os.hostname)()).trim();
|
|
4763
5198
|
if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
|
|
4764
|
-
const
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
5199
|
+
const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
|
|
5200
|
+
const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
|
|
5201
|
+
cwd,
|
|
5202
|
+
repository,
|
|
5203
|
+
options.readGitHead
|
|
5204
|
+
);
|
|
5205
|
+
try {
|
|
5206
|
+
const descriptor2 = localSource.descriptor;
|
|
5207
|
+
const approval = await (options.getToken ?? getScopedPlatformToken)({
|
|
5208
|
+
platform,
|
|
5209
|
+
scope: "app:code:host:connect",
|
|
5210
|
+
email: options.email,
|
|
5211
|
+
open: options.open,
|
|
5212
|
+
fetch: doFetch,
|
|
5213
|
+
stdout: out,
|
|
5214
|
+
openApprovalUrl: options.openApprovalUrl,
|
|
5215
|
+
cache: false,
|
|
5216
|
+
label: `Terminal connection for ${appId ?? repository}/${appEnv}`
|
|
5217
|
+
});
|
|
5218
|
+
const target = appId ? { appId } : { repository };
|
|
5219
|
+
const response2 = await doFetch(`${platform}/registry/code/hosts/connect`, {
|
|
5220
|
+
method: "POST",
|
|
5221
|
+
headers: { authorization: `Bearer ${approval}`, "content-type": "application/json" },
|
|
5222
|
+
body: JSON.stringify({ ...target, env: appEnv, name: hostName, platform: hostPlatform, slots }),
|
|
5223
|
+
redirect: "error",
|
|
5224
|
+
signal: options.signal
|
|
5225
|
+
});
|
|
5226
|
+
const raw = await response2.json().catch(() => null);
|
|
5227
|
+
if (!response2.ok) throw new Error(apiFailure("connect Code host", response2.status, raw));
|
|
5228
|
+
const connection = parseConnection(raw, appId, appEnv);
|
|
5229
|
+
const capabilities = {
|
|
5230
|
+
protocolVersion: CODE_RUNTIME_PROTOCOL_VERSION,
|
|
5231
|
+
platform: hostPlatform,
|
|
5232
|
+
arch: process.arch,
|
|
5233
|
+
engines: [engine],
|
|
5234
|
+
cpuCount: (0, import_node_os.cpus)().length,
|
|
5235
|
+
memoryBytes: (0, import_node_os.totalmem)(),
|
|
5236
|
+
source: descriptor2,
|
|
5237
|
+
images: {
|
|
5238
|
+
ready: true,
|
|
5239
|
+
pi: CODE_PI_IMAGE,
|
|
5240
|
+
recipes: CODE_BUILD_RECIPES.map((recipe2) => ({ id: recipe2.id, image: recipe2.image }))
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5243
|
+
out.log(`${connection.resumed ? "reconnected" : "enrolled"}: ${connection.host.name} (${connection.host.hostId})`);
|
|
5244
|
+
out.log(`binding: ${connection.binding.appId}/${connection.binding.env} \xB7 ${connection.offer.slots} slot(s) \xB7 ${engine}`);
|
|
5245
|
+
out.log(`source: local checkout \xB7 ${descriptor2.headCommitSha.slice(0, 10)} \xB7 ${descriptor2.fileCount} files \xB7 staged copy`);
|
|
5246
|
+
out.log("credentials: stored in odla-ai/db; host plaintext is memory-only");
|
|
5247
|
+
out.log(`Studio: ${platform}/studio/apps/${encodeURIComponent(connection.binding.appId)}/${connection.binding.env}/code/sessions`);
|
|
5248
|
+
await (options.runRuntime ?? runCodeRuntime)({
|
|
5249
|
+
endpoint: platform,
|
|
5250
|
+
token: connection.token,
|
|
5251
|
+
engine,
|
|
5252
|
+
capabilities,
|
|
5253
|
+
localSource,
|
|
5254
|
+
heartbeatMs,
|
|
5255
|
+
once: options.once === true,
|
|
5256
|
+
signal: options.signal,
|
|
5257
|
+
stdout: out,
|
|
5258
|
+
fetch: doFetch
|
|
5259
|
+
});
|
|
5260
|
+
} finally {
|
|
5261
|
+
await localSource.cleanup();
|
|
5262
|
+
}
|
|
4808
5263
|
}
|
|
4809
5264
|
async function runCodeRuntime(input) {
|
|
4810
5265
|
const controller = new AbortController();
|
|
@@ -4827,7 +5282,8 @@ async function runCodeRuntime(input) {
|
|
|
4827
5282
|
engine: input.engine,
|
|
4828
5283
|
image: CODE_PI_IMAGE,
|
|
4829
5284
|
recipes: CODE_BUILD_RECIPES,
|
|
4830
|
-
recipeAuthorization: "registered_recipe"
|
|
5285
|
+
recipeAuthorization: "registered_recipe",
|
|
5286
|
+
localSource: input.localSource
|
|
4831
5287
|
});
|
|
4832
5288
|
const reconciler = new CodeRuntimeReconciler(control, commandEngine);
|
|
4833
5289
|
try {
|
|
@@ -4868,8 +5324,8 @@ function parseConnection(value, appId, appEnv) {
|
|
|
4868
5324
|
return root;
|
|
4869
5325
|
}
|
|
4870
5326
|
function apiFailure(action, status, value) {
|
|
4871
|
-
const
|
|
4872
|
-
return `${action} failed (${status})${typeof
|
|
5327
|
+
const message2 = record4(record4(value)?.error)?.message;
|
|
5328
|
+
return `${action} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
|
|
4873
5329
|
}
|
|
4874
5330
|
function record4(value) {
|
|
4875
5331
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -4884,6 +5340,8 @@ async function codeCommand(parsed, dependencies) {
|
|
|
4884
5340
|
assertArgs(parsed, [
|
|
4885
5341
|
"config",
|
|
4886
5342
|
"env",
|
|
5343
|
+
"platform",
|
|
5344
|
+
"app-id",
|
|
4887
5345
|
"email",
|
|
4888
5346
|
"open",
|
|
4889
5347
|
"name",
|
|
@@ -4899,6 +5357,8 @@ async function codeCommand(parsed, dependencies) {
|
|
|
4899
5357
|
await (dependencies.codeConnect ?? codeConnect)({
|
|
4900
5358
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
4901
5359
|
env: stringOpt(parsed.options.env),
|
|
5360
|
+
platform: stringOpt(parsed.options.platform),
|
|
5361
|
+
appId: stringOpt(parsed.options["app-id"]),
|
|
4902
5362
|
email: stringOpt(parsed.options.email),
|
|
4903
5363
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
4904
5364
|
name: stringOpt(parsed.options.name),
|
|
@@ -4914,16 +5374,16 @@ async function codeCommand(parsed, dependencies) {
|
|
|
4914
5374
|
}
|
|
4915
5375
|
|
|
4916
5376
|
// src/doctor-checks.ts
|
|
4917
|
-
var
|
|
5377
|
+
var import_node_child_process6 = require("child_process");
|
|
4918
5378
|
var import_node_fs9 = require("fs");
|
|
4919
5379
|
var import_node_path6 = require("path");
|
|
4920
5380
|
|
|
4921
5381
|
// src/wrangler.ts
|
|
4922
|
-
var
|
|
5382
|
+
var import_node_child_process5 = require("child_process");
|
|
4923
5383
|
var import_node_fs8 = require("fs");
|
|
4924
5384
|
var import_node_path5 = require("path");
|
|
4925
5385
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
4926
|
-
const child = (0,
|
|
5386
|
+
const child = (0, import_node_child_process5.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
4927
5387
|
let stdout = "";
|
|
4928
5388
|
let stderr = "";
|
|
4929
5389
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
@@ -5014,7 +5474,7 @@ function lintRules(rules, entities, publicRead) {
|
|
|
5014
5474
|
}
|
|
5015
5475
|
return warnings;
|
|
5016
5476
|
}
|
|
5017
|
-
var defaultExec = (cmd, args, cwd) => (0,
|
|
5477
|
+
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process6.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
5018
5478
|
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
5019
5479
|
let output;
|
|
5020
5480
|
try {
|
|
@@ -5622,8 +6082,8 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
5622
6082
|
});
|
|
5623
6083
|
if (res.status === 404) return null;
|
|
5624
6084
|
if (!res.ok) return null;
|
|
5625
|
-
const
|
|
5626
|
-
return
|
|
6085
|
+
const json = await res.json();
|
|
6086
|
+
return json.app ?? null;
|
|
5627
6087
|
}
|
|
5628
6088
|
async function postJson2(doFetch, url, bearer, body) {
|
|
5629
6089
|
const res = await doFetch(url, {
|
|
@@ -5862,10 +6322,10 @@ async function provision(options) {
|
|
|
5862
6322
|
stdout: out
|
|
5863
6323
|
});
|
|
5864
6324
|
} catch (error) {
|
|
5865
|
-
const
|
|
6325
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
5866
6326
|
const consent = env === "prod" || env === "production" ? " --yes" : "";
|
|
5867
6327
|
throw new Error(
|
|
5868
|
-
`${
|
|
6328
|
+
`${message2}
|
|
5869
6329
|
${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
|
|
5870
6330
|
{ cause: error }
|
|
5871
6331
|
);
|
|
@@ -5981,7 +6441,7 @@ async function resolveVaultWrite(options) {
|
|
|
5981
6441
|
}
|
|
5982
6442
|
|
|
5983
6443
|
// src/security-command-context.ts
|
|
5984
|
-
var
|
|
6444
|
+
var import_promises10 = require("readline/promises");
|
|
5985
6445
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
5986
6446
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
5987
6447
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -6004,12 +6464,12 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
6004
6464
|
);
|
|
6005
6465
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
6006
6466
|
}
|
|
6007
|
-
async function interactiveConfirmation(
|
|
6008
|
-
if (dependencies.confirm) return dependencies.confirm(
|
|
6467
|
+
async function interactiveConfirmation(message2, dependencies) {
|
|
6468
|
+
if (dependencies.confirm) return dependencies.confirm(message2);
|
|
6009
6469
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
6010
|
-
const prompt = (0,
|
|
6470
|
+
const prompt = (0, import_promises10.createInterface)({ input: process.stdin, output: process.stdout });
|
|
6011
6471
|
try {
|
|
6012
|
-
const answer = await prompt.question(`${
|
|
6472
|
+
const answer = await prompt.question(`${message2} [y/N] `);
|
|
6013
6473
|
return /^y(?:es)?$/i.test(answer.trim());
|
|
6014
6474
|
} finally {
|
|
6015
6475
|
prompt.close();
|
|
@@ -6136,7 +6596,7 @@ var import_security2 = require("@odla-ai/security");
|
|
|
6136
6596
|
// src/security.ts
|
|
6137
6597
|
var import_node_path8 = require("path");
|
|
6138
6598
|
var import_security = require("@odla-ai/security");
|
|
6139
|
-
var
|
|
6599
|
+
var import_node3 = require("@odla-ai/security/node");
|
|
6140
6600
|
async function runHostedSecurity(options) {
|
|
6141
6601
|
if (options.sourceDisclosureAck !== "redacted") {
|
|
6142
6602
|
throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
|
|
@@ -6159,7 +6619,7 @@ async function runHostedSecurity(options) {
|
|
|
6159
6619
|
scope: selfAudit ? "platform:security:self" : null
|
|
6160
6620
|
};
|
|
6161
6621
|
const token = await injectedToken(options, tokenRequest);
|
|
6162
|
-
const snapshot = await (0,
|
|
6622
|
+
const snapshot = await (0, import_node3.snapshotDirectory)(target, {
|
|
6163
6623
|
exclude: !outputRelative.startsWith("../") && !(0, import_node_path8.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
6164
6624
|
});
|
|
6165
6625
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
@@ -6178,7 +6638,7 @@ async function runHostedSecurity(options) {
|
|
|
6178
6638
|
});
|
|
6179
6639
|
const harness = (0, import_security.createSecurityHarness)({
|
|
6180
6640
|
profile,
|
|
6181
|
-
store: new
|
|
6641
|
+
store: new import_node3.FileRunStore((0, import_node_path8.resolve)(output, "state")),
|
|
6182
6642
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
6183
6643
|
validationReasoner: hosted.validationReasoner,
|
|
6184
6644
|
policy: {
|
|
@@ -6188,7 +6648,7 @@ async function runHostedSecurity(options) {
|
|
|
6188
6648
|
}
|
|
6189
6649
|
});
|
|
6190
6650
|
const report2 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
6191
|
-
await (0,
|
|
6651
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report2);
|
|
6192
6652
|
const reportDigest = await (0, import_security.securityFingerprint)(report2);
|
|
6193
6653
|
await hosted.complete({
|
|
6194
6654
|
reportDigest,
|
|
@@ -6321,8 +6781,8 @@ async function startHostedSecurityJob(options) {
|
|
|
6321
6781
|
"start hosted security job"
|
|
6322
6782
|
);
|
|
6323
6783
|
} catch (error) {
|
|
6324
|
-
const
|
|
6325
|
-
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(
|
|
6784
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
6785
|
+
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(message2)) {
|
|
6326
6786
|
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 });
|
|
6327
6787
|
}
|
|
6328
6788
|
throw error;
|