@odla-ai/cli 0.7.1 → 0.9.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 +90 -4
- package/dist/bin.cjs +1113 -257
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-643B2AKG.js → chunk-MJYQ7YDH.js} +1075 -195
- package/dist/chunk-MJYQ7YDH.js.map +1 -0
- package/dist/index.cjs +1156 -259
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +188 -26
- package/dist/index.d.ts +188 -26
- package/dist/index.js +33 -3
- package/llms.txt +286 -33
- package/package.json +3 -3
- package/skills/odla/SKILL.md +8 -1
- package/skills/odla/references/build.md +17 -2
- package/skills/odla/references/sdks.md +2 -2
- package/skills/odla-migrate/references/phase-2-db.md +2 -2
- package/skills/odla-migrate/references/phase-3b-user-sync.md +15 -8
- package/skills/odla-migrate/references/phase-5-cutover.md +8 -2
- package/skills/odla-migrate/references/secrets-map.md +16 -2
- package/dist/chunk-643B2AKG.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -35,17 +35,32 @@ __export(index_exports, {
|
|
|
35
35
|
CAPABILITIES: () => CAPABILITIES,
|
|
36
36
|
SYSTEM_AI_PURPOSES: () => SYSTEM_AI_PURPOSES,
|
|
37
37
|
adminAi: () => adminAi,
|
|
38
|
+
connectGitHubSecuritySource: () => connectGitHubSecuritySource,
|
|
39
|
+
disconnectGitHubSecuritySource: () => disconnectGitHubSecuritySource,
|
|
38
40
|
doctor: () => doctor,
|
|
41
|
+
followHostedSecurityJob: () => followHostedSecurityJob,
|
|
42
|
+
getHostedSecurityIntent: () => getHostedSecurityIntent,
|
|
43
|
+
getHostedSecurityJob: () => getHostedSecurityJob,
|
|
44
|
+
getHostedSecurityPlan: () => getHostedSecurityPlan,
|
|
45
|
+
getHostedSecurityReport: () => getHostedSecurityReport,
|
|
39
46
|
getScopedPlatformToken: () => getScopedPlatformToken,
|
|
47
|
+
inferGitHubRepository: () => inferGitHubRepository,
|
|
40
48
|
initProject: () => initProject,
|
|
41
49
|
installSkill: () => installSkill,
|
|
50
|
+
isTerminalHostedSecurityStatus: () => isTerminalHostedSecurityStatus,
|
|
51
|
+
listGitHubSecuritySources: () => listGitHubSecuritySources,
|
|
52
|
+
listHostedSecurityJobs: () => listHostedSecurityJobs,
|
|
42
53
|
printCapabilities: () => printCapabilities,
|
|
43
54
|
provision: () => provision,
|
|
44
55
|
redactSecrets: () => redactSecrets,
|
|
56
|
+
repositoryFromGitRemote: () => repositoryFromGitRemote,
|
|
45
57
|
runCli: () => runCli,
|
|
46
58
|
runHostedSecurity: () => runHostedSecurity,
|
|
47
59
|
secretsPush: () => secretsPush,
|
|
48
|
-
|
|
60
|
+
secretsSet: () => secretsSet,
|
|
61
|
+
secretsSetClerkKey: () => secretsSetClerkKey,
|
|
62
|
+
smoke: () => smoke,
|
|
63
|
+
startHostedSecurityJob: () => startHostedSecurityJob
|
|
49
64
|
});
|
|
50
65
|
module.exports = __toCommonJS(index_exports);
|
|
51
66
|
|
|
@@ -53,11 +68,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
53
68
|
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
54
69
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
55
70
|
|
|
56
|
-
// src/cli.ts
|
|
57
|
-
var import_security2 = require("@odla-ai/security");
|
|
58
|
-
|
|
59
71
|
// src/admin-ai.ts
|
|
60
|
-
var
|
|
72
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
61
73
|
|
|
62
74
|
// src/token.ts
|
|
63
75
|
var import_db = require("@odla-ai/db");
|
|
@@ -287,9 +299,32 @@ function platformAudience(value) {
|
|
|
287
299
|
return url.origin;
|
|
288
300
|
}
|
|
289
301
|
|
|
302
|
+
// src/secret-input.ts
|
|
303
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
304
|
+
var MAX_BYTES = 64 * 1024;
|
|
305
|
+
async function secretInputValue(options, kind = "credential") {
|
|
306
|
+
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
307
|
+
let value;
|
|
308
|
+
if (options.fromEnv) value = import_node_process3.default.env[options.fromEnv];
|
|
309
|
+
else if (options.stdin) value = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
310
|
+
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
311
|
+
value = value?.replace(/[\r\n]+$/, "");
|
|
312
|
+
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : `stdin ${kind} is empty`);
|
|
313
|
+
if (new TextEncoder().encode(value).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
async function readSecretStream(kind, stream = import_node_process3.default.stdin) {
|
|
317
|
+
let value = "";
|
|
318
|
+
for await (const chunk of stream) {
|
|
319
|
+
value += String(chunk);
|
|
320
|
+
if (value.length > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
321
|
+
}
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
|
|
290
325
|
// src/admin-ai-auth.ts
|
|
291
326
|
var import_node_fs2 = require("fs");
|
|
292
|
-
var
|
|
327
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
293
328
|
var import_db2 = require("@odla-ai/db");
|
|
294
329
|
async function getScopedPlatformToken(options) {
|
|
295
330
|
return resolveAdminPlatformToken(options);
|
|
@@ -297,7 +332,7 @@ async function getScopedPlatformToken(options) {
|
|
|
297
332
|
async function resolveAdminPlatformToken(options) {
|
|
298
333
|
const audience = platformAudience(options.platform);
|
|
299
334
|
if (options.token) return options.token;
|
|
300
|
-
const fromEnv =
|
|
335
|
+
const fromEnv = import_node_process4.default.env.ODLA_ADMIN_TOKEN;
|
|
301
336
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
302
337
|
return scopedToken(
|
|
303
338
|
audience,
|
|
@@ -309,7 +344,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
309
344
|
}
|
|
310
345
|
function audienceBoundEnvToken(token, platform) {
|
|
311
346
|
const audience = platformAudience(platform);
|
|
312
|
-
const declared =
|
|
347
|
+
const declared = import_node_process4.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
313
348
|
if (declared) {
|
|
314
349
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
315
350
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -319,7 +354,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
319
354
|
}
|
|
320
355
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
321
356
|
const audience = platformAudience(platform);
|
|
322
|
-
const tokenFile = options.tokenFile ?? `${
|
|
357
|
+
const tokenFile = options.tokenFile ?? `${import_node_process4.default.cwd()}/.odla/admin-token.local.json`;
|
|
323
358
|
const cache = readJsonFile(tokenFile);
|
|
324
359
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
325
360
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
@@ -334,13 +369,13 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
334
369
|
onCode: async ({ userCode, expiresIn }) => {
|
|
335
370
|
const approvalUrl = handshakeUrl(audience, userCode);
|
|
336
371
|
out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
|
|
337
|
-
const shouldOpen = options.open === true || options.open !== false && !
|
|
372
|
+
const shouldOpen = options.open === true || options.open !== false && !import_node_process4.default.env.CI && Boolean(import_node_process4.default.stdin.isTTY && import_node_process4.default.stdout.isTTY);
|
|
338
373
|
if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
339
374
|
}
|
|
340
375
|
});
|
|
341
376
|
const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
|
|
342
377
|
tokens[scope] = { token, expiresAt };
|
|
343
|
-
if ((0, import_node_fs2.existsSync)(`${
|
|
378
|
+
if ((0, import_node_fs2.existsSync)(`${import_node_process4.default.cwd()}/.git`)) ensureGitignore(import_node_process4.default.cwd(), [tokenFile]);
|
|
344
379
|
writePrivateJson(tokenFile, { platform: audience, tokens });
|
|
345
380
|
out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
346
381
|
return token;
|
|
@@ -501,7 +536,7 @@ function isRecord2(value) {
|
|
|
501
536
|
|
|
502
537
|
// src/admin-ai.ts
|
|
503
538
|
async function adminAi(options) {
|
|
504
|
-
const platform = platformAudience(options.platform ??
|
|
539
|
+
const platform = platformAudience(options.platform ?? import_node_process5.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
505
540
|
const doFetch = options.fetch ?? fetch;
|
|
506
541
|
const out = options.stdout ?? console;
|
|
507
542
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -540,7 +575,7 @@ async function adminAi(options) {
|
|
|
540
575
|
}
|
|
541
576
|
if (options.action === "credential-set") {
|
|
542
577
|
const provider = requireProvider(options.credentialProvider);
|
|
543
|
-
const value = await
|
|
578
|
+
const value = await secretInputValue(options);
|
|
544
579
|
const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
|
|
545
580
|
method: "PUT",
|
|
546
581
|
headers,
|
|
@@ -650,25 +685,6 @@ function requireProvider(value) {
|
|
|
650
685
|
}
|
|
651
686
|
return value;
|
|
652
687
|
}
|
|
653
|
-
async function credentialValue(options) {
|
|
654
|
-
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
655
|
-
let value;
|
|
656
|
-
if (options.fromEnv) value = import_node_process4.default.env[options.fromEnv];
|
|
657
|
-
else if (options.stdin) value = await (options.readStdin ?? readStdin)();
|
|
658
|
-
else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
|
|
659
|
-
value = value?.replace(/[\r\n]+$/, "");
|
|
660
|
-
if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
|
|
661
|
-
if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
662
|
-
return value;
|
|
663
|
-
}
|
|
664
|
-
async function readStdin() {
|
|
665
|
-
let value = "";
|
|
666
|
-
for await (const chunk of import_node_process4.default.stdin) {
|
|
667
|
-
value += String(chunk);
|
|
668
|
-
if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
|
|
669
|
-
}
|
|
670
|
-
return value;
|
|
671
|
-
}
|
|
672
688
|
function policyArray(body) {
|
|
673
689
|
if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
|
|
674
690
|
if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
|
|
@@ -773,15 +789,76 @@ function addOption(options, name, value) {
|
|
|
773
789
|
else options[name] = [String(current), String(value)];
|
|
774
790
|
}
|
|
775
791
|
|
|
792
|
+
// src/admin-command.ts
|
|
793
|
+
async function adminCommand(parsed) {
|
|
794
|
+
const area = parsed.positionals[1];
|
|
795
|
+
const action = parsed.positionals[2];
|
|
796
|
+
const credentialSet = action === "credential" && parsed.positionals[3] === "set";
|
|
797
|
+
const credentials = action === "credentials";
|
|
798
|
+
const models = action === "models";
|
|
799
|
+
const usage = action === "usage";
|
|
800
|
+
const audit = action === "audit";
|
|
801
|
+
if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
|
|
802
|
+
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
803
|
+
}
|
|
804
|
+
assertArgs(parsed, [
|
|
805
|
+
"platform",
|
|
806
|
+
"json",
|
|
807
|
+
"open",
|
|
808
|
+
"provider",
|
|
809
|
+
"model",
|
|
810
|
+
"enabled",
|
|
811
|
+
"max-input-bytes",
|
|
812
|
+
"max-output-tokens",
|
|
813
|
+
"max-calls-per-run",
|
|
814
|
+
"from-env",
|
|
815
|
+
"stdin",
|
|
816
|
+
"discovery-provider",
|
|
817
|
+
"discovery-model",
|
|
818
|
+
"validation-provider",
|
|
819
|
+
"validation-model",
|
|
820
|
+
"app-id",
|
|
821
|
+
"env",
|
|
822
|
+
"run-id",
|
|
823
|
+
"limit"
|
|
824
|
+
], credentialSet ? 5 : action === "set" ? 4 : 3);
|
|
825
|
+
await adminAi({
|
|
826
|
+
action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
|
|
827
|
+
purpose: action === "set" ? parsed.positionals[3] : void 0,
|
|
828
|
+
provider: stringOpt(parsed.options.provider),
|
|
829
|
+
model: stringOpt(parsed.options.model),
|
|
830
|
+
enabled: boolOpt(parsed.options.enabled),
|
|
831
|
+
maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
|
|
832
|
+
maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
|
|
833
|
+
maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
|
|
834
|
+
platform: stringOpt(parsed.options.platform),
|
|
835
|
+
json: parsed.options.json === true,
|
|
836
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
837
|
+
credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
|
|
838
|
+
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
839
|
+
stdin: parsed.options.stdin === true,
|
|
840
|
+
discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
|
|
841
|
+
discoveryModel: stringOpt(parsed.options["discovery-model"]),
|
|
842
|
+
validationProvider: stringOpt(parsed.options["validation-provider"]),
|
|
843
|
+
validationModel: stringOpt(parsed.options["validation-model"]),
|
|
844
|
+
appId: stringOpt(parsed.options["app-id"]),
|
|
845
|
+
env: stringOpt(parsed.options.env),
|
|
846
|
+
runId: stringOpt(parsed.options["run-id"]),
|
|
847
|
+
limit: numberOpt(parsed.options.limit, "--limit")
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
776
851
|
// src/capabilities.ts
|
|
777
852
|
var CAPABILITIES = {
|
|
778
853
|
cli: [
|
|
779
854
|
"register the app and enable configured services per environment",
|
|
780
855
|
"issue, persist, and explicitly rotate configured service credentials",
|
|
781
856
|
"write local Worker values and push deployed Worker secrets through Wrangler stdin",
|
|
857
|
+
"store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
|
|
782
858
|
"push db schema/rules and configure platform AI, auth, and deployment links",
|
|
783
859
|
"validate config offline and smoke-test a provisioned db environment",
|
|
784
860
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
861
|
+
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
785
862
|
"let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
|
|
786
863
|
],
|
|
787
864
|
agent: [
|
|
@@ -794,12 +871,14 @@ var CAPABILITIES = {
|
|
|
794
871
|
"consent to production changes with --yes",
|
|
795
872
|
"request destructive credential rotation explicitly",
|
|
796
873
|
"review application semantics, security findings, releases, and merges",
|
|
797
|
-
"approve redacted-source disclosure before hosted security reasoning"
|
|
874
|
+
"approve redacted-source disclosure before hosted security reasoning",
|
|
875
|
+
"approve GitHub App repository access separately from redacted-snippet disclosure"
|
|
798
876
|
],
|
|
799
877
|
studio: [
|
|
800
878
|
"view telemetry and environment state",
|
|
801
879
|
"perform manual credential recovery when the CLI's local shown-once copy is unavailable",
|
|
802
|
-
"configure system AI purposes and view app/environment/run-attributed usage"
|
|
880
|
+
"configure system AI purposes and view app/environment/run-attributed usage",
|
|
881
|
+
"connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
|
|
803
882
|
]
|
|
804
883
|
};
|
|
805
884
|
function printCapabilities(json = false, out = console) {
|
|
@@ -1196,14 +1275,107 @@ async function doctor(options) {
|
|
|
1196
1275
|
}
|
|
1197
1276
|
}
|
|
1198
1277
|
|
|
1199
|
-
// src/
|
|
1278
|
+
// src/help.ts
|
|
1200
1279
|
var import_node_fs6 = require("fs");
|
|
1280
|
+
function cliVersion() {
|
|
1281
|
+
const pkg = JSON.parse((0, import_node_fs6.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
1282
|
+
return pkg.version ?? "unknown";
|
|
1283
|
+
}
|
|
1284
|
+
function printHelp() {
|
|
1285
|
+
console.log(`odla-ai
|
|
1286
|
+
|
|
1287
|
+
Usage:
|
|
1288
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1289
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
|
|
1290
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
1291
|
+
odla-ai capabilities [--json]
|
|
1292
|
+
odla-ai admin ai show [--platform https://odla.ai] [--json]
|
|
1293
|
+
odla-ai admin ai models [--provider <id>] [--json]
|
|
1294
|
+
odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
1295
|
+
odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
|
|
1296
|
+
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
|
|
1297
|
+
odla-ai admin ai credentials [--json]
|
|
1298
|
+
odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
|
|
1299
|
+
odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
1300
|
+
odla-ai admin ai audit [--limit <1-200>] [--json]
|
|
1301
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [--no-open]
|
|
1302
|
+
odla-ai security github disconnect --source <id> [--env dev] [--yes]
|
|
1303
|
+
odla-ai security plan [--env dev] [--json]
|
|
1304
|
+
odla-ai security sources [--env dev] [--json]
|
|
1305
|
+
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
1306
|
+
odla-ai security status <job-id> [--follow] [--json]
|
|
1307
|
+
odla-ai security report <job-id> [--json]
|
|
1308
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
1309
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
1310
|
+
odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
1311
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev]
|
|
1312
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
1313
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
1314
|
+
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
|
|
1315
|
+
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
|
|
1316
|
+
odla-ai version
|
|
1317
|
+
|
|
1318
|
+
Commands:
|
|
1319
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
1320
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
1321
|
+
doctor Validate and summarize the project config without network calls.
|
|
1322
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
1323
|
+
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
1324
|
+
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
1325
|
+
provision Register services, configure them, persist credentials, optionally push secrets.
|
|
1326
|
+
smoke Verify local credentials, public-config, live schema, and db aggregate.
|
|
1327
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
1328
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
1329
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
1330
|
+
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
1331
|
+
reserved Clerk secret key, write-only from stdin or an env var.
|
|
1332
|
+
version Print the CLI version.
|
|
1333
|
+
|
|
1334
|
+
Safety:
|
|
1335
|
+
New projects target dev only. Add prod explicitly to odla.config.mjs and pass
|
|
1336
|
+
--yes to provision it; use --dry-run first to inspect the resolved plan.
|
|
1337
|
+
Provision caches the approved developer token and service credentials under
|
|
1338
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
1339
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
1340
|
+
Provision opens the approval page automatically in interactive terminals;
|
|
1341
|
+
use --open to force browser launch or --no-open to suppress it.
|
|
1342
|
+
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
1343
|
+
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
1344
|
+
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
1345
|
+
plan are required before bounded snippets reach System AI.
|
|
1346
|
+
Run security plan first to inspect the admin-selected providers, models,
|
|
1347
|
+
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
1348
|
+
and digest that binds consent to that exact plan.
|
|
1349
|
+
`);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/harness-options.ts
|
|
1353
|
+
function harnessList(parsed) {
|
|
1354
|
+
const selected = [
|
|
1355
|
+
...harnessOption(parsed.options.agent, "--agent"),
|
|
1356
|
+
...harnessOption(parsed.options.harness, "--harness")
|
|
1357
|
+
];
|
|
1358
|
+
return selected.length ? selected : ["all"];
|
|
1359
|
+
}
|
|
1360
|
+
function harnessOption(value, flag) {
|
|
1361
|
+
if (value === void 0) return [];
|
|
1362
|
+
if (typeof value === "boolean") throw new Error(`${flag} requires a harness name`);
|
|
1363
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
1364
|
+
const parts = raw.flatMap((item) => item.split(","));
|
|
1365
|
+
if (parts.some((item) => !item.trim())) {
|
|
1366
|
+
throw new Error(`${flag} requires a harness name`);
|
|
1367
|
+
}
|
|
1368
|
+
return parts.map((item) => item.trim());
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/init.ts
|
|
1372
|
+
var import_node_fs7 = require("fs");
|
|
1201
1373
|
var import_node_path5 = require("path");
|
|
1202
1374
|
function initProject(options) {
|
|
1203
1375
|
const out = options.stdout ?? console;
|
|
1204
1376
|
const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
|
|
1205
1377
|
const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
1206
|
-
if ((0,
|
|
1378
|
+
if ((0, import_node_fs7.existsSync)(configPath) && !options.force) {
|
|
1207
1379
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
1208
1380
|
}
|
|
1209
1381
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -1212,10 +1384,10 @@ function initProject(options) {
|
|
|
1212
1384
|
const envs = options.envs?.length ? options.envs : ["dev"];
|
|
1213
1385
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
1214
1386
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
1215
|
-
(0,
|
|
1216
|
-
(0,
|
|
1217
|
-
(0,
|
|
1218
|
-
(0,
|
|
1387
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
1388
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
1389
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
|
|
1390
|
+
(0, import_node_fs7.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
1219
1391
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
1220
1392
|
writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
1221
1393
|
ensureGitignore(rootDir);
|
|
@@ -1224,8 +1396,8 @@ function initProject(options) {
|
|
|
1224
1396
|
out.log("updated .gitignore for local odla credentials");
|
|
1225
1397
|
}
|
|
1226
1398
|
function writeIfMissing(path, text) {
|
|
1227
|
-
if ((0,
|
|
1228
|
-
(0,
|
|
1399
|
+
if ((0, import_node_fs7.existsSync)(path)) return;
|
|
1400
|
+
(0, import_node_fs7.writeFileSync)(path, text);
|
|
1229
1401
|
}
|
|
1230
1402
|
function configTemplate(input) {
|
|
1231
1403
|
return `export default {
|
|
@@ -1314,7 +1486,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
1314
1486
|
// src/provision.ts
|
|
1315
1487
|
var import_apps2 = require("@odla-ai/apps");
|
|
1316
1488
|
var import_ai = require("@odla-ai/ai");
|
|
1317
|
-
var
|
|
1489
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
1318
1490
|
|
|
1319
1491
|
// src/provision-credentials.ts
|
|
1320
1492
|
var import_apps = require("@odla-ai/apps");
|
|
@@ -1618,7 +1790,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
1618
1790
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
1619
1791
|
}
|
|
1620
1792
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
1621
|
-
const key =
|
|
1793
|
+
const key = import_node_process6.default.env[cfg.ai.keyEnv];
|
|
1622
1794
|
if (key) {
|
|
1623
1795
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
1624
1796
|
await (0, import_ai.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -1678,6 +1850,219 @@ async function safeText2(res) {
|
|
|
1678
1850
|
}
|
|
1679
1851
|
}
|
|
1680
1852
|
|
|
1853
|
+
// src/secrets-set.ts
|
|
1854
|
+
var import_ai2 = require("@odla-ai/ai");
|
|
1855
|
+
var import_apps3 = require("@odla-ai/apps");
|
|
1856
|
+
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
1857
|
+
async function secretsSet(options) {
|
|
1858
|
+
const name = (options.name ?? "").trim();
|
|
1859
|
+
if (!name) throw new Error('secret name is required, e.g. "odla-ai secrets set clerk_webhook_secret --env dev --stdin"');
|
|
1860
|
+
if (name.startsWith("$")) {
|
|
1861
|
+
throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
|
|
1862
|
+
}
|
|
1863
|
+
const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
|
|
1864
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
1865
|
+
try {
|
|
1866
|
+
await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value);
|
|
1867
|
+
} catch (err) {
|
|
1868
|
+
throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value));
|
|
1869
|
+
}
|
|
1870
|
+
out.log(`${name} stored in the ${tenantId} vault (write-only; the value was never echoed and cannot be read back here)`);
|
|
1871
|
+
}
|
|
1872
|
+
async function secretsSetClerkKey(options) {
|
|
1873
|
+
const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
|
|
1874
|
+
if (!value.startsWith("sk_")) throw new Error("the Clerk secret key must start with sk_ (sk_test_\u2026 or sk_live_\u2026)");
|
|
1875
|
+
if (value.startsWith("sk_test_") && PROD_ENV_NAMES2.has(options.env)) {
|
|
1876
|
+
throw new Error(`refusing to store an sk_test_ Clerk key for "${options.env}" \u2014 use the production instance's sk_live_ key`);
|
|
1877
|
+
}
|
|
1878
|
+
if (value.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
1879
|
+
throw new Error(`refusing to store an sk_live_ Clerk key for "${options.env}" without --yes (live users would sync into a non-prod tenant)`);
|
|
1880
|
+
}
|
|
1881
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
1882
|
+
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
|
|
1883
|
+
method: "POST",
|
|
1884
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1885
|
+
body: JSON.stringify({ value })
|
|
1886
|
+
});
|
|
1887
|
+
if (!res.ok) {
|
|
1888
|
+
const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value);
|
|
1889
|
+
throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
|
|
1890
|
+
}
|
|
1891
|
+
out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
|
|
1892
|
+
}
|
|
1893
|
+
function scrubValue(text, value) {
|
|
1894
|
+
return redactSecrets(text).split(value).join("[value redacted]");
|
|
1895
|
+
}
|
|
1896
|
+
async function resolveVaultWrite(options) {
|
|
1897
|
+
const out = options.stdout ?? console;
|
|
1898
|
+
const doFetch = options.fetch ?? fetch;
|
|
1899
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1900
|
+
if (!cfg.envs.includes(options.env)) {
|
|
1901
|
+
throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
|
|
1902
|
+
}
|
|
1903
|
+
if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
1904
|
+
throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
|
|
1905
|
+
}
|
|
1906
|
+
const value = await secretInputValue(options, "secret");
|
|
1907
|
+
return { cfg, tenantId: (0, import_apps3.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/security-command-context.ts
|
|
1911
|
+
var import_promises = require("readline/promises");
|
|
1912
|
+
async function hostedSecurityContext(parsed, dependencies) {
|
|
1913
|
+
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
1914
|
+
const cfg = await loadProjectConfig(configPath);
|
|
1915
|
+
const env = stringOpt(parsed.options.env) ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
|
|
1916
|
+
if (!env || !cfg.envs.includes(env)) {
|
|
1917
|
+
throw new Error(`env "${env ?? ""}" is not declared in ${configPath}`);
|
|
1918
|
+
}
|
|
1919
|
+
const platform = platformAudience(stringOpt(parsed.options.platform) ?? cfg.platformUrl);
|
|
1920
|
+
if (platformAudience(cfg.platformUrl) !== platform) {
|
|
1921
|
+
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
1922
|
+
}
|
|
1923
|
+
const doFetch = dependencies.fetch ?? fetch;
|
|
1924
|
+
const stdout = dependencies.stdout ?? console;
|
|
1925
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
1926
|
+
const token = await getDeveloperToken(
|
|
1927
|
+
cfg,
|
|
1928
|
+
{ configPath, open, openApprovalUrl: dependencies.openUrl },
|
|
1929
|
+
doFetch,
|
|
1930
|
+
stdout
|
|
1931
|
+
);
|
|
1932
|
+
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
1933
|
+
}
|
|
1934
|
+
async function interactiveConfirmation(message, dependencies) {
|
|
1935
|
+
if (dependencies.confirm) return dependencies.confirm(message);
|
|
1936
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
1937
|
+
const prompt = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
|
|
1938
|
+
try {
|
|
1939
|
+
const answer = await prompt.question(`${message} [y/N] `);
|
|
1940
|
+
return /^y(?:es)?$/i.test(answer.trim());
|
|
1941
|
+
} finally {
|
|
1942
|
+
prompt.close();
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
function requiredSecurityPositional(parsed, index, label) {
|
|
1946
|
+
const value = parsed.positionals[index];
|
|
1947
|
+
if (!value) throw new Error(`${label} is required`);
|
|
1948
|
+
return value;
|
|
1949
|
+
}
|
|
1950
|
+
function securityProfile(value) {
|
|
1951
|
+
if (value === void 0) return void 0;
|
|
1952
|
+
if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
|
|
1953
|
+
throw new Error("--profile must be odla, cloudflare-app, or generic");
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// src/security-command-output.ts
|
|
1957
|
+
function printHostedSecurityPlan(out, plan, appId) {
|
|
1958
|
+
out.log(`Hosted security plan for ${appId}/${plan.env}: ${plan.ready ? "ready" : "not ready"}`);
|
|
1959
|
+
out.log(` plan digest: ${plan.planDigest}`);
|
|
1960
|
+
out.log(` consent/report/redaction: ${plan.consentContract} \xB7 ${plan.reportProjection} \xB7 ${plan.redactionContract}`);
|
|
1961
|
+
out.log(` prompt bundle: ${plan.promptBundle}`);
|
|
1962
|
+
printHostedSecurityPlanRoute(out, "discovery", plan.routes.discovery);
|
|
1963
|
+
printHostedSecurityPlanRoute(out, "validation", plan.routes.validation);
|
|
1964
|
+
out.log(` independent validation: ${plan.independent ? "yes" : "no"}`);
|
|
1965
|
+
out.log(" source disclosure: bounded best-effort credential-pattern-redacted snippets may be sent through odla.ai to the selected providers");
|
|
1966
|
+
out.log(` retained report: model-derived prose and repository-relative paths for up to ${plan.reportRetentionDays} days; treat it as sensitive`);
|
|
1967
|
+
out.log(" target execution: disabled");
|
|
1968
|
+
out.log(` to approve this exact plan, run security run --source <id> --plan-digest ${plan.planDigest} --ack-redacted-source`);
|
|
1969
|
+
}
|
|
1970
|
+
function printHostedSecurityIntent(out, intent) {
|
|
1971
|
+
out.log(`Hosted security execution intent for ${intent.appId}/${intent.env}:`);
|
|
1972
|
+
out.log(` execution digest: ${intent.executionDigest}`);
|
|
1973
|
+
out.log(` contract: ${intent.executionContract}`);
|
|
1974
|
+
out.log(` source: ${intent.repository} (${intent.sourceId})`);
|
|
1975
|
+
out.log(` ref: ${intent.requestedRef ?? `default branch (${intent.defaultBranch})`}`);
|
|
1976
|
+
out.log(` profile: ${intent.profile}`);
|
|
1977
|
+
out.log(` disclosure: ${intent.sourceDisclosure} \xB7 plan ${intent.planDigest}`);
|
|
1978
|
+
}
|
|
1979
|
+
function assertHostedSecurityPlanReady(plan) {
|
|
1980
|
+
const reasons = [];
|
|
1981
|
+
for (const [label, route] of Object.entries(plan.routes)) {
|
|
1982
|
+
if (!route.enabled) reasons.push(`${label} is disabled`);
|
|
1983
|
+
if (!route.credentialReady) reasons.push(`${label} provider credential is unavailable`);
|
|
1984
|
+
}
|
|
1985
|
+
if (!plan.independent) reasons.push("discovery and validation are not independently routed");
|
|
1986
|
+
if (plan.ready && reasons.length === 0) return;
|
|
1987
|
+
if (!plan.ready && reasons.length === 0) reasons.push("the platform marked the plan unavailable");
|
|
1988
|
+
throw new Error(`hosted security is not ready${reasons.length ? `: ${reasons.join("; ")}` : ""}. Ask a platform admin to update System AI security policy or credentials`);
|
|
1989
|
+
}
|
|
1990
|
+
function printHostedJob(out, job, platform, appId) {
|
|
1991
|
+
out.log(`security job ${job.jobId}: ${job.status}`);
|
|
1992
|
+
out.log(` source: ${job.repository} ${job.requestedRef ?? "default"} -> ${job.commitSha ?? "resolving"}`);
|
|
1993
|
+
if (job.routes) {
|
|
1994
|
+
out.log(` discovery: ${routeLabel(job.routes.discovery)}`);
|
|
1995
|
+
out.log(` validation: ${routeLabel(job.routes.validation)}`);
|
|
1996
|
+
} else {
|
|
1997
|
+
out.log(" routes: platform System AI policy (assigned when analysis starts)");
|
|
1998
|
+
}
|
|
1999
|
+
out.log(` disclosure: ${job.sourceDisclosure} snippets \xB7 metadata retained up to ${job.retentionDays} days`);
|
|
2000
|
+
out.log(` consent: plan ${job.planDigest} \xB7 execution ${job.executionDigest} (${job.executionContract})`);
|
|
2001
|
+
if (job.coverageStatus || job.coverage) printHostedCoverage(out, job);
|
|
2002
|
+
if (job.counts) {
|
|
2003
|
+
out.log(` findings: confirmed=${job.counts.confirmed} needs_reproduction=${job.counts.needsReproduction} candidates=${job.counts.candidates}`);
|
|
2004
|
+
}
|
|
2005
|
+
if (job.errorCode) out.log(` failure: ${job.errorCode}`);
|
|
2006
|
+
const url = new URL("/studio", platform);
|
|
2007
|
+
url.searchParams.set("app", appId);
|
|
2008
|
+
url.searchParams.set("env", job.env);
|
|
2009
|
+
url.searchParams.set("tab", "security");
|
|
2010
|
+
url.searchParams.set("job", job.jobId);
|
|
2011
|
+
out.log(` Studio: ${url.toString()}`);
|
|
2012
|
+
}
|
|
2013
|
+
function printHostedReport(out, report) {
|
|
2014
|
+
out.log(`security report ${report.jobId}: ${report.repository}@${report.revision}`);
|
|
2015
|
+
out.log(` coverage: ${report.coverageStatus} cells=${report.metrics.coverageCells} shallow=${report.metrics.shallowCells} blocked=${report.metrics.blockedCells} unscheduled=${report.metrics.unscheduledCells} budget_exhausted=${report.metrics.budgetExhaustedCells}`);
|
|
2016
|
+
out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates} rejected=${report.metrics.rejected}`);
|
|
2017
|
+
out.log(` discovery: ${report.provenance.discovery?.provider ?? "unknown"}/${report.provenance.discovery?.model ?? "unknown"}`);
|
|
2018
|
+
out.log(` validation: ${report.provenance.validation?.provider ?? "unknown"}/${report.provenance.validation?.model ?? "unknown"} independent=${String(report.provenance.independentValidation)}`);
|
|
2019
|
+
for (const finding of report.findings) {
|
|
2020
|
+
const location = finding.locations[0];
|
|
2021
|
+
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
2022
|
+
}
|
|
2023
|
+
for (const limitation of report.limitations) out.log(` limitation: ${limitation}`);
|
|
2024
|
+
}
|
|
2025
|
+
function enforceHostedReportGate(report, parsed, out, emitSuccess) {
|
|
2026
|
+
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
2027
|
+
const candidateValue = parsed.options["fail-on-candidates"];
|
|
2028
|
+
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
2029
|
+
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
2030
|
+
const confirmed = report.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
2031
|
+
const leads = failOnCandidates ? report.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
2032
|
+
const incomplete = report.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
2033
|
+
if (confirmed.length || leads.length || incomplete) {
|
|
2034
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report.coverageStatus}` : ""}`);
|
|
2035
|
+
}
|
|
2036
|
+
if (emitSuccess) {
|
|
2037
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report.coverageStatus}. This is not proof that the application is secure.`);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
function printHostedSecurityPlanRoute(out, label, route) {
|
|
2041
|
+
const readiness = route.enabled && route.credentialReady ? "ready" : [
|
|
2042
|
+
route.enabled ? void 0 : "disabled",
|
|
2043
|
+
route.credentialReady ? void 0 : "credential unavailable"
|
|
2044
|
+
].filter(Boolean).join(", ");
|
|
2045
|
+
out.log(` ${label}: ${route.provider}/${route.model} \xB7 policy v${route.policyVersion} \xB7 ${readiness}`);
|
|
2046
|
+
out.log(` bounds: ${route.maxCallsPerRun} calls/run \xB7 ${route.maxInputBytes} input bytes/call \xB7 ${route.maxOutputTokens} output tokens/call`);
|
|
2047
|
+
}
|
|
2048
|
+
function printHostedCoverage(out, job) {
|
|
2049
|
+
const coverage = job.coverage;
|
|
2050
|
+
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}` : ""}`);
|
|
2051
|
+
}
|
|
2052
|
+
function routeLabel(route) {
|
|
2053
|
+
return `${route.provider}/${route.model}${route.policyVersion ? ` policy v${route.policyVersion}` : ""}`;
|
|
2054
|
+
}
|
|
2055
|
+
var HOSTED_SEVERITIES = ["informational", "low", "medium", "high", "critical"];
|
|
2056
|
+
function hostedSeverity(value, flag) {
|
|
2057
|
+
if (HOSTED_SEVERITIES.includes(value)) {
|
|
2058
|
+
return value;
|
|
2059
|
+
}
|
|
2060
|
+
throw new Error(`${flag} must be informational, low, medium, high, or critical`);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/security-run-command.ts
|
|
2064
|
+
var import_security2 = require("@odla-ai/security");
|
|
2065
|
+
|
|
1681
2066
|
// src/security.ts
|
|
1682
2067
|
var import_node_path6 = require("path");
|
|
1683
2068
|
var import_security = require("@odla-ai/security");
|
|
@@ -1782,60 +2167,657 @@ function formatBudget(usage) {
|
|
|
1782
2167
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
1783
2168
|
}
|
|
1784
2169
|
|
|
1785
|
-
// src/
|
|
1786
|
-
var
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
2170
|
+
// src/security-hosted-github.ts
|
|
2171
|
+
var import_node_child_process4 = require("child_process");
|
|
2172
|
+
var import_node_util = require("util");
|
|
2173
|
+
|
|
2174
|
+
// src/security-hosted-request.ts
|
|
2175
|
+
async function requestHostedSecurityJson(options, path, init, action) {
|
|
2176
|
+
const platform = platformAudience(options.platform);
|
|
2177
|
+
const token = hostedSecurityCredential(options.token);
|
|
2178
|
+
const requestInit = {
|
|
2179
|
+
...init,
|
|
2180
|
+
redirect: "error",
|
|
2181
|
+
credentials: "omit",
|
|
2182
|
+
cache: "no-store",
|
|
2183
|
+
signal: options.signal,
|
|
2184
|
+
headers: {
|
|
2185
|
+
authorization: `Bearer ${token}`,
|
|
2186
|
+
"content-type": "application/json",
|
|
2187
|
+
...init.headers
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
const response = await (options.fetch ?? fetch)(new URL(path, platform), requestInit);
|
|
2191
|
+
const body = await response.json().catch(() => ({}));
|
|
2192
|
+
if (!response.ok) {
|
|
2193
|
+
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
2194
|
+
const message = optionalHostedText(body.error?.message, "error message", 300);
|
|
2195
|
+
throw new Error(`${action} failed (${response.status})${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`);
|
|
2196
|
+
}
|
|
2197
|
+
return body;
|
|
2198
|
+
}
|
|
2199
|
+
function hostedIdentifier(value, name) {
|
|
2200
|
+
const result = optionalHostedText(value, name, 180);
|
|
2201
|
+
if (!result || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(result)) {
|
|
2202
|
+
throw new Error(`${name} is invalid`);
|
|
2203
|
+
}
|
|
2204
|
+
return result;
|
|
2205
|
+
}
|
|
2206
|
+
function positivePolicyVersion(value, name) {
|
|
2207
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
2208
|
+
throw new Error(`${name} is invalid`);
|
|
2209
|
+
}
|
|
2210
|
+
return value;
|
|
2211
|
+
}
|
|
2212
|
+
function optionalHostedText(value, name, maxLength) {
|
|
2213
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
2214
|
+
if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
2215
|
+
throw new Error(`${name} is invalid`);
|
|
2216
|
+
}
|
|
2217
|
+
return value;
|
|
2218
|
+
}
|
|
2219
|
+
function githubRepositoryName(value) {
|
|
2220
|
+
if (typeof value !== "string" || value.length > 201) {
|
|
2221
|
+
throw new Error("repository must be owner/name");
|
|
2222
|
+
}
|
|
2223
|
+
const parts = value.split("/");
|
|
2224
|
+
const owner = parts[0] ?? "";
|
|
2225
|
+
const name = (parts[1] ?? "").replace(/\.git$/i, "");
|
|
2226
|
+
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 === "..") {
|
|
2227
|
+
throw new Error("repository must be owner/name");
|
|
2228
|
+
}
|
|
2229
|
+
return `${owner}/${name}`;
|
|
2230
|
+
}
|
|
2231
|
+
function trustedGitHubInstallUrl(value) {
|
|
2232
|
+
let url;
|
|
2233
|
+
try {
|
|
2234
|
+
url = new URL(value);
|
|
2235
|
+
} catch {
|
|
2236
|
+
throw new Error("odla.ai returned an invalid GitHub installation URL");
|
|
2237
|
+
}
|
|
2238
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
|
|
2239
|
+
throw new Error("odla.ai returned an untrusted GitHub installation URL");
|
|
2240
|
+
}
|
|
2241
|
+
return url.toString();
|
|
2242
|
+
}
|
|
2243
|
+
function hostedPollInterval(value = 2e3) {
|
|
2244
|
+
if (!Number.isSafeInteger(value) || value < 100 || value > 3e4) {
|
|
2245
|
+
throw new Error("poll interval must be 100-30000ms");
|
|
2246
|
+
}
|
|
2247
|
+
return value;
|
|
2248
|
+
}
|
|
2249
|
+
function hostedPollTimeout(value = 10 * 6e4) {
|
|
2250
|
+
if (!Number.isSafeInteger(value) || value < 1e3 || value > 60 * 6e4) {
|
|
2251
|
+
throw new Error("poll timeout must be 1000-3600000ms");
|
|
2252
|
+
}
|
|
2253
|
+
return value;
|
|
2254
|
+
}
|
|
2255
|
+
async function waitForHostedPoll(milliseconds, signal) {
|
|
2256
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
|
|
2257
|
+
await new Promise((resolve7, reject) => {
|
|
2258
|
+
const timer = setTimeout(resolve7, milliseconds);
|
|
2259
|
+
signal?.addEventListener("abort", () => {
|
|
2260
|
+
clearTimeout(timer);
|
|
2261
|
+
reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
2262
|
+
}, { once: true });
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
function isValidHostedSecurityPlan(value, env) {
|
|
2266
|
+
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;
|
|
2267
|
+
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;
|
|
2268
|
+
return validRoute(value.routes?.discovery, "security.discovery") && validRoute(value.routes?.validation, "security.validation");
|
|
2269
|
+
}
|
|
2270
|
+
function hostedSecurityCredential(value) {
|
|
2271
|
+
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
2272
|
+
throw new Error("Hosted security requires an injected odla developer token");
|
|
2273
|
+
}
|
|
2274
|
+
return value;
|
|
1790
2275
|
}
|
|
1791
|
-
function printHelp() {
|
|
1792
|
-
console.log(`odla-ai
|
|
1793
2276
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2277
|
+
// src/security-hosted-github.ts
|
|
2278
|
+
async function connectGitHubSecuritySource(options) {
|
|
2279
|
+
const out = options.stdout ?? console;
|
|
2280
|
+
const repository = githubRepositoryName(options.repository);
|
|
2281
|
+
const attempt = await requestHostedSecurityJson(
|
|
2282
|
+
options,
|
|
2283
|
+
"/registry/github/connect",
|
|
2284
|
+
{
|
|
2285
|
+
method: "POST",
|
|
2286
|
+
body: JSON.stringify({
|
|
2287
|
+
appId: hostedIdentifier(options.appId, "appId"),
|
|
2288
|
+
env: hostedIdentifier(options.env, "env"),
|
|
2289
|
+
repository
|
|
2290
|
+
})
|
|
2291
|
+
},
|
|
2292
|
+
"start GitHub connection"
|
|
2293
|
+
);
|
|
2294
|
+
const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
|
|
2295
|
+
if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
|
|
2296
|
+
throw new Error("odla.ai returned an invalid GitHub connection attempt");
|
|
2297
|
+
}
|
|
2298
|
+
const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
|
|
2299
|
+
out.log(`GitHub approval: ${installUrl}`);
|
|
2300
|
+
if (options.open !== false) {
|
|
2301
|
+
await (options.openInstallUrl ?? openUrl)(installUrl);
|
|
2302
|
+
out.log("github: opened installation approval in your browser");
|
|
2303
|
+
}
|
|
2304
|
+
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
2305
|
+
const now = options.now ?? Date.now;
|
|
2306
|
+
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
2307
|
+
const wait = options.wait ?? waitForHostedPoll;
|
|
2308
|
+
while (true) {
|
|
2309
|
+
const state = await requestHostedSecurityJson(
|
|
2310
|
+
options,
|
|
2311
|
+
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
2312
|
+
{},
|
|
2313
|
+
"check GitHub connection"
|
|
2314
|
+
);
|
|
2315
|
+
if (state.status !== "pending") {
|
|
2316
|
+
if (state.status === "connected") return state;
|
|
2317
|
+
throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
|
|
2318
|
+
}
|
|
2319
|
+
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
2320
|
+
await wait(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
async function listGitHubSecuritySources(options) {
|
|
2324
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
2325
|
+
const env = hostedIdentifier(options.env, "env");
|
|
2326
|
+
const body = await requestHostedSecurityJson(
|
|
2327
|
+
options,
|
|
2328
|
+
`/registry/apps/${encodeURIComponent(appId)}/github/sources?env=${encodeURIComponent(env)}`,
|
|
2329
|
+
{},
|
|
2330
|
+
"list GitHub security sources"
|
|
2331
|
+
);
|
|
2332
|
+
return Array.isArray(body.sources) ? body.sources : [];
|
|
2333
|
+
}
|
|
2334
|
+
async function disconnectGitHubSecuritySource(options) {
|
|
2335
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
2336
|
+
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
2337
|
+
await requestHostedSecurityJson(
|
|
2338
|
+
options,
|
|
2339
|
+
`/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
|
|
2340
|
+
{ method: "DELETE" },
|
|
2341
|
+
"disconnect GitHub security source"
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
function repositoryFromGitRemote(remoteInput) {
|
|
2345
|
+
const remote = remoteInput.trim();
|
|
2346
|
+
const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
|
|
2347
|
+
if (scp) return githubRepositoryName(`${scp[1]}/${scp[2].replace(/\.git$/, "")}`);
|
|
2348
|
+
let url;
|
|
2349
|
+
try {
|
|
2350
|
+
url = new URL(remote);
|
|
2351
|
+
} catch {
|
|
2352
|
+
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
2353
|
+
}
|
|
2354
|
+
if (url.hostname !== "github.com" || url.protocol !== "https:" && url.protocol !== "ssh:") {
|
|
2355
|
+
throw new Error("origin must be a github.com HTTPS or SSH repository URL");
|
|
2356
|
+
}
|
|
2357
|
+
if (url.password || url.protocol === "https:" && url.username || url.search || url.hash) {
|
|
2358
|
+
throw new Error("credential-bearing GitHub origin URLs are not accepted");
|
|
2359
|
+
}
|
|
2360
|
+
const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
2361
|
+
if (parts.length !== 2) {
|
|
2362
|
+
throw new Error("origin must identify one GitHub owner/name repository");
|
|
2363
|
+
}
|
|
2364
|
+
return githubRepositoryName(`${parts[0]}/${parts[1].replace(/\.git$/, "")}`);
|
|
2365
|
+
}
|
|
2366
|
+
async function inferGitHubRepository(cwd = process.cwd(), readOrigin = defaultReadOrigin) {
|
|
2367
|
+
let remote;
|
|
2368
|
+
try {
|
|
2369
|
+
remote = await readOrigin(cwd);
|
|
2370
|
+
} catch {
|
|
2371
|
+
throw new Error("Could not read git origin; pass --repo owner/name");
|
|
2372
|
+
}
|
|
2373
|
+
return repositoryFromGitRemote(remote);
|
|
2374
|
+
}
|
|
2375
|
+
async function defaultReadOrigin(cwd) {
|
|
2376
|
+
const result = await (0, import_node_util.promisify)(import_node_child_process4.execFile)(
|
|
2377
|
+
"git",
|
|
2378
|
+
["remote", "get-url", "origin"],
|
|
2379
|
+
{ cwd, encoding: "utf8" }
|
|
2380
|
+
);
|
|
2381
|
+
return result.stdout;
|
|
2382
|
+
}
|
|
1815
2383
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2384
|
+
// src/security-hosted-intent.ts
|
|
2385
|
+
async function getHostedSecurityIntent(options) {
|
|
2386
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
2387
|
+
const env = hostedIdentifier(options.env, "env");
|
|
2388
|
+
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
2389
|
+
const ref = optionalHostedText(options.ref, "ref", 255);
|
|
2390
|
+
const query = new URLSearchParams({ env, sourceId });
|
|
2391
|
+
if (ref) query.set("ref", ref);
|
|
2392
|
+
if (options.profile) query.set("profile", options.profile);
|
|
2393
|
+
const response = await requestHostedSecurityJson(
|
|
2394
|
+
options,
|
|
2395
|
+
`/registry/apps/${encodeURIComponent(appId)}/security/intent?${query.toString()}`,
|
|
2396
|
+
{},
|
|
2397
|
+
"read hosted security execution intent"
|
|
2398
|
+
);
|
|
2399
|
+
if (!isValidHostedSecurityPlan(response.plan, env) || !isValidIntent(response.intent, { appId, env, sourceId })) {
|
|
2400
|
+
throw new Error("odla.ai returned an invalid hosted security execution intent");
|
|
2401
|
+
}
|
|
2402
|
+
if (response.intent.planDigest !== response.plan.planDigest) {
|
|
2403
|
+
throw new Error("odla.ai returned a hosted security intent for a different processing plan");
|
|
2404
|
+
}
|
|
2405
|
+
return response;
|
|
2406
|
+
}
|
|
2407
|
+
function isValidIntent(value, expected) {
|
|
2408
|
+
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";
|
|
2409
|
+
}
|
|
1829
2410
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2411
|
+
// src/security-hosted-jobs.ts
|
|
2412
|
+
async function getHostedSecurityPlan(options) {
|
|
2413
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
2414
|
+
const env = hostedIdentifier(options.env, "env");
|
|
2415
|
+
const plan = await requestHostedSecurityJson(
|
|
2416
|
+
options,
|
|
2417
|
+
`/registry/apps/${encodeURIComponent(appId)}/security/plan?env=${encodeURIComponent(env)}`,
|
|
2418
|
+
{},
|
|
2419
|
+
"read hosted security disclosure plan"
|
|
2420
|
+
);
|
|
2421
|
+
if (!isValidHostedSecurityPlan(plan, env)) {
|
|
2422
|
+
throw new Error("odla.ai returned an invalid hosted security disclosure plan");
|
|
2423
|
+
}
|
|
2424
|
+
return plan;
|
|
2425
|
+
}
|
|
2426
|
+
async function startHostedSecurityJob(options) {
|
|
2427
|
+
if (options.sourceDisclosureAck !== "redacted") {
|
|
2428
|
+
throw new Error("Hosted GitHub security requires sourceDisclosureAck=redacted; repository access alone is not disclosure consent");
|
|
2429
|
+
}
|
|
2430
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
2431
|
+
const env = hostedIdentifier(options.env, "env");
|
|
2432
|
+
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
2433
|
+
const ref = optionalHostedText(options.ref, "ref", 255);
|
|
2434
|
+
const expectedPlanDigest = securityPlanDigest(options.expectedPlanDigest);
|
|
2435
|
+
const expectedExecutionDigest = securityExecutionDigest(options.expectedExecutionDigest);
|
|
2436
|
+
const expectedPolicyVersions = {
|
|
2437
|
+
discovery: positivePolicyVersion(
|
|
2438
|
+
options.expectedPolicyVersions?.discovery,
|
|
2439
|
+
"expected discovery policy version"
|
|
2440
|
+
),
|
|
2441
|
+
validation: positivePolicyVersion(
|
|
2442
|
+
options.expectedPolicyVersions?.validation,
|
|
2443
|
+
"expected validation policy version"
|
|
2444
|
+
)
|
|
2445
|
+
};
|
|
2446
|
+
let body;
|
|
2447
|
+
try {
|
|
2448
|
+
body = await requestHostedSecurityJson(
|
|
2449
|
+
options,
|
|
2450
|
+
`/registry/apps/${encodeURIComponent(appId)}/security/jobs`,
|
|
2451
|
+
{
|
|
2452
|
+
method: "POST",
|
|
2453
|
+
body: JSON.stringify({
|
|
2454
|
+
env,
|
|
2455
|
+
sourceId,
|
|
2456
|
+
...ref ? { ref } : {},
|
|
2457
|
+
...options.profile ? { profile: options.profile } : {},
|
|
2458
|
+
...options.clientRequestId ? { clientRequestId: hostedIdentifier(options.clientRequestId, "clientRequestId") } : {},
|
|
2459
|
+
sourceDisclosure: "redacted",
|
|
2460
|
+
expectedPlanDigest,
|
|
2461
|
+
expectedExecutionDigest,
|
|
2462
|
+
expectedPolicyVersions
|
|
2463
|
+
})
|
|
2464
|
+
},
|
|
2465
|
+
"start hosted security job"
|
|
2466
|
+
);
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2469
|
+
if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(message)) {
|
|
2470
|
+
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 });
|
|
2471
|
+
}
|
|
2472
|
+
throw error;
|
|
2473
|
+
}
|
|
2474
|
+
if (!body.job?.jobId) {
|
|
2475
|
+
throw new Error("odla.ai returned an invalid hosted security job");
|
|
2476
|
+
}
|
|
2477
|
+
return body.job;
|
|
2478
|
+
}
|
|
2479
|
+
async function getHostedSecurityJob(options) {
|
|
2480
|
+
const body = await requestHostedSecurityJson(
|
|
2481
|
+
options,
|
|
2482
|
+
hostedJobPath(options.appId, options.jobId),
|
|
2483
|
+
{},
|
|
2484
|
+
"read hosted security job"
|
|
2485
|
+
);
|
|
2486
|
+
if (!body.job?.jobId) throw new Error("odla.ai returned an invalid hosted security job");
|
|
2487
|
+
return body.job;
|
|
2488
|
+
}
|
|
2489
|
+
async function listHostedSecurityJobs(options) {
|
|
2490
|
+
const appId = hostedIdentifier(options.appId, "appId");
|
|
2491
|
+
const env = hostedIdentifier(options.env, "env");
|
|
2492
|
+
const body = await requestHostedSecurityJson(
|
|
2493
|
+
options,
|
|
2494
|
+
`/registry/apps/${encodeURIComponent(appId)}/security/jobs?env=${encodeURIComponent(env)}`,
|
|
2495
|
+
{},
|
|
2496
|
+
"list hosted security jobs"
|
|
2497
|
+
);
|
|
2498
|
+
return Array.isArray(body.jobs) ? body.jobs : [];
|
|
2499
|
+
}
|
|
2500
|
+
async function followHostedSecurityJob(options) {
|
|
2501
|
+
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
2502
|
+
const now = options.now ?? Date.now;
|
|
2503
|
+
const deadline = now() + hostedPollTimeout(options.pollTimeoutMs ?? 60 * 6e4);
|
|
2504
|
+
const wait = options.wait ?? waitForHostedPoll;
|
|
2505
|
+
let prior = "";
|
|
2506
|
+
while (true) {
|
|
2507
|
+
const job = await getHostedSecurityJob(options);
|
|
2508
|
+
const fingerprint = `${job.status}:${job.updatedAt}`;
|
|
2509
|
+
if (fingerprint !== prior) options.onUpdate?.(Object.freeze({ ...job }));
|
|
2510
|
+
prior = fingerprint;
|
|
2511
|
+
if (isTerminalHostedSecurityStatus(job.status)) return job;
|
|
2512
|
+
if (now() >= deadline) {
|
|
2513
|
+
throw new Error(`Hosted security job ${job.jobId} did not finish before the polling deadline`);
|
|
2514
|
+
}
|
|
2515
|
+
await wait(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
async function getHostedSecurityReport(options) {
|
|
2519
|
+
return requestHostedSecurityJson(
|
|
2520
|
+
options,
|
|
2521
|
+
`${hostedJobPath(options.appId, options.jobId)}/report`,
|
|
2522
|
+
{},
|
|
2523
|
+
"read hosted security report"
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
function isTerminalHostedSecurityStatus(status) {
|
|
2527
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
2528
|
+
}
|
|
2529
|
+
function hostedJobPath(appIdInput, jobIdInput) {
|
|
2530
|
+
const appId = hostedIdentifier(appIdInput, "appId");
|
|
2531
|
+
const jobId = hostedIdentifier(jobIdInput, "jobId");
|
|
2532
|
+
return `/registry/apps/${encodeURIComponent(appId)}/security/jobs/${encodeURIComponent(jobId)}`;
|
|
2533
|
+
}
|
|
2534
|
+
function securityPlanDigest(value) {
|
|
2535
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(value)) {
|
|
2536
|
+
throw new Error("expected plan digest must be a sha256 digest from security plan");
|
|
2537
|
+
}
|
|
2538
|
+
return value;
|
|
2539
|
+
}
|
|
2540
|
+
function securityExecutionDigest(value) {
|
|
2541
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(value)) {
|
|
2542
|
+
throw new Error("expected execution digest must be a sha256 digest from security intent");
|
|
2543
|
+
}
|
|
2544
|
+
return value;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/security-run-command.ts
|
|
2548
|
+
async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
2549
|
+
assertArgs(parsed, [
|
|
2550
|
+
"config",
|
|
2551
|
+
"env",
|
|
2552
|
+
"profile",
|
|
2553
|
+
"platform",
|
|
2554
|
+
"source",
|
|
2555
|
+
"ref",
|
|
2556
|
+
"follow",
|
|
2557
|
+
"open",
|
|
2558
|
+
"json",
|
|
2559
|
+
"ack-redacted-source",
|
|
2560
|
+
"plan-digest",
|
|
2561
|
+
"fail-on",
|
|
2562
|
+
"fail-on-candidates",
|
|
2563
|
+
"allow-incomplete"
|
|
2564
|
+
], 2);
|
|
2565
|
+
const follow = parsed.options.follow !== false;
|
|
2566
|
+
if (!follow && (parsed.options["fail-on"] !== void 0 || parsed.options["fail-on-candidates"] !== void 0 || parsed.options["allow-incomplete"] !== void 0)) {
|
|
2567
|
+
throw new Error("hosted security gate options require following the job; remove --no-follow");
|
|
2568
|
+
}
|
|
2569
|
+
const acknowledgedDigest = requiredString(parsed.options["plan-digest"], "--plan-digest");
|
|
2570
|
+
const context = await hostedSecurityContext(parsed, dependencies);
|
|
2571
|
+
const plan = await getHostedSecurityPlan(context);
|
|
2572
|
+
if (parsed.options.json !== true) printHostedSecurityPlan(context.stdout, plan, context.appId);
|
|
2573
|
+
assertHostedSecurityPlanReady(plan);
|
|
2574
|
+
if (acknowledgedDigest !== plan.planDigest) {
|
|
2575
|
+
throw new Error(`--plan-digest does not match the current hosted security plan (${plan.planDigest}); review and explicitly acknowledge the current digest`);
|
|
2576
|
+
}
|
|
2577
|
+
const requestedRef = stringOpt(parsed.options.ref);
|
|
2578
|
+
const requestedProfile = securityProfile(stringOpt(parsed.options.profile));
|
|
2579
|
+
const preview = await getHostedSecurityIntent({
|
|
2580
|
+
...context,
|
|
2581
|
+
sourceId,
|
|
2582
|
+
ref: requestedRef,
|
|
2583
|
+
profile: requestedProfile
|
|
2584
|
+
});
|
|
2585
|
+
if (parsed.options.json !== true) printHostedSecurityIntent(context.stdout, preview.intent);
|
|
2586
|
+
if (preview.plan.planDigest !== plan.planDigest || preview.intent.planDigest !== plan.planDigest) {
|
|
2587
|
+
throw new Error(`hosted security plan changed while previewing execution (${preview.plan.planDigest}); review and explicitly acknowledge the current plan digest`);
|
|
2588
|
+
}
|
|
2589
|
+
const job = await startHostedSecurityJob({
|
|
2590
|
+
...context,
|
|
2591
|
+
sourceId,
|
|
2592
|
+
ref: requestedRef,
|
|
2593
|
+
profile: requestedProfile,
|
|
2594
|
+
clientRequestId: crypto.randomUUID(),
|
|
2595
|
+
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
2596
|
+
expectedPlanDigest: plan.planDigest,
|
|
2597
|
+
expectedExecutionDigest: preview.intent.executionDigest,
|
|
2598
|
+
expectedPolicyVersions: {
|
|
2599
|
+
discovery: plan.routes.discovery.policyVersion,
|
|
2600
|
+
validation: plan.routes.validation.policyVersion
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
const result = follow ? await followHostedSecurityJob({
|
|
2604
|
+
...context,
|
|
2605
|
+
jobId: job.jobId,
|
|
2606
|
+
wait: dependencies.pollWait,
|
|
2607
|
+
onUpdate: parsed.options.json === true ? void 0 : (value) => printHostedJob(context.stdout, value, context.platform, context.appId)
|
|
2608
|
+
}) : job;
|
|
2609
|
+
if (!follow) {
|
|
2610
|
+
if (parsed.options.json === true) {
|
|
2611
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result }, null, 2));
|
|
2612
|
+
} else {
|
|
2613
|
+
printHostedJob(context.stdout, result, context.platform, context.appId);
|
|
2614
|
+
}
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (result.status !== "completed") {
|
|
2618
|
+
if (parsed.options.json === true) {
|
|
2619
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result }, null, 2));
|
|
2620
|
+
}
|
|
2621
|
+
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
2622
|
+
}
|
|
2623
|
+
const report = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
2624
|
+
if (parsed.options.json === true) {
|
|
2625
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report }, null, 2));
|
|
2626
|
+
} else {
|
|
2627
|
+
printHostedReport(context.stdout, report);
|
|
2628
|
+
}
|
|
2629
|
+
enforceHostedReportGate(report, parsed, context.stdout, parsed.options.json !== true);
|
|
2630
|
+
}
|
|
2631
|
+
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
2632
|
+
if (parsed.options.source === true) {
|
|
2633
|
+
throw new Error("--source requires a source id; run security sources first");
|
|
2634
|
+
}
|
|
2635
|
+
assertArgs(parsed, [
|
|
2636
|
+
"config",
|
|
2637
|
+
"env",
|
|
2638
|
+
"out",
|
|
2639
|
+
"profile",
|
|
2640
|
+
"max-hunt-tasks",
|
|
2641
|
+
"run-id",
|
|
2642
|
+
"platform",
|
|
2643
|
+
"self",
|
|
2644
|
+
"ack-redacted-source",
|
|
2645
|
+
"open",
|
|
2646
|
+
"fail-on",
|
|
2647
|
+
"fail-on-candidates",
|
|
2648
|
+
"allow-incomplete"
|
|
2649
|
+
], 3);
|
|
2650
|
+
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
2651
|
+
const selfAudit = parsed.options.self === true;
|
|
2652
|
+
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
2653
|
+
const platform = stringOpt(parsed.options.platform);
|
|
2654
|
+
const doFetch = dependencies.fetch ?? fetch;
|
|
2655
|
+
const out = dependencies.stdout ?? console;
|
|
2656
|
+
const result = await runHostedSecurity({
|
|
2657
|
+
configPath,
|
|
2658
|
+
selfAudit,
|
|
2659
|
+
target: parsed.positionals[2],
|
|
2660
|
+
out: stringOpt(parsed.options.out),
|
|
2661
|
+
env: stringOpt(parsed.options.env),
|
|
2662
|
+
profile: securityProfile(stringOpt(parsed.options.profile)),
|
|
2663
|
+
maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
|
|
2664
|
+
runId: stringOpt(parsed.options["run-id"]),
|
|
2665
|
+
platform,
|
|
2666
|
+
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
2667
|
+
fetch: doFetch,
|
|
2668
|
+
stdout: out,
|
|
2669
|
+
getToken: async (request) => {
|
|
2670
|
+
if (request.scope === "platform:security:self") {
|
|
2671
|
+
return getScopedPlatformToken({
|
|
2672
|
+
platform: request.platform,
|
|
2673
|
+
scope: request.scope,
|
|
2674
|
+
open,
|
|
2675
|
+
fetch: doFetch,
|
|
2676
|
+
stdout: out,
|
|
2677
|
+
openApprovalUrl: dependencies.openUrl
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
const cfg = await loadProjectConfig(configPath);
|
|
2681
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
|
|
2682
|
+
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
2683
|
+
}
|
|
2684
|
+
return getDeveloperToken(
|
|
2685
|
+
cfg,
|
|
2686
|
+
{ configPath, open, openApprovalUrl: dependencies.openUrl },
|
|
2687
|
+
doFetch,
|
|
2688
|
+
out
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
enforceLocalGate(result.report, parsed);
|
|
2693
|
+
}
|
|
2694
|
+
function enforceLocalGate(report, parsed) {
|
|
2695
|
+
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
2696
|
+
const candidateValue = parsed.options["fail-on-candidates"];
|
|
2697
|
+
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
2698
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report, failOn);
|
|
2699
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
2700
|
+
const incomplete = report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
2701
|
+
if (confirmed.length || leads.length || incomplete) {
|
|
2702
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
function severityOpt(value, flag) {
|
|
2706
|
+
if (["informational", "low", "medium", "high", "critical"].includes(value)) {
|
|
2707
|
+
return value;
|
|
2708
|
+
}
|
|
2709
|
+
throw new Error(`${flag} must be informational, low, medium, high, or critical`);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// src/security-command.ts
|
|
2713
|
+
async function securityCommand(parsed, dependencies) {
|
|
2714
|
+
const sub = parsed.positionals[1];
|
|
2715
|
+
if (sub === "github") {
|
|
2716
|
+
await githubSecurityCommand(parsed, dependencies);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (sub === "plan") {
|
|
2720
|
+
assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
|
|
2721
|
+
const context = await hostedSecurityContext(parsed, dependencies);
|
|
2722
|
+
const plan = await getHostedSecurityPlan(context);
|
|
2723
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(plan, null, 2));
|
|
2724
|
+
else printHostedSecurityPlan(context.stdout, plan, context.appId);
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (sub === "sources") {
|
|
2728
|
+
await listSecuritySources(parsed, dependencies);
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (sub === "status") {
|
|
2732
|
+
await securityStatus(parsed, dependencies);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
if (sub === "report") {
|
|
2736
|
+
assertArgs(parsed, ["config", "env", "platform", "open", "json"], 3);
|
|
2737
|
+
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
2738
|
+
const context = await hostedSecurityContext(parsed, dependencies);
|
|
2739
|
+
const report = await getHostedSecurityReport({ ...context, jobId });
|
|
2740
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report, null, 2));
|
|
2741
|
+
else printHostedReport(context.stdout, report);
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (sub !== "run") {
|
|
2745
|
+
throw new Error('unknown security command. Try "odla-ai security plan", "security sources", or "security run".');
|
|
2746
|
+
}
|
|
2747
|
+
const sourceId = stringOpt(parsed.options.source);
|
|
2748
|
+
if (sourceId) await runSourceSecurityCommand(parsed, dependencies, sourceId);
|
|
2749
|
+
else await runLocalSecurityCommand(parsed, dependencies);
|
|
2750
|
+
}
|
|
2751
|
+
async function githubSecurityCommand(parsed, dependencies) {
|
|
2752
|
+
const action = parsed.positionals[2];
|
|
2753
|
+
if (action === "disconnect") {
|
|
2754
|
+
assertArgs(parsed, ["config", "env", "platform", "source", "open", "yes"], 3);
|
|
2755
|
+
const context2 = await hostedSecurityContext(parsed, dependencies);
|
|
2756
|
+
const sourceId = requiredString(parsed.options.source, "--source");
|
|
2757
|
+
const confirmed = parsed.options.yes === true || await interactiveConfirmation(
|
|
2758
|
+
`Disconnect GitHub security source ${sourceId} from ${context2.appId}/${context2.env}?`,
|
|
2759
|
+
dependencies
|
|
2760
|
+
);
|
|
2761
|
+
if (!confirmed) {
|
|
2762
|
+
throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
|
|
2763
|
+
}
|
|
2764
|
+
await disconnectGitHubSecuritySource({ ...context2, sourceId });
|
|
2765
|
+
context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
if (action !== "connect") {
|
|
2769
|
+
throw new Error('unknown security github command. Try "odla-ai security github connect".');
|
|
2770
|
+
}
|
|
2771
|
+
assertArgs(parsed, ["config", "env", "platform", "repo", "open"], 3);
|
|
2772
|
+
const context = await hostedSecurityContext(parsed, dependencies);
|
|
2773
|
+
const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin);
|
|
2774
|
+
const connection = await connectGitHubSecuritySource({
|
|
2775
|
+
...context,
|
|
2776
|
+
repository,
|
|
2777
|
+
open: parsed.options.open !== false,
|
|
2778
|
+
openInstallUrl: dependencies.openUrl ?? openUrl,
|
|
2779
|
+
wait: dependencies.pollWait,
|
|
2780
|
+
stdout: context.stdout
|
|
2781
|
+
});
|
|
2782
|
+
context.stdout.log(`github: connected ${connection.repository ?? repository} (${connection.sourceId ?? "source pending"})`);
|
|
2783
|
+
context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
|
|
2784
|
+
}
|
|
2785
|
+
async function listSecuritySources(parsed, dependencies) {
|
|
2786
|
+
assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
|
|
2787
|
+
const context = await hostedSecurityContext(parsed, dependencies);
|
|
2788
|
+
const [plan, sources] = await Promise.all([
|
|
2789
|
+
getHostedSecurityPlan(context),
|
|
2790
|
+
listGitHubSecuritySources(context)
|
|
2791
|
+
]);
|
|
2792
|
+
if (parsed.options.json === true) {
|
|
2793
|
+
context.stdout.log(JSON.stringify({ plan, sources }, null, 2));
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
printHostedSecurityPlan(context.stdout, plan, context.appId);
|
|
2797
|
+
if (!sources.length) {
|
|
2798
|
+
context.stdout.log(`No GitHub security sources connected for ${context.appId}/${context.env}.`);
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
context.stdout.log(`GitHub security sources for ${context.appId}/${context.env}
|
|
2802
|
+
source repository default ref status`);
|
|
2803
|
+
for (const source of sources) {
|
|
2804
|
+
context.stdout.log(`${source.sourceId} ${source.repository} ${source.defaultBranch} ${source.status}`);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
async function securityStatus(parsed, dependencies) {
|
|
2808
|
+
assertArgs(parsed, ["config", "env", "platform", "open", "json", "follow"], 3);
|
|
2809
|
+
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
2810
|
+
const context = await hostedSecurityContext(parsed, dependencies);
|
|
2811
|
+
const job = parsed.options.follow === true ? await followHostedSecurityJob({
|
|
2812
|
+
...context,
|
|
2813
|
+
jobId,
|
|
2814
|
+
wait: dependencies.pollWait,
|
|
2815
|
+
onUpdate: parsed.options.json === true ? void 0 : (value) => printHostedJob(context.stdout, value, context.platform, context.appId)
|
|
2816
|
+
}) : await getHostedSecurityJob({ ...context, jobId });
|
|
2817
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(job, null, 2));
|
|
2818
|
+
else if (parsed.options.follow !== true) {
|
|
2819
|
+
printHostedJob(context.stdout, job, context.platform, context.appId);
|
|
2820
|
+
}
|
|
1839
2821
|
}
|
|
1840
2822
|
|
|
1841
2823
|
// src/skill.ts
|
|
@@ -2173,7 +3155,7 @@ async function safeText3(res) {
|
|
|
2173
3155
|
}
|
|
2174
3156
|
|
|
2175
3157
|
// src/cli.ts
|
|
2176
|
-
async function runCli(argv = process.argv.slice(2)) {
|
|
3158
|
+
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
2177
3159
|
const parsed = parseArgv(argv);
|
|
2178
3160
|
const command = parsed.positionals[0] ?? "help";
|
|
2179
3161
|
if (command === "version" || command === "-v" || parsed.options.version === true) {
|
|
@@ -2187,7 +3169,15 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
2187
3169
|
return;
|
|
2188
3170
|
}
|
|
2189
3171
|
if (command === "init") {
|
|
2190
|
-
assertArgs(parsed, [
|
|
3172
|
+
assertArgs(parsed, [
|
|
3173
|
+
"app-id",
|
|
3174
|
+
"name",
|
|
3175
|
+
"config",
|
|
3176
|
+
"env",
|
|
3177
|
+
"services",
|
|
3178
|
+
"ai-provider",
|
|
3179
|
+
"force"
|
|
3180
|
+
], 1);
|
|
2191
3181
|
initProject({
|
|
2192
3182
|
appId: requiredString(parsed.options["app-id"], "--app-id"),
|
|
2193
3183
|
name: requiredString(parsed.options.name, "--name"),
|
|
@@ -2210,149 +3200,15 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
2210
3200
|
return;
|
|
2211
3201
|
}
|
|
2212
3202
|
if (command === "admin") {
|
|
2213
|
-
|
|
2214
|
-
const action = parsed.positionals[2];
|
|
2215
|
-
const credentialSet = action === "credential" && parsed.positionals[3] === "set";
|
|
2216
|
-
const credentials = action === "credentials";
|
|
2217
|
-
const models = action === "models";
|
|
2218
|
-
const usage = action === "usage";
|
|
2219
|
-
const audit = action === "audit";
|
|
2220
|
-
if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
|
|
2221
|
-
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
2222
|
-
}
|
|
2223
|
-
assertArgs(parsed, [
|
|
2224
|
-
"platform",
|
|
2225
|
-
"json",
|
|
2226
|
-
"open",
|
|
2227
|
-
"provider",
|
|
2228
|
-
"model",
|
|
2229
|
-
"enabled",
|
|
2230
|
-
"max-input-bytes",
|
|
2231
|
-
"max-output-tokens",
|
|
2232
|
-
"max-calls-per-run",
|
|
2233
|
-
"from-env",
|
|
2234
|
-
"stdin",
|
|
2235
|
-
"discovery-provider",
|
|
2236
|
-
"discovery-model",
|
|
2237
|
-
"validation-provider",
|
|
2238
|
-
"validation-model",
|
|
2239
|
-
"app-id",
|
|
2240
|
-
"env",
|
|
2241
|
-
"run-id",
|
|
2242
|
-
"limit"
|
|
2243
|
-
], credentialSet ? 5 : action === "set" ? 4 : 3);
|
|
2244
|
-
await adminAi({
|
|
2245
|
-
action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
|
|
2246
|
-
purpose: action === "set" ? parsed.positionals[3] : void 0,
|
|
2247
|
-
provider: stringOpt(parsed.options.provider),
|
|
2248
|
-
model: stringOpt(parsed.options.model),
|
|
2249
|
-
enabled: boolOpt(parsed.options.enabled),
|
|
2250
|
-
maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
|
|
2251
|
-
maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
|
|
2252
|
-
maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
|
|
2253
|
-
platform: stringOpt(parsed.options.platform),
|
|
2254
|
-
json: parsed.options.json === true,
|
|
2255
|
-
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
2256
|
-
credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
|
|
2257
|
-
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
2258
|
-
stdin: parsed.options.stdin === true,
|
|
2259
|
-
discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
|
|
2260
|
-
discoveryModel: stringOpt(parsed.options["discovery-model"]),
|
|
2261
|
-
validationProvider: stringOpt(parsed.options["validation-provider"]),
|
|
2262
|
-
validationModel: stringOpt(parsed.options["validation-model"]),
|
|
2263
|
-
appId: stringOpt(parsed.options["app-id"]),
|
|
2264
|
-
env: stringOpt(parsed.options.env),
|
|
2265
|
-
runId: stringOpt(parsed.options["run-id"]),
|
|
2266
|
-
limit: numberOpt(parsed.options.limit, "--limit")
|
|
2267
|
-
});
|
|
3203
|
+
await adminCommand(parsed);
|
|
2268
3204
|
return;
|
|
2269
3205
|
}
|
|
2270
3206
|
if (command === "security") {
|
|
2271
|
-
|
|
2272
|
-
if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
|
|
2273
|
-
assertArgs(parsed, [
|
|
2274
|
-
"config",
|
|
2275
|
-
"env",
|
|
2276
|
-
"out",
|
|
2277
|
-
"profile",
|
|
2278
|
-
"max-hunt-tasks",
|
|
2279
|
-
"run-id",
|
|
2280
|
-
"platform",
|
|
2281
|
-
"self",
|
|
2282
|
-
"ack-redacted-source",
|
|
2283
|
-
"open",
|
|
2284
|
-
"fail-on",
|
|
2285
|
-
"fail-on-candidates",
|
|
2286
|
-
"allow-incomplete"
|
|
2287
|
-
], 3);
|
|
2288
|
-
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
2289
|
-
const selfAudit = parsed.options.self === true;
|
|
2290
|
-
const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
|
|
2291
|
-
const platform = stringOpt(parsed.options.platform);
|
|
2292
|
-
const result = await runHostedSecurity({
|
|
2293
|
-
configPath,
|
|
2294
|
-
selfAudit,
|
|
2295
|
-
target: parsed.positionals[2],
|
|
2296
|
-
out: stringOpt(parsed.options.out),
|
|
2297
|
-
env: stringOpt(parsed.options.env),
|
|
2298
|
-
profile: securityProfile(stringOpt(parsed.options.profile)),
|
|
2299
|
-
maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
|
|
2300
|
-
runId: stringOpt(parsed.options["run-id"]),
|
|
2301
|
-
platform,
|
|
2302
|
-
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
2303
|
-
getToken: async (request) => {
|
|
2304
|
-
if (request.scope === "platform:security:self") {
|
|
2305
|
-
return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
|
|
2306
|
-
}
|
|
2307
|
-
const cfg = await loadProjectConfig(configPath);
|
|
2308
|
-
if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
|
|
2309
|
-
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
2310
|
-
}
|
|
2311
|
-
return getDeveloperToken(cfg, { configPath, open }, fetch, console);
|
|
2312
|
-
}
|
|
2313
|
-
});
|
|
2314
|
-
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
2315
|
-
const candidateValue = parsed.options["fail-on-candidates"];
|
|
2316
|
-
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
2317
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(result.report, failOn);
|
|
2318
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
2319
|
-
const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
2320
|
-
if (confirmed.length || leads.length || incomplete) {
|
|
2321
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
2322
|
-
}
|
|
3207
|
+
await securityCommand(parsed, dependencies);
|
|
2323
3208
|
return;
|
|
2324
3209
|
}
|
|
2325
3210
|
if (command === "provision") {
|
|
2326
|
-
|
|
2327
|
-
parsed,
|
|
2328
|
-
[
|
|
2329
|
-
"config",
|
|
2330
|
-
"dry-run",
|
|
2331
|
-
"rotate-keys",
|
|
2332
|
-
"rotate-o11y-token",
|
|
2333
|
-
"push-secrets",
|
|
2334
|
-
"write-credentials",
|
|
2335
|
-
"write-dev-vars",
|
|
2336
|
-
"token",
|
|
2337
|
-
"open",
|
|
2338
|
-
"yes"
|
|
2339
|
-
],
|
|
2340
|
-
1
|
|
2341
|
-
);
|
|
2342
|
-
const writeDevVars2 = parsed.options["write-dev-vars"];
|
|
2343
|
-
const opts = {
|
|
2344
|
-
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
2345
|
-
dryRun: parsed.options["dry-run"] === true,
|
|
2346
|
-
rotateKeys: parsed.options["rotate-keys"] === true,
|
|
2347
|
-
rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
|
|
2348
|
-
pushSecrets: parsed.options["push-secrets"] === true,
|
|
2349
|
-
writeCredentials: parsed.options["write-credentials"] !== false,
|
|
2350
|
-
writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
|
|
2351
|
-
token: stringOpt(parsed.options.token),
|
|
2352
|
-
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
2353
|
-
yes: parsed.options.yes === true
|
|
2354
|
-
};
|
|
2355
|
-
await provision(opts);
|
|
3211
|
+
await provisionCommand(parsed);
|
|
2356
3212
|
return;
|
|
2357
3213
|
}
|
|
2358
3214
|
if (command === "smoke") {
|
|
@@ -2378,7 +3234,9 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
2378
3234
|
}
|
|
2379
3235
|
if (command === "skill") {
|
|
2380
3236
|
const sub = parsed.positionals[1];
|
|
2381
|
-
if (sub !== "install")
|
|
3237
|
+
if (sub !== "install") {
|
|
3238
|
+
throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
|
|
3239
|
+
}
|
|
2382
3240
|
assertArgs(parsed, ["dir", "global", "force", "agent", "harness"], 2);
|
|
2383
3241
|
installSkill({
|
|
2384
3242
|
dir: stringOpt(parsed.options.dir),
|
|
@@ -2390,7 +3248,27 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
2390
3248
|
}
|
|
2391
3249
|
if (command === "secrets") {
|
|
2392
3250
|
const sub = parsed.positionals[1];
|
|
2393
|
-
if (sub
|
|
3251
|
+
if (sub === "set" || sub === "set-clerk-key") {
|
|
3252
|
+
assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
|
|
3253
|
+
const options = {
|
|
3254
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
3255
|
+
name: parsed.positionals[2],
|
|
3256
|
+
env: requiredString(parsed.options.env, "--env"),
|
|
3257
|
+
fromEnv: stringOpt(parsed.options["from-env"]),
|
|
3258
|
+
stdin: parsed.options.stdin === true,
|
|
3259
|
+
token: stringOpt(parsed.options.token),
|
|
3260
|
+
yes: parsed.options.yes === true,
|
|
3261
|
+
fetch: dependencies.fetch,
|
|
3262
|
+
stdout: dependencies.stdout
|
|
3263
|
+
};
|
|
3264
|
+
await (sub === "set" ? secretsSet(options) : secretsSetClerkKey(options));
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
if (sub !== "push") {
|
|
3268
|
+
throw new Error(
|
|
3269
|
+
`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev", "odla-ai secrets set <name> --env dev --stdin", or "odla-ai secrets set-clerk-key --env dev --stdin".`
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
2394
3272
|
assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
|
|
2395
3273
|
await secretsPush({
|
|
2396
3274
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
@@ -2402,29 +3280,33 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
2402
3280
|
}
|
|
2403
3281
|
throw new Error(`unknown command "${command}". Run "odla-ai help".`);
|
|
2404
3282
|
}
|
|
2405
|
-
function
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
];
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
3283
|
+
async function provisionCommand(parsed) {
|
|
3284
|
+
assertArgs(parsed, [
|
|
3285
|
+
"config",
|
|
3286
|
+
"dry-run",
|
|
3287
|
+
"rotate-keys",
|
|
3288
|
+
"rotate-o11y-token",
|
|
3289
|
+
"push-secrets",
|
|
3290
|
+
"write-credentials",
|
|
3291
|
+
"write-dev-vars",
|
|
3292
|
+
"token",
|
|
3293
|
+
"open",
|
|
3294
|
+
"yes"
|
|
3295
|
+
], 1);
|
|
3296
|
+
const writeDevVars2 = parsed.options["write-dev-vars"];
|
|
3297
|
+
const options = {
|
|
3298
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
3299
|
+
dryRun: parsed.options["dry-run"] === true,
|
|
3300
|
+
rotateKeys: parsed.options["rotate-keys"] === true,
|
|
3301
|
+
rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
|
|
3302
|
+
pushSecrets: parsed.options["push-secrets"] === true,
|
|
3303
|
+
writeCredentials: parsed.options["write-credentials"] !== false,
|
|
3304
|
+
writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
|
|
3305
|
+
token: stringOpt(parsed.options.token),
|
|
3306
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
3307
|
+
yes: parsed.options.yes === true
|
|
3308
|
+
};
|
|
3309
|
+
await provision(options);
|
|
2428
3310
|
}
|
|
2429
3311
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2430
3312
|
0 && (module.exports = {
|
|
@@ -2432,16 +3314,31 @@ function harnessOption(value, flag) {
|
|
|
2432
3314
|
CAPABILITIES,
|
|
2433
3315
|
SYSTEM_AI_PURPOSES,
|
|
2434
3316
|
adminAi,
|
|
3317
|
+
connectGitHubSecuritySource,
|
|
3318
|
+
disconnectGitHubSecuritySource,
|
|
2435
3319
|
doctor,
|
|
3320
|
+
followHostedSecurityJob,
|
|
3321
|
+
getHostedSecurityIntent,
|
|
3322
|
+
getHostedSecurityJob,
|
|
3323
|
+
getHostedSecurityPlan,
|
|
3324
|
+
getHostedSecurityReport,
|
|
2436
3325
|
getScopedPlatformToken,
|
|
3326
|
+
inferGitHubRepository,
|
|
2437
3327
|
initProject,
|
|
2438
3328
|
installSkill,
|
|
3329
|
+
isTerminalHostedSecurityStatus,
|
|
3330
|
+
listGitHubSecuritySources,
|
|
3331
|
+
listHostedSecurityJobs,
|
|
2439
3332
|
printCapabilities,
|
|
2440
3333
|
provision,
|
|
2441
3334
|
redactSecrets,
|
|
3335
|
+
repositoryFromGitRemote,
|
|
2442
3336
|
runCli,
|
|
2443
3337
|
runHostedSecurity,
|
|
2444
3338
|
secretsPush,
|
|
2445
|
-
|
|
3339
|
+
secretsSet,
|
|
3340
|
+
secretsSetClerkKey,
|
|
3341
|
+
smoke,
|
|
3342
|
+
startHostedSecurityJob
|
|
2446
3343
|
});
|
|
2447
3344
|
//# sourceMappingURL=index.cjs.map
|