@odla-ai/cli 0.13.0 → 0.14.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 +20 -0
- package/dist/bin.cjs +3450 -692
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-I7XDJZB6.js → chunk-3AC74CD2.js} +3479 -717
- package/dist/chunk-3AC74CD2.js.map +1 -0
- package/dist/index.cjs +3458 -692
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -20
- package/dist/index.d.ts +97 -20
- package/dist/index.js +9 -1
- package/package.json +4 -3
- package/skills/odla/SKILL.md +4 -0
- package/skills/odla/references/co-owners.md +54 -0
- package/dist/chunk-I7XDJZB6.js.map +0 -1
|
@@ -137,7 +137,7 @@ import process2 from "process";
|
|
|
137
137
|
async function openUrl(url, options = {}) {
|
|
138
138
|
const command = openerFor(options.platform ?? process2.platform);
|
|
139
139
|
const doSpawn = options.spawnImpl ?? spawn;
|
|
140
|
-
await new Promise((
|
|
140
|
+
await new Promise((resolve10, reject) => {
|
|
141
141
|
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
142
142
|
stdio: "ignore",
|
|
143
143
|
detached: true
|
|
@@ -145,7 +145,7 @@ async function openUrl(url, options = {}) {
|
|
|
145
145
|
child.once("error", reject);
|
|
146
146
|
child.once("spawn", () => {
|
|
147
147
|
child.unref();
|
|
148
|
-
|
|
148
|
+
resolve10();
|
|
149
149
|
});
|
|
150
150
|
});
|
|
151
151
|
}
|
|
@@ -393,7 +393,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
393
393
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
394
394
|
const audience = platformAudience(platform);
|
|
395
395
|
const tokenFile = options.tokenFile ?? `${process5.cwd()}/.odla/admin-token.local.json`;
|
|
396
|
-
const cache = readJsonFile(tokenFile);
|
|
396
|
+
const cache = options.cache === false ? null : readJsonFile(tokenFile);
|
|
397
397
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
398
398
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
399
399
|
out.log(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
@@ -403,7 +403,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
403
403
|
const { token, expiresAt } = await requestToken2({
|
|
404
404
|
endpoint: audience,
|
|
405
405
|
email,
|
|
406
|
-
label: `odla CLI admin AI (${scope})`,
|
|
406
|
+
label: options.label ?? `odla CLI admin AI (${scope})`,
|
|
407
407
|
scopes: [scope],
|
|
408
408
|
fetch: doFetch,
|
|
409
409
|
onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
|
|
@@ -414,11 +414,15 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
414
414
|
}
|
|
415
415
|
}
|
|
416
416
|
});
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
417
|
+
if (options.cache !== false) {
|
|
418
|
+
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
419
|
+
tokens[scope] = { token, expiresAt };
|
|
420
|
+
if (existsSync2(`${process5.cwd()}/.git`)) ensureGitignore(process5.cwd(), [tokenFile]);
|
|
421
|
+
writePrivateJson(tokenFile, { platform: audience, email, tokens });
|
|
422
|
+
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
423
|
+
} else {
|
|
424
|
+
out.log(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
|
|
425
|
+
}
|
|
422
426
|
return token;
|
|
423
427
|
}
|
|
424
428
|
|
|
@@ -466,11 +470,11 @@ function adminAiAuditQuery(filters) {
|
|
|
466
470
|
return `?limit=${filters.limit}`;
|
|
467
471
|
}
|
|
468
472
|
async function readAdminAiAudit(request) {
|
|
469
|
-
const
|
|
473
|
+
const response2 = await request.fetch(`${request.platform}/registry/platform/ai-audit${request.query}`, {
|
|
470
474
|
headers: request.headers
|
|
471
475
|
});
|
|
472
|
-
const body = await responseBody(
|
|
473
|
-
if (!
|
|
476
|
+
const body = await responseBody(response2);
|
|
477
|
+
if (!response2.ok) throw new Error(apiError(response2.status, body));
|
|
474
478
|
if (request.json) {
|
|
475
479
|
request.stdout.log(JSON.stringify(body, null, 2));
|
|
476
480
|
return;
|
|
@@ -480,18 +484,18 @@ async function readAdminAiAudit(request) {
|
|
|
480
484
|
for (const event of events) {
|
|
481
485
|
const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
|
|
482
486
|
const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
|
|
483
|
-
const
|
|
487
|
+
const route2 = before && after ? `${String(before.provider)}/${String(before.model)}@v${String(before.version)} -> ${String(after.provider)}/${String(after.model)}@v${String(after.version)}` : "value not retained";
|
|
484
488
|
request.stdout.log([
|
|
485
489
|
timestamp(event.createdAt),
|
|
486
490
|
String(event.changeKind ?? ""),
|
|
487
491
|
String(event.purpose ?? event.provider ?? ""),
|
|
488
|
-
|
|
492
|
+
route2,
|
|
489
493
|
`${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
|
|
490
494
|
].join(" "));
|
|
491
495
|
}
|
|
492
496
|
}
|
|
493
|
-
async function responseBody(
|
|
494
|
-
const text = await
|
|
497
|
+
async function responseBody(response2) {
|
|
498
|
+
const text = await response2.text();
|
|
495
499
|
if (!text) return {};
|
|
496
500
|
try {
|
|
497
501
|
return JSON.parse(text);
|
|
@@ -501,8 +505,8 @@ async function responseBody(response) {
|
|
|
501
505
|
}
|
|
502
506
|
function apiError(status, body) {
|
|
503
507
|
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
504
|
-
const
|
|
505
|
-
return `read System AI admin changes failed (${status}): ${
|
|
508
|
+
const message3 = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
509
|
+
return `read System AI admin changes failed (${status}): ${message3}`;
|
|
506
510
|
}
|
|
507
511
|
function timestamp(value) {
|
|
508
512
|
if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
|
|
@@ -594,8 +598,8 @@ async function responseBody2(res) {
|
|
|
594
598
|
}
|
|
595
599
|
function apiError2(action, status, body) {
|
|
596
600
|
const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
|
|
597
|
-
const
|
|
598
|
-
return `${action} failed (${status}): ${
|
|
601
|
+
const message3 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
602
|
+
return `${action} failed (${status}): ${message3}`;
|
|
599
603
|
}
|
|
600
604
|
function isRecord2(value) {
|
|
601
605
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -722,9 +726,9 @@ async function adminAi(options) {
|
|
|
722
726
|
validation: validationUpdate
|
|
723
727
|
})
|
|
724
728
|
});
|
|
725
|
-
const
|
|
726
|
-
if (!res2.ok) throw new Error(apiError3("update security review routes", res2.status,
|
|
727
|
-
out.log(options.json ? JSON.stringify(
|
|
729
|
+
const response3 = await responseBody3(res2);
|
|
730
|
+
if (!res2.ok) throw new Error(apiError3("update security review routes", res2.status, response3));
|
|
731
|
+
out.log(options.json ? JSON.stringify(response3, null, 2) : "updated security review discovery and independent validation routes atomically");
|
|
728
732
|
return;
|
|
729
733
|
}
|
|
730
734
|
const purpose = requireSystemAiPurpose(options.purpose);
|
|
@@ -742,9 +746,9 @@ async function adminAi(options) {
|
|
|
742
746
|
headers,
|
|
743
747
|
body: JSON.stringify(body)
|
|
744
748
|
});
|
|
745
|
-
const
|
|
746
|
-
if (!res.ok) throw new Error(apiError3(`update ${purpose}`, res.status,
|
|
747
|
-
const policy = isRecord3(
|
|
749
|
+
const response2 = await responseBody3(res);
|
|
750
|
+
if (!res.ok) throw new Error(apiError3(`update ${purpose}`, res.status, response2));
|
|
751
|
+
const policy = isRecord3(response2) && isRecord3(response2.policy) ? response2.policy : isRecord3(response2) ? response2 : {};
|
|
748
752
|
out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? "unchanged")}/${String(policy.model ?? "unchanged")}`);
|
|
749
753
|
}
|
|
750
754
|
function requireProvider(value) {
|
|
@@ -775,8 +779,8 @@ async function responseBody3(res) {
|
|
|
775
779
|
}
|
|
776
780
|
function apiError3(action, status, body) {
|
|
777
781
|
const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
|
|
778
|
-
const
|
|
779
|
-
return `${action} failed (${status}): ${
|
|
782
|
+
const message3 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
783
|
+
return `${action} failed (${status}): ${message3}`;
|
|
780
784
|
}
|
|
781
785
|
function isRecord3(value) {
|
|
782
786
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -1072,6 +1076,7 @@ var CAPABILITIES = {
|
|
|
1072
1076
|
"start an email-bound device request without accepting a password or user session token",
|
|
1073
1077
|
"register the app and enable configured services per environment",
|
|
1074
1078
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
1079
|
+
"add or remove app co-owners, each of whom self-provisions their own credentials for the shared db without any secret handoff",
|
|
1075
1080
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
1076
1081
|
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
1077
1082
|
"compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
|
|
@@ -1100,13 +1105,13 @@ var CAPABILITIES = {
|
|
|
1100
1105
|
"view telemetry and environment state",
|
|
1101
1106
|
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
1102
1107
|
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
1103
|
-
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
1108
|
+
"perform manual credential recovery \u2014 for the primary owner or any co-owner \u2014 when the CLI's local shown-once copy is unavailable",
|
|
1104
1109
|
"configure system AI purposes and view app/environment/run-attributed usage",
|
|
1105
1110
|
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
1106
1111
|
]
|
|
1107
1112
|
};
|
|
1108
|
-
function printCapabilities(
|
|
1109
|
-
if (
|
|
1113
|
+
function printCapabilities(json2 = false, out = console) {
|
|
1114
|
+
if (json2) {
|
|
1110
1115
|
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
1111
1116
|
return;
|
|
1112
1117
|
}
|
|
@@ -1155,8 +1160,8 @@ var CalendarRequestError = class extends Error {
|
|
|
1155
1160
|
status;
|
|
1156
1161
|
/** Machine-readable `error.code` from the response body, when present. */
|
|
1157
1162
|
code;
|
|
1158
|
-
constructor(
|
|
1159
|
-
super(
|
|
1163
|
+
constructor(message3, status, code) {
|
|
1164
|
+
super(message3);
|
|
1160
1165
|
this.name = "CalendarRequestError";
|
|
1161
1166
|
this.status = status;
|
|
1162
1167
|
if (code !== void 0) this.code = code;
|
|
@@ -1290,9 +1295,9 @@ async function calendarJson(ctx, suffix, init) {
|
|
|
1290
1295
|
const url = new URL(`/registry/apps/${encodeURIComponent(appId)}/calendar${suffix}`, platform);
|
|
1291
1296
|
url.searchParams.set("env", env);
|
|
1292
1297
|
const token = credential(ctx.token);
|
|
1293
|
-
let
|
|
1298
|
+
let response2;
|
|
1294
1299
|
try {
|
|
1295
|
-
|
|
1300
|
+
response2 = await (ctx.fetch ?? fetch)(url, {
|
|
1296
1301
|
...init,
|
|
1297
1302
|
redirect: "error",
|
|
1298
1303
|
credentials: "omit",
|
|
@@ -1301,12 +1306,12 @@ async function calendarJson(ctx, suffix, init) {
|
|
|
1301
1306
|
} catch (error) {
|
|
1302
1307
|
throw new Error(`calendar request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1303
1308
|
}
|
|
1304
|
-
const body = await
|
|
1305
|
-
if (!
|
|
1309
|
+
const body = await response2.json().catch(() => ({}));
|
|
1310
|
+
if (!response2.ok) {
|
|
1306
1311
|
const code = textField(body.error?.code, 128);
|
|
1307
|
-
const
|
|
1308
|
-
const detail = `${code ? ` ${code}` : ""}${
|
|
1309
|
-
throw new CalendarRequestError(redactSecrets(`calendar request failed (${
|
|
1312
|
+
const message3 = textField(body.error?.message, 500);
|
|
1313
|
+
const detail = `${code ? ` ${code}` : ""}${message3 ? `: ${message3}` : ""}`;
|
|
1314
|
+
throw new CalendarRequestError(redactSecrets(`calendar request failed (${response2.status})${detail}`), response2.status, code);
|
|
1310
1315
|
}
|
|
1311
1316
|
return body;
|
|
1312
1317
|
}
|
|
@@ -1364,8 +1369,8 @@ function credential(value) {
|
|
|
1364
1369
|
// src/calendar-poll.ts
|
|
1365
1370
|
async function waitForCalendarPoll(milliseconds, signal) {
|
|
1366
1371
|
if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
|
|
1367
|
-
await new Promise((
|
|
1368
|
-
const timer = setTimeout(
|
|
1372
|
+
await new Promise((resolve10, reject) => {
|
|
1373
|
+
const timer = setTimeout(resolve10, milliseconds);
|
|
1369
1374
|
signal?.addEventListener("abort", () => {
|
|
1370
1375
|
clearTimeout(timer);
|
|
1371
1376
|
reject(signal.reason ?? new Error("calendar connection aborted"));
|
|
@@ -1467,7 +1472,7 @@ async function continueConnectedCalendar(ctx, current, options) {
|
|
|
1467
1472
|
if (current.status !== "authorizing") return null;
|
|
1468
1473
|
const now = options.now ?? Date.now;
|
|
1469
1474
|
const deadline = now() + pollTimeout(options.pollTimeoutMs);
|
|
1470
|
-
const
|
|
1475
|
+
const wait2 = options.wait ?? waitForCalendarPoll;
|
|
1471
1476
|
let prior = "";
|
|
1472
1477
|
while (now() < deadline) {
|
|
1473
1478
|
if (options.signal?.aborted) throw options.signal.reason ?? new Error("calendar connection aborted");
|
|
@@ -1483,7 +1488,7 @@ async function continueConnectedCalendar(ctx, current, options) {
|
|
|
1483
1488
|
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and reconnect explicitly`);
|
|
1484
1489
|
}
|
|
1485
1490
|
if (status.status !== "authorizing") return null;
|
|
1486
|
-
await
|
|
1491
|
+
await wait2(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1487
1492
|
}
|
|
1488
1493
|
throw new Error('Existing Google Calendar authorization timed out; run "odla-ai calendar status" and retry');
|
|
1489
1494
|
}
|
|
@@ -1498,7 +1503,7 @@ async function connectWithContext(ctx, options) {
|
|
|
1498
1503
|
const now = options.now ?? Date.now;
|
|
1499
1504
|
const timeout = pollTimeout(options.pollTimeoutMs);
|
|
1500
1505
|
const deadline = Math.min(attempt.expiresAt, now() + timeout);
|
|
1501
|
-
const
|
|
1506
|
+
const wait2 = options.wait ?? waitForCalendarPoll;
|
|
1502
1507
|
let prior = "";
|
|
1503
1508
|
while (now() < deadline) {
|
|
1504
1509
|
if (options.signal?.aborted) throw options.signal.reason ?? new Error("calendar connection aborted");
|
|
@@ -1515,7 +1520,7 @@ async function connectWithContext(ctx, options) {
|
|
|
1515
1520
|
const reason = status.error?.message ?? status.error?.code;
|
|
1516
1521
|
throw new Error(`calendar connection ${status.status}${reason ? `: ${reason}` : ""}`);
|
|
1517
1522
|
}
|
|
1518
|
-
await
|
|
1523
|
+
await wait2(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1519
1524
|
}
|
|
1520
1525
|
throw new Error('Google Calendar consent timed out; run "odla-ai calendar status" and retry connect');
|
|
1521
1526
|
}
|
|
@@ -1541,8 +1546,8 @@ function pollTimeout(value = 10 * 6e4) {
|
|
|
1541
1546
|
function productionConsent(env, yes, action) {
|
|
1542
1547
|
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action} for "${env}" without --yes`);
|
|
1543
1548
|
}
|
|
1544
|
-
function printStatus(status,
|
|
1545
|
-
if (
|
|
1549
|
+
function printStatus(status, json2, out) {
|
|
1550
|
+
if (json2) {
|
|
1546
1551
|
out.log(JSON.stringify(status, null, 2));
|
|
1547
1552
|
return;
|
|
1548
1553
|
}
|
|
@@ -1554,224 +1559,3163 @@ function printStatus(status, json, out) {
|
|
|
1554
1559
|
if (status.error?.code || status.error?.message) out.log(` error: ${status.error.code ?? "error"}${status.error.message ? ` \u2014 ${status.error.message}` : ""}`);
|
|
1555
1560
|
}
|
|
1556
1561
|
|
|
1557
|
-
// src/
|
|
1558
|
-
import {
|
|
1559
|
-
import {
|
|
1560
|
-
import { join as join3, resolve as resolve3 } from "path";
|
|
1562
|
+
// src/security-hosted-github.ts
|
|
1563
|
+
import { execFile } from "child_process";
|
|
1564
|
+
import { promisify } from "util";
|
|
1561
1565
|
|
|
1562
|
-
// src/
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
}
|
|
1584
|
-
function readWranglerConfig(path) {
|
|
1585
|
-
if (path.endsWith(".toml")) return null;
|
|
1586
|
-
try {
|
|
1587
|
-
return JSON.parse(stripJsonComments(readFileSync3(path, "utf8")));
|
|
1588
|
-
} catch {
|
|
1589
|
-
return null;
|
|
1566
|
+
// src/security-hosted-request.ts
|
|
1567
|
+
async function requestHostedSecurityJson(options, path, init, action) {
|
|
1568
|
+
const platform = platformAudience(options.platform);
|
|
1569
|
+
const token = hostedSecurityCredential(options.token);
|
|
1570
|
+
const requestInit = {
|
|
1571
|
+
...init,
|
|
1572
|
+
redirect: "error",
|
|
1573
|
+
credentials: "omit",
|
|
1574
|
+
cache: "no-store",
|
|
1575
|
+
signal: options.signal,
|
|
1576
|
+
headers: {
|
|
1577
|
+
authorization: `Bearer ${token}`,
|
|
1578
|
+
"content-type": "application/json",
|
|
1579
|
+
...init.headers
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
const response2 = await (options.fetch ?? fetch)(new URL(path, platform), requestInit);
|
|
1583
|
+
const body = await response2.json().catch(() => ({}));
|
|
1584
|
+
if (!response2.ok) {
|
|
1585
|
+
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
1586
|
+
const message3 = optionalHostedText(body.error?.message, "error message", 300);
|
|
1587
|
+
throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${message3 ? `: ${message3}` : ""}`);
|
|
1590
1588
|
}
|
|
1589
|
+
return body;
|
|
1591
1590
|
}
|
|
1592
|
-
function
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
const ch = text[i];
|
|
1597
|
-
if (inString) {
|
|
1598
|
-
result += ch;
|
|
1599
|
-
if (ch === "\\") {
|
|
1600
|
-
result += text[i + 1] ?? "";
|
|
1601
|
-
i++;
|
|
1602
|
-
} else if (ch === '"') {
|
|
1603
|
-
inString = false;
|
|
1604
|
-
}
|
|
1605
|
-
continue;
|
|
1606
|
-
}
|
|
1607
|
-
if (ch === '"') {
|
|
1608
|
-
inString = true;
|
|
1609
|
-
result += ch;
|
|
1610
|
-
continue;
|
|
1611
|
-
}
|
|
1612
|
-
if (ch === "/" && text[i + 1] === "/") {
|
|
1613
|
-
while (i < text.length && text[i] !== "\n") i++;
|
|
1614
|
-
result += "\n";
|
|
1615
|
-
continue;
|
|
1616
|
-
}
|
|
1617
|
-
if (ch === "/" && text[i + 1] === "*") {
|
|
1618
|
-
i += 2;
|
|
1619
|
-
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
1620
|
-
i++;
|
|
1621
|
-
continue;
|
|
1622
|
-
}
|
|
1623
|
-
result += ch;
|
|
1591
|
+
function hostedIdentifier(value, name) {
|
|
1592
|
+
const result = optionalHostedText(value, name, 180);
|
|
1593
|
+
if (!result || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(result)) {
|
|
1594
|
+
throw new Error(`${name} is invalid`);
|
|
1624
1595
|
}
|
|
1625
1596
|
return result;
|
|
1626
1597
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
return result.code === 0 && !/not authenticated/i.test(`${result.stdout}${result.stderr}`);
|
|
1631
|
-
} catch {
|
|
1632
|
-
return false;
|
|
1598
|
+
function positivePolicyVersion(value, name) {
|
|
1599
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
1600
|
+
throw new Error(`${name} is invalid`);
|
|
1633
1601
|
}
|
|
1602
|
+
return value;
|
|
1634
1603
|
}
|
|
1635
|
-
function
|
|
1636
|
-
|
|
1637
|
-
|
|
1604
|
+
function optionalHostedText(value, name, maxLength) {
|
|
1605
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
1606
|
+
if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
1607
|
+
throw new Error(`${name} is invalid`);
|
|
1608
|
+
}
|
|
1609
|
+
return value;
|
|
1638
1610
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const warnings = [];
|
|
1643
|
-
if (!rules) return warnings;
|
|
1644
|
-
for (const [ns, actions] of Object.entries(rules)) {
|
|
1645
|
-
for (const [action, expr] of Object.entries(actions)) {
|
|
1646
|
-
if (typeof expr !== "string" || expr.trim() !== "true") continue;
|
|
1647
|
-
if (action === "view" && publicRead.includes(ns)) continue;
|
|
1648
|
-
warnings.push(
|
|
1649
|
-
action === "view" ? `rules.${ns}.view is "true" \u2014 public read; add "${ns}" to db.publicRead if intended` : `rules.${ns}.${action} is "true" \u2014 any client can ${action} ${ns} rows`
|
|
1650
|
-
);
|
|
1651
|
-
}
|
|
1611
|
+
function githubRepositoryName(value) {
|
|
1612
|
+
if (typeof value !== "string" || value.length > 201) {
|
|
1613
|
+
throw new Error("repository must be owner/name");
|
|
1652
1614
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1615
|
+
const parts = value.split("/");
|
|
1616
|
+
const owner = parts[0] ?? "";
|
|
1617
|
+
const name = (parts[1] ?? "").replace(/\.git$/i, "");
|
|
1618
|
+
if (parts.length !== 2 || !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,98}[A-Za-z0-9])?$/.test(owner) || !/^[A-Za-z0-9_.-]{1,100}$/.test(name) || name === "." || name === "..") {
|
|
1619
|
+
throw new Error("repository must be owner/name");
|
|
1655
1620
|
}
|
|
1656
|
-
return
|
|
1621
|
+
return `${owner}/${name}`;
|
|
1657
1622
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
let output;
|
|
1623
|
+
function trustedGitHubInstallUrl(value) {
|
|
1624
|
+
let url;
|
|
1661
1625
|
try {
|
|
1662
|
-
|
|
1663
|
-
output = exec("git", ["ls-files", "--", ".dev.vars", ".odla", ...custom], rootDir);
|
|
1626
|
+
url = new URL(value);
|
|
1664
1627
|
} catch {
|
|
1665
|
-
|
|
1666
|
-
}
|
|
1667
|
-
return output.split(/\r?\n/).filter(Boolean).map((path) => `${path} is tracked by git \u2014 run "git rm --cached ${path}" and commit; it belongs in .gitignore`);
|
|
1668
|
-
}
|
|
1669
|
-
function wranglerWarnings(rootDir) {
|
|
1670
|
-
const configPath = findWranglerConfig(rootDir);
|
|
1671
|
-
if (!configPath) return [];
|
|
1672
|
-
const config = readWranglerConfig(configPath);
|
|
1673
|
-
if (!config) return [];
|
|
1674
|
-
const warnings = [];
|
|
1675
|
-
const blocks = [{ label: "", block: config }];
|
|
1676
|
-
const envs = config.env;
|
|
1677
|
-
if (envs && typeof envs === "object") {
|
|
1678
|
-
for (const [name, block] of Object.entries(envs)) {
|
|
1679
|
-
if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
for (const { label, block } of blocks) {
|
|
1683
|
-
const assets = block.assets;
|
|
1684
|
-
if (assets?.directory) {
|
|
1685
|
-
const dir = resolve3(rootDir, assets.directory);
|
|
1686
|
-
if (dir === resolve3(rootDir)) {
|
|
1687
|
-
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
1688
|
-
} else if (existsSync5(join3(dir, "node_modules"))) {
|
|
1689
|
-
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
const vars = block.vars;
|
|
1693
|
-
if (vars && typeof vars === "object") {
|
|
1694
|
-
for (const [name, value] of Object.entries(vars)) {
|
|
1695
|
-
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value === "string" && looksSecret(value)) {
|
|
1696
|
-
warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1628
|
+
throw new Error("odla.ai returned an invalid GitHub installation URL");
|
|
1700
1629
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
warnings.push(`package.json has a script named "deploy" \u2014 CI may auto-deploy it; rename to deploy:app`);
|
|
1630
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
|
|
1631
|
+
throw new Error("odla.ai returned an untrusted GitHub installation URL");
|
|
1704
1632
|
}
|
|
1705
|
-
return
|
|
1633
|
+
return url.toString();
|
|
1706
1634
|
}
|
|
1707
|
-
function
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
const dependencies = pkg?.dependencies;
|
|
1711
|
-
const devDependencies = pkg?.devDependencies;
|
|
1712
|
-
if (!dependencies?.["@odla-ai/o11y"]) {
|
|
1713
|
-
warnings.push(
|
|
1714
|
-
devDependencies?.["@odla-ai/o11y"] ? "@odla-ai/o11y is a devDependency \u2014 move it to dependencies so the Worker can import it" : 'Worker instrumentation is missing @odla-ai/o11y \u2014 install it and wrap the handler with "withObservability"'
|
|
1715
|
-
);
|
|
1716
|
-
}
|
|
1717
|
-
const configPath = findWranglerConfig(rootDir);
|
|
1718
|
-
const config = configPath ? readWranglerConfig(configPath) : null;
|
|
1719
|
-
if (!configPath || !config) {
|
|
1720
|
-
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
1721
|
-
return warnings;
|
|
1722
|
-
}
|
|
1723
|
-
const main = typeof config.main === "string" ? resolve3(rootDir, config.main) : null;
|
|
1724
|
-
if (!main || !existsSync5(main)) {
|
|
1725
|
-
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
1726
|
-
} else {
|
|
1727
|
-
let source = "";
|
|
1728
|
-
try {
|
|
1729
|
-
source = readFileSync4(main, "utf8");
|
|
1730
|
-
} catch {
|
|
1731
|
-
}
|
|
1732
|
-
if (!/\bwithObservability\b/.test(source)) {
|
|
1733
|
-
warnings.push(`wrangler entrypoint ${config.main} does not reference withObservability`);
|
|
1734
|
-
}
|
|
1635
|
+
function hostedPollInterval(value = 2e3) {
|
|
1636
|
+
if (!Number.isSafeInteger(value) || value < 100 || value > 3e4) {
|
|
1637
|
+
throw new Error("poll interval must be 100-30000ms");
|
|
1735
1638
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1639
|
+
return value;
|
|
1640
|
+
}
|
|
1641
|
+
function hostedPollTimeout(value = 10 * 6e4) {
|
|
1642
|
+
if (!Number.isSafeInteger(value) || value < 1e3 || value > 60 * 6e4) {
|
|
1643
|
+
throw new Error("poll timeout must be 1000-3600000ms");
|
|
1739
1644
|
}
|
|
1740
|
-
return
|
|
1645
|
+
return value;
|
|
1741
1646
|
}
|
|
1742
|
-
function
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1647
|
+
async function waitForHostedPoll(milliseconds, signal) {
|
|
1648
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
|
|
1649
|
+
await new Promise((resolve10, reject) => {
|
|
1650
|
+
const timer = setTimeout(resolve10, milliseconds);
|
|
1651
|
+
signal?.addEventListener("abort", () => {
|
|
1652
|
+
clearTimeout(timer);
|
|
1653
|
+
reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
1654
|
+
}, { once: true });
|
|
1655
|
+
});
|
|
1750
1656
|
}
|
|
1751
|
-
function
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1657
|
+
function isValidHostedSecurityPlan(value, env) {
|
|
1658
|
+
if (!value || value.env !== env || !/^sha256:[a-f0-9]{64}$/.test(value.planDigest) || value.consentContract !== "odla.hosted-security-consent.v1" || value.reportProjection !== "odla.hosted-security-report.v1" || value.redactionContract !== "odla.best-effort-credential-pattern-redaction.v1" || typeof value.promptBundle !== "string" || value.promptBundle.length < 1 || value.promptBundle.length > 100 || typeof value.ready !== "boolean" || typeof value.independent !== "boolean" || value.sourceDisclosure !== "redacted" || value.targetExecution !== false || !Number.isSafeInteger(value.reportRetentionDays) || value.reportRetentionDays < 1) return false;
|
|
1659
|
+
const validRoute = (route2, purpose) => !!route2 && route2.purpose === purpose && typeof route2.enabled === "boolean" && typeof route2.credentialReady === "boolean" && typeof route2.provider === "string" && route2.provider.length > 0 && route2.provider.length <= 100 && typeof route2.model === "string" && route2.model.length > 0 && route2.model.length <= 200 && Number.isSafeInteger(route2.policyVersion) && route2.policyVersion >= 1 && Number.isSafeInteger(route2.maxCallsPerRun) && route2.maxCallsPerRun >= 1 && Number.isSafeInteger(route2.maxInputBytes) && route2.maxInputBytes >= 1 && Number.isSafeInteger(route2.maxOutputTokens) && route2.maxOutputTokens >= 1;
|
|
1660
|
+
return validRoute(value.routes?.discovery, "security.discovery") && validRoute(value.routes?.validation, "security.validation");
|
|
1661
|
+
}
|
|
1662
|
+
function hostedSecurityCredential(value) {
|
|
1663
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
1664
|
+
throw new Error("Hosted security requires an injected odla developer token");
|
|
1756
1665
|
}
|
|
1666
|
+
return value;
|
|
1757
1667
|
}
|
|
1758
1668
|
|
|
1759
|
-
// src/
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1669
|
+
// src/security-hosted-github.ts
|
|
1670
|
+
async function connectGitHubSecuritySource(options) {
|
|
1671
|
+
const out = options.stdout ?? console;
|
|
1672
|
+
const repository = githubRepositoryName(options.repository);
|
|
1673
|
+
const attempt = await requestHostedSecurityJson(
|
|
1674
|
+
options,
|
|
1675
|
+
"/registry/github/connect",
|
|
1676
|
+
{
|
|
1677
|
+
method: "POST",
|
|
1678
|
+
body: JSON.stringify({
|
|
1679
|
+
appId: hostedIdentifier(options.appId, "appId"),
|
|
1680
|
+
env: hostedIdentifier(options.env, "env"),
|
|
1681
|
+
repository
|
|
1682
|
+
})
|
|
1683
|
+
},
|
|
1684
|
+
"start GitHub connection"
|
|
1685
|
+
);
|
|
1686
|
+
const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
|
|
1687
|
+
if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
|
|
1688
|
+
throw new Error("odla.ai returned an invalid GitHub connection attempt");
|
|
1689
|
+
}
|
|
1690
|
+
const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
|
|
1691
|
+
out.log(`GitHub approval: ${installUrl}`);
|
|
1692
|
+
if (options.open !== false) {
|
|
1693
|
+
await (options.openInstallUrl ?? openUrl)(installUrl);
|
|
1694
|
+
out.log("github: opened installation approval in your browser");
|
|
1695
|
+
}
|
|
1696
|
+
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
1697
|
+
const now = options.now ?? Date.now;
|
|
1698
|
+
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
1699
|
+
const wait2 = options.wait ?? waitForHostedPoll;
|
|
1700
|
+
while (true) {
|
|
1701
|
+
const state = await requestHostedSecurityJson(
|
|
1702
|
+
options,
|
|
1703
|
+
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
1704
|
+
{},
|
|
1705
|
+
"check GitHub connection"
|
|
1706
|
+
);
|
|
1707
|
+
if (state.status !== "pending") {
|
|
1708
|
+
if (state.status === "connected") return state;
|
|
1709
|
+
throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
|
|
1710
|
+
}
|
|
1711
|
+
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
1712
|
+
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
async function listGitHubSecuritySources(options) {
|
|
1716
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
1717
|
+
const env = hostedIdentifier(options.env, "env");
|
|
1718
|
+
const body = await requestHostedSecurityJson(
|
|
1719
|
+
options,
|
|
1720
|
+
`/registry/apps/${encodeURIComponent(appId)}/github/sources?env=${encodeURIComponent(env)}`,
|
|
1721
|
+
{},
|
|
1722
|
+
"list GitHub security sources"
|
|
1723
|
+
);
|
|
1724
|
+
return Array.isArray(body.sources) ? body.sources : [];
|
|
1725
|
+
}
|
|
1726
|
+
async function disconnectGitHubSecuritySource(options) {
|
|
1727
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
1728
|
+
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
1729
|
+
await requestHostedSecurityJson(
|
|
1730
|
+
options,
|
|
1731
|
+
`/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
|
|
1732
|
+
{ method: "DELETE" },
|
|
1733
|
+
"disconnect GitHub security source"
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
function repositoryFromGitRemote(remoteInput) {
|
|
1737
|
+
const remote = remoteInput.trim();
|
|
1738
|
+
const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
|
|
1739
|
+
if (scp) return githubRepositoryName(`${scp[1]}/${scp[2].replace(/\.git$/, "")}`);
|
|
1740
|
+
let url;
|
|
1741
|
+
try {
|
|
1742
|
+
url = new URL(remote);
|
|
1743
|
+
} catch {
|
|
1744
|
+
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
1745
|
+
}
|
|
1746
|
+
if (url.hostname !== "github.com" || url.protocol !== "https:" && url.protocol !== "ssh:") {
|
|
1747
|
+
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
1748
|
+
}
|
|
1749
|
+
if (url.password || url.protocol === "https:" && url.username || url.search || url.hash) {
|
|
1750
|
+
throw new Error("credential-bearing GitHub origin URLs are not accepted");
|
|
1751
|
+
}
|
|
1752
|
+
const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
1753
|
+
if (parts.length !== 2) {
|
|
1754
|
+
throw new Error("origin must identify one GitHub owner/name repository");
|
|
1755
|
+
}
|
|
1756
|
+
return githubRepositoryName(`${parts[0]}/${parts[1].replace(/\.git$/, "")}`);
|
|
1757
|
+
}
|
|
1758
|
+
async function inferGitHubRepository(cwd = process.cwd(), readOrigin = defaultReadOrigin) {
|
|
1759
|
+
let remote;
|
|
1760
|
+
try {
|
|
1761
|
+
remote = await readOrigin(cwd);
|
|
1762
|
+
} catch {
|
|
1763
|
+
throw new Error("Could not read git origin; pass --repo owner/name");
|
|
1764
|
+
}
|
|
1765
|
+
return repositoryFromGitRemote(remote);
|
|
1766
|
+
}
|
|
1767
|
+
async function defaultReadOrigin(cwd) {
|
|
1768
|
+
const result = await promisify(execFile)(
|
|
1769
|
+
"git",
|
|
1770
|
+
["remote", "get-url", "origin"],
|
|
1771
|
+
{ cwd, encoding: "utf8" }
|
|
1772
|
+
);
|
|
1773
|
+
return result.stdout;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/code-connect.ts
|
|
1777
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1778
|
+
import { cpus, hostname, totalmem } from "os";
|
|
1779
|
+
import { resolve as resolve5 } from "path";
|
|
1780
|
+
|
|
1781
|
+
// ../harness/dist/chunk-UG5XHNFB.js
|
|
1782
|
+
var HARNESS_PROTOCOL_VERSION = 1;
|
|
1783
|
+
|
|
1784
|
+
// ../harness/dist/chunk-QALIIZRJ.js
|
|
1785
|
+
var CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
1786
|
+
var HarnessProtocolError = class extends Error {
|
|
1787
|
+
name = "HarnessProtocolError";
|
|
1788
|
+
};
|
|
1789
|
+
function record2(value) {
|
|
1790
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1791
|
+
}
|
|
1792
|
+
function boundedText(value, label, max) {
|
|
1793
|
+
if (typeof value !== "string" || !value || value.length > max || CONTROL.test(value)) {
|
|
1794
|
+
throw new HarnessProtocolError(`${label} must be a non-empty string of at most ${max} characters`);
|
|
1795
|
+
}
|
|
1796
|
+
return value;
|
|
1797
|
+
}
|
|
1798
|
+
function parseAgentOutput(line) {
|
|
1799
|
+
if (Buffer.byteLength(line, "utf8") > 1e6) throw new HarnessProtocolError("agent message exceeds 1 MB");
|
|
1800
|
+
let value;
|
|
1801
|
+
try {
|
|
1802
|
+
value = JSON.parse(line);
|
|
1803
|
+
} catch {
|
|
1804
|
+
throw new HarnessProtocolError("agent emitted invalid JSON");
|
|
1805
|
+
}
|
|
1806
|
+
const message3 = record2(value);
|
|
1807
|
+
if (!message3 || message3.protocolVersion !== HARNESS_PROTOCOL_VERSION) {
|
|
1808
|
+
throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);
|
|
1809
|
+
}
|
|
1810
|
+
if (message3.type === "event") {
|
|
1811
|
+
return {
|
|
1812
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
1813
|
+
type: "event",
|
|
1814
|
+
kind: boundedText(message3.kind, "event.kind", 120),
|
|
1815
|
+
...message3.payload === void 0 ? {} : { payload: message3.payload }
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
if (message3.type === "inference.request") {
|
|
1819
|
+
const call = record2(message3.call);
|
|
1820
|
+
if (!call || !Array.isArray(call.messages) || !Number.isSafeInteger(call.maxTokens)) {
|
|
1821
|
+
throw new HarnessProtocolError("inference.request.call requires messages and maxTokens");
|
|
1822
|
+
}
|
|
1823
|
+
return {
|
|
1824
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
1825
|
+
type: "inference.request",
|
|
1826
|
+
requestId: boundedText(message3.requestId, "requestId", 180),
|
|
1827
|
+
call
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
if (message3.type === "tool.request") {
|
|
1831
|
+
const input = record2(message3.input);
|
|
1832
|
+
const tool = String(message3.tool);
|
|
1833
|
+
if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
|
|
1834
|
+
throw new HarnessProtocolError("tool.request requires a registered tool and object input");
|
|
1835
|
+
}
|
|
1836
|
+
return {
|
|
1837
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
1838
|
+
type: "tool.request",
|
|
1839
|
+
requestId: boundedText(message3.requestId, "requestId", 180),
|
|
1840
|
+
tool,
|
|
1841
|
+
input
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
if (message3.type === "attempt.complete") {
|
|
1845
|
+
if (!(/* @__PURE__ */ new Set(["completed", "failed", "cancelled"])).has(String(message3.status))) {
|
|
1846
|
+
throw new HarnessProtocolError("attempt.complete.status is invalid");
|
|
1847
|
+
}
|
|
1848
|
+
return {
|
|
1849
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
1850
|
+
type: "attempt.complete",
|
|
1851
|
+
status: message3.status,
|
|
1852
|
+
...message3.result === void 0 ? {} : { result: message3.result }
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
throw new HarnessProtocolError("agent message type is unsupported");
|
|
1856
|
+
}
|
|
1857
|
+
function encodeAgentInput(message3) {
|
|
1858
|
+
return `${JSON.stringify(message3)}
|
|
1859
|
+
`;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// ../harness/dist/chunk-FNYUIJUN.js
|
|
1863
|
+
import { execFile as execFile2, spawn as spawn2 } from "child_process";
|
|
1864
|
+
import { constants } from "fs";
|
|
1865
|
+
import { access } from "fs/promises";
|
|
1866
|
+
import { delimiter, join as join2 } from "path";
|
|
1867
|
+
import { getgid, getuid } from "process";
|
|
1868
|
+
import { chmod, copyFile, mkdir, mkdtemp, readdir, realpath, rm, stat } from "fs/promises";
|
|
1869
|
+
import { tmpdir } from "os";
|
|
1870
|
+
import { basename, join as join22, relative as relative2, resolve as resolve3 } from "path";
|
|
1871
|
+
import { spawn as spawn22 } from "child_process";
|
|
1872
|
+
var DIGEST_IMAGE = /^[a-z0-9][a-z0-9._/-]*(?::[a-zA-Z0-9._-]+)?@sha256:[0-9a-f]{64}$/;
|
|
1873
|
+
function assertPinnedImage(image) {
|
|
1874
|
+
if (!DIGEST_IMAGE.test(image)) throw new TypeError("container image must be pinned by sha256 digest");
|
|
1875
|
+
}
|
|
1876
|
+
async function commandAvailable(engine) {
|
|
1877
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
1878
|
+
try {
|
|
1879
|
+
await access(join2(directory, engine), constants.X_OK);
|
|
1880
|
+
return true;
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
async function selectContainerEngine(requested = "auto", options = {}) {
|
|
1887
|
+
const platform = options.platform ?? process.platform;
|
|
1888
|
+
const arch = options.arch ?? process.arch;
|
|
1889
|
+
const uid = options.uid ?? (typeof getuid === "function" ? getuid() : 1e3);
|
|
1890
|
+
const available = options.available ?? commandAvailable;
|
|
1891
|
+
const validate3 = async (engine) => {
|
|
1892
|
+
if (engine === "container" && (platform !== "darwin" || arch !== "arm64")) {
|
|
1893
|
+
throw new TypeError("Apple container requires Apple Silicon macOS");
|
|
1894
|
+
}
|
|
1895
|
+
if (engine === "podman" && platform === "linux" && uid === 0) {
|
|
1896
|
+
throw new TypeError("the Linux harness requires rootless Podman; do not run the runner as root");
|
|
1897
|
+
}
|
|
1898
|
+
if (!await available(engine)) throw new TypeError(`${engine} is not installed or executable`);
|
|
1899
|
+
return engine;
|
|
1900
|
+
};
|
|
1901
|
+
if (requested !== "auto") return validate3(requested);
|
|
1902
|
+
const candidates = platform === "darwin" ? arch === "arm64" ? ["container", "podman"] : ["podman"] : platform === "linux" ? ["podman"] : [];
|
|
1903
|
+
for (const engine of candidates) {
|
|
1904
|
+
if (await available(engine)) return validate3(engine);
|
|
1905
|
+
}
|
|
1906
|
+
if (platform === "linux") {
|
|
1907
|
+
throw new TypeError("no rootless Podman found; install Podman or explicitly choose --engine docker after reviewing its daemon boundary");
|
|
1908
|
+
}
|
|
1909
|
+
if (platform === "darwin") {
|
|
1910
|
+
throw new TypeError("no Apple container or Podman Machine found; install one or explicitly choose --engine docker");
|
|
1911
|
+
}
|
|
1912
|
+
throw new TypeError("no supported container engine found");
|
|
1913
|
+
}
|
|
1914
|
+
function inspectRootlessPodman() {
|
|
1915
|
+
return new Promise((resolve23, reject) => {
|
|
1916
|
+
execFile2(
|
|
1917
|
+
"podman",
|
|
1918
|
+
["info", "--format", "{{.Host.Security.Rootless}}"],
|
|
1919
|
+
{ encoding: "utf8", maxBuffer: 16 * 1024, timeout: 1e4 },
|
|
1920
|
+
(error, stdout) => {
|
|
1921
|
+
if (error) {
|
|
1922
|
+
reject(new TypeError("could not verify that the active Podman service is rootless"));
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
resolve23(stdout.trim() === "true");
|
|
1926
|
+
}
|
|
1927
|
+
);
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
async function verifyContainerEngineBoundary(engine, options = {}) {
|
|
1931
|
+
const platform = options.platform ?? process.platform;
|
|
1932
|
+
const arch = options.arch ?? process.arch;
|
|
1933
|
+
const uid = options.uid ?? (typeof getuid === "function" ? getuid() : 1e3);
|
|
1934
|
+
if (engine === "container" && (platform !== "darwin" || arch !== "arm64")) {
|
|
1935
|
+
throw new TypeError("Apple container requires Apple Silicon macOS");
|
|
1936
|
+
}
|
|
1937
|
+
if (engine !== "podman" || platform !== "linux") return;
|
|
1938
|
+
if (uid === 0) throw new TypeError("the Linux harness requires rootless Podman; do not run the runner as root");
|
|
1939
|
+
const rootless = await (options.podmanRootless ?? inspectRootlessPodman)();
|
|
1940
|
+
if (!rootless) throw new TypeError("the active Podman service is not rootless; refusing to run the harness");
|
|
1941
|
+
}
|
|
1942
|
+
function buildContainerRunArgs(options) {
|
|
1943
|
+
if (!options.allowUnpinnedImage) assertPinnedImage(options.image);
|
|
1944
|
+
if (/[,\r\n]/.test(options.workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
|
|
1945
|
+
const uid = typeof getuid === "function" ? getuid() : 1e3;
|
|
1946
|
+
const gid = typeof getgid === "function" ? getgid() : 1e3;
|
|
1947
|
+
const safeAttempt = options.task.attemptId.toLowerCase().replace(/[^a-z0-9_.-]/g, "-").slice(0, 40);
|
|
1948
|
+
const name = `odla-harness-${safeAttempt}-${crypto.randomUUID().slice(0, 8)}`;
|
|
1949
|
+
const limits = options.limits ?? {};
|
|
1950
|
+
const access2 = options.workspaceAccess ?? "read-write";
|
|
1951
|
+
const appleMount = access2 === "none" ? [] : [`--mount=type=bind,source=${options.workspaceDir},target=/workspace${access2 === "read-only" ? ",readonly" : ""}`];
|
|
1952
|
+
const ociMount = access2 === "none" ? [] : [`--mount=type=bind,src=${options.workspaceDir},dst=/workspace${access2 === "read-only" ? ",readonly" : ""}`];
|
|
1953
|
+
if (options.engine === "container") {
|
|
1954
|
+
return [
|
|
1955
|
+
"run",
|
|
1956
|
+
"--rm",
|
|
1957
|
+
"--interactive",
|
|
1958
|
+
`--name=${name}`,
|
|
1959
|
+
"--network=none",
|
|
1960
|
+
"--read-only",
|
|
1961
|
+
"--cap-drop=ALL",
|
|
1962
|
+
`--memory=${limits.memory ?? "1g"}`,
|
|
1963
|
+
`--cpus=${limits.cpus ?? 1}`,
|
|
1964
|
+
`--user=${uid}:${gid}`,
|
|
1965
|
+
"--tmpfs=/tmp",
|
|
1966
|
+
...appleMount,
|
|
1967
|
+
"--workdir=/workspace",
|
|
1968
|
+
`--env=ODLA_HARNESS_PROTOCOL=${HARNESS_PROTOCOL_VERSION}`,
|
|
1969
|
+
`--label=ai.odla.harness.attempt=${options.task.attemptId}`,
|
|
1970
|
+
options.image
|
|
1971
|
+
];
|
|
1972
|
+
}
|
|
1973
|
+
return [
|
|
1974
|
+
"run",
|
|
1975
|
+
"--rm",
|
|
1976
|
+
"--interactive",
|
|
1977
|
+
`--name=${name}`,
|
|
1978
|
+
"--pull=never",
|
|
1979
|
+
"--network=none",
|
|
1980
|
+
"--read-only",
|
|
1981
|
+
"--cap-drop=ALL",
|
|
1982
|
+
"--security-opt=no-new-privileges",
|
|
1983
|
+
`--pids-limit=${limits.pids ?? 256}`,
|
|
1984
|
+
`--memory=${limits.memory ?? "1g"}`,
|
|
1985
|
+
`--cpus=${limits.cpus ?? 1}`,
|
|
1986
|
+
`--user=${uid}:${gid}`,
|
|
1987
|
+
`--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=${limits.tmpfsBytes ?? 64 * 1024 * 1024}`,
|
|
1988
|
+
...ociMount,
|
|
1989
|
+
"--workdir=/workspace",
|
|
1990
|
+
`--env=ODLA_HARNESS_PROTOCOL=${HARNESS_PROTOCOL_VERSION}`,
|
|
1991
|
+
`--label=ai.odla.harness.attempt=${options.task.attemptId}`,
|
|
1992
|
+
options.image
|
|
1993
|
+
];
|
|
1994
|
+
}
|
|
1995
|
+
function containerName(args) {
|
|
1996
|
+
return args.find((arg) => arg.startsWith("--name=")).slice("--name=".length);
|
|
1997
|
+
}
|
|
1998
|
+
async function runContainerAttempt(options) {
|
|
1999
|
+
if (options.signal?.aborted) return { exitCode: 1, status: "cancelled", stderr: "" };
|
|
2000
|
+
await verifyContainerEngineBoundary(options.engine);
|
|
2001
|
+
const args = buildContainerRunArgs(options);
|
|
2002
|
+
const name = containerName(args);
|
|
2003
|
+
const child = spawn2(options.engine, args, { stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
2004
|
+
let stderr = "";
|
|
2005
|
+
let outputBytes = 0;
|
|
2006
|
+
let complete = null;
|
|
2007
|
+
let stopped = false;
|
|
2008
|
+
let exited = false;
|
|
2009
|
+
child.stderr.setEncoding("utf8");
|
|
2010
|
+
child.stderr.on("data", (text) => {
|
|
2011
|
+
if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
|
|
2012
|
+
});
|
|
2013
|
+
const stop = (reason) => {
|
|
2014
|
+
if (stopped || exited) return;
|
|
2015
|
+
stopped = true;
|
|
2016
|
+
if (!child.stdin.destroyed) {
|
|
2017
|
+
const cancel = { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "attempt.cancel", reason };
|
|
2018
|
+
child.stdin.write(encodeAgentInput(cancel));
|
|
2019
|
+
}
|
|
2020
|
+
const removeArgs = options.engine === "container" ? ["delete", "--force", name] : ["rm", "-f", name];
|
|
2021
|
+
const killer = spawn2(options.engine, removeArgs, { stdio: "ignore", shell: false });
|
|
2022
|
+
killer.unref();
|
|
2023
|
+
};
|
|
2024
|
+
const abort = () => stop("runner_cancelled");
|
|
2025
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
2026
|
+
const timeout = setTimeout(() => stop("timeout"), options.task.policy.timeoutMs);
|
|
2027
|
+
const start = { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "task.start", task: options.task };
|
|
2028
|
+
if (!stopped && !options.signal?.aborted) child.stdin.write(encodeAgentInput(start));
|
|
2029
|
+
const consume = (async () => {
|
|
2030
|
+
let pending = Buffer.alloc(0);
|
|
2031
|
+
const handleLine = async (raw) => {
|
|
2032
|
+
const bytes = raw.at(-1) === 13 ? raw.subarray(0, -1) : raw;
|
|
2033
|
+
if (bytes.byteLength > 1e6) throw new Error("agent message exceeds 1 MB");
|
|
2034
|
+
const line = bytes.toString("utf8");
|
|
2035
|
+
if (!line.trim()) return;
|
|
2036
|
+
const message3 = parseAgentOutput(line);
|
|
2037
|
+
if (message3.type === "attempt.complete") complete = message3;
|
|
2038
|
+
const response2 = await options.onMessage(message3);
|
|
2039
|
+
if (response2 && !child.stdin.destroyed) child.stdin.write(encodeAgentInput(response2));
|
|
2040
|
+
};
|
|
2041
|
+
try {
|
|
2042
|
+
for await (const raw of child.stdout) {
|
|
2043
|
+
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
2044
|
+
outputBytes += chunk.byteLength;
|
|
2045
|
+
if (outputBytes > options.task.policy.maxOutputBytes) {
|
|
2046
|
+
throw new Error(`agent output exceeds ${options.task.policy.maxOutputBytes} bytes`);
|
|
2047
|
+
}
|
|
2048
|
+
pending = Buffer.concat([pending, chunk]);
|
|
2049
|
+
let newline = pending.indexOf(10);
|
|
2050
|
+
while (newline >= 0) {
|
|
2051
|
+
await handleLine(pending.subarray(0, newline));
|
|
2052
|
+
pending = pending.subarray(newline + 1);
|
|
2053
|
+
newline = pending.indexOf(10);
|
|
2054
|
+
}
|
|
2055
|
+
if (pending.byteLength > 1e6) throw new Error("agent message exceeds 1 MB");
|
|
2056
|
+
}
|
|
2057
|
+
if (pending.byteLength) await handleLine(pending);
|
|
2058
|
+
} catch (error) {
|
|
2059
|
+
stop("protocol_error");
|
|
2060
|
+
throw error;
|
|
2061
|
+
}
|
|
2062
|
+
})();
|
|
2063
|
+
const exit = new Promise((accept, reject) => {
|
|
2064
|
+
child.once("error", reject);
|
|
2065
|
+
child.once("exit", (code) => {
|
|
2066
|
+
exited = true;
|
|
2067
|
+
accept(code ?? 1);
|
|
2068
|
+
});
|
|
2069
|
+
});
|
|
2070
|
+
try {
|
|
2071
|
+
const [exitCode] = await Promise.all([exit, consume]);
|
|
2072
|
+
if (stderr && options.onStderr) await options.onStderr(stderr);
|
|
2073
|
+
if (options.signal?.aborted) return { exitCode, status: "cancelled", stderr };
|
|
2074
|
+
const terminal = complete;
|
|
2075
|
+
if (!terminal) return { exitCode, status: "failed", result: { error: "agent exited without completion" }, stderr };
|
|
2076
|
+
return { exitCode, status: exitCode === 0 ? terminal.status : "failed", result: terminal.result, stderr };
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
stop("runner_error");
|
|
2079
|
+
await exit.catch(() => 1);
|
|
2080
|
+
throw error;
|
|
2081
|
+
} finally {
|
|
2082
|
+
clearTimeout(timeout);
|
|
2083
|
+
options.signal?.removeEventListener("abort", abort);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
2087
|
+
var SECRET_FILE = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
2088
|
+
async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
2089
|
+
const files = [];
|
|
2090
|
+
let bytes = 0;
|
|
2091
|
+
const walk = async (dir) => {
|
|
2092
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
2093
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
2094
|
+
if (!entry.isDirectory() && SECRET_FILE.test(entry.name)) continue;
|
|
2095
|
+
const path = join22(dir, entry.name);
|
|
2096
|
+
if (entry.isSymbolicLink()) continue;
|
|
2097
|
+
if (entry.isDirectory()) {
|
|
2098
|
+
await walk(path);
|
|
2099
|
+
continue;
|
|
2100
|
+
}
|
|
2101
|
+
if (!entry.isFile()) continue;
|
|
2102
|
+
const metadata2 = await stat(path);
|
|
2103
|
+
bytes += metadata2.size;
|
|
2104
|
+
if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
2105
|
+
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2106
|
+
files.push({
|
|
2107
|
+
source: path,
|
|
2108
|
+
relativePath: relative2(sourceDir, path),
|
|
2109
|
+
mode: metadata2.mode & 511,
|
|
2110
|
+
bytes: metadata2.size
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
await walk(sourceDir);
|
|
2115
|
+
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
2116
|
+
}
|
|
2117
|
+
async function copyTree(files, destination) {
|
|
2118
|
+
for (const file of files) {
|
|
2119
|
+
const target = join22(destination, file.relativePath);
|
|
2120
|
+
await mkdir(resolve3(target, ".."), { recursive: true });
|
|
2121
|
+
await copyFile(file.source, target);
|
|
2122
|
+
await chmod(target, file.mode);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
async function captureGitDiff(root, maxBytes) {
|
|
2126
|
+
const child = spawn22("git", [
|
|
2127
|
+
"diff",
|
|
2128
|
+
"--no-index",
|
|
2129
|
+
"--binary",
|
|
2130
|
+
"--no-ext-diff",
|
|
2131
|
+
"--src-prefix=a/",
|
|
2132
|
+
"--dst-prefix=b/",
|
|
2133
|
+
"--",
|
|
2134
|
+
"baseline",
|
|
2135
|
+
"workspace"
|
|
2136
|
+
], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
2137
|
+
const stdout = [];
|
|
2138
|
+
const stderr = [];
|
|
2139
|
+
let bytes = 0;
|
|
2140
|
+
child.stdout.on("data", (chunk) => {
|
|
2141
|
+
bytes += chunk.byteLength;
|
|
2142
|
+
if (bytes > maxBytes) child.kill("SIGKILL");
|
|
2143
|
+
else stdout.push(chunk);
|
|
2144
|
+
});
|
|
2145
|
+
child.stderr.on("data", (chunk) => {
|
|
2146
|
+
if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
|
|
2147
|
+
});
|
|
2148
|
+
const code = await new Promise((accept, reject) => {
|
|
2149
|
+
child.once("error", reject);
|
|
2150
|
+
child.once("exit", accept);
|
|
2151
|
+
});
|
|
2152
|
+
if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
|
|
2153
|
+
if (code !== 0 && code !== 1) {
|
|
2154
|
+
throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
|
|
2155
|
+
}
|
|
2156
|
+
return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
|
|
2157
|
+
}
|
|
2158
|
+
async function stageWorkspace(source, options = {}) {
|
|
2159
|
+
const sourceDir = await realpath(resolve3(source));
|
|
2160
|
+
const sourceStat = await stat(sourceDir);
|
|
2161
|
+
if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
|
|
2162
|
+
const root = await mkdtemp(join22(options.tempRoot ?? tmpdir(), "odla-harness-"));
|
|
2163
|
+
const baselineDir = join22(root, "baseline");
|
|
2164
|
+
const workspaceDir = join22(root, "workspace");
|
|
2165
|
+
await Promise.all([mkdir(baselineDir), mkdir(workspaceDir)]);
|
|
2166
|
+
try {
|
|
2167
|
+
const files = await sourceFiles(sourceDir, options.maxFiles ?? 2e4, options.maxBytes ?? 512 * 1024 * 1024);
|
|
2168
|
+
await Promise.all([copyTree(files, baselineDir), copyTree(files, workspaceDir)]);
|
|
2169
|
+
return {
|
|
2170
|
+
root,
|
|
2171
|
+
baselineDir,
|
|
2172
|
+
workspaceDir,
|
|
2173
|
+
fileCount: files.length,
|
|
2174
|
+
byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
|
|
2175
|
+
patch: (maxBytes) => captureGitDiff(root, maxBytes),
|
|
2176
|
+
cleanup: () => rm(root, { recursive: true, force: true })
|
|
2177
|
+
};
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
await rm(root, { recursive: true, force: true });
|
|
2180
|
+
throw error;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// ../camel/dist/chunk-7FHPOQVP.js
|
|
2185
|
+
var CamelError = class extends Error {
|
|
2186
|
+
code;
|
|
2187
|
+
safeMessage;
|
|
2188
|
+
constructor(code, safeMessage, options) {
|
|
2189
|
+
super(safeMessage, options);
|
|
2190
|
+
this.name = "CamelError";
|
|
2191
|
+
this.code = code;
|
|
2192
|
+
this.safeMessage = safeMessage;
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
// ../camel/dist/chunk-L5DYU2E2.js
|
|
2197
|
+
function canonicalJson(value) {
|
|
2198
|
+
return JSON.stringify(normalize(value));
|
|
2199
|
+
}
|
|
2200
|
+
async function sha256Hex(value) {
|
|
2201
|
+
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
2202
|
+
const digest2 = await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer);
|
|
2203
|
+
return [...new Uint8Array(digest2)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2204
|
+
}
|
|
2205
|
+
function utf8Length(value) {
|
|
2206
|
+
if (typeof value === "string") return new TextEncoder().encode(value).byteLength;
|
|
2207
|
+
if (value instanceof Uint8Array) return value.byteLength;
|
|
2208
|
+
try {
|
|
2209
|
+
return new TextEncoder().encode(canonicalJson(value)).byteLength;
|
|
2210
|
+
} catch {
|
|
2211
|
+
throw new CamelError("conversion_rejected", "Unsafe input could not be canonically measured.");
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
function normalize(value) {
|
|
2215
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2216
|
+
if (typeof value === "number") {
|
|
2217
|
+
if (!Number.isFinite(value)) throw new CamelError("state_conflict", "Canonical JSON rejects non-finite numbers.");
|
|
2218
|
+
return value;
|
|
2219
|
+
}
|
|
2220
|
+
if (Array.isArray(value)) return value.map(normalize);
|
|
2221
|
+
if (value instanceof Uint8Array) return { $bytes: [...value] };
|
|
2222
|
+
if (typeof value === "object") {
|
|
2223
|
+
const record5 = value;
|
|
2224
|
+
return Object.fromEntries(Object.keys(record5).filter((key) => record5[key] !== void 0).sort().map((key) => [key, normalize(record5[key])]));
|
|
2225
|
+
}
|
|
2226
|
+
throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
|
|
2227
|
+
}
|
|
2228
|
+
function canRead(readers, readerId) {
|
|
2229
|
+
return readers.kind === "public" || readers.principalIds.includes(readerId);
|
|
2230
|
+
}
|
|
2231
|
+
function dependenciesOf(values, influence = "data") {
|
|
2232
|
+
const result = [];
|
|
2233
|
+
for (const value of values) {
|
|
2234
|
+
result.push(...value.label.dependencies);
|
|
2235
|
+
for (const ref of value.label.provenance) {
|
|
2236
|
+
result.push({ ref, influence, promptSafetyAtUse: value.label.promptSafety });
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
const unique3 = /* @__PURE__ */ new Map();
|
|
2240
|
+
for (const dep of result) unique3.set(`${dep.ref.kind}\0${dep.ref.id}\0${dep.influence}\0${dep.promptSafetyAtUse}`, dep);
|
|
2241
|
+
return [...unique3.values()];
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// ../camel/dist/chunk-S7EVNA2U.js
|
|
2245
|
+
var camelValueBrand = /* @__PURE__ */ Symbol("@odla-ai/camel/value");
|
|
2246
|
+
var authenticCamelValues = /* @__PURE__ */ new WeakSet();
|
|
2247
|
+
function isCamelValue(value) {
|
|
2248
|
+
return typeof value === "object" && value !== null && authenticCamelValues.has(value);
|
|
2249
|
+
}
|
|
2250
|
+
function isSafe(value) {
|
|
2251
|
+
return isCamelValue(value) && value.label.promptSafety === "safe";
|
|
2252
|
+
}
|
|
2253
|
+
function isUnsafe(value) {
|
|
2254
|
+
return isCamelValue(value) && value.label.promptSafety === "unsafe";
|
|
2255
|
+
}
|
|
2256
|
+
function createSafeInternal(value, safeBasis, metadata2) {
|
|
2257
|
+
return createValue(value, {
|
|
2258
|
+
schemaVersion: 1,
|
|
2259
|
+
promptSafety: "safe",
|
|
2260
|
+
safeBasis,
|
|
2261
|
+
...copyMetadata(metadata2)
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
function createUnsafeInternal(value, metadata2) {
|
|
2265
|
+
return createValue(value, {
|
|
2266
|
+
schemaVersion: 1,
|
|
2267
|
+
promptSafety: "unsafe",
|
|
2268
|
+
...copyMetadata(metadata2)
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
function createValue(value, label) {
|
|
2272
|
+
const result = { value, label: Object.freeze(label) };
|
|
2273
|
+
Object.defineProperty(result, camelValueBrand, { value: label.promptSafety, enumerable: false });
|
|
2274
|
+
authenticCamelValues.add(result);
|
|
2275
|
+
return Object.freeze(result);
|
|
2276
|
+
}
|
|
2277
|
+
function copyMetadata(metadata2) {
|
|
2278
|
+
if (metadata2.provenance.length === 0) throw new CamelError("state_conflict", "Label provenance must be non-empty.");
|
|
2279
|
+
const provenance = metadata2.provenance.map(copyRef);
|
|
2280
|
+
const dependencies = (metadata2.dependencies ?? []).map((dependency) => Object.freeze({
|
|
2281
|
+
ref: copyRef(dependency.ref),
|
|
2282
|
+
influence: dependency.influence,
|
|
2283
|
+
promptSafetyAtUse: dependency.promptSafetyAtUse
|
|
2284
|
+
}));
|
|
2285
|
+
return {
|
|
2286
|
+
readers: normalizeReaders(metadata2.readers),
|
|
2287
|
+
provenance: Object.freeze(provenance),
|
|
2288
|
+
dependencies: Object.freeze(dependencies)
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
function copyRef(ref) {
|
|
2292
|
+
if (!ref.id || ref.digest !== void 0 && !ref.digest) throw new CamelError("state_conflict", "Provenance references must have non-empty identities.");
|
|
2293
|
+
return Object.freeze({ kind: ref.kind, id: ref.id, ...ref.digest === void 0 ? {} : { digest: ref.digest } });
|
|
2294
|
+
}
|
|
2295
|
+
function normalizeReaders(readers) {
|
|
2296
|
+
if (readers.kind === "public") return Object.freeze({ kind: "public" });
|
|
2297
|
+
const principalIds = [...new Set(readers.principalIds)].sort();
|
|
2298
|
+
if (principalIds.some((id) => !id)) throw new CamelError("reader_mismatch", "Reader principal IDs must be non-empty.");
|
|
2299
|
+
return Object.freeze({ kind: "principals", principalIds: Object.freeze(principalIds) });
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// ../camel/dist/code.js
|
|
2303
|
+
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
2304
|
+
var SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
2305
|
+
var ID = /^[A-Za-z0-9._:-]{1,160}$/;
|
|
2306
|
+
async function digestCodeVerificationReceipt(fields) {
|
|
2307
|
+
validate(fields);
|
|
2308
|
+
const canonical = {
|
|
2309
|
+
schemaVersion: fields.schemaVersion,
|
|
2310
|
+
verificationId: fields.verificationId,
|
|
2311
|
+
trustedBaseCommitSha: fields.trustedBaseCommitSha,
|
|
2312
|
+
trustedBaseDigest: fields.trustedBaseDigest,
|
|
2313
|
+
patchDigest: fields.patchDigest,
|
|
2314
|
+
candidateDigest: fields.candidateDigest,
|
|
2315
|
+
sourceDigest: fields.sourceDigest,
|
|
2316
|
+
policyDigest: fields.policyDigest,
|
|
2317
|
+
recipes: fields.recipes.map((recipe2) => ({
|
|
2318
|
+
recipeId: recipe2.recipeId,
|
|
2319
|
+
recipeDigest: recipe2.recipeDigest,
|
|
2320
|
+
status: recipe2.status,
|
|
2321
|
+
exitCode: recipe2.exitCode,
|
|
2322
|
+
durationMs: recipe2.durationMs,
|
|
2323
|
+
artifacts: recipe2.artifacts.map((artifact) => ({
|
|
2324
|
+
artifactId: artifact.artifactId,
|
|
2325
|
+
status: artifact.status,
|
|
2326
|
+
bytes: artifact.bytes,
|
|
2327
|
+
digest: artifact.digest
|
|
2328
|
+
}))
|
|
2329
|
+
})),
|
|
2330
|
+
changedTestCount: fields.changedTestCount,
|
|
2331
|
+
changedTestSetDigest: fields.changedTestSetDigest,
|
|
2332
|
+
changedTestsRequireReview: fields.changedTestsRequireReview,
|
|
2333
|
+
outcome: fields.outcome
|
|
2334
|
+
};
|
|
2335
|
+
return `sha256:${await sha256Hex(canonicalJson(canonical))}`;
|
|
2336
|
+
}
|
|
2337
|
+
function validate(fields) {
|
|
2338
|
+
if (fields.schemaVersion !== 1 || typeof fields.verificationId !== "string" || !ID.test(fields.verificationId) || typeof fields.trustedBaseCommitSha !== "string" || !SHA.test(fields.trustedBaseCommitSha) || !DIGEST.test(fields.trustedBaseDigest) || !DIGEST.test(fields.patchDigest) || !DIGEST.test(fields.candidateDigest) || !DIGEST.test(fields.sourceDigest) || !DIGEST.test(fields.policyDigest) || !DIGEST.test(fields.changedTestSetDigest) || !Number.isSafeInteger(fields.changedTestCount) || fields.changedTestCount < 0 || fields.changedTestCount > 1e4 || fields.changedTestsRequireReview !== fields.changedTestCount > 0 || fields.recipes.length < 1 || fields.recipes.length > 64) {
|
|
2339
|
+
throw new CamelError("state_conflict", "Code verification receipt is malformed or outside its bounds.");
|
|
2340
|
+
}
|
|
2341
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2342
|
+
for (const recipe2 of fields.recipes) {
|
|
2343
|
+
if (typeof recipe2.recipeId !== "string" || !ID.test(recipe2.recipeId) || ids.has(recipe2.recipeId) || typeof recipe2.recipeDigest !== "string" || !DIGEST.test(recipe2.recipeDigest) || !["passed", "failed", "timed_out", "output_limited"].includes(recipe2.status) || !Number.isSafeInteger(recipe2.exitCode) || recipe2.exitCode < 0 || recipe2.exitCode > 255 || !Number.isSafeInteger(recipe2.durationMs) || recipe2.durationMs < 0 || recipe2.durationMs > 30 * 6e4) {
|
|
2344
|
+
throw new CamelError("state_conflict", "Code verification recipe receipt is malformed or outside its bounds.");
|
|
2345
|
+
}
|
|
2346
|
+
const artifactIds = /* @__PURE__ */ new Set();
|
|
2347
|
+
if (recipe2.artifacts.length > 64) throw new CamelError("state_conflict", "Code verification has too many artifacts.");
|
|
2348
|
+
for (const artifact of recipe2.artifacts) {
|
|
2349
|
+
if (typeof artifact.artifactId !== "string" || !ID.test(artifact.artifactId) || artifactIds.has(artifact.artifactId) || !["verified", "missing", "invalid", "too_large"].includes(artifact.status) || artifact.bytes !== null && (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0) || artifact.digest !== null && !DIGEST.test(artifact.digest) || artifact.status === "verified" !== (artifact.bytes !== null && artifact.digest !== null) || ["missing", "invalid"].includes(artifact.status) && (artifact.bytes !== null || artifact.digest !== null) || artifact.status === "too_large" && (artifact.bytes === null || artifact.digest !== null)) {
|
|
2350
|
+
throw new CamelError("state_conflict", "Code verification artifact receipt is malformed.");
|
|
2351
|
+
}
|
|
2352
|
+
artifactIds.add(artifact.artifactId);
|
|
2353
|
+
}
|
|
2354
|
+
if (recipe2.status === "passed" && (recipe2.exitCode !== 0 || recipe2.artifacts.some((artifact) => artifact.status !== "verified"))) {
|
|
2355
|
+
throw new CamelError("state_conflict", "A passing recipe must verify every registered artifact.");
|
|
2356
|
+
}
|
|
2357
|
+
ids.add(recipe2.recipeId);
|
|
2358
|
+
}
|
|
2359
|
+
const passed = fields.recipes.every((recipe2) => recipe2.status === "passed");
|
|
2360
|
+
if (fields.outcome === "passed" !== passed) {
|
|
2361
|
+
throw new CamelError("state_conflict", "Code verification outcome does not match its recipe receipts.");
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
var SHA2 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
2365
|
+
var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
|
|
2366
|
+
var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
|
|
2367
|
+
var MAX_PATCH_BYTES = 256 * 1024;
|
|
2368
|
+
var MAX_STATE_BYTES = 64 * 1024;
|
|
2369
|
+
async function createCodePortableCheckpoint(input) {
|
|
2370
|
+
const state = normalizeState(input.state);
|
|
2371
|
+
validateBaseAndPatch(input.baseCommitSha, input.patch);
|
|
2372
|
+
const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
|
|
2373
|
+
const stateDigest = `sha256:${await sha256Hex(canonicalJson(state))}`;
|
|
2374
|
+
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
|
|
2375
|
+
const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
|
|
2376
|
+
return freeze({ ...fields, patch: input.patch, checkpointDigest });
|
|
2377
|
+
}
|
|
2378
|
+
async function verifyCodePortableCheckpoint(value) {
|
|
2379
|
+
const input = object(value, "checkpoint");
|
|
2380
|
+
exact2(input, ["schemaVersion", "baseCommitSha", "patch", "patchDigest", "state", "stateDigest", "checkpointDigest"]);
|
|
2381
|
+
if (input.schemaVersion !== 1 || typeof input.baseCommitSha !== "string" || typeof input.patch !== "string" || typeof input.patchDigest !== "string" || typeof input.stateDigest !== "string" || typeof input.checkpointDigest !== "string") {
|
|
2382
|
+
throw invalid2("Portable checkpoint fields are malformed.");
|
|
2383
|
+
}
|
|
2384
|
+
const created = await createCodePortableCheckpoint({
|
|
2385
|
+
baseCommitSha: input.baseCommitSha,
|
|
2386
|
+
patch: input.patch,
|
|
2387
|
+
state: normalizeState(input.state)
|
|
2388
|
+
});
|
|
2389
|
+
if (created.patchDigest !== input.patchDigest || created.stateDigest !== input.stateDigest || created.checkpointDigest !== input.checkpointDigest) {
|
|
2390
|
+
throw invalid2("Portable checkpoint digest does not match its content.");
|
|
2391
|
+
}
|
|
2392
|
+
return created;
|
|
2393
|
+
}
|
|
2394
|
+
function normalizeState(value) {
|
|
2395
|
+
const state = object(value, "state");
|
|
2396
|
+
exact2(state, [
|
|
2397
|
+
"planCursor",
|
|
2398
|
+
"conversationRefs",
|
|
2399
|
+
"planningInputDigest",
|
|
2400
|
+
"buildPolicyDigest",
|
|
2401
|
+
"dependencyLayerDigest",
|
|
2402
|
+
"verificationDigest",
|
|
2403
|
+
"reviewDigest",
|
|
2404
|
+
"completedEffects",
|
|
2405
|
+
"unresolvedApprovals",
|
|
2406
|
+
"trustStatus"
|
|
2407
|
+
]);
|
|
2408
|
+
const planCursor = state.planCursor;
|
|
2409
|
+
const conversations = strings(state.conversationRefs, "conversationRefs", ID2, 256, false);
|
|
2410
|
+
const approvals = strings(state.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
|
|
2411
|
+
if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state.planningInputDigest !== "string" || !DIGEST2.test(state.planningInputDigest) || typeof state.buildPolicyDigest !== "string" || !DIGEST2.test(state.buildPolicyDigest) || state.dependencyLayerDigest !== null && (typeof state.dependencyLayerDigest !== "string" || !DIGEST2.test(state.dependencyLayerDigest)) || state.verificationDigest !== null && (typeof state.verificationDigest !== "string" || !DIGEST2.test(state.verificationDigest)) || state.reviewDigest !== null && (typeof state.reviewDigest !== "string" || !DIGEST2.test(state.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state.trustStatus)) || !Array.isArray(state.completedEffects) || state.completedEffects.length > 256) {
|
|
2412
|
+
throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
|
|
2413
|
+
}
|
|
2414
|
+
const effects = state.completedEffects.map((item) => {
|
|
2415
|
+
const effect = object(item, "completed effect");
|
|
2416
|
+
exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
|
|
2417
|
+
if (typeof effect.effectId !== "string" || !ID2.test(effect.effectId) || typeof effect.actionDigest !== "string" || !DIGEST2.test(effect.actionDigest) || typeof effect.receiptDigest !== "string" || !DIGEST2.test(effect.receiptDigest)) {
|
|
2418
|
+
throw invalid2("Portable checkpoint effect receipt is malformed.");
|
|
2419
|
+
}
|
|
2420
|
+
return {
|
|
2421
|
+
effectId: effect.effectId,
|
|
2422
|
+
actionDigest: effect.actionDigest,
|
|
2423
|
+
receiptDigest: effect.receiptDigest
|
|
2424
|
+
};
|
|
2425
|
+
});
|
|
2426
|
+
if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
|
|
2427
|
+
throw invalid2("Portable checkpoint repeats a completed effect.");
|
|
2428
|
+
}
|
|
2429
|
+
const trustStatus = state.trustStatus;
|
|
2430
|
+
if (trustStatus === "candidate_untrusted" && (state.verificationDigest !== null || state.reviewDigest !== null) || trustStatus === "verified" && (state.verificationDigest === null || state.reviewDigest !== null) || trustStatus === "reviewed" && (state.verificationDigest === null || state.reviewDigest === null)) {
|
|
2431
|
+
throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
|
|
2432
|
+
}
|
|
2433
|
+
const normalized = {
|
|
2434
|
+
planCursor,
|
|
2435
|
+
conversationRefs: conversations,
|
|
2436
|
+
planningInputDigest: state.planningInputDigest,
|
|
2437
|
+
buildPolicyDigest: state.buildPolicyDigest,
|
|
2438
|
+
dependencyLayerDigest: state.dependencyLayerDigest,
|
|
2439
|
+
verificationDigest: state.verificationDigest,
|
|
2440
|
+
reviewDigest: state.reviewDigest,
|
|
2441
|
+
completedEffects: effects,
|
|
2442
|
+
unresolvedApprovals: approvals,
|
|
2443
|
+
trustStatus
|
|
2444
|
+
};
|
|
2445
|
+
if (utf8Length(normalized) > MAX_STATE_BYTES) throw invalid2("Portable checkpoint state exceeds 64 KB.");
|
|
2446
|
+
return normalized;
|
|
2447
|
+
}
|
|
2448
|
+
function validateBaseAndPatch(baseCommitSha, patch2) {
|
|
2449
|
+
if (!SHA2.test(baseCommitSha) || utf8Length(patch2) > MAX_PATCH_BYTES || patch2.includes("\0")) {
|
|
2450
|
+
throw invalid2("Portable checkpoint base or patch is malformed or outside its bounds.");
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
function strings(value, name, pattern, maximum, sort) {
|
|
2454
|
+
if (!Array.isArray(value) || value.length > maximum || value.some((item) => typeof item !== "string" || !pattern.test(item)) || new Set(value).size !== value.length) {
|
|
2455
|
+
throw invalid2(`Portable checkpoint ${name} is malformed or outside its bounds.`);
|
|
2456
|
+
}
|
|
2457
|
+
const result = [...value];
|
|
2458
|
+
return sort ? result.sort() : result;
|
|
2459
|
+
}
|
|
2460
|
+
function object(value, name) {
|
|
2461
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid2(`Portable ${name} must be an object.`);
|
|
2462
|
+
return value;
|
|
2463
|
+
}
|
|
2464
|
+
function exact2(value, keys) {
|
|
2465
|
+
if (Object.keys(value).some((key) => !keys.includes(key)) || keys.some((key) => !(key in value))) {
|
|
2466
|
+
throw invalid2("Portable checkpoint contains missing or unsupported fields.");
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
function freeze(value) {
|
|
2470
|
+
return Object.freeze({
|
|
2471
|
+
...value,
|
|
2472
|
+
state: Object.freeze({
|
|
2473
|
+
...value.state,
|
|
2474
|
+
conversationRefs: Object.freeze([...value.state.conversationRefs]),
|
|
2475
|
+
completedEffects: Object.freeze(value.state.completedEffects.map((item) => Object.freeze({ ...item }))),
|
|
2476
|
+
unresolvedApprovals: Object.freeze([...value.state.unresolvedApprovals])
|
|
2477
|
+
})
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
function invalid2(message3) {
|
|
2481
|
+
return new CamelError("state_conflict", message3);
|
|
2482
|
+
}
|
|
2483
|
+
var SHA4 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
2484
|
+
var REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
2485
|
+
var DEFAULT_LIMITS = { maximumFiles: 4e3, maximumBytes: 2 * 1024 * 1024 };
|
|
2486
|
+
async function digestCodeRepositorySnapshot(snapshot, limits = DEFAULT_LIMITS) {
|
|
2487
|
+
validateSnapshot(snapshot, limits);
|
|
2488
|
+
const normalized = {
|
|
2489
|
+
repository: snapshot.repository,
|
|
2490
|
+
commitSha: snapshot.commitSha,
|
|
2491
|
+
files: [...snapshot.files].map((file) => ({ path: file.path, content: file.content })).sort((left, right) => left.path.localeCompare(right.path))
|
|
2492
|
+
};
|
|
2493
|
+
return `sha256:${await sha256Hex(canonicalJson(normalized))}`;
|
|
2494
|
+
}
|
|
2495
|
+
function validateSnapshot(snapshot, limits) {
|
|
2496
|
+
if (!REPOSITORY.test(snapshot.repository) || !SHA4.test(snapshot.commitSha)) {
|
|
2497
|
+
throw new CamelError("state_conflict", "Code snapshot repository or commit is invalid.");
|
|
2498
|
+
}
|
|
2499
|
+
if (!Number.isSafeInteger(limits.maximumFiles) || limits.maximumFiles < 1 || !Number.isSafeInteger(limits.maximumBytes) || limits.maximumBytes < 1 || snapshot.files.length > limits.maximumFiles) {
|
|
2500
|
+
throw new CamelError("limit_exceeded", "Code snapshot exceeds its registered limits.");
|
|
2501
|
+
}
|
|
2502
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2503
|
+
let bytes = 0;
|
|
2504
|
+
for (const file of snapshot.files) {
|
|
2505
|
+
if (!file.path || file.path.startsWith("/") || file.path.includes("\\") || file.path.split("/").some((part) => !part || part === "." || part === "..") || paths.has(file.path) || typeof file.content !== "string") {
|
|
2506
|
+
throw new CamelError("state_conflict", "Code snapshot contains an invalid or duplicate path.");
|
|
2507
|
+
}
|
|
2508
|
+
paths.add(file.path);
|
|
2509
|
+
bytes += utf8Length(file.path) + utf8Length(file.content);
|
|
2510
|
+
}
|
|
2511
|
+
if (bytes > limits.maximumBytes) {
|
|
2512
|
+
throw new CamelError("limit_exceeded", "Code snapshot exceeds its registered limits.");
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// ../harness/dist/chunk-RVUUURK2.js
|
|
2517
|
+
import { spawn as spawn3 } from "child_process";
|
|
2518
|
+
import { lstat } from "fs/promises";
|
|
2519
|
+
import { resolve as resolve4, sep } from "path";
|
|
2520
|
+
import { spawn as spawn23 } from "child_process";
|
|
2521
|
+
import { getgid as getgid2, getuid as getuid2 } from "process";
|
|
2522
|
+
import { randomUUID } from "crypto";
|
|
2523
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
2524
|
+
import { createReadStream } from "fs";
|
|
2525
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
2526
|
+
import { join as join3 } from "path";
|
|
2527
|
+
import { createHash } from "crypto";
|
|
2528
|
+
import { readFile, readdir as readdir2 } from "fs/promises";
|
|
2529
|
+
import { relative as relative3, resolve as resolve22 } from "path";
|
|
2530
|
+
import { mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm2, writeFile } from "fs/promises";
|
|
2531
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2532
|
+
import { dirname as dirname4, join as join23, resolve as resolve32, sep as sep2 } from "path";
|
|
2533
|
+
import { readFile as readFile2, readdir as readdir22, stat as stat2 } from "fs/promises";
|
|
2534
|
+
import { relative as relative22, resolve as resolve42 } from "path";
|
|
2535
|
+
|
|
2536
|
+
// ../camel/dist/chunk-LAXU2AVK.js
|
|
2537
|
+
function conversionPolicyDigest(policy) {
|
|
2538
|
+
return sha256Hex(canonicalJson(policy));
|
|
2539
|
+
}
|
|
2540
|
+
function registeredIdRegistryDigest(values) {
|
|
2541
|
+
return sha256Hex(canonicalJson(values));
|
|
2542
|
+
}
|
|
2543
|
+
async function createConversionRegistry(config) {
|
|
2544
|
+
const policies = /* @__PURE__ */ new Map();
|
|
2545
|
+
for (const policy of config.policies) {
|
|
2546
|
+
validatePolicyShape(policy);
|
|
2547
|
+
if (policies.has(policy.conversionId)) throw new CamelError("state_conflict", "Conversion IDs must be unique.");
|
|
2548
|
+
const { digest: _digest, ...definition } = policy;
|
|
2549
|
+
if (await conversionPolicyDigest(definition) !== policy.digest) throw new CamelError("state_conflict", "Conversion policy digest mismatch.");
|
|
2550
|
+
if (policy.output.kind === "registered_id") {
|
|
2551
|
+
const registry = config.registeredIds?.[policy.output.registryId];
|
|
2552
|
+
const validValues = registry && Object.entries(registry.values).every(([candidate, id]) => candidate.length > 0 && typeof id === "string" && id.length > 0);
|
|
2553
|
+
if (!registry || !validValues || registry.digest !== policy.output.registryDigest || await registeredIdRegistryDigest(registry.values) !== registry.digest) {
|
|
2554
|
+
throw new CamelError("state_conflict", "Registered-ID registry digest mismatch.");
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
policies.set(policy.conversionId, Object.freeze(policy));
|
|
2558
|
+
}
|
|
2559
|
+
const outputCounts = /* @__PURE__ */ new Map();
|
|
2560
|
+
const get = (id, kind) => {
|
|
2561
|
+
const policy = policies.get(id);
|
|
2562
|
+
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
2563
|
+
return policy;
|
|
2564
|
+
};
|
|
2565
|
+
const checked = (source, id, kind) => {
|
|
2566
|
+
const policy = get(id, kind);
|
|
2567
|
+
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
2568
|
+
return policy;
|
|
2569
|
+
};
|
|
2570
|
+
const emit = (source, policy, value) => convert(source, policy, value, outputCounts);
|
|
2571
|
+
const operations = Object.freeze({
|
|
2572
|
+
boolean: async (value, id) => {
|
|
2573
|
+
const policy = checked(value, id, "boolean");
|
|
2574
|
+
return emit(value, policy, requireBoolean(value.value));
|
|
2575
|
+
},
|
|
2576
|
+
integer: async (value, id) => {
|
|
2577
|
+
const policy = checked(value, id, "integer");
|
|
2578
|
+
return emit(value, policy, boundedInteger(value.value, policy.output));
|
|
2579
|
+
},
|
|
2580
|
+
finiteNumber: async (value, id) => {
|
|
2581
|
+
const policy = checked(value, id, "finite_number");
|
|
2582
|
+
return emit(value, policy, boundedNumber(value.value, policy.output));
|
|
2583
|
+
},
|
|
2584
|
+
enum: async (value, id) => {
|
|
2585
|
+
const policy = checked(value, id, "enum");
|
|
2586
|
+
return emit(value, policy, enumMember(value.value, policy.output));
|
|
2587
|
+
},
|
|
2588
|
+
date: async (value, id) => {
|
|
2589
|
+
const policy = checked(value, id, "date");
|
|
2590
|
+
return emit(value, policy, canonicalDate(value.value, policy.output));
|
|
2591
|
+
},
|
|
2592
|
+
registeredId: async (value, id) => {
|
|
2593
|
+
const policy = checked(value, id, "registered_id");
|
|
2594
|
+
const registry = config.registeredIds?.[policy.output.registryId];
|
|
2595
|
+
const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
|
|
2596
|
+
if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
|
|
2597
|
+
return emit(value, policy, output);
|
|
2598
|
+
},
|
|
2599
|
+
digest: async (value, id) => {
|
|
2600
|
+
const policy = checked(value, id, "digest");
|
|
2601
|
+
if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
|
|
2602
|
+
return emit(value, policy, await sha256Hex(value.value));
|
|
2603
|
+
},
|
|
2604
|
+
measure: async (value, metric, id) => {
|
|
2605
|
+
const policy = checked(value, id, "integer");
|
|
2606
|
+
const measured = measure(value.value, metric);
|
|
2607
|
+
return emit(value, policy, boundedInteger(measured, policy.output));
|
|
2608
|
+
},
|
|
2609
|
+
test: async (value, predicateId, id) => {
|
|
2610
|
+
const policy = checked(value, id, "boolean");
|
|
2611
|
+
const predicate = config.predicates?.[predicateId];
|
|
2612
|
+
if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
|
|
2613
|
+
return emit(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
|
|
2617
|
+
}
|
|
2618
|
+
async function convert(source, policy, value, counts) {
|
|
2619
|
+
const sourceKey = sourceIdentity(source);
|
|
2620
|
+
const countKey = `${policy.conversionId}\0${sourceKey}`;
|
|
2621
|
+
const count = counts.get(countKey) ?? 0;
|
|
2622
|
+
if (count >= policy.maximumOutputsPerArtifact) throw new CamelError("limit_exceeded", "Conversion output count exceeds its per-source bound.");
|
|
2623
|
+
counts.set(countKey, count + 1);
|
|
2624
|
+
return createSafeInternal(value, "atomic_conversion", {
|
|
2625
|
+
readers: source.label.readers,
|
|
2626
|
+
provenance: [...source.label.provenance, { kind: "converter", id: policy.conversionId, digest: policy.digest }],
|
|
2627
|
+
dependencies: dependenciesOf([source])
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
function validatePolicyShape(policy) {
|
|
2631
|
+
if (!policy.conversionId || !Number.isSafeInteger(policy.version) || policy.version < 1 || !policy.digest) throw new CamelError("state_conflict", "Conversion policy identity is invalid.");
|
|
2632
|
+
if (!Number.isSafeInteger(policy.maximumSourceBytes) || policy.maximumSourceBytes < 0 || !Number.isSafeInteger(policy.maximumOutputsPerArtifact) || policy.maximumOutputsPerArtifact < 1) throw new CamelError("state_conflict", "Conversion policy bounds are invalid.");
|
|
2633
|
+
const output = policy.output;
|
|
2634
|
+
if ((output.kind === "integer" || output.kind === "finite_number") && (!Number.isFinite(output.minimum) || !Number.isFinite(output.maximum) || output.minimum > output.maximum)) throw new CamelError("state_conflict", "Numeric conversion bounds are invalid.");
|
|
2635
|
+
if (output.kind === "finite_number" && (!Number.isSafeInteger(output.maximumDecimalPlaces) || output.maximumDecimalPlaces < 0 || output.maximumDecimalPlaces > 15)) throw new CamelError("state_conflict", "Decimal-place bound is invalid.");
|
|
2636
|
+
if (output.kind === "enum" && (output.values.length === 0 || output.values.some((value) => typeof value !== "string" || !value) || new Set(output.values).size !== output.values.length)) throw new CamelError("state_conflict", "Enum conversion members must be unique and non-empty.");
|
|
2637
|
+
if (output.kind === "registered_id" && (!output.registryId || !output.registryDigest)) throw new CamelError("state_conflict", "Registered-ID conversion identity is invalid.");
|
|
2638
|
+
if (output.kind === "date" && output.format !== "date" && output.format !== "rfc3339") throw new CamelError("state_conflict", "Date conversion format is invalid.");
|
|
2639
|
+
if (output.kind === "digest" && output.algorithm !== "sha256") throw new CamelError("state_conflict", "Digest conversion algorithm is invalid.");
|
|
2640
|
+
}
|
|
2641
|
+
function requireBoolean(value) {
|
|
2642
|
+
if (typeof value !== "boolean") throw new CamelError("conversion_rejected", "Boolean conversion requires a structured boolean.");
|
|
2643
|
+
return value;
|
|
2644
|
+
}
|
|
2645
|
+
function boundedInteger(value, spec) {
|
|
2646
|
+
if (spec.kind !== "integer" || typeof value !== "number" || !Number.isSafeInteger(value) || value < spec.minimum || value > spec.maximum) throw new CamelError("conversion_rejected", "Integer conversion rejected the structured value.");
|
|
2647
|
+
return value;
|
|
2648
|
+
}
|
|
2649
|
+
function boundedNumber(value, spec) {
|
|
2650
|
+
if (spec.kind !== "finite_number" || typeof value !== "number" || !Number.isFinite(value) || value < spec.minimum || value > spec.maximum) throw new CamelError("conversion_rejected", "Finite-number conversion rejected the structured value.");
|
|
2651
|
+
const text = String(value);
|
|
2652
|
+
if (/e/i.test(text) || (text.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
|
|
2653
|
+
return value;
|
|
2654
|
+
}
|
|
2655
|
+
function enumMember(value, spec) {
|
|
2656
|
+
if (spec.kind !== "enum" || typeof value !== "string") throw new CamelError("conversion_rejected", "Enum conversion requires a structured string member.");
|
|
2657
|
+
const member = spec.caseSensitive ? spec.values.find((item) => item === value) : spec.values.find((item) => item.toLocaleLowerCase("en-US") === value.toLocaleLowerCase("en-US"));
|
|
2658
|
+
if (member === void 0) throw new CamelError("conversion_rejected", "Enum conversion rejected a non-member.");
|
|
2659
|
+
return member;
|
|
2660
|
+
}
|
|
2661
|
+
function canonicalDate(value, spec) {
|
|
2662
|
+
if (spec.kind !== "date" || typeof value !== "string") throw new CamelError("conversion_rejected", "Date conversion requires a canonical structured string.");
|
|
2663
|
+
const pattern = spec.format === "date" ? /^\d{4}-\d{2}-\d{2}$/ : /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
2664
|
+
const instant = Date.parse(spec.format === "date" ? `${value}T00:00:00Z` : value);
|
|
2665
|
+
if (!pattern.test(value) || !Number.isFinite(instant) || spec.format === "date" && new Date(instant).toISOString().slice(0, 10) !== value) throw new CamelError("conversion_rejected", "Date conversion rejected a non-canonical value.");
|
|
2666
|
+
if (spec.earliest && value < spec.earliest || spec.latest && value > spec.latest) throw new CamelError("conversion_rejected", "Date conversion rejected an out-of-range value.");
|
|
2667
|
+
return value;
|
|
2668
|
+
}
|
|
2669
|
+
function measure(value, metric) {
|
|
2670
|
+
if (metric === "byte_length" && (typeof value === "string" || value instanceof Uint8Array)) return utf8Length(value);
|
|
2671
|
+
if (metric === "codepoint_length" && typeof value === "string") return [...value].length;
|
|
2672
|
+
if (metric === "item_count" && Array.isArray(value)) return value.length;
|
|
2673
|
+
throw new CamelError("conversion_rejected", "Measurement input does not match the registered metric.");
|
|
2674
|
+
}
|
|
2675
|
+
function evaluatePredicate(value, predicate, registries) {
|
|
2676
|
+
if (predicate.kind === "has_fields") return typeof value === "object" && value !== null && !Array.isArray(value) && predicate.fields.every((field) => Object.hasOwn(value, field));
|
|
2677
|
+
if (predicate.kind === "registered_id") return typeof value === "string" && registries?.[predicate.registryId]?.values[value] !== void 0;
|
|
2678
|
+
const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
2679
|
+
return actual === predicate.valueType;
|
|
2680
|
+
}
|
|
2681
|
+
function sourceIdentity(source) {
|
|
2682
|
+
const artifact = [...source.label.provenance, ...source.label.dependencies.map((dependency) => dependency.ref)].find((ref2) => ref2.kind === "artifact");
|
|
2683
|
+
const fallback = source.label.provenance[0];
|
|
2684
|
+
if (!artifact && !fallback) throw new CamelError("conversion_rejected", "Conversion source has no immutable provenance.");
|
|
2685
|
+
const ref = artifact ?? fallback;
|
|
2686
|
+
return `${ref.kind}:${ref.id}:${ref.digest ?? ""}`;
|
|
2687
|
+
}
|
|
2688
|
+
function missingPolicy() {
|
|
2689
|
+
throw new CamelError("conversion_rejected", "Conversion policy is not registered.");
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
// ../camel/dist/chunk-P2WQIAG6.js
|
|
2693
|
+
function createCamelIngress(constants2 = []) {
|
|
2694
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2695
|
+
for (const item of constants2) {
|
|
2696
|
+
if (!item.id || byId.has(item.id)) throw new CamelError("state_conflict", "Control constant IDs must be unique and non-empty.");
|
|
2697
|
+
assertNoUnsafeConstant(item.value);
|
|
2698
|
+
byId.set(item.id, item);
|
|
2699
|
+
}
|
|
2700
|
+
const ingress = {
|
|
2701
|
+
userInstruction: (value, input) => createSafeInternal(value, "user_instruction", metadata("user_instruction", input.id, input.readers)),
|
|
2702
|
+
systemPolicy: (value, input) => createSafeInternal(value, "system_policy", metadata("system_policy", input.id, input.readers)),
|
|
2703
|
+
control: (id) => {
|
|
2704
|
+
const item = byId.get(id);
|
|
2705
|
+
if (!item) throw new CamelError("permission_denied", "Unknown control constant.");
|
|
2706
|
+
return createSafeInternal(item.value, "harness_constant", metadata("harness", id, item.readers));
|
|
2707
|
+
},
|
|
2708
|
+
external: (value, label) => createUnsafeInternal(value, label),
|
|
2709
|
+
quarantinedOutput: (value, input) => {
|
|
2710
|
+
if (!input.runId) throw new CamelError("state_conflict", "Quarantined run IDs must be non-empty.");
|
|
2711
|
+
return createUnsafeInternal(value, {
|
|
2712
|
+
readers: input.readers,
|
|
2713
|
+
dependencies: input.dependencies,
|
|
2714
|
+
provenance: [{ kind: "quarantined_llm", id: input.runId, digest: input.digest }]
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
return Object.freeze(ingress);
|
|
2719
|
+
}
|
|
2720
|
+
function assertNoUnsafeConstant(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2721
|
+
if (typeof value !== "object" || value === null || seen.has(value)) return;
|
|
2722
|
+
seen.add(value);
|
|
2723
|
+
if (isCamelValue(value)) {
|
|
2724
|
+
if (isUnsafe(value)) throw new CamelError("unsafe_privileged_flow", "Control constants cannot contain Prompt-Unsafe values.");
|
|
2725
|
+
assertNoUnsafeConstant(value.value, seen);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
for (const child of Object.values(value)) assertNoUnsafeConstant(child, seen);
|
|
2729
|
+
}
|
|
2730
|
+
function metadata(kind, id, readers) {
|
|
2731
|
+
if (!id) throw new CamelError("state_conflict", "Provenance IDs must be non-empty.");
|
|
2732
|
+
return { readers, provenance: [{ kind, id }] };
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
// ../camel/dist/policy.js
|
|
2736
|
+
function createEffectPolicy(config = {}) {
|
|
2737
|
+
const approvalEffects = new Set(config.approvalEffects ?? ["irreversible_mutation", "external_send", "financial", "secret_read", "code_execution"]);
|
|
2738
|
+
const registries = copyRegistries(config.destinationRegistries ?? {});
|
|
2739
|
+
return Object.freeze({ evaluate: (input) => evaluate(input, registries, approvalEffects) });
|
|
2740
|
+
}
|
|
2741
|
+
function destinationRegistryDigest(values) {
|
|
2742
|
+
return sha256Hex(canonicalJson([...values].sort()));
|
|
2743
|
+
}
|
|
2744
|
+
async function evaluate(input, registries, approvals) {
|
|
2745
|
+
const actionDigest = await sha256Hex(canonicalJson(input));
|
|
2746
|
+
const decisionId = `decision_${actionDigest.slice(0, 24)}`;
|
|
2747
|
+
const deny = (reasonCode) => ({ outcome: "deny", decisionId, reasonCode });
|
|
2748
|
+
if (!input.planId || !input.tool.name || !input.tool.policyId || !Number.isSafeInteger(input.tool.version)) return deny("invalid_descriptor");
|
|
2749
|
+
const expectedPaths = Object.keys(input.tool.argumentRoles).sort();
|
|
2750
|
+
const actualPaths = Object.keys(input.args).sort();
|
|
2751
|
+
if (expectedPaths.length !== actualPaths.length || expectedPaths.some((path, index) => path !== actualPaths[index])) return deny("argument_shape_mismatch");
|
|
2752
|
+
const confined = /* @__PURE__ */ new Set();
|
|
2753
|
+
for (const [path, arg] of Object.entries(input.args)) {
|
|
2754
|
+
if (arg.role !== input.tool.argumentRoles[path]) return deny("argument_role_mismatch");
|
|
2755
|
+
const invalid3 = await validateArgument(path, arg, input.tool, registries);
|
|
2756
|
+
if (invalid3) return deny(invalid3);
|
|
2757
|
+
if (arg.role === "selector" && !isSafe(arg.value)) confined.add(arg.value);
|
|
2758
|
+
}
|
|
2759
|
+
if (input.controlDependencies.some((value) => !isSafe(value) && !confined.has(value))) return deny("unsafe_control_dependency");
|
|
2760
|
+
if (confined.size > 0) {
|
|
2761
|
+
const fixed = input.tool.unsafeSelectorPolicy?.fixedProviderIds ?? [];
|
|
2762
|
+
const destinations = Object.values(input.args).filter((arg) => arg.role === "destination");
|
|
2763
|
+
if (destinations.length === 0 || destinations.some((arg) => !fixed.includes(arg.value.value))) return deny("unsafe_selector_provider_mismatch");
|
|
2764
|
+
}
|
|
2765
|
+
const readerValues = input.intendedReaderIds ?? [];
|
|
2766
|
+
if (readerValues.some((reader) => !isSafe(reader) || typeof reader.value !== "string" || !reader.value || !isControlOwned(reader))) return deny("invalid_reader");
|
|
2767
|
+
const readers = readerValues.map((reader) => reader.value);
|
|
2768
|
+
for (const arg of Object.values(input.args)) {
|
|
2769
|
+
if (readers.some((reader) => !canRead(arg.value.label.readers, reader))) return deny("reader_mismatch");
|
|
2770
|
+
}
|
|
2771
|
+
const hasUnsafePayload = Object.values(input.args).some((arg) => arg.role === "payload" && !isSafe(arg.value));
|
|
2772
|
+
if (hasUnsafePayload && input.tool.effect === "external_send" && readers.length === 0) return deny("reader_required");
|
|
2773
|
+
if (approvals.has(input.tool.effect)) return { outcome: "require_approval", decisionId, reasonCode: "effect_requires_approval", actionDigest };
|
|
2774
|
+
return { outcome: "allow", decisionId };
|
|
2775
|
+
}
|
|
2776
|
+
async function validateArgument(path, arg, tool, registries) {
|
|
2777
|
+
if (arg.role === "instruction" && !isSafe(arg.value)) return "unsafe_instruction_argument";
|
|
2778
|
+
if (arg.role === "authority") {
|
|
2779
|
+
if (!isSafe(arg.value)) return "unsafe_authority_argument";
|
|
2780
|
+
if (!isControlOwned(arg.value)) return "authority_not_control_owned";
|
|
2781
|
+
}
|
|
2782
|
+
if (arg.role === "destination") {
|
|
2783
|
+
if (!isSafe(arg.value)) return "unsafe_destination_argument";
|
|
2784
|
+
if (!isControlOwned(arg.value)) return "destination_not_control_owned";
|
|
2785
|
+
const registry = registries[arg.registryId];
|
|
2786
|
+
const validValues = registry && registry.values.length > 0 && registry.values.every((value) => typeof value === "string" && value.length > 0) && new Set(registry.values).size === registry.values.length;
|
|
2787
|
+
if (!registry || !validValues || registry.digest !== arg.registryDigest || await destinationRegistryDigest(registry.values) !== registry.digest || !registry.values.includes(arg.value.value)) return "destination_not_registered";
|
|
2788
|
+
}
|
|
2789
|
+
if (arg.role === "selector" && !isSafe(arg.value)) return validateUnsafeSelector(path, arg.value, tool);
|
|
2790
|
+
return void 0;
|
|
2791
|
+
}
|
|
2792
|
+
function isControlOwned(value) {
|
|
2793
|
+
return value.label.safeBasis === "system_policy" || value.label.safeBasis === "harness_constant";
|
|
2794
|
+
}
|
|
2795
|
+
function copyRegistries(registries) {
|
|
2796
|
+
return Object.freeze(Object.fromEntries(Object.entries(registries).map(([id, registry]) => [id, Object.freeze({ digest: registry.digest, values: Object.freeze([...registry.values]) })])));
|
|
2797
|
+
}
|
|
2798
|
+
function validateUnsafeSelector(path, value, tool) {
|
|
2799
|
+
const policy = tool.unsafeSelectorPolicy;
|
|
2800
|
+
if (!policy || policy.effect !== tool.effect || !policy.selectorPaths.includes(path)) return "unsafe_selector_not_confined";
|
|
2801
|
+
if (!policy.fixedDestination || !policy.unsafeOutput || policy.fixedProviderIds.length === 0) return "unsafe_selector_not_confined";
|
|
2802
|
+
if (![policy.maximumCalls, policy.maximumResultsPerCall, policy.maximumOutputBytes, policy.maximumSelectorBytes].every((bound) => Number.isSafeInteger(bound) && bound > 0)) return "unsafe_selector_not_confined";
|
|
2803
|
+
if (utf8Length(value.value) > policy.maximumSelectorBytes) return "unsafe_selector_too_large";
|
|
2804
|
+
if (typeof value.value === "string" && looksLikeDestination(value.value)) return "unsafe_selector_is_destination";
|
|
2805
|
+
return void 0;
|
|
2806
|
+
}
|
|
2807
|
+
function looksLikeDestination(value) {
|
|
2808
|
+
const text = value.trim();
|
|
2809
|
+
return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// ../harness/dist/chunk-RVUUURK2.js
|
|
2813
|
+
import { createHash as createHash3 } from "crypto";
|
|
2814
|
+
var CODE_RUNTIME_PROTOCOL_VERSION = 1;
|
|
2815
|
+
async function runCodeRuntimeHeartbeatLoop(options) {
|
|
2816
|
+
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
2817
|
+
if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
|
|
2818
|
+
throw new TypeError("heartbeatMs must be an integer from 1000 to 300000");
|
|
2819
|
+
}
|
|
2820
|
+
let retryMs = 1e3;
|
|
2821
|
+
do {
|
|
2822
|
+
if (options.signal?.aborted) return;
|
|
2823
|
+
try {
|
|
2824
|
+
const snapshot = await options.control.heartbeat(options.runtimeVersion, options.capabilities);
|
|
2825
|
+
await options.onSnapshot?.(snapshot);
|
|
2826
|
+
retryMs = 1e3;
|
|
2827
|
+
if (options.once) return;
|
|
2828
|
+
await wait(heartbeatMs, options.signal);
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
if (options.signal?.aborted) return;
|
|
2831
|
+
if (options.once || !retryableControlFailure(error)) throw error;
|
|
2832
|
+
await options.onRetry?.(error, retryMs);
|
|
2833
|
+
await wait(retryMs, options.signal);
|
|
2834
|
+
retryMs = Math.min(retryMs * 2, 3e4);
|
|
2835
|
+
}
|
|
2836
|
+
} while (!options.signal?.aborted);
|
|
2837
|
+
}
|
|
2838
|
+
var CodeRuntimeReconciler = class {
|
|
2839
|
+
constructor(control, engine) {
|
|
2840
|
+
this.control = control;
|
|
2841
|
+
this.engine = engine;
|
|
2842
|
+
}
|
|
2843
|
+
control;
|
|
2844
|
+
engine;
|
|
2845
|
+
results = /* @__PURE__ */ new Map();
|
|
2846
|
+
async reconcile(snapshot) {
|
|
2847
|
+
for (const command of snapshot.commands) {
|
|
2848
|
+
let completed = this.results.get(command.commandId);
|
|
2849
|
+
if (!completed) {
|
|
2850
|
+
let result;
|
|
2851
|
+
try {
|
|
2852
|
+
result = await this.engine.execute(command);
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
result = { status: "failed", message: (error instanceof Error ? error.message : String(error)).slice(0, 2e3) };
|
|
2855
|
+
}
|
|
2856
|
+
completed = { result, notified: false };
|
|
2857
|
+
this.results.set(command.commandId, completed);
|
|
2858
|
+
if (this.results.size > 1024) this.results.delete(this.results.keys().next().value);
|
|
2859
|
+
}
|
|
2860
|
+
await this.control.acknowledge(command.commandId, completed.result);
|
|
2861
|
+
if (!completed.notified) {
|
|
2862
|
+
await this.engine.acknowledged?.(command, completed.result);
|
|
2863
|
+
completed.notified = true;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
};
|
|
2868
|
+
function retryableControlFailure(value) {
|
|
2869
|
+
if (!value || typeof value !== "object") return false;
|
|
2870
|
+
const failure = value;
|
|
2871
|
+
if (failure.code === "invalid_response" || typeof failure.status !== "number") return false;
|
|
2872
|
+
return failure.status === 408 || failure.status === 425 || failure.status === 429 || failure.status >= 500;
|
|
2873
|
+
}
|
|
2874
|
+
function wait(ms, signal) {
|
|
2875
|
+
return new Promise((resolve52) => {
|
|
2876
|
+
if (signal?.aborted) return resolve52();
|
|
2877
|
+
const timer = setTimeout(resolve52, ms);
|
|
2878
|
+
signal?.addEventListener("abort", () => {
|
|
2879
|
+
clearTimeout(timer);
|
|
2880
|
+
resolve52();
|
|
2881
|
+
}, { once: true });
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
var CodeRuntimeControlError = class extends Error {
|
|
2885
|
+
constructor(message3, status, code = "control_error") {
|
|
2886
|
+
super(message3);
|
|
2887
|
+
this.status = status;
|
|
2888
|
+
this.code = code;
|
|
2889
|
+
}
|
|
2890
|
+
status;
|
|
2891
|
+
code;
|
|
2892
|
+
name = "CodeRuntimeControlError";
|
|
2893
|
+
};
|
|
2894
|
+
function createCodeRuntimeControlClient(options) {
|
|
2895
|
+
const endpoint = validatedEndpoint(options.endpoint);
|
|
2896
|
+
if (!/^odla_code_host_[0-9a-f]{64}$/.test(options.token)) throw new TypeError("invalid Code host credential");
|
|
2897
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
2898
|
+
if (!Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 1e3 || requestTimeoutMs > 12e4) {
|
|
2899
|
+
throw new TypeError("requestTimeoutMs must be an integer from 1000 to 120000");
|
|
2900
|
+
}
|
|
2901
|
+
const request = options.fetch ?? fetch;
|
|
2902
|
+
const call = async (path, body) => {
|
|
2903
|
+
const timeout = AbortSignal.timeout(requestTimeoutMs);
|
|
2904
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
2905
|
+
let response2;
|
|
2906
|
+
try {
|
|
2907
|
+
response2 = await request(`${endpoint}${path}`, {
|
|
2908
|
+
method: "POST",
|
|
2909
|
+
headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
|
|
2910
|
+
body: JSON.stringify(body),
|
|
2911
|
+
redirect: "error",
|
|
2912
|
+
signal
|
|
2913
|
+
});
|
|
2914
|
+
} catch (cause) {
|
|
2915
|
+
if (options.signal?.aborted) throw cause;
|
|
2916
|
+
throw new CodeRuntimeControlError("Code runtime control plane is unavailable", 503, "transport_unavailable");
|
|
2917
|
+
}
|
|
2918
|
+
const value = await response2.json().catch(() => null);
|
|
2919
|
+
if (!response2.ok) {
|
|
2920
|
+
const problem = record3(record3(value)?.error);
|
|
2921
|
+
throw new CodeRuntimeControlError(
|
|
2922
|
+
typeof problem?.message === "string" ? problem.message : `Code runtime request failed (${response2.status})`,
|
|
2923
|
+
response2.status,
|
|
2924
|
+
typeof problem?.code === "string" ? problem.code : void 0
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
return value;
|
|
2928
|
+
};
|
|
2929
|
+
return {
|
|
2930
|
+
heartbeat: async (version, capabilities) => {
|
|
2931
|
+
validateHeartbeat(version, capabilities);
|
|
2932
|
+
return parseSnapshot(await call("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
|
|
2933
|
+
},
|
|
2934
|
+
acknowledge: async (commandId, result) => {
|
|
2935
|
+
if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
|
|
2936
|
+
await call(`/registry/code/runtime/commands/${commandId}/ack`, result);
|
|
2937
|
+
},
|
|
2938
|
+
source: async (sessionId) => parseSource(
|
|
2939
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
|
|
2940
|
+
),
|
|
2941
|
+
infer: async (sessionId, inference) => {
|
|
2942
|
+
const value = record3(await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`, inference));
|
|
2943
|
+
if (!value || value.requestId !== inference.requestId || !record3(value.response) || !record3(value.receipt)) {
|
|
2944
|
+
throw new CodeRuntimeControlError("invalid Code inference response", 502, "invalid_response");
|
|
2945
|
+
}
|
|
2946
|
+
return value;
|
|
2947
|
+
},
|
|
2948
|
+
review: async (sessionId, review) => parseReview(
|
|
2949
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review)
|
|
2950
|
+
),
|
|
2951
|
+
submitCandidate: async (sessionId, checkpointId, verification) => {
|
|
2952
|
+
if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
|
|
2953
|
+
return parseCandidate(await call(
|
|
2954
|
+
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
|
|
2955
|
+
{ checkpointId, verification }
|
|
2956
|
+
));
|
|
2957
|
+
},
|
|
2958
|
+
appendSessionEvent: async (sessionId, eventId, event) => {
|
|
2959
|
+
const serialized = JSON.stringify(event);
|
|
2960
|
+
if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
|
|
2961
|
+
throw new TypeError("invalid Code session event");
|
|
2962
|
+
}
|
|
2963
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
|
|
2964
|
+
},
|
|
2965
|
+
reportSessionFailure: async (sessionId, message3) => {
|
|
2966
|
+
if (!message3.trim() || message3.length > 2e3) throw new TypeError("invalid Code session failure");
|
|
2967
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message3 });
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function validatedEndpoint(value) {
|
|
2972
|
+
const endpoint = value.replace(/\/+$/, "");
|
|
2973
|
+
let url;
|
|
2974
|
+
try {
|
|
2975
|
+
url = new URL(endpoint);
|
|
2976
|
+
} catch {
|
|
2977
|
+
throw new TypeError("endpoint must be an HTTPS URL");
|
|
2978
|
+
}
|
|
2979
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
2980
|
+
if (url.username || url.password || url.protocol !== "https:" && !(loopback && url.protocol === "http:")) {
|
|
2981
|
+
throw new TypeError("endpoint must use HTTPS (HTTP is allowed only for loopback testing)");
|
|
2982
|
+
}
|
|
2983
|
+
return endpoint;
|
|
2984
|
+
}
|
|
2985
|
+
function validSessionId(value) {
|
|
2986
|
+
if (!/^csess_[0-9a-f]{32}$/.test(value)) throw new TypeError("invalid Code session id");
|
|
2987
|
+
return value;
|
|
2988
|
+
}
|
|
2989
|
+
function validateHeartbeat(version, capabilities) {
|
|
2990
|
+
if (!version.trim() || version.length > 80) throw new TypeError("runtimeVersion is required and at most 80 characters");
|
|
2991
|
+
if (capabilities.protocolVersion !== CODE_RUNTIME_PROTOCOL_VERSION) throw new TypeError("unsupported Code runtime protocol version");
|
|
2992
|
+
if (capabilities.platform !== "macos" && capabilities.platform !== "linux") throw new TypeError("invalid runtime platform");
|
|
2993
|
+
if (!capabilities.arch || !capabilities.engines.length || !capabilities.engines.every((engine) => ["container", "podman", "docker"].includes(engine))) {
|
|
2994
|
+
throw new TypeError("runtime arch and supported engine are required");
|
|
2995
|
+
}
|
|
2996
|
+
if (!Number.isSafeInteger(capabilities.cpuCount) || capabilities.cpuCount < 1 || !Number.isSafeInteger(capabilities.memoryBytes) || capabilities.memoryBytes < 1) {
|
|
2997
|
+
throw new TypeError("runtime resources must be positive integers");
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function parseSnapshot(value) {
|
|
3001
|
+
const root = record3(value);
|
|
3002
|
+
const host = record3(root?.host);
|
|
3003
|
+
if (!host || typeof host.hostId !== "string" || typeof host.runtimeVersion !== "string" || !Number.isSafeInteger(host.lastSeenAt) || host.revokedAt !== null || !Array.isArray(root?.bindings) || root.bindings.length > 1024 || !Array.isArray(root?.commands) || root.commands.length > 64) throw invalid("heartbeat");
|
|
3004
|
+
const bindingIds = /* @__PURE__ */ new Set();
|
|
3005
|
+
const bindings = root.bindings.map((item) => {
|
|
3006
|
+
const binding = record3(item);
|
|
3007
|
+
if (!binding || typeof binding.bindingId !== "string" || typeof binding.appId !== "string" || binding.env !== "dev" && binding.env !== "prod" || typeof binding.offerId !== "string" || binding.hostId !== host.hostId || !Number.isSafeInteger(binding.generation) || Number(binding.generation) < 1 || binding.revokedAt !== null || bindingIds.has(binding.bindingId)) {
|
|
3008
|
+
throw invalid("binding");
|
|
3009
|
+
}
|
|
3010
|
+
bindingIds.add(binding.bindingId);
|
|
3011
|
+
return binding;
|
|
3012
|
+
});
|
|
3013
|
+
const commandIds = /* @__PURE__ */ new Set();
|
|
3014
|
+
const commandSequences = /* @__PURE__ */ new Set();
|
|
3015
|
+
const commands = root.commands.map((item) => {
|
|
3016
|
+
const command = record3(item);
|
|
3017
|
+
const binding = bindings.find((candidate) => candidate.bindingId === command?.bindingId);
|
|
3018
|
+
const sequenceKey = `${String(command?.instanceId)}:${String(command?.sequence)}`;
|
|
3019
|
+
if (!command || typeof command.commandId !== "string" || !/^ccmd_[0-9a-f]{32}$/.test(command.commandId) || typeof command.instanceId !== "string" || typeof command.sessionId !== "string" || !/^csess_[0-9a-f]{32}$/.test(command.sessionId) || typeof command.appId !== "string" || command.env !== "dev" && command.env !== "prod" || command.hostId !== host.hostId || !binding || binding.appId !== command.appId || binding.generation !== command.bindingGeneration || !Number.isSafeInteger(command.sequence) || Number(command.sequence) < 1 || commandIds.has(command.commandId) || commandSequences.has(sequenceKey) || !["start", "prompt", "checkpoint_stop", "resume"].includes(String(command.kind)) || !record3(command.payload) || !Number.isSafeInteger(command.createdAt)) throw invalid("command");
|
|
3020
|
+
commandIds.add(command.commandId);
|
|
3021
|
+
commandSequences.add(sequenceKey);
|
|
3022
|
+
return command;
|
|
3023
|
+
});
|
|
3024
|
+
return { host, bindings, commands };
|
|
3025
|
+
}
|
|
3026
|
+
async function parseSource(value) {
|
|
3027
|
+
const snapshot = record3(record3(value)?.snapshot);
|
|
3028
|
+
if (!snapshot || typeof snapshot.repository !== "string" || typeof snapshot.commitSha !== "string" || typeof snapshot.treeDigest !== "string" || !Array.isArray(snapshot.files)) throw invalid("source");
|
|
3029
|
+
const files = snapshot.files.map((value2) => {
|
|
3030
|
+
const file = record3(value2);
|
|
3031
|
+
if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("source file");
|
|
3032
|
+
return { path: file.path, content: file.content };
|
|
3033
|
+
});
|
|
3034
|
+
const source = { repository: snapshot.repository, commitSha: snapshot.commitSha, files };
|
|
3035
|
+
const digest2 = await digestCodeRepositorySnapshot(source, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
|
|
3036
|
+
if (digest2 !== snapshot.treeDigest) throw invalid("source digest");
|
|
3037
|
+
return { ...source, treeDigest: digest2 };
|
|
3038
|
+
}
|
|
3039
|
+
function parseReview(value) {
|
|
3040
|
+
const review = record3(record3(value)?.review);
|
|
3041
|
+
if (!review || !["approved", "rejected"].includes(String(review.verdict)) || typeof review.reviewDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(review.reviewDigest) || typeof review.provider !== "string" || !review.provider || typeof review.model !== "string" || !review.model || !Number.isSafeInteger(review.policyVersion) || Number(review.policyVersion) < 1) throw invalid("review");
|
|
3042
|
+
return review;
|
|
3043
|
+
}
|
|
3044
|
+
function parseCandidate(value) {
|
|
3045
|
+
const candidate = record3(record3(value)?.candidate);
|
|
3046
|
+
if (!candidate || typeof candidate.candidateId !== "string" || !/^ccand_[0-9a-f]{32}$/.test(candidate.candidateId) || !["submitted", "approved", "published", "failed"].includes(String(candidate.status))) {
|
|
3047
|
+
throw invalid("candidate");
|
|
3048
|
+
}
|
|
3049
|
+
return { candidateId: candidate.candidateId, status: candidate.status };
|
|
3050
|
+
}
|
|
3051
|
+
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3052
|
+
var invalid = (part) => new CodeRuntimeControlError(`invalid Code runtime ${part} response`, 502, "invalid_response");
|
|
3053
|
+
var RESERVED = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
3054
|
+
var SECRET = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
3055
|
+
var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
3056
|
+
var FORBIDDEN = /^(?:GIT binary patch|Binary files |rename (?:from|to) |copy (?:from|to) |similarity index |old mode |new mode |deleted file mode 160000|new file mode 160000)/m;
|
|
3057
|
+
function validateCodePatch(patch2, maxBytes) {
|
|
3058
|
+
if (!patch2 || Buffer.byteLength(patch2) > maxBytes || patch2.includes("\0") || patch2.includes("\r")) {
|
|
3059
|
+
throw new TypeError("patch is empty, malformed, or exceeds its byte limit");
|
|
3060
|
+
}
|
|
3061
|
+
if (FORBIDDEN.test(patch2) || /(?:old|new)(?: file)? mode 120000/.test(patch2)) {
|
|
3062
|
+
throw new TypeError("patch uses a forbidden binary, link, mode, rename, or copy operation");
|
|
3063
|
+
}
|
|
3064
|
+
const paths = [];
|
|
3065
|
+
const lines = patch2.split("\n");
|
|
3066
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3067
|
+
const line = lines[index];
|
|
3068
|
+
if (!line.startsWith("diff --git ")) continue;
|
|
3069
|
+
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line);
|
|
3070
|
+
const path = match?.[1];
|
|
3071
|
+
if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
|
|
3072
|
+
validateRelativePath(path);
|
|
3073
|
+
const header = lines.slice(index + 1).findIndex((candidate) => candidate.startsWith("diff --git "));
|
|
3074
|
+
const section = lines.slice(index + 1, header < 0 ? lines.length : index + 1 + header);
|
|
3075
|
+
const oldPath = section.find((candidate) => candidate.startsWith("--- "))?.slice(4);
|
|
3076
|
+
const newPath = section.find((candidate) => candidate.startsWith("+++ "))?.slice(4);
|
|
3077
|
+
if (!validHeaderPath(oldPath, path, "a") || !validHeaderPath(newPath, path, "b")) {
|
|
3078
|
+
throw new TypeError("patch file headers do not match the declared path");
|
|
3079
|
+
}
|
|
3080
|
+
paths.push(path);
|
|
3081
|
+
}
|
|
3082
|
+
if (!paths.length || new Set(paths).size !== paths.length) throw new TypeError("patch has no diffs or repeats a path");
|
|
3083
|
+
return paths;
|
|
3084
|
+
}
|
|
3085
|
+
function validHeaderPath(value, path, prefix) {
|
|
3086
|
+
return value === "/dev/null" || value === `${prefix}/${path}`;
|
|
3087
|
+
}
|
|
3088
|
+
function validateRelativePath(path) {
|
|
3089
|
+
const parts = path.split("/");
|
|
3090
|
+
if (!PATH.test(path) || parts.some((part) => part === "." || part === ".." || RESERVED.has(part)) || parts.some((part) => SECRET.test(part))) {
|
|
3091
|
+
throw new TypeError("path is outside the allowed staged source tree");
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
function resolveCodePath(workspaceDir, path) {
|
|
3095
|
+
validateRelativePath(path);
|
|
3096
|
+
const root = resolve4(workspaceDir);
|
|
3097
|
+
const target = resolve4(root, path);
|
|
3098
|
+
if (target !== root && !target.startsWith(`${root}${sep}`)) throw new TypeError("path escapes the staged workspace");
|
|
3099
|
+
return target;
|
|
3100
|
+
}
|
|
3101
|
+
async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
3102
|
+
await gitApply(workspaceDir, patch2, true);
|
|
3103
|
+
await gitApply(workspaceDir, patch2, false);
|
|
3104
|
+
for (const path of paths) {
|
|
3105
|
+
try {
|
|
3106
|
+
const info = await lstat(resolveCodePath(workspaceDir, path));
|
|
3107
|
+
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
3108
|
+
throw new TypeError("patch created a non-regular workspace entry");
|
|
3109
|
+
}
|
|
3110
|
+
} catch (reason) {
|
|
3111
|
+
if (reason.code !== "ENOENT") throw reason;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
function gitApply(cwd, patch2, check) {
|
|
3116
|
+
return new Promise((accept, reject) => {
|
|
3117
|
+
const args = ["apply", "--recount", "--whitespace=nowarn", ...check ? ["--check"] : [], "-"];
|
|
3118
|
+
const child = spawn3("git", args, {
|
|
3119
|
+
cwd,
|
|
3120
|
+
shell: false,
|
|
3121
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
3122
|
+
env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
|
|
3123
|
+
});
|
|
3124
|
+
let stderr = "";
|
|
3125
|
+
child.stderr.setEncoding("utf8");
|
|
3126
|
+
child.stderr.on("data", (text) => {
|
|
3127
|
+
if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
|
|
3128
|
+
});
|
|
3129
|
+
child.once("error", reject);
|
|
3130
|
+
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
|
|
3131
|
+
child.stdin.end(patch2);
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
async function createCodeWorkspaceCheckpoint(input) {
|
|
3135
|
+
const maximum = input.maximumPatchBytes ?? 256 * 1024;
|
|
3136
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 256 * 1024) {
|
|
3137
|
+
throw new TypeError("checkpoint patch bound must be from 1 to 262144 bytes");
|
|
3138
|
+
}
|
|
3139
|
+
const patch2 = await input.workspace.patch(maximum);
|
|
3140
|
+
if (patch2) validateCodePatch(patch2, maximum);
|
|
3141
|
+
return createCodePortableCheckpoint({ baseCommitSha: input.baseCommitSha, patch: patch2, state: input.state });
|
|
3142
|
+
}
|
|
3143
|
+
async function restoreCodeWorkspaceCheckpoint(input) {
|
|
3144
|
+
const checkpoint = await verifyCodePortableCheckpoint(input.checkpoint);
|
|
3145
|
+
if (checkpoint.baseCommitSha !== input.trustedBaseCommitSha) {
|
|
3146
|
+
throw new TypeError("checkpoint trusted base does not match the fetched commit");
|
|
3147
|
+
}
|
|
3148
|
+
const workspace = await stageWorkspace(input.trustedBaseDir, input.stage);
|
|
3149
|
+
try {
|
|
3150
|
+
if (checkpoint.patch) {
|
|
3151
|
+
const paths = validateCodePatch(checkpoint.patch, 256 * 1024);
|
|
3152
|
+
await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths);
|
|
3153
|
+
}
|
|
3154
|
+
return { workspace, checkpoint };
|
|
3155
|
+
} catch (error) {
|
|
3156
|
+
await workspace.cleanup();
|
|
3157
|
+
throw error;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
var ARTIFACT_PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
3161
|
+
var PRIVATE_ARTIFACT_PART = /^(?:\.git|\.odla|\.wrangler|\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json)$/i;
|
|
3162
|
+
function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${randomUUID().slice(0, 12)}`) {
|
|
3163
|
+
assertCodeBuildRecipe(recipe2);
|
|
3164
|
+
if (/[,\r\n]/.test(workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
|
|
3165
|
+
const uid = typeof getuid2 === "function" ? getuid2() : 1e3;
|
|
3166
|
+
const gid = typeof getgid2 === "function" ? getgid2() : 1e3;
|
|
3167
|
+
const limits = {
|
|
3168
|
+
cpus: recipe2.cpus ?? 1,
|
|
3169
|
+
memory: recipe2.memory ?? "1g",
|
|
3170
|
+
pids: recipe2.pids ?? 256,
|
|
3171
|
+
tmpfs: recipe2.tmpfsBytes ?? 64 * 1024 * 1024
|
|
3172
|
+
};
|
|
3173
|
+
if (engine === "container") {
|
|
3174
|
+
return [
|
|
3175
|
+
"run",
|
|
3176
|
+
"--rm",
|
|
3177
|
+
`--name=${name}`,
|
|
3178
|
+
"--network=none",
|
|
3179
|
+
"--read-only",
|
|
3180
|
+
"--cap-drop=ALL",
|
|
3181
|
+
`--memory=${limits.memory}`,
|
|
3182
|
+
`--cpus=${limits.cpus}`,
|
|
3183
|
+
`--user=${uid}:${gid}`,
|
|
3184
|
+
"--tmpfs=/tmp",
|
|
3185
|
+
`--mount=type=bind,source=${workspaceDir},target=/workspace`,
|
|
3186
|
+
"--workdir=/workspace",
|
|
3187
|
+
"--env=CI=1",
|
|
3188
|
+
recipe2.image,
|
|
3189
|
+
...recipe2.command
|
|
3190
|
+
];
|
|
3191
|
+
}
|
|
3192
|
+
return [
|
|
3193
|
+
"run",
|
|
3194
|
+
"--rm",
|
|
3195
|
+
`--name=${name}`,
|
|
3196
|
+
"--pull=never",
|
|
3197
|
+
"--network=none",
|
|
3198
|
+
"--read-only",
|
|
3199
|
+
"--cap-drop=ALL",
|
|
3200
|
+
"--security-opt=no-new-privileges",
|
|
3201
|
+
`--pids-limit=${limits.pids}`,
|
|
3202
|
+
`--memory=${limits.memory}`,
|
|
3203
|
+
`--cpus=${limits.cpus}`,
|
|
3204
|
+
`--user=${uid}:${gid}`,
|
|
3205
|
+
`--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=${limits.tmpfs}`,
|
|
3206
|
+
`--mount=type=bind,src=${workspaceDir},dst=/workspace`,
|
|
3207
|
+
"--workdir=/workspace",
|
|
3208
|
+
"--env=CI=1",
|
|
3209
|
+
recipe2.image,
|
|
3210
|
+
...recipe2.command
|
|
3211
|
+
];
|
|
3212
|
+
}
|
|
3213
|
+
function createContainerRecipeExecutor(engine) {
|
|
3214
|
+
return {
|
|
3215
|
+
async run(input) {
|
|
3216
|
+
await verifyContainerEngineBoundary(engine);
|
|
3217
|
+
const name = `odla-recipe-${randomUUID().slice(0, 12)}`;
|
|
3218
|
+
const args = buildRecipeContainerArgs(engine, input.workspaceDir, input.recipe, name);
|
|
3219
|
+
return execute(engine, args, name, input.recipe, input.signal);
|
|
3220
|
+
}
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
function assertCodeBuildRecipe(recipe2) {
|
|
3224
|
+
const memoryBytes = parseMemory(recipe2.memory ?? "1g");
|
|
3225
|
+
const tmpfsBytes = recipe2.tmpfsBytes ?? 64 * 1024 * 1024;
|
|
3226
|
+
const artifacts = recipe2.expectedArtifacts ?? [];
|
|
3227
|
+
assertPinnedImage(recipe2.image);
|
|
3228
|
+
if (!/^[A-Za-z0-9._:-]{1,120}$/.test(recipe2.id) || recipe2.command.length < 1 || recipe2.command.length > 64 || recipe2.command.some((part) => !part || part.length > 4096 || /[\0\r\n]/.test(part)) || !Number.isSafeInteger(recipe2.timeoutMs) || recipe2.timeoutMs < 1 || recipe2.timeoutMs > 30 * 6e4 || !Number.isSafeInteger(recipe2.maxOutputBytes) || recipe2.maxOutputBytes < 1 || recipe2.maxOutputBytes > 16 * 1024 * 1024 || !Number.isFinite(recipe2.cpus ?? 1) || (recipe2.cpus ?? 1) < 0.1 || (recipe2.cpus ?? 1) > 32 || memoryBytes < 64 * 1024 * 1024 || memoryBytes > 32 * 1024 * 1024 * 1024 || !Number.isSafeInteger(recipe2.pids ?? 256) || (recipe2.pids ?? 256) < 16 || (recipe2.pids ?? 256) > 4096 || !Number.isSafeInteger(tmpfsBytes) || tmpfsBytes < 1024 * 1024 || tmpfsBytes > 1024 * 1024 * 1024 || artifacts.length > 64 || new Set(artifacts.map((item) => item.id)).size !== artifacts.length || artifacts.some((item) => !/^[A-Za-z0-9._:-]{1,120}$/.test(item.id) || !ARTIFACT_PATH.test(item.path) || item.path.split("/").some((part) => PRIVATE_ARTIFACT_PART.test(part)) || !Number.isSafeInteger(item.maximumBytes) || item.maximumBytes < 1 || item.maximumBytes > 512 * 1024 * 1024)) {
|
|
3229
|
+
throw new TypeError("build recipe is malformed or exceeds its control bounds");
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
function parseMemory(value) {
|
|
3233
|
+
const match = /^([1-9][0-9]{0,4})([kmg])$/.exec(value.toLowerCase());
|
|
3234
|
+
if (!match?.[1] || !match[2]) return 0;
|
|
3235
|
+
const scale = match[2] === "k" ? 1024 : match[2] === "m" ? 1024 ** 2 : 1024 ** 3;
|
|
3236
|
+
return Number(match[1]) * scale;
|
|
3237
|
+
}
|
|
3238
|
+
function execute(engine, args, name, recipe2, signal) {
|
|
3239
|
+
return new Promise((accept, reject) => {
|
|
3240
|
+
const started = Date.now();
|
|
3241
|
+
const child = spawn23(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
3242
|
+
const stdout = [];
|
|
3243
|
+
const stderr = [];
|
|
3244
|
+
let bytes = 0;
|
|
3245
|
+
let outputLimitExceeded = false;
|
|
3246
|
+
let timedOut = false;
|
|
3247
|
+
let stopping = false;
|
|
3248
|
+
const stop = (reason) => {
|
|
3249
|
+
if (stopping) return;
|
|
3250
|
+
stopping = true;
|
|
3251
|
+
timedOut = reason === "timeout";
|
|
3252
|
+
outputLimitExceeded = reason === "output";
|
|
3253
|
+
const remove = engine === "container" ? ["delete", "--force", name] : ["rm", "-f", name];
|
|
3254
|
+
const killer = spawn23(engine, remove, { shell: false, stdio: "ignore" });
|
|
3255
|
+
killer.unref();
|
|
3256
|
+
child.kill("SIGTERM");
|
|
3257
|
+
};
|
|
3258
|
+
const collect = (target) => (chunk) => {
|
|
3259
|
+
bytes += chunk.byteLength;
|
|
3260
|
+
if (bytes > recipe2.maxOutputBytes) stop("output");
|
|
3261
|
+
else target.push(chunk);
|
|
3262
|
+
};
|
|
3263
|
+
child.stdout.on("data", collect(stdout));
|
|
3264
|
+
child.stderr.on("data", collect(stderr));
|
|
3265
|
+
const abort = () => stop("abort");
|
|
3266
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3267
|
+
if (signal?.aborted) abort();
|
|
3268
|
+
const timer = setTimeout(() => stop("timeout"), recipe2.timeoutMs);
|
|
3269
|
+
child.once("error", (error) => {
|
|
3270
|
+
clearTimeout(timer);
|
|
3271
|
+
signal?.removeEventListener("abort", abort);
|
|
3272
|
+
reject(error);
|
|
3273
|
+
});
|
|
3274
|
+
child.once("exit", (code) => {
|
|
3275
|
+
clearTimeout(timer);
|
|
3276
|
+
signal?.removeEventListener("abort", abort);
|
|
3277
|
+
accept({
|
|
3278
|
+
exitCode: code ?? 1,
|
|
3279
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
3280
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
3281
|
+
durationMs: Date.now() - started,
|
|
3282
|
+
outputLimitExceeded,
|
|
3283
|
+
timedOut
|
|
3284
|
+
});
|
|
3285
|
+
});
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
async function digestStagedWorkspace(root, limits) {
|
|
3289
|
+
const files = [];
|
|
3290
|
+
const walk = async (directory) => {
|
|
3291
|
+
const entries = await readdir2(directory, { withFileTypes: true });
|
|
3292
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
3293
|
+
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
3294
|
+
const target = resolve22(directory, entry.name);
|
|
3295
|
+
if (entry.isDirectory()) await walk(target);
|
|
3296
|
+
else if (entry.isFile()) {
|
|
3297
|
+
files.push({ path: relative3(root, target).split("\\").join("/"), target });
|
|
3298
|
+
if (files.length > limits.maxFiles) throw new TypeError("workspace digest exceeds its file bound");
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
};
|
|
3302
|
+
await walk(resolve22(root));
|
|
3303
|
+
const hash = createHash("sha256");
|
|
3304
|
+
let bytes = 0;
|
|
3305
|
+
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
3306
|
+
const content = await readFile(file.target);
|
|
3307
|
+
bytes += Buffer.byteLength(file.path) + content.byteLength;
|
|
3308
|
+
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
3309
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
|
|
3310
|
+
hash.update(content);
|
|
3311
|
+
}
|
|
3312
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3313
|
+
}
|
|
3314
|
+
var SHA3 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
3315
|
+
var DIGEST3 = /^sha256:[0-9a-f]{64}$/;
|
|
3316
|
+
var ID3 = /^[A-Za-z0-9._:-]{1,160}$/;
|
|
3317
|
+
var RULE = /^[A-Za-z0-9_@+.,/-]{1,160}$/;
|
|
3318
|
+
var DEFAULT_PREFIXES = ["test/", "tests/", "__tests__/"];
|
|
3319
|
+
var DEFAULT_SUFFIXES = [".test.js", ".test.ts", ".test.tsx", ".spec.js", ".spec.ts", ".spec.tsx"];
|
|
3320
|
+
async function verifyCodeCandidate(input) {
|
|
3321
|
+
const policy = validate2(input);
|
|
3322
|
+
const limits = { maxFiles: policy.maximumFiles, maxBytes: policy.maximumBytes };
|
|
3323
|
+
const staged = await stageWorkspace(input.trustedBaseDir, limits);
|
|
3324
|
+
try {
|
|
3325
|
+
const baseDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
|
|
3326
|
+
if (baseDigest !== input.trustedBaseDigest) throw new TypeError("trusted base does not match its registered digest");
|
|
3327
|
+
const paths = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
|
|
3328
|
+
await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths);
|
|
3329
|
+
const sourceDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
|
|
3330
|
+
const policyDigest = digestPolicy(policy);
|
|
3331
|
+
const patchDigest = digestBytes(input.candidatePatch);
|
|
3332
|
+
const candidateDigest = digestJson({
|
|
3333
|
+
trustedBaseCommitSha: input.trustedBaseCommitSha,
|
|
3334
|
+
trustedBaseDigest: input.trustedBaseDigest,
|
|
3335
|
+
patchDigest
|
|
3336
|
+
});
|
|
3337
|
+
const changedTests = changedTestPaths(paths, policy);
|
|
3338
|
+
if (changedTests.length > policy.maximumChangedTests) throw new TypeError("candidate changes too many test files");
|
|
3339
|
+
const recipes = [];
|
|
3340
|
+
const logs = [];
|
|
3341
|
+
for (const recipe2 of policy.recipes) {
|
|
3342
|
+
const clean = await stageWorkspace(staged.workspaceDir, limits);
|
|
3343
|
+
try {
|
|
3344
|
+
if (await digestStagedWorkspace(clean.workspaceDir, limits) !== sourceDigest) {
|
|
3345
|
+
throw new TypeError("clean verifier source changed before execution");
|
|
3346
|
+
}
|
|
3347
|
+
const result = checkedResult(await input.recipeExecutor.run({
|
|
3348
|
+
workspaceDir: clean.workspaceDir,
|
|
3349
|
+
recipe: recipe2,
|
|
3350
|
+
signal: input.signal
|
|
3351
|
+
}), recipe2.maxOutputBytes);
|
|
3352
|
+
const artifacts = await inspectArtifacts(clean.workspaceDir, recipe2);
|
|
3353
|
+
recipes.push(recipeReceipt(recipe2, result, artifacts));
|
|
3354
|
+
logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
|
|
3355
|
+
} finally {
|
|
3356
|
+
await clean.cleanup();
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
const fields = {
|
|
3360
|
+
schemaVersion: 1,
|
|
3361
|
+
verificationId: input.verificationId ?? `verify-${randomUUID2()}`,
|
|
3362
|
+
trustedBaseCommitSha: input.trustedBaseCommitSha,
|
|
3363
|
+
trustedBaseDigest: input.trustedBaseDigest,
|
|
3364
|
+
patchDigest,
|
|
3365
|
+
candidateDigest,
|
|
3366
|
+
sourceDigest,
|
|
3367
|
+
policyDigest,
|
|
3368
|
+
recipes,
|
|
3369
|
+
changedTestCount: changedTests.length,
|
|
3370
|
+
changedTestSetDigest: digestJson(changedTests),
|
|
3371
|
+
changedTestsRequireReview: changedTests.length > 0,
|
|
3372
|
+
outcome: recipes.every((recipe2) => recipe2.status === "passed") ? "passed" : "failed"
|
|
3373
|
+
};
|
|
3374
|
+
return {
|
|
3375
|
+
receipt: { ...fields, receiptDigest: await digestCodeVerificationReceipt(fields) },
|
|
3376
|
+
changedTests: Object.freeze(changedTests),
|
|
3377
|
+
logs: Object.freeze(logs)
|
|
3378
|
+
};
|
|
3379
|
+
} finally {
|
|
3380
|
+
await staged.cleanup();
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
function validate2(input) {
|
|
3384
|
+
const policy = input.policy;
|
|
3385
|
+
if (!SHA3.test(input.trustedBaseCommitSha) || !DIGEST3.test(input.trustedBaseDigest) || !ID3.test(input.verificationId ?? "verify-generated") || !ID3.test(policy.policyId) || policy.recipes.length < 1 || policy.recipes.length > 64 || new Set(policy.recipes.map((recipe2) => recipe2.id)).size !== policy.recipes.length) {
|
|
3386
|
+
throw new TypeError("clean verification input is malformed");
|
|
3387
|
+
}
|
|
3388
|
+
for (const recipe2 of policy.recipes) assertCodeBuildRecipe(recipe2);
|
|
3389
|
+
const result = {
|
|
3390
|
+
policyId: policy.policyId,
|
|
3391
|
+
recipes: policy.recipes,
|
|
3392
|
+
testPathPrefixes: policy.testPathPrefixes ?? DEFAULT_PREFIXES,
|
|
3393
|
+
testPathSuffixes: policy.testPathSuffixes ?? DEFAULT_SUFFIXES,
|
|
3394
|
+
maximumChangedTests: policy.maximumChangedTests ?? 1e3,
|
|
3395
|
+
maximumPatchBytes: policy.maximumPatchBytes ?? 256 * 1024,
|
|
3396
|
+
maximumFiles: policy.maximumFiles ?? 2e4,
|
|
3397
|
+
maximumBytes: policy.maximumBytes ?? 512 * 1024 * 1024
|
|
3398
|
+
};
|
|
3399
|
+
if ([...result.testPathPrefixes, ...result.testPathSuffixes].some((rule) => !RULE.test(rule)) || !integer(result.maximumChangedTests, 0, 1e4) || !integer(result.maximumPatchBytes, 1, 4 * 1024 * 1024) || !integer(result.maximumFiles, 1, 1e5) || !integer(result.maximumBytes, 1, 2 * 1024 * 1024 * 1024)) {
|
|
3400
|
+
throw new TypeError("clean verification policy exceeds its bounds");
|
|
3401
|
+
}
|
|
3402
|
+
return result;
|
|
3403
|
+
}
|
|
3404
|
+
function changedTestPaths(paths, policy) {
|
|
3405
|
+
return paths.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
|
|
3406
|
+
}
|
|
3407
|
+
function recipeReceipt(recipe2, result, artifacts) {
|
|
3408
|
+
const status = result.timedOut ? "timed_out" : result.outputLimitExceeded ? "output_limited" : result.exitCode === 0 && artifacts.every((item) => item.status === "verified") ? "passed" : "failed";
|
|
3409
|
+
return {
|
|
3410
|
+
recipeId: recipe2.id,
|
|
3411
|
+
recipeDigest: digestRecipe(recipe2),
|
|
3412
|
+
status,
|
|
3413
|
+
exitCode: result.exitCode,
|
|
3414
|
+
durationMs: result.durationMs,
|
|
3415
|
+
artifacts
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
async function inspectArtifacts(workspaceDir, recipe2) {
|
|
3419
|
+
const receipts = [];
|
|
3420
|
+
for (const artifact of recipe2.expectedArtifacts ?? []) {
|
|
3421
|
+
try {
|
|
3422
|
+
const path = join3(workspaceDir, artifact.path);
|
|
3423
|
+
const info = await lstat2(path);
|
|
3424
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
3425
|
+
receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
|
|
3426
|
+
} else if (info.size > artifact.maximumBytes) {
|
|
3427
|
+
receipts.push({ artifactId: artifact.id, status: "too_large", bytes: info.size, digest: null });
|
|
3428
|
+
} else {
|
|
3429
|
+
receipts.push({ artifactId: artifact.id, status: "verified", bytes: info.size, digest: await hashFile(path) });
|
|
3430
|
+
}
|
|
3431
|
+
} catch (reason) {
|
|
3432
|
+
if (reason.code !== "ENOENT") throw reason;
|
|
3433
|
+
receipts.push({ artifactId: artifact.id, status: "missing", bytes: null, digest: null });
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
return receipts;
|
|
3437
|
+
}
|
|
3438
|
+
function hashFile(path) {
|
|
3439
|
+
return new Promise((accept, reject) => {
|
|
3440
|
+
const hash = createHash2("sha256");
|
|
3441
|
+
const stream = createReadStream(path);
|
|
3442
|
+
stream.on("data", (chunk) => {
|
|
3443
|
+
hash.update(chunk);
|
|
3444
|
+
});
|
|
3445
|
+
stream.once("error", reject);
|
|
3446
|
+
stream.once("end", () => accept(`sha256:${hash.digest("hex")}`));
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
function checkedResult(result, maximumOutputBytes) {
|
|
3450
|
+
if (!integer(result.exitCode, 0, 255) || !integer(result.durationMs, 0, 30 * 6e4) || typeof result.stdout !== "string" || typeof result.stderr !== "string" || typeof result.outputLimitExceeded !== "boolean" || typeof result.timedOut !== "boolean") {
|
|
3451
|
+
throw new TypeError("recipe executor returned an invalid result");
|
|
3452
|
+
}
|
|
3453
|
+
const bytes = Buffer.byteLength(result.stdout) + Buffer.byteLength(result.stderr);
|
|
3454
|
+
return bytes > maximumOutputBytes ? { ...result, outputLimitExceeded: true } : result;
|
|
3455
|
+
}
|
|
3456
|
+
function boundedLogs(result, maximum) {
|
|
3457
|
+
const stdout = Buffer.from(result.stdout);
|
|
3458
|
+
const stderr = Buffer.from(result.stderr);
|
|
3459
|
+
const first = stdout.subarray(0, maximum);
|
|
3460
|
+
return {
|
|
3461
|
+
stdout: first.toString("utf8"),
|
|
3462
|
+
stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
function digestPolicy(policy) {
|
|
3466
|
+
return digestJson({
|
|
3467
|
+
policyId: policy.policyId,
|
|
3468
|
+
recipes: policy.recipes.map((recipe2) => normalizedRecipe(recipe2)),
|
|
3469
|
+
testPathPrefixes: [...policy.testPathPrefixes].sort(),
|
|
3470
|
+
testPathSuffixes: [...policy.testPathSuffixes].sort(),
|
|
3471
|
+
maximumChangedTests: policy.maximumChangedTests,
|
|
3472
|
+
maximumPatchBytes: policy.maximumPatchBytes,
|
|
3473
|
+
maximumFiles: policy.maximumFiles,
|
|
3474
|
+
maximumBytes: policy.maximumBytes
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
function digestRecipe(recipe2) {
|
|
3478
|
+
return digestJson(normalizedRecipe(recipe2));
|
|
3479
|
+
}
|
|
3480
|
+
function normalizedRecipe(recipe2) {
|
|
3481
|
+
return {
|
|
3482
|
+
id: recipe2.id,
|
|
3483
|
+
image: recipe2.image,
|
|
3484
|
+
command: [...recipe2.command],
|
|
3485
|
+
timeoutMs: recipe2.timeoutMs,
|
|
3486
|
+
maxOutputBytes: recipe2.maxOutputBytes,
|
|
3487
|
+
cpus: recipe2.cpus ?? 1,
|
|
3488
|
+
memory: recipe2.memory ?? "1g",
|
|
3489
|
+
pids: recipe2.pids ?? 256,
|
|
3490
|
+
tmpfsBytes: recipe2.tmpfsBytes ?? 64 * 1024 * 1024,
|
|
3491
|
+
expectedArtifacts: [...recipe2.expectedArtifacts ?? []].sort((left, right) => left.id.localeCompare(right.id)).map((artifact) => ({ id: artifact.id, path: artifact.path, maximumBytes: artifact.maximumBytes }))
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
function digestJson(value) {
|
|
3495
|
+
return digestBytes(JSON.stringify(value));
|
|
3496
|
+
}
|
|
3497
|
+
function digestBytes(value) {
|
|
3498
|
+
return `sha256:${createHash2("sha256").update(value).digest("hex")}`;
|
|
3499
|
+
}
|
|
3500
|
+
function integer(value, minimum, maximum) {
|
|
3501
|
+
return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
|
|
3502
|
+
}
|
|
3503
|
+
async function prepareRuntimeCheckpoint(input) {
|
|
3504
|
+
const patch2 = await input.workspace.patch(256 * 1024);
|
|
3505
|
+
let verification = null;
|
|
3506
|
+
let review = null;
|
|
3507
|
+
let note = patch2 ? "Candidate remains untrusted" : "Checkpoint has no source changes";
|
|
3508
|
+
if (patch2 && input.role === "coding") {
|
|
3509
|
+
try {
|
|
3510
|
+
const evidence = await verifyCodeCandidate({
|
|
3511
|
+
verificationId: `verify-${input.sessionId.slice("csess_".length)}`,
|
|
3512
|
+
trustedBaseDir: input.workspace.baselineDir,
|
|
3513
|
+
trustedBaseCommitSha: input.baseCommitSha,
|
|
3514
|
+
trustedBaseDigest: input.trustedBaseDigest,
|
|
3515
|
+
candidatePatch: patch2,
|
|
3516
|
+
policy: {
|
|
3517
|
+
policyId: "code.runtime",
|
|
3518
|
+
recipes: input.recipes,
|
|
3519
|
+
maximumFiles: 1e4,
|
|
3520
|
+
maximumBytes: 16 * 1024 * 1024
|
|
3521
|
+
},
|
|
3522
|
+
recipeExecutor: input.recipeExecutor
|
|
3523
|
+
});
|
|
3524
|
+
if (evidence.receipt.outcome === "passed") {
|
|
3525
|
+
verification = evidence.receipt;
|
|
3526
|
+
review = await input.review(patch2, verification);
|
|
3527
|
+
note = review.verdict === "approved" ? "Clean verification and independent review passed" : "Clean verification passed; independent review rejected the candidate";
|
|
3528
|
+
} else {
|
|
3529
|
+
note = `Clean verification failed: ${evidence.receipt.recipes.filter((recipe2) => recipe2.status !== "passed").map((recipe2) => `${recipe2.recipeId}=${recipe2.status}`).join(", ")}`;
|
|
3530
|
+
}
|
|
3531
|
+
} catch (cause) {
|
|
3532
|
+
note = `Candidate verification or review failed closed: ${message(cause)}`;
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
const reviewed = verification && review?.verdict === "approved";
|
|
3536
|
+
const checkpoint = await createCodeWorkspaceCheckpoint({
|
|
3537
|
+
workspace: input.workspace,
|
|
3538
|
+
baseCommitSha: input.baseCommitSha,
|
|
3539
|
+
state: {
|
|
3540
|
+
planCursor: null,
|
|
3541
|
+
conversationRefs: input.conversationRefs,
|
|
3542
|
+
planningInputDigest: input.planningInputDigest,
|
|
3543
|
+
buildPolicyDigest: verification?.policyDigest ?? input.fallbackPolicyDigest,
|
|
3544
|
+
dependencyLayerDigest: null,
|
|
3545
|
+
verificationDigest: verification?.receiptDigest ?? null,
|
|
3546
|
+
reviewDigest: reviewed && review ? review.reviewDigest : null,
|
|
3547
|
+
completedEffects: [],
|
|
3548
|
+
unresolvedApprovals: [],
|
|
3549
|
+
trustStatus: reviewed ? "reviewed" : verification ? "verified" : "candidate_untrusted"
|
|
3550
|
+
}
|
|
3551
|
+
});
|
|
3552
|
+
return { checkpoint, verification, review, note };
|
|
3553
|
+
}
|
|
3554
|
+
var message = (value) => (value instanceof Error ? value.message : String(value)).slice(0, 500);
|
|
3555
|
+
var CodeRuntimeCheckpointManager = class {
|
|
3556
|
+
constructor(options) {
|
|
3557
|
+
this.options = options;
|
|
3558
|
+
}
|
|
3559
|
+
options;
|
|
3560
|
+
#pending = /* @__PURE__ */ new Map();
|
|
3561
|
+
async prepare(command, active) {
|
|
3562
|
+
active.abort.abort("checkpoint_stop");
|
|
3563
|
+
await active.done;
|
|
3564
|
+
const prepared = await prepareRuntimeCheckpoint({
|
|
3565
|
+
sessionId: command.sessionId,
|
|
3566
|
+
role: active.role,
|
|
3567
|
+
workspace: active.workspace,
|
|
3568
|
+
baseCommitSha: active.baseCommitSha,
|
|
3569
|
+
trustedBaseDigest: active.trustedBaseDigest,
|
|
3570
|
+
planningInputDigest: active.planningInputDigest,
|
|
3571
|
+
conversationRefs: active.conversationRefs,
|
|
3572
|
+
fallbackPolicyDigest: this.options.fallbackPolicyDigest,
|
|
3573
|
+
recipes: this.options.recipes,
|
|
3574
|
+
recipeExecutor: this.options.recipeExecutor,
|
|
3575
|
+
review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
|
|
3576
|
+
});
|
|
3577
|
+
if (prepared.review?.verdict === "approved" && prepared.verification) {
|
|
3578
|
+
this.#pending.set(command.commandId, { verification: prepared.verification, refs: active.conversationRefs });
|
|
3579
|
+
}
|
|
3580
|
+
await this.options.event(command, { type: "message", actor: "system", body: prepared.note }, active.conversationRefs).catch(() => void 0);
|
|
3581
|
+
await active.workspace.cleanup();
|
|
3582
|
+
await this.options.event(command, { type: "status", status: "checkpointed" }, active.conversationRefs).catch(() => void 0);
|
|
3583
|
+
return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "Pi stopped at a portable checkpoint" };
|
|
3584
|
+
}
|
|
3585
|
+
async acknowledged(command, result) {
|
|
3586
|
+
if (command.kind !== "checkpoint_stop" || result.status !== "checkpointed") return false;
|
|
3587
|
+
const pending = this.#pending.get(command.commandId);
|
|
3588
|
+
if (!pending) return true;
|
|
3589
|
+
const checkpointId = `cpoint_${command.commandId.slice("ccmd_".length)}`;
|
|
3590
|
+
const candidate = await this.options.control.submitCandidate(command.sessionId, checkpointId, pending.verification);
|
|
3591
|
+
await this.options.event(command, {
|
|
3592
|
+
type: "message",
|
|
3593
|
+
actor: "system",
|
|
3594
|
+
body: `Candidate ${candidate.candidateId} is ready for exact owner publication approval`
|
|
3595
|
+
}, pending.refs);
|
|
3596
|
+
this.#pending.delete(command.commandId);
|
|
3597
|
+
return true;
|
|
3598
|
+
}
|
|
3599
|
+
};
|
|
3600
|
+
var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
3601
|
+
var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
3602
|
+
async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir2()) {
|
|
3603
|
+
if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
|
|
3604
|
+
const root = await mkdtemp2(join23(tempRoot, "odla-code-source-"));
|
|
3605
|
+
const sourceDir = join23(root, "source");
|
|
3606
|
+
await mkdir2(sourceDir);
|
|
3607
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3608
|
+
let bytes = 0;
|
|
3609
|
+
try {
|
|
3610
|
+
for (const file of snapshot.files) {
|
|
3611
|
+
validatePath(file.path);
|
|
3612
|
+
if (seen.has(file.path)) throw new TypeError("Code source repeats a path");
|
|
3613
|
+
seen.add(file.path);
|
|
3614
|
+
bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
|
|
3615
|
+
if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
|
|
3616
|
+
const target = resolve32(sourceDir, file.path);
|
|
3617
|
+
if (!target.startsWith(`${resolve32(sourceDir)}${sep2}`)) throw new TypeError("Code source path escapes its root");
|
|
3618
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
3619
|
+
await writeFile(target, file.content, { flag: "wx", mode: 420 });
|
|
3620
|
+
}
|
|
3621
|
+
return { sourceDir, cleanup: () => rm2(root, { recursive: true, force: true }) };
|
|
3622
|
+
} catch (cause) {
|
|
3623
|
+
await rm2(root, { recursive: true, force: true });
|
|
3624
|
+
throw cause;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
function validatePath(path) {
|
|
3628
|
+
const parts = path.split("/");
|
|
3629
|
+
if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED2.has(part) || SECRET2.test(part))) {
|
|
3630
|
+
throw new TypeError("Code source contains an unsafe path");
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
var DESTINATIONS = "code-workspaces.v1";
|
|
3634
|
+
var READ = descriptor("sandbox.read", "scoped_data_read", {
|
|
3635
|
+
workspace: "destination",
|
|
3636
|
+
authority: "authority",
|
|
3637
|
+
path: "selector",
|
|
3638
|
+
startLine: "selector",
|
|
3639
|
+
endLine: "selector"
|
|
3640
|
+
});
|
|
3641
|
+
var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
|
|
3642
|
+
workspace: "destination",
|
|
3643
|
+
authority: "authority",
|
|
3644
|
+
patch: "payload"
|
|
3645
|
+
});
|
|
3646
|
+
var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
|
|
3647
|
+
workspace: "destination",
|
|
3648
|
+
authority: "authority",
|
|
3649
|
+
recipeId: "selector",
|
|
3650
|
+
sourceDigest: "payload"
|
|
3651
|
+
});
|
|
3652
|
+
function createCodePolicyGate(options) {
|
|
3653
|
+
return {
|
|
3654
|
+
read: async (input) => {
|
|
3655
|
+
const base = await environment(input, options, "sandbox.read");
|
|
3656
|
+
const conversions = await conversionRegistry([
|
|
3657
|
+
await registeredPolicy("code.path.v1", "code.paths.v1", input.paths),
|
|
3658
|
+
await conversionPolicy("code.line.v1", { kind: "integer", minimum: 1, maximum: 1e6 })
|
|
3659
|
+
], { "code.paths.v1": input.paths });
|
|
3660
|
+
const path = await conversions.operations.registeredId(unsafe(base, input.path, "path"), "code.path.v1");
|
|
3661
|
+
const start = await conversions.operations.integer(unsafe(base, input.startLine, "start"), "code.line.v1");
|
|
3662
|
+
const end = await conversions.operations.integer(unsafe(base, input.endLine, "end"), "code.line.v1");
|
|
3663
|
+
if (end.value < start.value) return false;
|
|
3664
|
+
return authorize(input, options, base, READ, {
|
|
3665
|
+
...base.fixedArgs,
|
|
3666
|
+
path: { role: "selector", value: path },
|
|
3667
|
+
startLine: { role: "selector", value: start },
|
|
3668
|
+
endLine: { role: "selector", value: end }
|
|
3669
|
+
}, [path, start, end]);
|
|
3670
|
+
},
|
|
3671
|
+
patch: async (input) => {
|
|
3672
|
+
const base = await environment(input, options, "sandbox.apply_patch");
|
|
3673
|
+
const patch2 = unsafe(base, input.patch, "patch");
|
|
3674
|
+
return authorize(input, options, base, PATCH, {
|
|
3675
|
+
...base.fixedArgs,
|
|
3676
|
+
patch: { role: "payload", value: patch2 }
|
|
3677
|
+
}, []);
|
|
3678
|
+
},
|
|
3679
|
+
recipe: async (input) => {
|
|
3680
|
+
const base = await environment(input, options, "sandbox.run_recipe");
|
|
3681
|
+
const conversions = await conversionRegistry([
|
|
3682
|
+
await registeredPolicy("code.recipe.v1", "code.recipes.v1", input.recipeIds)
|
|
3683
|
+
], { "code.recipes.v1": input.recipeIds });
|
|
3684
|
+
const recipe2 = await conversions.operations.registeredId(unsafe(base, input.recipeId, "recipe"), "code.recipe.v1");
|
|
3685
|
+
const source = unsafe(base, input.sourceDigest, "source");
|
|
3686
|
+
return authorize(input, options, base, RECIPE, {
|
|
3687
|
+
...base.fixedArgs,
|
|
3688
|
+
recipeId: { role: "selector", value: recipe2 },
|
|
3689
|
+
sourceDigest: { role: "payload", value: source }
|
|
3690
|
+
}, [recipe2]);
|
|
3691
|
+
}
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
3694
|
+
function descriptor(name, effect, argumentRoles) {
|
|
3695
|
+
return { name, version: 1, effect, inputSchema: { type: "object" }, argumentRoles, policyId: `odla.code.${name}.v1` };
|
|
3696
|
+
}
|
|
3697
|
+
async function conversionPolicy(id, output) {
|
|
3698
|
+
const definition = {
|
|
3699
|
+
conversionId: id,
|
|
3700
|
+
version: 1,
|
|
3701
|
+
output,
|
|
3702
|
+
maximumSourceBytes: 1e6,
|
|
3703
|
+
maximumOutputsPerArtifact: 4,
|
|
3704
|
+
presentation: "json_scalar"
|
|
3705
|
+
};
|
|
3706
|
+
return { ...definition, digest: await conversionPolicyDigest(definition) };
|
|
3707
|
+
}
|
|
3708
|
+
async function registeredPolicy(id, registryId, values) {
|
|
3709
|
+
const mapping = Object.fromEntries(values.map((value) => [value, value]));
|
|
3710
|
+
return conversionPolicy(id, {
|
|
3711
|
+
kind: "registered_id",
|
|
3712
|
+
registryId,
|
|
3713
|
+
registryDigest: await registeredIdRegistryDigest(mapping)
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
async function conversionRegistry(policies, values) {
|
|
3717
|
+
const registeredIds = Object.fromEntries(await Promise.all(Object.entries(values).map(async ([id, entries]) => {
|
|
3718
|
+
const mapping = Object.fromEntries(entries.map((value) => [value, value]));
|
|
3719
|
+
return [id, { values: mapping, digest: await registeredIdRegistryDigest(mapping) }];
|
|
3720
|
+
})));
|
|
3721
|
+
return createConversionRegistry({ policies, registeredIds });
|
|
3722
|
+
}
|
|
3723
|
+
async function environment(input, options, tool) {
|
|
3724
|
+
const ingress = createCamelIngress([
|
|
3725
|
+
{ id: "workspace", value: input.workspaceId, readers: input.readers },
|
|
3726
|
+
{ id: "authority", value: `lease:${input.lease.leaseId}`, readers: input.readers },
|
|
3727
|
+
{ id: "reader", value: options.readerId, readers: input.readers }
|
|
3728
|
+
]);
|
|
3729
|
+
const digest2 = await destinationRegistryDigest([input.workspaceId]);
|
|
3730
|
+
const approvals = ["irreversible_mutation", "external_send", "financial", "secret_read"];
|
|
3731
|
+
if (options.recipeAuthorization === "exact_approval") approvals.push("code_execution");
|
|
3732
|
+
const policy = createEffectPolicy({
|
|
3733
|
+
destinationRegistries: { [DESTINATIONS]: { digest: digest2, values: [input.workspaceId] } },
|
|
3734
|
+
approvalEffects: approvals
|
|
3735
|
+
});
|
|
3736
|
+
const fixedArgs = {
|
|
3737
|
+
workspace: { role: "destination", value: ingress.control("workspace"), registryId: DESTINATIONS, registryDigest: digest2 },
|
|
3738
|
+
authority: { role: "authority", value: ingress.control("authority") }
|
|
3739
|
+
};
|
|
3740
|
+
return { ingress, policy, fixedArgs, reader: ingress.control("reader"), runId: `${input.request.requestId}:${tool}` };
|
|
3741
|
+
}
|
|
3742
|
+
function unsafe(base, value, field) {
|
|
3743
|
+
return base.ingress.quarantinedOutput(value, {
|
|
3744
|
+
readers: base.reader.label.readers,
|
|
3745
|
+
runId: `${base.runId}:${field}`
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
async function authorize(input, options, base, tool, args, controlDependencies) {
|
|
3749
|
+
const policy = await base.policy.evaluate({
|
|
3750
|
+
planId: input.lease.task.taskId,
|
|
3751
|
+
tool,
|
|
3752
|
+
args,
|
|
3753
|
+
controlDependencies,
|
|
3754
|
+
intendedReaderIds: [base.reader]
|
|
3755
|
+
});
|
|
3756
|
+
let approvalConsumed = false;
|
|
3757
|
+
if (policy.outcome === "require_approval" && options.consumeApproval) {
|
|
3758
|
+
approvalConsumed = await options.consumeApproval(decision(input, policy, false, tool.name, policy.actionDigest));
|
|
3759
|
+
}
|
|
3760
|
+
await options.onDecision?.(decision(input, policy, approvalConsumed, tool.name));
|
|
3761
|
+
return policy.outcome === "allow" || approvalConsumed;
|
|
3762
|
+
}
|
|
3763
|
+
function decision(input, policy, approvalConsumed, tool, actionDigest) {
|
|
3764
|
+
return {
|
|
3765
|
+
lease: input.lease,
|
|
3766
|
+
request: input.request,
|
|
3767
|
+
tool,
|
|
3768
|
+
policy,
|
|
3769
|
+
approvalConsumed,
|
|
3770
|
+
actionDigest: actionDigest ?? (policy.outcome === "require_approval" ? policy.actionDigest : "")
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function createCodeToolBroker(options) {
|
|
3774
|
+
validateOptions(options);
|
|
3775
|
+
const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
|
|
3776
|
+
const policy = createCodePolicyGate(options);
|
|
3777
|
+
let tail = Promise.resolve();
|
|
3778
|
+
return {
|
|
3779
|
+
execute(context, request) {
|
|
3780
|
+
const result = tail.then(() => route(context, request, options, recipes, policy));
|
|
3781
|
+
tail = result.then(() => void 0, () => void 0);
|
|
3782
|
+
return result;
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
async function route(context, request, options, recipes, policy) {
|
|
3787
|
+
try {
|
|
3788
|
+
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
3789
|
+
if (request.tool === "sandbox.read") return await read(context, request, options, policy);
|
|
3790
|
+
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
|
|
3791
|
+
return await recipe(context, request, options, recipes, policy);
|
|
3792
|
+
} catch (reason) {
|
|
3793
|
+
return response(request, false, reason instanceof TypeError ? reason.message : "tool failed closed");
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
async function read(context, request, options, policy) {
|
|
3797
|
+
exactKeys(request.input, ["path", "startLine", "endLine"]);
|
|
3798
|
+
const path = stringField(request.input, "path");
|
|
3799
|
+
const startLine = optionalInteger(request.input.startLine) ?? 1;
|
|
3800
|
+
const endLine = optionalInteger(request.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
|
|
3801
|
+
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
3802
|
+
throw new TypeError("requested line range exceeds its bound");
|
|
3803
|
+
}
|
|
3804
|
+
const paths = await registeredFiles(context.workspaceDir, 2e4);
|
|
3805
|
+
const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
|
|
3806
|
+
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3807
|
+
const target = resolveCodePath(context.workspaceDir, path);
|
|
3808
|
+
const info = await stat2(target);
|
|
3809
|
+
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
3810
|
+
throw new TypeError("file is not a bounded regular source file");
|
|
3811
|
+
}
|
|
3812
|
+
const source = await readFile2(target);
|
|
3813
|
+
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
3814
|
+
const lines = source.toString("utf8").split("\n");
|
|
3815
|
+
const content = lines.slice(startLine - 1, endLine).join("\n");
|
|
3816
|
+
if (Buffer.byteLength(content) > (options.maxReadBytes ?? 128 * 1024)) {
|
|
3817
|
+
throw new TypeError("read result exceeds its byte bound");
|
|
3818
|
+
}
|
|
3819
|
+
return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
3820
|
+
}
|
|
3821
|
+
async function patch(context, request, options, policy) {
|
|
3822
|
+
exactKeys(request.input, ["patch"]);
|
|
3823
|
+
const value = stringField(request.input, "patch");
|
|
3824
|
+
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
3825
|
+
const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
|
|
3826
|
+
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3827
|
+
await applyCodePatch(context.workspaceDir, value, paths);
|
|
3828
|
+
return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
3829
|
+
}
|
|
3830
|
+
async function recipe(context, request, options, recipes, policy) {
|
|
3831
|
+
exactKeys(request.input, ["recipeId"]);
|
|
3832
|
+
const recipeId = stringField(request.input, "recipeId");
|
|
3833
|
+
const digestLimits = {
|
|
3834
|
+
maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
|
|
3835
|
+
maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
|
|
3836
|
+
};
|
|
3837
|
+
const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
|
|
3838
|
+
const allowed = await policy.recipe(policyContext(context, request, options, {
|
|
3839
|
+
recipeIds: [...recipes.keys()].sort(),
|
|
3840
|
+
recipeId,
|
|
3841
|
+
sourceDigest
|
|
3842
|
+
}));
|
|
3843
|
+
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3844
|
+
const selected = recipes.get(recipeId);
|
|
3845
|
+
if (!selected) return response(request, false, "build recipe is not registered");
|
|
3846
|
+
const staged = await stageWorkspace(context.workspaceDir, {
|
|
3847
|
+
maxFiles: digestLimits.maxFiles,
|
|
3848
|
+
maxBytes: digestLimits.maxBytes
|
|
3849
|
+
});
|
|
3850
|
+
try {
|
|
3851
|
+
if (await digestStagedWorkspace(staged.workspaceDir, digestLimits) !== sourceDigest) {
|
|
3852
|
+
throw new TypeError("workspace changed after recipe authorization");
|
|
3853
|
+
}
|
|
3854
|
+
const result = await options.recipeExecutor.run({
|
|
3855
|
+
workspaceDir: staged.workspaceDir,
|
|
3856
|
+
recipe: selected,
|
|
3857
|
+
signal: context.signal
|
|
3858
|
+
});
|
|
3859
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
3860
|
+
const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
|
|
3861
|
+
const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
|
|
3862
|
+
return response(request, ok, `Recipe ${recipeId} ${status}.${output ? `
|
|
3863
|
+
${output}` : ""}`, {
|
|
3864
|
+
recipeId,
|
|
3865
|
+
exitCode: result.exitCode,
|
|
3866
|
+
durationMs: result.durationMs,
|
|
3867
|
+
outputLimitExceeded: result.outputLimitExceeded,
|
|
3868
|
+
timedOut: result.timedOut
|
|
3869
|
+
});
|
|
3870
|
+
} finally {
|
|
3871
|
+
await staged.cleanup();
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
function policyContext(context, request, options, extra) {
|
|
3875
|
+
return {
|
|
3876
|
+
lease: context.lease,
|
|
3877
|
+
request,
|
|
3878
|
+
workspaceId: `workspace:${context.lease.task.attemptId}`,
|
|
3879
|
+
readers: { kind: "principals", principalIds: [options.readerId] },
|
|
3880
|
+
...extra
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3883
|
+
async function registeredFiles(root, limit) {
|
|
3884
|
+
const paths = [];
|
|
3885
|
+
const walk = async (directory) => {
|
|
3886
|
+
for (const entry of await readdir22(directory, { withFileTypes: true })) {
|
|
3887
|
+
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
3888
|
+
const target = resolve42(directory, entry.name);
|
|
3889
|
+
if (entry.isDirectory()) await walk(target);
|
|
3890
|
+
else if (entry.isFile()) {
|
|
3891
|
+
const path = relative22(root, target).split("\\").join("/");
|
|
3892
|
+
try {
|
|
3893
|
+
validateRelativePath(path);
|
|
3894
|
+
} catch {
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3897
|
+
paths.push(path);
|
|
3898
|
+
if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
};
|
|
3902
|
+
await walk(resolve42(root));
|
|
3903
|
+
return paths.sort();
|
|
3904
|
+
}
|
|
3905
|
+
function validateOptions(options) {
|
|
3906
|
+
if (!options.readerId || !options.recipes.length || new Set(options.recipes.map((item) => item.id)).size !== options.recipes.length) {
|
|
3907
|
+
throw new TypeError("Code tool broker requires a reader and unique registered recipes");
|
|
3908
|
+
}
|
|
3909
|
+
for (const recipe2 of options.recipes) assertCodeBuildRecipe(recipe2);
|
|
3910
|
+
}
|
|
3911
|
+
function exactKeys(input, allowed) {
|
|
3912
|
+
if (Object.keys(input).some((key) => !allowed.includes(key))) throw new TypeError("tool input contains an unsupported field");
|
|
3913
|
+
}
|
|
3914
|
+
function stringField(input, name) {
|
|
3915
|
+
const value = input[name];
|
|
3916
|
+
if (typeof value !== "string" || !value) throw new TypeError(`${name} must be a non-empty string`);
|
|
3917
|
+
return value;
|
|
3918
|
+
}
|
|
3919
|
+
function optionalInteger(value) {
|
|
3920
|
+
if (value === void 0) return void 0;
|
|
3921
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
|
|
3922
|
+
return value;
|
|
3923
|
+
}
|
|
3924
|
+
function response(request, ok, content, details) {
|
|
3925
|
+
return { requestId: request.requestId, ok, content, ...details ? { details } : {} };
|
|
3926
|
+
}
|
|
3927
|
+
function codeCommandMetadata(payload, resume) {
|
|
3928
|
+
const trusted = record22(payload.trustedBase);
|
|
3929
|
+
const role = payload.role;
|
|
3930
|
+
const title = payload.title;
|
|
3931
|
+
const prompt = payload.prompt;
|
|
3932
|
+
if (role !== "coding" && role !== "review" || typeof title !== "string" || typeof prompt !== "string") {
|
|
3933
|
+
throw new TypeError(`invalid Code ${resume ? "resume" : "start"} metadata`);
|
|
3934
|
+
}
|
|
3935
|
+
const planning = trusted?.planningInputDigest;
|
|
3936
|
+
const attestation = trusted?.attestationDigest;
|
|
3937
|
+
return {
|
|
3938
|
+
role,
|
|
3939
|
+
title,
|
|
3940
|
+
prompt,
|
|
3941
|
+
planningInputDigest: typeof planning === "string" && /^sha256:[0-9a-f]{64}$/.test(planning) ? planning : null,
|
|
3942
|
+
attestationDigest: typeof attestation === "string" ? attestation : "resume"
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
function codeCheckpointPayload(payload) {
|
|
3946
|
+
const value = payload.checkpoint;
|
|
3947
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("resume checkpoint is missing");
|
|
3948
|
+
return value;
|
|
3949
|
+
}
|
|
3950
|
+
function fakeCodeLease(command, metadata2) {
|
|
3951
|
+
return {
|
|
3952
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
3953
|
+
leaseId: `code:${command.commandId}`,
|
|
3954
|
+
generation: command.bindingGeneration,
|
|
3955
|
+
expiresAt: Date.now() + 24 * 60 * 6e4,
|
|
3956
|
+
task: {
|
|
3957
|
+
taskId: command.sessionId,
|
|
3958
|
+
attemptId: command.instanceId,
|
|
3959
|
+
title: metadata2.title,
|
|
3960
|
+
prompt: metadata2.prompt,
|
|
3961
|
+
workspace: command.appId,
|
|
3962
|
+
aiRoute: metadata2.role,
|
|
3963
|
+
policy: {
|
|
3964
|
+
network: "none",
|
|
3965
|
+
timeoutMs: 30 * 6e4,
|
|
3966
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
3967
|
+
maxPatchBytes: 256 * 1024
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
var record22 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3973
|
+
var CodePiRuntimeEngine = class {
|
|
3974
|
+
constructor(options) {
|
|
3975
|
+
this.options = options;
|
|
3976
|
+
this.#run = options.runAttempt ?? runContainerAttempt;
|
|
3977
|
+
this.#buildPolicyDigest = digest(JSON.stringify(options.recipes));
|
|
3978
|
+
this.#checkpoints = new CodeRuntimeCheckpointManager({
|
|
3979
|
+
control: options.control,
|
|
3980
|
+
recipes: options.recipes,
|
|
3981
|
+
recipeExecutor: options.recipeExecutor ?? createContainerRecipeExecutor(options.engine),
|
|
3982
|
+
fallbackPolicyDigest: this.#buildPolicyDigest,
|
|
3983
|
+
event: (command, event, refs) => this.#event(command, event, refs)
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
options;
|
|
3987
|
+
#active = /* @__PURE__ */ new Map();
|
|
3988
|
+
#run;
|
|
3989
|
+
#buildPolicyDigest;
|
|
3990
|
+
#checkpoints;
|
|
3991
|
+
execute(command) {
|
|
3992
|
+
if (command.kind === "checkpoint_stop") return this.#checkpoint(command);
|
|
3993
|
+
if (command.kind === "prompt") return this.#prompt(command);
|
|
3994
|
+
return this.#start(command, command.kind === "resume");
|
|
3995
|
+
}
|
|
3996
|
+
async acknowledged(command, result) {
|
|
3997
|
+
if (await this.#checkpoints.acknowledged(command, result)) return;
|
|
3998
|
+
const active = this.#active.get(command.sessionId);
|
|
3999
|
+
if (!active || result.status !== "running") return;
|
|
4000
|
+
active.acknowledged = true;
|
|
4001
|
+
if (active.failure) await this.options.control.reportSessionFailure(command.sessionId, active.failure).catch(() => void 0);
|
|
4002
|
+
}
|
|
4003
|
+
async close() {
|
|
4004
|
+
const sessions = [...this.#active.values()];
|
|
4005
|
+
for (const session of sessions) session.abort.abort("runtime_shutdown");
|
|
4006
|
+
await Promise.allSettled(sessions.map((session) => session.done));
|
|
4007
|
+
await Promise.allSettled(sessions.map((session) => session.workspace.cleanup()));
|
|
4008
|
+
this.#active.clear();
|
|
4009
|
+
}
|
|
4010
|
+
async #start(command, resume) {
|
|
4011
|
+
if (this.#active.has(command.sessionId)) throw new TypeError("Code session is already active on this runtime");
|
|
4012
|
+
const source = await this.options.control.source(command.sessionId);
|
|
4013
|
+
const materialized = await materializeCodeRuntimeSource(source);
|
|
4014
|
+
let workspace;
|
|
4015
|
+
try {
|
|
4016
|
+
workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
|
|
4017
|
+
trustedBaseDir: materialized.sourceDir,
|
|
4018
|
+
trustedBaseCommitSha: source.commitSha,
|
|
4019
|
+
checkpoint: codeCheckpointPayload(command.payload)
|
|
4020
|
+
})).workspace : await stageWorkspace(materialized.sourceDir);
|
|
4021
|
+
} finally {
|
|
4022
|
+
await materialized.cleanup();
|
|
4023
|
+
}
|
|
4024
|
+
const metadata2 = codeCommandMetadata(command.payload, resume);
|
|
4025
|
+
const abort = new AbortController();
|
|
4026
|
+
const conversationRefs = [];
|
|
4027
|
+
const active = {
|
|
4028
|
+
workspace,
|
|
4029
|
+
abort,
|
|
4030
|
+
conversationRefs,
|
|
4031
|
+
acknowledged: false,
|
|
4032
|
+
role: metadata2.role,
|
|
4033
|
+
title: metadata2.title,
|
|
4034
|
+
baseCommitSha: source.commitSha,
|
|
4035
|
+
trustedBaseDigest: await digestStagedWorkspace(workspace.baselineDir, {
|
|
4036
|
+
maxFiles: 1e4,
|
|
4037
|
+
maxBytes: 16 * 1024 * 1024
|
|
4038
|
+
}),
|
|
4039
|
+
planningInputDigest: metadata2.planningInputDigest ?? digest(
|
|
4040
|
+
JSON.stringify({ attestation: metadata2.attestationDigest, prompt: metadata2.prompt, tree: source.treeDigest })
|
|
4041
|
+
),
|
|
4042
|
+
done: Promise.resolve(null)
|
|
4043
|
+
};
|
|
4044
|
+
this.#active.set(command.sessionId, active);
|
|
4045
|
+
active.done = this.#runAttempt(command, metadata2, active).catch(async (cause) => {
|
|
4046
|
+
await this.#event(command, { type: "message", actor: "system", body: `Pi failed: ${message2(cause)}` }, conversationRefs).catch(() => void 0);
|
|
4047
|
+
await this.#event(command, { type: "status", status: "failed" }, conversationRefs).catch(() => void 0);
|
|
4048
|
+
await this.#failure(command, active, message2(cause));
|
|
4049
|
+
return null;
|
|
4050
|
+
});
|
|
4051
|
+
return { status: "running", message: resume ? "Pi resumed from a portable checkpoint" : "Pi started" };
|
|
4052
|
+
}
|
|
4053
|
+
async #prompt(command) {
|
|
4054
|
+
const active = this.#active.get(command.sessionId);
|
|
4055
|
+
const prompt = command.payload.prompt;
|
|
4056
|
+
if (!active || typeof prompt !== "string" || !prompt.trim() || prompt.length > 2e4) {
|
|
4057
|
+
throw new TypeError("prompt requires an active Code session and bounded text");
|
|
4058
|
+
}
|
|
4059
|
+
await active.done;
|
|
4060
|
+
active.abort = new AbortController();
|
|
4061
|
+
active.acknowledged = false;
|
|
4062
|
+
active.failure = void 0;
|
|
4063
|
+
active.done = this.#runAttempt(command, {
|
|
4064
|
+
role: active.role,
|
|
4065
|
+
title: active.title,
|
|
4066
|
+
prompt,
|
|
4067
|
+
planningInputDigest: active.planningInputDigest,
|
|
4068
|
+
attestationDigest: "follow-up"
|
|
4069
|
+
}, active).catch(async (cause) => {
|
|
4070
|
+
await this.#event(
|
|
4071
|
+
command,
|
|
4072
|
+
{ type: "message", actor: "system", body: `Pi failed: ${message2(cause)}` },
|
|
4073
|
+
active.conversationRefs
|
|
4074
|
+
).catch(() => void 0);
|
|
4075
|
+
await this.#event(command, { type: "status", status: "failed" }, active.conversationRefs).catch(() => void 0);
|
|
4076
|
+
await this.#failure(command, active, message2(cause));
|
|
4077
|
+
return null;
|
|
4078
|
+
});
|
|
4079
|
+
return { status: "running", message: "Pi accepted the owner prompt" };
|
|
4080
|
+
}
|
|
4081
|
+
async #runAttempt(command, metadata2, active) {
|
|
4082
|
+
const lease = fakeCodeLease(command, metadata2);
|
|
4083
|
+
const broker = this.#toolBroker(lease, metadata2.role);
|
|
4084
|
+
const startedAt = Date.now();
|
|
4085
|
+
let completionSeen = false;
|
|
4086
|
+
const result = await this.#run({
|
|
4087
|
+
engine: this.options.engine,
|
|
4088
|
+
image: this.options.image,
|
|
4089
|
+
workspaceDir: active.workspace.workspaceDir,
|
|
4090
|
+
workspaceAccess: "none",
|
|
4091
|
+
task: lease.task,
|
|
4092
|
+
limits: this.options.limits,
|
|
4093
|
+
signal: active.abort.signal,
|
|
4094
|
+
onStderr: (text) => this.#event(command, {
|
|
4095
|
+
type: "message",
|
|
4096
|
+
actor: "system",
|
|
4097
|
+
body: text.slice(0, 4e3)
|
|
4098
|
+
}, active.conversationRefs),
|
|
4099
|
+
onMessage: async (output) => {
|
|
4100
|
+
if (output.type === "inference.request") {
|
|
4101
|
+
const inferenceStarted = Date.now();
|
|
4102
|
+
const response2 = await this.options.control.infer(command.sessionId, {
|
|
4103
|
+
requestId: output.requestId,
|
|
4104
|
+
call: output.call
|
|
4105
|
+
});
|
|
4106
|
+
await this.#event(command, {
|
|
4107
|
+
type: "usage",
|
|
4108
|
+
provider: response2.receipt.provider,
|
|
4109
|
+
model: response2.receipt.model,
|
|
4110
|
+
inputTokens: response2.receipt.inputTokens,
|
|
4111
|
+
outputTokens: response2.receipt.outputTokens,
|
|
4112
|
+
durationMs: Date.now() - inferenceStarted
|
|
4113
|
+
}, active.conversationRefs).catch(() => void 0);
|
|
4114
|
+
return {
|
|
4115
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
4116
|
+
type: "inference.response",
|
|
4117
|
+
requestId: output.requestId,
|
|
4118
|
+
response: response2.response
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
if (output.type === "tool.request") {
|
|
4122
|
+
const toolStarted = Date.now();
|
|
4123
|
+
await this.#event(
|
|
4124
|
+
command,
|
|
4125
|
+
{ type: "tool", phase: "started", tool: output.tool },
|
|
4126
|
+
active.conversationRefs
|
|
4127
|
+
).catch(() => void 0);
|
|
4128
|
+
const response2 = await broker.execute({
|
|
4129
|
+
lease,
|
|
4130
|
+
workspaceDir: active.workspace.workspaceDir,
|
|
4131
|
+
signal: active.abort.signal
|
|
4132
|
+
}, output);
|
|
4133
|
+
await this.#event(command, {
|
|
4134
|
+
type: "tool",
|
|
4135
|
+
phase: "completed",
|
|
4136
|
+
tool: output.tool,
|
|
4137
|
+
ok: response2.ok,
|
|
4138
|
+
durationMs: Date.now() - toolStarted
|
|
4139
|
+
}, active.conversationRefs).catch(() => void 0);
|
|
4140
|
+
return { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "tool.response", ...response2 };
|
|
4141
|
+
}
|
|
4142
|
+
if (output.type === "event") {
|
|
4143
|
+
const payload = object2(output.payload);
|
|
4144
|
+
if (output.kind === "pi.started") {
|
|
4145
|
+
await this.#event(command, { type: "status", status: "running" }, active.conversationRefs);
|
|
4146
|
+
} else if (output.kind === "pi.thinking" && payload?.available === true && Number.isSafeInteger(payload.durationMs) && Number(payload.durationMs) >= 0) {
|
|
4147
|
+
await this.#event(command, {
|
|
4148
|
+
type: "thinking",
|
|
4149
|
+
available: true,
|
|
4150
|
+
durationMs: Math.min(Number(payload.durationMs), 864e5)
|
|
4151
|
+
}, active.conversationRefs);
|
|
4152
|
+
} else {
|
|
4153
|
+
await this.#event(command, {
|
|
4154
|
+
type: "message",
|
|
4155
|
+
actor: "system",
|
|
4156
|
+
body: `${output.kind}${output.payload === void 0 ? "" : ` ${json(output.payload)}`}`
|
|
4157
|
+
}, active.conversationRefs);
|
|
4158
|
+
}
|
|
4159
|
+
} else if (output.type === "attempt.complete") {
|
|
4160
|
+
completionSeen = true;
|
|
4161
|
+
const body = resultText(output.result) ?? `Pi ${output.status}.`;
|
|
4162
|
+
await this.#event(command, {
|
|
4163
|
+
type: "message",
|
|
4164
|
+
actor: output.status === "completed" ? "agent" : "system",
|
|
4165
|
+
body
|
|
4166
|
+
}, active.conversationRefs);
|
|
4167
|
+
await this.#event(command, {
|
|
4168
|
+
type: "status",
|
|
4169
|
+
status: output.status === "completed" ? "idle" : "failed",
|
|
4170
|
+
durationMs: Date.now() - startedAt
|
|
4171
|
+
}, active.conversationRefs);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
if (result.status === "failed" && result.stderr) {
|
|
4176
|
+
await this.#event(command, { type: "message", actor: "system", body: result.stderr.slice(0, 4e3) }, active.conversationRefs);
|
|
4177
|
+
}
|
|
4178
|
+
if (!completionSeen) await this.#event(command, {
|
|
4179
|
+
type: "status",
|
|
4180
|
+
status: result.status === "completed" ? "idle" : "failed",
|
|
4181
|
+
durationMs: Date.now() - startedAt
|
|
4182
|
+
}, active.conversationRefs).catch(() => void 0);
|
|
4183
|
+
if (result.status === "failed") {
|
|
4184
|
+
await this.#failure(command, active, result.stderr || "Pi container failed");
|
|
4185
|
+
}
|
|
4186
|
+
return result;
|
|
4187
|
+
}
|
|
4188
|
+
#toolBroker(lease, role) {
|
|
4189
|
+
const broker = createCodeToolBroker({
|
|
4190
|
+
recipes: this.options.recipes,
|
|
4191
|
+
recipeExecutor: createContainerRecipeExecutor(this.options.engine),
|
|
4192
|
+
recipeAuthorization: this.options.recipeAuthorization ?? "registered_recipe",
|
|
4193
|
+
readerId: `code-session:${lease.task.taskId}`
|
|
4194
|
+
});
|
|
4195
|
+
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" }) };
|
|
4196
|
+
}
|
|
4197
|
+
async #checkpoint(command) {
|
|
4198
|
+
const active = this.#active.get(command.sessionId);
|
|
4199
|
+
if (!active) throw new TypeError("Code session workspace is not active on this runtime");
|
|
4200
|
+
const result = await this.#checkpoints.prepare(command, active);
|
|
4201
|
+
this.#active.delete(command.sessionId);
|
|
4202
|
+
return result;
|
|
4203
|
+
}
|
|
4204
|
+
async #failure(command, active, value) {
|
|
4205
|
+
active.failure = value.slice(0, 2e3);
|
|
4206
|
+
if (active.acknowledged) {
|
|
4207
|
+
await this.options.control.reportSessionFailure(command.sessionId, active.failure).catch(() => void 0);
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
async #event(command, event, refs) {
|
|
4211
|
+
const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
|
|
4212
|
+
refs.push(eventId);
|
|
4213
|
+
const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
|
|
4214
|
+
await this.options.control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
var object2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4218
|
+
var digest = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
|
|
4219
|
+
var message2 = (value) => value instanceof Error ? value.message : String(value);
|
|
4220
|
+
var json = (value) => {
|
|
4221
|
+
try {
|
|
4222
|
+
return JSON.stringify(value).slice(0, 1e4);
|
|
4223
|
+
} catch {
|
|
4224
|
+
return "[event]";
|
|
4225
|
+
}
|
|
4226
|
+
};
|
|
4227
|
+
function resultText(value) {
|
|
4228
|
+
const record32 = object2(value);
|
|
4229
|
+
return record32 && typeof record32.text === "string" ? record32.text.slice(0, 2e4) : null;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
// src/help.ts
|
|
4233
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4234
|
+
function cliVersion() {
|
|
4235
|
+
const pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
|
|
4236
|
+
return pkg.version ?? "unknown";
|
|
4237
|
+
}
|
|
4238
|
+
function printHelp() {
|
|
4239
|
+
console.log(`odla-ai
|
|
4240
|
+
|
|
4241
|
+
Usage:
|
|
4242
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
4243
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
4244
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
4245
|
+
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
4246
|
+
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
4247
|
+
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
4248
|
+
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
4249
|
+
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
4250
|
+
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
4251
|
+
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
4252
|
+
odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
4253
|
+
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
4254
|
+
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
4255
|
+
odla-ai capabilities [--json]
|
|
4256
|
+
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
4257
|
+
odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
4258
|
+
odla-ai admin ai models [--provider <id>] [--json]
|
|
4259
|
+
odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
4260
|
+
odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
|
|
4261
|
+
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
|
|
4262
|
+
odla-ai admin ai credentials [--json]
|
|
4263
|
+
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
4264
|
+
odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
4265
|
+
odla-ai admin ai audit [--limit <1-200>] [--json]
|
|
4266
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
|
|
4267
|
+
odla-ai security github disconnect --source <id> [--env dev] [--yes]
|
|
4268
|
+
odla-ai security plan [--env dev] [--json]
|
|
4269
|
+
odla-ai security sources [--env dev] [--json]
|
|
4270
|
+
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
4271
|
+
odla-ai security status <job-id> [--follow] [--json]
|
|
4272
|
+
odla-ai security report <job-id> [--json]
|
|
4273
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
4274
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
4275
|
+
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
4276
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
4277
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
4278
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
4279
|
+
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
4280
|
+
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
4281
|
+
odla-ai version
|
|
4282
|
+
|
|
4283
|
+
Commands:
|
|
4284
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
4285
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
4286
|
+
doctor Validate and summarize the project config without network calls.
|
|
4287
|
+
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
4288
|
+
app Archive (suspend, data retained), restore, export, or manage the
|
|
4289
|
+
co-owners of the app. Archiving takes every environment's data
|
|
4290
|
+
plane down until restored; permanent deletion has NO CLI \u2014 it
|
|
4291
|
+
requires a signed-in owner in Studio, and no machine or agent
|
|
4292
|
+
credential can purge. "app export" downloads a portable
|
|
4293
|
+
gzipped-JSONL snapshot of one environment's database (--fresh
|
|
4294
|
+
takes a new snapshot first) \u2014 your data is always yours to take.
|
|
4295
|
+
"app owners add <email>" grants a signed-up odla member the SAME
|
|
4296
|
+
full access as you; they then run their own "provision" to mint
|
|
4297
|
+
their own credentials for the shared db (prod included) \u2014 no
|
|
4298
|
+
secret is ever copied between people.
|
|
4299
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
4300
|
+
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
4301
|
+
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
4302
|
+
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
4303
|
+
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
4304
|
+
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
4305
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
4306
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
4307
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
4308
|
+
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
4309
|
+
reserved Clerk secret key, write-only from stdin or an env var.
|
|
4310
|
+
version Print the CLI version.
|
|
4311
|
+
|
|
4312
|
+
Safety:
|
|
4313
|
+
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
4314
|
+
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
4315
|
+
Provision caches the approved developer token and service credentials under
|
|
4316
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
4317
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
4318
|
+
Provision opens the approval page in your browser automatically whenever the
|
|
4319
|
+
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
4320
|
+
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
4321
|
+
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
4322
|
+
agents must relay it to the human verbatim. A started handshake is persisted
|
|
4323
|
+
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
4324
|
+
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
4325
|
+
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
4326
|
+
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
4327
|
+
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
4328
|
+
The email is a non-secret identity hint: never provide a password or session
|
|
4329
|
+
token. The matching account must already exist, be signed in, explicitly
|
|
4330
|
+
review the exact code, and finish any current request before claiming another.
|
|
4331
|
+
Run Code from a GitHub checkout already connected to an app in Studio; an
|
|
4332
|
+
odla.config.mjs may select the app explicitly but is not required. Code host
|
|
4333
|
+
approval and credential hashes live in odla-ai/db. The host
|
|
4334
|
+
credential is never written under .odla/; it exists only in the foreground
|
|
4335
|
+
"code connect" process and is rotated by the next approved connection.
|
|
4336
|
+
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
4337
|
+
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
4338
|
+
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
4339
|
+
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
4340
|
+
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
4341
|
+
plan are required before bounded snippets reach System AI.
|
|
4342
|
+
Run security plan first to inspect the admin-selected providers, models,
|
|
4343
|
+
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
4344
|
+
and digest that binds consent to that exact plan.
|
|
4345
|
+
`);
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
// src/code-connect.ts
|
|
4349
|
+
var CODE_PI_IMAGE = "ghcr.io/cory/odla-pi-agent@sha256:713db58428bc05cb486167c5747de8fd826669c9b283ff78e0139ac194c3d058";
|
|
4350
|
+
var CODE_BUILD_RECIPES = Object.freeze([{
|
|
4351
|
+
id: "node-test",
|
|
4352
|
+
image: "node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd",
|
|
4353
|
+
command: ["node", "--test"],
|
|
4354
|
+
timeoutMs: 12e4,
|
|
4355
|
+
maxOutputBytes: 1024 * 1024,
|
|
4356
|
+
cpus: 1,
|
|
4357
|
+
memory: "512m",
|
|
4358
|
+
pids: 128
|
|
4359
|
+
}]);
|
|
4360
|
+
async function codeConnect(options) {
|
|
4361
|
+
const cwd = options.cwd ?? process.cwd();
|
|
4362
|
+
const configPath = resolve5(cwd, options.configPath);
|
|
4363
|
+
const cfg = existsSync4(configPath) ? await loadProjectConfig(configPath) : null;
|
|
4364
|
+
const repository = cfg ? null : await inferGitHubRepository(cwd, options.readGitOrigin);
|
|
4365
|
+
const platform = cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai";
|
|
4366
|
+
const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : "dev");
|
|
4367
|
+
if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
|
|
4368
|
+
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`);
|
|
4369
|
+
}
|
|
4370
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
4371
|
+
throw new Error("odla Code hosts require macOS or Linux");
|
|
4372
|
+
}
|
|
4373
|
+
const slots = options.slots ?? 1;
|
|
4374
|
+
if (!Number.isSafeInteger(slots) || slots < 1 || slots > 64) {
|
|
4375
|
+
throw new Error("--slots must be an integer from 1 to 64");
|
|
4376
|
+
}
|
|
4377
|
+
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
4378
|
+
if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
|
|
4379
|
+
throw new Error("--heartbeat-ms must be an integer from 1000 to 300000");
|
|
4380
|
+
}
|
|
4381
|
+
const out = options.stdout ?? console;
|
|
4382
|
+
const doFetch = options.fetch ?? fetch;
|
|
4383
|
+
const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
|
|
4384
|
+
const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
|
|
4385
|
+
const hostName = (options.name ?? hostname()).trim();
|
|
4386
|
+
if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
|
|
4387
|
+
const approval = await (options.getToken ?? getScopedPlatformToken)({
|
|
4388
|
+
platform,
|
|
4389
|
+
scope: "platform:code:host:connect",
|
|
4390
|
+
email: options.email,
|
|
4391
|
+
open: options.open,
|
|
4392
|
+
fetch: doFetch,
|
|
4393
|
+
stdout: out,
|
|
4394
|
+
openApprovalUrl: options.openApprovalUrl,
|
|
4395
|
+
cache: false,
|
|
4396
|
+
label: `odla CLI Code host for ${cfg?.app.id ?? repository}/${appEnv}`
|
|
4397
|
+
});
|
|
4398
|
+
const target = cfg ? { appId: cfg.app.id } : { repository };
|
|
4399
|
+
const response2 = await doFetch(`${platform}/registry/code/hosts/connect`, {
|
|
4400
|
+
method: "POST",
|
|
4401
|
+
headers: { authorization: `Bearer ${approval}`, "content-type": "application/json" },
|
|
4402
|
+
body: JSON.stringify({ ...target, env: appEnv, name: hostName, platform: hostPlatform, slots }),
|
|
4403
|
+
redirect: "error",
|
|
4404
|
+
signal: options.signal
|
|
4405
|
+
});
|
|
4406
|
+
const raw = await response2.json().catch(() => null);
|
|
4407
|
+
if (!response2.ok) throw new Error(apiFailure("connect Code host", response2.status, raw));
|
|
4408
|
+
const connection = parseConnection(raw, cfg?.app.id, appEnv);
|
|
4409
|
+
const capabilities = {
|
|
4410
|
+
protocolVersion: CODE_RUNTIME_PROTOCOL_VERSION,
|
|
4411
|
+
platform: hostPlatform,
|
|
4412
|
+
arch: process.arch,
|
|
4413
|
+
engines: [engine],
|
|
4414
|
+
cpuCount: cpus().length,
|
|
4415
|
+
memoryBytes: totalmem()
|
|
4416
|
+
};
|
|
4417
|
+
out.log(`${connection.resumed ? "reconnected" : "enrolled"}: ${connection.host.name} (${connection.host.hostId})`);
|
|
4418
|
+
out.log(`binding: ${connection.binding.appId}/${connection.binding.env} \xB7 ${connection.offer.slots} slot(s) \xB7 ${engine}`);
|
|
4419
|
+
out.log("credentials: stored in odla-ai/db; host plaintext is memory-only");
|
|
4420
|
+
await (options.runRuntime ?? runCodeRuntime)({
|
|
4421
|
+
endpoint: platform,
|
|
4422
|
+
token: connection.token,
|
|
4423
|
+
engine,
|
|
4424
|
+
capabilities,
|
|
4425
|
+
heartbeatMs,
|
|
4426
|
+
once: options.once === true,
|
|
4427
|
+
signal: options.signal,
|
|
4428
|
+
stdout: out,
|
|
4429
|
+
fetch: doFetch
|
|
4430
|
+
});
|
|
4431
|
+
}
|
|
4432
|
+
async function runCodeRuntime(input) {
|
|
4433
|
+
const controller = new AbortController();
|
|
4434
|
+
const abort = () => controller.abort(input.signal?.reason ?? "runtime_shutdown");
|
|
4435
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
4436
|
+
const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
|
|
4437
|
+
const handlers = ["SIGINT", "SIGTERM"].map((name) => {
|
|
4438
|
+
const handler = () => controller.abort(name);
|
|
4439
|
+
process.once(name, handler);
|
|
4440
|
+
return [name, handler];
|
|
4441
|
+
});
|
|
4442
|
+
const control = createCodeRuntimeControlClient({
|
|
4443
|
+
endpoint: input.endpoint,
|
|
4444
|
+
token: input.token,
|
|
4445
|
+
signal,
|
|
4446
|
+
fetch: input.fetch
|
|
4447
|
+
});
|
|
4448
|
+
const commandEngine = new CodePiRuntimeEngine({
|
|
4449
|
+
control,
|
|
4450
|
+
engine: input.engine,
|
|
4451
|
+
image: CODE_PI_IMAGE,
|
|
4452
|
+
recipes: CODE_BUILD_RECIPES,
|
|
4453
|
+
recipeAuthorization: "registered_recipe"
|
|
4454
|
+
});
|
|
4455
|
+
const reconciler = new CodeRuntimeReconciler(control, commandEngine);
|
|
4456
|
+
try {
|
|
4457
|
+
await runCodeRuntimeHeartbeatLoop({
|
|
4458
|
+
control,
|
|
4459
|
+
runtimeVersion: `odla-ai-cli/${cliVersion()}`,
|
|
4460
|
+
capabilities: input.capabilities,
|
|
4461
|
+
heartbeatMs: input.heartbeatMs,
|
|
4462
|
+
once: input.once,
|
|
4463
|
+
signal,
|
|
4464
|
+
onSnapshot: async (snapshot) => {
|
|
4465
|
+
input.stdout.log(
|
|
4466
|
+
`online: ${snapshot.host.hostId} \xB7 ${snapshot.bindings.length} binding(s) \xB7 ${snapshot.commands.length} command(s)`
|
|
4467
|
+
);
|
|
4468
|
+
if (input.once && snapshot.commands.length) {
|
|
4469
|
+
throw new Error("--once is diagnostic-only and refuses pending Code commands; run without --once");
|
|
4470
|
+
}
|
|
4471
|
+
await reconciler.reconcile(snapshot);
|
|
4472
|
+
},
|
|
4473
|
+
onRetry: (error, delayMs) => input.stdout.error(
|
|
4474
|
+
`Code control plane unavailable; retrying in ${delayMs}ms \xB7 ${error instanceof Error ? error.message : String(error)}`
|
|
4475
|
+
)
|
|
4476
|
+
});
|
|
4477
|
+
} finally {
|
|
4478
|
+
await commandEngine.close();
|
|
4479
|
+
input.signal?.removeEventListener("abort", abort);
|
|
4480
|
+
for (const [name, handler] of handlers) process.removeListener(name, handler);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
function parseConnection(value, appId, appEnv) {
|
|
4484
|
+
const root = record4(value);
|
|
4485
|
+
const host = record4(root?.host);
|
|
4486
|
+
const offer = record4(root?.offer);
|
|
4487
|
+
const binding = record4(root?.binding);
|
|
4488
|
+
if (!root || typeof root.token !== "string" || !/^odla_code_host_[0-9a-f]{64}$/.test(root.token) || typeof root.resumed !== "boolean" || !host || !/^chost_[0-9a-f]{32}$/.test(String(host.hostId)) || typeof host.name !== "string" || !offer || !Number.isSafeInteger(offer.slots) || !binding || typeof binding.appId !== "string" || !binding.appId || appId && binding.appId !== appId || binding.env !== appEnv || !Number.isSafeInteger(binding.generation)) {
|
|
4489
|
+
throw new Error("connect Code host returned an invalid response");
|
|
4490
|
+
}
|
|
4491
|
+
return root;
|
|
4492
|
+
}
|
|
4493
|
+
function apiFailure(action, status, value) {
|
|
4494
|
+
const message3 = record4(record4(value)?.error)?.message;
|
|
4495
|
+
return `${action} failed (${status})${typeof message3 === "string" ? `: ${message3}` : ""}`;
|
|
4496
|
+
}
|
|
4497
|
+
function record4(value) {
|
|
4498
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
// src/doctor-checks.ts
|
|
4502
|
+
import { execFileSync } from "child_process";
|
|
4503
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
4504
|
+
import { join as join5, resolve as resolve6 } from "path";
|
|
4505
|
+
|
|
4506
|
+
// src/wrangler.ts
|
|
4507
|
+
import { spawn as spawn4 } from "child_process";
|
|
4508
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
4509
|
+
import { join as join4 } from "path";
|
|
4510
|
+
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
4511
|
+
const child = spawn4(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
4512
|
+
let stdout = "";
|
|
4513
|
+
let stderr = "";
|
|
4514
|
+
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
4515
|
+
child.stderr.on("data", (chunk) => stderr += chunk.toString());
|
|
4516
|
+
child.on("error", reject);
|
|
4517
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
|
|
4518
|
+
child.stdin.end(opts?.input ?? "");
|
|
4519
|
+
});
|
|
4520
|
+
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
4521
|
+
function findWranglerConfig(rootDir) {
|
|
4522
|
+
for (const name of WRANGLER_CONFIG_FILES) {
|
|
4523
|
+
const path = join4(rootDir, name);
|
|
4524
|
+
if (existsSync5(path)) return path;
|
|
4525
|
+
}
|
|
4526
|
+
return null;
|
|
4527
|
+
}
|
|
4528
|
+
function readWranglerConfig(path) {
|
|
4529
|
+
if (path.endsWith(".toml")) return null;
|
|
4530
|
+
try {
|
|
4531
|
+
return JSON.parse(stripJsonComments(readFileSync4(path, "utf8")));
|
|
4532
|
+
} catch {
|
|
4533
|
+
return null;
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
function stripJsonComments(text) {
|
|
4537
|
+
let result = "";
|
|
4538
|
+
let inString = false;
|
|
4539
|
+
for (let i = 0; i < text.length; i++) {
|
|
4540
|
+
const ch = text[i];
|
|
4541
|
+
if (inString) {
|
|
4542
|
+
result += ch;
|
|
4543
|
+
if (ch === "\\") {
|
|
4544
|
+
result += text[i + 1] ?? "";
|
|
4545
|
+
i++;
|
|
4546
|
+
} else if (ch === '"') {
|
|
4547
|
+
inString = false;
|
|
4548
|
+
}
|
|
4549
|
+
continue;
|
|
4550
|
+
}
|
|
4551
|
+
if (ch === '"') {
|
|
4552
|
+
inString = true;
|
|
4553
|
+
result += ch;
|
|
4554
|
+
continue;
|
|
4555
|
+
}
|
|
4556
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
4557
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
4558
|
+
result += "\n";
|
|
4559
|
+
continue;
|
|
4560
|
+
}
|
|
4561
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
4562
|
+
i += 2;
|
|
4563
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
4564
|
+
i++;
|
|
4565
|
+
continue;
|
|
4566
|
+
}
|
|
4567
|
+
result += ch;
|
|
4568
|
+
}
|
|
4569
|
+
return result;
|
|
4570
|
+
}
|
|
4571
|
+
async function wranglerLoggedIn(run, cwd) {
|
|
4572
|
+
try {
|
|
4573
|
+
const result = await run("npx", ["wrangler", "whoami"], { cwd });
|
|
4574
|
+
return result.code === 0 && !/not authenticated/i.test(`${result.stdout}${result.stderr}`);
|
|
4575
|
+
} catch {
|
|
4576
|
+
return false;
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
function wranglerPutSecret(run, opts) {
|
|
4580
|
+
const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
|
|
4581
|
+
return run("npx", args, { input: opts.value, cwd: opts.cwd });
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
// src/doctor-checks.ts
|
|
4585
|
+
function lintRules(rules, entities, publicRead) {
|
|
4586
|
+
const warnings = [];
|
|
4587
|
+
if (!rules) return warnings;
|
|
4588
|
+
for (const [ns, actions] of Object.entries(rules)) {
|
|
4589
|
+
for (const [action, expr] of Object.entries(actions)) {
|
|
4590
|
+
if (typeof expr !== "string" || expr.trim() !== "true") continue;
|
|
4591
|
+
if (action === "view" && publicRead.includes(ns)) continue;
|
|
4592
|
+
warnings.push(
|
|
4593
|
+
action === "view" ? `rules.${ns}.view is "true" \u2014 public read; add "${ns}" to db.publicRead if intended` : `rules.${ns}.${action} is "true" \u2014 any client can ${action} ${ns} rows`
|
|
4594
|
+
);
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
for (const entity of entities) {
|
|
4598
|
+
if (!(entity in rules)) warnings.push(`schema entity "${entity}" has no rules entry (all client access denied)`);
|
|
4599
|
+
}
|
|
4600
|
+
return warnings;
|
|
4601
|
+
}
|
|
4602
|
+
var defaultExec = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
4603
|
+
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
4604
|
+
let output;
|
|
4605
|
+
try {
|
|
4606
|
+
const custom = localPaths.map((path) => gitignoreEntry(rootDir, path)).filter((path) => !!path);
|
|
4607
|
+
output = exec("git", ["ls-files", "--", ".dev.vars", ".odla", ...custom], rootDir);
|
|
4608
|
+
} catch {
|
|
4609
|
+
return [];
|
|
4610
|
+
}
|
|
4611
|
+
return output.split(/\r?\n/).filter(Boolean).map((path) => `${path} is tracked by git \u2014 run "git rm --cached ${path}" and commit; it belongs in .gitignore`);
|
|
4612
|
+
}
|
|
4613
|
+
function wranglerWarnings(rootDir) {
|
|
4614
|
+
const configPath = findWranglerConfig(rootDir);
|
|
4615
|
+
if (!configPath) return [];
|
|
4616
|
+
const config = readWranglerConfig(configPath);
|
|
4617
|
+
if (!config) return [];
|
|
4618
|
+
const warnings = [];
|
|
4619
|
+
const blocks = [{ label: "", block: config }];
|
|
4620
|
+
const envs = config.env;
|
|
4621
|
+
if (envs && typeof envs === "object") {
|
|
4622
|
+
for (const [name, block] of Object.entries(envs)) {
|
|
4623
|
+
if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
for (const { label, block } of blocks) {
|
|
4627
|
+
const assets = block.assets;
|
|
4628
|
+
if (assets?.directory) {
|
|
4629
|
+
const dir = resolve6(rootDir, assets.directory);
|
|
4630
|
+
if (dir === resolve6(rootDir)) {
|
|
4631
|
+
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
4632
|
+
} else if (existsSync6(join5(dir, "node_modules"))) {
|
|
4633
|
+
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
const vars = block.vars;
|
|
4637
|
+
if (vars && typeof vars === "object") {
|
|
4638
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
4639
|
+
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value === "string" && looksSecret(value)) {
|
|
4640
|
+
warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
const pkg = readPackageJson(rootDir);
|
|
4646
|
+
if (pkg && typeof pkg.scripts === "object" && pkg.scripts && "deploy" in pkg.scripts) {
|
|
4647
|
+
warnings.push(`package.json has a script named "deploy" \u2014 CI may auto-deploy it; rename to deploy:app`);
|
|
4648
|
+
}
|
|
4649
|
+
return warnings;
|
|
4650
|
+
}
|
|
4651
|
+
function o11yProjectWarnings(rootDir) {
|
|
4652
|
+
const warnings = [];
|
|
4653
|
+
const pkg = readPackageJson(rootDir);
|
|
4654
|
+
const dependencies = pkg?.dependencies;
|
|
4655
|
+
const devDependencies = pkg?.devDependencies;
|
|
4656
|
+
if (!dependencies?.["@odla-ai/o11y"]) {
|
|
4657
|
+
warnings.push(
|
|
4658
|
+
devDependencies?.["@odla-ai/o11y"] ? "@odla-ai/o11y is a devDependency \u2014 move it to dependencies so the Worker can import it" : 'Worker instrumentation is missing @odla-ai/o11y \u2014 install it and wrap the handler with "withObservability"'
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
const configPath = findWranglerConfig(rootDir);
|
|
4662
|
+
const config = configPath ? readWranglerConfig(configPath) : null;
|
|
4663
|
+
if (!configPath || !config) {
|
|
4664
|
+
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
4665
|
+
return warnings;
|
|
4666
|
+
}
|
|
4667
|
+
const main = typeof config.main === "string" ? resolve6(rootDir, config.main) : null;
|
|
4668
|
+
if (!main || !existsSync6(main)) {
|
|
4669
|
+
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
4670
|
+
} else {
|
|
4671
|
+
let source = "";
|
|
4672
|
+
try {
|
|
4673
|
+
source = readFileSync5(main, "utf8");
|
|
4674
|
+
} catch {
|
|
4675
|
+
}
|
|
4676
|
+
if (!/\bwithObservability\b/.test(source)) {
|
|
4677
|
+
warnings.push(`wrangler entrypoint ${config.main} does not reference withObservability`);
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
const flags = Array.isArray(config.compatibility_flags) ? config.compatibility_flags : [];
|
|
4681
|
+
if (!flags.includes("nodejs_compat")) {
|
|
4682
|
+
warnings.push('wrangler compatibility_flags is missing "nodejs_compat" required by @odla-ai/o11y');
|
|
4683
|
+
}
|
|
4684
|
+
return warnings;
|
|
4685
|
+
}
|
|
4686
|
+
function calendarProjectWarnings(rootDir) {
|
|
4687
|
+
const pkg = readPackageJson(rootDir);
|
|
4688
|
+
const dependencies = pkg?.dependencies;
|
|
4689
|
+
const devDependencies = pkg?.devDependencies;
|
|
4690
|
+
if (dependencies?.["@odla-ai/calendar"]) return [];
|
|
4691
|
+
return [
|
|
4692
|
+
devDependencies?.["@odla-ai/calendar"] ? "@odla-ai/calendar is a devDependency \u2014 move it to dependencies so trusted Worker code can import it" : "calendar service is enabled but @odla-ai/calendar is not installed in dependencies"
|
|
4693
|
+
];
|
|
4694
|
+
}
|
|
4695
|
+
function readPackageJson(rootDir) {
|
|
4696
|
+
try {
|
|
4697
|
+
return JSON.parse(readFileSync5(join5(rootDir, "package.json"), "utf8"));
|
|
4698
|
+
} catch {
|
|
4699
|
+
return null;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
// src/integrations.ts
|
|
4704
|
+
import { isDeepStrictEqual } from "util";
|
|
4705
|
+
async function resolveDatabaseConfig(cfg, options = {}) {
|
|
4706
|
+
const integrations = cfg.integrations ?? [];
|
|
4707
|
+
if (!cfg.services.includes("db")) return { schema: void 0, rules: void 0, integrations };
|
|
4708
|
+
const authoredSchema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
|
|
4709
|
+
const schema = mergeSchemas(authoredSchema, integrations);
|
|
4710
|
+
if (options.validateSeeds !== false) assertSeedContracts(integrations, schema);
|
|
4711
|
+
const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
|
|
4712
|
+
const explicitRules = mergeRules(configuredRules, integrations);
|
|
4713
|
+
const rules = configuredRules === void 0 && schema && cfg.db?.defaultRules !== false ? { ...rulesFromSchema(schema), ...explicitRules ?? {} } : explicitRules;
|
|
4714
|
+
return { schema, rules, integrations };
|
|
4715
|
+
}
|
|
4716
|
+
function integrationWarnings(integrations, schema, rules) {
|
|
4717
|
+
const warnings = [];
|
|
4718
|
+
const entities = new Set(serializedEntities(schema));
|
|
1775
4719
|
for (const integration of integrations) {
|
|
1776
4720
|
const namespaces = Object.keys(integration.schema?.entities ?? {});
|
|
1777
4721
|
for (const ns of namespaces) {
|
|
@@ -1932,13 +4876,13 @@ async function doctor(options) {
|
|
|
1932
4876
|
}
|
|
1933
4877
|
|
|
1934
4878
|
// src/init.ts
|
|
1935
|
-
import { existsSync as
|
|
1936
|
-
import { dirname as
|
|
4879
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4880
|
+
import { dirname as dirname5, resolve as resolve7 } from "path";
|
|
1937
4881
|
function initProject(options) {
|
|
1938
4882
|
const out = options.stdout ?? console;
|
|
1939
|
-
const rootDir =
|
|
1940
|
-
const configPath =
|
|
1941
|
-
if (
|
|
4883
|
+
const rootDir = resolve7(options.rootDir ?? process.cwd());
|
|
4884
|
+
const configPath = resolve7(rootDir, options.configPath ?? "odla.config.mjs");
|
|
4885
|
+
if (existsSync7(configPath) && !options.force) {
|
|
1942
4886
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
1943
4887
|
}
|
|
1944
4888
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -1948,19 +4892,19 @@ function initProject(options) {
|
|
|
1948
4892
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
1949
4893
|
if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
|
|
1950
4894
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
1951
|
-
mkdirSync2(
|
|
1952
|
-
mkdirSync2(
|
|
1953
|
-
mkdirSync2(
|
|
4895
|
+
mkdirSync2(dirname5(configPath), { recursive: true });
|
|
4896
|
+
mkdirSync2(resolve7(rootDir, "src/odla"), { recursive: true });
|
|
4897
|
+
mkdirSync2(resolve7(rootDir, ".odla"), { recursive: true });
|
|
1954
4898
|
writeFileSync2(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
1955
|
-
writeIfMissing(
|
|
1956
|
-
writeIfMissing(
|
|
4899
|
+
writeIfMissing(resolve7(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
4900
|
+
writeIfMissing(resolve7(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
1957
4901
|
ensureGitignore(rootDir);
|
|
1958
4902
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
1959
4903
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
1960
4904
|
out.log("updated .gitignore for local odla credentials");
|
|
1961
4905
|
}
|
|
1962
4906
|
function writeIfMissing(path, text) {
|
|
1963
|
-
if (
|
|
4907
|
+
if (existsSync7(path)) return;
|
|
1964
4908
|
writeFileSync2(path, text);
|
|
1965
4909
|
}
|
|
1966
4910
|
function configTemplate(input) {
|
|
@@ -2432,10 +5376,10 @@ async function provision(options) {
|
|
|
2432
5376
|
stdout: out
|
|
2433
5377
|
});
|
|
2434
5378
|
} catch (error) {
|
|
2435
|
-
const
|
|
5379
|
+
const message3 = error instanceof Error ? error.message : String(error);
|
|
2436
5380
|
const consent = env === "prod" || env === "production" ? " --yes" : "";
|
|
2437
5381
|
throw new Error(
|
|
2438
|
-
`${
|
|
5382
|
+
`${message3}
|
|
2439
5383
|
${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
|
|
2440
5384
|
{ cause: error }
|
|
2441
5385
|
);
|
|
@@ -2498,8 +5442,8 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
2498
5442
|
});
|
|
2499
5443
|
if (res.status === 404) return null;
|
|
2500
5444
|
if (!res.ok) return null;
|
|
2501
|
-
const
|
|
2502
|
-
return
|
|
5445
|
+
const json2 = await res.json();
|
|
5446
|
+
return json2.app ?? null;
|
|
2503
5447
|
}
|
|
2504
5448
|
async function postJson2(doFetch, url, bearer, body) {
|
|
2505
5449
|
const res = await doFetch(url, {
|
|
@@ -2586,7 +5530,7 @@ async function resolveVaultWrite(options) {
|
|
|
2586
5530
|
}
|
|
2587
5531
|
|
|
2588
5532
|
// src/security.ts
|
|
2589
|
-
import { isAbsolute as isAbsolute3, relative as
|
|
5533
|
+
import { isAbsolute as isAbsolute3, relative as relative4, resolve as resolve8, sep as sep3 } from "path";
|
|
2590
5534
|
import {
|
|
2591
5535
|
cloudflareAppProfile,
|
|
2592
5536
|
createPlatformSecurityReasoners,
|
|
@@ -2605,9 +5549,9 @@ async function runHostedSecurity(options) {
|
|
|
2605
5549
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
2606
5550
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
2607
5551
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
2608
|
-
const target =
|
|
2609
|
-
const output =
|
|
2610
|
-
const outputRelative =
|
|
5552
|
+
const target = resolve8(options.target ?? cfg?.rootDir ?? ".");
|
|
5553
|
+
const output = resolve8(options.out ?? resolve8(target, ".odla/security/hosted"));
|
|
5554
|
+
const outputRelative = relative4(target, output).split(sep3).join("/");
|
|
2611
5555
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
2612
5556
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
2613
5557
|
const tokenRequest = {
|
|
@@ -2634,280 +5578,66 @@ async function runHostedSecurity(options) {
|
|
|
2634
5578
|
...selfAudit ? { selfAudit: { enabled: true, subject: "service:odla-security-self-audit" } } : {},
|
|
2635
5579
|
fetch: options.fetch,
|
|
2636
5580
|
signal: options.signal
|
|
2637
|
-
});
|
|
2638
|
-
const harness = createSecurityHarness({
|
|
2639
|
-
profile,
|
|
2640
|
-
store: new FileRunStore(
|
|
2641
|
-
discoveryReasoner: hosted.discoveryReasoner,
|
|
2642
|
-
validationReasoner: hosted.validationReasoner,
|
|
2643
|
-
policy: {
|
|
2644
|
-
modelSourceDisclosure: "redacted",
|
|
2645
|
-
active: false,
|
|
2646
|
-
allowNetwork: false
|
|
2647
|
-
}
|
|
2648
|
-
});
|
|
2649
|
-
const report = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
2650
|
-
await writeSecurityArtifacts(output, report);
|
|
2651
|
-
const reportDigest = await securityFingerprint(report);
|
|
2652
|
-
await hosted.complete({
|
|
2653
|
-
reportDigest,
|
|
2654
|
-
coverageStatus: report.coverageStatus,
|
|
2655
|
-
confirmed: report.metrics.confirmed,
|
|
2656
|
-
candidates: report.metrics.candidates
|
|
2657
|
-
}, { signal: options.signal });
|
|
2658
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run, report, output);
|
|
2659
|
-
return Object.freeze({ report, run: hosted.run, output });
|
|
2660
|
-
}
|
|
2661
|
-
function selectEnv(requested, declared, configPath, rootDir) {
|
|
2662
|
-
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
2663
|
-
if (!env || !declared.includes(env)) {
|
|
2664
|
-
const shown = relative2(rootDir, configPath) || configPath;
|
|
2665
|
-
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
2666
|
-
}
|
|
2667
|
-
return env;
|
|
2668
|
-
}
|
|
2669
|
-
async function injectedToken(options, request) {
|
|
2670
|
-
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
|
|
2671
|
-
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
2672
|
-
throw new Error(
|
|
2673
|
-
request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
2674
|
-
);
|
|
2675
|
-
}
|
|
2676
|
-
return value;
|
|
2677
|
-
}
|
|
2678
|
-
function profileFor(name, maxHuntTasks) {
|
|
2679
|
-
const profile = name === "odla" ? odlaProfile() : name === "cloudflare-app" ? cloudflareAppProfile() : name === "generic" ? genericProfile() : void 0;
|
|
2680
|
-
if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
|
|
2681
|
-
if (maxHuntTasks === void 0) return profile;
|
|
2682
|
-
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
2683
|
-
return { ...profile, maxHuntTasks };
|
|
2684
|
-
}
|
|
2685
|
-
function printSummary(out, appId, env, run, report, output) {
|
|
2686
|
-
const complete = report.coverage.filter((cell) => cell.state === "complete").length;
|
|
2687
|
-
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
2688
|
-
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
2689
|
-
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
2690
|
-
out.log(` coverage: ${report.coverageStatus} ${complete}/${report.coverage.length} blocked=${report.metrics.blockedCells} shallow=${report.metrics.shallowCells} unscheduled=${report.metrics.unscheduledCells} budget_exhausted=${report.metrics.budgetExhaustedCells}`);
|
|
2691
|
-
if (report.callBudget) out.log(` calls: discovery=${formatBudget(report.callBudget.discovery)} validation=${formatBudget(report.callBudget.validation)}`);
|
|
2692
|
-
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates}`);
|
|
2693
|
-
out.log(` report: ${resolve5(output, "REPORT.md")}`);
|
|
2694
|
-
}
|
|
2695
|
-
function formatBudget(usage) {
|
|
2696
|
-
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
// src/security-hosted-github.ts
|
|
2700
|
-
import { execFile } from "child_process";
|
|
2701
|
-
import { promisify } from "util";
|
|
2702
|
-
|
|
2703
|
-
// src/security-hosted-request.ts
|
|
2704
|
-
async function requestHostedSecurityJson(options, path, init, action) {
|
|
2705
|
-
const platform = platformAudience(options.platform);
|
|
2706
|
-
const token = hostedSecurityCredential(options.token);
|
|
2707
|
-
const requestInit = {
|
|
2708
|
-
...init,
|
|
2709
|
-
redirect: "error",
|
|
2710
|
-
credentials: "omit",
|
|
2711
|
-
cache: "no-store",
|
|
2712
|
-
signal: options.signal,
|
|
2713
|
-
headers: {
|
|
2714
|
-
authorization: `Bearer ${token}`,
|
|
2715
|
-
"content-type": "application/json",
|
|
2716
|
-
...init.headers
|
|
2717
|
-
}
|
|
2718
|
-
};
|
|
2719
|
-
const response = await (options.fetch ?? fetch)(new URL(path, platform), requestInit);
|
|
2720
|
-
const body = await response.json().catch(() => ({}));
|
|
2721
|
-
if (!response.ok) {
|
|
2722
|
-
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
2723
|
-
const message = optionalHostedText(body.error?.message, "error message", 300);
|
|
2724
|
-
throw new Error(`${action} failed (${response.status})${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`);
|
|
2725
|
-
}
|
|
2726
|
-
return body;
|
|
2727
|
-
}
|
|
2728
|
-
function hostedIdentifier(value, name) {
|
|
2729
|
-
const result = optionalHostedText(value, name, 180);
|
|
2730
|
-
if (!result || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(result)) {
|
|
2731
|
-
throw new Error(`${name} is invalid`);
|
|
2732
|
-
}
|
|
2733
|
-
return result;
|
|
2734
|
-
}
|
|
2735
|
-
function positivePolicyVersion(value, name) {
|
|
2736
|
-
if (!Number.isSafeInteger(value) || value < 1) {
|
|
2737
|
-
throw new Error(`${name} is invalid`);
|
|
2738
|
-
}
|
|
2739
|
-
return value;
|
|
2740
|
-
}
|
|
2741
|
-
function optionalHostedText(value, name, maxLength) {
|
|
2742
|
-
if (value === void 0 || value === null || value === "") return void 0;
|
|
2743
|
-
if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
2744
|
-
throw new Error(`${name} is invalid`);
|
|
2745
|
-
}
|
|
2746
|
-
return value;
|
|
2747
|
-
}
|
|
2748
|
-
function githubRepositoryName(value) {
|
|
2749
|
-
if (typeof value !== "string" || value.length > 201) {
|
|
2750
|
-
throw new Error("repository must be owner/name");
|
|
2751
|
-
}
|
|
2752
|
-
const parts = value.split("/");
|
|
2753
|
-
const owner = parts[0] ?? "";
|
|
2754
|
-
const name = (parts[1] ?? "").replace(/\.git$/i, "");
|
|
2755
|
-
if (parts.length !== 2 || !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,98}[A-Za-z0-9])?$/.test(owner) || !/^[A-Za-z0-9_.-]{1,100}$/.test(name) || name === "." || name === "..") {
|
|
2756
|
-
throw new Error("repository must be owner/name");
|
|
2757
|
-
}
|
|
2758
|
-
return `${owner}/${name}`;
|
|
2759
|
-
}
|
|
2760
|
-
function trustedGitHubInstallUrl(value) {
|
|
2761
|
-
let url;
|
|
2762
|
-
try {
|
|
2763
|
-
url = new URL(value);
|
|
2764
|
-
} catch {
|
|
2765
|
-
throw new Error("odla.ai returned an invalid GitHub installation URL");
|
|
2766
|
-
}
|
|
2767
|
-
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
|
|
2768
|
-
throw new Error("odla.ai returned an untrusted GitHub installation URL");
|
|
2769
|
-
}
|
|
2770
|
-
return url.toString();
|
|
2771
|
-
}
|
|
2772
|
-
function hostedPollInterval(value = 2e3) {
|
|
2773
|
-
if (!Number.isSafeInteger(value) || value < 100 || value > 3e4) {
|
|
2774
|
-
throw new Error("poll interval must be 100-30000ms");
|
|
2775
|
-
}
|
|
2776
|
-
return value;
|
|
2777
|
-
}
|
|
2778
|
-
function hostedPollTimeout(value = 10 * 6e4) {
|
|
2779
|
-
if (!Number.isSafeInteger(value) || value < 1e3 || value > 60 * 6e4) {
|
|
2780
|
-
throw new Error("poll timeout must be 1000-3600000ms");
|
|
2781
|
-
}
|
|
2782
|
-
return value;
|
|
2783
|
-
}
|
|
2784
|
-
async function waitForHostedPoll(milliseconds, signal) {
|
|
2785
|
-
if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
|
|
2786
|
-
await new Promise((resolve7, reject) => {
|
|
2787
|
-
const timer = setTimeout(resolve7, milliseconds);
|
|
2788
|
-
signal?.addEventListener("abort", () => {
|
|
2789
|
-
clearTimeout(timer);
|
|
2790
|
-
reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
2791
|
-
}, { once: true });
|
|
2792
|
-
});
|
|
2793
|
-
}
|
|
2794
|
-
function isValidHostedSecurityPlan(value, env) {
|
|
2795
|
-
if (!value || value.env !== env || !/^sha256:[a-f0-9]{64}$/.test(value.planDigest) || value.consentContract !== "odla.hosted-security-consent.v1" || value.reportProjection !== "odla.hosted-security-report.v1" || value.redactionContract !== "odla.best-effort-credential-pattern-redaction.v1" || typeof value.promptBundle !== "string" || value.promptBundle.length < 1 || value.promptBundle.length > 100 || typeof value.ready !== "boolean" || typeof value.independent !== "boolean" || value.sourceDisclosure !== "redacted" || value.targetExecution !== false || !Number.isSafeInteger(value.reportRetentionDays) || value.reportRetentionDays < 1) return false;
|
|
2796
|
-
const validRoute = (route, purpose) => !!route && route.purpose === purpose && typeof route.enabled === "boolean" && typeof route.credentialReady === "boolean" && typeof route.provider === "string" && route.provider.length > 0 && route.provider.length <= 100 && typeof route.model === "string" && route.model.length > 0 && route.model.length <= 200 && Number.isSafeInteger(route.policyVersion) && route.policyVersion >= 1 && Number.isSafeInteger(route.maxCallsPerRun) && route.maxCallsPerRun >= 1 && Number.isSafeInteger(route.maxInputBytes) && route.maxInputBytes >= 1 && Number.isSafeInteger(route.maxOutputTokens) && route.maxOutputTokens >= 1;
|
|
2797
|
-
return validRoute(value.routes?.discovery, "security.discovery") && validRoute(value.routes?.validation, "security.validation");
|
|
2798
|
-
}
|
|
2799
|
-
function hostedSecurityCredential(value) {
|
|
2800
|
-
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
2801
|
-
throw new Error("Hosted security requires an injected odla developer token");
|
|
2802
|
-
}
|
|
2803
|
-
return value;
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
|
-
// src/security-hosted-github.ts
|
|
2807
|
-
async function connectGitHubSecuritySource(options) {
|
|
2808
|
-
const out = options.stdout ?? console;
|
|
2809
|
-
const repository = githubRepositoryName(options.repository);
|
|
2810
|
-
const attempt = await requestHostedSecurityJson(
|
|
2811
|
-
options,
|
|
2812
|
-
"/registry/github/connect",
|
|
2813
|
-
{
|
|
2814
|
-
method: "POST",
|
|
2815
|
-
body: JSON.stringify({
|
|
2816
|
-
appId: hostedIdentifier(options.appId, "appId"),
|
|
2817
|
-
env: hostedIdentifier(options.env, "env"),
|
|
2818
|
-
repository
|
|
2819
|
-
})
|
|
2820
|
-
},
|
|
2821
|
-
"start GitHub connection"
|
|
2822
|
-
);
|
|
2823
|
-
const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
|
|
2824
|
-
if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
|
|
2825
|
-
throw new Error("odla.ai returned an invalid GitHub connection attempt");
|
|
2826
|
-
}
|
|
2827
|
-
const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
|
|
2828
|
-
out.log(`GitHub approval: ${installUrl}`);
|
|
2829
|
-
if (options.open !== false) {
|
|
2830
|
-
await (options.openInstallUrl ?? openUrl)(installUrl);
|
|
2831
|
-
out.log("github: opened installation approval in your browser");
|
|
2832
|
-
}
|
|
2833
|
-
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
2834
|
-
const now = options.now ?? Date.now;
|
|
2835
|
-
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
2836
|
-
const wait = options.wait ?? waitForHostedPoll;
|
|
2837
|
-
while (true) {
|
|
2838
|
-
const state = await requestHostedSecurityJson(
|
|
2839
|
-
options,
|
|
2840
|
-
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
2841
|
-
{},
|
|
2842
|
-
"check GitHub connection"
|
|
2843
|
-
);
|
|
2844
|
-
if (state.status !== "pending") {
|
|
2845
|
-
if (state.status === "connected") return state;
|
|
2846
|
-
throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
|
|
5581
|
+
});
|
|
5582
|
+
const harness = createSecurityHarness({
|
|
5583
|
+
profile,
|
|
5584
|
+
store: new FileRunStore(resolve8(output, "state")),
|
|
5585
|
+
discoveryReasoner: hosted.discoveryReasoner,
|
|
5586
|
+
validationReasoner: hosted.validationReasoner,
|
|
5587
|
+
policy: {
|
|
5588
|
+
modelSourceDisclosure: "redacted",
|
|
5589
|
+
active: false,
|
|
5590
|
+
allowNetwork: false
|
|
2847
5591
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
);
|
|
2861
|
-
return Array.isArray(body.sources) ? body.sources : [];
|
|
2862
|
-
}
|
|
2863
|
-
async function disconnectGitHubSecuritySource(options) {
|
|
2864
|
-
const appId = hostedIdentifier(options.appId, "appId");
|
|
2865
|
-
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
2866
|
-
await requestHostedSecurityJson(
|
|
2867
|
-
options,
|
|
2868
|
-
`/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
|
|
2869
|
-
{ method: "DELETE" },
|
|
2870
|
-
"disconnect GitHub security source"
|
|
2871
|
-
);
|
|
5592
|
+
});
|
|
5593
|
+
const report2 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
5594
|
+
await writeSecurityArtifacts(output, report2);
|
|
5595
|
+
const reportDigest = await securityFingerprint(report2);
|
|
5596
|
+
await hosted.complete({
|
|
5597
|
+
reportDigest,
|
|
5598
|
+
coverageStatus: report2.coverageStatus,
|
|
5599
|
+
confirmed: report2.metrics.confirmed,
|
|
5600
|
+
candidates: report2.metrics.candidates
|
|
5601
|
+
}, { signal: options.signal });
|
|
5602
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report2, output);
|
|
5603
|
+
return Object.freeze({ report: report2, run: hosted.run, output });
|
|
2872
5604
|
}
|
|
2873
|
-
function
|
|
2874
|
-
const
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
try {
|
|
2879
|
-
url = new URL(remote);
|
|
2880
|
-
} catch {
|
|
2881
|
-
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
2882
|
-
}
|
|
2883
|
-
if (url.hostname !== "github.com" || url.protocol !== "https:" && url.protocol !== "ssh:") {
|
|
2884
|
-
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
2885
|
-
}
|
|
2886
|
-
if (url.password || url.protocol === "https:" && url.username || url.search || url.hash) {
|
|
2887
|
-
throw new Error("credential-bearing GitHub origin URLs are not accepted");
|
|
2888
|
-
}
|
|
2889
|
-
const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
2890
|
-
if (parts.length !== 2) {
|
|
2891
|
-
throw new Error("origin must identify one GitHub owner/name repository");
|
|
5605
|
+
function selectEnv(requested, declared, configPath, rootDir) {
|
|
5606
|
+
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
5607
|
+
if (!env || !declared.includes(env)) {
|
|
5608
|
+
const shown = relative4(rootDir, configPath) || configPath;
|
|
5609
|
+
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
2892
5610
|
}
|
|
2893
|
-
return
|
|
5611
|
+
return env;
|
|
2894
5612
|
}
|
|
2895
|
-
async function
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
5613
|
+
async function injectedToken(options, request) {
|
|
5614
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request }));
|
|
5615
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
5616
|
+
throw new Error(
|
|
5617
|
+
request.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
5618
|
+
);
|
|
2901
5619
|
}
|
|
2902
|
-
return
|
|
5620
|
+
return value;
|
|
2903
5621
|
}
|
|
2904
|
-
|
|
2905
|
-
const
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
5622
|
+
function profileFor(name, maxHuntTasks) {
|
|
5623
|
+
const profile = name === "odla" ? odlaProfile() : name === "cloudflare-app" ? cloudflareAppProfile() : name === "generic" ? genericProfile() : void 0;
|
|
5624
|
+
if (!profile) throw new Error(`unknown security profile "${String(name)}"`);
|
|
5625
|
+
if (maxHuntTasks === void 0) return profile;
|
|
5626
|
+
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
5627
|
+
return { ...profile, maxHuntTasks };
|
|
5628
|
+
}
|
|
5629
|
+
function printSummary(out, appId, env, run, report2, output) {
|
|
5630
|
+
const complete = report2.coverage.filter((cell) => cell.state === "complete").length;
|
|
5631
|
+
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
5632
|
+
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
5633
|
+
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
5634
|
+
out.log(` coverage: ${report2.coverageStatus} ${complete}/${report2.coverage.length} blocked=${report2.metrics.blockedCells} shallow=${report2.metrics.shallowCells} unscheduled=${report2.metrics.unscheduledCells} budget_exhausted=${report2.metrics.budgetExhaustedCells}`);
|
|
5635
|
+
if (report2.callBudget) out.log(` calls: discovery=${formatBudget(report2.callBudget.discovery)} validation=${formatBudget(report2.callBudget.validation)}`);
|
|
5636
|
+
out.log(` findings: confirmed=${report2.metrics.confirmed} needs_reproduction=${report2.metrics.needsReproduction} candidates=${report2.metrics.candidates}`);
|
|
5637
|
+
out.log(` report: ${resolve8(output, "REPORT.md")}`);
|
|
5638
|
+
}
|
|
5639
|
+
function formatBudget(usage) {
|
|
5640
|
+
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
2911
5641
|
}
|
|
2912
5642
|
|
|
2913
5643
|
// src/security-hosted-intent.ts
|
|
@@ -2919,19 +5649,19 @@ async function getHostedSecurityIntent(options) {
|
|
|
2919
5649
|
const query = new URLSearchParams({ env, sourceId });
|
|
2920
5650
|
if (ref) query.set("ref", ref);
|
|
2921
5651
|
if (options.profile) query.set("profile", options.profile);
|
|
2922
|
-
const
|
|
5652
|
+
const response2 = await requestHostedSecurityJson(
|
|
2923
5653
|
options,
|
|
2924
5654
|
`/registry/apps/${encodeURIComponent(appId)}/security/intent?${query.toString()}`,
|
|
2925
5655
|
{},
|
|
2926
5656
|
"read hosted security execution intent"
|
|
2927
5657
|
);
|
|
2928
|
-
if (!isValidHostedSecurityPlan(
|
|
5658
|
+
if (!isValidHostedSecurityPlan(response2.plan, env) || !isValidIntent(response2.intent, { appId, env, sourceId })) {
|
|
2929
5659
|
throw new Error("odla.ai returned an invalid hosted security execution intent");
|
|
2930
5660
|
}
|
|
2931
|
-
if (
|
|
5661
|
+
if (response2.intent.planDigest !== response2.plan.planDigest) {
|
|
2932
5662
|
throw new Error("odla.ai returned a hosted security intent for a different processing plan");
|
|
2933
5663
|
}
|
|
2934
|
-
return
|
|
5664
|
+
return response2;
|
|
2935
5665
|
}
|
|
2936
5666
|
function isValidIntent(value, expected) {
|
|
2937
5667
|
return !!value && value.executionContract === "odla.hosted-security-execution-consent.v1" && /^sha256:[a-f0-9]{64}$/.test(value.executionDigest) && /^sha256:[a-f0-9]{64}$/.test(value.planDigest) && value.appId === expected.appId && value.env === expected.env && value.sourceId === expected.sourceId && typeof value.repository === "string" && value.repository.length > 2 && value.repository.length <= 201 && typeof value.defaultBranch === "string" && value.defaultBranch.length > 0 && value.defaultBranch.length <= 255 && typeof value.requestedRef === "string" && value.requestedRef.length > 0 && value.requestedRef.length <= 255 && !/[\u0000-\u001f\u007f]/.test(value.requestedRef) && ["odla", "cloudflare-app", "generic"].includes(value.profile) && value.sourceDisclosure === "redacted";
|
|
@@ -2994,8 +5724,8 @@ async function startHostedSecurityJob(options) {
|
|
|
2994
5724
|
"start hosted security job"
|
|
2995
5725
|
);
|
|
2996
5726
|
} catch (error) {
|
|
2997
|
-
const
|
|
2998
|
-
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(
|
|
5727
|
+
const message3 = error instanceof Error ? error.message : String(error);
|
|
5728
|
+
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(message3)) {
|
|
2999
5729
|
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 });
|
|
3000
5730
|
}
|
|
3001
5731
|
throw error;
|
|
@@ -3030,7 +5760,7 @@ async function followHostedSecurityJob(options) {
|
|
|
3030
5760
|
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
3031
5761
|
const now = options.now ?? Date.now;
|
|
3032
5762
|
const deadline = now() + hostedPollTimeout(options.pollTimeoutMs ?? 60 * 6e4);
|
|
3033
|
-
const
|
|
5763
|
+
const wait2 = options.wait ?? waitForHostedPoll;
|
|
3034
5764
|
let prior = "";
|
|
3035
5765
|
while (true) {
|
|
3036
5766
|
const job = await getHostedSecurityJob(options);
|
|
@@ -3041,7 +5771,7 @@ async function followHostedSecurityJob(options) {
|
|
|
3041
5771
|
if (now() >= deadline) {
|
|
3042
5772
|
throw new Error(`Hosted security job ${job.jobId} did not finish before the polling deadline`);
|
|
3043
5773
|
}
|
|
3044
|
-
await
|
|
5774
|
+
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
3045
5775
|
}
|
|
3046
5776
|
}
|
|
3047
5777
|
async function getHostedSecurityReport(options) {
|
|
@@ -3074,9 +5804,9 @@ function securityExecutionDigest(value) {
|
|
|
3074
5804
|
}
|
|
3075
5805
|
|
|
3076
5806
|
// src/skill.ts
|
|
3077
|
-
import { existsSync as
|
|
5807
|
+
import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3078
5808
|
import { homedir } from "os";
|
|
3079
|
-
import { dirname as
|
|
5809
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join6, relative as relative5, resolve as resolve9, sep as sep4 } from "path";
|
|
3080
5810
|
import { fileURLToPath } from "url";
|
|
3081
5811
|
|
|
3082
5812
|
// src/skill-adapters.ts
|
|
@@ -3137,8 +5867,8 @@ function installSkill(options = {}) {
|
|
|
3137
5867
|
const files = listFiles(sourceDir);
|
|
3138
5868
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
3139
5869
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
3140
|
-
const root =
|
|
3141
|
-
const home =
|
|
5870
|
+
const root = resolve9(options.dir ?? process.cwd());
|
|
5871
|
+
const home = resolve9(options.homeDir ?? homedir());
|
|
3142
5872
|
const plans = /* @__PURE__ */ new Map();
|
|
3143
5873
|
const targets = /* @__PURE__ */ new Map();
|
|
3144
5874
|
const rememberTarget = (harness, target) => {
|
|
@@ -3152,48 +5882,48 @@ function installSkill(options = {}) {
|
|
|
3152
5882
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
3153
5883
|
};
|
|
3154
5884
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3155
|
-
for (const rel of files) plan(
|
|
5885
|
+
for (const rel of files) plan(join6(targetDir2, rel), readFileSync6(join6(sourceDir, rel), "utf8"), false, boundary);
|
|
3156
5886
|
};
|
|
3157
5887
|
let targetDir;
|
|
3158
5888
|
if (options.global) {
|
|
3159
|
-
const claudeRoot =
|
|
3160
|
-
const codexRoot =
|
|
5889
|
+
const claudeRoot = join6(home, ".claude", "skills");
|
|
5890
|
+
const codexRoot = resolve9(options.codexHomeDir ?? process.env.CODEX_HOME ?? join6(home, ".codex"), "skills");
|
|
3161
5891
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
3162
5892
|
for (const harness of harnesses) {
|
|
3163
5893
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
3164
|
-
planSkillTree(skillRoot, harness === "claude" ? home :
|
|
5894
|
+
planSkillTree(skillRoot, harness === "claude" ? home : dirname6(dirname6(codexRoot)));
|
|
3165
5895
|
rememberTarget(harness, skillRoot);
|
|
3166
5896
|
}
|
|
3167
5897
|
} else {
|
|
3168
|
-
const sharedRoot =
|
|
5898
|
+
const sharedRoot = join6(root, ".agents", "skills");
|
|
3169
5899
|
planSkillTree(sharedRoot);
|
|
3170
|
-
const claudeRoot =
|
|
5900
|
+
const claudeRoot = join6(root, ".claude", "skills");
|
|
3171
5901
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
3172
5902
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
3173
5903
|
if (harnesses.includes("claude")) {
|
|
3174
5904
|
for (const skill of skillNames(files)) {
|
|
3175
|
-
const canonical =
|
|
3176
|
-
plan(
|
|
5905
|
+
const canonical = readFileSync6(join6(sourceDir, skill, "SKILL.md"), "utf8");
|
|
5906
|
+
plan(join6(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
3177
5907
|
}
|
|
3178
5908
|
rememberTarget("claude", claudeRoot);
|
|
3179
5909
|
}
|
|
3180
5910
|
if (harnesses.includes("cursor")) {
|
|
3181
|
-
const cursorRule =
|
|
5911
|
+
const cursorRule = join6(root, ".cursor", "rules", "odla.mdc");
|
|
3182
5912
|
plan(cursorRule, CURSOR_RULE);
|
|
3183
5913
|
rememberTarget("cursor", cursorRule);
|
|
3184
5914
|
}
|
|
3185
5915
|
if (harnesses.includes("agents")) {
|
|
3186
|
-
const agentsFile =
|
|
5916
|
+
const agentsFile = join6(root, "AGENTS.md");
|
|
3187
5917
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3188
5918
|
rememberTarget("agents", agentsFile);
|
|
3189
5919
|
}
|
|
3190
5920
|
if (harnesses.includes("copilot")) {
|
|
3191
|
-
const copilotFile =
|
|
5921
|
+
const copilotFile = join6(root, ".github", "copilot-instructions.md");
|
|
3192
5922
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3193
5923
|
rememberTarget("copilot", copilotFile);
|
|
3194
5924
|
}
|
|
3195
5925
|
if (harnesses.includes("gemini")) {
|
|
3196
|
-
const geminiFile =
|
|
5926
|
+
const geminiFile = join6(root, "GEMINI.md");
|
|
3197
5927
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3198
5928
|
rememberTarget("gemini", geminiFile);
|
|
3199
5929
|
}
|
|
@@ -3207,11 +5937,11 @@ function installSkill(options = {}) {
|
|
|
3207
5937
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
3208
5938
|
continue;
|
|
3209
5939
|
}
|
|
3210
|
-
if (!
|
|
5940
|
+
if (!existsSync8(file.target)) {
|
|
3211
5941
|
writtenPaths.add(file.target);
|
|
3212
5942
|
continue;
|
|
3213
5943
|
}
|
|
3214
|
-
const current =
|
|
5944
|
+
const current = readFileSync6(file.target, "utf8");
|
|
3215
5945
|
if (current === file.content) {
|
|
3216
5946
|
unchangedPaths.add(file.target);
|
|
3217
5947
|
} else if (file.managedMerge || options.force) {
|
|
@@ -3228,8 +5958,8 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3228
5958
|
);
|
|
3229
5959
|
}
|
|
3230
5960
|
for (const file of plans.values()) {
|
|
3231
|
-
if (!
|
|
3232
|
-
mkdirSync3(
|
|
5961
|
+
if (!existsSync8(file.target) || readFileSync6(file.target, "utf8") !== file.content) {
|
|
5962
|
+
mkdirSync3(dirname6(file.target), { recursive: true });
|
|
3233
5963
|
writeFileSync3(file.target, file.content);
|
|
3234
5964
|
}
|
|
3235
5965
|
}
|
|
@@ -3249,7 +5979,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3249
5979
|
};
|
|
3250
5980
|
}
|
|
3251
5981
|
function pathsUnder(root, paths) {
|
|
3252
|
-
return [...paths].map((path) =>
|
|
5982
|
+
return [...paths].map((path) => relative5(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${sep4}`) && !isAbsolute4(path)).sort();
|
|
3253
5983
|
}
|
|
3254
5984
|
function normalizeHarnesses(values, global) {
|
|
3255
5985
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -3271,9 +6001,9 @@ function normalizeHarnesses(values, global) {
|
|
|
3271
6001
|
function managedFileContent(path, block, force, boundary) {
|
|
3272
6002
|
const symlink = symlinkedComponent(boundary, path);
|
|
3273
6003
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
3274
|
-
if (!
|
|
6004
|
+
if (!existsSync8(path)) return `${block}
|
|
3275
6005
|
`;
|
|
3276
|
-
const current =
|
|
6006
|
+
const current = readFileSync6(path, "utf8");
|
|
3277
6007
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
3278
6008
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
3279
6009
|
const startAt = current.indexOf(start);
|
|
@@ -3294,13 +6024,13 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
3294
6024
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
3295
6025
|
}
|
|
3296
6026
|
function symlinkedComponent(boundary, target) {
|
|
3297
|
-
const rel =
|
|
3298
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
6027
|
+
const rel = relative5(boundary, target);
|
|
6028
|
+
if (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute4(rel)) {
|
|
3299
6029
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
3300
6030
|
}
|
|
3301
6031
|
let current = boundary;
|
|
3302
|
-
for (const part of rel.split(
|
|
3303
|
-
current =
|
|
6032
|
+
for (const part of rel.split(sep4).filter(Boolean)) {
|
|
6033
|
+
current = join6(current, part);
|
|
3304
6034
|
try {
|
|
3305
6035
|
if (lstatSync(current).isSymbolicLink()) return current;
|
|
3306
6036
|
} catch (error) {
|
|
@@ -3313,13 +6043,13 @@ function skillNames(files) {
|
|
|
3313
6043
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
3314
6044
|
}
|
|
3315
6045
|
function listFiles(dir) {
|
|
3316
|
-
if (!
|
|
6046
|
+
if (!existsSync8(dir)) return [];
|
|
3317
6047
|
const results = [];
|
|
3318
6048
|
const walk = (current) => {
|
|
3319
6049
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
3320
|
-
const path =
|
|
6050
|
+
const path = join6(current, entry.name);
|
|
3321
6051
|
if (entry.isDirectory()) walk(path);
|
|
3322
|
-
else results.push(
|
|
6052
|
+
else results.push(relative5(dir, path));
|
|
3323
6053
|
}
|
|
3324
6054
|
};
|
|
3325
6055
|
walk(dir);
|
|
@@ -3632,6 +6362,93 @@ async function appExport(options) {
|
|
|
3632
6362
|
return { file, backup: newest };
|
|
3633
6363
|
}
|
|
3634
6364
|
|
|
6365
|
+
// src/app-owners.ts
|
|
6366
|
+
var sink = (options) => options.stdout ?? console;
|
|
6367
|
+
async function ownersRequest(method, suffix, options, body) {
|
|
6368
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
6369
|
+
const doFetch = options.fetch ?? fetch;
|
|
6370
|
+
const token = await getDeveloperToken(
|
|
6371
|
+
cfg,
|
|
6372
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
6373
|
+
doFetch,
|
|
6374
|
+
sink(options)
|
|
6375
|
+
);
|
|
6376
|
+
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
|
|
6377
|
+
method,
|
|
6378
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6379
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
6380
|
+
});
|
|
6381
|
+
const data = await res.json().catch(() => ({}));
|
|
6382
|
+
if (!res.ok) {
|
|
6383
|
+
throw new Error(
|
|
6384
|
+
`owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
6385
|
+
);
|
|
6386
|
+
}
|
|
6387
|
+
return data.owners ?? [];
|
|
6388
|
+
}
|
|
6389
|
+
function report(options, owners, headline) {
|
|
6390
|
+
const out = sink(options);
|
|
6391
|
+
if (options.json === true) {
|
|
6392
|
+
out.log(JSON.stringify(owners, null, 2));
|
|
6393
|
+
return;
|
|
6394
|
+
}
|
|
6395
|
+
if (headline) out.log(headline);
|
|
6396
|
+
out.log(`owners (${owners.length}):`);
|
|
6397
|
+
for (const o of owners) {
|
|
6398
|
+
out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
async function ownersList(options) {
|
|
6402
|
+
report(options, await ownersRequest("GET", "", options));
|
|
6403
|
+
}
|
|
6404
|
+
async function ownersAdd(email, options) {
|
|
6405
|
+
const owners = await ownersRequest("POST", "", options, { email });
|
|
6406
|
+
report(
|
|
6407
|
+
options,
|
|
6408
|
+
owners,
|
|
6409
|
+
`added ${email} as a co-owner \u2014 they share full access. They can now run \`odla-ai provision\` to mint their own credentials (no secret sharing).`
|
|
6410
|
+
);
|
|
6411
|
+
}
|
|
6412
|
+
async function ownersRemove(target, options) {
|
|
6413
|
+
let ownerId = target;
|
|
6414
|
+
if (target.includes("@")) {
|
|
6415
|
+
const owners2 = await ownersRequest("GET", "", options);
|
|
6416
|
+
const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
|
|
6417
|
+
if (!match) throw new Error(`no co-owner with email ${target}`);
|
|
6418
|
+
if (match.primary) throw new Error("can't remove the primary owner");
|
|
6419
|
+
ownerId = match.ownerId;
|
|
6420
|
+
}
|
|
6421
|
+
const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
|
|
6422
|
+
report(options, owners, `removed ${target}`);
|
|
6423
|
+
}
|
|
6424
|
+
async function appOwnersCommand(parsed, dependencies = {}) {
|
|
6425
|
+
const sub = parsed.positionals[2] ?? "list";
|
|
6426
|
+
const options = {
|
|
6427
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
6428
|
+
token: stringOpt(parsed.options.token),
|
|
6429
|
+
email: stringOpt(parsed.options.email),
|
|
6430
|
+
json: parsed.options.json === true,
|
|
6431
|
+
fetch: dependencies.fetch,
|
|
6432
|
+
stdout: dependencies.stdout
|
|
6433
|
+
};
|
|
6434
|
+
const allowed = ["config", "token", "email", "json"];
|
|
6435
|
+
if (sub === "list") {
|
|
6436
|
+
assertArgs(parsed, allowed, 3);
|
|
6437
|
+
await ownersList(options);
|
|
6438
|
+
return;
|
|
6439
|
+
}
|
|
6440
|
+
if (sub === "add" || sub === "remove") {
|
|
6441
|
+
assertArgs(parsed, allowed, 4);
|
|
6442
|
+
const email = parsed.positionals[3];
|
|
6443
|
+
if (!email) throw new Error(`"app owners ${sub}" needs an email \u2014 try "odla-ai app owners ${sub} teammate@example.com".`);
|
|
6444
|
+
await (sub === "add" ? ownersAdd(email, options) : ownersRemove(email, options));
|
|
6445
|
+
return;
|
|
6446
|
+
}
|
|
6447
|
+
throw new Error(
|
|
6448
|
+
`unknown app owners subcommand "${sub}". Try "odla-ai app owners list", "odla-ai app owners add <email>", or "odla-ai app owners remove <email>".`
|
|
6449
|
+
);
|
|
6450
|
+
}
|
|
6451
|
+
|
|
3635
6452
|
// src/app-lifecycle.ts
|
|
3636
6453
|
async function lifecycleCall(action, options) {
|
|
3637
6454
|
const cfg = await loadProjectConfig(options.configPath);
|
|
@@ -3674,6 +6491,10 @@ async function appRestore(options) {
|
|
|
3674
6491
|
}
|
|
3675
6492
|
async function appCommand(parsed, dependencies = {}) {
|
|
3676
6493
|
const sub = parsed.positionals[1];
|
|
6494
|
+
if (sub === "owners") {
|
|
6495
|
+
await appOwnersCommand(parsed, dependencies);
|
|
6496
|
+
return;
|
|
6497
|
+
}
|
|
3677
6498
|
if (sub === "export") {
|
|
3678
6499
|
assertArgs(parsed, ["config", "env", "token", "email", "fresh", "out", "json"], 2);
|
|
3679
6500
|
await appExport({
|
|
@@ -3691,7 +6512,7 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
3691
6512
|
}
|
|
3692
6513
|
if (sub !== "archive" && sub !== "restore") {
|
|
3693
6514
|
throw new Error(
|
|
3694
|
-
`unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", or "odla-ai app
|
|
6515
|
+
`unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", "odla-ai app export", or "odla-ai app owners <list|add|remove>". (Permanent deletion has no CLI: it requires a signed-in owner in Studio.)`
|
|
3695
6516
|
);
|
|
3696
6517
|
}
|
|
3697
6518
|
assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
|
|
@@ -3708,106 +6529,42 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
3708
6529
|
else await appRestore(options);
|
|
3709
6530
|
}
|
|
3710
6531
|
|
|
3711
|
-
// src/
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
}
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
odla-ai security report <job-id> [--json]
|
|
3748
|
-
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
3749
|
-
odla-ai security run [target] --self --ack-redacted-source
|
|
3750
|
-
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
3751
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
3752
|
-
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
3753
|
-
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
3754
|
-
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
3755
|
-
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
3756
|
-
odla-ai version
|
|
3757
|
-
|
|
3758
|
-
Commands:
|
|
3759
|
-
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
3760
|
-
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
3761
|
-
doctor Validate and summarize the project config without network calls.
|
|
3762
|
-
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
3763
|
-
app Archive (suspend, data retained), restore, or export the app.
|
|
3764
|
-
Archiving takes every environment's data plane down until restored;
|
|
3765
|
-
permanent deletion has NO CLI \u2014 it requires a signed-in owner in
|
|
3766
|
-
Studio, and no machine or agent credential can purge. "app export"
|
|
3767
|
-
downloads a portable gzipped-JSONL snapshot of one environment's
|
|
3768
|
-
database (--fresh takes a new snapshot first) \u2014 your data is
|
|
3769
|
-
always yours to take.
|
|
3770
|
-
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
3771
|
-
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
3772
|
-
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
3773
|
-
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
3774
|
-
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
3775
|
-
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
3776
|
-
copilot, gemini, or agents (repeatable or comma-separated).
|
|
3777
|
-
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
3778
|
-
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
3779
|
-
reserved Clerk secret key, write-only from stdin or an env var.
|
|
3780
|
-
version Print the CLI version.
|
|
3781
|
-
|
|
3782
|
-
Safety:
|
|
3783
|
-
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
3784
|
-
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
3785
|
-
Provision caches the approved developer token and service credentials under
|
|
3786
|
-
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
3787
|
-
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
3788
|
-
Provision opens the approval page in your browser automatically whenever the
|
|
3789
|
-
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
3790
|
-
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
3791
|
-
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
3792
|
-
agents must relay it to the human verbatim. A started handshake is persisted
|
|
3793
|
-
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
3794
|
-
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
3795
|
-
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
3796
|
-
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
3797
|
-
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
3798
|
-
The email is a non-secret identity hint: never provide a password or session
|
|
3799
|
-
token. The matching account must already exist, be signed in, explicitly
|
|
3800
|
-
review the exact code, and finish any current request before claiming another.
|
|
3801
|
-
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
3802
|
-
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
3803
|
-
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
3804
|
-
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
3805
|
-
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
3806
|
-
plan are required before bounded snippets reach System AI.
|
|
3807
|
-
Run security plan first to inspect the admin-selected providers, models,
|
|
3808
|
-
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
3809
|
-
and digest that binds consent to that exact plan.
|
|
3810
|
-
`);
|
|
6532
|
+
// src/code-command.ts
|
|
6533
|
+
async function codeCommand(parsed, dependencies) {
|
|
6534
|
+
const sub = parsed.positionals[1];
|
|
6535
|
+
if (sub !== "connect") {
|
|
6536
|
+
throw new Error(`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev".`);
|
|
6537
|
+
}
|
|
6538
|
+
assertArgs(parsed, [
|
|
6539
|
+
"config",
|
|
6540
|
+
"env",
|
|
6541
|
+
"email",
|
|
6542
|
+
"open",
|
|
6543
|
+
"name",
|
|
6544
|
+
"slots",
|
|
6545
|
+
"engine",
|
|
6546
|
+
"heartbeat-ms",
|
|
6547
|
+
"once"
|
|
6548
|
+
], 2);
|
|
6549
|
+
const engine = stringOpt(parsed.options.engine) ?? "auto";
|
|
6550
|
+
if (!["auto", "container", "podman", "docker"].includes(engine)) {
|
|
6551
|
+
throw new Error("--engine must be auto, container, podman, or docker");
|
|
6552
|
+
}
|
|
6553
|
+
await (dependencies.codeConnect ?? codeConnect)({
|
|
6554
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
6555
|
+
env: stringOpt(parsed.options.env),
|
|
6556
|
+
email: stringOpt(parsed.options.email),
|
|
6557
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
6558
|
+
name: stringOpt(parsed.options.name),
|
|
6559
|
+
slots: numberOpt(parsed.options.slots, "--slots"),
|
|
6560
|
+
engine,
|
|
6561
|
+
heartbeatMs: numberOpt(parsed.options["heartbeat-ms"], "--heartbeat-ms"),
|
|
6562
|
+
once: parsed.options.once === true,
|
|
6563
|
+
fetch: dependencies.fetch,
|
|
6564
|
+
stdout: dependencies.stdout,
|
|
6565
|
+
openApprovalUrl: dependencies.openUrl,
|
|
6566
|
+
readGitOrigin: dependencies.readGitOrigin
|
|
6567
|
+
});
|
|
3811
6568
|
}
|
|
3812
6569
|
|
|
3813
6570
|
// src/harness-options.ts
|
|
@@ -3853,12 +6610,12 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
3853
6610
|
);
|
|
3854
6611
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
3855
6612
|
}
|
|
3856
|
-
async function interactiveConfirmation(
|
|
3857
|
-
if (dependencies.confirm) return dependencies.confirm(
|
|
6613
|
+
async function interactiveConfirmation(message3, dependencies) {
|
|
6614
|
+
if (dependencies.confirm) return dependencies.confirm(message3);
|
|
3858
6615
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
3859
6616
|
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
3860
6617
|
try {
|
|
3861
|
-
const answer = await prompt.question(`${
|
|
6618
|
+
const answer = await prompt.question(`${message3} [y/N] `);
|
|
3862
6619
|
return /^y(?:es)?$/i.test(answer.trim());
|
|
3863
6620
|
} finally {
|
|
3864
6621
|
prompt.close();
|
|
@@ -3900,9 +6657,9 @@ function printHostedSecurityIntent(out, intent) {
|
|
|
3900
6657
|
}
|
|
3901
6658
|
function assertHostedSecurityPlanReady(plan) {
|
|
3902
6659
|
const reasons = [];
|
|
3903
|
-
for (const [label,
|
|
3904
|
-
if (!
|
|
3905
|
-
if (!
|
|
6660
|
+
for (const [label, route2] of Object.entries(plan.routes)) {
|
|
6661
|
+
if (!route2.enabled) reasons.push(`${label} is disabled`);
|
|
6662
|
+
if (!route2.credentialReady) reasons.push(`${label} provider credential is unavailable`);
|
|
3906
6663
|
}
|
|
3907
6664
|
if (!plan.independent) reasons.push("discovery and validation are not independently routed");
|
|
3908
6665
|
if (plan.ready && reasons.length === 0) return;
|
|
@@ -3925,54 +6682,51 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
3925
6682
|
out.log(` findings: confirmed=${job.counts.confirmed} needs_reproduction=${job.counts.needsReproduction} candidates=${job.counts.candidates}`);
|
|
3926
6683
|
}
|
|
3927
6684
|
if (job.errorCode) out.log(` failure: ${job.errorCode}`);
|
|
3928
|
-
const url = new URL(
|
|
3929
|
-
url.searchParams.set("app", appId);
|
|
3930
|
-
url.searchParams.set("env", job.env);
|
|
3931
|
-
url.searchParams.set("tab", "security");
|
|
6685
|
+
const url = new URL(`/studio/apps/${encodeURIComponent(appId)}/${encodeURIComponent(job.env)}/security`, platform);
|
|
3932
6686
|
url.searchParams.set("job", job.jobId);
|
|
3933
6687
|
out.log(` Studio: ${url.toString()}`);
|
|
3934
6688
|
}
|
|
3935
|
-
function printHostedReport(out,
|
|
3936
|
-
out.log(`security report ${
|
|
3937
|
-
out.log(` coverage: ${
|
|
3938
|
-
out.log(` findings: confirmed=${
|
|
3939
|
-
out.log(` discovery: ${
|
|
3940
|
-
out.log(` validation: ${
|
|
3941
|
-
for (const finding of
|
|
6689
|
+
function printHostedReport(out, report2) {
|
|
6690
|
+
out.log(`security report ${report2.jobId}: ${report2.repository}@${report2.revision}`);
|
|
6691
|
+
out.log(` coverage: ${report2.coverageStatus} cells=${report2.metrics.coverageCells} shallow=${report2.metrics.shallowCells} blocked=${report2.metrics.blockedCells} unscheduled=${report2.metrics.unscheduledCells} budget_exhausted=${report2.metrics.budgetExhaustedCells}`);
|
|
6692
|
+
out.log(` findings: confirmed=${report2.metrics.confirmed} needs_reproduction=${report2.metrics.needsReproduction} candidates=${report2.metrics.candidates} rejected=${report2.metrics.rejected}`);
|
|
6693
|
+
out.log(` discovery: ${report2.provenance.discovery?.provider ?? "unknown"}/${report2.provenance.discovery?.model ?? "unknown"}`);
|
|
6694
|
+
out.log(` validation: ${report2.provenance.validation?.provider ?? "unknown"}/${report2.provenance.validation?.model ?? "unknown"} independent=${String(report2.provenance.independentValidation)}`);
|
|
6695
|
+
for (const finding of report2.findings) {
|
|
3942
6696
|
const location = finding.locations[0];
|
|
3943
6697
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
3944
6698
|
}
|
|
3945
|
-
for (const limitation of
|
|
6699
|
+
for (const limitation of report2.limitations) out.log(` limitation: ${limitation}`);
|
|
3946
6700
|
}
|
|
3947
|
-
function enforceHostedReportGate(
|
|
6701
|
+
function enforceHostedReportGate(report2, parsed, out, emitSuccess) {
|
|
3948
6702
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
3949
6703
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
3950
6704
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
3951
6705
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
3952
|
-
const confirmed =
|
|
3953
|
-
const leads = failOnCandidates ?
|
|
3954
|
-
const incomplete =
|
|
6706
|
+
const confirmed = report2.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
6707
|
+
const leads = failOnCandidates ? report2.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
6708
|
+
const incomplete = report2.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
3955
6709
|
if (confirmed.length || leads.length || incomplete) {
|
|
3956
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
6710
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report2.coverageStatus}` : ""}`);
|
|
3957
6711
|
}
|
|
3958
6712
|
if (emitSuccess) {
|
|
3959
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
6713
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report2.coverageStatus}. This is not proof that the application is secure.`);
|
|
3960
6714
|
}
|
|
3961
6715
|
}
|
|
3962
|
-
function printHostedSecurityPlanRoute(out, label,
|
|
3963
|
-
const readiness =
|
|
3964
|
-
|
|
3965
|
-
|
|
6716
|
+
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
6717
|
+
const readiness = route2.enabled && route2.credentialReady ? "ready" : [
|
|
6718
|
+
route2.enabled ? void 0 : "disabled",
|
|
6719
|
+
route2.credentialReady ? void 0 : "credential unavailable"
|
|
3966
6720
|
].filter(Boolean).join(", ");
|
|
3967
|
-
out.log(` ${label}: ${
|
|
3968
|
-
out.log(` bounds: ${
|
|
6721
|
+
out.log(` ${label}: ${route2.provider}/${route2.model} \xB7 policy v${route2.policyVersion} \xB7 ${readiness}`);
|
|
6722
|
+
out.log(` bounds: ${route2.maxCallsPerRun} calls/run \xB7 ${route2.maxInputBytes} input bytes/call \xB7 ${route2.maxOutputTokens} output tokens/call`);
|
|
3969
6723
|
}
|
|
3970
6724
|
function printHostedCoverage(out, job) {
|
|
3971
6725
|
const coverage = job.coverage;
|
|
3972
6726
|
out.log(` coverage: ${job.coverageStatus ?? "pending"}${coverage?.completeCells !== void 0 ? ` ${coverage.completeCells}/${coverage.totalCells ?? "?"}` : ""}${coverage?.shallowCells ? ` shallow=${coverage.shallowCells}` : ""}${coverage?.blockedCells ? ` blocked=${coverage.blockedCells}` : ""}${coverage?.unscheduledCells ? ` unscheduled=${coverage.unscheduledCells}` : ""}${coverage?.budgetExhaustedCells ? ` budget_exhausted=${coverage.budgetExhaustedCells}` : ""}`);
|
|
3973
6727
|
}
|
|
3974
|
-
function routeLabel(
|
|
3975
|
-
return `${
|
|
6728
|
+
function routeLabel(route2) {
|
|
6729
|
+
return `${route2.provider}/${route2.model}${route2.policyVersion ? ` policy v${route2.policyVersion}` : ""}`;
|
|
3976
6730
|
}
|
|
3977
6731
|
var HOSTED_SEVERITIES = ["informational", "low", "medium", "high", "critical"];
|
|
3978
6732
|
function hostedSeverity(value, flag) {
|
|
@@ -4060,13 +6814,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
4060
6814
|
}
|
|
4061
6815
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
4062
6816
|
}
|
|
4063
|
-
const
|
|
6817
|
+
const report2 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
4064
6818
|
if (parsed.options.json === true) {
|
|
4065
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report }, null, 2));
|
|
6819
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report2 }, null, 2));
|
|
4066
6820
|
} else {
|
|
4067
|
-
printHostedReport(context.stdout,
|
|
6821
|
+
printHostedReport(context.stdout, report2);
|
|
4068
6822
|
}
|
|
4069
|
-
enforceHostedReportGate(
|
|
6823
|
+
enforceHostedReportGate(report2, parsed, context.stdout, parsed.options.json !== true);
|
|
4070
6824
|
}
|
|
4071
6825
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
4072
6826
|
if (parsed.options.source === true) {
|
|
@@ -4133,13 +6887,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
4133
6887
|
});
|
|
4134
6888
|
enforceLocalGate(result.report, parsed);
|
|
4135
6889
|
}
|
|
4136
|
-
function enforceLocalGate(
|
|
6890
|
+
function enforceLocalGate(report2, parsed) {
|
|
4137
6891
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
4138
6892
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
4139
6893
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
4140
|
-
const confirmed = findingsAtOrAbove(
|
|
4141
|
-
const leads = failOnCandidates ? findingsAtOrAbove(
|
|
4142
|
-
const incomplete =
|
|
6894
|
+
const confirmed = findingsAtOrAbove(report2, failOn);
|
|
6895
|
+
const leads = failOnCandidates ? findingsAtOrAbove(report2, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
6896
|
+
const incomplete = report2.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
4143
6897
|
if (confirmed.length || leads.length || incomplete) {
|
|
4144
6898
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
4145
6899
|
}
|
|
@@ -4178,9 +6932,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
4178
6932
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
4179
6933
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
4180
6934
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
4181
|
-
const
|
|
4182
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
4183
|
-
else printHostedReport(context.stdout,
|
|
6935
|
+
const report2 = await getHostedSecurityReport({ ...context, jobId });
|
|
6936
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report2, null, 2));
|
|
6937
|
+
else printHostedReport(context.stdout, report2);
|
|
4184
6938
|
return;
|
|
4185
6939
|
}
|
|
4186
6940
|
if (sub !== "run") {
|
|
@@ -4314,6 +7068,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
4314
7068
|
printCapabilities(parsed.options.json === true);
|
|
4315
7069
|
return;
|
|
4316
7070
|
}
|
|
7071
|
+
if (command === "code") {
|
|
7072
|
+
await codeCommand(parsed, dependencies);
|
|
7073
|
+
return;
|
|
7074
|
+
}
|
|
4317
7075
|
if (command === "admin") {
|
|
4318
7076
|
await adminCommand(parsed);
|
|
4319
7077
|
return;
|
|
@@ -4481,6 +7239,15 @@ export {
|
|
|
4481
7239
|
calendarCalendars,
|
|
4482
7240
|
calendarConnect,
|
|
4483
7241
|
calendarDisconnect,
|
|
7242
|
+
connectGitHubSecuritySource,
|
|
7243
|
+
listGitHubSecuritySources,
|
|
7244
|
+
disconnectGitHubSecuritySource,
|
|
7245
|
+
repositoryFromGitRemote,
|
|
7246
|
+
inferGitHubRepository,
|
|
7247
|
+
CODE_PI_IMAGE,
|
|
7248
|
+
CODE_BUILD_RECIPES,
|
|
7249
|
+
codeConnect,
|
|
7250
|
+
runCodeRuntime,
|
|
4484
7251
|
doctor,
|
|
4485
7252
|
initProject,
|
|
4486
7253
|
secretsPush,
|
|
@@ -4488,11 +7255,6 @@ export {
|
|
|
4488
7255
|
secretsSet,
|
|
4489
7256
|
secretsSetClerkKey,
|
|
4490
7257
|
runHostedSecurity,
|
|
4491
|
-
connectGitHubSecuritySource,
|
|
4492
|
-
listGitHubSecuritySources,
|
|
4493
|
-
disconnectGitHubSecuritySource,
|
|
4494
|
-
repositoryFromGitRemote,
|
|
4495
|
-
inferGitHubRepository,
|
|
4496
7258
|
getHostedSecurityIntent,
|
|
4497
7259
|
getHostedSecurityPlan,
|
|
4498
7260
|
startHostedSecurityJob,
|
|
@@ -4507,4 +7269,4 @@ export {
|
|
|
4507
7269
|
exitCodeFor,
|
|
4508
7270
|
runCli
|
|
4509
7271
|
};
|
|
4510
|
-
//# sourceMappingURL=chunk-
|
|
7272
|
+
//# sourceMappingURL=chunk-3AC74CD2.js.map
|