@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.
@@ -297,7 +297,30 @@ function requireSystemAiPurpose(value) {
297
297
  }
298
298
 
299
299
  // src/admin-ai.ts
300
+ import process6 from "process";
301
+
302
+ // src/secret-input.ts
300
303
  import process5 from "process";
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 = process5.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 = process5.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
+ }
301
324
 
302
325
  // src/admin-ai-audit.ts
303
326
  function adminAiAuditQuery(filters) {
@@ -445,7 +468,7 @@ function isRecord2(value) {
445
468
 
446
469
  // src/admin-ai.ts
447
470
  async function adminAi(options) {
448
- const platform = platformAudience(options.platform ?? process5.env.ODLA_PLATFORM ?? "https://odla.ai");
471
+ const platform = platformAudience(options.platform ?? process6.env.ODLA_PLATFORM ?? "https://odla.ai");
449
472
  const doFetch = options.fetch ?? fetch;
450
473
  const out = options.stdout ?? console;
451
474
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -484,7 +507,7 @@ async function adminAi(options) {
484
507
  }
485
508
  if (options.action === "credential-set") {
486
509
  const provider = requireProvider(options.credentialProvider);
487
- const value = await credentialValue(options);
510
+ const value = await secretInputValue(options);
488
511
  const res2 = await doFetch(`${platform}/registry/platform/ai-credentials/${encodeURIComponent(provider)}`, {
489
512
  method: "PUT",
490
513
  headers,
@@ -594,25 +617,6 @@ function requireProvider(value) {
594
617
  }
595
618
  return value;
596
619
  }
597
- async function credentialValue(options) {
598
- if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
599
- let value;
600
- if (options.fromEnv) value = process5.env[options.fromEnv];
601
- else if (options.stdin) value = await (options.readStdin ?? readStdin)();
602
- else throw new Error("credential input required: use --from-env <NAME> or --stdin; values are never accepted as arguments");
603
- value = value?.replace(/[\r\n]+$/, "");
604
- if (!value) throw new Error(options.fromEnv ? `environment variable ${options.fromEnv} is empty or unset` : "stdin credential is empty");
605
- if (new TextEncoder().encode(value).byteLength > 64 * 1024) throw new Error("credential exceeds 64 KiB");
606
- return value;
607
- }
608
- async function readStdin() {
609
- let value = "";
610
- for await (const chunk of process5.stdin) {
611
- value += String(chunk);
612
- if (value.length > 64 * 1024) throw new Error("credential exceeds 64 KiB");
613
- }
614
- return value;
615
- }
616
620
  function policyArray(body) {
617
621
  if (isRecord3(body) && Array.isArray(body.policies)) return body.policies.filter(isRecord3);
618
622
  if (isRecord3(body) && isRecord3(body.policies)) return Object.values(body.policies).filter(isRecord3);
@@ -648,9 +652,11 @@ var CAPABILITIES = {
648
652
  "register the app and enable configured services per environment",
649
653
  "issue, persist, and explicitly rotate configured service credentials",
650
654
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
655
+ "store tenant-vault secrets (including the Clerk webhook secret and reserved Clerk secret key) write-only from stdin or an env var",
651
656
  "push db schema/rules and configure platform AI, auth, and deployment links",
652
657
  "validate config offline and smoke-test a provisioned db environment",
653
658
  "run app-attributed hosted security discovery and independent validation without provider keys",
659
+ "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
654
660
  "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
655
661
  ],
656
662
  agent: [
@@ -663,12 +669,14 @@ var CAPABILITIES = {
663
669
  "consent to production changes with --yes",
664
670
  "request destructive credential rotation explicitly",
665
671
  "review application semantics, security findings, releases, and merges",
666
- "approve redacted-source disclosure before hosted security reasoning"
672
+ "approve redacted-source disclosure before hosted security reasoning",
673
+ "approve GitHub App repository access separately from redacted-snippet disclosure"
667
674
  ],
668
675
  studio: [
669
676
  "view telemetry and environment state",
670
677
  "perform manual credential recovery when the CLI's local shown-once copy is unavailable",
671
- "configure system AI purposes and view app/environment/run-attributed usage"
678
+ "configure system AI purposes and view app/environment/run-attributed usage",
679
+ "connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
672
680
  ]
673
681
  };
674
682
  function printCapabilities(json = false, out = console) {
@@ -1256,7 +1264,7 @@ function assertWranglerConfig(cfg) {
1256
1264
  // src/provision.ts
1257
1265
  import { createAppsClient, tenantIdFor as tenantIdFor2 } from "@odla-ai/apps";
1258
1266
  import { DEFAULT_SECRET_NAMES, putSecret } from "@odla-ai/ai";
1259
- import process6 from "process";
1267
+ import process7 from "process";
1260
1268
 
1261
1269
  // src/provision-credentials.ts
1262
1270
  import { tenantIdFor } from "@odla-ai/apps";
@@ -1487,7 +1495,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
1487
1495
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
1488
1496
  }
1489
1497
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
1490
- const key = process6.env[cfg.ai.keyEnv];
1498
+ const key = process7.env[cfg.ai.keyEnv];
1491
1499
  if (key) {
1492
1500
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
1493
1501
  await putSecret({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -1547,6 +1555,63 @@ async function safeText2(res) {
1547
1555
  }
1548
1556
  }
1549
1557
 
1558
+ // src/secrets-set.ts
1559
+ import { putSecret as putSecret2 } from "@odla-ai/ai";
1560
+ import { tenantIdFor as tenantIdFor3 } from "@odla-ai/apps";
1561
+ var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
1562
+ async function secretsSet(options) {
1563
+ const name = (options.name ?? "").trim();
1564
+ if (!name) throw new Error('secret name is required, e.g. "odla-ai secrets set clerk_webhook_secret --env dev --stdin"');
1565
+ if (name.startsWith("$")) {
1566
+ throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
1567
+ }
1568
+ const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
1569
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
1570
+ try {
1571
+ await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value);
1572
+ } catch (err) {
1573
+ throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value));
1574
+ }
1575
+ out.log(`${name} stored in the ${tenantId} vault (write-only; the value was never echoed and cannot be read back here)`);
1576
+ }
1577
+ async function secretsSetClerkKey(options) {
1578
+ const { cfg, tenantId, value, doFetch, out } = await resolveVaultWrite(options);
1579
+ if (!value.startsWith("sk_")) throw new Error("the Clerk secret key must start with sk_ (sk_test_\u2026 or sk_live_\u2026)");
1580
+ if (value.startsWith("sk_test_") && PROD_ENV_NAMES2.has(options.env)) {
1581
+ throw new Error(`refusing to store an sk_test_ Clerk key for "${options.env}" \u2014 use the production instance's sk_live_ key`);
1582
+ }
1583
+ if (value.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
1584
+ 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)`);
1585
+ }
1586
+ const token = await getDeveloperToken(cfg, options, doFetch, out);
1587
+ const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
1588
+ method: "POST",
1589
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1590
+ body: JSON.stringify({ value })
1591
+ });
1592
+ if (!res.ok) {
1593
+ const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value);
1594
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
1595
+ }
1596
+ out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
1597
+ }
1598
+ function scrubValue(text, value) {
1599
+ return redactSecrets(text).split(value).join("[value redacted]");
1600
+ }
1601
+ async function resolveVaultWrite(options) {
1602
+ const out = options.stdout ?? console;
1603
+ const doFetch = options.fetch ?? fetch;
1604
+ const cfg = await loadProjectConfig(options.configPath);
1605
+ if (!cfg.envs.includes(options.env)) {
1606
+ throw new Error(`env "${options.env}" is not in config envs (${cfg.envs.join(", ")})`);
1607
+ }
1608
+ if (PROD_ENV_NAMES2.has(options.env) && !options.yes) {
1609
+ throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
1610
+ }
1611
+ const value = await secretInputValue(options, "secret");
1612
+ return { cfg, tenantId: tenantIdFor3(cfg.app.id, options.env), value, doFetch, out };
1613
+ }
1614
+
1550
1615
  // src/security.ts
1551
1616
  import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve5, sep } from "path";
1552
1617
  import {
@@ -1658,6 +1723,383 @@ function formatBudget(usage) {
1658
1723
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
1659
1724
  }
1660
1725
 
1726
+ // src/security-hosted-github.ts
1727
+ import { execFile } from "child_process";
1728
+ import { promisify } from "util";
1729
+
1730
+ // src/security-hosted-request.ts
1731
+ async function requestHostedSecurityJson(options, path, init, action) {
1732
+ const platform = platformAudience(options.platform);
1733
+ const token = hostedSecurityCredential(options.token);
1734
+ const requestInit = {
1735
+ ...init,
1736
+ redirect: "error",
1737
+ credentials: "omit",
1738
+ cache: "no-store",
1739
+ signal: options.signal,
1740
+ headers: {
1741
+ authorization: `Bearer ${token}`,
1742
+ "content-type": "application/json",
1743
+ ...init.headers
1744
+ }
1745
+ };
1746
+ const response = await (options.fetch ?? fetch)(new URL(path, platform), requestInit);
1747
+ const body = await response.json().catch(() => ({}));
1748
+ if (!response.ok) {
1749
+ const code = optionalHostedText(body.error?.code, "error code", 128);
1750
+ const message = optionalHostedText(body.error?.message, "error message", 300);
1751
+ throw new Error(`${action} failed (${response.status})${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`);
1752
+ }
1753
+ return body;
1754
+ }
1755
+ function hostedIdentifier(value, name) {
1756
+ const result = optionalHostedText(value, name, 180);
1757
+ if (!result || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(result)) {
1758
+ throw new Error(`${name} is invalid`);
1759
+ }
1760
+ return result;
1761
+ }
1762
+ function positivePolicyVersion(value, name) {
1763
+ if (!Number.isSafeInteger(value) || value < 1) {
1764
+ throw new Error(`${name} is invalid`);
1765
+ }
1766
+ return value;
1767
+ }
1768
+ function optionalHostedText(value, name, maxLength) {
1769
+ if (value === void 0 || value === null || value === "") return void 0;
1770
+ if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
1771
+ throw new Error(`${name} is invalid`);
1772
+ }
1773
+ return value;
1774
+ }
1775
+ function githubRepositoryName(value) {
1776
+ if (typeof value !== "string" || value.length > 201) {
1777
+ throw new Error("repository must be owner/name");
1778
+ }
1779
+ const parts = value.split("/");
1780
+ const owner = parts[0] ?? "";
1781
+ const name = (parts[1] ?? "").replace(/\.git$/i, "");
1782
+ 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 === "..") {
1783
+ throw new Error("repository must be owner/name");
1784
+ }
1785
+ return `${owner}/${name}`;
1786
+ }
1787
+ function trustedGitHubInstallUrl(value) {
1788
+ let url;
1789
+ try {
1790
+ url = new URL(value);
1791
+ } catch {
1792
+ throw new Error("odla.ai returned an invalid GitHub installation URL");
1793
+ }
1794
+ if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
1795
+ throw new Error("odla.ai returned an untrusted GitHub installation URL");
1796
+ }
1797
+ return url.toString();
1798
+ }
1799
+ function hostedPollInterval(value = 2e3) {
1800
+ if (!Number.isSafeInteger(value) || value < 100 || value > 3e4) {
1801
+ throw new Error("poll interval must be 100-30000ms");
1802
+ }
1803
+ return value;
1804
+ }
1805
+ function hostedPollTimeout(value = 10 * 6e4) {
1806
+ if (!Number.isSafeInteger(value) || value < 1e3 || value > 60 * 6e4) {
1807
+ throw new Error("poll timeout must be 1000-3600000ms");
1808
+ }
1809
+ return value;
1810
+ }
1811
+ async function waitForHostedPoll(milliseconds, signal) {
1812
+ if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
1813
+ await new Promise((resolve7, reject) => {
1814
+ const timer = setTimeout(resolve7, milliseconds);
1815
+ signal?.addEventListener("abort", () => {
1816
+ clearTimeout(timer);
1817
+ reject(signal.reason ?? new DOMException("aborted", "AbortError"));
1818
+ }, { once: true });
1819
+ });
1820
+ }
1821
+ function isValidHostedSecurityPlan(value, env) {
1822
+ 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;
1823
+ 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;
1824
+ return validRoute(value.routes?.discovery, "security.discovery") && validRoute(value.routes?.validation, "security.validation");
1825
+ }
1826
+ function hostedSecurityCredential(value) {
1827
+ if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
1828
+ throw new Error("Hosted security requires an injected odla developer token");
1829
+ }
1830
+ return value;
1831
+ }
1832
+
1833
+ // src/security-hosted-github.ts
1834
+ async function connectGitHubSecuritySource(options) {
1835
+ const out = options.stdout ?? console;
1836
+ const repository = githubRepositoryName(options.repository);
1837
+ const attempt = await requestHostedSecurityJson(
1838
+ options,
1839
+ "/registry/github/connect",
1840
+ {
1841
+ method: "POST",
1842
+ body: JSON.stringify({
1843
+ appId: hostedIdentifier(options.appId, "appId"),
1844
+ env: hostedIdentifier(options.env, "env"),
1845
+ repository
1846
+ })
1847
+ },
1848
+ "start GitHub connection"
1849
+ );
1850
+ const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
1851
+ if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
1852
+ throw new Error("odla.ai returned an invalid GitHub connection attempt");
1853
+ }
1854
+ const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
1855
+ out.log(`GitHub approval: ${installUrl}`);
1856
+ if (options.open !== false) {
1857
+ await (options.openInstallUrl ?? openUrl)(installUrl);
1858
+ out.log("github: opened installation approval in your browser");
1859
+ }
1860
+ const interval = hostedPollInterval(options.pollIntervalMs);
1861
+ const now = options.now ?? Date.now;
1862
+ const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
1863
+ const wait = options.wait ?? waitForHostedPoll;
1864
+ while (true) {
1865
+ const state = await requestHostedSecurityJson(
1866
+ options,
1867
+ `/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
1868
+ {},
1869
+ "check GitHub connection"
1870
+ );
1871
+ if (state.status !== "pending") {
1872
+ if (state.status === "connected") return state;
1873
+ throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
1874
+ }
1875
+ if (now() >= deadline) throw new Error("GitHub connection approval timed out");
1876
+ await wait(Math.min(interval, Math.max(0, deadline - now())), options.signal);
1877
+ }
1878
+ }
1879
+ async function listGitHubSecuritySources(options) {
1880
+ const appId = hostedIdentifier(options.appId, "appId");
1881
+ const env = hostedIdentifier(options.env, "env");
1882
+ const body = await requestHostedSecurityJson(
1883
+ options,
1884
+ `/registry/apps/${encodeURIComponent(appId)}/github/sources?env=${encodeURIComponent(env)}`,
1885
+ {},
1886
+ "list GitHub security sources"
1887
+ );
1888
+ return Array.isArray(body.sources) ? body.sources : [];
1889
+ }
1890
+ async function disconnectGitHubSecuritySource(options) {
1891
+ const appId = hostedIdentifier(options.appId, "appId");
1892
+ const sourceId = hostedIdentifier(options.sourceId, "sourceId");
1893
+ await requestHostedSecurityJson(
1894
+ options,
1895
+ `/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
1896
+ { method: "DELETE" },
1897
+ "disconnect GitHub security source"
1898
+ );
1899
+ }
1900
+ function repositoryFromGitRemote(remoteInput) {
1901
+ const remote = remoteInput.trim();
1902
+ const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
1903
+ if (scp) return githubRepositoryName(`${scp[1]}/${scp[2].replace(/\.git$/, "")}`);
1904
+ let url;
1905
+ try {
1906
+ url = new URL(remote);
1907
+ } catch {
1908
+ throw new Error("origin must be a github.com HTTPS or SSH repository URL");
1909
+ }
1910
+ if (url.hostname !== "github.com" || url.protocol !== "https:" && url.protocol !== "ssh:") {
1911
+ throw new Error("origin must be a github.com HTTPS or SSH repository URL");
1912
+ }
1913
+ if (url.password || url.protocol === "https:" && url.username || url.search || url.hash) {
1914
+ throw new Error("credential-bearing GitHub origin URLs are not accepted");
1915
+ }
1916
+ const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
1917
+ if (parts.length !== 2) {
1918
+ throw new Error("origin must identify one GitHub owner/name repository");
1919
+ }
1920
+ return githubRepositoryName(`${parts[0]}/${parts[1].replace(/\.git$/, "")}`);
1921
+ }
1922
+ async function inferGitHubRepository(cwd = process.cwd(), readOrigin = defaultReadOrigin) {
1923
+ let remote;
1924
+ try {
1925
+ remote = await readOrigin(cwd);
1926
+ } catch {
1927
+ throw new Error("Could not read git origin; pass --repo owner/name");
1928
+ }
1929
+ return repositoryFromGitRemote(remote);
1930
+ }
1931
+ async function defaultReadOrigin(cwd) {
1932
+ const result = await promisify(execFile)(
1933
+ "git",
1934
+ ["remote", "get-url", "origin"],
1935
+ { cwd, encoding: "utf8" }
1936
+ );
1937
+ return result.stdout;
1938
+ }
1939
+
1940
+ // src/security-hosted-intent.ts
1941
+ async function getHostedSecurityIntent(options) {
1942
+ const appId = hostedIdentifier(options.appId, "appId");
1943
+ const env = hostedIdentifier(options.env, "env");
1944
+ const sourceId = hostedIdentifier(options.sourceId, "sourceId");
1945
+ const ref = optionalHostedText(options.ref, "ref", 255);
1946
+ const query = new URLSearchParams({ env, sourceId });
1947
+ if (ref) query.set("ref", ref);
1948
+ if (options.profile) query.set("profile", options.profile);
1949
+ const response = await requestHostedSecurityJson(
1950
+ options,
1951
+ `/registry/apps/${encodeURIComponent(appId)}/security/intent?${query.toString()}`,
1952
+ {},
1953
+ "read hosted security execution intent"
1954
+ );
1955
+ if (!isValidHostedSecurityPlan(response.plan, env) || !isValidIntent(response.intent, { appId, env, sourceId })) {
1956
+ throw new Error("odla.ai returned an invalid hosted security execution intent");
1957
+ }
1958
+ if (response.intent.planDigest !== response.plan.planDigest) {
1959
+ throw new Error("odla.ai returned a hosted security intent for a different processing plan");
1960
+ }
1961
+ return response;
1962
+ }
1963
+ function isValidIntent(value, expected) {
1964
+ 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";
1965
+ }
1966
+
1967
+ // src/security-hosted-jobs.ts
1968
+ async function getHostedSecurityPlan(options) {
1969
+ const appId = hostedIdentifier(options.appId, "appId");
1970
+ const env = hostedIdentifier(options.env, "env");
1971
+ const plan = await requestHostedSecurityJson(
1972
+ options,
1973
+ `/registry/apps/${encodeURIComponent(appId)}/security/plan?env=${encodeURIComponent(env)}`,
1974
+ {},
1975
+ "read hosted security disclosure plan"
1976
+ );
1977
+ if (!isValidHostedSecurityPlan(plan, env)) {
1978
+ throw new Error("odla.ai returned an invalid hosted security disclosure plan");
1979
+ }
1980
+ return plan;
1981
+ }
1982
+ async function startHostedSecurityJob(options) {
1983
+ if (options.sourceDisclosureAck !== "redacted") {
1984
+ throw new Error("Hosted GitHub security requires sourceDisclosureAck=redacted; repository access alone is not disclosure consent");
1985
+ }
1986
+ const appId = hostedIdentifier(options.appId, "appId");
1987
+ const env = hostedIdentifier(options.env, "env");
1988
+ const sourceId = hostedIdentifier(options.sourceId, "sourceId");
1989
+ const ref = optionalHostedText(options.ref, "ref", 255);
1990
+ const expectedPlanDigest = securityPlanDigest(options.expectedPlanDigest);
1991
+ const expectedExecutionDigest = securityExecutionDigest(options.expectedExecutionDigest);
1992
+ const expectedPolicyVersions = {
1993
+ discovery: positivePolicyVersion(
1994
+ options.expectedPolicyVersions?.discovery,
1995
+ "expected discovery policy version"
1996
+ ),
1997
+ validation: positivePolicyVersion(
1998
+ options.expectedPolicyVersions?.validation,
1999
+ "expected validation policy version"
2000
+ )
2001
+ };
2002
+ let body;
2003
+ try {
2004
+ body = await requestHostedSecurityJson(
2005
+ options,
2006
+ `/registry/apps/${encodeURIComponent(appId)}/security/jobs`,
2007
+ {
2008
+ method: "POST",
2009
+ body: JSON.stringify({
2010
+ env,
2011
+ sourceId,
2012
+ ...ref ? { ref } : {},
2013
+ ...options.profile ? { profile: options.profile } : {},
2014
+ ...options.clientRequestId ? { clientRequestId: hostedIdentifier(options.clientRequestId, "clientRequestId") } : {},
2015
+ sourceDisclosure: "redacted",
2016
+ expectedPlanDigest,
2017
+ expectedExecutionDigest,
2018
+ expectedPolicyVersions
2019
+ })
2020
+ },
2021
+ "start hosted security job"
2022
+ );
2023
+ } catch (error) {
2024
+ const message = error instanceof Error ? error.message : String(error);
2025
+ if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(message)) {
2026
+ 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 });
2027
+ }
2028
+ throw error;
2029
+ }
2030
+ if (!body.job?.jobId) {
2031
+ throw new Error("odla.ai returned an invalid hosted security job");
2032
+ }
2033
+ return body.job;
2034
+ }
2035
+ async function getHostedSecurityJob(options) {
2036
+ const body = await requestHostedSecurityJson(
2037
+ options,
2038
+ hostedJobPath(options.appId, options.jobId),
2039
+ {},
2040
+ "read hosted security job"
2041
+ );
2042
+ if (!body.job?.jobId) throw new Error("odla.ai returned an invalid hosted security job");
2043
+ return body.job;
2044
+ }
2045
+ async function listHostedSecurityJobs(options) {
2046
+ const appId = hostedIdentifier(options.appId, "appId");
2047
+ const env = hostedIdentifier(options.env, "env");
2048
+ const body = await requestHostedSecurityJson(
2049
+ options,
2050
+ `/registry/apps/${encodeURIComponent(appId)}/security/jobs?env=${encodeURIComponent(env)}`,
2051
+ {},
2052
+ "list hosted security jobs"
2053
+ );
2054
+ return Array.isArray(body.jobs) ? body.jobs : [];
2055
+ }
2056
+ async function followHostedSecurityJob(options) {
2057
+ const interval = hostedPollInterval(options.pollIntervalMs);
2058
+ const now = options.now ?? Date.now;
2059
+ const deadline = now() + hostedPollTimeout(options.pollTimeoutMs ?? 60 * 6e4);
2060
+ const wait = options.wait ?? waitForHostedPoll;
2061
+ let prior = "";
2062
+ while (true) {
2063
+ const job = await getHostedSecurityJob(options);
2064
+ const fingerprint = `${job.status}:${job.updatedAt}`;
2065
+ if (fingerprint !== prior) options.onUpdate?.(Object.freeze({ ...job }));
2066
+ prior = fingerprint;
2067
+ if (isTerminalHostedSecurityStatus(job.status)) return job;
2068
+ if (now() >= deadline) {
2069
+ throw new Error(`Hosted security job ${job.jobId} did not finish before the polling deadline`);
2070
+ }
2071
+ await wait(Math.min(interval, Math.max(0, deadline - now())), options.signal);
2072
+ }
2073
+ }
2074
+ async function getHostedSecurityReport(options) {
2075
+ return requestHostedSecurityJson(
2076
+ options,
2077
+ `${hostedJobPath(options.appId, options.jobId)}/report`,
2078
+ {},
2079
+ "read hosted security report"
2080
+ );
2081
+ }
2082
+ function isTerminalHostedSecurityStatus(status) {
2083
+ return status === "completed" || status === "failed" || status === "cancelled";
2084
+ }
2085
+ function hostedJobPath(appIdInput, jobIdInput) {
2086
+ const appId = hostedIdentifier(appIdInput, "appId");
2087
+ const jobId = hostedIdentifier(jobIdInput, "jobId");
2088
+ return `/registry/apps/${encodeURIComponent(appId)}/security/jobs/${encodeURIComponent(jobId)}`;
2089
+ }
2090
+ function securityPlanDigest(value) {
2091
+ if (!/^sha256:[a-f0-9]{64}$/.test(value)) {
2092
+ throw new Error("expected plan digest must be a sha256 digest from security plan");
2093
+ }
2094
+ return value;
2095
+ }
2096
+ function securityExecutionDigest(value) {
2097
+ if (!/^sha256:[a-f0-9]{64}$/.test(value)) {
2098
+ throw new Error("expected execution digest must be a sha256 digest from security intent");
2099
+ }
2100
+ return value;
2101
+ }
2102
+
1661
2103
  // src/skill.ts
1662
2104
  import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3 } from "fs";
1663
2105
  import { homedir } from "os";
@@ -1992,9 +2434,6 @@ async function safeText3(res) {
1992
2434
  }
1993
2435
  }
1994
2436
 
1995
- // src/cli.ts
1996
- import { findingsAtOrAbove } from "@odla-ai/security";
1997
-
1998
2437
  // src/argv.ts
1999
2438
  function parseArgv(argv) {
2000
2439
  const positionals = [];
@@ -2070,6 +2509,65 @@ function addOption(options, name, value) {
2070
2509
  else options[name] = [String(current), String(value)];
2071
2510
  }
2072
2511
 
2512
+ // src/admin-command.ts
2513
+ async function adminCommand(parsed) {
2514
+ const area = parsed.positionals[1];
2515
+ const action = parsed.positionals[2];
2516
+ const credentialSet = action === "credential" && parsed.positionals[3] === "set";
2517
+ const credentials = action === "credentials";
2518
+ const models = action === "models";
2519
+ const usage = action === "usage";
2520
+ const audit = action === "audit";
2521
+ if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
2522
+ throw new Error('unknown admin command. Try "odla-ai admin ai show".');
2523
+ }
2524
+ assertArgs(parsed, [
2525
+ "platform",
2526
+ "json",
2527
+ "open",
2528
+ "provider",
2529
+ "model",
2530
+ "enabled",
2531
+ "max-input-bytes",
2532
+ "max-output-tokens",
2533
+ "max-calls-per-run",
2534
+ "from-env",
2535
+ "stdin",
2536
+ "discovery-provider",
2537
+ "discovery-model",
2538
+ "validation-provider",
2539
+ "validation-model",
2540
+ "app-id",
2541
+ "env",
2542
+ "run-id",
2543
+ "limit"
2544
+ ], credentialSet ? 5 : action === "set" ? 4 : 3);
2545
+ await adminAi({
2546
+ action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
2547
+ purpose: action === "set" ? parsed.positionals[3] : void 0,
2548
+ provider: stringOpt(parsed.options.provider),
2549
+ model: stringOpt(parsed.options.model),
2550
+ enabled: boolOpt(parsed.options.enabled),
2551
+ maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
2552
+ maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
2553
+ maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
2554
+ platform: stringOpt(parsed.options.platform),
2555
+ json: parsed.options.json === true,
2556
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
2557
+ credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
2558
+ fromEnv: stringOpt(parsed.options["from-env"]),
2559
+ stdin: parsed.options.stdin === true,
2560
+ discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
2561
+ discoveryModel: stringOpt(parsed.options["discovery-model"]),
2562
+ validationProvider: stringOpt(parsed.options["validation-provider"]),
2563
+ validationModel: stringOpt(parsed.options["validation-model"]),
2564
+ appId: stringOpt(parsed.options["app-id"]),
2565
+ env: stringOpt(parsed.options.env),
2566
+ runId: stringOpt(parsed.options["run-id"]),
2567
+ limit: numberOpt(parsed.options.limit, "--limit")
2568
+ });
2569
+ }
2570
+
2073
2571
  // src/help.ts
2074
2572
  import { readFileSync as readFileSync6 } from "fs";
2075
2573
  function cliVersion() {
@@ -2093,12 +2591,21 @@ Usage:
2093
2591
  odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
2094
2592
  odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
2095
2593
  odla-ai admin ai audit [--limit <1-200>] [--json]
2594
+ odla-ai security github connect [--repo owner/name] [--env dev] [--no-open]
2595
+ odla-ai security github disconnect --source <id> [--env dev] [--yes]
2596
+ odla-ai security plan [--env dev] [--json]
2597
+ odla-ai security sources [--env dev] [--json]
2598
+ odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
2599
+ odla-ai security status <job-id> [--follow] [--json]
2600
+ odla-ai security report <job-id> [--json]
2096
2601
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
2097
2602
  odla-ai security run [target] --self --ack-redacted-source
2098
2603
  odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
2099
2604
  odla-ai smoke [--config odla.config.mjs] [--env dev]
2100
2605
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
2101
2606
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
2607
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
2608
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
2102
2609
  odla-ai version
2103
2610
 
2104
2611
  Commands:
@@ -2107,12 +2614,14 @@ Commands:
2107
2614
  doctor Validate and summarize the project config without network calls.
2108
2615
  capabilities Show what the CLI automates vs agent edits and human checkpoints.
2109
2616
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
2110
- security Run hosted discovery + independent validation with app/run attribution.
2617
+ security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
2111
2618
  provision Register services, configure them, persist credentials, optionally push secrets.
2112
2619
  smoke Verify local credentials, public-config, live schema, and db aggregate.
2113
2620
  skill Same installer; --agent accepts all, claude, codex, cursor,
2114
2621
  copilot, gemini, or agents (repeatable or comma-separated).
2115
- secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
2622
+ secrets Push configured db/o11y secrets into the Worker via wrangler
2623
+ stdin; set stores a tenant-vault secret and set-clerk-key the
2624
+ reserved Clerk secret key, write-only from stdin or an env var.
2116
2625
  version Print the CLI version.
2117
2626
 
2118
2627
  Safety:
@@ -2123,11 +2632,467 @@ Safety:
2123
2632
  preflights Wrangler before any shown-once issuance or destructive rotation.
2124
2633
  Provision opens the approval page automatically in interactive terminals;
2125
2634
  use --open to force browser launch or --no-open to suppress it.
2635
+ GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
2636
+ for a PAT or provider key. GitHub read access is separate from the explicit
2637
+ --ack-redacted-source consent and the exact --plan-digest printed by security
2638
+ plan are required before bounded snippets reach System AI.
2639
+ Run security plan first to inspect the admin-selected providers, models,
2640
+ per-route bounds, credential readiness, retention, no-execution boundary,
2641
+ and digest that binds consent to that exact plan.
2126
2642
  `);
2127
2643
  }
2128
2644
 
2645
+ // src/harness-options.ts
2646
+ function harnessList(parsed) {
2647
+ const selected = [
2648
+ ...harnessOption(parsed.options.agent, "--agent"),
2649
+ ...harnessOption(parsed.options.harness, "--harness")
2650
+ ];
2651
+ return selected.length ? selected : ["all"];
2652
+ }
2653
+ function harnessOption(value, flag) {
2654
+ if (value === void 0) return [];
2655
+ if (typeof value === "boolean") throw new Error(`${flag} requires a harness name`);
2656
+ const raw = Array.isArray(value) ? value : [value];
2657
+ const parts = raw.flatMap((item) => item.split(","));
2658
+ if (parts.some((item) => !item.trim())) {
2659
+ throw new Error(`${flag} requires a harness name`);
2660
+ }
2661
+ return parts.map((item) => item.trim());
2662
+ }
2663
+
2664
+ // src/security-command-context.ts
2665
+ import { createInterface } from "readline/promises";
2666
+ async function hostedSecurityContext(parsed, dependencies) {
2667
+ const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
2668
+ const cfg = await loadProjectConfig(configPath);
2669
+ const env = stringOpt(parsed.options.env) ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
2670
+ if (!env || !cfg.envs.includes(env)) {
2671
+ throw new Error(`env "${env ?? ""}" is not declared in ${configPath}`);
2672
+ }
2673
+ const platform = platformAudience(stringOpt(parsed.options.platform) ?? cfg.platformUrl);
2674
+ if (platformAudience(cfg.platformUrl) !== platform) {
2675
+ throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
2676
+ }
2677
+ const doFetch = dependencies.fetch ?? fetch;
2678
+ const stdout = dependencies.stdout ?? console;
2679
+ const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2680
+ const token = await getDeveloperToken(
2681
+ cfg,
2682
+ { configPath, open, openApprovalUrl: dependencies.openUrl },
2683
+ doFetch,
2684
+ stdout
2685
+ );
2686
+ return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
2687
+ }
2688
+ async function interactiveConfirmation(message, dependencies) {
2689
+ if (dependencies.confirm) return dependencies.confirm(message);
2690
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
2691
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
2692
+ try {
2693
+ const answer = await prompt.question(`${message} [y/N] `);
2694
+ return /^y(?:es)?$/i.test(answer.trim());
2695
+ } finally {
2696
+ prompt.close();
2697
+ }
2698
+ }
2699
+ function requiredSecurityPositional(parsed, index, label) {
2700
+ const value = parsed.positionals[index];
2701
+ if (!value) throw new Error(`${label} is required`);
2702
+ return value;
2703
+ }
2704
+ function securityProfile(value) {
2705
+ if (value === void 0) return void 0;
2706
+ if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
2707
+ throw new Error("--profile must be odla, cloudflare-app, or generic");
2708
+ }
2709
+
2710
+ // src/security-command-output.ts
2711
+ function printHostedSecurityPlan(out, plan, appId) {
2712
+ out.log(`Hosted security plan for ${appId}/${plan.env}: ${plan.ready ? "ready" : "not ready"}`);
2713
+ out.log(` plan digest: ${plan.planDigest}`);
2714
+ out.log(` consent/report/redaction: ${plan.consentContract} \xB7 ${plan.reportProjection} \xB7 ${plan.redactionContract}`);
2715
+ out.log(` prompt bundle: ${plan.promptBundle}`);
2716
+ printHostedSecurityPlanRoute(out, "discovery", plan.routes.discovery);
2717
+ printHostedSecurityPlanRoute(out, "validation", plan.routes.validation);
2718
+ out.log(` independent validation: ${plan.independent ? "yes" : "no"}`);
2719
+ out.log(" source disclosure: bounded best-effort credential-pattern-redacted snippets may be sent through odla.ai to the selected providers");
2720
+ out.log(` retained report: model-derived prose and repository-relative paths for up to ${plan.reportRetentionDays} days; treat it as sensitive`);
2721
+ out.log(" target execution: disabled");
2722
+ out.log(` to approve this exact plan, run security run --source <id> --plan-digest ${plan.planDigest} --ack-redacted-source`);
2723
+ }
2724
+ function printHostedSecurityIntent(out, intent) {
2725
+ out.log(`Hosted security execution intent for ${intent.appId}/${intent.env}:`);
2726
+ out.log(` execution digest: ${intent.executionDigest}`);
2727
+ out.log(` contract: ${intent.executionContract}`);
2728
+ out.log(` source: ${intent.repository} (${intent.sourceId})`);
2729
+ out.log(` ref: ${intent.requestedRef ?? `default branch (${intent.defaultBranch})`}`);
2730
+ out.log(` profile: ${intent.profile}`);
2731
+ out.log(` disclosure: ${intent.sourceDisclosure} \xB7 plan ${intent.planDigest}`);
2732
+ }
2733
+ function assertHostedSecurityPlanReady(plan) {
2734
+ const reasons = [];
2735
+ for (const [label, route] of Object.entries(plan.routes)) {
2736
+ if (!route.enabled) reasons.push(`${label} is disabled`);
2737
+ if (!route.credentialReady) reasons.push(`${label} provider credential is unavailable`);
2738
+ }
2739
+ if (!plan.independent) reasons.push("discovery and validation are not independently routed");
2740
+ if (plan.ready && reasons.length === 0) return;
2741
+ if (!plan.ready && reasons.length === 0) reasons.push("the platform marked the plan unavailable");
2742
+ throw new Error(`hosted security is not ready${reasons.length ? `: ${reasons.join("; ")}` : ""}. Ask a platform admin to update System AI security policy or credentials`);
2743
+ }
2744
+ function printHostedJob(out, job, platform, appId) {
2745
+ out.log(`security job ${job.jobId}: ${job.status}`);
2746
+ out.log(` source: ${job.repository} ${job.requestedRef ?? "default"} -> ${job.commitSha ?? "resolving"}`);
2747
+ if (job.routes) {
2748
+ out.log(` discovery: ${routeLabel(job.routes.discovery)}`);
2749
+ out.log(` validation: ${routeLabel(job.routes.validation)}`);
2750
+ } else {
2751
+ out.log(" routes: platform System AI policy (assigned when analysis starts)");
2752
+ }
2753
+ out.log(` disclosure: ${job.sourceDisclosure} snippets \xB7 metadata retained up to ${job.retentionDays} days`);
2754
+ out.log(` consent: plan ${job.planDigest} \xB7 execution ${job.executionDigest} (${job.executionContract})`);
2755
+ if (job.coverageStatus || job.coverage) printHostedCoverage(out, job);
2756
+ if (job.counts) {
2757
+ out.log(` findings: confirmed=${job.counts.confirmed} needs_reproduction=${job.counts.needsReproduction} candidates=${job.counts.candidates}`);
2758
+ }
2759
+ if (job.errorCode) out.log(` failure: ${job.errorCode}`);
2760
+ const url = new URL("/studio", platform);
2761
+ url.searchParams.set("app", appId);
2762
+ url.searchParams.set("env", job.env);
2763
+ url.searchParams.set("tab", "security");
2764
+ url.searchParams.set("job", job.jobId);
2765
+ out.log(` Studio: ${url.toString()}`);
2766
+ }
2767
+ function printHostedReport(out, report) {
2768
+ out.log(`security report ${report.jobId}: ${report.repository}@${report.revision}`);
2769
+ 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}`);
2770
+ out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates} rejected=${report.metrics.rejected}`);
2771
+ out.log(` discovery: ${report.provenance.discovery?.provider ?? "unknown"}/${report.provenance.discovery?.model ?? "unknown"}`);
2772
+ out.log(` validation: ${report.provenance.validation?.provider ?? "unknown"}/${report.provenance.validation?.model ?? "unknown"} independent=${String(report.provenance.independentValidation)}`);
2773
+ for (const finding of report.findings) {
2774
+ const location = finding.locations[0];
2775
+ out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
2776
+ }
2777
+ for (const limitation of report.limitations) out.log(` limitation: ${limitation}`);
2778
+ }
2779
+ function enforceHostedReportGate(report, parsed, out, emitSuccess) {
2780
+ const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
2781
+ const candidateValue = parsed.options["fail-on-candidates"];
2782
+ const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
2783
+ const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
2784
+ const confirmed = report.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
2785
+ const leads = failOnCandidates ? report.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
2786
+ const incomplete = report.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
2787
+ if (confirmed.length || leads.length || incomplete) {
2788
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report.coverageStatus}` : ""}`);
2789
+ }
2790
+ if (emitSuccess) {
2791
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report.coverageStatus}. This is not proof that the application is secure.`);
2792
+ }
2793
+ }
2794
+ function printHostedSecurityPlanRoute(out, label, route) {
2795
+ const readiness = route.enabled && route.credentialReady ? "ready" : [
2796
+ route.enabled ? void 0 : "disabled",
2797
+ route.credentialReady ? void 0 : "credential unavailable"
2798
+ ].filter(Boolean).join(", ");
2799
+ out.log(` ${label}: ${route.provider}/${route.model} \xB7 policy v${route.policyVersion} \xB7 ${readiness}`);
2800
+ out.log(` bounds: ${route.maxCallsPerRun} calls/run \xB7 ${route.maxInputBytes} input bytes/call \xB7 ${route.maxOutputTokens} output tokens/call`);
2801
+ }
2802
+ function printHostedCoverage(out, job) {
2803
+ const coverage = job.coverage;
2804
+ 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}` : ""}`);
2805
+ }
2806
+ function routeLabel(route) {
2807
+ return `${route.provider}/${route.model}${route.policyVersion ? ` policy v${route.policyVersion}` : ""}`;
2808
+ }
2809
+ var HOSTED_SEVERITIES = ["informational", "low", "medium", "high", "critical"];
2810
+ function hostedSeverity(value, flag) {
2811
+ if (HOSTED_SEVERITIES.includes(value)) {
2812
+ return value;
2813
+ }
2814
+ throw new Error(`${flag} must be informational, low, medium, high, or critical`);
2815
+ }
2816
+
2817
+ // src/security-run-command.ts
2818
+ import { findingsAtOrAbove } from "@odla-ai/security";
2819
+ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
2820
+ assertArgs(parsed, [
2821
+ "config",
2822
+ "env",
2823
+ "profile",
2824
+ "platform",
2825
+ "source",
2826
+ "ref",
2827
+ "follow",
2828
+ "open",
2829
+ "json",
2830
+ "ack-redacted-source",
2831
+ "plan-digest",
2832
+ "fail-on",
2833
+ "fail-on-candidates",
2834
+ "allow-incomplete"
2835
+ ], 2);
2836
+ const follow = parsed.options.follow !== false;
2837
+ if (!follow && (parsed.options["fail-on"] !== void 0 || parsed.options["fail-on-candidates"] !== void 0 || parsed.options["allow-incomplete"] !== void 0)) {
2838
+ throw new Error("hosted security gate options require following the job; remove --no-follow");
2839
+ }
2840
+ const acknowledgedDigest = requiredString(parsed.options["plan-digest"], "--plan-digest");
2841
+ const context = await hostedSecurityContext(parsed, dependencies);
2842
+ const plan = await getHostedSecurityPlan(context);
2843
+ if (parsed.options.json !== true) printHostedSecurityPlan(context.stdout, plan, context.appId);
2844
+ assertHostedSecurityPlanReady(plan);
2845
+ if (acknowledgedDigest !== plan.planDigest) {
2846
+ throw new Error(`--plan-digest does not match the current hosted security plan (${plan.planDigest}); review and explicitly acknowledge the current digest`);
2847
+ }
2848
+ const requestedRef = stringOpt(parsed.options.ref);
2849
+ const requestedProfile = securityProfile(stringOpt(parsed.options.profile));
2850
+ const preview = await getHostedSecurityIntent({
2851
+ ...context,
2852
+ sourceId,
2853
+ ref: requestedRef,
2854
+ profile: requestedProfile
2855
+ });
2856
+ if (parsed.options.json !== true) printHostedSecurityIntent(context.stdout, preview.intent);
2857
+ if (preview.plan.planDigest !== plan.planDigest || preview.intent.planDigest !== plan.planDigest) {
2858
+ throw new Error(`hosted security plan changed while previewing execution (${preview.plan.planDigest}); review and explicitly acknowledge the current plan digest`);
2859
+ }
2860
+ const job = await startHostedSecurityJob({
2861
+ ...context,
2862
+ sourceId,
2863
+ ref: requestedRef,
2864
+ profile: requestedProfile,
2865
+ clientRequestId: crypto.randomUUID(),
2866
+ sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
2867
+ expectedPlanDigest: plan.planDigest,
2868
+ expectedExecutionDigest: preview.intent.executionDigest,
2869
+ expectedPolicyVersions: {
2870
+ discovery: plan.routes.discovery.policyVersion,
2871
+ validation: plan.routes.validation.policyVersion
2872
+ }
2873
+ });
2874
+ const result = follow ? await followHostedSecurityJob({
2875
+ ...context,
2876
+ jobId: job.jobId,
2877
+ wait: dependencies.pollWait,
2878
+ onUpdate: parsed.options.json === true ? void 0 : (value) => printHostedJob(context.stdout, value, context.platform, context.appId)
2879
+ }) : job;
2880
+ if (!follow) {
2881
+ if (parsed.options.json === true) {
2882
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result }, null, 2));
2883
+ } else {
2884
+ printHostedJob(context.stdout, result, context.platform, context.appId);
2885
+ }
2886
+ return;
2887
+ }
2888
+ if (result.status !== "completed") {
2889
+ if (parsed.options.json === true) {
2890
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result }, null, 2));
2891
+ }
2892
+ throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
2893
+ }
2894
+ const report = await getHostedSecurityReport({ ...context, jobId: result.jobId });
2895
+ if (parsed.options.json === true) {
2896
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report }, null, 2));
2897
+ } else {
2898
+ printHostedReport(context.stdout, report);
2899
+ }
2900
+ enforceHostedReportGate(report, parsed, context.stdout, parsed.options.json !== true);
2901
+ }
2902
+ async function runLocalSecurityCommand(parsed, dependencies) {
2903
+ if (parsed.options.source === true) {
2904
+ throw new Error("--source requires a source id; run security sources first");
2905
+ }
2906
+ assertArgs(parsed, [
2907
+ "config",
2908
+ "env",
2909
+ "out",
2910
+ "profile",
2911
+ "max-hunt-tasks",
2912
+ "run-id",
2913
+ "platform",
2914
+ "self",
2915
+ "ack-redacted-source",
2916
+ "open",
2917
+ "fail-on",
2918
+ "fail-on-candidates",
2919
+ "allow-incomplete"
2920
+ ], 3);
2921
+ const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
2922
+ const selfAudit = parsed.options.self === true;
2923
+ const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2924
+ const platform = stringOpt(parsed.options.platform);
2925
+ const doFetch = dependencies.fetch ?? fetch;
2926
+ const out = dependencies.stdout ?? console;
2927
+ const result = await runHostedSecurity({
2928
+ configPath,
2929
+ selfAudit,
2930
+ target: parsed.positionals[2],
2931
+ out: stringOpt(parsed.options.out),
2932
+ env: stringOpt(parsed.options.env),
2933
+ profile: securityProfile(stringOpt(parsed.options.profile)),
2934
+ maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
2935
+ runId: stringOpt(parsed.options["run-id"]),
2936
+ platform,
2937
+ sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
2938
+ fetch: doFetch,
2939
+ stdout: out,
2940
+ getToken: async (request) => {
2941
+ if (request.scope === "platform:security:self") {
2942
+ return getScopedPlatformToken({
2943
+ platform: request.platform,
2944
+ scope: request.scope,
2945
+ open,
2946
+ fetch: doFetch,
2947
+ stdout: out,
2948
+ openApprovalUrl: dependencies.openUrl
2949
+ });
2950
+ }
2951
+ const cfg = await loadProjectConfig(configPath);
2952
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
2953
+ throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
2954
+ }
2955
+ return getDeveloperToken(
2956
+ cfg,
2957
+ { configPath, open, openApprovalUrl: dependencies.openUrl },
2958
+ doFetch,
2959
+ out
2960
+ );
2961
+ }
2962
+ });
2963
+ enforceLocalGate(result.report, parsed);
2964
+ }
2965
+ function enforceLocalGate(report, parsed) {
2966
+ const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
2967
+ const candidateValue = parsed.options["fail-on-candidates"];
2968
+ const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
2969
+ const confirmed = findingsAtOrAbove(report, failOn);
2970
+ const leads = failOnCandidates ? findingsAtOrAbove(report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
2971
+ const incomplete = report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
2972
+ if (confirmed.length || leads.length || incomplete) {
2973
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
2974
+ }
2975
+ }
2976
+ function severityOpt(value, flag) {
2977
+ if (["informational", "low", "medium", "high", "critical"].includes(value)) {
2978
+ return value;
2979
+ }
2980
+ throw new Error(`${flag} must be informational, low, medium, high, or critical`);
2981
+ }
2982
+
2983
+ // src/security-command.ts
2984
+ async function securityCommand(parsed, dependencies) {
2985
+ const sub = parsed.positionals[1];
2986
+ if (sub === "github") {
2987
+ await githubSecurityCommand(parsed, dependencies);
2988
+ return;
2989
+ }
2990
+ if (sub === "plan") {
2991
+ assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
2992
+ const context = await hostedSecurityContext(parsed, dependencies);
2993
+ const plan = await getHostedSecurityPlan(context);
2994
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(plan, null, 2));
2995
+ else printHostedSecurityPlan(context.stdout, plan, context.appId);
2996
+ return;
2997
+ }
2998
+ if (sub === "sources") {
2999
+ await listSecuritySources(parsed, dependencies);
3000
+ return;
3001
+ }
3002
+ if (sub === "status") {
3003
+ await securityStatus(parsed, dependencies);
3004
+ return;
3005
+ }
3006
+ if (sub === "report") {
3007
+ assertArgs(parsed, ["config", "env", "platform", "open", "json"], 3);
3008
+ const jobId = requiredSecurityPositional(parsed, 2, "job id");
3009
+ const context = await hostedSecurityContext(parsed, dependencies);
3010
+ const report = await getHostedSecurityReport({ ...context, jobId });
3011
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report, null, 2));
3012
+ else printHostedReport(context.stdout, report);
3013
+ return;
3014
+ }
3015
+ if (sub !== "run") {
3016
+ throw new Error('unknown security command. Try "odla-ai security plan", "security sources", or "security run".');
3017
+ }
3018
+ const sourceId = stringOpt(parsed.options.source);
3019
+ if (sourceId) await runSourceSecurityCommand(parsed, dependencies, sourceId);
3020
+ else await runLocalSecurityCommand(parsed, dependencies);
3021
+ }
3022
+ async function githubSecurityCommand(parsed, dependencies) {
3023
+ const action = parsed.positionals[2];
3024
+ if (action === "disconnect") {
3025
+ assertArgs(parsed, ["config", "env", "platform", "source", "open", "yes"], 3);
3026
+ const context2 = await hostedSecurityContext(parsed, dependencies);
3027
+ const sourceId = requiredString(parsed.options.source, "--source");
3028
+ const confirmed = parsed.options.yes === true || await interactiveConfirmation(
3029
+ `Disconnect GitHub security source ${sourceId} from ${context2.appId}/${context2.env}?`,
3030
+ dependencies
3031
+ );
3032
+ if (!confirmed) {
3033
+ throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
3034
+ }
3035
+ await disconnectGitHubSecuritySource({ ...context2, sourceId });
3036
+ context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
3037
+ return;
3038
+ }
3039
+ if (action !== "connect") {
3040
+ throw new Error('unknown security github command. Try "odla-ai security github connect".');
3041
+ }
3042
+ assertArgs(parsed, ["config", "env", "platform", "repo", "open"], 3);
3043
+ const context = await hostedSecurityContext(parsed, dependencies);
3044
+ const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin);
3045
+ const connection = await connectGitHubSecuritySource({
3046
+ ...context,
3047
+ repository,
3048
+ open: parsed.options.open !== false,
3049
+ openInstallUrl: dependencies.openUrl ?? openUrl,
3050
+ wait: dependencies.pollWait,
3051
+ stdout: context.stdout
3052
+ });
3053
+ context.stdout.log(`github: connected ${connection.repository ?? repository} (${connection.sourceId ?? "source pending"})`);
3054
+ context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
3055
+ }
3056
+ async function listSecuritySources(parsed, dependencies) {
3057
+ assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
3058
+ const context = await hostedSecurityContext(parsed, dependencies);
3059
+ const [plan, sources] = await Promise.all([
3060
+ getHostedSecurityPlan(context),
3061
+ listGitHubSecuritySources(context)
3062
+ ]);
3063
+ if (parsed.options.json === true) {
3064
+ context.stdout.log(JSON.stringify({ plan, sources }, null, 2));
3065
+ return;
3066
+ }
3067
+ printHostedSecurityPlan(context.stdout, plan, context.appId);
3068
+ if (!sources.length) {
3069
+ context.stdout.log(`No GitHub security sources connected for ${context.appId}/${context.env}.`);
3070
+ return;
3071
+ }
3072
+ context.stdout.log(`GitHub security sources for ${context.appId}/${context.env}
3073
+ source repository default ref status`);
3074
+ for (const source of sources) {
3075
+ context.stdout.log(`${source.sourceId} ${source.repository} ${source.defaultBranch} ${source.status}`);
3076
+ }
3077
+ }
3078
+ async function securityStatus(parsed, dependencies) {
3079
+ assertArgs(parsed, ["config", "env", "platform", "open", "json", "follow"], 3);
3080
+ const jobId = requiredSecurityPositional(parsed, 2, "job id");
3081
+ const context = await hostedSecurityContext(parsed, dependencies);
3082
+ const job = parsed.options.follow === true ? await followHostedSecurityJob({
3083
+ ...context,
3084
+ jobId,
3085
+ wait: dependencies.pollWait,
3086
+ onUpdate: parsed.options.json === true ? void 0 : (value) => printHostedJob(context.stdout, value, context.platform, context.appId)
3087
+ }) : await getHostedSecurityJob({ ...context, jobId });
3088
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(job, null, 2));
3089
+ else if (parsed.options.follow !== true) {
3090
+ printHostedJob(context.stdout, job, context.platform, context.appId);
3091
+ }
3092
+ }
3093
+
2129
3094
  // src/cli.ts
2130
- async function runCli(argv = process.argv.slice(2)) {
3095
+ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
2131
3096
  const parsed = parseArgv(argv);
2132
3097
  const command = parsed.positionals[0] ?? "help";
2133
3098
  if (command === "version" || command === "-v" || parsed.options.version === true) {
@@ -2141,7 +3106,15 @@ async function runCli(argv = process.argv.slice(2)) {
2141
3106
  return;
2142
3107
  }
2143
3108
  if (command === "init") {
2144
- assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
3109
+ assertArgs(parsed, [
3110
+ "app-id",
3111
+ "name",
3112
+ "config",
3113
+ "env",
3114
+ "services",
3115
+ "ai-provider",
3116
+ "force"
3117
+ ], 1);
2145
3118
  initProject({
2146
3119
  appId: requiredString(parsed.options["app-id"], "--app-id"),
2147
3120
  name: requiredString(parsed.options.name, "--name"),
@@ -2164,149 +3137,15 @@ async function runCli(argv = process.argv.slice(2)) {
2164
3137
  return;
2165
3138
  }
2166
3139
  if (command === "admin") {
2167
- const area = parsed.positionals[1];
2168
- const action = parsed.positionals[2];
2169
- const credentialSet = action === "credential" && parsed.positionals[3] === "set";
2170
- const credentials = action === "credentials";
2171
- const models = action === "models";
2172
- const usage = action === "usage";
2173
- const audit = action === "audit";
2174
- if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
2175
- throw new Error('unknown admin command. Try "odla-ai admin ai show".');
2176
- }
2177
- assertArgs(parsed, [
2178
- "platform",
2179
- "json",
2180
- "open",
2181
- "provider",
2182
- "model",
2183
- "enabled",
2184
- "max-input-bytes",
2185
- "max-output-tokens",
2186
- "max-calls-per-run",
2187
- "from-env",
2188
- "stdin",
2189
- "discovery-provider",
2190
- "discovery-model",
2191
- "validation-provider",
2192
- "validation-model",
2193
- "app-id",
2194
- "env",
2195
- "run-id",
2196
- "limit"
2197
- ], credentialSet ? 5 : action === "set" ? 4 : 3);
2198
- await adminAi({
2199
- action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
2200
- purpose: action === "set" ? parsed.positionals[3] : void 0,
2201
- provider: stringOpt(parsed.options.provider),
2202
- model: stringOpt(parsed.options.model),
2203
- enabled: boolOpt(parsed.options.enabled),
2204
- maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
2205
- maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
2206
- maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
2207
- platform: stringOpt(parsed.options.platform),
2208
- json: parsed.options.json === true,
2209
- open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
2210
- credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
2211
- fromEnv: stringOpt(parsed.options["from-env"]),
2212
- stdin: parsed.options.stdin === true,
2213
- discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
2214
- discoveryModel: stringOpt(parsed.options["discovery-model"]),
2215
- validationProvider: stringOpt(parsed.options["validation-provider"]),
2216
- validationModel: stringOpt(parsed.options["validation-model"]),
2217
- appId: stringOpt(parsed.options["app-id"]),
2218
- env: stringOpt(parsed.options.env),
2219
- runId: stringOpt(parsed.options["run-id"]),
2220
- limit: numberOpt(parsed.options.limit, "--limit")
2221
- });
3140
+ await adminCommand(parsed);
2222
3141
  return;
2223
3142
  }
2224
3143
  if (command === "security") {
2225
- const sub = parsed.positionals[1];
2226
- if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
2227
- assertArgs(parsed, [
2228
- "config",
2229
- "env",
2230
- "out",
2231
- "profile",
2232
- "max-hunt-tasks",
2233
- "run-id",
2234
- "platform",
2235
- "self",
2236
- "ack-redacted-source",
2237
- "open",
2238
- "fail-on",
2239
- "fail-on-candidates",
2240
- "allow-incomplete"
2241
- ], 3);
2242
- const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
2243
- const selfAudit = parsed.options.self === true;
2244
- const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2245
- const platform = stringOpt(parsed.options.platform);
2246
- const result = await runHostedSecurity({
2247
- configPath,
2248
- selfAudit,
2249
- target: parsed.positionals[2],
2250
- out: stringOpt(parsed.options.out),
2251
- env: stringOpt(parsed.options.env),
2252
- profile: securityProfile(stringOpt(parsed.options.profile)),
2253
- maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
2254
- runId: stringOpt(parsed.options["run-id"]),
2255
- platform,
2256
- sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
2257
- getToken: async (request) => {
2258
- if (request.scope === "platform:security:self") {
2259
- return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
2260
- }
2261
- const cfg = await loadProjectConfig(configPath);
2262
- if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
2263
- throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
2264
- }
2265
- return getDeveloperToken(cfg, { configPath, open }, fetch, console);
2266
- }
2267
- });
2268
- const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
2269
- const candidateValue = parsed.options["fail-on-candidates"];
2270
- const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
2271
- const confirmed = findingsAtOrAbove(result.report, failOn);
2272
- const leads = failOnCandidates ? findingsAtOrAbove(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
2273
- const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
2274
- if (confirmed.length || leads.length || incomplete) {
2275
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
2276
- }
3144
+ await securityCommand(parsed, dependencies);
2277
3145
  return;
2278
3146
  }
2279
3147
  if (command === "provision") {
2280
- assertArgs(
2281
- parsed,
2282
- [
2283
- "config",
2284
- "dry-run",
2285
- "rotate-keys",
2286
- "rotate-o11y-token",
2287
- "push-secrets",
2288
- "write-credentials",
2289
- "write-dev-vars",
2290
- "token",
2291
- "open",
2292
- "yes"
2293
- ],
2294
- 1
2295
- );
2296
- const writeDevVars2 = parsed.options["write-dev-vars"];
2297
- const opts = {
2298
- configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
2299
- dryRun: parsed.options["dry-run"] === true,
2300
- rotateKeys: parsed.options["rotate-keys"] === true,
2301
- rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
2302
- pushSecrets: parsed.options["push-secrets"] === true,
2303
- writeCredentials: parsed.options["write-credentials"] !== false,
2304
- writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
2305
- token: stringOpt(parsed.options.token),
2306
- open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
2307
- yes: parsed.options.yes === true
2308
- };
2309
- await provision(opts);
3148
+ await provisionCommand(parsed);
2310
3149
  return;
2311
3150
  }
2312
3151
  if (command === "smoke") {
@@ -2332,7 +3171,9 @@ async function runCli(argv = process.argv.slice(2)) {
2332
3171
  }
2333
3172
  if (command === "skill") {
2334
3173
  const sub = parsed.positionals[1];
2335
- if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
3174
+ if (sub !== "install") {
3175
+ throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
3176
+ }
2336
3177
  assertArgs(parsed, ["dir", "global", "force", "agent", "harness"], 2);
2337
3178
  installSkill({
2338
3179
  dir: stringOpt(parsed.options.dir),
@@ -2344,7 +3185,27 @@ async function runCli(argv = process.argv.slice(2)) {
2344
3185
  }
2345
3186
  if (command === "secrets") {
2346
3187
  const sub = parsed.positionals[1];
2347
- if (sub !== "push") throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
3188
+ if (sub === "set" || sub === "set-clerk-key") {
3189
+ assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
3190
+ const options = {
3191
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3192
+ name: parsed.positionals[2],
3193
+ env: requiredString(parsed.options.env, "--env"),
3194
+ fromEnv: stringOpt(parsed.options["from-env"]),
3195
+ stdin: parsed.options.stdin === true,
3196
+ token: stringOpt(parsed.options.token),
3197
+ yes: parsed.options.yes === true,
3198
+ fetch: dependencies.fetch,
3199
+ stdout: dependencies.stdout
3200
+ };
3201
+ await (sub === "set" ? secretsSet(options) : secretsSetClerkKey(options));
3202
+ return;
3203
+ }
3204
+ if (sub !== "push") {
3205
+ throw new Error(
3206
+ `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".`
3207
+ );
3208
+ }
2348
3209
  assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
