@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
package/dist/bin.cjs
CHANGED
|
@@ -205,7 +205,7 @@ var import_node_process2 = __toESM(require("process"), 1);
|
|
|
205
205
|
async function openUrl(url, options = {}) {
|
|
206
206
|
const command = openerFor(options.platform ?? import_node_process2.default.platform);
|
|
207
207
|
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
208
|
-
await new Promise((
|
|
208
|
+
await new Promise((resolve10, reject) => {
|
|
209
209
|
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
210
210
|
stdio: "ignore",
|
|
211
211
|
detached: true
|
|
@@ -213,7 +213,7 @@ async function openUrl(url, options = {}) {
|
|
|
213
213
|
child.once("error", reject);
|
|
214
214
|
child.once("spawn", () => {
|
|
215
215
|
child.unref();
|
|
216
|
-
|
|
216
|
+
resolve10();
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
219
|
}
|
|
@@ -446,7 +446,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
446
446
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
447
447
|
const audience = platformAudience(platform);
|
|
448
448
|
const tokenFile = options.tokenFile ?? `${import_node_process5.default.cwd()}/.odla/admin-token.local.json`;
|
|
449
|
-
const cache = readJsonFile(tokenFile);
|
|
449
|
+
const cache = options.cache === false ? null : readJsonFile(tokenFile);
|
|
450
450
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
451
451
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
452
452
|
out.log(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
@@ -456,7 +456,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
456
456
|
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
457
457
|
endpoint: audience,
|
|
458
458
|
email,
|
|
459
|
-
label: `odla CLI admin AI (${scope})`,
|
|
459
|
+
label: options.label ?? `odla CLI admin AI (${scope})`,
|
|
460
460
|
scopes: [scope],
|
|
461
461
|
fetch: doFetch,
|
|
462
462
|
onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
|
|
@@ -467,11 +467,15 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
467
467
|
}
|
|
468
468
|
}
|
|
469
469
|
});
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
470
|
+
if (options.cache !== false) {
|
|
471
|
+
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
472
|
+
tokens[scope] = { token, expiresAt };
|
|
473
|
+
if ((0, import_node_fs3.existsSync)(`${import_node_process5.default.cwd()}/.git`)) ensureGitignore(import_node_process5.default.cwd(), [tokenFile]);
|
|
474
|
+
writePrivateJson(tokenFile, { platform: audience, email, tokens });
|
|
475
|
+
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
476
|
+
} else {
|
|
477
|
+
out.log(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
|
|
478
|
+
}
|
|
475
479
|
return token;
|
|
476
480
|
}
|
|
477
481
|
|
|
@@ -484,11 +488,11 @@ function adminAiAuditQuery(filters) {
|
|
|
484
488
|
return `?limit=${filters.limit}`;
|
|
485
489
|
}
|
|
486
490
|
async function readAdminAiAudit(request) {
|
|
487
|
-
const
|
|
491
|
+
const response2 = await request.fetch(`${request.platform}/registry/platform/ai-audit${request.query}`, {
|
|
488
492
|
headers: request.headers
|
|
489
493
|
});
|
|
490
|
-
const body = await responseBody(
|
|
491
|
-
if (!
|
|
494
|
+
const body = await responseBody(response2);
|
|
495
|
+
if (!response2.ok) throw new Error(apiError(response2.status, body));
|
|
492
496
|
if (request.json) {
|
|
493
497
|
request.stdout.log(JSON.stringify(body, null, 2));
|
|
494
498
|
return;
|
|
@@ -498,18 +502,18 @@ async function readAdminAiAudit(request) {
|
|
|
498
502
|
for (const event of events) {
|
|
499
503
|
const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
|
|
500
504
|
const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
|
|
501
|
-
const
|
|
505
|
+
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";
|
|
502
506
|
request.stdout.log([
|
|
503
507
|
timestamp(event.createdAt),
|
|
504
508
|
String(event.changeKind ?? ""),
|
|
505
509
|
String(event.purpose ?? event.provider ?? ""),
|
|
506
|
-
|
|
510
|
+
route2,
|
|
507
511
|
`${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
|
|
508
512
|
].join(" "));
|
|
509
513
|
}
|
|
510
514
|
}
|
|
511
|
-
async function responseBody(
|
|
512
|
-
const text = await
|
|
515
|
+
async function responseBody(response2) {
|
|
516
|
+
const text = await response2.text();
|
|
513
517
|
if (!text) return {};
|
|
514
518
|
try {
|
|
515
519
|
return JSON.parse(text);
|
|
@@ -519,8 +523,8 @@ async function responseBody(response) {
|
|
|
519
523
|
}
|
|
520
524
|
function apiError(status, body) {
|
|
521
525
|
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
522
|
-
const
|
|
523
|
-
return `read System AI admin changes failed (${status}): ${
|
|
526
|
+
const message3 = error && typeof error.message === "string" ? error.message : isRecord(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
527
|
+
return `read System AI admin changes failed (${status}): ${message3}`;
|
|
524
528
|
}
|
|
525
529
|
function timestamp(value) {
|
|
526
530
|
if (typeof value !== "number" || !Number.isFinite(value)) return String(value ?? "");
|
|
@@ -621,8 +625,8 @@ async function responseBody2(res) {
|
|
|
621
625
|
}
|
|
622
626
|
function apiError2(action, status, body) {
|
|
623
627
|
const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
|
|
624
|
-
const
|
|
625
|
-
return `${action} failed (${status}): ${
|
|
628
|
+
const message3 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
629
|
+
return `${action} failed (${status}): ${message3}`;
|
|
626
630
|
}
|
|
627
631
|
function isRecord2(value) {
|
|
628
632
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -749,9 +753,9 @@ async function adminAi(options) {
|
|
|
749
753
|
validation: validationUpdate
|
|
750
754
|
})
|
|
751
755
|
});
|
|
752
|
-
const
|
|
753
|
-
if (!res2.ok) throw new Error(apiError3("update security review routes", res2.status,
|
|
754
|
-
out.log(options.json ? JSON.stringify(
|
|
756
|
+
const response3 = await responseBody3(res2);
|
|
757
|
+
if (!res2.ok) throw new Error(apiError3("update security review routes", res2.status, response3));
|
|
758
|
+
out.log(options.json ? JSON.stringify(response3, null, 2) : "updated security review discovery and independent validation routes atomically");
|
|
755
759
|
return;
|
|
756
760
|
}
|
|
757
761
|
const purpose = requireSystemAiPurpose(options.purpose);
|
|
@@ -769,9 +773,9 @@ async function adminAi(options) {
|
|
|
769
773
|
headers,
|
|
770
774
|
body: JSON.stringify(body)
|
|
771
775
|
});
|
|
772
|
-
const
|
|
773
|
-
if (!res.ok) throw new Error(apiError3(`update ${purpose}`, res.status,
|
|
774
|
-
const policy = isRecord3(
|
|
776
|
+
const response2 = await responseBody3(res);
|
|
777
|
+
if (!res.ok) throw new Error(apiError3(`update ${purpose}`, res.status, response2));
|
|
778
|
+
const policy = isRecord3(response2) && isRecord3(response2.policy) ? response2.policy : isRecord3(response2) ? response2 : {};
|
|
775
779
|
out.log(options.json ? JSON.stringify({ policy }, null, 2) : `updated ${purpose}: ${String(policy.provider ?? "unchanged")}/${String(policy.model ?? "unchanged")}`);
|
|
776
780
|
}
|
|
777
781
|
function requireProvider(value) {
|
|
@@ -802,8 +806,8 @@ async function responseBody3(res) {
|
|
|
802
806
|
}
|
|
803
807
|
function apiError3(action, status, body) {
|
|
804
808
|
const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
|
|
805
|
-
const
|
|
806
|
-
return `${action} failed (${status}): ${
|
|
809
|
+
const message3 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
810
|
+
return `${action} failed (${status}): ${message3}`;
|
|
807
811
|
}
|
|
808
812
|
function isRecord3(value) {
|
|
809
813
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -1276,6 +1280,93 @@ async function appExport(options) {
|
|
|
1276
1280
|
return { file, backup: newest };
|
|
1277
1281
|
}
|
|
1278
1282
|
|
|
1283
|
+
// src/app-owners.ts
|
|
1284
|
+
var sink = (options) => options.stdout ?? console;
|
|
1285
|
+
async function ownersRequest(method, suffix, options, body) {
|
|
1286
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1287
|
+
const doFetch = options.fetch ?? fetch;
|
|
1288
|
+
const token = await getDeveloperToken(
|
|
1289
|
+
cfg,
|
|
1290
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1291
|
+
doFetch,
|
|
1292
|
+
sink(options)
|
|
1293
|
+
);
|
|
1294
|
+
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
|
|
1295
|
+
method,
|
|
1296
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1297
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1298
|
+
});
|
|
1299
|
+
const data = await res.json().catch(() => ({}));
|
|
1300
|
+
if (!res.ok) {
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
`owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
return data.owners ?? [];
|
|
1306
|
+
}
|
|
1307
|
+
function report(options, owners, headline) {
|
|
1308
|
+
const out = sink(options);
|
|
1309
|
+
if (options.json === true) {
|
|
1310
|
+
out.log(JSON.stringify(owners, null, 2));
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (headline) out.log(headline);
|
|
1314
|
+
out.log(`owners (${owners.length}):`);
|
|
1315
|
+
for (const o of owners) {
|
|
1316
|
+
out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async function ownersList(options) {
|
|
1320
|
+
report(options, await ownersRequest("GET", "", options));
|
|
1321
|
+
}
|
|
1322
|
+
async function ownersAdd(email, options) {
|
|
1323
|
+
const owners = await ownersRequest("POST", "", options, { email });
|
|
1324
|
+
report(
|
|
1325
|
+
options,
|
|
1326
|
+
owners,
|
|
1327
|
+
`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).`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
async function ownersRemove(target, options) {
|
|
1331
|
+
let ownerId = target;
|
|
1332
|
+
if (target.includes("@")) {
|
|
1333
|
+
const owners2 = await ownersRequest("GET", "", options);
|
|
1334
|
+
const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
|
|
1335
|
+
if (!match) throw new Error(`no co-owner with email ${target}`);
|
|
1336
|
+
if (match.primary) throw new Error("can't remove the primary owner");
|
|
1337
|
+
ownerId = match.ownerId;
|
|
1338
|
+
}
|
|
1339
|
+
const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
|
|
1340
|
+
report(options, owners, `removed ${target}`);
|
|
1341
|
+
}
|
|
1342
|
+
async function appOwnersCommand(parsed, dependencies = {}) {
|
|
1343
|
+
const sub = parsed.positionals[2] ?? "list";
|
|
1344
|
+
const options = {
|
|
1345
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1346
|
+
token: stringOpt(parsed.options.token),
|
|
1347
|
+
email: stringOpt(parsed.options.email),
|
|
1348
|
+
json: parsed.options.json === true,
|
|
1349
|
+
fetch: dependencies.fetch,
|
|
1350
|
+
stdout: dependencies.stdout
|
|
1351
|
+
};
|
|
1352
|
+
const allowed = ["config", "token", "email", "json"];
|
|
1353
|
+
if (sub === "list") {
|
|
1354
|
+
assertArgs(parsed, allowed, 3);
|
|
1355
|
+
await ownersList(options);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (sub === "add" || sub === "remove") {
|
|
1359
|
+
assertArgs(parsed, allowed, 4);
|
|
1360
|
+
const email = parsed.positionals[3];
|
|
1361
|
+
if (!email) throw new Error(`"app owners ${sub}" needs an email \u2014 try "odla-ai app owners ${sub} teammate@example.com".`);
|
|
1362
|
+
await (sub === "add" ? ownersAdd(email, options) : ownersRemove(email, options));
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
throw new Error(
|
|
1366
|
+
`unknown app owners subcommand "${sub}". Try "odla-ai app owners list", "odla-ai app owners add <email>", or "odla-ai app owners remove <email>".`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1279
1370
|
// src/app-lifecycle.ts
|
|
1280
1371
|
async function lifecycleCall(action, options) {
|
|
1281
1372
|
const cfg = await loadProjectConfig(options.configPath);
|
|
@@ -1318,6 +1409,10 @@ async function appRestore(options) {
|
|
|
1318
1409
|
}
|
|
1319
1410
|
async function appCommand(parsed, dependencies = {}) {
|
|
1320
1411
|
const sub = parsed.positionals[1];
|
|
1412
|
+
if (sub === "owners") {
|
|
1413
|
+
await appOwnersCommand(parsed, dependencies);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1321
1416
|
if (sub === "export") {
|
|
1322
1417
|
assertArgs(parsed, ["config", "env", "token", "email", "fresh", "out", "json"], 2);
|
|
1323
1418
|
await appExport({
|
|
@@ -1335,7 +1430,7 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
1335
1430
|
}
|
|
1336
1431
|
if (sub !== "archive" && sub !== "restore") {
|
|
1337
1432
|
throw new Error(
|
|
1338
|
-
`unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", or "odla-ai app
|
|
1433
|
+
`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.)`
|
|
1339
1434
|
);
|
|
1340
1435
|
}
|
|
1341
1436
|
assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
|
|
@@ -1358,6 +1453,7 @@ var CAPABILITIES = {
|
|
|
1358
1453
|
"start an email-bound device request without accepting a password or user session token",
|
|
1359
1454
|
"register the app and enable configured services per environment",
|
|
1360
1455
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
1456
|
+
"add or remove app co-owners, each of whom self-provisions their own credentials for the shared db without any secret handoff",
|
|
1361
1457
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
1362
1458
|
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
1363
1459
|
"compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
|
|
@@ -1386,13 +1482,13 @@ var CAPABILITIES = {
|
|
|
1386
1482
|
"view telemetry and environment state",
|
|
1387
1483
|
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
1388
1484
|
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
1389
|
-
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
1485
|
+
"perform manual credential recovery \u2014 for the primary owner or any co-owner \u2014 when the CLI's local shown-once copy is unavailable",
|
|
1390
1486
|
"configure system AI purposes and view app/environment/run-attributed usage",
|
|
1391
1487
|
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
1392
1488
|
]
|
|
1393
1489
|
};
|
|
1394
|
-
function printCapabilities(
|
|
1395
|
-
if (
|
|
1490
|
+
function printCapabilities(json2 = false, out = console) {
|
|
1491
|
+
if (json2) {
|
|
1396
1492
|
out.log(JSON.stringify(CAPABILITIES, null, 2));
|
|
1397
1493
|
return;
|
|
1398
1494
|
}
|
|
@@ -1422,8 +1518,8 @@ var CalendarRequestError = class extends Error {
|
|
|
1422
1518
|
status;
|
|
1423
1519
|
/** Machine-readable `error.code` from the response body, when present. */
|
|
1424
1520
|
code;
|
|
1425
|
-
constructor(
|
|
1426
|
-
super(
|
|
1521
|
+
constructor(message3, status, code) {
|
|
1522
|
+
super(message3);
|
|
1427
1523
|
this.name = "CalendarRequestError";
|
|
1428
1524
|
this.status = status;
|
|
1429
1525
|
if (code !== void 0) this.code = code;
|
|
@@ -1576,9 +1672,9 @@ async function calendarJson(ctx, suffix, init) {
|
|
|
1576
1672
|
const url = new URL(`/registry/apps/${encodeURIComponent(appId)}/calendar${suffix}`, platform);
|
|
1577
1673
|
url.searchParams.set("env", env);
|
|
1578
1674
|
const token = credential(ctx.token);
|
|
1579
|
-
let
|
|
1675
|
+
let response2;
|
|
1580
1676
|
try {
|
|
1581
|
-
|
|
1677
|
+
response2 = await (ctx.fetch ?? fetch)(url, {
|
|
1582
1678
|
...init,
|
|
1583
1679
|
redirect: "error",
|
|
1584
1680
|
credentials: "omit",
|
|
@@ -1587,12 +1683,12 @@ async function calendarJson(ctx, suffix, init) {
|
|
|
1587
1683
|
} catch (error) {
|
|
1588
1684
|
throw new Error(`calendar request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1589
1685
|
}
|
|
1590
|
-
const body = await
|
|
1591
|
-
if (!
|
|
1686
|
+
const body = await response2.json().catch(() => ({}));
|
|
1687
|
+
if (!response2.ok) {
|
|
1592
1688
|
const code = textField(body.error?.code, 128);
|
|
1593
|
-
const
|
|
1594
|
-
const detail = `${code ? ` ${code}` : ""}${
|
|
1595
|
-
throw new CalendarRequestError(redactSecrets(`calendar request failed (${
|
|
1689
|
+
const message3 = textField(body.error?.message, 500);
|
|
1690
|
+
const detail = `${code ? ` ${code}` : ""}${message3 ? `: ${message3}` : ""}`;
|
|
1691
|
+
throw new CalendarRequestError(redactSecrets(`calendar request failed (${response2.status})${detail}`), response2.status, code);
|
|
1596
1692
|
}
|
|
1597
1693
|
return body;
|
|
1598
1694
|
}
|
|
@@ -1650,8 +1746,8 @@ function credential(value) {
|
|
|
1650
1746
|
// src/calendar-poll.ts
|
|
1651
1747
|
async function waitForCalendarPoll(milliseconds, signal) {
|
|
1652
1748
|
if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
|
|
1653
|
-
await new Promise((
|
|
1654
|
-
const timer = setTimeout(
|
|
1749
|
+
await new Promise((resolve10, reject) => {
|
|
1750
|
+
const timer = setTimeout(resolve10, milliseconds);
|
|
1655
1751
|
signal?.addEventListener("abort", () => {
|
|
1656
1752
|
clearTimeout(timer);
|
|
1657
1753
|
reject(signal.reason ?? new Error("calendar connection aborted"));
|
|
@@ -1753,7 +1849,7 @@ async function continueConnectedCalendar(ctx, current, options) {
|
|
|
1753
1849
|
if (current.status !== "authorizing") return null;
|
|
1754
1850
|
const now = options.now ?? Date.now;
|
|
1755
1851
|
const deadline = now() + pollTimeout(options.pollTimeoutMs);
|
|
1756
|
-
const
|
|
1852
|
+
const wait2 = options.wait ?? waitForCalendarPoll;
|
|
1757
1853
|
let prior = "";
|
|
1758
1854
|
while (now() < deadline) {
|
|
1759
1855
|
if (options.signal?.aborted) throw options.signal.reason ?? new Error("calendar connection aborted");
|
|
@@ -1769,7 +1865,7 @@ async function continueConnectedCalendar(ctx, current, options) {
|
|
|
1769
1865
|
throw new Error(`calendar connection failed${reason ? `: ${reason}` : ""}; inspect status and reconnect explicitly`);
|
|
1770
1866
|
}
|
|
1771
1867
|
if (status.status !== "authorizing") return null;
|
|
1772
|
-
await
|
|
1868
|
+
await wait2(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1773
1869
|
}
|
|
1774
1870
|
throw new Error('Existing Google Calendar authorization timed out; run "odla-ai calendar status" and retry');
|
|
1775
1871
|
}
|
|
@@ -1784,7 +1880,7 @@ async function connectWithContext(ctx, options) {
|
|
|
1784
1880
|
const now = options.now ?? Date.now;
|
|
1785
1881
|
const timeout = pollTimeout(options.pollTimeoutMs);
|
|
1786
1882
|
const deadline = Math.min(attempt.expiresAt, now() + timeout);
|
|
1787
|
-
const
|
|
1883
|
+
const wait2 = options.wait ?? waitForCalendarPoll;
|
|
1788
1884
|
let prior = "";
|
|
1789
1885
|
while (now() < deadline) {
|
|
1790
1886
|
if (options.signal?.aborted) throw options.signal.reason ?? new Error("calendar connection aborted");
|
|
@@ -1801,7 +1897,7 @@ async function connectWithContext(ctx, options) {
|
|
|
1801
1897
|
const reason = status.error?.message ?? status.error?.code;
|
|
1802
1898
|
throw new Error(`calendar connection ${status.status}${reason ? `: ${reason}` : ""}`);
|
|
1803
1899
|
}
|
|
1804
|
-
await
|
|
1900
|
+
await wait2(Math.min(2e3, Math.max(0, deadline - now())), options.signal);
|
|
1805
1901
|
}
|
|
1806
1902
|
throw new Error('Google Calendar consent timed out; run "odla-ai calendar status" and retry connect');
|
|
1807
1903
|
}
|
|
@@ -1827,8 +1923,8 @@ function pollTimeout(value = 10 * 6e4) {
|
|
|
1827
1923
|
function productionConsent(env, yes, action) {
|
|
1828
1924
|
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action} for "${env}" without --yes`);
|
|
1829
1925
|
}
|
|
1830
|
-
function printStatus(status,
|
|
1831
|
-
if (
|
|
1926
|
+
function printStatus(status, json2, out) {
|
|
1927
|
+
if (json2) {
|
|
1832
1928
|
out.log(JSON.stringify(status, null, 2));
|
|
1833
1929
|
return;
|
|
1834
1930
|
}
|
|
@@ -1840,192 +1936,3169 @@ function printStatus(status, json, out) {
|
|
|
1840
1936
|
if (status.error?.code || status.error?.message) out.log(` error: ${status.error.code ?? "error"}${status.error.message ? ` \u2014 ${status.error.message}` : ""}`);
|
|
1841
1937
|
}
|
|
1842
1938
|
|
|
1843
|
-
// src/
|
|
1844
|
-
var import_node_child_process3 = require("child_process");
|
|
1939
|
+
// src/code-connect.ts
|
|
1845
1940
|
var import_node_fs7 = require("fs");
|
|
1846
|
-
var
|
|
1847
|
-
|
|
1848
|
-
// src/wrangler.ts
|
|
1849
|
-
var import_node_child_process2 = require("child_process");
|
|
1850
|
-
var import_node_fs6 = require("fs");
|
|
1941
|
+
var import_node_os = require("os");
|
|
1851
1942
|
var import_node_path4 = require("path");
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1943
|
+
|
|
1944
|
+
// ../harness/dist/chunk-UG5XHNFB.js
|
|
1945
|
+
var HARNESS_PROTOCOL_VERSION = 1;
|
|
1946
|
+
|
|
1947
|
+
// ../harness/dist/chunk-QALIIZRJ.js
|
|
1948
|
+
var CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
1949
|
+
var HarnessProtocolError = class extends Error {
|
|
1950
|
+
name = "HarnessProtocolError";
|
|
1951
|
+
};
|
|
1952
|
+
function record2(value) {
|
|
1953
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1954
|
+
}
|
|
1955
|
+
function boundedText(value, label, max) {
|
|
1956
|
+
if (typeof value !== "string" || !value || value.length > max || CONTROL.test(value)) {
|
|
1957
|
+
throw new HarnessProtocolError(`${label} must be a non-empty string of at most ${max} characters`);
|
|
1867
1958
|
}
|
|
1868
|
-
return
|
|
1959
|
+
return value;
|
|
1869
1960
|
}
|
|
1870
|
-
function
|
|
1871
|
-
if (
|
|
1961
|
+
function parseAgentOutput(line) {
|
|
1962
|
+
if (Buffer.byteLength(line, "utf8") > 1e6) throw new HarnessProtocolError("agent message exceeds 1 MB");
|
|
1963
|
+
let value;
|
|
1872
1964
|
try {
|
|
1873
|
-
|
|
1965
|
+
value = JSON.parse(line);
|
|
1874
1966
|
} catch {
|
|
1875
|
-
|
|
1967
|
+
throw new HarnessProtocolError("agent emitted invalid JSON");
|
|
1876
1968
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
if (ch === '"') {
|
|
1894
|
-
inString = true;
|
|
1895
|
-
result += ch;
|
|
1896
|
-
continue;
|
|
1897
|
-
}
|
|
1898
|
-
if (ch === "/" && text[i + 1] === "/") {
|
|
1899
|
-
while (i < text.length && text[i] !== "\n") i++;
|
|
1900
|
-
result += "\n";
|
|
1901
|
-
continue;
|
|
1969
|
+
const message3 = record2(value);
|
|
1970
|
+
if (!message3 || message3.protocolVersion !== HARNESS_PROTOCOL_VERSION) {
|
|
1971
|
+
throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);
|
|
1972
|
+
}
|
|
1973
|
+
if (message3.type === "event") {
|
|
1974
|
+
return {
|
|
1975
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
1976
|
+
type: "event",
|
|
1977
|
+
kind: boundedText(message3.kind, "event.kind", 120),
|
|
1978
|
+
...message3.payload === void 0 ? {} : { payload: message3.payload }
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
if (message3.type === "inference.request") {
|
|
1982
|
+
const call = record2(message3.call);
|
|
1983
|
+
if (!call || !Array.isArray(call.messages) || !Number.isSafeInteger(call.maxTokens)) {
|
|
1984
|
+
throw new HarnessProtocolError("inference.request.call requires messages and maxTokens");
|
|
1902
1985
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1986
|
+
return {
|
|
1987
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
1988
|
+
type: "inference.request",
|
|
1989
|
+
requestId: boundedText(message3.requestId, "requestId", 180),
|
|
1990
|
+
call
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
if (message3.type === "tool.request") {
|
|
1994
|
+
const input = record2(message3.input);
|
|
1995
|
+
const tool = String(message3.tool);
|
|
1996
|
+
if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
|
|
1997
|
+
throw new HarnessProtocolError("tool.request requires a registered tool and object input");
|
|
1908
1998
|
}
|
|
1909
|
-
|
|
1999
|
+
return {
|
|
2000
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
2001
|
+
type: "tool.request",
|
|
2002
|
+
requestId: boundedText(message3.requestId, "requestId", 180),
|
|
2003
|
+
tool,
|
|
2004
|
+
input
|
|
2005
|
+
};
|
|
1910
2006
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2007
|
+
if (message3.type === "attempt.complete") {
|
|
2008
|
+
if (!(/* @__PURE__ */ new Set(["completed", "failed", "cancelled"])).has(String(message3.status))) {
|
|
2009
|
+
throw new HarnessProtocolError("attempt.complete.status is invalid");
|
|
2010
|
+
}
|
|
2011
|
+
return {
|
|
2012
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
2013
|
+
type: "attempt.complete",
|
|
2014
|
+
status: message3.status,
|
|
2015
|
+
...message3.result === void 0 ? {} : { result: message3.result }
|
|
2016
|
+
};
|
|
1919
2017
|
}
|
|
2018
|
+
throw new HarnessProtocolError("agent message type is unsupported");
|
|
1920
2019
|
}
|
|
1921
|
-
function
|
|
1922
|
-
|
|
1923
|
-
|
|
2020
|
+
function encodeAgentInput(message3) {
|
|
2021
|
+
return `${JSON.stringify(message3)}
|
|
2022
|
+
`;
|
|
1924
2023
|
}
|
|
1925
2024
|
|
|
1926
|
-
//
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2025
|
+
// ../harness/dist/chunk-FNYUIJUN.js
|
|
2026
|
+
var import_child_process = require("child_process");
|
|
2027
|
+
var import_fs = require("fs");
|
|
2028
|
+
var import_promises2 = require("fs/promises");
|
|
2029
|
+
var import_path = require("path");
|
|
2030
|
+
var import_process = require("process");
|
|
2031
|
+
var import_promises3 = require("fs/promises");
|
|
2032
|
+
var import_os = require("os");
|
|
2033
|
+
var import_path2 = require("path");
|
|
2034
|
+
var import_child_process2 = require("child_process");
|
|
2035
|
+
var DIGEST_IMAGE = /^[a-z0-9][a-z0-9._/-]*(?::[a-zA-Z0-9._-]+)?@sha256:[0-9a-f]{64}$/;
|
|
2036
|
+
function assertPinnedImage(image) {
|
|
2037
|
+
if (!DIGEST_IMAGE.test(image)) throw new TypeError("container image must be pinned by sha256 digest");
|
|
2038
|
+
}
|
|
2039
|
+
async function commandAvailable(engine) {
|
|
2040
|
+
for (const directory of (process.env.PATH ?? "").split(import_path.delimiter).filter(Boolean)) {
|
|
2041
|
+
try {
|
|
2042
|
+
await (0, import_promises2.access)((0, import_path.join)(directory, engine), import_fs.constants.X_OK);
|
|
2043
|
+
return true;
|
|
2044
|
+
} catch {
|
|
1937
2045
|
}
|
|
1938
2046
|
}
|
|
1939
|
-
|
|
1940
|
-
if (!(entity in rules)) warnings.push(`schema entity "${entity}" has no rules entry (all client access denied)`);
|
|
1941
|
-
}
|
|
1942
|
-
return warnings;
|
|
2047
|
+
return false;
|
|
1943
2048
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2049
|
+
async function selectContainerEngine(requested = "auto", options = {}) {
|
|
2050
|
+
const platform = options.platform ?? process.platform;
|
|
2051
|
+
const arch = options.arch ?? process.arch;
|
|
2052
|
+
const uid = options.uid ?? (typeof import_process.getuid === "function" ? (0, import_process.getuid)() : 1e3);
|
|
2053
|
+
const available = options.available ?? commandAvailable;
|
|
2054
|
+
const validate3 = async (engine) => {
|
|
2055
|
+
if (engine === "container" && (platform !== "darwin" || arch !== "arm64")) {
|
|
2056
|
+
throw new TypeError("Apple container requires Apple Silicon macOS");
|
|
2057
|
+
}
|
|
2058
|
+
if (engine === "podman" && platform === "linux" && uid === 0) {
|
|
2059
|
+
throw new TypeError("the Linux harness requires rootless Podman; do not run the runner as root");
|
|
2060
|
+
}
|
|
2061
|
+
if (!await available(engine)) throw new TypeError(`${engine} is not installed or executable`);
|
|
2062
|
+
return engine;
|
|
2063
|
+
};
|
|
2064
|
+
if (requested !== "auto") return validate3(requested);
|
|
2065
|
+
const candidates = platform === "darwin" ? arch === "arm64" ? ["container", "podman"] : ["podman"] : platform === "linux" ? ["podman"] : [];
|
|
2066
|
+
for (const engine of candidates) {
|
|
2067
|
+
if (await available(engine)) return validate3(engine);
|
|
2068
|
+
}
|
|
2069
|
+
if (platform === "linux") {
|
|
2070
|
+
throw new TypeError("no rootless Podman found; install Podman or explicitly choose --engine docker after reviewing its daemon boundary");
|
|
2071
|
+
}
|
|
2072
|
+
if (platform === "darwin") {
|
|
2073
|
+
throw new TypeError("no Apple container or Podman Machine found; install one or explicitly choose --engine docker");
|
|
2074
|
+
}
|
|
2075
|
+
throw new TypeError("no supported container engine found");
|
|
2076
|
+
}
|
|
2077
|
+
function inspectRootlessPodman() {
|
|
2078
|
+
return new Promise((resolve23, reject) => {
|
|
2079
|
+
(0, import_child_process.execFile)(
|
|
2080
|
+
"podman",
|
|
2081
|
+
["info", "--format", "{{.Host.Security.Rootless}}"],
|
|
2082
|
+
{ encoding: "utf8", maxBuffer: 16 * 1024, timeout: 1e4 },
|
|
2083
|
+
(error, stdout) => {
|
|
2084
|
+
if (error) {
|
|
2085
|
+
reject(new TypeError("could not verify that the active Podman service is rootless"));
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
resolve23(stdout.trim() === "true");
|
|
2089
|
+
}
|
|
2090
|
+
);
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
async function verifyContainerEngineBoundary(engine, options = {}) {
|
|
2094
|
+
const platform = options.platform ?? process.platform;
|
|
2095
|
+
const arch = options.arch ?? process.arch;
|
|
2096
|
+
const uid = options.uid ?? (typeof import_process.getuid === "function" ? (0, import_process.getuid)() : 1e3);
|
|
2097
|
+
if (engine === "container" && (platform !== "darwin" || arch !== "arm64")) {
|
|
2098
|
+
throw new TypeError("Apple container requires Apple Silicon macOS");
|
|
2099
|
+
}
|
|
2100
|
+
if (engine !== "podman" || platform !== "linux") return;
|
|
2101
|
+
if (uid === 0) throw new TypeError("the Linux harness requires rootless Podman; do not run the runner as root");
|
|
2102
|
+
const rootless = await (options.podmanRootless ?? inspectRootlessPodman)();
|
|
2103
|
+
if (!rootless) throw new TypeError("the active Podman service is not rootless; refusing to run the harness");
|
|
2104
|
+
}
|
|
2105
|
+
function buildContainerRunArgs(options) {
|
|
2106
|
+
if (!options.allowUnpinnedImage) assertPinnedImage(options.image);
|
|
2107
|
+
if (/[,\r\n]/.test(options.workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
|
|
2108
|
+
const uid = typeof import_process.getuid === "function" ? (0, import_process.getuid)() : 1e3;
|
|
2109
|
+
const gid = typeof import_process.getgid === "function" ? (0, import_process.getgid)() : 1e3;
|
|
2110
|
+
const safeAttempt = options.task.attemptId.toLowerCase().replace(/[^a-z0-9_.-]/g, "-").slice(0, 40);
|
|
2111
|
+
const name = `odla-harness-${safeAttempt}-${crypto.randomUUID().slice(0, 8)}`;
|
|
2112
|
+
const limits = options.limits ?? {};
|
|
2113
|
+
const access2 = options.workspaceAccess ?? "read-write";
|
|
2114
|
+
const appleMount = access2 === "none" ? [] : [`--mount=type=bind,source=${options.workspaceDir},target=/workspace${access2 === "read-only" ? ",readonly" : ""}`];
|
|
2115
|
+
const ociMount = access2 === "none" ? [] : [`--mount=type=bind,src=${options.workspaceDir},dst=/workspace${access2 === "read-only" ? ",readonly" : ""}`];
|
|
2116
|
+
if (options.engine === "container") {
|
|
2117
|
+
return [
|
|
2118
|
+
"run",
|
|
2119
|
+
"--rm",
|
|
2120
|
+
"--interactive",
|
|
2121
|
+
`--name=${name}`,
|
|
2122
|
+
"--network=none",
|
|
2123
|
+
"--read-only",
|
|
2124
|
+
"--cap-drop=ALL",
|
|
2125
|
+
`--memory=${limits.memory ?? "1g"}`,
|
|
2126
|
+
`--cpus=${limits.cpus ?? 1}`,
|
|
2127
|
+
`--user=${uid}:${gid}`,
|
|
2128
|
+
"--tmpfs=/tmp",
|
|
2129
|
+
...appleMount,
|
|
2130
|
+
"--workdir=/workspace",
|
|
2131
|
+
`--env=ODLA_HARNESS_PROTOCOL=${HARNESS_PROTOCOL_VERSION}`,
|
|
2132
|
+
`--label=ai.odla.harness.attempt=${options.task.attemptId}`,
|
|
2133
|
+
options.image
|
|
2134
|
+
];
|
|
1952
2135
|
}
|
|
1953
|
-
return
|
|
2136
|
+
return [
|
|
2137
|
+
"run",
|
|
2138
|
+
"--rm",
|
|
2139
|
+
"--interactive",
|
|
2140
|
+
`--name=${name}`,
|
|
2141
|
+
"--pull=never",
|
|
2142
|
+
"--network=none",
|
|
2143
|
+
"--read-only",
|
|
2144
|
+
"--cap-drop=ALL",
|
|
2145
|
+
"--security-opt=no-new-privileges",
|
|
2146
|
+
`--pids-limit=${limits.pids ?? 256}`,
|
|
2147
|
+
`--memory=${limits.memory ?? "1g"}`,
|
|
2148
|
+
`--cpus=${limits.cpus ?? 1}`,
|
|
2149
|
+
`--user=${uid}:${gid}`,
|
|
2150
|
+
`--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=${limits.tmpfsBytes ?? 64 * 1024 * 1024}`,
|
|
2151
|
+
...ociMount,
|
|
2152
|
+
"--workdir=/workspace",
|
|
2153
|
+
`--env=ODLA_HARNESS_PROTOCOL=${HARNESS_PROTOCOL_VERSION}`,
|
|
2154
|
+
`--label=ai.odla.harness.attempt=${options.task.attemptId}`,
|
|
2155
|
+
options.image
|
|
2156
|
+
];
|
|
1954
2157
|
}
|
|
1955
|
-
function
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
1961
|
-
const
|
|
1962
|
-
const
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2158
|
+
function containerName(args) {
|
|
2159
|
+
return args.find((arg) => arg.startsWith("--name=")).slice("--name=".length);
|
|
2160
|
+
}
|
|
2161
|
+
async function runContainerAttempt(options) {
|
|
2162
|
+
if (options.signal?.aborted) return { exitCode: 1, status: "cancelled", stderr: "" };
|
|
2163
|
+
await verifyContainerEngineBoundary(options.engine);
|
|
2164
|
+
const args = buildContainerRunArgs(options);
|
|
2165
|
+
const name = containerName(args);
|
|
2166
|
+
const child = (0, import_child_process.spawn)(options.engine, args, { stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
2167
|
+
let stderr = "";
|
|
2168
|
+
let outputBytes = 0;
|
|
2169
|
+
let complete = null;
|
|
2170
|
+
let stopped = false;
|
|
2171
|
+
let exited = false;
|
|
2172
|
+
child.stderr.setEncoding("utf8");
|
|
2173
|
+
child.stderr.on("data", (text) => {
|
|
2174
|
+
if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
|
|
2175
|
+
});
|
|
2176
|
+
const stop = (reason) => {
|
|
2177
|
+
if (stopped || exited) return;
|
|
2178
|
+
stopped = true;
|
|
2179
|
+
if (!child.stdin.destroyed) {
|
|
2180
|
+
const cancel = { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "attempt.cancel", reason };
|
|
2181
|
+
child.stdin.write(encodeAgentInput(cancel));
|
|
1966
2182
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2183
|
+
const removeArgs = options.engine === "container" ? ["delete", "--force", name] : ["rm", "-f", name];
|
|
2184
|
+
const killer = (0, import_child_process.spawn)(options.engine, removeArgs, { stdio: "ignore", shell: false });
|
|
2185
|
+
killer.unref();
|
|
2186
|
+
};
|
|
2187
|
+
const abort = () => stop("runner_cancelled");
|
|
2188
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
2189
|
+
const timeout = setTimeout(() => stop("timeout"), options.task.policy.timeoutMs);
|
|
2190
|
+
const start = { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "task.start", task: options.task };
|
|
2191
|
+
if (!stopped && !options.signal?.aborted) child.stdin.write(encodeAgentInput(start));
|
|
2192
|
+
const consume = (async () => {
|
|
2193
|
+
let pending = Buffer.alloc(0);
|
|
2194
|
+
const handleLine = async (raw) => {
|
|
2195
|
+
const bytes = raw.at(-1) === 13 ? raw.subarray(0, -1) : raw;
|
|
2196
|
+
if (bytes.byteLength > 1e6) throw new Error("agent message exceeds 1 MB");
|
|
2197
|
+
const line = bytes.toString("utf8");
|
|
2198
|
+
if (!line.trim()) return;
|
|
2199
|
+
const message3 = parseAgentOutput(line);
|
|
2200
|
+
if (message3.type === "attempt.complete") complete = message3;
|
|
2201
|
+
const response2 = await options.onMessage(message3);
|
|
2202
|
+
if (response2 && !child.stdin.destroyed) child.stdin.write(encodeAgentInput(response2));
|
|
2203
|
+
};
|
|
2204
|
+
try {
|
|
2205
|
+
for await (const raw of child.stdout) {
|
|
2206
|
+
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
2207
|
+
outputBytes += chunk.byteLength;
|
|
2208
|
+
if (outputBytes > options.task.policy.maxOutputBytes) {
|
|
2209
|
+
throw new Error(`agent output exceeds ${options.task.policy.maxOutputBytes} bytes`);
|
|
2210
|
+
}
|
|
2211
|
+
pending = Buffer.concat([pending, chunk]);
|
|
2212
|
+
let newline = pending.indexOf(10);
|
|
2213
|
+
while (newline >= 0) {
|
|
2214
|
+
await handleLine(pending.subarray(0, newline));
|
|
2215
|
+
pending = pending.subarray(newline + 1);
|
|
2216
|
+
newline = pending.indexOf(10);
|
|
2217
|
+
}
|
|
2218
|
+
if (pending.byteLength > 1e6) throw new Error("agent message exceeds 1 MB");
|
|
1976
2219
|
}
|
|
2220
|
+
if (pending.byteLength) await handleLine(pending);
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
stop("protocol_error");
|
|
2223
|
+
throw error;
|
|
1977
2224
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2225
|
+
})();
|
|
2226
|
+
const exit = new Promise((accept, reject) => {
|
|
2227
|
+
child.once("error", reject);
|
|
2228
|
+
child.once("exit", (code) => {
|
|
2229
|
+
exited = true;
|
|
2230
|
+
accept(code ?? 1);
|
|
2231
|
+
});
|
|
2232
|
+
});
|
|
2233
|
+
try {
|
|
2234
|
+
const [exitCode] = await Promise.all([exit, consume]);
|
|
2235
|
+
if (stderr && options.onStderr) await options.onStderr(stderr);
|
|
2236
|
+
if (options.signal?.aborted) return { exitCode, status: "cancelled", stderr };
|
|
2237
|
+
const terminal = complete;
|
|
2238
|
+
if (!terminal) return { exitCode, status: "failed", result: { error: "agent exited without completion" }, stderr };
|
|
2239
|
+
return { exitCode, status: exitCode === 0 ? terminal.status : "failed", result: terminal.result, stderr };
|
|
2240
|
+
} catch (error) {
|
|
2241
|
+
stop("runner_error");
|
|
2242
|
+
await exit.catch(() => 1);
|
|
2243
|
+
throw error;
|
|
2244
|
+
} finally {
|
|
2245
|
+
clearTimeout(timeout);
|
|
2246
|
+
options.signal?.removeEventListener("abort", abort);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
2250
|
+
var SECRET_FILE = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
2251
|
+
async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
2252
|
+
const files = [];
|
|
2253
|
+
let bytes = 0;
|
|
2254
|
+
const walk = async (dir) => {
|
|
2255
|
+
for (const entry of await (0, import_promises3.readdir)(dir, { withFileTypes: true })) {
|
|
2256
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
2257
|
+
if (!entry.isDirectory() && SECRET_FILE.test(entry.name)) continue;
|
|
2258
|
+
const path = (0, import_path2.join)(dir, entry.name);
|
|
2259
|
+
if (entry.isSymbolicLink()) continue;
|
|
2260
|
+
if (entry.isDirectory()) {
|
|
2261
|
+
await walk(path);
|
|
2262
|
+
continue;
|
|
1984
2263
|
}
|
|
2264
|
+
if (!entry.isFile()) continue;
|
|
2265
|
+
const metadata2 = await (0, import_promises3.stat)(path);
|
|
2266
|
+
bytes += metadata2.size;
|
|
2267
|
+
if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
2268
|
+
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
2269
|
+
files.push({
|
|
2270
|
+
source: path,
|
|
2271
|
+
relativePath: (0, import_path2.relative)(sourceDir, path),
|
|
2272
|
+
mode: metadata2.mode & 511,
|
|
2273
|
+
bytes: metadata2.size
|
|
2274
|
+
});
|
|
1985
2275
|
}
|
|
2276
|
+
};
|
|
2277
|
+
await walk(sourceDir);
|
|
2278
|
+
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
2279
|
+
}
|
|
2280
|
+
async function copyTree(files, destination) {
|
|
2281
|
+
for (const file of files) {
|
|
2282
|
+
const target = (0, import_path2.join)(destination, file.relativePath);
|
|
2283
|
+
await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
|
|
2284
|
+
await (0, import_promises3.copyFile)(file.source, target);
|
|
2285
|
+
await (0, import_promises3.chmod)(target, file.mode);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
async function captureGitDiff(root, maxBytes) {
|
|
2289
|
+
const child = (0, import_child_process2.spawn)("git", [
|
|
2290
|
+
"diff",
|
|
2291
|
+
"--no-index",
|
|
2292
|
+
"--binary",
|
|
2293
|
+
"--no-ext-diff",
|
|
2294
|
+
"--src-prefix=a/",
|
|
2295
|
+
"--dst-prefix=b/",
|
|
2296
|
+
"--",
|
|
2297
|
+
"baseline",
|
|
2298
|
+
"workspace"
|
|
2299
|
+
], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
2300
|
+
const stdout = [];
|
|
2301
|
+
const stderr = [];
|
|
2302
|
+
let bytes = 0;
|
|
2303
|
+
child.stdout.on("data", (chunk) => {
|
|
2304
|
+
bytes += chunk.byteLength;
|
|
2305
|
+
if (bytes > maxBytes) child.kill("SIGKILL");
|
|
2306
|
+
else stdout.push(chunk);
|
|
2307
|
+
});
|
|
2308
|
+
child.stderr.on("data", (chunk) => {
|
|
2309
|
+
if (stderr.reduce((sum, value) => sum + value.byteLength, 0) < 16384) stderr.push(chunk);
|
|
2310
|
+
});
|
|
2311
|
+
const code = await new Promise((accept, reject) => {
|
|
2312
|
+
child.once("error", reject);
|
|
2313
|
+
child.once("exit", accept);
|
|
2314
|
+
});
|
|
2315
|
+
if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
|
|
2316
|
+
if (code !== 0 && code !== 1) {
|
|
2317
|
+
throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
|
|
2318
|
+
}
|
|
2319
|
+
return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
|
|
2320
|
+
}
|
|
2321
|
+
async function stageWorkspace(source, options = {}) {
|
|
2322
|
+
const sourceDir = await (0, import_promises3.realpath)((0, import_path2.resolve)(source));
|
|
2323
|
+
const sourceStat = await (0, import_promises3.stat)(sourceDir);
|
|
2324
|
+
if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
|
|
2325
|
+
const root = await (0, import_promises3.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-harness-"));
|
|
2326
|
+
const baselineDir = (0, import_path2.join)(root, "baseline");
|
|
2327
|
+
const workspaceDir = (0, import_path2.join)(root, "workspace");
|
|
2328
|
+
await Promise.all([(0, import_promises3.mkdir)(baselineDir), (0, import_promises3.mkdir)(workspaceDir)]);
|
|
2329
|
+
try {
|
|
2330
|
+
const files = await sourceFiles(sourceDir, options.maxFiles ?? 2e4, options.maxBytes ?? 512 * 1024 * 1024);
|
|
2331
|
+
await Promise.all([copyTree(files, baselineDir), copyTree(files, workspaceDir)]);
|
|
2332
|
+
return {
|
|
2333
|
+
root,
|
|
2334
|
+
baselineDir,
|
|
2335
|
+
workspaceDir,
|
|
2336
|
+
fileCount: files.length,
|
|
2337
|
+
byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
|
|
2338
|
+
patch: (maxBytes) => captureGitDiff(root, maxBytes),
|
|
2339
|
+
cleanup: () => (0, import_promises3.rm)(root, { recursive: true, force: true })
|
|
2340
|
+
};
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
await (0, import_promises3.rm)(root, { recursive: true, force: true });
|
|
2343
|
+
throw error;
|
|
1986
2344
|
}
|
|
1987
|
-
const pkg = readPackageJson(rootDir);
|
|
1988
|
-
if (pkg && typeof pkg.scripts === "object" && pkg.scripts && "deploy" in pkg.scripts) {
|
|
1989
|
-
warnings.push(`package.json has a script named "deploy" \u2014 CI may auto-deploy it; rename to deploy:app`);
|
|
1990
|
-
}
|
|
1991
|
-
return warnings;
|
|
1992
2345
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2346
|
+
|
|
2347
|
+
// ../camel/dist/chunk-7FHPOQVP.js
|
|
2348
|
+
var CamelError = class extends Error {
|
|
2349
|
+
code;
|
|
2350
|
+
safeMessage;
|
|
2351
|
+
constructor(code, safeMessage, options) {
|
|
2352
|
+
super(safeMessage, options);
|
|
2353
|
+
this.name = "CamelError";
|
|
2354
|
+
this.code = code;
|
|
2355
|
+
this.safeMessage = safeMessage;
|
|
2002
2356
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2357
|
+
};
|
|
2358
|
+
|
|
2359
|
+
// ../camel/dist/chunk-L5DYU2E2.js
|
|
2360
|
+
function canonicalJson(value) {
|
|
2361
|
+
return JSON.stringify(normalize(value));
|
|
2362
|
+
}
|
|
2363
|
+
async function sha256Hex(value) {
|
|
2364
|
+
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
|
|
2365
|
+
const digest2 = await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer);
|
|
2366
|
+
return [...new Uint8Array(digest2)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2367
|
+
}
|
|
2368
|
+
function utf8Length(value) {
|
|
2369
|
+
if (typeof value === "string") return new TextEncoder().encode(value).byteLength;
|
|
2370
|
+
if (value instanceof Uint8Array) return value.byteLength;
|
|
2371
|
+
try {
|
|
2372
|
+
return new TextEncoder().encode(canonicalJson(value)).byteLength;
|
|
2373
|
+
} catch {
|
|
2374
|
+
throw new CamelError("conversion_rejected", "Unsafe input could not be canonically measured.");
|
|
2008
2375
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
source = (0, import_node_fs7.readFileSync)(main, "utf8");
|
|
2016
|
-
} catch {
|
|
2017
|
-
}
|
|
2018
|
-
if (!/\bwithObservability\b/.test(source)) {
|
|
2019
|
-
warnings.push(`wrangler entrypoint ${config.main} does not reference withObservability`);
|
|
2020
|
-
}
|
|
2376
|
+
}
|
|
2377
|
+
function normalize(value) {
|
|
2378
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2379
|
+
if (typeof value === "number") {
|
|
2380
|
+
if (!Number.isFinite(value)) throw new CamelError("state_conflict", "Canonical JSON rejects non-finite numbers.");
|
|
2381
|
+
return value;
|
|
2021
2382
|
}
|
|
2022
|
-
|
|
2023
|
-
if (
|
|
2024
|
-
|
|
2383
|
+
if (Array.isArray(value)) return value.map(normalize);
|
|
2384
|
+
if (value instanceof Uint8Array) return { $bytes: [...value] };
|
|
2385
|
+
if (typeof value === "object") {
|
|
2386
|
+
const record5 = value;
|
|
2387
|
+
return Object.fromEntries(Object.keys(record5).filter((key) => record5[key] !== void 0).sort().map((key) => [key, normalize(record5[key])]));
|
|
2025
2388
|
}
|
|
2026
|
-
|
|
2389
|
+
throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
|
|
2027
2390
|
}
|
|
2028
|
-
function
|
|
2391
|
+
function canRead(readers, readerId) {
|
|
2392
|
+
return readers.kind === "public" || readers.principalIds.includes(readerId);
|
|
2393
|
+
}
|
|
2394
|
+
function dependenciesOf(values, influence = "data") {
|
|
2395
|
+
const result = [];
|
|
2396
|
+
for (const value of values) {
|
|
2397
|
+
result.push(...value.label.dependencies);
|
|
2398
|
+
for (const ref of value.label.provenance) {
|
|
2399
|
+
result.push({ ref, influence, promptSafetyAtUse: value.label.promptSafety });
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
const unique3 = /* @__PURE__ */ new Map();
|
|
2403
|
+
for (const dep of result) unique3.set(`${dep.ref.kind}\0${dep.ref.id}\0${dep.influence}\0${dep.promptSafetyAtUse}`, dep);
|
|
2404
|
+
return [...unique3.values()];
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// ../camel/dist/chunk-S7EVNA2U.js
|
|
2408
|
+
var camelValueBrand = /* @__PURE__ */ Symbol("@odla-ai/camel/value");
|
|
2409
|
+
var authenticCamelValues = /* @__PURE__ */ new WeakSet();
|
|
2410
|
+
function isCamelValue(value) {
|
|
2411
|
+
return typeof value === "object" && value !== null && authenticCamelValues.has(value);
|
|
2412
|
+
}
|
|
2413
|
+
function isSafe(value) {
|
|
2414
|
+
return isCamelValue(value) && value.label.promptSafety === "safe";
|
|
2415
|
+
}
|
|
2416
|
+
function isUnsafe(value) {
|
|
2417
|
+
return isCamelValue(value) && value.label.promptSafety === "unsafe";
|
|
2418
|
+
}
|
|
2419
|
+
function createSafeInternal(value, safeBasis, metadata2) {
|
|
2420
|
+
return createValue(value, {
|
|
2421
|
+
schemaVersion: 1,
|
|
2422
|
+
promptSafety: "safe",
|
|
2423
|
+
safeBasis,
|
|
2424
|
+
...copyMetadata(metadata2)
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
function createUnsafeInternal(value, metadata2) {
|
|
2428
|
+
return createValue(value, {
|
|
2429
|
+
schemaVersion: 1,
|
|
2430
|
+
promptSafety: "unsafe",
|
|
2431
|
+
...copyMetadata(metadata2)
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
function createValue(value, label) {
|
|
2435
|
+
const result = { value, label: Object.freeze(label) };
|
|
2436
|
+
Object.defineProperty(result, camelValueBrand, { value: label.promptSafety, enumerable: false });
|
|
2437
|
+
authenticCamelValues.add(result);
|
|
2438
|
+
return Object.freeze(result);
|
|
2439
|
+
}
|
|
2440
|
+
function copyMetadata(metadata2) {
|
|
2441
|
+
if (metadata2.provenance.length === 0) throw new CamelError("state_conflict", "Label provenance must be non-empty.");
|
|
2442
|
+
const provenance = metadata2.provenance.map(copyRef);
|
|
2443
|
+
const dependencies = (metadata2.dependencies ?? []).map((dependency) => Object.freeze({
|
|
2444
|
+
ref: copyRef(dependency.ref),
|
|
2445
|
+
influence: dependency.influence,
|
|
2446
|
+
promptSafetyAtUse: dependency.promptSafetyAtUse
|
|
2447
|
+
}));
|
|
2448
|
+
return {
|
|
2449
|
+
readers: normalizeReaders(metadata2.readers),
|
|
2450
|
+
provenance: Object.freeze(provenance),
|
|
2451
|
+
dependencies: Object.freeze(dependencies)
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
function copyRef(ref) {
|
|
2455
|
+
if (!ref.id || ref.digest !== void 0 && !ref.digest) throw new CamelError("state_conflict", "Provenance references must have non-empty identities.");
|
|
2456
|
+
return Object.freeze({ kind: ref.kind, id: ref.id, ...ref.digest === void 0 ? {} : { digest: ref.digest } });
|
|
2457
|
+
}
|
|
2458
|
+
function normalizeReaders(readers) {
|
|
2459
|
+
if (readers.kind === "public") return Object.freeze({ kind: "public" });
|
|
2460
|
+
const principalIds = [...new Set(readers.principalIds)].sort();
|
|
2461
|
+
if (principalIds.some((id) => !id)) throw new CamelError("reader_mismatch", "Reader principal IDs must be non-empty.");
|
|
2462
|
+
return Object.freeze({ kind: "principals", principalIds: Object.freeze(principalIds) });
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// ../camel/dist/code.js
|
|
2466
|
+
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
2467
|
+
var SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
2468
|
+
var ID = /^[A-Za-z0-9._:-]{1,160}$/;
|
|
2469
|
+
async function digestCodeVerificationReceipt(fields) {
|
|
2470
|
+
validate(fields);
|
|
2471
|
+
const canonical = {
|
|
2472
|
+
schemaVersion: fields.schemaVersion,
|
|
2473
|
+
verificationId: fields.verificationId,
|
|
2474
|
+
trustedBaseCommitSha: fields.trustedBaseCommitSha,
|
|
2475
|
+
trustedBaseDigest: fields.trustedBaseDigest,
|
|
2476
|
+
patchDigest: fields.patchDigest,
|
|
2477
|
+
candidateDigest: fields.candidateDigest,
|
|
2478
|
+
sourceDigest: fields.sourceDigest,
|
|
2479
|
+
policyDigest: fields.policyDigest,
|
|
2480
|
+
recipes: fields.recipes.map((recipe2) => ({
|
|
2481
|
+
recipeId: recipe2.recipeId,
|
|
2482
|
+
recipeDigest: recipe2.recipeDigest,
|
|
2483
|
+
status: recipe2.status,
|
|
2484
|
+
exitCode: recipe2.exitCode,
|
|
2485
|
+
durationMs: recipe2.durationMs,
|
|
2486
|
+
artifacts: recipe2.artifacts.map((artifact) => ({
|
|
2487
|
+
artifactId: artifact.artifactId,
|
|
2488
|
+
status: artifact.status,
|
|
2489
|
+
bytes: artifact.bytes,
|
|
2490
|
+
digest: artifact.digest
|
|
2491
|
+
}))
|
|
2492
|
+
})),
|
|
2493
|
+
changedTestCount: fields.changedTestCount,
|
|
2494
|
+
changedTestSetDigest: fields.changedTestSetDigest,
|
|
2495
|
+
changedTestsRequireReview: fields.changedTestsRequireReview,
|
|
2496
|
+
outcome: fields.outcome
|
|
2497
|
+
};
|
|
2498
|
+
return `sha256:${await sha256Hex(canonicalJson(canonical))}`;
|
|
2499
|
+
}
|
|
2500
|
+
function validate(fields) {
|
|
2501
|
+
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) {
|
|
2502
|
+
throw new CamelError("state_conflict", "Code verification receipt is malformed or outside its bounds.");
|
|
2503
|
+
}
|
|
2504
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2505
|
+
for (const recipe2 of fields.recipes) {
|
|
2506
|
+
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) {
|
|
2507
|
+
throw new CamelError("state_conflict", "Code verification recipe receipt is malformed or outside its bounds.");
|
|
2508
|
+
}
|
|
2509
|
+
const artifactIds = /* @__PURE__ */ new Set();
|
|
2510
|
+
if (recipe2.artifacts.length > 64) throw new CamelError("state_conflict", "Code verification has too many artifacts.");
|
|
2511
|
+
for (const artifact of recipe2.artifacts) {
|
|
2512
|
+
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)) {
|
|
2513
|
+
throw new CamelError("state_conflict", "Code verification artifact receipt is malformed.");
|
|
2514
|
+
}
|
|
2515
|
+
artifactIds.add(artifact.artifactId);
|
|
2516
|
+
}
|
|
2517
|
+
if (recipe2.status === "passed" && (recipe2.exitCode !== 0 || recipe2.artifacts.some((artifact) => artifact.status !== "verified"))) {
|
|
2518
|
+
throw new CamelError("state_conflict", "A passing recipe must verify every registered artifact.");
|
|
2519
|
+
}
|
|
2520
|
+
ids.add(recipe2.recipeId);
|
|
2521
|
+
}
|
|
2522
|
+
const passed = fields.recipes.every((recipe2) => recipe2.status === "passed");
|
|
2523
|
+
if (fields.outcome === "passed" !== passed) {
|
|
2524
|
+
throw new CamelError("state_conflict", "Code verification outcome does not match its recipe receipts.");
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
var SHA2 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
2528
|
+
var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
|
|
2529
|
+
var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
|
|
2530
|
+
var MAX_PATCH_BYTES = 256 * 1024;
|
|
2531
|
+
var MAX_STATE_BYTES = 64 * 1024;
|
|
2532
|
+
async function createCodePortableCheckpoint(input) {
|
|
2533
|
+
const state = normalizeState(input.state);
|
|
2534
|
+
validateBaseAndPatch(input.baseCommitSha, input.patch);
|
|
2535
|
+
const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
|
|
2536
|
+
const stateDigest = `sha256:${await sha256Hex(canonicalJson(state))}`;
|
|
2537
|
+
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
|
|
2538
|
+
const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
|
|
2539
|
+
return freeze({ ...fields, patch: input.patch, checkpointDigest });
|
|
2540
|
+
}
|
|
2541
|
+
async function verifyCodePortableCheckpoint(value) {
|
|
2542
|
+
const input = object(value, "checkpoint");
|
|
2543
|
+
exact2(input, ["schemaVersion", "baseCommitSha", "patch", "patchDigest", "state", "stateDigest", "checkpointDigest"]);
|
|
2544
|
+
if (input.schemaVersion !== 1 || typeof input.baseCommitSha !== "string" || typeof input.patch !== "string" || typeof input.patchDigest !== "string" || typeof input.stateDigest !== "string" || typeof input.checkpointDigest !== "string") {
|
|
2545
|
+
throw invalid2("Portable checkpoint fields are malformed.");
|
|
2546
|
+
}
|
|
2547
|
+
const created = await createCodePortableCheckpoint({
|
|
2548
|
+
baseCommitSha: input.baseCommitSha,
|
|
2549
|
+
patch: input.patch,
|
|
2550
|
+
state: normalizeState(input.state)
|
|
2551
|
+
});
|
|
2552
|
+
if (created.patchDigest !== input.patchDigest || created.stateDigest !== input.stateDigest || created.checkpointDigest !== input.checkpointDigest) {
|
|
2553
|
+
throw invalid2("Portable checkpoint digest does not match its content.");
|
|
2554
|
+
}
|
|
2555
|
+
return created;
|
|
2556
|
+
}
|
|
2557
|
+
function normalizeState(value) {
|
|
2558
|
+
const state = object(value, "state");
|
|
2559
|
+
exact2(state, [
|
|
2560
|
+
"planCursor",
|
|
2561
|
+
"conversationRefs",
|
|
2562
|
+
"planningInputDigest",
|
|
2563
|
+
"buildPolicyDigest",
|
|
2564
|
+
"dependencyLayerDigest",
|
|
2565
|
+
"verificationDigest",
|
|
2566
|
+
"reviewDigest",
|
|
2567
|
+
"completedEffects",
|
|
2568
|
+
"unresolvedApprovals",
|
|
2569
|
+
"trustStatus"
|
|
2570
|
+
]);
|
|
2571
|
+
const planCursor = state.planCursor;
|
|
2572
|
+
const conversations = strings(state.conversationRefs, "conversationRefs", ID2, 256, false);
|
|
2573
|
+
const approvals = strings(state.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
|
|
2574
|
+
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) {
|
|
2575
|
+
throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
|
|
2576
|
+
}
|
|
2577
|
+
const effects = state.completedEffects.map((item) => {
|
|
2578
|
+
const effect = object(item, "completed effect");
|
|
2579
|
+
exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
|
|
2580
|
+
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)) {
|
|
2581
|
+
throw invalid2("Portable checkpoint effect receipt is malformed.");
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
effectId: effect.effectId,
|
|
2585
|
+
actionDigest: effect.actionDigest,
|
|
2586
|
+
receiptDigest: effect.receiptDigest
|
|
2587
|
+
};
|
|
2588
|
+
});
|
|
2589
|
+
if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
|
|
2590
|
+
throw invalid2("Portable checkpoint repeats a completed effect.");
|
|
2591
|
+
}
|
|
2592
|
+
const trustStatus = state.trustStatus;
|
|
2593
|
+
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)) {
|
|
2594
|
+
throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
|
|
2595
|
+
}
|
|
2596
|
+
const normalized = {
|
|
2597
|
+
planCursor,
|
|
2598
|
+
conversationRefs: conversations,
|
|
2599
|
+
planningInputDigest: state.planningInputDigest,
|
|
2600
|
+
buildPolicyDigest: state.buildPolicyDigest,
|
|
2601
|
+
dependencyLayerDigest: state.dependencyLayerDigest,
|
|
2602
|
+
verificationDigest: state.verificationDigest,
|
|
2603
|
+
reviewDigest: state.reviewDigest,
|
|
2604
|
+
completedEffects: effects,
|
|
2605
|
+
unresolvedApprovals: approvals,
|
|
2606
|
+
trustStatus
|
|
2607
|
+
};
|
|
2608
|
+
if (utf8Length(normalized) > MAX_STATE_BYTES) throw invalid2("Portable checkpoint state exceeds 64 KB.");
|
|
2609
|
+
return normalized;
|
|
2610
|
+
}
|
|
2611
|
+
function validateBaseAndPatch(baseCommitSha, patch2) {
|
|
2612
|
+
if (!SHA2.test(baseCommitSha) || utf8Length(patch2) > MAX_PATCH_BYTES || patch2.includes("\0")) {
|
|
2613
|
+
throw invalid2("Portable checkpoint base or patch is malformed or outside its bounds.");
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
function strings(value, name, pattern, maximum, sort) {
|
|
2617
|
+
if (!Array.isArray(value) || value.length > maximum || value.some((item) => typeof item !== "string" || !pattern.test(item)) || new Set(value).size !== value.length) {
|
|
2618
|
+
throw invalid2(`Portable checkpoint ${name} is malformed or outside its bounds.`);
|
|
2619
|
+
}
|
|
2620
|
+
const result = [...value];
|
|
2621
|
+
return sort ? result.sort() : result;
|
|
2622
|
+
}
|
|
2623
|
+
function object(value, name) {
|
|
2624
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid2(`Portable ${name} must be an object.`);
|
|
2625
|
+
return value;
|
|
2626
|
+
}
|
|
2627
|
+
function exact2(value, keys) {
|
|
2628
|
+
if (Object.keys(value).some((key) => !keys.includes(key)) || keys.some((key) => !(key in value))) {
|
|
2629
|
+
throw invalid2("Portable checkpoint contains missing or unsupported fields.");
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
function freeze(value) {
|
|
2633
|
+
return Object.freeze({
|
|
2634
|
+
...value,
|
|
2635
|
+
state: Object.freeze({
|
|
2636
|
+
...value.state,
|
|
2637
|
+
conversationRefs: Object.freeze([...value.state.conversationRefs]),
|
|
2638
|
+
completedEffects: Object.freeze(value.state.completedEffects.map((item) => Object.freeze({ ...item }))),
|
|
2639
|
+
unresolvedApprovals: Object.freeze([...value.state.unresolvedApprovals])
|
|
2640
|
+
})
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
function invalid2(message3) {
|
|
2644
|
+
return new CamelError("state_conflict", message3);
|
|
2645
|
+
}
|
|
2646
|
+
var SHA4 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
2647
|
+
var REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
2648
|
+
var DEFAULT_LIMITS = { maximumFiles: 4e3, maximumBytes: 2 * 1024 * 1024 };
|
|
2649
|
+
async function digestCodeRepositorySnapshot(snapshot, limits = DEFAULT_LIMITS) {
|
|
2650
|
+
validateSnapshot(snapshot, limits);
|
|
2651
|
+
const normalized = {
|
|
2652
|
+
repository: snapshot.repository,
|
|
2653
|
+
commitSha: snapshot.commitSha,
|
|
2654
|
+
files: [...snapshot.files].map((file) => ({ path: file.path, content: file.content })).sort((left, right) => left.path.localeCompare(right.path))
|
|
2655
|
+
};
|
|
2656
|
+
return `sha256:${await sha256Hex(canonicalJson(normalized))}`;
|
|
2657
|
+
}
|
|
2658
|
+
function validateSnapshot(snapshot, limits) {
|
|
2659
|
+
if (!REPOSITORY.test(snapshot.repository) || !SHA4.test(snapshot.commitSha)) {
|
|
2660
|
+
throw new CamelError("state_conflict", "Code snapshot repository or commit is invalid.");
|
|
2661
|
+
}
|
|
2662
|
+
if (!Number.isSafeInteger(limits.maximumFiles) || limits.maximumFiles < 1 || !Number.isSafeInteger(limits.maximumBytes) || limits.maximumBytes < 1 || snapshot.files.length > limits.maximumFiles) {
|
|
2663
|
+
throw new CamelError("limit_exceeded", "Code snapshot exceeds its registered limits.");
|
|
2664
|
+
}
|
|
2665
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2666
|
+
let bytes = 0;
|
|
2667
|
+
for (const file of snapshot.files) {
|
|
2668
|
+
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") {
|
|
2669
|
+
throw new CamelError("state_conflict", "Code snapshot contains an invalid or duplicate path.");
|
|
2670
|
+
}
|
|
2671
|
+
paths.add(file.path);
|
|
2672
|
+
bytes += utf8Length(file.path) + utf8Length(file.content);
|
|
2673
|
+
}
|
|
2674
|
+
if (bytes > limits.maximumBytes) {
|
|
2675
|
+
throw new CamelError("limit_exceeded", "Code snapshot exceeds its registered limits.");
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// ../harness/dist/chunk-RVUUURK2.js
|
|
2680
|
+
var import_child_process3 = require("child_process");
|
|
2681
|
+
var import_promises4 = require("fs/promises");
|
|
2682
|
+
var import_path3 = require("path");
|
|
2683
|
+
var import_child_process4 = require("child_process");
|
|
2684
|
+
var import_process2 = require("process");
|
|
2685
|
+
var import_crypto = require("crypto");
|
|
2686
|
+
var import_crypto2 = require("crypto");
|
|
2687
|
+
var import_fs2 = require("fs");
|
|
2688
|
+
var import_promises5 = require("fs/promises");
|
|
2689
|
+
var import_path4 = require("path");
|
|
2690
|
+
var import_crypto3 = require("crypto");
|
|
2691
|
+
var import_promises6 = require("fs/promises");
|
|
2692
|
+
var import_path5 = require("path");
|
|
2693
|
+
var import_promises7 = require("fs/promises");
|
|
2694
|
+
var import_os2 = require("os");
|
|
2695
|
+
var import_path6 = require("path");
|
|
2696
|
+
var import_promises8 = require("fs/promises");
|
|
2697
|
+
var import_path7 = require("path");
|
|
2698
|
+
|
|
2699
|
+
// ../camel/dist/chunk-LAXU2AVK.js
|
|
2700
|
+
function conversionPolicyDigest(policy) {
|
|
2701
|
+
return sha256Hex(canonicalJson(policy));
|
|
2702
|
+
}
|
|
2703
|
+
function registeredIdRegistryDigest(values) {
|
|
2704
|
+
return sha256Hex(canonicalJson(values));
|
|
2705
|
+
}
|
|
2706
|
+
async function createConversionRegistry(config) {
|
|
2707
|
+
const policies = /* @__PURE__ */ new Map();
|
|
2708
|
+
for (const policy of config.policies) {
|
|
2709
|
+
validatePolicyShape(policy);
|
|
2710
|
+
if (policies.has(policy.conversionId)) throw new CamelError("state_conflict", "Conversion IDs must be unique.");
|
|
2711
|
+
const { digest: _digest, ...definition } = policy;
|
|
2712
|
+
if (await conversionPolicyDigest(definition) !== policy.digest) throw new CamelError("state_conflict", "Conversion policy digest mismatch.");
|
|
2713
|
+
if (policy.output.kind === "registered_id") {
|
|
2714
|
+
const registry = config.registeredIds?.[policy.output.registryId];
|
|
2715
|
+
const validValues = registry && Object.entries(registry.values).every(([candidate, id]) => candidate.length > 0 && typeof id === "string" && id.length > 0);
|
|
2716
|
+
if (!registry || !validValues || registry.digest !== policy.output.registryDigest || await registeredIdRegistryDigest(registry.values) !== registry.digest) {
|
|
2717
|
+
throw new CamelError("state_conflict", "Registered-ID registry digest mismatch.");
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
policies.set(policy.conversionId, Object.freeze(policy));
|
|
2721
|
+
}
|
|
2722
|
+
const outputCounts = /* @__PURE__ */ new Map();
|
|
2723
|
+
const get = (id, kind) => {
|
|
2724
|
+
const policy = policies.get(id);
|
|
2725
|
+
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
2726
|
+
return policy;
|
|
2727
|
+
};
|
|
2728
|
+
const checked = (source, id, kind) => {
|
|
2729
|
+
const policy = get(id, kind);
|
|
2730
|
+
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
2731
|
+
return policy;
|
|
2732
|
+
};
|
|
2733
|
+
const emit = (source, policy, value) => convert(source, policy, value, outputCounts);
|
|
2734
|
+
const operations = Object.freeze({
|
|
2735
|
+
boolean: async (value, id) => {
|
|
2736
|
+
const policy = checked(value, id, "boolean");
|
|
2737
|
+
return emit(value, policy, requireBoolean(value.value));
|
|
2738
|
+
},
|
|
2739
|
+
integer: async (value, id) => {
|
|
2740
|
+
const policy = checked(value, id, "integer");
|
|
2741
|
+
return emit(value, policy, boundedInteger(value.value, policy.output));
|
|
2742
|
+
},
|
|
2743
|
+
finiteNumber: async (value, id) => {
|
|
2744
|
+
const policy = checked(value, id, "finite_number");
|
|
2745
|
+
return emit(value, policy, boundedNumber(value.value, policy.output));
|
|
2746
|
+
},
|
|
2747
|
+
enum: async (value, id) => {
|
|
2748
|
+
const policy = checked(value, id, "enum");
|
|
2749
|
+
return emit(value, policy, enumMember(value.value, policy.output));
|
|
2750
|
+
},
|
|
2751
|
+
date: async (value, id) => {
|
|
2752
|
+
const policy = checked(value, id, "date");
|
|
2753
|
+
return emit(value, policy, canonicalDate(value.value, policy.output));
|
|
2754
|
+
},
|
|
2755
|
+
registeredId: async (value, id) => {
|
|
2756
|
+
const policy = checked(value, id, "registered_id");
|
|
2757
|
+
const registry = config.registeredIds?.[policy.output.registryId];
|
|
2758
|
+
const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
|
|
2759
|
+
if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
|
|
2760
|
+
return emit(value, policy, output);
|
|
2761
|
+
},
|
|
2762
|
+
digest: async (value, id) => {
|
|
2763
|
+
const policy = checked(value, id, "digest");
|
|
2764
|
+
if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
|
|
2765
|
+
return emit(value, policy, await sha256Hex(value.value));
|
|
2766
|
+
},
|
|
2767
|
+
measure: async (value, metric, id) => {
|
|
2768
|
+
const policy = checked(value, id, "integer");
|
|
2769
|
+
const measured = measure(value.value, metric);
|
|
2770
|
+
return emit(value, policy, boundedInteger(measured, policy.output));
|
|
2771
|
+
},
|
|
2772
|
+
test: async (value, predicateId, id) => {
|
|
2773
|
+
const policy = checked(value, id, "boolean");
|
|
2774
|
+
const predicate = config.predicates?.[predicateId];
|
|
2775
|
+
if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
|
|
2776
|
+
return emit(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
|
|
2777
|
+
}
|
|
2778
|
+
});
|
|
2779
|
+
return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
|
|
2780
|
+
}
|
|
2781
|
+
async function convert(source, policy, value, counts) {
|
|
2782
|
+
const sourceKey = sourceIdentity(source);
|
|
2783
|
+
const countKey = `${policy.conversionId}\0${sourceKey}`;
|
|
2784
|
+
const count = counts.get(countKey) ?? 0;
|
|
2785
|
+
if (count >= policy.maximumOutputsPerArtifact) throw new CamelError("limit_exceeded", "Conversion output count exceeds its per-source bound.");
|
|
2786
|
+
counts.set(countKey, count + 1);
|
|
2787
|
+
return createSafeInternal(value, "atomic_conversion", {
|
|
2788
|
+
readers: source.label.readers,
|
|
2789
|
+
provenance: [...source.label.provenance, { kind: "converter", id: policy.conversionId, digest: policy.digest }],
|
|
2790
|
+
dependencies: dependenciesOf([source])
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
function validatePolicyShape(policy) {
|
|
2794
|
+
if (!policy.conversionId || !Number.isSafeInteger(policy.version) || policy.version < 1 || !policy.digest) throw new CamelError("state_conflict", "Conversion policy identity is invalid.");
|
|
2795
|
+
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.");
|
|
2796
|
+
const output = policy.output;
|
|
2797
|
+
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.");
|
|
2798
|
+
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.");
|
|
2799
|
+
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.");
|
|
2800
|
+
if (output.kind === "registered_id" && (!output.registryId || !output.registryDigest)) throw new CamelError("state_conflict", "Registered-ID conversion identity is invalid.");
|
|
2801
|
+
if (output.kind === "date" && output.format !== "date" && output.format !== "rfc3339") throw new CamelError("state_conflict", "Date conversion format is invalid.");
|
|
2802
|
+
if (output.kind === "digest" && output.algorithm !== "sha256") throw new CamelError("state_conflict", "Digest conversion algorithm is invalid.");
|
|
2803
|
+
}
|
|
2804
|
+
function requireBoolean(value) {
|
|
2805
|
+
if (typeof value !== "boolean") throw new CamelError("conversion_rejected", "Boolean conversion requires a structured boolean.");
|
|
2806
|
+
return value;
|
|
2807
|
+
}
|
|
2808
|
+
function boundedInteger(value, spec) {
|
|
2809
|
+
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.");
|
|
2810
|
+
return value;
|
|
2811
|
+
}
|
|
2812
|
+
function boundedNumber(value, spec) {
|
|
2813
|
+
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.");
|
|
2814
|
+
const text = String(value);
|
|
2815
|
+
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.");
|
|
2816
|
+
return value;
|
|
2817
|
+
}
|
|
2818
|
+
function enumMember(value, spec) {
|
|
2819
|
+
if (spec.kind !== "enum" || typeof value !== "string") throw new CamelError("conversion_rejected", "Enum conversion requires a structured string member.");
|
|
2820
|
+
const member = spec.caseSensitive ? spec.values.find((item) => item === value) : spec.values.find((item) => item.toLocaleLowerCase("en-US") === value.toLocaleLowerCase("en-US"));
|
|
2821
|
+
if (member === void 0) throw new CamelError("conversion_rejected", "Enum conversion rejected a non-member.");
|
|
2822
|
+
return member;
|
|
2823
|
+
}
|
|
2824
|
+
function canonicalDate(value, spec) {
|
|
2825
|
+
if (spec.kind !== "date" || typeof value !== "string") throw new CamelError("conversion_rejected", "Date conversion requires a canonical structured string.");
|
|
2826
|
+
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})$/;
|
|
2827
|
+
const instant = Date.parse(spec.format === "date" ? `${value}T00:00:00Z` : value);
|
|
2828
|
+
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.");
|
|
2829
|
+
if (spec.earliest && value < spec.earliest || spec.latest && value > spec.latest) throw new CamelError("conversion_rejected", "Date conversion rejected an out-of-range value.");
|
|
2830
|
+
return value;
|
|
2831
|
+
}
|
|
2832
|
+
function measure(value, metric) {
|
|
2833
|
+
if (metric === "byte_length" && (typeof value === "string" || value instanceof Uint8Array)) return utf8Length(value);
|
|
2834
|
+
if (metric === "codepoint_length" && typeof value === "string") return [...value].length;
|
|
2835
|
+
if (metric === "item_count" && Array.isArray(value)) return value.length;
|
|
2836
|
+
throw new CamelError("conversion_rejected", "Measurement input does not match the registered metric.");
|
|
2837
|
+
}
|
|
2838
|
+
function evaluatePredicate(value, predicate, registries) {
|
|
2839
|
+
if (predicate.kind === "has_fields") return typeof value === "object" && value !== null && !Array.isArray(value) && predicate.fields.every((field) => Object.hasOwn(value, field));
|
|
2840
|
+
if (predicate.kind === "registered_id") return typeof value === "string" && registries?.[predicate.registryId]?.values[value] !== void 0;
|
|
2841
|
+
const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
2842
|
+
return actual === predicate.valueType;
|
|
2843
|
+
}
|
|
2844
|
+
function sourceIdentity(source) {
|
|
2845
|
+
const artifact = [...source.label.provenance, ...source.label.dependencies.map((dependency) => dependency.ref)].find((ref2) => ref2.kind === "artifact");
|
|
2846
|
+
const fallback = source.label.provenance[0];
|
|
2847
|
+
if (!artifact && !fallback) throw new CamelError("conversion_rejected", "Conversion source has no immutable provenance.");
|
|
2848
|
+
const ref = artifact ?? fallback;
|
|
2849
|
+
return `${ref.kind}:${ref.id}:${ref.digest ?? ""}`;
|
|
2850
|
+
}
|
|
2851
|
+
function missingPolicy() {
|
|
2852
|
+
throw new CamelError("conversion_rejected", "Conversion policy is not registered.");
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
// ../camel/dist/chunk-P2WQIAG6.js
|
|
2856
|
+
function createCamelIngress(constants2 = []) {
|
|
2857
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2858
|
+
for (const item of constants2) {
|
|
2859
|
+
if (!item.id || byId.has(item.id)) throw new CamelError("state_conflict", "Control constant IDs must be unique and non-empty.");
|
|
2860
|
+
assertNoUnsafeConstant(item.value);
|
|
2861
|
+
byId.set(item.id, item);
|
|
2862
|
+
}
|
|
2863
|
+
const ingress = {
|
|
2864
|
+
userInstruction: (value, input) => createSafeInternal(value, "user_instruction", metadata("user_instruction", input.id, input.readers)),
|
|
2865
|
+
systemPolicy: (value, input) => createSafeInternal(value, "system_policy", metadata("system_policy", input.id, input.readers)),
|
|
2866
|
+
control: (id) => {
|
|
2867
|
+
const item = byId.get(id);
|
|
2868
|
+
if (!item) throw new CamelError("permission_denied", "Unknown control constant.");
|
|
2869
|
+
return createSafeInternal(item.value, "harness_constant", metadata("harness", id, item.readers));
|
|
2870
|
+
},
|
|
2871
|
+
external: (value, label) => createUnsafeInternal(value, label),
|
|
2872
|
+
quarantinedOutput: (value, input) => {
|
|
2873
|
+
if (!input.runId) throw new CamelError("state_conflict", "Quarantined run IDs must be non-empty.");
|
|
2874
|
+
return createUnsafeInternal(value, {
|
|
2875
|
+
readers: input.readers,
|
|
2876
|
+
dependencies: input.dependencies,
|
|
2877
|
+
provenance: [{ kind: "quarantined_llm", id: input.runId, digest: input.digest }]
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
return Object.freeze(ingress);
|
|
2882
|
+
}
|
|
2883
|
+
function assertNoUnsafeConstant(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2884
|
+
if (typeof value !== "object" || value === null || seen.has(value)) return;
|
|
2885
|
+
seen.add(value);
|
|
2886
|
+
if (isCamelValue(value)) {
|
|
2887
|
+
if (isUnsafe(value)) throw new CamelError("unsafe_privileged_flow", "Control constants cannot contain Prompt-Unsafe values.");
|
|
2888
|
+
assertNoUnsafeConstant(value.value, seen);
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
for (const child of Object.values(value)) assertNoUnsafeConstant(child, seen);
|
|
2892
|
+
}
|
|
2893
|
+
function metadata(kind, id, readers) {
|
|
2894
|
+
if (!id) throw new CamelError("state_conflict", "Provenance IDs must be non-empty.");
|
|
2895
|
+
return { readers, provenance: [{ kind, id }] };
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// ../camel/dist/policy.js
|
|
2899
|
+
function createEffectPolicy(config = {}) {
|
|
2900
|
+
const approvalEffects = new Set(config.approvalEffects ?? ["irreversible_mutation", "external_send", "financial", "secret_read", "code_execution"]);
|
|
2901
|
+
const registries = copyRegistries(config.destinationRegistries ?? {});
|
|
2902
|
+
return Object.freeze({ evaluate: (input) => evaluate(input, registries, approvalEffects) });
|
|
2903
|
+
}
|
|
2904
|
+
function destinationRegistryDigest(values) {
|
|
2905
|
+
return sha256Hex(canonicalJson([...values].sort()));
|
|
2906
|
+
}
|
|
2907
|
+
async function evaluate(input, registries, approvals) {
|
|
2908
|
+
const actionDigest = await sha256Hex(canonicalJson(input));
|
|
2909
|
+
const decisionId = `decision_${actionDigest.slice(0, 24)}`;
|
|
2910
|
+
const deny = (reasonCode) => ({ outcome: "deny", decisionId, reasonCode });
|
|
2911
|
+
if (!input.planId || !input.tool.name || !input.tool.policyId || !Number.isSafeInteger(input.tool.version)) return deny("invalid_descriptor");
|
|
2912
|
+
const expectedPaths = Object.keys(input.tool.argumentRoles).sort();
|
|
2913
|
+
const actualPaths = Object.keys(input.args).sort();
|
|
2914
|
+
if (expectedPaths.length !== actualPaths.length || expectedPaths.some((path, index) => path !== actualPaths[index])) return deny("argument_shape_mismatch");
|
|
2915
|
+
const confined = /* @__PURE__ */ new Set();
|
|
2916
|
+
for (const [path, arg] of Object.entries(input.args)) {
|
|
2917
|
+
if (arg.role !== input.tool.argumentRoles[path]) return deny("argument_role_mismatch");
|
|
2918
|
+
const invalid3 = await validateArgument(path, arg, input.tool, registries);
|
|
2919
|
+
if (invalid3) return deny(invalid3);
|
|
2920
|
+
if (arg.role === "selector" && !isSafe(arg.value)) confined.add(arg.value);
|
|
2921
|
+
}
|
|
2922
|
+
if (input.controlDependencies.some((value) => !isSafe(value) && !confined.has(value))) return deny("unsafe_control_dependency");
|
|
2923
|
+
if (confined.size > 0) {
|
|
2924
|
+
const fixed = input.tool.unsafeSelectorPolicy?.fixedProviderIds ?? [];
|
|
2925
|
+
const destinations = Object.values(input.args).filter((arg) => arg.role === "destination");
|
|
2926
|
+
if (destinations.length === 0 || destinations.some((arg) => !fixed.includes(arg.value.value))) return deny("unsafe_selector_provider_mismatch");
|
|
2927
|
+
}
|
|
2928
|
+
const readerValues = input.intendedReaderIds ?? [];
|
|
2929
|
+
if (readerValues.some((reader) => !isSafe(reader) || typeof reader.value !== "string" || !reader.value || !isControlOwned(reader))) return deny("invalid_reader");
|
|
2930
|
+
const readers = readerValues.map((reader) => reader.value);
|
|
2931
|
+
for (const arg of Object.values(input.args)) {
|
|
2932
|
+
if (readers.some((reader) => !canRead(arg.value.label.readers, reader))) return deny("reader_mismatch");
|
|
2933
|
+
}
|
|
2934
|
+
const hasUnsafePayload = Object.values(input.args).some((arg) => arg.role === "payload" && !isSafe(arg.value));
|
|
2935
|
+
if (hasUnsafePayload && input.tool.effect === "external_send" && readers.length === 0) return deny("reader_required");
|
|
2936
|
+
if (approvals.has(input.tool.effect)) return { outcome: "require_approval", decisionId, reasonCode: "effect_requires_approval", actionDigest };
|
|
2937
|
+
return { outcome: "allow", decisionId };
|
|
2938
|
+
}
|
|
2939
|
+
async function validateArgument(path, arg, tool, registries) {
|
|
2940
|
+
if (arg.role === "instruction" && !isSafe(arg.value)) return "unsafe_instruction_argument";
|
|
2941
|
+
if (arg.role === "authority") {
|
|
2942
|
+
if (!isSafe(arg.value)) return "unsafe_authority_argument";
|
|
2943
|
+
if (!isControlOwned(arg.value)) return "authority_not_control_owned";
|
|
2944
|
+
}
|
|
2945
|
+
if (arg.role === "destination") {
|
|
2946
|
+
if (!isSafe(arg.value)) return "unsafe_destination_argument";
|
|
2947
|
+
if (!isControlOwned(arg.value)) return "destination_not_control_owned";
|
|
2948
|
+
const registry = registries[arg.registryId];
|
|
2949
|
+
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;
|
|
2950
|
+
if (!registry || !validValues || registry.digest !== arg.registryDigest || await destinationRegistryDigest(registry.values) !== registry.digest || !registry.values.includes(arg.value.value)) return "destination_not_registered";
|
|
2951
|
+
}
|
|
2952
|
+
if (arg.role === "selector" && !isSafe(arg.value)) return validateUnsafeSelector(path, arg.value, tool);
|
|
2953
|
+
return void 0;
|
|
2954
|
+
}
|
|
2955
|
+
function isControlOwned(value) {
|
|
2956
|
+
return value.label.safeBasis === "system_policy" || value.label.safeBasis === "harness_constant";
|
|
2957
|
+
}
|
|
2958
|
+
function copyRegistries(registries) {
|
|
2959
|
+
return Object.freeze(Object.fromEntries(Object.entries(registries).map(([id, registry]) => [id, Object.freeze({ digest: registry.digest, values: Object.freeze([...registry.values]) })])));
|
|
2960
|
+
}
|
|
2961
|
+
function validateUnsafeSelector(path, value, tool) {
|
|
2962
|
+
const policy = tool.unsafeSelectorPolicy;
|
|
2963
|
+
if (!policy || policy.effect !== tool.effect || !policy.selectorPaths.includes(path)) return "unsafe_selector_not_confined";
|
|
2964
|
+
if (!policy.fixedDestination || !policy.unsafeOutput || policy.fixedProviderIds.length === 0) return "unsafe_selector_not_confined";
|
|
2965
|
+
if (![policy.maximumCalls, policy.maximumResultsPerCall, policy.maximumOutputBytes, policy.maximumSelectorBytes].every((bound) => Number.isSafeInteger(bound) && bound > 0)) return "unsafe_selector_not_confined";
|
|
2966
|
+
if (utf8Length(value.value) > policy.maximumSelectorBytes) return "unsafe_selector_too_large";
|
|
2967
|
+
if (typeof value.value === "string" && looksLikeDestination(value.value)) return "unsafe_selector_is_destination";
|
|
2968
|
+
return void 0;
|
|
2969
|
+
}
|
|
2970
|
+
function looksLikeDestination(value) {
|
|
2971
|
+
const text = value.trim();
|
|
2972
|
+
return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
// ../harness/dist/chunk-RVUUURK2.js
|
|
2976
|
+
var import_crypto4 = require("crypto");
|
|
2977
|
+
var CODE_RUNTIME_PROTOCOL_VERSION = 1;
|
|
2978
|
+
async function runCodeRuntimeHeartbeatLoop(options) {
|
|
2979
|
+
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
2980
|
+
if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
|
|
2981
|
+
throw new TypeError("heartbeatMs must be an integer from 1000 to 300000");
|
|
2982
|
+
}
|
|
2983
|
+
let retryMs = 1e3;
|
|
2984
|
+
do {
|
|
2985
|
+
if (options.signal?.aborted) return;
|
|
2986
|
+
try {
|
|
2987
|
+
const snapshot = await options.control.heartbeat(options.runtimeVersion, options.capabilities);
|
|
2988
|
+
await options.onSnapshot?.(snapshot);
|
|
2989
|
+
retryMs = 1e3;
|
|
2990
|
+
if (options.once) return;
|
|
2991
|
+
await wait(heartbeatMs, options.signal);
|
|
2992
|
+
} catch (error) {
|
|
2993
|
+
if (options.signal?.aborted) return;
|
|
2994
|
+
if (options.once || !retryableControlFailure(error)) throw error;
|
|
2995
|
+
await options.onRetry?.(error, retryMs);
|
|
2996
|
+
await wait(retryMs, options.signal);
|
|
2997
|
+
retryMs = Math.min(retryMs * 2, 3e4);
|
|
2998
|
+
}
|
|
2999
|
+
} while (!options.signal?.aborted);
|
|
3000
|
+
}
|
|
3001
|
+
var CodeRuntimeReconciler = class {
|
|
3002
|
+
constructor(control, engine) {
|
|
3003
|
+
this.control = control;
|
|
3004
|
+
this.engine = engine;
|
|
3005
|
+
}
|
|
3006
|
+
control;
|
|
3007
|
+
engine;
|
|
3008
|
+
results = /* @__PURE__ */ new Map();
|
|
3009
|
+
async reconcile(snapshot) {
|
|
3010
|
+
for (const command of snapshot.commands) {
|
|
3011
|
+
let completed = this.results.get(command.commandId);
|
|
3012
|
+
if (!completed) {
|
|
3013
|
+
let result;
|
|
3014
|
+
try {
|
|
3015
|
+
result = await this.engine.execute(command);
|
|
3016
|
+
} catch (error) {
|
|
3017
|
+
result = { status: "failed", message: (error instanceof Error ? error.message : String(error)).slice(0, 2e3) };
|
|
3018
|
+
}
|
|
3019
|
+
completed = { result, notified: false };
|
|
3020
|
+
this.results.set(command.commandId, completed);
|
|
3021
|
+
if (this.results.size > 1024) this.results.delete(this.results.keys().next().value);
|
|
3022
|
+
}
|
|
3023
|
+
await this.control.acknowledge(command.commandId, completed.result);
|
|
3024
|
+
if (!completed.notified) {
|
|
3025
|
+
await this.engine.acknowledged?.(command, completed.result);
|
|
3026
|
+
completed.notified = true;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
function retryableControlFailure(value) {
|
|
3032
|
+
if (!value || typeof value !== "object") return false;
|
|
3033
|
+
const failure = value;
|
|
3034
|
+
if (failure.code === "invalid_response" || typeof failure.status !== "number") return false;
|
|
3035
|
+
return failure.status === 408 || failure.status === 425 || failure.status === 429 || failure.status >= 500;
|
|
3036
|
+
}
|
|
3037
|
+
function wait(ms, signal) {
|
|
3038
|
+
return new Promise((resolve52) => {
|
|
3039
|
+
if (signal?.aborted) return resolve52();
|
|
3040
|
+
const timer = setTimeout(resolve52, ms);
|
|
3041
|
+
signal?.addEventListener("abort", () => {
|
|
3042
|
+
clearTimeout(timer);
|
|
3043
|
+
resolve52();
|
|
3044
|
+
}, { once: true });
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
var CodeRuntimeControlError = class extends Error {
|
|
3048
|
+
constructor(message3, status, code = "control_error") {
|
|
3049
|
+
super(message3);
|
|
3050
|
+
this.status = status;
|
|
3051
|
+
this.code = code;
|
|
3052
|
+
}
|
|
3053
|
+
status;
|
|
3054
|
+
code;
|
|
3055
|
+
name = "CodeRuntimeControlError";
|
|
3056
|
+
};
|
|
3057
|
+
function createCodeRuntimeControlClient(options) {
|
|
3058
|
+
const endpoint = validatedEndpoint(options.endpoint);
|
|
3059
|
+
if (!/^odla_code_host_[0-9a-f]{64}$/.test(options.token)) throw new TypeError("invalid Code host credential");
|
|
3060
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
3061
|
+
if (!Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 1e3 || requestTimeoutMs > 12e4) {
|
|
3062
|
+
throw new TypeError("requestTimeoutMs must be an integer from 1000 to 120000");
|
|
3063
|
+
}
|
|
3064
|
+
const request = options.fetch ?? fetch;
|
|
3065
|
+
const call = async (path, body) => {
|
|
3066
|
+
const timeout = AbortSignal.timeout(requestTimeoutMs);
|
|
3067
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
3068
|
+
let response2;
|
|
3069
|
+
try {
|
|
3070
|
+
response2 = await request(`${endpoint}${path}`, {
|
|
3071
|
+
method: "POST",
|
|
3072
|
+
headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
|
|
3073
|
+
body: JSON.stringify(body),
|
|
3074
|
+
redirect: "error",
|
|
3075
|
+
signal
|
|
3076
|
+
});
|
|
3077
|
+
} catch (cause) {
|
|
3078
|
+
if (options.signal?.aborted) throw cause;
|
|
3079
|
+
throw new CodeRuntimeControlError("Code runtime control plane is unavailable", 503, "transport_unavailable");
|
|
3080
|
+
}
|
|
3081
|
+
const value = await response2.json().catch(() => null);
|
|
3082
|
+
if (!response2.ok) {
|
|
3083
|
+
const problem = record3(record3(value)?.error);
|
|
3084
|
+
throw new CodeRuntimeControlError(
|
|
3085
|
+
typeof problem?.message === "string" ? problem.message : `Code runtime request failed (${response2.status})`,
|
|
3086
|
+
response2.status,
|
|
3087
|
+
typeof problem?.code === "string" ? problem.code : void 0
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
return value;
|
|
3091
|
+
};
|
|
3092
|
+
return {
|
|
3093
|
+
heartbeat: async (version, capabilities) => {
|
|
3094
|
+
validateHeartbeat(version, capabilities);
|
|
3095
|
+
return parseSnapshot(await call("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
|
|
3096
|
+
},
|
|
3097
|
+
acknowledge: async (commandId, result) => {
|
|
3098
|
+
if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
|
|
3099
|
+
await call(`/registry/code/runtime/commands/${commandId}/ack`, result);
|
|
3100
|
+
},
|
|
3101
|
+
source: async (sessionId) => parseSource(
|
|
3102
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
|
|
3103
|
+
),
|
|
3104
|
+
infer: async (sessionId, inference) => {
|
|
3105
|
+
const value = record3(await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`, inference));
|
|
3106
|
+
if (!value || value.requestId !== inference.requestId || !record3(value.response) || !record3(value.receipt)) {
|
|
3107
|
+
throw new CodeRuntimeControlError("invalid Code inference response", 502, "invalid_response");
|
|
3108
|
+
}
|
|
3109
|
+
return value;
|
|
3110
|
+
},
|
|
3111
|
+
review: async (sessionId, review) => parseReview(
|
|
3112
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review)
|
|
3113
|
+
),
|
|
3114
|
+
submitCandidate: async (sessionId, checkpointId, verification) => {
|
|
3115
|
+
if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
|
|
3116
|
+
return parseCandidate(await call(
|
|
3117
|
+
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
|
|
3118
|
+
{ checkpointId, verification }
|
|
3119
|
+
));
|
|
3120
|
+
},
|
|
3121
|
+
appendSessionEvent: async (sessionId, eventId, event) => {
|
|
3122
|
+
const serialized = JSON.stringify(event);
|
|
3123
|
+
if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
|
|
3124
|
+
throw new TypeError("invalid Code session event");
|
|
3125
|
+
}
|
|
3126
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
|
|
3127
|
+
},
|
|
3128
|
+
reportSessionFailure: async (sessionId, message3) => {
|
|
3129
|
+
if (!message3.trim() || message3.length > 2e3) throw new TypeError("invalid Code session failure");
|
|
3130
|
+
await call(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message3 });
|
|
3131
|
+
}
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
function validatedEndpoint(value) {
|
|
3135
|
+
const endpoint = value.replace(/\/+$/, "");
|
|
3136
|
+
let url;
|
|
3137
|
+
try {
|
|
3138
|
+
url = new URL(endpoint);
|
|
3139
|
+
} catch {
|
|
3140
|
+
throw new TypeError("endpoint must be an HTTPS URL");
|
|
3141
|
+
}
|
|
3142
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
3143
|
+
if (url.username || url.password || url.protocol !== "https:" && !(loopback && url.protocol === "http:")) {
|
|
3144
|
+
throw new TypeError("endpoint must use HTTPS (HTTP is allowed only for loopback testing)");
|
|
3145
|
+
}
|
|
3146
|
+
return endpoint;
|
|
3147
|
+
}
|
|
3148
|
+
function validSessionId(value) {
|
|
3149
|
+
if (!/^csess_[0-9a-f]{32}$/.test(value)) throw new TypeError("invalid Code session id");
|
|
3150
|
+
return value;
|
|
3151
|
+
}
|
|
3152
|
+
function validateHeartbeat(version, capabilities) {
|
|
3153
|
+
if (!version.trim() || version.length > 80) throw new TypeError("runtimeVersion is required and at most 80 characters");
|
|
3154
|
+
if (capabilities.protocolVersion !== CODE_RUNTIME_PROTOCOL_VERSION) throw new TypeError("unsupported Code runtime protocol version");
|
|
3155
|
+
if (capabilities.platform !== "macos" && capabilities.platform !== "linux") throw new TypeError("invalid runtime platform");
|
|
3156
|
+
if (!capabilities.arch || !capabilities.engines.length || !capabilities.engines.every((engine) => ["container", "podman", "docker"].includes(engine))) {
|
|
3157
|
+
throw new TypeError("runtime arch and supported engine are required");
|
|
3158
|
+
}
|
|
3159
|
+
if (!Number.isSafeInteger(capabilities.cpuCount) || capabilities.cpuCount < 1 || !Number.isSafeInteger(capabilities.memoryBytes) || capabilities.memoryBytes < 1) {
|
|
3160
|
+
throw new TypeError("runtime resources must be positive integers");
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
function parseSnapshot(value) {
|
|
3164
|
+
const root = record3(value);
|
|
3165
|
+
const host = record3(root?.host);
|
|
3166
|
+
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");
|
|
3167
|
+
const bindingIds = /* @__PURE__ */ new Set();
|
|
3168
|
+
const bindings = root.bindings.map((item) => {
|
|
3169
|
+
const binding = record3(item);
|
|
3170
|
+
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)) {
|
|
3171
|
+
throw invalid("binding");
|
|
3172
|
+
}
|
|
3173
|
+
bindingIds.add(binding.bindingId);
|
|
3174
|
+
return binding;
|
|
3175
|
+
});
|
|
3176
|
+
const commandIds = /* @__PURE__ */ new Set();
|
|
3177
|
+
const commandSequences = /* @__PURE__ */ new Set();
|
|
3178
|
+
const commands = root.commands.map((item) => {
|
|
3179
|
+
const command = record3(item);
|
|
3180
|
+
const binding = bindings.find((candidate) => candidate.bindingId === command?.bindingId);
|
|
3181
|
+
const sequenceKey = `${String(command?.instanceId)}:${String(command?.sequence)}`;
|
|
3182
|
+
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");
|
|
3183
|
+
commandIds.add(command.commandId);
|
|
3184
|
+
commandSequences.add(sequenceKey);
|
|
3185
|
+
return command;
|
|
3186
|
+
});
|
|
3187
|
+
return { host, bindings, commands };
|
|
3188
|
+
}
|
|
3189
|
+
async function parseSource(value) {
|
|
3190
|
+
const snapshot = record3(record3(value)?.snapshot);
|
|
3191
|
+
if (!snapshot || typeof snapshot.repository !== "string" || typeof snapshot.commitSha !== "string" || typeof snapshot.treeDigest !== "string" || !Array.isArray(snapshot.files)) throw invalid("source");
|
|
3192
|
+
const files = snapshot.files.map((value2) => {
|
|
3193
|
+
const file = record3(value2);
|
|
3194
|
+
if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("source file");
|
|
3195
|
+
return { path: file.path, content: file.content };
|
|
3196
|
+
});
|
|
3197
|
+
const source = { repository: snapshot.repository, commitSha: snapshot.commitSha, files };
|
|
3198
|
+
const digest2 = await digestCodeRepositorySnapshot(source, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
|
|
3199
|
+
if (digest2 !== snapshot.treeDigest) throw invalid("source digest");
|
|
3200
|
+
return { ...source, treeDigest: digest2 };
|
|
3201
|
+
}
|
|
3202
|
+
function parseReview(value) {
|
|
3203
|
+
const review = record3(record3(value)?.review);
|
|
3204
|
+
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");
|
|
3205
|
+
return review;
|
|
3206
|
+
}
|
|
3207
|
+
function parseCandidate(value) {
|
|
3208
|
+
const candidate = record3(record3(value)?.candidate);
|
|
3209
|
+
if (!candidate || typeof candidate.candidateId !== "string" || !/^ccand_[0-9a-f]{32}$/.test(candidate.candidateId) || !["submitted", "approved", "published", "failed"].includes(String(candidate.status))) {
|
|
3210
|
+
throw invalid("candidate");
|
|
3211
|
+
}
|
|
3212
|
+
return { candidateId: candidate.candidateId, status: candidate.status };
|
|
3213
|
+
}
|
|
3214
|
+
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3215
|
+
var invalid = (part) => new CodeRuntimeControlError(`invalid Code runtime ${part} response`, 502, "invalid_response");
|
|
3216
|
+
var RESERVED = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
3217
|
+
var SECRET = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
3218
|
+
var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
3219
|
+
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;
|
|
3220
|
+
function validateCodePatch(patch2, maxBytes) {
|
|
3221
|
+
if (!patch2 || Buffer.byteLength(patch2) > maxBytes || patch2.includes("\0") || patch2.includes("\r")) {
|
|
3222
|
+
throw new TypeError("patch is empty, malformed, or exceeds its byte limit");
|
|
3223
|
+
}
|
|
3224
|
+
if (FORBIDDEN.test(patch2) || /(?:old|new)(?: file)? mode 120000/.test(patch2)) {
|
|
3225
|
+
throw new TypeError("patch uses a forbidden binary, link, mode, rename, or copy operation");
|
|
3226
|
+
}
|
|
3227
|
+
const paths = [];
|
|
3228
|
+
const lines = patch2.split("\n");
|
|
3229
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3230
|
+
const line = lines[index];
|
|
3231
|
+
if (!line.startsWith("diff --git ")) continue;
|
|
3232
|
+
const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line);
|
|
3233
|
+
const path = match?.[1];
|
|
3234
|
+
if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
|
|
3235
|
+
validateRelativePath(path);
|
|
3236
|
+
const header = lines.slice(index + 1).findIndex((candidate) => candidate.startsWith("diff --git "));
|
|
3237
|
+
const section = lines.slice(index + 1, header < 0 ? lines.length : index + 1 + header);
|
|
3238
|
+
const oldPath = section.find((candidate) => candidate.startsWith("--- "))?.slice(4);
|
|
3239
|
+
const newPath = section.find((candidate) => candidate.startsWith("+++ "))?.slice(4);
|
|
3240
|
+
if (!validHeaderPath(oldPath, path, "a") || !validHeaderPath(newPath, path, "b")) {
|
|
3241
|
+
throw new TypeError("patch file headers do not match the declared path");
|
|
3242
|
+
}
|
|
3243
|
+
paths.push(path);
|
|
3244
|
+
}
|
|
3245
|
+
if (!paths.length || new Set(paths).size !== paths.length) throw new TypeError("patch has no diffs or repeats a path");
|
|
3246
|
+
return paths;
|
|
3247
|
+
}
|
|
3248
|
+
function validHeaderPath(value, path, prefix) {
|
|
3249
|
+
return value === "/dev/null" || value === `${prefix}/${path}`;
|
|
3250
|
+
}
|
|
3251
|
+
function validateRelativePath(path) {
|
|
3252
|
+
const parts = path.split("/");
|
|
3253
|
+
if (!PATH.test(path) || parts.some((part) => part === "." || part === ".." || RESERVED.has(part)) || parts.some((part) => SECRET.test(part))) {
|
|
3254
|
+
throw new TypeError("path is outside the allowed staged source tree");
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
function resolveCodePath(workspaceDir, path) {
|
|
3258
|
+
validateRelativePath(path);
|
|
3259
|
+
const root = (0, import_path3.resolve)(workspaceDir);
|
|
3260
|
+
const target = (0, import_path3.resolve)(root, path);
|
|
3261
|
+
if (target !== root && !target.startsWith(`${root}${import_path3.sep}`)) throw new TypeError("path escapes the staged workspace");
|
|
3262
|
+
return target;
|
|
3263
|
+
}
|
|
3264
|
+
async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
3265
|
+
await gitApply(workspaceDir, patch2, true);
|
|
3266
|
+
await gitApply(workspaceDir, patch2, false);
|
|
3267
|
+
for (const path of paths) {
|
|
3268
|
+
try {
|
|
3269
|
+
const info = await (0, import_promises4.lstat)(resolveCodePath(workspaceDir, path));
|
|
3270
|
+
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
3271
|
+
throw new TypeError("patch created a non-regular workspace entry");
|
|
3272
|
+
}
|
|
3273
|
+
} catch (reason) {
|
|
3274
|
+
if (reason.code !== "ENOENT") throw reason;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
function gitApply(cwd, patch2, check) {
|
|
3279
|
+
return new Promise((accept, reject) => {
|
|
3280
|
+
const args = ["apply", "--recount", "--whitespace=nowarn", ...check ? ["--check"] : [], "-"];
|
|
3281
|
+
const child = (0, import_child_process3.spawn)("git", args, {
|
|
3282
|
+
cwd,
|
|
3283
|
+
shell: false,
|
|
3284
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
3285
|
+
env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
|
|
3286
|
+
});
|
|
3287
|
+
let stderr = "";
|
|
3288
|
+
child.stderr.setEncoding("utf8");
|
|
3289
|
+
child.stderr.on("data", (text) => {
|
|
3290
|
+
if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
|
|
3291
|
+
});
|
|
3292
|
+
child.once("error", reject);
|
|
3293
|
+
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
|
|
3294
|
+
child.stdin.end(patch2);
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
async function createCodeWorkspaceCheckpoint(input) {
|
|
3298
|
+
const maximum = input.maximumPatchBytes ?? 256 * 1024;
|
|
3299
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 256 * 1024) {
|
|
3300
|
+
throw new TypeError("checkpoint patch bound must be from 1 to 262144 bytes");
|
|
3301
|
+
}
|
|
3302
|
+
const patch2 = await input.workspace.patch(maximum);
|
|
3303
|
+
if (patch2) validateCodePatch(patch2, maximum);
|
|
3304
|
+
return createCodePortableCheckpoint({ baseCommitSha: input.baseCommitSha, patch: patch2, state: input.state });
|
|
3305
|
+
}
|
|
3306
|
+
async function restoreCodeWorkspaceCheckpoint(input) {
|
|
3307
|
+
const checkpoint = await verifyCodePortableCheckpoint(input.checkpoint);
|
|
3308
|
+
if (checkpoint.baseCommitSha !== input.trustedBaseCommitSha) {
|
|
3309
|
+
throw new TypeError("checkpoint trusted base does not match the fetched commit");
|
|
3310
|
+
}
|
|
3311
|
+
const workspace = await stageWorkspace(input.trustedBaseDir, input.stage);
|
|
3312
|
+
try {
|
|
3313
|
+
if (checkpoint.patch) {
|
|
3314
|
+
const paths = validateCodePatch(checkpoint.patch, 256 * 1024);
|
|
3315
|
+
await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths);
|
|
3316
|
+
}
|
|
3317
|
+
return { workspace, checkpoint };
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
await workspace.cleanup();
|
|
3320
|
+
throw error;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
var ARTIFACT_PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
|
|
3324
|
+
var PRIVATE_ARTIFACT_PART = /^(?:\.git|\.odla|\.wrangler|\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json)$/i;
|
|
3325
|
+
function buildRecipeContainerArgs(engine, workspaceDir, recipe2, name = `odla-recipe-${(0, import_crypto.randomUUID)().slice(0, 12)}`) {
|
|
3326
|
+
assertCodeBuildRecipe(recipe2);
|
|
3327
|
+
if (/[,\r\n]/.test(workspaceDir)) throw new TypeError("workspace path contains unsupported mount characters");
|
|
3328
|
+
const uid = typeof import_process2.getuid === "function" ? (0, import_process2.getuid)() : 1e3;
|
|
3329
|
+
const gid = typeof import_process2.getgid === "function" ? (0, import_process2.getgid)() : 1e3;
|
|
3330
|
+
const limits = {
|
|
3331
|
+
cpus: recipe2.cpus ?? 1,
|
|
3332
|
+
memory: recipe2.memory ?? "1g",
|
|
3333
|
+
pids: recipe2.pids ?? 256,
|
|
3334
|
+
tmpfs: recipe2.tmpfsBytes ?? 64 * 1024 * 1024
|
|
3335
|
+
};
|
|
3336
|
+
if (engine === "container") {
|
|
3337
|
+
return [
|
|
3338
|
+
"run",
|
|
3339
|
+
"--rm",
|
|
3340
|
+
`--name=${name}`,
|
|
3341
|
+
"--network=none",
|
|
3342
|
+
"--read-only",
|
|
3343
|
+
"--cap-drop=ALL",
|
|
3344
|
+
`--memory=${limits.memory}`,
|
|
3345
|
+
`--cpus=${limits.cpus}`,
|
|
3346
|
+
`--user=${uid}:${gid}`,
|
|
3347
|
+
"--tmpfs=/tmp",
|
|
3348
|
+
`--mount=type=bind,source=${workspaceDir},target=/workspace`,
|
|
3349
|
+
"--workdir=/workspace",
|
|
3350
|
+
"--env=CI=1",
|
|
3351
|
+
recipe2.image,
|
|
3352
|
+
...recipe2.command
|
|
3353
|
+
];
|
|
3354
|
+
}
|
|
3355
|
+
return [
|
|
3356
|
+
"run",
|
|
3357
|
+
"--rm",
|
|
3358
|
+
`--name=${name}`,
|
|
3359
|
+
"--pull=never",
|
|
3360
|
+
"--network=none",
|
|
3361
|
+
"--read-only",
|
|
3362
|
+
"--cap-drop=ALL",
|
|
3363
|
+
"--security-opt=no-new-privileges",
|
|
3364
|
+
`--pids-limit=${limits.pids}`,
|
|
3365
|
+
`--memory=${limits.memory}`,
|
|
3366
|
+
`--cpus=${limits.cpus}`,
|
|
3367
|
+
`--user=${uid}:${gid}`,
|
|
3368
|
+
`--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=${limits.tmpfs}`,
|
|
3369
|
+
`--mount=type=bind,src=${workspaceDir},dst=/workspace`,
|
|
3370
|
+
"--workdir=/workspace",
|
|
3371
|
+
"--env=CI=1",
|
|
3372
|
+
recipe2.image,
|
|
3373
|
+
...recipe2.command
|
|
3374
|
+
];
|
|
3375
|
+
}
|
|
3376
|
+
function createContainerRecipeExecutor(engine) {
|
|
3377
|
+
return {
|
|
3378
|
+
async run(input) {
|
|
3379
|
+
await verifyContainerEngineBoundary(engine);
|
|
3380
|
+
const name = `odla-recipe-${(0, import_crypto.randomUUID)().slice(0, 12)}`;
|
|
3381
|
+
const args = buildRecipeContainerArgs(engine, input.workspaceDir, input.recipe, name);
|
|
3382
|
+
return execute(engine, args, name, input.recipe, input.signal);
|
|
3383
|
+
}
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
function assertCodeBuildRecipe(recipe2) {
|
|
3387
|
+
const memoryBytes = parseMemory(recipe2.memory ?? "1g");
|
|
3388
|
+
const tmpfsBytes = recipe2.tmpfsBytes ?? 64 * 1024 * 1024;
|
|
3389
|
+
const artifacts = recipe2.expectedArtifacts ?? [];
|
|
3390
|
+
assertPinnedImage(recipe2.image);
|
|
3391
|
+
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)) {
|
|
3392
|
+
throw new TypeError("build recipe is malformed or exceeds its control bounds");
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
function parseMemory(value) {
|
|
3396
|
+
const match = /^([1-9][0-9]{0,4})([kmg])$/.exec(value.toLowerCase());
|
|
3397
|
+
if (!match?.[1] || !match[2]) return 0;
|
|
3398
|
+
const scale = match[2] === "k" ? 1024 : match[2] === "m" ? 1024 ** 2 : 1024 ** 3;
|
|
3399
|
+
return Number(match[1]) * scale;
|
|
3400
|
+
}
|
|
3401
|
+
function execute(engine, args, name, recipe2, signal) {
|
|
3402
|
+
return new Promise((accept, reject) => {
|
|
3403
|
+
const started = Date.now();
|
|
3404
|
+
const child = (0, import_child_process4.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
3405
|
+
const stdout = [];
|
|
3406
|
+
const stderr = [];
|
|
3407
|
+
let bytes = 0;
|
|
3408
|
+
let outputLimitExceeded = false;
|
|
3409
|
+
let timedOut = false;
|
|
3410
|
+
let stopping = false;
|
|
3411
|
+
const stop = (reason) => {
|
|
3412
|
+
if (stopping) return;
|
|
3413
|
+
stopping = true;
|
|
3414
|
+
timedOut = reason === "timeout";
|
|
3415
|
+
outputLimitExceeded = reason === "output";
|
|
3416
|
+
const remove = engine === "container" ? ["delete", "--force", name] : ["rm", "-f", name];
|
|
3417
|
+
const killer = (0, import_child_process4.spawn)(engine, remove, { shell: false, stdio: "ignore" });
|
|
3418
|
+
killer.unref();
|
|
3419
|
+
child.kill("SIGTERM");
|
|
3420
|
+
};
|
|
3421
|
+
const collect = (target) => (chunk) => {
|
|
3422
|
+
bytes += chunk.byteLength;
|
|
3423
|
+
if (bytes > recipe2.maxOutputBytes) stop("output");
|
|
3424
|
+
else target.push(chunk);
|
|
3425
|
+
};
|
|
3426
|
+
child.stdout.on("data", collect(stdout));
|
|
3427
|
+
child.stderr.on("data", collect(stderr));
|
|
3428
|
+
const abort = () => stop("abort");
|
|
3429
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3430
|
+
if (signal?.aborted) abort();
|
|
3431
|
+
const timer = setTimeout(() => stop("timeout"), recipe2.timeoutMs);
|
|
3432
|
+
child.once("error", (error) => {
|
|
3433
|
+
clearTimeout(timer);
|
|
3434
|
+
signal?.removeEventListener("abort", abort);
|
|
3435
|
+
reject(error);
|
|
3436
|
+
});
|
|
3437
|
+
child.once("exit", (code) => {
|
|
3438
|
+
clearTimeout(timer);
|
|
3439
|
+
signal?.removeEventListener("abort", abort);
|
|
3440
|
+
accept({
|
|
3441
|
+
exitCode: code ?? 1,
|
|
3442
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
3443
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
3444
|
+
durationMs: Date.now() - started,
|
|
3445
|
+
outputLimitExceeded,
|
|
3446
|
+
timedOut
|
|
3447
|
+
});
|
|
3448
|
+
});
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
async function digestStagedWorkspace(root, limits) {
|
|
3452
|
+
const files = [];
|
|
3453
|
+
const walk = async (directory) => {
|
|
3454
|
+
const entries = await (0, import_promises6.readdir)(directory, { withFileTypes: true });
|
|
3455
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
3456
|
+
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
3457
|
+
const target = (0, import_path5.resolve)(directory, entry.name);
|
|
3458
|
+
if (entry.isDirectory()) await walk(target);
|
|
3459
|
+
else if (entry.isFile()) {
|
|
3460
|
+
files.push({ path: (0, import_path5.relative)(root, target).split("\\").join("/"), target });
|
|
3461
|
+
if (files.length > limits.maxFiles) throw new TypeError("workspace digest exceeds its file bound");
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
await walk((0, import_path5.resolve)(root));
|
|
3466
|
+
const hash = (0, import_crypto3.createHash)("sha256");
|
|
3467
|
+
let bytes = 0;
|
|
3468
|
+
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
3469
|
+
const content = await (0, import_promises6.readFile)(file.target);
|
|
3470
|
+
bytes += Buffer.byteLength(file.path) + content.byteLength;
|
|
3471
|
+
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
3472
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content.byteLength}:`);
|
|
3473
|
+
hash.update(content);
|
|
3474
|
+
}
|
|
3475
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3476
|
+
}
|
|
3477
|
+
var SHA3 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
3478
|
+
var DIGEST3 = /^sha256:[0-9a-f]{64}$/;
|
|
3479
|
+
var ID3 = /^[A-Za-z0-9._:-]{1,160}$/;
|
|
3480
|
+
var RULE = /^[A-Za-z0-9_@+.,/-]{1,160}$/;
|
|
3481
|
+
var DEFAULT_PREFIXES = ["test/", "tests/", "__tests__/"];
|
|
3482
|
+
var DEFAULT_SUFFIXES = [".test.js", ".test.ts", ".test.tsx", ".spec.js", ".spec.ts", ".spec.tsx"];
|
|
3483
|
+
async function verifyCodeCandidate(input) {
|
|
3484
|
+
const policy = validate2(input);
|
|
3485
|
+
const limits = { maxFiles: policy.maximumFiles, maxBytes: policy.maximumBytes };
|
|
3486
|
+
const staged = await stageWorkspace(input.trustedBaseDir, limits);
|
|
3487
|
+
try {
|
|
3488
|
+
const baseDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
|
|
3489
|
+
if (baseDigest !== input.trustedBaseDigest) throw new TypeError("trusted base does not match its registered digest");
|
|
3490
|
+
const paths = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
|
|
3491
|
+
await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths);
|
|
3492
|
+
const sourceDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
|
|
3493
|
+
const policyDigest = digestPolicy(policy);
|
|
3494
|
+
const patchDigest = digestBytes(input.candidatePatch);
|
|
3495
|
+
const candidateDigest = digestJson({
|
|
3496
|
+
trustedBaseCommitSha: input.trustedBaseCommitSha,
|
|
3497
|
+
trustedBaseDigest: input.trustedBaseDigest,
|
|
3498
|
+
patchDigest
|
|
3499
|
+
});
|
|
3500
|
+
const changedTests = changedTestPaths(paths, policy);
|
|
3501
|
+
if (changedTests.length > policy.maximumChangedTests) throw new TypeError("candidate changes too many test files");
|
|
3502
|
+
const recipes = [];
|
|
3503
|
+
const logs = [];
|
|
3504
|
+
for (const recipe2 of policy.recipes) {
|
|
3505
|
+
const clean = await stageWorkspace(staged.workspaceDir, limits);
|
|
3506
|
+
try {
|
|
3507
|
+
if (await digestStagedWorkspace(clean.workspaceDir, limits) !== sourceDigest) {
|
|
3508
|
+
throw new TypeError("clean verifier source changed before execution");
|
|
3509
|
+
}
|
|
3510
|
+
const result = checkedResult(await input.recipeExecutor.run({
|
|
3511
|
+
workspaceDir: clean.workspaceDir,
|
|
3512
|
+
recipe: recipe2,
|
|
3513
|
+
signal: input.signal
|
|
3514
|
+
}), recipe2.maxOutputBytes);
|
|
3515
|
+
const artifacts = await inspectArtifacts(clean.workspaceDir, recipe2);
|
|
3516
|
+
recipes.push(recipeReceipt(recipe2, result, artifacts));
|
|
3517
|
+
logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
|
|
3518
|
+
} finally {
|
|
3519
|
+
await clean.cleanup();
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
const fields = {
|
|
3523
|
+
schemaVersion: 1,
|
|
3524
|
+
verificationId: input.verificationId ?? `verify-${(0, import_crypto2.randomUUID)()}`,
|
|
3525
|
+
trustedBaseCommitSha: input.trustedBaseCommitSha,
|
|
3526
|
+
trustedBaseDigest: input.trustedBaseDigest,
|
|
3527
|
+
patchDigest,
|
|
3528
|
+
candidateDigest,
|
|
3529
|
+
sourceDigest,
|
|
3530
|
+
policyDigest,
|
|
3531
|
+
recipes,
|
|
3532
|
+
changedTestCount: changedTests.length,
|
|
3533
|
+
changedTestSetDigest: digestJson(changedTests),
|
|
3534
|
+
changedTestsRequireReview: changedTests.length > 0,
|
|
3535
|
+
outcome: recipes.every((recipe2) => recipe2.status === "passed") ? "passed" : "failed"
|
|
3536
|
+
};
|
|
3537
|
+
return {
|
|
3538
|
+
receipt: { ...fields, receiptDigest: await digestCodeVerificationReceipt(fields) },
|
|
3539
|
+
changedTests: Object.freeze(changedTests),
|
|
3540
|
+
logs: Object.freeze(logs)
|
|
3541
|
+
};
|
|
3542
|
+
} finally {
|
|
3543
|
+
await staged.cleanup();
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
function validate2(input) {
|
|
3547
|
+
const policy = input.policy;
|
|
3548
|
+
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) {
|
|
3549
|
+
throw new TypeError("clean verification input is malformed");
|
|
3550
|
+
}
|
|
3551
|
+
for (const recipe2 of policy.recipes) assertCodeBuildRecipe(recipe2);
|
|
3552
|
+
const result = {
|
|
3553
|
+
policyId: policy.policyId,
|
|
3554
|
+
recipes: policy.recipes,
|
|
3555
|
+
testPathPrefixes: policy.testPathPrefixes ?? DEFAULT_PREFIXES,
|
|
3556
|
+
testPathSuffixes: policy.testPathSuffixes ?? DEFAULT_SUFFIXES,
|
|
3557
|
+
maximumChangedTests: policy.maximumChangedTests ?? 1e3,
|
|
3558
|
+
maximumPatchBytes: policy.maximumPatchBytes ?? 256 * 1024,
|
|
3559
|
+
maximumFiles: policy.maximumFiles ?? 2e4,
|
|
3560
|
+
maximumBytes: policy.maximumBytes ?? 512 * 1024 * 1024
|
|
3561
|
+
};
|
|
3562
|
+
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)) {
|
|
3563
|
+
throw new TypeError("clean verification policy exceeds its bounds");
|
|
3564
|
+
}
|
|
3565
|
+
return result;
|
|
3566
|
+
}
|
|
3567
|
+
function changedTestPaths(paths, policy) {
|
|
3568
|
+
return paths.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
|
|
3569
|
+
}
|
|
3570
|
+
function recipeReceipt(recipe2, result, artifacts) {
|
|
3571
|
+
const status = result.timedOut ? "timed_out" : result.outputLimitExceeded ? "output_limited" : result.exitCode === 0 && artifacts.every((item) => item.status === "verified") ? "passed" : "failed";
|
|
3572
|
+
return {
|
|
3573
|
+
recipeId: recipe2.id,
|
|
3574
|
+
recipeDigest: digestRecipe(recipe2),
|
|
3575
|
+
status,
|
|
3576
|
+
exitCode: result.exitCode,
|
|
3577
|
+
durationMs: result.durationMs,
|
|
3578
|
+
artifacts
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
async function inspectArtifacts(workspaceDir, recipe2) {
|
|
3582
|
+
const receipts = [];
|
|
3583
|
+
for (const artifact of recipe2.expectedArtifacts ?? []) {
|
|
3584
|
+
try {
|
|
3585
|
+
const path = (0, import_path4.join)(workspaceDir, artifact.path);
|
|
3586
|
+
const info = await (0, import_promises5.lstat)(path);
|
|
3587
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
3588
|
+
receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
|
|
3589
|
+
} else if (info.size > artifact.maximumBytes) {
|
|
3590
|
+
receipts.push({ artifactId: artifact.id, status: "too_large", bytes: info.size, digest: null });
|
|
3591
|
+
} else {
|
|
3592
|
+
receipts.push({ artifactId: artifact.id, status: "verified", bytes: info.size, digest: await hashFile(path) });
|
|
3593
|
+
}
|
|
3594
|
+
} catch (reason) {
|
|
3595
|
+
if (reason.code !== "ENOENT") throw reason;
|
|
3596
|
+
receipts.push({ artifactId: artifact.id, status: "missing", bytes: null, digest: null });
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
return receipts;
|
|
3600
|
+
}
|
|
3601
|
+
function hashFile(path) {
|
|
3602
|
+
return new Promise((accept, reject) => {
|
|
3603
|
+
const hash = (0, import_crypto2.createHash)("sha256");
|
|
3604
|
+
const stream = (0, import_fs2.createReadStream)(path);
|
|
3605
|
+
stream.on("data", (chunk) => {
|
|
3606
|
+
hash.update(chunk);
|
|
3607
|
+
});
|
|
3608
|
+
stream.once("error", reject);
|
|
3609
|
+
stream.once("end", () => accept(`sha256:${hash.digest("hex")}`));
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
function checkedResult(result, maximumOutputBytes) {
|
|
3613
|
+
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") {
|
|
3614
|
+
throw new TypeError("recipe executor returned an invalid result");
|
|
3615
|
+
}
|
|
3616
|
+
const bytes = Buffer.byteLength(result.stdout) + Buffer.byteLength(result.stderr);
|
|
3617
|
+
return bytes > maximumOutputBytes ? { ...result, outputLimitExceeded: true } : result;
|
|
3618
|
+
}
|
|
3619
|
+
function boundedLogs(result, maximum) {
|
|
3620
|
+
const stdout = Buffer.from(result.stdout);
|
|
3621
|
+
const stderr = Buffer.from(result.stderr);
|
|
3622
|
+
const first = stdout.subarray(0, maximum);
|
|
3623
|
+
return {
|
|
3624
|
+
stdout: first.toString("utf8"),
|
|
3625
|
+
stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
function digestPolicy(policy) {
|
|
3629
|
+
return digestJson({
|
|
3630
|
+
policyId: policy.policyId,
|
|
3631
|
+
recipes: policy.recipes.map((recipe2) => normalizedRecipe(recipe2)),
|
|
3632
|
+
testPathPrefixes: [...policy.testPathPrefixes].sort(),
|
|
3633
|
+
testPathSuffixes: [...policy.testPathSuffixes].sort(),
|
|
3634
|
+
maximumChangedTests: policy.maximumChangedTests,
|
|
3635
|
+
maximumPatchBytes: policy.maximumPatchBytes,
|
|
3636
|
+
maximumFiles: policy.maximumFiles,
|
|
3637
|
+
maximumBytes: policy.maximumBytes
|
|
3638
|
+
});
|
|
3639
|
+
}
|
|
3640
|
+
function digestRecipe(recipe2) {
|
|
3641
|
+
return digestJson(normalizedRecipe(recipe2));
|
|
3642
|
+
}
|
|
3643
|
+
function normalizedRecipe(recipe2) {
|
|
3644
|
+
return {
|
|
3645
|
+
id: recipe2.id,
|
|
3646
|
+
image: recipe2.image,
|
|
3647
|
+
command: [...recipe2.command],
|
|
3648
|
+
timeoutMs: recipe2.timeoutMs,
|
|
3649
|
+
maxOutputBytes: recipe2.maxOutputBytes,
|
|
3650
|
+
cpus: recipe2.cpus ?? 1,
|
|
3651
|
+
memory: recipe2.memory ?? "1g",
|
|
3652
|
+
pids: recipe2.pids ?? 256,
|
|
3653
|
+
tmpfsBytes: recipe2.tmpfsBytes ?? 64 * 1024 * 1024,
|
|
3654
|
+
expectedArtifacts: [...recipe2.expectedArtifacts ?? []].sort((left, right) => left.id.localeCompare(right.id)).map((artifact) => ({ id: artifact.id, path: artifact.path, maximumBytes: artifact.maximumBytes }))
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
function digestJson(value) {
|
|
3658
|
+
return digestBytes(JSON.stringify(value));
|
|
3659
|
+
}
|
|
3660
|
+
function digestBytes(value) {
|
|
3661
|
+
return `sha256:${(0, import_crypto2.createHash)("sha256").update(value).digest("hex")}`;
|
|
3662
|
+
}
|
|
3663
|
+
function integer(value, minimum, maximum) {
|
|
3664
|
+
return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
|
|
3665
|
+
}
|
|
3666
|
+
async function prepareRuntimeCheckpoint(input) {
|
|
3667
|
+
const patch2 = await input.workspace.patch(256 * 1024);
|
|
3668
|
+
let verification = null;
|
|
3669
|
+
let review = null;
|
|
3670
|
+
let note = patch2 ? "Candidate remains untrusted" : "Checkpoint has no source changes";
|
|
3671
|
+
if (patch2 && input.role === "coding") {
|
|
3672
|
+
try {
|
|
3673
|
+
const evidence = await verifyCodeCandidate({
|
|
3674
|
+
verificationId: `verify-${input.sessionId.slice("csess_".length)}`,
|
|
3675
|
+
trustedBaseDir: input.workspace.baselineDir,
|
|
3676
|
+
trustedBaseCommitSha: input.baseCommitSha,
|
|
3677
|
+
trustedBaseDigest: input.trustedBaseDigest,
|
|
3678
|
+
candidatePatch: patch2,
|
|
3679
|
+
policy: {
|
|
3680
|
+
policyId: "code.runtime",
|
|
3681
|
+
recipes: input.recipes,
|
|
3682
|
+
maximumFiles: 1e4,
|
|
3683
|
+
maximumBytes: 16 * 1024 * 1024
|
|
3684
|
+
},
|
|
3685
|
+
recipeExecutor: input.recipeExecutor
|
|
3686
|
+
});
|
|
3687
|
+
if (evidence.receipt.outcome === "passed") {
|
|
3688
|
+
verification = evidence.receipt;
|
|
3689
|
+
review = await input.review(patch2, verification);
|
|
3690
|
+
note = review.verdict === "approved" ? "Clean verification and independent review passed" : "Clean verification passed; independent review rejected the candidate";
|
|
3691
|
+
} else {
|
|
3692
|
+
note = `Clean verification failed: ${evidence.receipt.recipes.filter((recipe2) => recipe2.status !== "passed").map((recipe2) => `${recipe2.recipeId}=${recipe2.status}`).join(", ")}`;
|
|
3693
|
+
}
|
|
3694
|
+
} catch (cause) {
|
|
3695
|
+
note = `Candidate verification or review failed closed: ${message(cause)}`;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
const reviewed = verification && review?.verdict === "approved";
|
|
3699
|
+
const checkpoint = await createCodeWorkspaceCheckpoint({
|
|
3700
|
+
workspace: input.workspace,
|
|
3701
|
+
baseCommitSha: input.baseCommitSha,
|
|
3702
|
+
state: {
|
|
3703
|
+
planCursor: null,
|
|
3704
|
+
conversationRefs: input.conversationRefs,
|
|
3705
|
+
planningInputDigest: input.planningInputDigest,
|
|
3706
|
+
buildPolicyDigest: verification?.policyDigest ?? input.fallbackPolicyDigest,
|
|
3707
|
+
dependencyLayerDigest: null,
|
|
3708
|
+
verificationDigest: verification?.receiptDigest ?? null,
|
|
3709
|
+
reviewDigest: reviewed && review ? review.reviewDigest : null,
|
|
3710
|
+
completedEffects: [],
|
|
3711
|
+
unresolvedApprovals: [],
|
|
3712
|
+
trustStatus: reviewed ? "reviewed" : verification ? "verified" : "candidate_untrusted"
|
|
3713
|
+
}
|
|
3714
|
+
});
|
|
3715
|
+
return { checkpoint, verification, review, note };
|
|
3716
|
+
}
|
|
3717
|
+
var message = (value) => (value instanceof Error ? value.message : String(value)).slice(0, 500);
|
|
3718
|
+
var CodeRuntimeCheckpointManager = class {
|
|
3719
|
+
constructor(options) {
|
|
3720
|
+
this.options = options;
|
|
3721
|
+
}
|
|
3722
|
+
options;
|
|
3723
|
+
#pending = /* @__PURE__ */ new Map();
|
|
3724
|
+
async prepare(command, active) {
|
|
3725
|
+
active.abort.abort("checkpoint_stop");
|
|
3726
|
+
await active.done;
|
|
3727
|
+
const prepared = await prepareRuntimeCheckpoint({
|
|
3728
|
+
sessionId: command.sessionId,
|
|
3729
|
+
role: active.role,
|
|
3730
|
+
workspace: active.workspace,
|
|
3731
|
+
baseCommitSha: active.baseCommitSha,
|
|
3732
|
+
trustedBaseDigest: active.trustedBaseDigest,
|
|
3733
|
+
planningInputDigest: active.planningInputDigest,
|
|
3734
|
+
conversationRefs: active.conversationRefs,
|
|
3735
|
+
fallbackPolicyDigest: this.options.fallbackPolicyDigest,
|
|
3736
|
+
recipes: this.options.recipes,
|
|
3737
|
+
recipeExecutor: this.options.recipeExecutor,
|
|
3738
|
+
review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
|
|
3739
|
+
});
|
|
3740
|
+
if (prepared.review?.verdict === "approved" && prepared.verification) {
|
|
3741
|
+
this.#pending.set(command.commandId, { verification: prepared.verification, refs: active.conversationRefs });
|
|
3742
|
+
}
|
|
3743
|
+
await this.options.event(command, { type: "message", actor: "system", body: prepared.note }, active.conversationRefs).catch(() => void 0);
|
|
3744
|
+
await active.workspace.cleanup();
|
|
3745
|
+
await this.options.event(command, { type: "status", status: "checkpointed" }, active.conversationRefs).catch(() => void 0);
|
|
3746
|
+
return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "Pi stopped at a portable checkpoint" };
|
|
3747
|
+
}
|
|
3748
|
+
async acknowledged(command, result) {
|
|
3749
|
+
if (command.kind !== "checkpoint_stop" || result.status !== "checkpointed") return false;
|
|
3750
|
+
const pending = this.#pending.get(command.commandId);
|
|
3751
|
+
if (!pending) return true;
|
|
3752
|
+
const checkpointId = `cpoint_${command.commandId.slice("ccmd_".length)}`;
|
|
3753
|
+
const candidate = await this.options.control.submitCandidate(command.sessionId, checkpointId, pending.verification);
|
|
3754
|
+
await this.options.event(command, {
|
|
3755
|
+
type: "message",
|
|
3756
|
+
actor: "system",
|
|
3757
|
+
body: `Candidate ${candidate.candidateId} is ready for exact owner publication approval`
|
|
3758
|
+
}, pending.refs);
|
|
3759
|
+
this.#pending.delete(command.commandId);
|
|
3760
|
+
return true;
|
|
3761
|
+
}
|
|
3762
|
+
};
|
|
3763
|
+
var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
|
|
3764
|
+
var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
3765
|
+
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os2.tmpdir)()) {
|
|
3766
|
+
if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
|
|
3767
|
+
const root = await (0, import_promises7.mkdtemp)((0, import_path6.join)(tempRoot, "odla-code-source-"));
|
|
3768
|
+
const sourceDir = (0, import_path6.join)(root, "source");
|
|
3769
|
+
await (0, import_promises7.mkdir)(sourceDir);
|
|
3770
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3771
|
+
let bytes = 0;
|
|
3772
|
+
try {
|
|
3773
|
+
for (const file of snapshot.files) {
|
|
3774
|
+
validatePath(file.path);
|
|
3775
|
+
if (seen.has(file.path)) throw new TypeError("Code source repeats a path");
|
|
3776
|
+
seen.add(file.path);
|
|
3777
|
+
bytes += Buffer.byteLength(file.path) + Buffer.byteLength(file.content);
|
|
3778
|
+
if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
|
|
3779
|
+
const target = (0, import_path6.resolve)(sourceDir, file.path);
|
|
3780
|
+
if (!target.startsWith(`${(0, import_path6.resolve)(sourceDir)}${import_path6.sep}`)) throw new TypeError("Code source path escapes its root");
|
|
3781
|
+
await (0, import_promises7.mkdir)((0, import_path6.dirname)(target), { recursive: true });
|
|
3782
|
+
await (0, import_promises7.writeFile)(target, file.content, { flag: "wx", mode: 420 });
|
|
3783
|
+
}
|
|
3784
|
+
return { sourceDir, cleanup: () => (0, import_promises7.rm)(root, { recursive: true, force: true }) };
|
|
3785
|
+
} catch (cause) {
|
|
3786
|
+
await (0, import_promises7.rm)(root, { recursive: true, force: true });
|
|
3787
|
+
throw cause;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
function validatePath(path) {
|
|
3791
|
+
const parts = path.split("/");
|
|
3792
|
+
if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED2.has(part) || SECRET2.test(part))) {
|
|
3793
|
+
throw new TypeError("Code source contains an unsafe path");
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
var DESTINATIONS = "code-workspaces.v1";
|
|
3797
|
+
var READ = descriptor("sandbox.read", "scoped_data_read", {
|
|
3798
|
+
workspace: "destination",
|
|
3799
|
+
authority: "authority",
|
|
3800
|
+
path: "selector",
|
|
3801
|
+
startLine: "selector",
|
|
3802
|
+
endLine: "selector"
|
|
3803
|
+
});
|
|
3804
|
+
var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
|
|
3805
|
+
workspace: "destination",
|
|
3806
|
+
authority: "authority",
|
|
3807
|
+
patch: "payload"
|
|
3808
|
+
});
|
|
3809
|
+
var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
|
|
3810
|
+
workspace: "destination",
|
|
3811
|
+
authority: "authority",
|
|
3812
|
+
recipeId: "selector",
|
|
3813
|
+
sourceDigest: "payload"
|
|
3814
|
+
});
|
|
3815
|
+
function createCodePolicyGate(options) {
|
|
3816
|
+
return {
|
|
3817
|
+
read: async (input) => {
|
|
3818
|
+
const base = await environment(input, options, "sandbox.read");
|
|
3819
|
+
const conversions = await conversionRegistry([
|
|
3820
|
+
await registeredPolicy("code.path.v1", "code.paths.v1", input.paths),
|
|
3821
|
+
await conversionPolicy("code.line.v1", { kind: "integer", minimum: 1, maximum: 1e6 })
|
|
3822
|
+
], { "code.paths.v1": input.paths });
|
|
3823
|
+
const path = await conversions.operations.registeredId(unsafe(base, input.path, "path"), "code.path.v1");
|
|
3824
|
+
const start = await conversions.operations.integer(unsafe(base, input.startLine, "start"), "code.line.v1");
|
|
3825
|
+
const end = await conversions.operations.integer(unsafe(base, input.endLine, "end"), "code.line.v1");
|
|
3826
|
+
if (end.value < start.value) return false;
|
|
3827
|
+
return authorize(input, options, base, READ, {
|
|
3828
|
+
...base.fixedArgs,
|
|
3829
|
+
path: { role: "selector", value: path },
|
|
3830
|
+
startLine: { role: "selector", value: start },
|
|
3831
|
+
endLine: { role: "selector", value: end }
|
|
3832
|
+
}, [path, start, end]);
|
|
3833
|
+
},
|
|
3834
|
+
patch: async (input) => {
|
|
3835
|
+
const base = await environment(input, options, "sandbox.apply_patch");
|
|
3836
|
+
const patch2 = unsafe(base, input.patch, "patch");
|
|
3837
|
+
return authorize(input, options, base, PATCH, {
|
|
3838
|
+
...base.fixedArgs,
|
|
3839
|
+
patch: { role: "payload", value: patch2 }
|
|
3840
|
+
}, []);
|
|
3841
|
+
},
|
|
3842
|
+
recipe: async (input) => {
|
|
3843
|
+
const base = await environment(input, options, "sandbox.run_recipe");
|
|
3844
|
+
const conversions = await conversionRegistry([
|
|
3845
|
+
await registeredPolicy("code.recipe.v1", "code.recipes.v1", input.recipeIds)
|
|
3846
|
+
], { "code.recipes.v1": input.recipeIds });
|
|
3847
|
+
const recipe2 = await conversions.operations.registeredId(unsafe(base, input.recipeId, "recipe"), "code.recipe.v1");
|
|
3848
|
+
const source = unsafe(base, input.sourceDigest, "source");
|
|
3849
|
+
return authorize(input, options, base, RECIPE, {
|
|
3850
|
+
...base.fixedArgs,
|
|
3851
|
+
recipeId: { role: "selector", value: recipe2 },
|
|
3852
|
+
sourceDigest: { role: "payload", value: source }
|
|
3853
|
+
}, [recipe2]);
|
|
3854
|
+
}
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
function descriptor(name, effect, argumentRoles) {
|
|
3858
|
+
return { name, version: 1, effect, inputSchema: { type: "object" }, argumentRoles, policyId: `odla.code.${name}.v1` };
|
|
3859
|
+
}
|
|
3860
|
+
async function conversionPolicy(id, output) {
|
|
3861
|
+
const definition = {
|
|
3862
|
+
conversionId: id,
|
|
3863
|
+
version: 1,
|
|
3864
|
+
output,
|
|
3865
|
+
maximumSourceBytes: 1e6,
|
|
3866
|
+
maximumOutputsPerArtifact: 4,
|
|
3867
|
+
presentation: "json_scalar"
|
|
3868
|
+
};
|
|
3869
|
+
return { ...definition, digest: await conversionPolicyDigest(definition) };
|
|
3870
|
+
}
|
|
3871
|
+
async function registeredPolicy(id, registryId, values) {
|
|
3872
|
+
const mapping = Object.fromEntries(values.map((value) => [value, value]));
|
|
3873
|
+
return conversionPolicy(id, {
|
|
3874
|
+
kind: "registered_id",
|
|
3875
|
+
registryId,
|
|
3876
|
+
registryDigest: await registeredIdRegistryDigest(mapping)
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
async function conversionRegistry(policies, values) {
|
|
3880
|
+
const registeredIds = Object.fromEntries(await Promise.all(Object.entries(values).map(async ([id, entries]) => {
|
|
3881
|
+
const mapping = Object.fromEntries(entries.map((value) => [value, value]));
|
|
3882
|
+
return [id, { values: mapping, digest: await registeredIdRegistryDigest(mapping) }];
|
|
3883
|
+
})));
|
|
3884
|
+
return createConversionRegistry({ policies, registeredIds });
|
|
3885
|
+
}
|
|
3886
|
+
async function environment(input, options, tool) {
|
|
3887
|
+
const ingress = createCamelIngress([
|
|
3888
|
+
{ id: "workspace", value: input.workspaceId, readers: input.readers },
|
|
3889
|
+
{ id: "authority", value: `lease:${input.lease.leaseId}`, readers: input.readers },
|
|
3890
|
+
{ id: "reader", value: options.readerId, readers: input.readers }
|
|
3891
|
+
]);
|
|
3892
|
+
const digest2 = await destinationRegistryDigest([input.workspaceId]);
|
|
3893
|
+
const approvals = ["irreversible_mutation", "external_send", "financial", "secret_read"];
|
|
3894
|
+
if (options.recipeAuthorization === "exact_approval") approvals.push("code_execution");
|
|
3895
|
+
const policy = createEffectPolicy({
|
|
3896
|
+
destinationRegistries: { [DESTINATIONS]: { digest: digest2, values: [input.workspaceId] } },
|
|
3897
|
+
approvalEffects: approvals
|
|
3898
|
+
});
|
|
3899
|
+
const fixedArgs = {
|
|
3900
|
+
workspace: { role: "destination", value: ingress.control("workspace"), registryId: DESTINATIONS, registryDigest: digest2 },
|
|
3901
|
+
authority: { role: "authority", value: ingress.control("authority") }
|
|
3902
|
+
};
|
|
3903
|
+
return { ingress, policy, fixedArgs, reader: ingress.control("reader"), runId: `${input.request.requestId}:${tool}` };
|
|
3904
|
+
}
|
|
3905
|
+
function unsafe(base, value, field) {
|
|
3906
|
+
return base.ingress.quarantinedOutput(value, {
|
|
3907
|
+
readers: base.reader.label.readers,
|
|
3908
|
+
runId: `${base.runId}:${field}`
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
async function authorize(input, options, base, tool, args, controlDependencies) {
|
|
3912
|
+
const policy = await base.policy.evaluate({
|
|
3913
|
+
planId: input.lease.task.taskId,
|
|
3914
|
+
tool,
|
|
3915
|
+
args,
|
|
3916
|
+
controlDependencies,
|
|
3917
|
+
intendedReaderIds: [base.reader]
|
|
3918
|
+
});
|
|
3919
|
+
let approvalConsumed = false;
|
|
3920
|
+
if (policy.outcome === "require_approval" && options.consumeApproval) {
|
|
3921
|
+
approvalConsumed = await options.consumeApproval(decision(input, policy, false, tool.name, policy.actionDigest));
|
|
3922
|
+
}
|
|
3923
|
+
await options.onDecision?.(decision(input, policy, approvalConsumed, tool.name));
|
|
3924
|
+
return policy.outcome === "allow" || approvalConsumed;
|
|
3925
|
+
}
|
|
3926
|
+
function decision(input, policy, approvalConsumed, tool, actionDigest) {
|
|
3927
|
+
return {
|
|
3928
|
+
lease: input.lease,
|
|
3929
|
+
request: input.request,
|
|
3930
|
+
tool,
|
|
3931
|
+
policy,
|
|
3932
|
+
approvalConsumed,
|
|
3933
|
+
actionDigest: actionDigest ?? (policy.outcome === "require_approval" ? policy.actionDigest : "")
|
|
3934
|
+
};
|
|
3935
|
+
}
|
|
3936
|
+
function createCodeToolBroker(options) {
|
|
3937
|
+
validateOptions(options);
|
|
3938
|
+
const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
|
|
3939
|
+
const policy = createCodePolicyGate(options);
|
|
3940
|
+
let tail = Promise.resolve();
|
|
3941
|
+
return {
|
|
3942
|
+
execute(context, request) {
|
|
3943
|
+
const result = tail.then(() => route(context, request, options, recipes, policy));
|
|
3944
|
+
tail = result.then(() => void 0, () => void 0);
|
|
3945
|
+
return result;
|
|
3946
|
+
}
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
async function route(context, request, options, recipes, policy) {
|
|
3950
|
+
try {
|
|
3951
|
+
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
3952
|
+
if (request.tool === "sandbox.read") return await read(context, request, options, policy);
|
|
3953
|
+
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
|
|
3954
|
+
return await recipe(context, request, options, recipes, policy);
|
|
3955
|
+
} catch (reason) {
|
|
3956
|
+
return response(request, false, reason instanceof TypeError ? reason.message : "tool failed closed");
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
async function read(context, request, options, policy) {
|
|
3960
|
+
exactKeys(request.input, ["path", "startLine", "endLine"]);
|
|
3961
|
+
const path = stringField(request.input, "path");
|
|
3962
|
+
const startLine = optionalInteger(request.input.startLine) ?? 1;
|
|
3963
|
+
const endLine = optionalInteger(request.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
|
|
3964
|
+
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
3965
|
+
throw new TypeError("requested line range exceeds its bound");
|
|
3966
|
+
}
|
|
3967
|
+
const paths = await registeredFiles(context.workspaceDir, 2e4);
|
|
3968
|
+
const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
|
|
3969
|
+
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3970
|
+
const target = resolveCodePath(context.workspaceDir, path);
|
|
3971
|
+
const info = await (0, import_promises8.stat)(target);
|
|
3972
|
+
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
3973
|
+
throw new TypeError("file is not a bounded regular source file");
|
|
3974
|
+
}
|
|
3975
|
+
const source = await (0, import_promises8.readFile)(target);
|
|
3976
|
+
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
3977
|
+
const lines = source.toString("utf8").split("\n");
|
|
3978
|
+
const content = lines.slice(startLine - 1, endLine).join("\n");
|
|
3979
|
+
if (Buffer.byteLength(content) > (options.maxReadBytes ?? 128 * 1024)) {
|
|
3980
|
+
throw new TypeError("read result exceeds its byte bound");
|
|
3981
|
+
}
|
|
3982
|
+
return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
3983
|
+
}
|
|
3984
|
+
async function patch(context, request, options, policy) {
|
|
3985
|
+
exactKeys(request.input, ["patch"]);
|
|
3986
|
+
const value = stringField(request.input, "patch");
|
|
3987
|
+
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
3988
|
+
const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
|
|
3989
|
+
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
3990
|
+
await applyCodePatch(context.workspaceDir, value, paths);
|
|
3991
|
+
return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
3992
|
+
}
|
|
3993
|
+
async function recipe(context, request, options, recipes, policy) {
|
|
3994
|
+
exactKeys(request.input, ["recipeId"]);
|
|
3995
|
+
const recipeId = stringField(request.input, "recipeId");
|
|
3996
|
+
const digestLimits = {
|
|
3997
|
+
maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
|
|
3998
|
+
maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
|
|
3999
|
+
};
|
|
4000
|
+
const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
|
|
4001
|
+
const allowed = await policy.recipe(policyContext(context, request, options, {
|
|
4002
|
+
recipeIds: [...recipes.keys()].sort(),
|
|
4003
|
+
recipeId,
|
|
4004
|
+
sourceDigest
|
|
4005
|
+
}));
|
|
4006
|
+
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
4007
|
+
const selected = recipes.get(recipeId);
|
|
4008
|
+
if (!selected) return response(request, false, "build recipe is not registered");
|
|
4009
|
+
const staged = await stageWorkspace(context.workspaceDir, {
|
|
4010
|
+
maxFiles: digestLimits.maxFiles,
|
|
4011
|
+
maxBytes: digestLimits.maxBytes
|
|
4012
|
+
});
|
|
4013
|
+
try {
|
|
4014
|
+
if (await digestStagedWorkspace(staged.workspaceDir, digestLimits) !== sourceDigest) {
|
|
4015
|
+
throw new TypeError("workspace changed after recipe authorization");
|
|
4016
|
+
}
|
|
4017
|
+
const result = await options.recipeExecutor.run({
|
|
4018
|
+
workspaceDir: staged.workspaceDir,
|
|
4019
|
+
recipe: selected,
|
|
4020
|
+
signal: context.signal
|
|
4021
|
+
});
|
|
4022
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
4023
|
+
const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
|
|
4024
|
+
const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
|
|
4025
|
+
return response(request, ok, `Recipe ${recipeId} ${status}.${output ? `
|
|
4026
|
+
${output}` : ""}`, {
|
|
4027
|
+
recipeId,
|
|
4028
|
+
exitCode: result.exitCode,
|
|
4029
|
+
durationMs: result.durationMs,
|
|
4030
|
+
outputLimitExceeded: result.outputLimitExceeded,
|
|
4031
|
+
timedOut: result.timedOut
|
|
4032
|
+
});
|
|
4033
|
+
} finally {
|
|
4034
|
+
await staged.cleanup();
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
function policyContext(context, request, options, extra) {
|
|
4038
|
+
return {
|
|
4039
|
+
lease: context.lease,
|
|
4040
|
+
request,
|
|
4041
|
+
workspaceId: `workspace:${context.lease.task.attemptId}`,
|
|
4042
|
+
readers: { kind: "principals", principalIds: [options.readerId] },
|
|
4043
|
+
...extra
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
async function registeredFiles(root, limit) {
|
|
4047
|
+
const paths = [];
|
|
4048
|
+
const walk = async (directory) => {
|
|
4049
|
+
for (const entry of await (0, import_promises8.readdir)(directory, { withFileTypes: true })) {
|
|
4050
|
+
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
4051
|
+
const target = (0, import_path7.resolve)(directory, entry.name);
|
|
4052
|
+
if (entry.isDirectory()) await walk(target);
|
|
4053
|
+
else if (entry.isFile()) {
|
|
4054
|
+
const path = (0, import_path7.relative)(root, target).split("\\").join("/");
|
|
4055
|
+
try {
|
|
4056
|
+
validateRelativePath(path);
|
|
4057
|
+
} catch {
|
|
4058
|
+
continue;
|
|
4059
|
+
}
|
|
4060
|
+
paths.push(path);
|
|
4061
|
+
if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
};
|
|
4065
|
+
await walk((0, import_path7.resolve)(root));
|
|
4066
|
+
return paths.sort();
|
|
4067
|
+
}
|
|
4068
|
+
function validateOptions(options) {
|
|
4069
|
+
if (!options.readerId || !options.recipes.length || new Set(options.recipes.map((item) => item.id)).size !== options.recipes.length) {
|
|
4070
|
+
throw new TypeError("Code tool broker requires a reader and unique registered recipes");
|
|
4071
|
+
}
|
|
4072
|
+
for (const recipe2 of options.recipes) assertCodeBuildRecipe(recipe2);
|
|
4073
|
+
}
|
|
4074
|
+
function exactKeys(input, allowed) {
|
|
4075
|
+
if (Object.keys(input).some((key) => !allowed.includes(key))) throw new TypeError("tool input contains an unsupported field");
|
|
4076
|
+
}
|
|
4077
|
+
function stringField(input, name) {
|
|
4078
|
+
const value = input[name];
|
|
4079
|
+
if (typeof value !== "string" || !value) throw new TypeError(`${name} must be a non-empty string`);
|
|
4080
|
+
return value;
|
|
4081
|
+
}
|
|
4082
|
+
function optionalInteger(value) {
|
|
4083
|
+
if (value === void 0) return void 0;
|
|
4084
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
|
|
4085
|
+
return value;
|
|
4086
|
+
}
|
|
4087
|
+
function response(request, ok, content, details) {
|
|
4088
|
+
return { requestId: request.requestId, ok, content, ...details ? { details } : {} };
|
|
4089
|
+
}
|
|
4090
|
+
function codeCommandMetadata(payload, resume) {
|
|
4091
|
+
const trusted = record22(payload.trustedBase);
|
|
4092
|
+
const role = payload.role;
|
|
4093
|
+
const title = payload.title;
|
|
4094
|
+
const prompt = payload.prompt;
|
|
4095
|
+
if (role !== "coding" && role !== "review" || typeof title !== "string" || typeof prompt !== "string") {
|
|
4096
|
+
throw new TypeError(`invalid Code ${resume ? "resume" : "start"} metadata`);
|
|
4097
|
+
}
|
|
4098
|
+
const planning = trusted?.planningInputDigest;
|
|
4099
|
+
const attestation = trusted?.attestationDigest;
|
|
4100
|
+
return {
|
|
4101
|
+
role,
|
|
4102
|
+
title,
|
|
4103
|
+
prompt,
|
|
4104
|
+
planningInputDigest: typeof planning === "string" && /^sha256:[0-9a-f]{64}$/.test(planning) ? planning : null,
|
|
4105
|
+
attestationDigest: typeof attestation === "string" ? attestation : "resume"
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
function codeCheckpointPayload(payload) {
|
|
4109
|
+
const value = payload.checkpoint;
|
|
4110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("resume checkpoint is missing");
|
|
4111
|
+
return value;
|
|
4112
|
+
}
|
|
4113
|
+
function fakeCodeLease(command, metadata2) {
|
|
4114
|
+
return {
|
|
4115
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
4116
|
+
leaseId: `code:${command.commandId}`,
|
|
4117
|
+
generation: command.bindingGeneration,
|
|
4118
|
+
expiresAt: Date.now() + 24 * 60 * 6e4,
|
|
4119
|
+
task: {
|
|
4120
|
+
taskId: command.sessionId,
|
|
4121
|
+
attemptId: command.instanceId,
|
|
4122
|
+
title: metadata2.title,
|
|
4123
|
+
prompt: metadata2.prompt,
|
|
4124
|
+
workspace: command.appId,
|
|
4125
|
+
aiRoute: metadata2.role,
|
|
4126
|
+
policy: {
|
|
4127
|
+
network: "none",
|
|
4128
|
+
timeoutMs: 30 * 6e4,
|
|
4129
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
4130
|
+
maxPatchBytes: 256 * 1024
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
var record22 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4136
|
+
var CodePiRuntimeEngine = class {
|
|
4137
|
+
constructor(options) {
|
|
4138
|
+
this.options = options;
|
|
4139
|
+
this.#run = options.runAttempt ?? runContainerAttempt;
|
|
4140
|
+
this.#buildPolicyDigest = digest(JSON.stringify(options.recipes));
|
|
4141
|
+
this.#checkpoints = new CodeRuntimeCheckpointManager({
|
|
4142
|
+
control: options.control,
|
|
4143
|
+
recipes: options.recipes,
|
|
4144
|
+
recipeExecutor: options.recipeExecutor ?? createContainerRecipeExecutor(options.engine),
|
|
4145
|
+
fallbackPolicyDigest: this.#buildPolicyDigest,
|
|
4146
|
+
event: (command, event, refs) => this.#event(command, event, refs)
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
options;
|
|
4150
|
+
#active = /* @__PURE__ */ new Map();
|
|
4151
|
+
#run;
|
|
4152
|
+
#buildPolicyDigest;
|
|
4153
|
+
#checkpoints;
|
|
4154
|
+
execute(command) {
|
|
4155
|
+
if (command.kind === "checkpoint_stop") return this.#checkpoint(command);
|
|
4156
|
+
if (command.kind === "prompt") return this.#prompt(command);
|
|
4157
|
+
return this.#start(command, command.kind === "resume");
|
|
4158
|
+
}
|
|
4159
|
+
async acknowledged(command, result) {
|
|
4160
|
+
if (await this.#checkpoints.acknowledged(command, result)) return;
|
|
4161
|
+
const active = this.#active.get(command.sessionId);
|
|
4162
|
+
if (!active || result.status !== "running") return;
|
|
4163
|
+
active.acknowledged = true;
|
|
4164
|
+
if (active.failure) await this.options.control.reportSessionFailure(command.sessionId, active.failure).catch(() => void 0);
|
|
4165
|
+
}
|
|
4166
|
+
async close() {
|
|
4167
|
+
const sessions = [...this.#active.values()];
|
|
4168
|
+
for (const session of sessions) session.abort.abort("runtime_shutdown");
|
|
4169
|
+
await Promise.allSettled(sessions.map((session) => session.done));
|
|
4170
|
+
await Promise.allSettled(sessions.map((session) => session.workspace.cleanup()));
|
|
4171
|
+
this.#active.clear();
|
|
4172
|
+
}
|
|
4173
|
+
async #start(command, resume) {
|
|
4174
|
+
if (this.#active.has(command.sessionId)) throw new TypeError("Code session is already active on this runtime");
|
|
4175
|
+
const source = await this.options.control.source(command.sessionId);
|
|
4176
|
+
const materialized = await materializeCodeRuntimeSource(source);
|
|
4177
|
+
let workspace;
|
|
4178
|
+
try {
|
|
4179
|
+
workspace = resume ? (await restoreCodeWorkspaceCheckpoint({
|
|
4180
|
+
trustedBaseDir: materialized.sourceDir,
|
|
4181
|
+
trustedBaseCommitSha: source.commitSha,
|
|
4182
|
+
checkpoint: codeCheckpointPayload(command.payload)
|
|
4183
|
+
})).workspace : await stageWorkspace(materialized.sourceDir);
|
|
4184
|
+
} finally {
|
|
4185
|
+
await materialized.cleanup();
|
|
4186
|
+
}
|
|
4187
|
+
const metadata2 = codeCommandMetadata(command.payload, resume);
|
|
4188
|
+
const abort = new AbortController();
|
|
4189
|
+
const conversationRefs = [];
|
|
4190
|
+
const active = {
|
|
4191
|
+
workspace,
|
|
4192
|
+
abort,
|
|
4193
|
+
conversationRefs,
|
|
4194
|
+
acknowledged: false,
|
|
4195
|
+
role: metadata2.role,
|
|
4196
|
+
title: metadata2.title,
|
|
4197
|
+
baseCommitSha: source.commitSha,
|
|
4198
|
+
trustedBaseDigest: await digestStagedWorkspace(workspace.baselineDir, {
|
|
4199
|
+
maxFiles: 1e4,
|
|
4200
|
+
maxBytes: 16 * 1024 * 1024
|
|
4201
|
+
}),
|
|
4202
|
+
planningInputDigest: metadata2.planningInputDigest ?? digest(
|
|
4203
|
+
JSON.stringify({ attestation: metadata2.attestationDigest, prompt: metadata2.prompt, tree: source.treeDigest })
|
|
4204
|
+
),
|
|
4205
|
+
done: Promise.resolve(null)
|
|
4206
|
+
};
|
|
4207
|
+
this.#active.set(command.sessionId, active);
|
|
4208
|
+
active.done = this.#runAttempt(command, metadata2, active).catch(async (cause) => {
|
|
4209
|
+
await this.#event(command, { type: "message", actor: "system", body: `Pi failed: ${message2(cause)}` }, conversationRefs).catch(() => void 0);
|
|
4210
|
+
await this.#event(command, { type: "status", status: "failed" }, conversationRefs).catch(() => void 0);
|
|
4211
|
+
await this.#failure(command, active, message2(cause));
|
|
4212
|
+
return null;
|
|
4213
|
+
});
|
|
4214
|
+
return { status: "running", message: resume ? "Pi resumed from a portable checkpoint" : "Pi started" };
|
|
4215
|
+
}
|
|
4216
|
+
async #prompt(command) {
|
|
4217
|
+
const active = this.#active.get(command.sessionId);
|
|
4218
|
+
const prompt = command.payload.prompt;
|
|
4219
|
+
if (!active || typeof prompt !== "string" || !prompt.trim() || prompt.length > 2e4) {
|
|
4220
|
+
throw new TypeError("prompt requires an active Code session and bounded text");
|
|
4221
|
+
}
|
|
4222
|
+
await active.done;
|
|
4223
|
+
active.abort = new AbortController();
|
|
4224
|
+
active.acknowledged = false;
|
|
4225
|
+
active.failure = void 0;
|
|
4226
|
+
active.done = this.#runAttempt(command, {
|
|
4227
|
+
role: active.role,
|
|
4228
|
+
title: active.title,
|
|
4229
|
+
prompt,
|
|
4230
|
+
planningInputDigest: active.planningInputDigest,
|
|
4231
|
+
attestationDigest: "follow-up"
|
|
4232
|
+
}, active).catch(async (cause) => {
|
|
4233
|
+
await this.#event(
|
|
4234
|
+
command,
|
|
4235
|
+
{ type: "message", actor: "system", body: `Pi failed: ${message2(cause)}` },
|
|
4236
|
+
active.conversationRefs
|
|
4237
|
+
).catch(() => void 0);
|
|
4238
|
+
await this.#event(command, { type: "status", status: "failed" }, active.conversationRefs).catch(() => void 0);
|
|
4239
|
+
await this.#failure(command, active, message2(cause));
|
|
4240
|
+
return null;
|
|
4241
|
+
});
|
|
4242
|
+
return { status: "running", message: "Pi accepted the owner prompt" };
|
|
4243
|
+
}
|
|
4244
|
+
async #runAttempt(command, metadata2, active) {
|
|
4245
|
+
const lease = fakeCodeLease(command, metadata2);
|
|
4246
|
+
const broker = this.#toolBroker(lease, metadata2.role);
|
|
4247
|
+
const startedAt = Date.now();
|
|
4248
|
+
let completionSeen = false;
|
|
4249
|
+
const result = await this.#run({
|
|
4250
|
+
engine: this.options.engine,
|
|
4251
|
+
image: this.options.image,
|
|
4252
|
+
workspaceDir: active.workspace.workspaceDir,
|
|
4253
|
+
workspaceAccess: "none",
|
|
4254
|
+
task: lease.task,
|
|
4255
|
+
limits: this.options.limits,
|
|
4256
|
+
signal: active.abort.signal,
|
|
4257
|
+
onStderr: (text) => this.#event(command, {
|
|
4258
|
+
type: "message",
|
|
4259
|
+
actor: "system",
|
|
4260
|
+
body: text.slice(0, 4e3)
|
|
4261
|
+
}, active.conversationRefs),
|
|
4262
|
+
onMessage: async (output) => {
|
|
4263
|
+
if (output.type === "inference.request") {
|
|
4264
|
+
const inferenceStarted = Date.now();
|
|
4265
|
+
const response2 = await this.options.control.infer(command.sessionId, {
|
|
4266
|
+
requestId: output.requestId,
|
|
4267
|
+
call: output.call
|
|
4268
|
+
});
|
|
4269
|
+
await this.#event(command, {
|
|
4270
|
+
type: "usage",
|
|
4271
|
+
provider: response2.receipt.provider,
|
|
4272
|
+
model: response2.receipt.model,
|
|
4273
|
+
inputTokens: response2.receipt.inputTokens,
|
|
4274
|
+
outputTokens: response2.receipt.outputTokens,
|
|
4275
|
+
durationMs: Date.now() - inferenceStarted
|
|
4276
|
+
}, active.conversationRefs).catch(() => void 0);
|
|
4277
|
+
return {
|
|
4278
|
+
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
4279
|
+
type: "inference.response",
|
|
4280
|
+
requestId: output.requestId,
|
|
4281
|
+
response: response2.response
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
if (output.type === "tool.request") {
|
|
4285
|
+
const toolStarted = Date.now();
|
|
4286
|
+
await this.#event(
|
|
4287
|
+
command,
|
|
4288
|
+
{ type: "tool", phase: "started", tool: output.tool },
|
|
4289
|
+
active.conversationRefs
|
|
4290
|
+
).catch(() => void 0);
|
|
4291
|
+
const response2 = await broker.execute({
|
|
4292
|
+
lease,
|
|
4293
|
+
workspaceDir: active.workspace.workspaceDir,
|
|
4294
|
+
signal: active.abort.signal
|
|
4295
|
+
}, output);
|
|
4296
|
+
await this.#event(command, {
|
|
4297
|
+
type: "tool",
|
|
4298
|
+
phase: "completed",
|
|
4299
|
+
tool: output.tool,
|
|
4300
|
+
ok: response2.ok,
|
|
4301
|
+
durationMs: Date.now() - toolStarted
|
|
4302
|
+
}, active.conversationRefs).catch(() => void 0);
|
|
4303
|
+
return { protocolVersion: HARNESS_PROTOCOL_VERSION, type: "tool.response", ...response2 };
|
|
4304
|
+
}
|
|
4305
|
+
if (output.type === "event") {
|
|
4306
|
+
const payload = object2(output.payload);
|
|
4307
|
+
if (output.kind === "pi.started") {
|
|
4308
|
+
await this.#event(command, { type: "status", status: "running" }, active.conversationRefs);
|
|
4309
|
+
} else if (output.kind === "pi.thinking" && payload?.available === true && Number.isSafeInteger(payload.durationMs) && Number(payload.durationMs) >= 0) {
|
|
4310
|
+
await this.#event(command, {
|
|
4311
|
+
type: "thinking",
|
|
4312
|
+
available: true,
|
|
4313
|
+
durationMs: Math.min(Number(payload.durationMs), 864e5)
|
|
4314
|
+
}, active.conversationRefs);
|
|
4315
|
+
} else {
|
|
4316
|
+
await this.#event(command, {
|
|
4317
|
+
type: "message",
|
|
4318
|
+
actor: "system",
|
|
4319
|
+
body: `${output.kind}${output.payload === void 0 ? "" : ` ${json(output.payload)}`}`
|
|
4320
|
+
}, active.conversationRefs);
|
|
4321
|
+
}
|
|
4322
|
+
} else if (output.type === "attempt.complete") {
|
|
4323
|
+
completionSeen = true;
|
|
4324
|
+
const body = resultText(output.result) ?? `Pi ${output.status}.`;
|
|
4325
|
+
await this.#event(command, {
|
|
4326
|
+
type: "message",
|
|
4327
|
+
actor: output.status === "completed" ? "agent" : "system",
|
|
4328
|
+
body
|
|
4329
|
+
}, active.conversationRefs);
|
|
4330
|
+
await this.#event(command, {
|
|
4331
|
+
type: "status",
|
|
4332
|
+
status: output.status === "completed" ? "idle" : "failed",
|
|
4333
|
+
durationMs: Date.now() - startedAt
|
|
4334
|
+
}, active.conversationRefs);
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
});
|
|
4338
|
+
if (result.status === "failed" && result.stderr) {
|
|
4339
|
+
await this.#event(command, { type: "message", actor: "system", body: result.stderr.slice(0, 4e3) }, active.conversationRefs);
|
|
4340
|
+
}
|
|
4341
|
+
if (!completionSeen) await this.#event(command, {
|
|
4342
|
+
type: "status",
|
|
4343
|
+
status: result.status === "completed" ? "idle" : "failed",
|
|
4344
|
+
durationMs: Date.now() - startedAt
|
|
4345
|
+
}, active.conversationRefs).catch(() => void 0);
|
|
4346
|
+
if (result.status === "failed") {
|
|
4347
|
+
await this.#failure(command, active, result.stderr || "Pi container failed");
|
|
4348
|
+
}
|
|
4349
|
+
return result;
|
|
4350
|
+
}
|
|
4351
|
+
#toolBroker(lease, role) {
|
|
4352
|
+
const broker = createCodeToolBroker({
|
|
4353
|
+
recipes: this.options.recipes,
|
|
4354
|
+
recipeExecutor: createContainerRecipeExecutor(this.options.engine),
|
|
4355
|
+
recipeAuthorization: this.options.recipeAuthorization ?? "registered_recipe",
|
|
4356
|
+
readerId: `code-session:${lease.task.taskId}`
|
|
4357
|
+
});
|
|
4358
|
+
return role === "coding" ? broker : { execute: (context, request) => request.tool === "sandbox.read" ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
|
|
4359
|
+
}
|
|
4360
|
+
async #checkpoint(command) {
|
|
4361
|
+
const active = this.#active.get(command.sessionId);
|
|
4362
|
+
if (!active) throw new TypeError("Code session workspace is not active on this runtime");
|
|
4363
|
+
const result = await this.#checkpoints.prepare(command, active);
|
|
4364
|
+
this.#active.delete(command.sessionId);
|
|
4365
|
+
return result;
|
|
4366
|
+
}
|
|
4367
|
+
async #failure(command, active, value) {
|
|
4368
|
+
active.failure = value.slice(0, 2e3);
|
|
4369
|
+
if (active.acknowledged) {
|
|
4370
|
+
await this.options.control.reportSessionFailure(command.sessionId, active.failure).catch(() => void 0);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
async #event(command, event, refs) {
|
|
4374
|
+
const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
|
|
4375
|
+
refs.push(eventId);
|
|
4376
|
+
const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
|
|
4377
|
+
await this.options.control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
4378
|
+
}
|
|
4379
|
+
};
|
|
4380
|
+
var object2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4381
|
+
var digest = (value) => `sha256:${(0, import_crypto4.createHash)("sha256").update(value).digest("hex")}`;
|
|
4382
|
+
var message2 = (value) => value instanceof Error ? value.message : String(value);
|
|
4383
|
+
var json = (value) => {
|
|
4384
|
+
try {
|
|
4385
|
+
return JSON.stringify(value).slice(0, 1e4);
|
|
4386
|
+
} catch {
|
|
4387
|
+
return "[event]";
|
|
4388
|
+
}
|
|
4389
|
+
};
|
|
4390
|
+
function resultText(value) {
|
|
4391
|
+
const record32 = object2(value);
|
|
4392
|
+
return record32 && typeof record32.text === "string" ? record32.text.slice(0, 2e4) : null;
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
// src/help.ts
|
|
4396
|
+
var import_node_fs6 = require("fs");
|
|
4397
|
+
function cliVersion() {
|
|
4398
|
+
const pkg = JSON.parse((0, import_node_fs6.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
4399
|
+
return pkg.version ?? "unknown";
|
|
4400
|
+
}
|
|
4401
|
+
function printHelp() {
|
|
4402
|
+
console.log(`odla-ai
|
|
4403
|
+
|
|
4404
|
+
Usage:
|
|
4405
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
4406
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
4407
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
4408
|
+
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
4409
|
+
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
4410
|
+
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
4411
|
+
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
4412
|
+
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
4413
|
+
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
4414
|
+
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
4415
|
+
odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
4416
|
+
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
4417
|
+
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
4418
|
+
odla-ai capabilities [--json]
|
|
4419
|
+
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
4420
|
+
odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
4421
|
+
odla-ai admin ai models [--provider <id>] [--json]
|
|
4422
|
+
odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
4423
|
+
odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
|
|
4424
|
+
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
|
|
4425
|
+
odla-ai admin ai credentials [--json]
|
|
4426
|
+
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
4427
|
+
odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
4428
|
+
odla-ai admin ai audit [--limit <1-200>] [--json]
|
|
4429
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
|
|
4430
|
+
odla-ai security github disconnect --source <id> [--env dev] [--yes]
|
|
4431
|
+
odla-ai security plan [--env dev] [--json]
|
|
4432
|
+
odla-ai security sources [--env dev] [--json]
|
|
4433
|
+
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
4434
|
+
odla-ai security status <job-id> [--follow] [--json]
|
|
4435
|
+
odla-ai security report <job-id> [--json]
|
|
4436
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
4437
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
4438
|
+
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
4439
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
4440
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
4441
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
4442
|
+
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
4443
|
+
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
4444
|
+
odla-ai version
|
|
4445
|
+
|
|
4446
|
+
Commands:
|
|
4447
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
4448
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
4449
|
+
doctor Validate and summarize the project config without network calls.
|
|
4450
|
+
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
4451
|
+
app Archive (suspend, data retained), restore, export, or manage the
|
|
4452
|
+
co-owners of the app. Archiving takes every environment's data
|
|
4453
|
+
plane down until restored; permanent deletion has NO CLI \u2014 it
|
|
4454
|
+
requires a signed-in owner in Studio, and no machine or agent
|
|
4455
|
+
credential can purge. "app export" downloads a portable
|
|
4456
|
+
gzipped-JSONL snapshot of one environment's database (--fresh
|
|
4457
|
+
takes a new snapshot first) \u2014 your data is always yours to take.
|
|
4458
|
+
"app owners add <email>" grants a signed-up odla member the SAME
|
|
4459
|
+
full access as you; they then run their own "provision" to mint
|
|
4460
|
+
their own credentials for the shared db (prod included) \u2014 no
|
|
4461
|
+
secret is ever copied between people.
|
|
4462
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
4463
|
+
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
4464
|
+
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
4465
|
+
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
4466
|
+
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
4467
|
+
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
4468
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
4469
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
4470
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
4471
|
+
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
4472
|
+
reserved Clerk secret key, write-only from stdin or an env var.
|
|
4473
|
+
version Print the CLI version.
|
|
4474
|
+
|
|
4475
|
+
Safety:
|
|
4476
|
+
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
4477
|
+
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
4478
|
+
Provision caches the approved developer token and service credentials under
|
|
4479
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
4480
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
4481
|
+
Provision opens the approval page in your browser automatically whenever the
|
|
4482
|
+
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
4483
|
+
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
4484
|
+
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
4485
|
+
agents must relay it to the human verbatim. A started handshake is persisted
|
|
4486
|
+
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
4487
|
+
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
4488
|
+
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
4489
|
+
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
4490
|
+
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
4491
|
+
The email is a non-secret identity hint: never provide a password or session
|
|
4492
|
+
token. The matching account must already exist, be signed in, explicitly
|
|
4493
|
+
review the exact code, and finish any current request before claiming another.
|
|
4494
|
+
Run Code from a GitHub checkout already connected to an app in Studio; an
|
|
4495
|
+
odla.config.mjs may select the app explicitly but is not required. Code host
|
|
4496
|
+
approval and credential hashes live in odla-ai/db. The host
|
|
4497
|
+
credential is never written under .odla/; it exists only in the foreground
|
|
4498
|
+
"code connect" process and is rotated by the next approved connection.
|
|
4499
|
+
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
4500
|
+
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
4501
|
+
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
4502
|
+
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
4503
|
+
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
4504
|
+
plan are required before bounded snippets reach System AI.
|
|
4505
|
+
Run security plan first to inspect the admin-selected providers, models,
|
|
4506
|
+
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
4507
|
+
and digest that binds consent to that exact plan.
|
|
4508
|
+
`);
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
// src/security-hosted-github.ts
|
|
4512
|
+
var import_node_child_process2 = require("child_process");
|
|
4513
|
+
var import_node_util = require("util");
|
|
4514
|
+
|
|
4515
|
+
// src/security-hosted-request.ts
|
|
4516
|
+
async function requestHostedSecurityJson(options, path, init, action) {
|
|
4517
|
+
const platform = platformAudience(options.platform);
|
|
4518
|
+
const token = hostedSecurityCredential(options.token);
|
|
4519
|
+
const requestInit = {
|
|
4520
|
+
...init,
|
|
4521
|
+
redirect: "error",
|
|
4522
|
+
credentials: "omit",
|
|
4523
|
+
cache: "no-store",
|
|
4524
|
+
signal: options.signal,
|
|
4525
|
+
headers: {
|
|
4526
|
+
authorization: `Bearer ${token}`,
|
|
4527
|
+
"content-type": "application/json",
|
|
4528
|
+
...init.headers
|
|
4529
|
+
}
|
|
4530
|
+
};
|
|
4531
|
+
const response2 = await (options.fetch ?? fetch)(new URL(path, platform), requestInit);
|
|
4532
|
+
const body = await response2.json().catch(() => ({}));
|
|
4533
|
+
if (!response2.ok) {
|
|
4534
|
+
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
4535
|
+
const message3 = optionalHostedText(body.error?.message, "error message", 300);
|
|
4536
|
+
throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${message3 ? `: ${message3}` : ""}`);
|
|
4537
|
+
}
|
|
4538
|
+
return body;
|
|
4539
|
+
}
|
|
4540
|
+
function hostedIdentifier(value, name) {
|
|
4541
|
+
const result = optionalHostedText(value, name, 180);
|
|
4542
|
+
if (!result || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(result)) {
|
|
4543
|
+
throw new Error(`${name} is invalid`);
|
|
4544
|
+
}
|
|
4545
|
+
return result;
|
|
4546
|
+
}
|
|
4547
|
+
function positivePolicyVersion(value, name) {
|
|
4548
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
4549
|
+
throw new Error(`${name} is invalid`);
|
|
4550
|
+
}
|
|
4551
|
+
return value;
|
|
4552
|
+
}
|
|
4553
|
+
function optionalHostedText(value, name, maxLength) {
|
|
4554
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
4555
|
+
if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
4556
|
+
throw new Error(`${name} is invalid`);
|
|
4557
|
+
}
|
|
4558
|
+
return value;
|
|
4559
|
+
}
|
|
4560
|
+
function githubRepositoryName(value) {
|
|
4561
|
+
if (typeof value !== "string" || value.length > 201) {
|
|
4562
|
+
throw new Error("repository must be owner/name");
|
|
4563
|
+
}
|
|
4564
|
+
const parts = value.split("/");
|
|
4565
|
+
const owner = parts[0] ?? "";
|
|
4566
|
+
const name = (parts[1] ?? "").replace(/\.git$/i, "");
|
|
4567
|
+
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 === "..") {
|
|
4568
|
+
throw new Error("repository must be owner/name");
|
|
4569
|
+
}
|
|
4570
|
+
return `${owner}/${name}`;
|
|
4571
|
+
}
|
|
4572
|
+
function trustedGitHubInstallUrl(value) {
|
|
4573
|
+
let url;
|
|
4574
|
+
try {
|
|
4575
|
+
url = new URL(value);
|
|
4576
|
+
} catch {
|
|
4577
|
+
throw new Error("odla.ai returned an invalid GitHub installation URL");
|
|
4578
|
+
}
|
|
4579
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
|
|
4580
|
+
throw new Error("odla.ai returned an untrusted GitHub installation URL");
|
|
4581
|
+
}
|
|
4582
|
+
return url.toString();
|
|
4583
|
+
}
|
|
4584
|
+
function hostedPollInterval(value = 2e3) {
|
|
4585
|
+
if (!Number.isSafeInteger(value) || value < 100 || value > 3e4) {
|
|
4586
|
+
throw new Error("poll interval must be 100-30000ms");
|
|
4587
|
+
}
|
|
4588
|
+
return value;
|
|
4589
|
+
}
|
|
4590
|
+
function hostedPollTimeout(value = 10 * 6e4) {
|
|
4591
|
+
if (!Number.isSafeInteger(value) || value < 1e3 || value > 60 * 6e4) {
|
|
4592
|
+
throw new Error("poll timeout must be 1000-3600000ms");
|
|
4593
|
+
}
|
|
4594
|
+
return value;
|
|
4595
|
+
}
|
|
4596
|
+
async function waitForHostedPoll(milliseconds, signal) {
|
|
4597
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
|
|
4598
|
+
await new Promise((resolve10, reject) => {
|
|
4599
|
+
const timer = setTimeout(resolve10, milliseconds);
|
|
4600
|
+
signal?.addEventListener("abort", () => {
|
|
4601
|
+
clearTimeout(timer);
|
|
4602
|
+
reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
4603
|
+
}, { once: true });
|
|
4604
|
+
});
|
|
4605
|
+
}
|
|
4606
|
+
function isValidHostedSecurityPlan(value, env) {
|
|
4607
|
+
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;
|
|
4608
|
+
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;
|
|
4609
|
+
return validRoute(value.routes?.discovery, "security.discovery") && validRoute(value.routes?.validation, "security.validation");
|
|
4610
|
+
}
|
|
4611
|
+
function hostedSecurityCredential(value) {
|
|
4612
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
4613
|
+
throw new Error("Hosted security requires an injected odla developer token");
|
|
4614
|
+
}
|
|
4615
|
+
return value;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
// src/security-hosted-github.ts
|
|
4619
|
+
async function connectGitHubSecuritySource(options) {
|
|
4620
|
+
const out = options.stdout ?? console;
|
|
4621
|
+
const repository = githubRepositoryName(options.repository);
|
|
4622
|
+
const attempt = await requestHostedSecurityJson(
|
|
4623
|
+
options,
|
|
4624
|
+
"/registry/github/connect",
|
|
4625
|
+
{
|
|
4626
|
+
method: "POST",
|
|
4627
|
+
body: JSON.stringify({
|
|
4628
|
+
appId: hostedIdentifier(options.appId, "appId"),
|
|
4629
|
+
env: hostedIdentifier(options.env, "env"),
|
|
4630
|
+
repository
|
|
4631
|
+
})
|
|
4632
|
+
},
|
|
4633
|
+
"start GitHub connection"
|
|
4634
|
+
);
|
|
4635
|
+
const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
|
|
4636
|
+
if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
|
|
4637
|
+
throw new Error("odla.ai returned an invalid GitHub connection attempt");
|
|
4638
|
+
}
|
|
4639
|
+
const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
|
|
4640
|
+
out.log(`GitHub approval: ${installUrl}`);
|
|
4641
|
+
if (options.open !== false) {
|
|
4642
|
+
await (options.openInstallUrl ?? openUrl)(installUrl);
|
|
4643
|
+
out.log("github: opened installation approval in your browser");
|
|
4644
|
+
}
|
|
4645
|
+
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
4646
|
+
const now = options.now ?? Date.now;
|
|
4647
|
+
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
4648
|
+
const wait2 = options.wait ?? waitForHostedPoll;
|
|
4649
|
+
while (true) {
|
|
4650
|
+
const state = await requestHostedSecurityJson(
|
|
4651
|
+
options,
|
|
4652
|
+
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
4653
|
+
{},
|
|
4654
|
+
"check GitHub connection"
|
|
4655
|
+
);
|
|
4656
|
+
if (state.status !== "pending") {
|
|
4657
|
+
if (state.status === "connected") return state;
|
|
4658
|
+
throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
|
|
4659
|
+
}
|
|
4660
|
+
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
4661
|
+
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
async function listGitHubSecuritySources(options) {
|
|
4665
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
4666
|
+
const env = hostedIdentifier(options.env, "env");
|
|
4667
|
+
const body = await requestHostedSecurityJson(
|
|
4668
|
+
options,
|
|
4669
|
+
`/registry/apps/${encodeURIComponent(appId)}/github/sources?env=${encodeURIComponent(env)}`,
|
|
4670
|
+
{},
|
|
4671
|
+
"list GitHub security sources"
|
|
4672
|
+
);
|
|
4673
|
+
return Array.isArray(body.sources) ? body.sources : [];
|
|
4674
|
+
}
|
|
4675
|
+
async function disconnectGitHubSecuritySource(options) {
|
|
4676
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
4677
|
+
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
4678
|
+
await requestHostedSecurityJson(
|
|
4679
|
+
options,
|
|
4680
|
+
`/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
|
|
4681
|
+
{ method: "DELETE" },
|
|
4682
|
+
"disconnect GitHub security source"
|
|
4683
|
+
);
|
|
4684
|
+
}
|
|
4685
|
+
function repositoryFromGitRemote(remoteInput) {
|
|
4686
|
+
const remote = remoteInput.trim();
|
|
4687
|
+
const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
|
|
4688
|
+
if (scp) return githubRepositoryName(`${scp[1]}/${scp[2].replace(/\.git$/, "")}`);
|
|
4689
|
+
let url;
|
|
4690
|
+
try {
|
|
4691
|
+
url = new URL(remote);
|
|
4692
|
+
} catch {
|
|
4693
|
+
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
4694
|
+
}
|
|
4695
|
+
if (url.hostname !== "github.com" || url.protocol !== "https:" && url.protocol !== "ssh:") {
|
|
4696
|
+
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
4697
|
+
}
|
|
4698
|
+
if (url.password || url.protocol === "https:" && url.username || url.search || url.hash) {
|
|
4699
|
+
throw new Error("credential-bearing GitHub origin URLs are not accepted");
|
|
4700
|
+
}
|
|
4701
|
+
const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
4702
|
+
if (parts.length !== 2) {
|
|
4703
|
+
throw new Error("origin must identify one GitHub owner/name repository");
|
|
4704
|
+
}
|
|
4705
|
+
return githubRepositoryName(`${parts[0]}/${parts[1].replace(/\.git$/, "")}`);
|
|
4706
|
+
}
|
|
4707
|
+
async function inferGitHubRepository(cwd = process.cwd(), readOrigin = defaultReadOrigin) {
|
|
4708
|
+
let remote;
|
|
4709
|
+
try {
|
|
4710
|
+
remote = await readOrigin(cwd);
|
|
4711
|
+
} catch {
|
|
4712
|
+
throw new Error("Could not read git origin; pass --repo owner/name");
|
|
4713
|
+
}
|
|
4714
|
+
return repositoryFromGitRemote(remote);
|
|
4715
|
+
}
|
|
4716
|
+
async function defaultReadOrigin(cwd) {
|
|
4717
|
+
const result = await (0, import_node_util.promisify)(import_node_child_process2.execFile)(
|
|
4718
|
+
"git",
|
|
4719
|
+
["remote", "get-url", "origin"],
|
|
4720
|
+
{ cwd, encoding: "utf8" }
|
|
4721
|
+
);
|
|
4722
|
+
return result.stdout;
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
// src/code-connect.ts
|
|
4726
|
+
var CODE_PI_IMAGE = "ghcr.io/cory/odla-pi-agent@sha256:713db58428bc05cb486167c5747de8fd826669c9b283ff78e0139ac194c3d058";
|
|
4727
|
+
var CODE_BUILD_RECIPES = Object.freeze([{
|
|
4728
|
+
id: "node-test",
|
|
4729
|
+
image: "node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd",
|
|
4730
|
+
command: ["node", "--test"],
|
|
4731
|
+
timeoutMs: 12e4,
|
|
4732
|
+
maxOutputBytes: 1024 * 1024,
|
|
4733
|
+
cpus: 1,
|
|
4734
|
+
memory: "512m",
|
|
4735
|
+
pids: 128
|
|
4736
|
+
}]);
|
|
4737
|
+
async function codeConnect(options) {
|
|
4738
|
+
const cwd = options.cwd ?? process.cwd();
|
|
4739
|
+
const configPath = (0, import_node_path4.resolve)(cwd, options.configPath);
|
|
4740
|
+
const cfg = (0, import_node_fs7.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
4741
|
+
const repository = cfg ? null : await inferGitHubRepository(cwd, options.readGitOrigin);
|
|
4742
|
+
const platform = cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai";
|
|
4743
|
+
const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : "dev");
|
|
4744
|
+
if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
|
|
4745
|
+
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`);
|
|
4746
|
+
}
|
|
4747
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
4748
|
+
throw new Error("odla Code hosts require macOS or Linux");
|
|
4749
|
+
}
|
|
4750
|
+
const slots = options.slots ?? 1;
|
|
4751
|
+
if (!Number.isSafeInteger(slots) || slots < 1 || slots > 64) {
|
|
4752
|
+
throw new Error("--slots must be an integer from 1 to 64");
|
|
4753
|
+
}
|
|
4754
|
+
const heartbeatMs = options.heartbeatMs ?? 15e3;
|
|
4755
|
+
if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs < 1e3 || heartbeatMs > 3e5) {
|
|
4756
|
+
throw new Error("--heartbeat-ms must be an integer from 1000 to 300000");
|
|
4757
|
+
}
|
|
4758
|
+
const out = options.stdout ?? console;
|
|
4759
|
+
const doFetch = options.fetch ?? fetch;
|
|
4760
|
+
const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
|
|
4761
|
+
const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
|
|
4762
|
+
const hostName = (options.name ?? (0, import_node_os.hostname)()).trim();
|
|
4763
|
+
if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
|
|
4764
|
+
const approval = await (options.getToken ?? getScopedPlatformToken)({
|
|
4765
|
+
platform,
|
|
4766
|
+
scope: "platform:code:host:connect",
|
|
4767
|
+
email: options.email,
|
|
4768
|
+
open: options.open,
|
|
4769
|
+
fetch: doFetch,
|
|
4770
|
+
stdout: out,
|
|
4771
|
+
openApprovalUrl: options.openApprovalUrl,
|
|
4772
|
+
cache: false,
|
|
4773
|
+
label: `odla CLI Code host for ${cfg?.app.id ?? repository}/${appEnv}`
|
|
4774
|
+
});
|
|
4775
|
+
const target = cfg ? { appId: cfg.app.id } : { repository };
|
|
4776
|
+
const response2 = await doFetch(`${platform}/registry/code/hosts/connect`, {
|
|
4777
|
+
method: "POST",
|
|
4778
|
+
headers: { authorization: `Bearer ${approval}`, "content-type": "application/json" },
|
|
4779
|
+
body: JSON.stringify({ ...target, env: appEnv, name: hostName, platform: hostPlatform, slots }),
|
|
4780
|
+
redirect: "error",
|
|
4781
|
+
signal: options.signal
|
|
4782
|
+
});
|
|
4783
|
+
const raw = await response2.json().catch(() => null);
|
|
4784
|
+
if (!response2.ok) throw new Error(apiFailure("connect Code host", response2.status, raw));
|
|
4785
|
+
const connection = parseConnection(raw, cfg?.app.id, appEnv);
|
|
4786
|
+
const capabilities = {
|
|
4787
|
+
protocolVersion: CODE_RUNTIME_PROTOCOL_VERSION,
|
|
4788
|
+
platform: hostPlatform,
|
|
4789
|
+
arch: process.arch,
|
|
4790
|
+
engines: [engine],
|
|
4791
|
+
cpuCount: (0, import_node_os.cpus)().length,
|
|
4792
|
+
memoryBytes: (0, import_node_os.totalmem)()
|
|
4793
|
+
};
|
|
4794
|
+
out.log(`${connection.resumed ? "reconnected" : "enrolled"}: ${connection.host.name} (${connection.host.hostId})`);
|
|
4795
|
+
out.log(`binding: ${connection.binding.appId}/${connection.binding.env} \xB7 ${connection.offer.slots} slot(s) \xB7 ${engine}`);
|
|
4796
|
+
out.log("credentials: stored in odla-ai/db; host plaintext is memory-only");
|
|
4797
|
+
await (options.runRuntime ?? runCodeRuntime)({
|
|
4798
|
+
endpoint: platform,
|
|
4799
|
+
token: connection.token,
|
|
4800
|
+
engine,
|
|
4801
|
+
capabilities,
|
|
4802
|
+
heartbeatMs,
|
|
4803
|
+
once: options.once === true,
|
|
4804
|
+
signal: options.signal,
|
|
4805
|
+
stdout: out,
|
|
4806
|
+
fetch: doFetch
|
|
4807
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
async function runCodeRuntime(input) {
|
|
4810
|
+
const controller = new AbortController();
|
|
4811
|
+
const abort = () => controller.abort(input.signal?.reason ?? "runtime_shutdown");
|
|
4812
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
4813
|
+
const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
|
|
4814
|
+
const handlers = ["SIGINT", "SIGTERM"].map((name) => {
|
|
4815
|
+
const handler = () => controller.abort(name);
|
|
4816
|
+
process.once(name, handler);
|
|
4817
|
+
return [name, handler];
|
|
4818
|
+
});
|
|
4819
|
+
const control = createCodeRuntimeControlClient({
|
|
4820
|
+
endpoint: input.endpoint,
|
|
4821
|
+
token: input.token,
|
|
4822
|
+
signal,
|
|
4823
|
+
fetch: input.fetch
|
|
4824
|
+
});
|
|
4825
|
+
const commandEngine = new CodePiRuntimeEngine({
|
|
4826
|
+
control,
|
|
4827
|
+
engine: input.engine,
|
|
4828
|
+
image: CODE_PI_IMAGE,
|
|
4829
|
+
recipes: CODE_BUILD_RECIPES,
|
|
4830
|
+
recipeAuthorization: "registered_recipe"
|
|
4831
|
+
});
|
|
4832
|
+
const reconciler = new CodeRuntimeReconciler(control, commandEngine);
|
|
4833
|
+
try {
|
|
4834
|
+
await runCodeRuntimeHeartbeatLoop({
|
|
4835
|
+
control,
|
|
4836
|
+
runtimeVersion: `odla-ai-cli/${cliVersion()}`,
|
|
4837
|
+
capabilities: input.capabilities,
|
|
4838
|
+
heartbeatMs: input.heartbeatMs,
|
|
4839
|
+
once: input.once,
|
|
4840
|
+
signal,
|
|
4841
|
+
onSnapshot: async (snapshot) => {
|
|
4842
|
+
input.stdout.log(
|
|
4843
|
+
`online: ${snapshot.host.hostId} \xB7 ${snapshot.bindings.length} binding(s) \xB7 ${snapshot.commands.length} command(s)`
|
|
4844
|
+
);
|
|
4845
|
+
if (input.once && snapshot.commands.length) {
|
|
4846
|
+
throw new Error("--once is diagnostic-only and refuses pending Code commands; run without --once");
|
|
4847
|
+
}
|
|
4848
|
+
await reconciler.reconcile(snapshot);
|
|
4849
|
+
},
|
|
4850
|
+
onRetry: (error, delayMs) => input.stdout.error(
|
|
4851
|
+
`Code control plane unavailable; retrying in ${delayMs}ms \xB7 ${error instanceof Error ? error.message : String(error)}`
|
|
4852
|
+
)
|
|
4853
|
+
});
|
|
4854
|
+
} finally {
|
|
4855
|
+
await commandEngine.close();
|
|
4856
|
+
input.signal?.removeEventListener("abort", abort);
|
|
4857
|
+
for (const [name, handler] of handlers) process.removeListener(name, handler);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
function parseConnection(value, appId, appEnv) {
|
|
4861
|
+
const root = record4(value);
|
|
4862
|
+
const host = record4(root?.host);
|
|
4863
|
+
const offer = record4(root?.offer);
|
|
4864
|
+
const binding = record4(root?.binding);
|
|
4865
|
+
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)) {
|
|
4866
|
+
throw new Error("connect Code host returned an invalid response");
|
|
4867
|
+
}
|
|
4868
|
+
return root;
|
|
4869
|
+
}
|
|
4870
|
+
function apiFailure(action, status, value) {
|
|
4871
|
+
const message3 = record4(record4(value)?.error)?.message;
|
|
4872
|
+
return `${action} failed (${status})${typeof message3 === "string" ? `: ${message3}` : ""}`;
|
|
4873
|
+
}
|
|
4874
|
+
function record4(value) {
|
|
4875
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
// src/code-command.ts
|
|
4879
|
+
async function codeCommand(parsed, dependencies) {
|
|
4880
|
+
const sub = parsed.positionals[1];
|
|
4881
|
+
if (sub !== "connect") {
|
|
4882
|
+
throw new Error(`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev".`);
|
|
4883
|
+
}
|
|
4884
|
+
assertArgs(parsed, [
|
|
4885
|
+
"config",
|
|
4886
|
+
"env",
|
|
4887
|
+
"email",
|
|
4888
|
+
"open",
|
|
4889
|
+
"name",
|
|
4890
|
+
"slots",
|
|
4891
|
+
"engine",
|
|
4892
|
+
"heartbeat-ms",
|
|
4893
|
+
"once"
|
|
4894
|
+
], 2);
|
|
4895
|
+
const engine = stringOpt(parsed.options.engine) ?? "auto";
|
|
4896
|
+
if (!["auto", "container", "podman", "docker"].includes(engine)) {
|
|
4897
|
+
throw new Error("--engine must be auto, container, podman, or docker");
|
|
4898
|
+
}
|
|
4899
|
+
await (dependencies.codeConnect ?? codeConnect)({
|
|
4900
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
4901
|
+
env: stringOpt(parsed.options.env),
|
|
4902
|
+
email: stringOpt(parsed.options.email),
|
|
4903
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
4904
|
+
name: stringOpt(parsed.options.name),
|
|
4905
|
+
slots: numberOpt(parsed.options.slots, "--slots"),
|
|
4906
|
+
engine,
|
|
4907
|
+
heartbeatMs: numberOpt(parsed.options["heartbeat-ms"], "--heartbeat-ms"),
|
|
4908
|
+
once: parsed.options.once === true,
|
|
4909
|
+
fetch: dependencies.fetch,
|
|
4910
|
+
stdout: dependencies.stdout,
|
|
4911
|
+
openApprovalUrl: dependencies.openUrl,
|
|
4912
|
+
readGitOrigin: dependencies.readGitOrigin
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4916
|
+
// src/doctor-checks.ts
|
|
4917
|
+
var import_node_child_process4 = require("child_process");
|
|
4918
|
+
var import_node_fs9 = require("fs");
|
|
4919
|
+
var import_node_path6 = require("path");
|
|
4920
|
+
|
|
4921
|
+
// src/wrangler.ts
|
|
4922
|
+
var import_node_child_process3 = require("child_process");
|
|
4923
|
+
var import_node_fs8 = require("fs");
|
|
4924
|
+
var import_node_path5 = require("path");
|
|
4925
|
+
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
4926
|
+
const child = (0, import_node_child_process3.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
4927
|
+
let stdout = "";
|
|
4928
|
+
let stderr = "";
|
|
4929
|
+
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
4930
|
+
child.stderr.on("data", (chunk) => stderr += chunk.toString());
|
|
4931
|
+
child.on("error", reject);
|
|
4932
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
|
|
4933
|
+
child.stdin.end(opts?.input ?? "");
|
|
4934
|
+
});
|
|
4935
|
+
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
4936
|
+
function findWranglerConfig(rootDir) {
|
|
4937
|
+
for (const name of WRANGLER_CONFIG_FILES) {
|
|
4938
|
+
const path = (0, import_node_path5.join)(rootDir, name);
|
|
4939
|
+
if ((0, import_node_fs8.existsSync)(path)) return path;
|
|
4940
|
+
}
|
|
4941
|
+
return null;
|
|
4942
|
+
}
|
|
4943
|
+
function readWranglerConfig(path) {
|
|
4944
|
+
if (path.endsWith(".toml")) return null;
|
|
4945
|
+
try {
|
|
4946
|
+
return JSON.parse(stripJsonComments((0, import_node_fs8.readFileSync)(path, "utf8")));
|
|
4947
|
+
} catch {
|
|
4948
|
+
return null;
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
function stripJsonComments(text) {
|
|
4952
|
+
let result = "";
|
|
4953
|
+
let inString = false;
|
|
4954
|
+
for (let i = 0; i < text.length; i++) {
|
|
4955
|
+
const ch = text[i];
|
|
4956
|
+
if (inString) {
|
|
4957
|
+
result += ch;
|
|
4958
|
+
if (ch === "\\") {
|
|
4959
|
+
result += text[i + 1] ?? "";
|
|
4960
|
+
i++;
|
|
4961
|
+
} else if (ch === '"') {
|
|
4962
|
+
inString = false;
|
|
4963
|
+
}
|
|
4964
|
+
continue;
|
|
4965
|
+
}
|
|
4966
|
+
if (ch === '"') {
|
|
4967
|
+
inString = true;
|
|
4968
|
+
result += ch;
|
|
4969
|
+
continue;
|
|
4970
|
+
}
|
|
4971
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
4972
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
4973
|
+
result += "\n";
|
|
4974
|
+
continue;
|
|
4975
|
+
}
|
|
4976
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
4977
|
+
i += 2;
|
|
4978
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
4979
|
+
i++;
|
|
4980
|
+
continue;
|
|
4981
|
+
}
|
|
4982
|
+
result += ch;
|
|
4983
|
+
}
|
|
4984
|
+
return result;
|
|
4985
|
+
}
|
|
4986
|
+
async function wranglerLoggedIn(run, cwd) {
|
|
4987
|
+
try {
|
|
4988
|
+
const result = await run("npx", ["wrangler", "whoami"], { cwd });
|
|
4989
|
+
return result.code === 0 && !/not authenticated/i.test(`${result.stdout}${result.stderr}`);
|
|
4990
|
+
} catch {
|
|
4991
|
+
return false;
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
function wranglerPutSecret(run, opts) {
|
|
4995
|
+
const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
|
|
4996
|
+
return run("npx", args, { input: opts.value, cwd: opts.cwd });
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4999
|
+
// src/doctor-checks.ts
|
|
5000
|
+
function lintRules(rules, entities, publicRead) {
|
|
5001
|
+
const warnings = [];
|
|
5002
|
+
if (!rules) return warnings;
|
|
5003
|
+
for (const [ns, actions] of Object.entries(rules)) {
|
|
5004
|
+
for (const [action, expr] of Object.entries(actions)) {
|
|
5005
|
+
if (typeof expr !== "string" || expr.trim() !== "true") continue;
|
|
5006
|
+
if (action === "view" && publicRead.includes(ns)) continue;
|
|
5007
|
+
warnings.push(
|
|
5008
|
+
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`
|
|
5009
|
+
);
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
for (const entity of entities) {
|
|
5013
|
+
if (!(entity in rules)) warnings.push(`schema entity "${entity}" has no rules entry (all client access denied)`);
|
|
5014
|
+
}
|
|
5015
|
+
return warnings;
|
|
5016
|
+
}
|
|
5017
|
+
var defaultExec = (cmd, args, cwd) => (0, import_node_child_process4.execFileSync)(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
5018
|
+
function trackedSecretFiles(rootDir, exec = defaultExec, localPaths = []) {
|
|
5019
|
+
let output;
|
|
5020
|
+
try {
|
|
5021
|
+
const custom = localPaths.map((path) => gitignoreEntry(rootDir, path)).filter((path) => !!path);
|
|
5022
|
+
output = exec("git", ["ls-files", "--", ".dev.vars", ".odla", ...custom], rootDir);
|
|
5023
|
+
} catch {
|
|
5024
|
+
return [];
|
|
5025
|
+
}
|
|
5026
|
+
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`);
|
|
5027
|
+
}
|
|
5028
|
+
function wranglerWarnings(rootDir) {
|
|
5029
|
+
const configPath = findWranglerConfig(rootDir);
|
|
5030
|
+
if (!configPath) return [];
|
|
5031
|
+
const config = readWranglerConfig(configPath);
|
|
5032
|
+
if (!config) return [];
|
|
5033
|
+
const warnings = [];
|
|
5034
|
+
const blocks = [{ label: "", block: config }];
|
|
5035
|
+
const envs = config.env;
|
|
5036
|
+
if (envs && typeof envs === "object") {
|
|
5037
|
+
for (const [name, block] of Object.entries(envs)) {
|
|
5038
|
+
if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
for (const { label, block } of blocks) {
|
|
5042
|
+
const assets = block.assets;
|
|
5043
|
+
if (assets?.directory) {
|
|
5044
|
+
const dir = (0, import_node_path6.resolve)(rootDir, assets.directory);
|
|
5045
|
+
if (dir === (0, import_node_path6.resolve)(rootDir)) {
|
|
5046
|
+
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
5047
|
+
} else if ((0, import_node_fs9.existsSync)((0, import_node_path6.join)(dir, "node_modules"))) {
|
|
5048
|
+
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
const vars = block.vars;
|
|
5052
|
+
if (vars && typeof vars === "object") {
|
|
5053
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
5054
|
+
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value === "string" && looksSecret(value)) {
|
|
5055
|
+
warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
const pkg = readPackageJson(rootDir);
|
|
5061
|
+
if (pkg && typeof pkg.scripts === "object" && pkg.scripts && "deploy" in pkg.scripts) {
|
|
5062
|
+
warnings.push(`package.json has a script named "deploy" \u2014 CI may auto-deploy it; rename to deploy:app`);
|
|
5063
|
+
}
|
|
5064
|
+
return warnings;
|
|
5065
|
+
}
|
|
5066
|
+
function o11yProjectWarnings(rootDir) {
|
|
5067
|
+
const warnings = [];
|
|
5068
|
+
const pkg = readPackageJson(rootDir);
|
|
5069
|
+
const dependencies = pkg?.dependencies;
|
|
5070
|
+
const devDependencies = pkg?.devDependencies;
|
|
5071
|
+
if (!dependencies?.["@odla-ai/o11y"]) {
|
|
5072
|
+
warnings.push(
|
|
5073
|
+
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"'
|
|
5074
|
+
);
|
|
5075
|
+
}
|
|
5076
|
+
const configPath = findWranglerConfig(rootDir);
|
|
5077
|
+
const config = configPath ? readWranglerConfig(configPath) : null;
|
|
5078
|
+
if (!configPath || !config) {
|
|
5079
|
+
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
5080
|
+
return warnings;
|
|
5081
|
+
}
|
|
5082
|
+
const main = typeof config.main === "string" ? (0, import_node_path6.resolve)(rootDir, config.main) : null;
|
|
5083
|
+
if (!main || !(0, import_node_fs9.existsSync)(main)) {
|
|
5084
|
+
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
5085
|
+
} else {
|
|
5086
|
+
let source = "";
|
|
5087
|
+
try {
|
|
5088
|
+
source = (0, import_node_fs9.readFileSync)(main, "utf8");
|
|
5089
|
+
} catch {
|
|
5090
|
+
}
|
|
5091
|
+
if (!/\bwithObservability\b/.test(source)) {
|
|
5092
|
+
warnings.push(`wrangler entrypoint ${config.main} does not reference withObservability`);
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
const flags = Array.isArray(config.compatibility_flags) ? config.compatibility_flags : [];
|
|
5096
|
+
if (!flags.includes("nodejs_compat")) {
|
|
5097
|
+
warnings.push('wrangler compatibility_flags is missing "nodejs_compat" required by @odla-ai/o11y');
|
|
5098
|
+
}
|
|
5099
|
+
return warnings;
|
|
5100
|
+
}
|
|
5101
|
+
function calendarProjectWarnings(rootDir) {
|
|
2029
5102
|
const pkg = readPackageJson(rootDir);
|
|
2030
5103
|
const dependencies = pkg?.dependencies;
|
|
2031
5104
|
const devDependencies = pkg?.devDependencies;
|
|
@@ -2036,14 +5109,14 @@ function calendarProjectWarnings(rootDir) {
|
|
|
2036
5109
|
}
|
|
2037
5110
|
function readPackageJson(rootDir) {
|
|
2038
5111
|
try {
|
|
2039
|
-
return JSON.parse((0,
|
|
5112
|
+
return JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path6.join)(rootDir, "package.json"), "utf8"));
|
|
2040
5113
|
} catch {
|
|
2041
5114
|
return null;
|
|
2042
5115
|
}
|
|
2043
5116
|
}
|
|
2044
5117
|
|
|
2045
5118
|
// src/integrations.ts
|
|
2046
|
-
var
|
|
5119
|
+
var import_node_util2 = require("util");
|
|
2047
5120
|
async function resolveDatabaseConfig(cfg, options = {}) {
|
|
2048
5121
|
const integrations = cfg.integrations ?? [];
|
|
2049
5122
|
if (!cfg.services.includes("db")) return { schema: void 0, rules: void 0, integrations };
|
|
@@ -2134,7 +5207,7 @@ function normalizeSchema(value) {
|
|
|
2134
5207
|
}
|
|
2135
5208
|
function mergeMap(target, fragment, label) {
|
|
2136
5209
|
for (const [name, value] of Object.entries(fragment)) {
|
|
2137
|
-
if (Object.hasOwn(target, name) && !(0,
|
|
5210
|
+
if (Object.hasOwn(target, name) && !(0, import_node_util2.isDeepStrictEqual)(target[name], value)) {
|
|
2138
5211
|
throw new Error(`${label} "${name}" conflicts with an existing definition`);
|
|
2139
5212
|
}
|
|
2140
5213
|
target[name] = value;
|
|
@@ -2173,150 +5246,48 @@ async function doctor(options) {
|
|
|
2173
5246
|
}
|
|
2174
5247
|
const warnings = [];
|
|
2175
5248
|
if (schema && entities.length === 0) warnings.push("schema has no entities");
|
|
2176
|
-
if (rules) {
|
|
2177
|
-
for (const ns of Object.keys(rules)) {
|
|
2178
|
-
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
warnings.push(...integrationWarnings(database.integrations, schema, rules));
|
|
2182
|
-
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
2183
|
-
if (cfg.auth?.clerk) {
|
|
2184
|
-
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
2185
|
-
if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
|
|
2186
|
-
warnings.push(`auth.clerk.${env} references unset env var ${value}`);
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
if (cfg.services.includes("ai") && cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) {
|
|
2191
|
-
warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
2192
|
-
}
|
|
2193
|
-
if (cfg.services.includes("o11y")) {
|
|
2194
|
-
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
2195
|
-
for (const env of cfg.envs) {
|
|
2196
|
-
if (!credentials?.envs[env]?.o11yToken) {
|
|
2197
|
-
warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
warnings.push(...o11yProjectWarnings(cfg.rootDir));
|
|
2201
|
-
}
|
|
2202
|
-
if (cfg.services.includes("calendar")) warnings.push(...calendarProjectWarnings(cfg.rootDir));
|
|
2203
|
-
else if (cfg.calendar) warnings.push('calendar config is present but "calendar" is not in services');
|
|
2204
|
-
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
2205
|
-
warnings.push(...trackedSecretFiles(cfg.rootDir, void 0, [
|
|
2206
|
-
cfg.local.tokenFile,
|
|
2207
|
-
cfg.local.credentialsFile,
|
|
2208
|
-
cfg.local.devVarsFile
|
|
2209
|
-
]));
|
|
2210
|
-
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
2211
|
-
if (warnings.length) {
|
|
2212
|
-
out.log("");
|
|
2213
|
-
out.log("warnings:");
|
|
2214
|
-
for (const warning of warnings) out.log(` - ${warning}`);
|
|
2215
|
-
} else {
|
|
2216
|
-
out.log("ok");
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
// src/help.ts
|
|
2221
|
-
var import_node_fs8 = require("fs");
|
|
2222
|
-
function cliVersion() {
|
|
2223
|
-
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
2224
|
-
return pkg.version ?? "unknown";
|
|
2225
|
-
}
|
|
2226
|
-
function printHelp() {
|
|
2227
|
-
console.log(`odla-ai
|
|
2228
|
-
|
|
2229
|
-
Usage:
|
|
2230
|
-
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
2231
|
-
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
2232
|
-
odla-ai doctor [--config odla.config.mjs]
|
|
2233
|
-
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
2234
|
-
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
2235
|
-
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
2236
|
-
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
2237
|
-
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
2238
|
-
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
2239
|
-
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
2240
|
-
odla-ai capabilities [--json]
|
|
2241
|
-
odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
2242
|
-
odla-ai admin ai models [--provider <id>] [--json]
|
|
2243
|
-
odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
2244
|
-
odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
|
|
2245
|
-
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
|
|
2246
|
-
odla-ai admin ai credentials [--json]
|
|
2247
|
-
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
2248
|
-
odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
2249
|
-
odla-ai admin ai audit [--limit <1-200>] [--json]
|
|
2250
|
-
odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
|
|
2251
|
-
odla-ai security github disconnect --source <id> [--env dev] [--yes]
|
|
2252
|
-
odla-ai security plan [--env dev] [--json]
|
|
2253
|
-
odla-ai security sources [--env dev] [--json]
|
|
2254
|
-
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
2255
|
-
odla-ai security status <job-id> [--follow] [--json]
|
|
2256
|
-
odla-ai security report <job-id> [--json]
|
|
2257
|
-
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
2258
|
-
odla-ai security run [target] --self --ack-redacted-source
|
|
2259
|
-
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
2260
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
2261
|
-
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
2262
|
-
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
2263
|
-
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
2264
|
-
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
2265
|
-
odla-ai version
|
|
2266
|
-
|
|
2267
|
-
Commands:
|
|
2268
|
-
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
2269
|
-
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
2270
|
-
doctor Validate and summarize the project config without network calls.
|
|
2271
|
-
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
2272
|
-
app Archive (suspend, data retained), restore, or export the app.
|
|
2273
|
-
Archiving takes every environment's data plane down until restored;
|
|
2274
|
-
permanent deletion has NO CLI \u2014 it requires a signed-in owner in
|
|
2275
|
-
Studio, and no machine or agent credential can purge. "app export"
|
|
2276
|
-
downloads a portable gzipped-JSONL snapshot of one environment's
|
|
2277
|
-
database (--fresh takes a new snapshot first) \u2014 your data is
|
|
2278
|
-
always yours to take.
|
|
2279
|
-
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
2280
|
-
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
2281
|
-
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
2282
|
-
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
2283
|
-
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
2284
|
-
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
2285
|
-
copilot, gemini, or agents (repeatable or comma-separated).
|
|
2286
|
-
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
2287
|
-
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
2288
|
-
reserved Clerk secret key, write-only from stdin or an env var.
|
|
2289
|
-
version Print the CLI version.
|
|
2290
|
-
|
|
2291
|
-
Safety:
|
|
2292
|
-
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
2293
|
-
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
2294
|
-
Provision caches the approved developer token and service credentials under
|
|
2295
|
-
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
2296
|
-
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
2297
|
-
Provision opens the approval page in your browser automatically whenever the
|
|
2298
|
-
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
2299
|
-
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
2300
|
-
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
2301
|
-
agents must relay it to the human verbatim. A started handshake is persisted
|
|
2302
|
-
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
2303
|
-
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
2304
|
-
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
2305
|
-
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
2306
|
-
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
2307
|
-
The email is a non-secret identity hint: never provide a password or session
|
|
2308
|
-
token. The matching account must already exist, be signed in, explicitly
|
|
2309
|
-
review the exact code, and finish any current request before claiming another.
|
|
2310
|
-
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
2311
|
-
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
2312
|
-
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
2313
|
-
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
2314
|
-
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
2315
|
-
plan are required before bounded snippets reach System AI.
|
|
2316
|
-
Run security plan first to inspect the admin-selected providers, models,
|
|
2317
|
-
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
2318
|
-
and digest that binds consent to that exact plan.
|
|
2319
|
-
`);
|
|
5249
|
+
if (rules) {
|
|
5250
|
+
for (const ns of Object.keys(rules)) {
|
|
5251
|
+
if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
warnings.push(...integrationWarnings(database.integrations, schema, rules));
|
|
5255
|
+
if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
|
|
5256
|
+
if (cfg.auth?.clerk) {
|
|
5257
|
+
for (const [env, value] of Object.entries(cfg.auth.clerk)) {
|
|
5258
|
+
if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
|
|
5259
|
+
warnings.push(`auth.clerk.${env} references unset env var ${value}`);
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
if (cfg.services.includes("ai") && cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) {
|
|
5264
|
+
warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
|
|
5265
|
+
}
|
|
5266
|
+
if (cfg.services.includes("o11y")) {
|
|
5267
|
+
const credentials = readCredentials(cfg.local.credentialsFile);
|
|
5268
|
+
for (const env of cfg.envs) {
|
|
5269
|
+
if (!credentials?.envs[env]?.o11yToken) {
|
|
5270
|
+
warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
warnings.push(...o11yProjectWarnings(cfg.rootDir));
|
|
5274
|
+
}
|
|
5275
|
+
if (cfg.services.includes("calendar")) warnings.push(...calendarProjectWarnings(cfg.rootDir));
|
|
5276
|
+
else if (cfg.calendar) warnings.push('calendar config is present but "calendar" is not in services');
|
|
5277
|
+
warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
|
|
5278
|
+
warnings.push(...trackedSecretFiles(cfg.rootDir, void 0, [
|
|
5279
|
+
cfg.local.tokenFile,
|
|
5280
|
+
cfg.local.credentialsFile,
|
|
5281
|
+
cfg.local.devVarsFile
|
|
5282
|
+
]));
|
|
5283
|
+
warnings.push(...wranglerWarnings(cfg.rootDir));
|
|
5284
|
+
if (warnings.length) {
|
|
5285
|
+
out.log("");
|
|
5286
|
+
out.log("warnings:");
|
|
5287
|
+
for (const warning of warnings) out.log(` - ${warning}`);
|
|
5288
|
+
} else {
|
|
5289
|
+
out.log("ok");
|
|
5290
|
+
}
|
|
2320
5291
|
}
|
|
2321
5292
|
|
|
2322
5293
|
// src/harness-options.ts
|
|
@@ -2339,13 +5310,13 @@ function harnessOption(value, flag) {
|
|
|
2339
5310
|
}
|
|
2340
5311
|
|
|
2341
5312
|
// src/init.ts
|
|
2342
|
-
var
|
|
2343
|
-
var
|
|
5313
|
+
var import_node_fs10 = require("fs");
|
|
5314
|
+
var import_node_path7 = require("path");
|
|
2344
5315
|
function initProject(options) {
|
|
2345
5316
|
const out = options.stdout ?? console;
|
|
2346
|
-
const rootDir = (0,
|
|
2347
|
-
const configPath = (0,
|
|
2348
|
-
if ((0,
|
|
5317
|
+
const rootDir = (0, import_node_path7.resolve)(options.rootDir ?? process.cwd());
|
|
5318
|
+
const configPath = (0, import_node_path7.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
5319
|
+
if ((0, import_node_fs10.existsSync)(configPath) && !options.force) {
|
|
2349
5320
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
2350
5321
|
}
|
|
2351
5322
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -2355,20 +5326,20 @@ function initProject(options) {
|
|
|
2355
5326
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
2356
5327
|
if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
|
|
2357
5328
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
2358
|
-
(0,
|
|
2359
|
-
(0,
|
|
2360
|
-
(0,
|
|
2361
|
-
(0,
|
|
2362
|
-
writeIfMissing((0,
|
|
2363
|
-
writeIfMissing((0,
|
|
5329
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path7.dirname)(configPath), { recursive: true });
|
|
5330
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path7.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
5331
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path7.resolve)(rootDir, ".odla"), { recursive: true });
|
|
5332
|
+
(0, import_node_fs10.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
5333
|
+
writeIfMissing((0, import_node_path7.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
5334
|
+
writeIfMissing((0, import_node_path7.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
2364
5335
|
ensureGitignore(rootDir);
|
|
2365
5336
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
2366
5337
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
2367
5338
|
out.log("updated .gitignore for local odla credentials");
|
|
2368
5339
|
}
|
|
2369
5340
|
function writeIfMissing(path, text) {
|
|
2370
|
-
if ((0,
|
|
2371
|
-
(0,
|
|
5341
|
+
if ((0, import_node_fs10.existsSync)(path)) return;
|
|
5342
|
+
(0, import_node_fs10.writeFileSync)(path, text);
|
|
2372
5343
|
}
|
|
2373
5344
|
function configTemplate(input) {
|
|
2374
5345
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
@@ -2839,10 +5810,10 @@ async function provision(options) {
|
|
|
2839
5810
|
stdout: out
|
|
2840
5811
|
});
|
|
2841
5812
|
} catch (error) {
|
|
2842
|
-
const
|
|
5813
|
+
const message3 = error instanceof Error ? error.message : String(error);
|
|
2843
5814
|
const consent = env === "prod" || env === "production" ? " --yes" : "";
|
|
2844
5815
|
throw new Error(
|
|
2845
|
-
`${
|
|
5816
|
+
`${message3}
|
|
2846
5817
|
${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}${consent}" without issuing or rotating again`,
|
|
2847
5818
|
{ cause: error }
|
|
2848
5819
|
);
|
|
@@ -2905,8 +5876,8 @@ async function readRegistryApp(cfg, token, doFetch) {
|
|
|
2905
5876
|
});
|
|
2906
5877
|
if (res.status === 404) return null;
|
|
2907
5878
|
if (!res.ok) return null;
|
|
2908
|
-
const
|
|
2909
|
-
return
|
|
5879
|
+
const json2 = await res.json();
|
|
5880
|
+
return json2.app ?? null;
|
|
2910
5881
|
}
|
|
2911
5882
|
async function postJson2(doFetch, url, bearer, body) {
|
|
2912
5883
|
const res = await doFetch(url, {
|
|
@@ -2993,7 +5964,7 @@ async function resolveVaultWrite(options) {
|
|
|
2993
5964
|
}
|
|
2994
5965
|
|
|
2995
5966
|
// src/security-command-context.ts
|
|
2996
|
-
var
|
|
5967
|
+
var import_promises9 = require("readline/promises");
|
|
2997
5968
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
2998
5969
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
2999
5970
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -3016,12 +5987,12 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
3016
5987
|
);
|
|
3017
5988
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
3018
5989
|
}
|
|
3019
|
-
async function interactiveConfirmation(
|
|
3020
|
-
if (dependencies.confirm) return dependencies.confirm(
|
|
5990
|
+
async function interactiveConfirmation(message3, dependencies) {
|
|
5991
|
+
if (dependencies.confirm) return dependencies.confirm(message3);
|
|
3021
5992
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
3022
|
-
const prompt = (0,
|
|
5993
|
+
const prompt = (0, import_promises9.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3023
5994
|
try {
|
|
3024
|
-
const answer = await prompt.question(`${
|
|
5995
|
+
const answer = await prompt.question(`${message3} [y/N] `);
|
|
3025
5996
|
return /^y(?:es)?$/i.test(answer.trim());
|
|
3026
5997
|
} finally {
|
|
3027
5998
|
prompt.close();
|
|
@@ -3063,9 +6034,9 @@ function printHostedSecurityIntent(out, intent) {
|
|
|
3063
6034
|
}
|
|
3064
6035
|
function assertHostedSecurityPlanReady(plan) {
|
|
3065
6036
|
const reasons = [];
|
|
3066
|
-
for (const [label,
|
|
3067
|
-
if (!
|
|
3068
|
-
if (!
|
|
6037
|
+
for (const [label, route2] of Object.entries(plan.routes)) {
|
|
6038
|
+
if (!route2.enabled) reasons.push(`${label} is disabled`);
|
|
6039
|
+
if (!route2.credentialReady) reasons.push(`${label} provider credential is unavailable`);
|
|
3069
6040
|
}
|
|
3070
6041
|
if (!plan.independent) reasons.push("discovery and validation are not independently routed");
|
|
3071
6042
|
if (plan.ready && reasons.length === 0) return;
|
|
@@ -3088,54 +6059,51 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
3088
6059
|
out.log(` findings: confirmed=${job.counts.confirmed} needs_reproduction=${job.counts.needsReproduction} candidates=${job.counts.candidates}`);
|
|
3089
6060
|
}
|
|
3090
6061
|
if (job.errorCode) out.log(` failure: ${job.errorCode}`);
|
|
3091
|
-
const url = new URL(
|
|
3092
|
-
url.searchParams.set("app", appId);
|
|
3093
|
-
url.searchParams.set("env", job.env);
|
|
3094
|
-
url.searchParams.set("tab", "security");
|
|
6062
|
+
const url = new URL(`/studio/apps/${encodeURIComponent(appId)}/${encodeURIComponent(job.env)}/security`, platform);
|
|
3095
6063
|
url.searchParams.set("job", job.jobId);
|
|
3096
6064
|
out.log(` Studio: ${url.toString()}`);
|
|
3097
6065
|
}
|
|
3098
|
-
function printHostedReport(out,
|
|
3099
|
-
out.log(`security report ${
|
|
3100
|
-
out.log(` coverage: ${
|
|
3101
|
-
out.log(` findings: confirmed=${
|
|
3102
|
-
out.log(` discovery: ${
|
|
3103
|
-
out.log(` validation: ${
|
|
3104
|
-
for (const finding of
|
|
6066
|
+
function printHostedReport(out, report2) {
|
|
6067
|
+
out.log(`security report ${report2.jobId}: ${report2.repository}@${report2.revision}`);
|
|
6068
|
+
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}`);
|
|
6069
|
+
out.log(` findings: confirmed=${report2.metrics.confirmed} needs_reproduction=${report2.metrics.needsReproduction} candidates=${report2.metrics.candidates} rejected=${report2.metrics.rejected}`);
|
|
6070
|
+
out.log(` discovery: ${report2.provenance.discovery?.provider ?? "unknown"}/${report2.provenance.discovery?.model ?? "unknown"}`);
|
|
6071
|
+
out.log(` validation: ${report2.provenance.validation?.provider ?? "unknown"}/${report2.provenance.validation?.model ?? "unknown"} independent=${String(report2.provenance.independentValidation)}`);
|
|
6072
|
+
for (const finding of report2.findings) {
|
|
3105
6073
|
const location = finding.locations[0];
|
|
3106
6074
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
3107
6075
|
}
|
|
3108
|
-
for (const limitation of
|
|
6076
|
+
for (const limitation of report2.limitations) out.log(` limitation: ${limitation}`);
|
|
3109
6077
|
}
|
|
3110
|
-
function enforceHostedReportGate(
|
|
6078
|
+
function enforceHostedReportGate(report2, parsed, out, emitSuccess) {
|
|
3111
6079
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
3112
6080
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
3113
6081
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
3114
6082
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
3115
|
-
const confirmed =
|
|
3116
|
-
const leads = failOnCandidates ?
|
|
3117
|
-
const incomplete =
|
|
6083
|
+
const confirmed = report2.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
6084
|
+
const leads = failOnCandidates ? report2.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
6085
|
+
const incomplete = report2.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
3118
6086
|
if (confirmed.length || leads.length || incomplete) {
|
|
3119
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
6087
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report2.coverageStatus}` : ""}`);
|
|
3120
6088
|
}
|
|
3121
6089
|
if (emitSuccess) {
|
|
3122
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
6090
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report2.coverageStatus}. This is not proof that the application is secure.`);
|
|
3123
6091
|
}
|
|
3124
6092
|
}
|
|
3125
|
-
function printHostedSecurityPlanRoute(out, label,
|
|
3126
|
-
const readiness =
|
|
3127
|
-
|
|
3128
|
-
|
|
6093
|
+
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
6094
|
+
const readiness = route2.enabled && route2.credentialReady ? "ready" : [
|
|
6095
|
+
route2.enabled ? void 0 : "disabled",
|
|
6096
|
+
route2.credentialReady ? void 0 : "credential unavailable"
|
|
3129
6097
|
].filter(Boolean).join(", ");
|
|
3130
|
-
out.log(` ${label}: ${
|
|
3131
|
-
out.log(` bounds: ${
|
|
6098
|
+
out.log(` ${label}: ${route2.provider}/${route2.model} \xB7 policy v${route2.policyVersion} \xB7 ${readiness}`);
|
|
6099
|
+
out.log(` bounds: ${route2.maxCallsPerRun} calls/run \xB7 ${route2.maxInputBytes} input bytes/call \xB7 ${route2.maxOutputTokens} output tokens/call`);
|
|
3132
6100
|
}
|
|
3133
6101
|
function printHostedCoverage(out, job) {
|
|
3134
6102
|
const coverage = job.coverage;
|
|
3135
6103
|
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}` : ""}`);
|
|
3136
6104
|
}
|
|
3137
|
-
function routeLabel(
|
|
3138
|
-
return `${
|
|
6105
|
+
function routeLabel(route2) {
|
|
6106
|
+
return `${route2.provider}/${route2.model}${route2.policyVersion ? ` policy v${route2.policyVersion}` : ""}`;
|
|
3139
6107
|
}
|
|
3140
6108
|
var HOSTED_SEVERITIES = ["informational", "low", "medium", "high", "critical"];
|
|
3141
6109
|
function hostedSeverity(value, flag) {
|
|
@@ -3149,9 +6117,9 @@ function hostedSeverity(value, flag) {
|
|
|
3149
6117
|
var import_security2 = require("@odla-ai/security");
|
|
3150
6118
|
|
|
3151
6119
|
// src/security.ts
|
|
3152
|
-
var
|
|
6120
|
+
var import_node_path8 = require("path");
|
|
3153
6121
|
var import_security = require("@odla-ai/security");
|
|
3154
|
-
var
|
|
6122
|
+
var import_node2 = require("@odla-ai/security/node");
|
|
3155
6123
|
async function runHostedSecurity(options) {
|
|
3156
6124
|
if (options.sourceDisclosureAck !== "redacted") {
|
|
3157
6125
|
throw new Error("Hosted security requires sourceDisclosureAck=redacted before repository source may leave this process");
|
|
@@ -3161,9 +6129,9 @@ async function runHostedSecurity(options) {
|
|
|
3161
6129
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
3162
6130
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
3163
6131
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
3164
|
-
const target = (0,
|
|
3165
|
-
const output = (0,
|
|
3166
|
-
const outputRelative = (0,
|
|
6132
|
+
const target = (0, import_node_path8.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
6133
|
+
const output = (0, import_node_path8.resolve)(options.out ?? (0, import_node_path8.resolve)(target, ".odla/security/hosted"));
|
|
6134
|
+
const outputRelative = (0, import_node_path8.relative)(target, output).split(import_node_path8.sep).join("/");
|
|
3167
6135
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
3168
6136
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
3169
6137
|
const tokenRequest = {
|
|
@@ -3174,8 +6142,8 @@ async function runHostedSecurity(options) {
|
|
|
3174
6142
|
scope: selfAudit ? "platform:security:self" : null
|
|
3175
6143
|
};
|
|
3176
6144
|
const token = await injectedToken(options, tokenRequest);
|
|
3177
|
-
const snapshot = await (0,
|
|
3178
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
6145
|
+
const snapshot = await (0, import_node2.snapshotDirectory)(target, {
|
|
6146
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path8.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
3179
6147
|
});
|
|
3180
6148
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
3181
6149
|
platform,
|
|
@@ -3193,7 +6161,7 @@ async function runHostedSecurity(options) {
|
|
|
3193
6161
|
});
|
|
3194
6162
|
const harness = (0, import_security.createSecurityHarness)({
|
|
3195
6163
|
profile,
|
|
3196
|
-
store: new
|
|
6164
|
+
store: new import_node2.FileRunStore((0, import_node_path8.resolve)(output, "state")),
|
|
3197
6165
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
3198
6166
|
validationReasoner: hosted.validationReasoner,
|
|
3199
6167
|
policy: {
|
|
@@ -3202,22 +6170,22 @@ async function runHostedSecurity(options) {
|
|
|
3202
6170
|
allowNetwork: false
|
|
3203
6171
|
}
|
|
3204
6172
|
});
|
|
3205
|
-
const
|
|
3206
|
-
await (0,
|
|
3207
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
6173
|
+
const report2 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
6174
|
+
await (0, import_node2.writeSecurityArtifacts)(output, report2);
|
|
6175
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report2);
|
|
3208
6176
|
await hosted.complete({
|
|
3209
6177
|
reportDigest,
|
|
3210
|
-
coverageStatus:
|
|
3211
|
-
confirmed:
|
|
3212
|
-
candidates:
|
|
6178
|
+
coverageStatus: report2.coverageStatus,
|
|
6179
|
+
confirmed: report2.metrics.confirmed,
|
|
6180
|
+
candidates: report2.metrics.candidates
|
|
3213
6181
|
}, { signal: options.signal });
|
|
3214
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
3215
|
-
return Object.freeze({ report, run: hosted.run, output });
|
|
6182
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report2, output);
|
|
6183
|
+
return Object.freeze({ report: report2, run: hosted.run, output });
|
|
3216
6184
|
}
|
|
3217
6185
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
3218
6186
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
3219
6187
|
if (!env || !declared.includes(env)) {
|
|
3220
|
-
const shown = (0,
|
|
6188
|
+
const shown = (0, import_node_path8.relative)(rootDir, configPath) || configPath;
|
|
3221
6189
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
3222
6190
|
}
|
|
3223
6191
|
return env;
|
|
@@ -3238,234 +6206,20 @@ function profileFor(name, maxHuntTasks) {
|
|
|
3238
6206
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
3239
6207
|
return { ...profile, maxHuntTasks };
|
|
3240
6208
|
}
|
|
3241
|
-
function printSummary(out, appId, env, run,
|
|
3242
|
-
const complete =
|
|
6209
|
+
function printSummary(out, appId, env, run, report2, output) {
|
|
6210
|
+
const complete = report2.coverage.filter((cell) => cell.state === "complete").length;
|
|
3243
6211
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
3244
6212
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
3245
6213
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
3246
|
-
out.log(` coverage: ${
|
|
3247
|
-
if (
|
|
3248
|
-
out.log(` findings: confirmed=${
|
|
3249
|
-
out.log(` report: ${(0,
|
|
6214
|
+
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}`);
|
|
6215
|
+
if (report2.callBudget) out.log(` calls: discovery=${formatBudget(report2.callBudget.discovery)} validation=${formatBudget(report2.callBudget.validation)}`);
|
|
6216
|
+
out.log(` findings: confirmed=${report2.metrics.confirmed} needs_reproduction=${report2.metrics.needsReproduction} candidates=${report2.metrics.candidates}`);
|
|
6217
|
+
out.log(` report: ${(0, import_node_path8.resolve)(output, "REPORT.md")}`);
|
|
3250
6218
|
}
|
|
3251
6219
|
function formatBudget(usage) {
|
|
3252
6220
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
3253
6221
|
}
|
|
3254
6222
|
|
|
3255
|
-
// src/security-hosted-github.ts
|
|
3256
|
-
var import_node_child_process4 = require("child_process");
|
|
3257
|
-
var import_node_util2 = require("util");
|
|
3258
|
-
|
|
3259
|
-
// src/security-hosted-request.ts
|
|
3260
|
-
async function requestHostedSecurityJson(options, path, init, action) {
|
|
3261
|
-
const platform = platformAudience(options.platform);
|
|
3262
|
-
const token = hostedSecurityCredential(options.token);
|
|
3263
|
-
const requestInit = {
|
|
3264
|
-
...init,
|
|
3265
|
-
redirect: "error",
|
|
3266
|
-
credentials: "omit",
|
|
3267
|
-
cache: "no-store",
|
|
3268
|
-
signal: options.signal,
|
|
3269
|
-
headers: {
|
|
3270
|
-
authorization: `Bearer ${token}`,
|
|
3271
|
-
"content-type": "application/json",
|
|
3272
|
-
...init.headers
|
|
3273
|
-
}
|
|
3274
|
-
};
|
|
3275
|
-
const response = await (options.fetch ?? fetch)(new URL(path, platform), requestInit);
|
|
3276
|
-
const body = await response.json().catch(() => ({}));
|
|
3277
|
-
if (!response.ok) {
|
|
3278
|
-
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
3279
|
-
const message = optionalHostedText(body.error?.message, "error message", 300);
|
|
3280
|
-
throw new Error(`${action} failed (${response.status})${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`);
|
|
3281
|
-
}
|
|
3282
|
-
return body;
|
|
3283
|
-
}
|
|
3284
|
-
function hostedIdentifier(value, name) {
|
|
3285
|
-
const result = optionalHostedText(value, name, 180);
|
|
3286
|
-
if (!result || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(result)) {
|
|
3287
|
-
throw new Error(`${name} is invalid`);
|
|
3288
|
-
}
|
|
3289
|
-
return result;
|
|
3290
|
-
}
|
|
3291
|
-
function positivePolicyVersion(value, name) {
|
|
3292
|
-
if (!Number.isSafeInteger(value) || value < 1) {
|
|
3293
|
-
throw new Error(`${name} is invalid`);
|
|
3294
|
-
}
|
|
3295
|
-
return value;
|
|
3296
|
-
}
|
|
3297
|
-
function optionalHostedText(value, name, maxLength) {
|
|
3298
|
-
if (value === void 0 || value === null || value === "") return void 0;
|
|
3299
|
-
if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
3300
|
-
throw new Error(`${name} is invalid`);
|
|
3301
|
-
}
|
|
3302
|
-
return value;
|
|
3303
|
-
}
|
|
3304
|
-
function githubRepositoryName(value) {
|
|
3305
|
-
if (typeof value !== "string" || value.length > 201) {
|
|
3306
|
-
throw new Error("repository must be owner/name");
|
|
3307
|
-
}
|
|
3308
|
-
const parts = value.split("/");
|
|
3309
|
-
const owner = parts[0] ?? "";
|
|
3310
|
-
const name = (parts[1] ?? "").replace(/\.git$/i, "");
|
|
3311
|
-
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 === "..") {
|
|
3312
|
-
throw new Error("repository must be owner/name");
|
|
3313
|
-
}
|
|
3314
|
-
return `${owner}/${name}`;
|
|
3315
|
-
}
|
|
3316
|
-
function trustedGitHubInstallUrl(value) {
|
|
3317
|
-
let url;
|
|
3318
|
-
try {
|
|
3319
|
-
url = new URL(value);
|
|
3320
|
-
} catch {
|
|
3321
|
-
throw new Error("odla.ai returned an invalid GitHub installation URL");
|
|
3322
|
-
}
|
|
3323
|
-
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
|
|
3324
|
-
throw new Error("odla.ai returned an untrusted GitHub installation URL");
|
|
3325
|
-
}
|
|
3326
|
-
return url.toString();
|
|
3327
|
-
}
|
|
3328
|
-
function hostedPollInterval(value = 2e3) {
|
|
3329
|
-
if (!Number.isSafeInteger(value) || value < 100 || value > 3e4) {
|
|
3330
|
-
throw new Error("poll interval must be 100-30000ms");
|
|
3331
|
-
}
|
|
3332
|
-
return value;
|
|
3333
|
-
}
|
|
3334
|
-
function hostedPollTimeout(value = 10 * 6e4) {
|
|
3335
|
-
if (!Number.isSafeInteger(value) || value < 1e3 || value > 60 * 6e4) {
|
|
3336
|
-
throw new Error("poll timeout must be 1000-3600000ms");
|
|
3337
|
-
}
|
|
3338
|
-
return value;
|
|
3339
|
-
}
|
|
3340
|
-
async function waitForHostedPoll(milliseconds, signal) {
|
|
3341
|
-
if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
|
|
3342
|
-
await new Promise((resolve7, reject) => {
|
|
3343
|
-
const timer = setTimeout(resolve7, milliseconds);
|
|
3344
|
-
signal?.addEventListener("abort", () => {
|
|
3345
|
-
clearTimeout(timer);
|
|
3346
|
-
reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
3347
|
-
}, { once: true });
|
|
3348
|
-
});
|
|
3349
|
-
}
|
|
3350
|
-
function isValidHostedSecurityPlan(value, env) {
|
|
3351
|
-
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;
|
|
3352
|
-
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;
|
|
3353
|
-
return validRoute(value.routes?.discovery, "security.discovery") && validRoute(value.routes?.validation, "security.validation");
|
|
3354
|
-
}
|
|
3355
|
-
function hostedSecurityCredential(value) {
|
|
3356
|
-
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
3357
|
-
throw new Error("Hosted security requires an injected odla developer token");
|
|
3358
|
-
}
|
|
3359
|
-
return value;
|
|
3360
|
-
}
|
|
3361
|
-
|
|
3362
|
-
// src/security-hosted-github.ts
|
|
3363
|
-
async function connectGitHubSecuritySource(options) {
|
|
3364
|
-
const out = options.stdout ?? console;
|
|
3365
|
-
const repository = githubRepositoryName(options.repository);
|
|
3366
|
-
const attempt = await requestHostedSecurityJson(
|
|
3367
|
-
options,
|
|
3368
|
-
"/registry/github/connect",
|
|
3369
|
-
{
|
|
3370
|
-
method: "POST",
|
|
3371
|
-
body: JSON.stringify({
|
|
3372
|
-
appId: hostedIdentifier(options.appId, "appId"),
|
|
3373
|
-
env: hostedIdentifier(options.env, "env"),
|
|
3374
|
-
repository
|
|
3375
|
-
})
|
|
3376
|
-
},
|
|
3377
|
-
"start GitHub connection"
|
|
3378
|
-
);
|
|
3379
|
-
const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
|
|
3380
|
-
if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
|
|
3381
|
-
throw new Error("odla.ai returned an invalid GitHub connection attempt");
|
|
3382
|
-
}
|
|
3383
|
-
const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
|
|
3384
|
-
out.log(`GitHub approval: ${installUrl}`);
|
|
3385
|
-
if (options.open !== false) {
|
|
3386
|
-
await (options.openInstallUrl ?? openUrl)(installUrl);
|
|
3387
|
-
out.log("github: opened installation approval in your browser");
|
|
3388
|
-
}
|
|
3389
|
-
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
3390
|
-
const now = options.now ?? Date.now;
|
|
3391
|
-
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
3392
|
-
const wait = options.wait ?? waitForHostedPoll;
|
|
3393
|
-
while (true) {
|
|
3394
|
-
const state = await requestHostedSecurityJson(
|
|
3395
|
-
options,
|
|
3396
|
-
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
3397
|
-
{},
|
|
3398
|
-
"check GitHub connection"
|
|
3399
|
-
);
|
|
3400
|
-
if (state.status !== "pending") {
|
|
3401
|
-
if (state.status === "connected") return state;
|
|
3402
|
-
throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
|
|
3403
|
-
}
|
|
3404
|
-
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
3405
|
-
await wait(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
async function listGitHubSecuritySources(options) {
|
|
3409
|
-
const appId = hostedIdentifier(options.appId, "appId");
|
|
3410
|
-
const env = hostedIdentifier(options.env, "env");
|
|
3411
|
-
const body = await requestHostedSecurityJson(
|
|
3412
|
-
options,
|
|
3413
|
-
`/registry/apps/${encodeURIComponent(appId)}/github/sources?env=${encodeURIComponent(env)}`,
|
|
3414
|
-
{},
|
|
3415
|
-
"list GitHub security sources"
|
|
3416
|
-
);
|
|
3417
|
-
return Array.isArray(body.sources) ? body.sources : [];
|
|
3418
|
-
}
|
|
3419
|
-
async function disconnectGitHubSecuritySource(options) {
|
|
3420
|
-
const appId = hostedIdentifier(options.appId, "appId");
|
|
3421
|
-
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
3422
|
-
await requestHostedSecurityJson(
|
|
3423
|
-
options,
|
|
3424
|
-
`/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
|
|
3425
|
-
{ method: "DELETE" },
|
|
3426
|
-
"disconnect GitHub security source"
|
|
3427
|
-
);
|
|
3428
|
-
}
|
|
3429
|
-
function repositoryFromGitRemote(remoteInput) {
|
|
3430
|
-
const remote = remoteInput.trim();
|
|
3431
|
-
const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
|
|
3432
|
-
if (scp) return githubRepositoryName(`${scp[1]}/${scp[2].replace(/\.git$/, "")}`);
|
|
3433
|
-
let url;
|
|
3434
|
-
try {
|
|
3435
|
-
url = new URL(remote);
|
|
3436
|
-
} catch {
|
|
3437
|
-
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
3438
|
-
}
|
|
3439
|
-
if (url.hostname !== "github.com" || url.protocol !== "https:" && url.protocol !== "ssh:") {
|
|
3440
|
-
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
3441
|
-
}
|
|
3442
|
-
if (url.password || url.protocol === "https:" && url.username || url.search || url.hash) {
|
|
3443
|
-
throw new Error("credential-bearing GitHub origin URLs are not accepted");
|
|
3444
|
-
}
|
|
3445
|
-
const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
3446
|
-
if (parts.length !== 2) {
|
|
3447
|
-
throw new Error("origin must identify one GitHub owner/name repository");
|
|
3448
|
-
}
|
|
3449
|
-
return githubRepositoryName(`${parts[0]}/${parts[1].replace(/\.git$/, "")}`);
|
|
3450
|
-
}
|
|
3451
|
-
async function inferGitHubRepository(cwd = process.cwd(), readOrigin = defaultReadOrigin) {
|
|
3452
|
-
let remote;
|
|
3453
|
-
try {
|
|
3454
|
-
remote = await readOrigin(cwd);
|
|
3455
|
-
} catch {
|
|
3456
|
-
throw new Error("Could not read git origin; pass --repo owner/name");
|
|
3457
|
-
}
|
|
3458
|
-
return repositoryFromGitRemote(remote);
|
|
3459
|
-
}
|
|
3460
|
-
async function defaultReadOrigin(cwd) {
|
|
3461
|
-
const result = await (0, import_node_util2.promisify)(import_node_child_process4.execFile)(
|
|
3462
|
-
"git",
|
|
3463
|
-
["remote", "get-url", "origin"],
|
|
3464
|
-
{ cwd, encoding: "utf8" }
|
|
3465
|
-
);
|
|
3466
|
-
return result.stdout;
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
6223
|
// src/security-hosted-intent.ts
|
|
3470
6224
|
async function getHostedSecurityIntent(options) {
|
|
3471
6225
|
const appId = hostedIdentifier(options.appId, "appId");
|
|
@@ -3475,19 +6229,19 @@ async function getHostedSecurityIntent(options) {
|
|
|
3475
6229
|
const query = new URLSearchParams({ env, sourceId });
|
|
3476
6230
|
if (ref) query.set("ref", ref);
|
|
3477
6231
|
if (options.profile) query.set("profile", options.profile);
|
|
3478
|
-
const
|
|
6232
|
+
const response2 = await requestHostedSecurityJson(
|
|
3479
6233
|
options,
|
|
3480
6234
|
`/registry/apps/${encodeURIComponent(appId)}/security/intent?${query.toString()}`,
|
|
3481
6235
|
{},
|
|
3482
6236
|
"read hosted security execution intent"
|
|
3483
6237
|
);
|
|
3484
|
-
if (!isValidHostedSecurityPlan(
|
|
6238
|
+
if (!isValidHostedSecurityPlan(response2.plan, env) || !isValidIntent(response2.intent, { appId, env, sourceId })) {
|
|
3485
6239
|
throw new Error("odla.ai returned an invalid hosted security execution intent");
|
|
3486
6240
|
}
|
|
3487
|
-
if (
|
|
6241
|
+
if (response2.intent.planDigest !== response2.plan.planDigest) {
|
|
3488
6242
|
throw new Error("odla.ai returned a hosted security intent for a different processing plan");
|
|
3489
6243
|
}
|
|
3490
|
-
return
|
|
6244
|
+
return response2;
|
|
3491
6245
|
}
|
|
3492
6246
|
function isValidIntent(value, expected) {
|
|
3493
6247
|
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";
|
|
@@ -3550,8 +6304,8 @@ async function startHostedSecurityJob(options) {
|
|
|
3550
6304
|
"start hosted security job"
|
|
3551
6305
|
);
|
|
3552
6306
|
} catch (error) {
|
|
3553
|
-
const
|
|
3554
|
-
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(
|
|
6307
|
+
const message3 = error instanceof Error ? error.message : String(error);
|
|
6308
|
+
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(message3)) {
|
|
3555
6309
|
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 });
|
|
3556
6310
|
}
|
|
3557
6311
|
throw error;
|
|
@@ -3575,7 +6329,7 @@ async function followHostedSecurityJob(options) {
|
|
|
3575
6329
|
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
3576
6330
|
const now = options.now ?? Date.now;
|
|
3577
6331
|
const deadline = now() + hostedPollTimeout(options.pollTimeoutMs ?? 60 * 6e4);
|
|
3578
|
-
const
|
|
6332
|
+
const wait2 = options.wait ?? waitForHostedPoll;
|
|
3579
6333
|
let prior = "";
|
|
3580
6334
|
while (true) {
|
|
3581
6335
|
const job = await getHostedSecurityJob(options);
|
|
@@ -3586,7 +6340,7 @@ async function followHostedSecurityJob(options) {
|
|
|
3586
6340
|
if (now() >= deadline) {
|
|
3587
6341
|
throw new Error(`Hosted security job ${job.jobId} did not finish before the polling deadline`);
|
|
3588
6342
|
}
|
|
3589
|
-
await
|
|
6343
|
+
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
3590
6344
|
}
|
|
3591
6345
|
}
|
|
3592
6346
|
async function getHostedSecurityReport(options) {
|
|
@@ -3695,13 +6449,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
3695
6449
|
}
|
|
3696
6450
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
3697
6451
|
}
|
|
3698
|
-
const
|
|
6452
|
+
const report2 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
3699
6453
|
if (parsed.options.json === true) {
|
|
3700
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report }, null, 2));
|
|
6454
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report2 }, null, 2));
|
|
3701
6455
|
} else {
|
|
3702
|
-
printHostedReport(context.stdout,
|
|
6456
|
+
printHostedReport(context.stdout, report2);
|
|
3703
6457
|
}
|
|
3704
|
-
enforceHostedReportGate(
|
|
6458
|
+
enforceHostedReportGate(report2, parsed, context.stdout, parsed.options.json !== true);
|
|
3705
6459
|
}
|
|
3706
6460
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
3707
6461
|
if (parsed.options.source === true) {
|
|
@@ -3768,13 +6522,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
3768
6522
|
});
|
|
3769
6523
|
enforceLocalGate(result.report, parsed);
|
|
3770
6524
|
}
|
|
3771
|
-
function enforceLocalGate(
|
|
6525
|
+
function enforceLocalGate(report2, parsed) {
|
|
3772
6526
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
3773
6527
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
3774
6528
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
3775
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
3776
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
3777
|
-
const incomplete =
|
|
6529
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report2, failOn);
|
|
6530
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report2, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
6531
|
+
const incomplete = report2.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
3778
6532
|
if (confirmed.length || leads.length || incomplete) {
|
|
3779
6533
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
3780
6534
|
}
|
|
@@ -3813,9 +6567,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
3813
6567
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
3814
6568
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
3815
6569
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
3816
|
-
const
|
|
3817
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
3818
|
-
else printHostedReport(context.stdout,
|
|
6570
|
+
const report2 = await getHostedSecurityReport({ ...context, jobId });
|
|
6571
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report2, null, 2));
|
|
6572
|
+
else printHostedReport(context.stdout, report2);
|
|
3819
6573
|
return;
|
|
3820
6574
|
}
|
|
3821
6575
|
if (sub !== "run") {
|
|
@@ -3898,9 +6652,9 @@ async function securityStatus(parsed, dependencies) {
|
|
|
3898
6652
|
}
|
|
3899
6653
|
|
|
3900
6654
|
// src/skill.ts
|
|
3901
|
-
var
|
|
3902
|
-
var
|
|
3903
|
-
var
|
|
6655
|
+
var import_node_fs11 = require("fs");
|
|
6656
|
+
var import_node_os2 = require("os");
|
|
6657
|
+
var import_node_path9 = require("path");
|
|
3904
6658
|
var import_node_url2 = require("url");
|
|
3905
6659
|
|
|
3906
6660
|
// src/skill-adapters.ts
|
|
@@ -3961,8 +6715,8 @@ function installSkill(options = {}) {
|
|
|
3961
6715
|
const files = listFiles(sourceDir);
|
|
3962
6716
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
3963
6717
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
3964
|
-
const root = (0,
|
|
3965
|
-
const home = (0,
|
|
6718
|
+
const root = (0, import_node_path9.resolve)(options.dir ?? process.cwd());
|
|
6719
|
+
const home = (0, import_node_path9.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
|
|
3966
6720
|
const plans = /* @__PURE__ */ new Map();
|
|
3967
6721
|
const targets = /* @__PURE__ */ new Map();
|
|
3968
6722
|
const rememberTarget = (harness, target) => {
|
|
@@ -3976,48 +6730,48 @@ function installSkill(options = {}) {
|
|
|
3976
6730
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
3977
6731
|
};
|
|
3978
6732
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3979
|
-
for (const rel of files) plan((0,
|
|
6733
|
+
for (const rel of files) plan((0, import_node_path9.join)(targetDir2, rel), (0, import_node_fs11.readFileSync)((0, import_node_path9.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
3980
6734
|
};
|
|
3981
6735
|
let targetDir;
|
|
3982
6736
|
if (options.global) {
|
|
3983
|
-
const claudeRoot = (0,
|
|
3984
|
-
const codexRoot = (0,
|
|
6737
|
+
const claudeRoot = (0, import_node_path9.join)(home, ".claude", "skills");
|
|
6738
|
+
const codexRoot = (0, import_node_path9.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path9.join)(home, ".codex"), "skills");
|
|
3985
6739
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
3986
6740
|
for (const harness of harnesses) {
|
|
3987
6741
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
3988
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
6742
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path9.dirname)((0, import_node_path9.dirname)(codexRoot)));
|
|
3989
6743
|
rememberTarget(harness, skillRoot);
|
|
3990
6744
|
}
|
|
3991
6745
|
} else {
|
|
3992
|
-
const sharedRoot = (0,
|
|
6746
|
+
const sharedRoot = (0, import_node_path9.join)(root, ".agents", "skills");
|
|
3993
6747
|
planSkillTree(sharedRoot);
|
|
3994
|
-
const claudeRoot = (0,
|
|
6748
|
+
const claudeRoot = (0, import_node_path9.join)(root, ".claude", "skills");
|
|
3995
6749
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
3996
6750
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
3997
6751
|
if (harnesses.includes("claude")) {
|
|
3998
6752
|
for (const skill of skillNames(files)) {
|
|
3999
|
-
const canonical = (0,
|
|
4000
|
-
plan((0,
|
|
6753
|
+
const canonical = (0, import_node_fs11.readFileSync)((0, import_node_path9.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
6754
|
+
plan((0, import_node_path9.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
4001
6755
|
}
|
|
4002
6756
|
rememberTarget("claude", claudeRoot);
|
|
4003
6757
|
}
|
|
4004
6758
|
if (harnesses.includes("cursor")) {
|
|
4005
|
-
const cursorRule = (0,
|
|
6759
|
+
const cursorRule = (0, import_node_path9.join)(root, ".cursor", "rules", "odla.mdc");
|
|
4006
6760
|
plan(cursorRule, CURSOR_RULE);
|
|
4007
6761
|
rememberTarget("cursor", cursorRule);
|
|
4008
6762
|
}
|
|
4009
6763
|
if (harnesses.includes("agents")) {
|
|
4010
|
-
const agentsFile = (0,
|
|
6764
|
+
const agentsFile = (0, import_node_path9.join)(root, "AGENTS.md");
|
|
4011
6765
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
4012
6766
|
rememberTarget("agents", agentsFile);
|
|
4013
6767
|
}
|
|
4014
6768
|
if (harnesses.includes("copilot")) {
|
|
4015
|
-
const copilotFile = (0,
|
|
6769
|
+
const copilotFile = (0, import_node_path9.join)(root, ".github", "copilot-instructions.md");
|
|
4016
6770
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
4017
6771
|
rememberTarget("copilot", copilotFile);
|
|
4018
6772
|
}
|
|
4019
6773
|
if (harnesses.includes("gemini")) {
|
|
4020
|
-
const geminiFile = (0,
|
|
6774
|
+
const geminiFile = (0, import_node_path9.join)(root, "GEMINI.md");
|
|
4021
6775
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
4022
6776
|
rememberTarget("gemini", geminiFile);
|
|
4023
6777
|
}
|
|
@@ -4031,11 +6785,11 @@ function installSkill(options = {}) {
|
|
|
4031
6785
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
4032
6786
|
continue;
|
|
4033
6787
|
}
|
|
4034
|
-
if (!(0,
|
|
6788
|
+
if (!(0, import_node_fs11.existsSync)(file.target)) {
|
|
4035
6789
|
writtenPaths.add(file.target);
|
|
4036
6790
|
continue;
|
|
4037
6791
|
}
|
|
4038
|
-
const current = (0,
|
|
6792
|
+
const current = (0, import_node_fs11.readFileSync)(file.target, "utf8");
|
|
4039
6793
|
if (current === file.content) {
|
|
4040
6794
|
unchangedPaths.add(file.target);
|
|
4041
6795
|
} else if (file.managedMerge || options.force) {
|
|
@@ -4052,9 +6806,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
4052
6806
|
);
|
|
4053
6807
|
}
|
|
4054
6808
|
for (const file of plans.values()) {
|
|
4055
|
-
if (!(0,
|
|
4056
|
-
(0,
|
|
4057
|
-
(0,
|
|
6809
|
+
if (!(0, import_node_fs11.existsSync)(file.target) || (0, import_node_fs11.readFileSync)(file.target, "utf8") !== file.content) {
|
|
6810
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path9.dirname)(file.target), { recursive: true });
|
|
6811
|
+
(0, import_node_fs11.writeFileSync)(file.target, file.content);
|
|
4058
6812
|
}
|
|
4059
6813
|
}
|
|
4060
6814
|
const skills = skillNames(files);
|
|
@@ -4073,7 +6827,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
4073
6827
|
};
|
|
4074
6828
|
}
|
|
4075
6829
|
function pathsUnder(root, paths) {
|
|
4076
|
-
return [...paths].map((path) => (0,
|
|
6830
|
+
return [...paths].map((path) => (0, import_node_path9.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path9.sep}`) && !(0, import_node_path9.isAbsolute)(path)).sort();
|
|
4077
6831
|
}
|
|
4078
6832
|
function normalizeHarnesses(values, global) {
|
|
4079
6833
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -4095,9 +6849,9 @@ function normalizeHarnesses(values, global) {
|
|
|
4095
6849
|
function managedFileContent(path, block, force, boundary) {
|
|
4096
6850
|
const symlink = symlinkedComponent(boundary, path);
|
|
4097
6851
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
4098
|
-
if (!(0,
|
|
6852
|
+
if (!(0, import_node_fs11.existsSync)(path)) return `${block}
|
|
4099
6853
|
`;
|
|
4100
|
-
const current = (0,
|
|
6854
|
+
const current = (0, import_node_fs11.readFileSync)(path, "utf8");
|
|
4101
6855
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
4102
6856
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
4103
6857
|
const startAt = current.indexOf(start);
|
|
@@ -4118,15 +6872,15 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
4118
6872
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
4119
6873
|
}
|
|
4120
6874
|
function symlinkedComponent(boundary, target) {
|
|
4121
|
-
const rel = (0,
|
|
4122
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
6875
|
+
const rel = (0, import_node_path9.relative)(boundary, target);
|
|
6876
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path9.sep}`) || (0, import_node_path9.isAbsolute)(rel)) {
|
|
4123
6877
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
4124
6878
|
}
|
|
4125
6879
|
let current = boundary;
|
|
4126
|
-
for (const part of rel.split(
|
|
4127
|
-
current = (0,
|
|
6880
|
+
for (const part of rel.split(import_node_path9.sep).filter(Boolean)) {
|
|
6881
|
+
current = (0, import_node_path9.join)(current, part);
|
|
4128
6882
|
try {
|
|
4129
|
-
if ((0,
|
|
6883
|
+
if ((0, import_node_fs11.lstatSync)(current).isSymbolicLink()) return current;
|
|
4130
6884
|
} catch (error) {
|
|
4131
6885
|
if (error.code !== "ENOENT") throw error;
|
|
4132
6886
|
}
|
|
@@ -4137,13 +6891,13 @@ function skillNames(files) {
|
|
|
4137
6891
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
4138
6892
|
}
|
|
4139
6893
|
function listFiles(dir) {
|
|
4140
|
-
if (!(0,
|
|
6894
|
+
if (!(0, import_node_fs11.existsSync)(dir)) return [];
|
|
4141
6895
|
const results = [];
|
|
4142
6896
|
const walk = (current) => {
|
|
4143
|
-
for (const entry of (0,
|
|
4144
|
-
const path = (0,
|
|
6897
|
+
for (const entry of (0, import_node_fs11.readdirSync)(current, { withFileTypes: true })) {
|
|
6898
|
+
const path = (0, import_node_path9.join)(current, entry.name);
|
|
4145
6899
|
if (entry.isDirectory()) walk(path);
|
|
4146
|
-
else results.push((0,
|
|
6900
|
+
else results.push((0, import_node_path9.relative)(dir, path));
|
|
4147
6901
|
}
|
|
4148
6902
|
};
|
|
4149
6903
|
walk(dir);
|
|
@@ -4327,6 +7081,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
4327
7081
|
printCapabilities(parsed.options.json === true);
|
|
4328
7082
|
return;
|
|
4329
7083
|
}
|
|
7084
|
+
if (command === "code") {
|
|
7085
|
+
await codeCommand(parsed, dependencies);
|
|
7086
|
+
return;
|
|
7087
|
+
}
|
|
4330
7088
|
if (command === "admin") {
|
|
4331
7089
|
await adminCommand(parsed);
|
|
4332
7090
|
return;
|