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