2349
3210
  await secretsPush({
2350
3211
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
@@ -2356,29 +3217,33 @@ async function runCli(argv = process.argv.slice(2)) {
2356
3217
  }
2357
3218
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
2358
3219
  }
2359
- function securityProfile(value) {
2360
- if (value === void 0) return void 0;
2361
- if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
2362
- throw new Error("--profile must be odla, cloudflare-app, or generic");
2363
- }
2364
- function severityOpt(value, flag) {
2365
- if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
2366
- throw new Error(`${flag} must be informational, low, medium, high, or critical`);
2367
- }
2368
- function harnessList(parsed) {
2369
- const selected = [
2370
- ...harnessOption(parsed.options.agent, "--agent"),
2371
- ...harnessOption(parsed.options.harness, "--harness")
2372
- ];
2373
- return selected.length ? selected : ["all"];
2374
- }
2375
- function harnessOption(value, flag) {
2376
- if (value === void 0) return [];
2377
- if (typeof value === "boolean") throw new Error(`${flag} requires a harness name`);
2378
- const raw = Array.isArray(value) ? value : [value];
2379
- const parts = raw.flatMap((item) => item.split(","));
2380
- if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
2381
- return parts.map((item) => item.trim());
3220
+ async function provisionCommand(parsed) {
3221
+ assertArgs(parsed, [
3222
+ "config",
3223
+ "dry-run",
3224
+ "rotate-keys",
3225
+ "rotate-o11y-token",
3226
+ "push-secrets",
3227
+ "write-credentials",
3228
+ "write-dev-vars",
3229
+ "token",
3230
+ "open",
3231
+ "yes"
3232
+ ], 1);
3233
+ const writeDevVars2 = parsed.options["write-dev-vars"];
3234
+ const options = {
3235
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3236
+ dryRun: parsed.options["dry-run"] === true,
3237
+ rotateKeys: parsed.options["rotate-keys"] === true,
3238
+ rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
3239
+ pushSecrets: parsed.options["push-secrets"] === true,
3240
+ writeCredentials: parsed.options["write-credentials"] !== false,
3241
+ writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
3242
+ token: stringOpt(parsed.options.token),
3243
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3244
+ yes: parsed.options.yes === true
3245
+ };
3246
+ await provision(options);
2382
3247
  }
2383
3248
 
2384
3249
  export {
@@ -2392,10 +3257,25 @@ export {
2392
3257
  initProject,
2393
3258
  secretsPush,
2394
3259
  provision,
3260
+ secretsSet,
3261
+ secretsSetClerkKey,
2395
3262
  runHostedSecurity,
3263
+ connectGitHubSecuritySource,
3264
+ listGitHubSecuritySources,
3265
+ disconnectGitHubSecuritySource,
3266
+ repositoryFromGitRemote,
3267
+ inferGitHubRepository,
3268
+ getHostedSecurityIntent,
3269
+ getHostedSecurityPlan,
3270
+ startHostedSecurityJob,
3271
+ getHostedSecurityJob,
3272
+ listHostedSecurityJobs,
3273
+ followHostedSecurityJob,
3274
+ getHostedSecurityReport,
3275
+ isTerminalHostedSecurityStatus,
2396
3276
  AGENT_HARNESSES,
2397
3277
  installSkill,
2398
3278
  smoke,
2399
3279
  runCli
2400
3280
  };
2401
- //# sourceMappingURL=chunk-643B2AKG.js.map
3281
+ //# sourceMappingURL=chunk-MJYQ7YDH.js.map