@odla-ai/cli 0.27.10 → 0.27.12

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.
@@ -1372,12 +1372,6 @@ async function pollCalendarConnection(ctx, attemptId) {
1372
1372
  ctx.env
1373
1373
  );
1374
1374
  }
1375
- async function requestCalendarDisconnect(ctx) {
1376
- return parseCalendarStatus(
1377
- await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
1378
- ctx.env
1379
- );
1380
- }
1381
1375
  function parseCalendarStatus(raw, env) {
1382
1376
  const outer = wrapped(raw, "calendar");
1383
1377
  const value2 = record(outer.attempt) ?? record(outer.status) ?? outer;
@@ -1528,6 +1522,16 @@ async function waitForCalendarPoll(milliseconds, signal) {
1528
1522
  });
1529
1523
  }
1530
1524
 
1525
+ // src/human-session.ts
1526
+ async function requireStudioHuman(configPath, action2, destination = "app", env) {
1527
+ const cfg = await loadProjectConfig(configPath);
1528
+ const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
1529
+ const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
1530
+ throw new Error(
1531
+ `human_session_required: ${action2} requires the signed-in owner in Studio. A CLI device token is an agent credential, so another approval or retry cannot work. ${new URL(path, cfg.platformUrl).href}`
1532
+ );
1533
+ }
1534
+
1531
1535
  // src/calendar.ts
1532
1536
  async function calendarStatus(options) {
1533
1537
  const { ctx, out } = await lifecycleContext(options);
@@ -1546,7 +1550,7 @@ async function calendarCalendars(options) {
1546
1550
  return calendars;
1547
1551
  }
1548
1552
  async function calendarConnect(options) {
1549
- const { cfg, ctx, out } = await lifecycleContext(options);
1553
+ const { cfg, ctx, out } = await lifecycleContext(options, ["app.manage"]);
1550
1554
  productionConsent(ctx.env, options.yes, "connect calendar");
1551
1555
  const page2 = calendarBookingPageUrl(cfg, ctx.env);
1552
1556
  const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
@@ -1561,10 +1565,7 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
1561
1565
  }
1562
1566
  async function calendarDisconnect(options) {
1563
1567
  if (!options.yes) throw new Error("calendar disconnect requires --yes");
1564
- const { ctx, out } = await lifecycleContext(options);
1565
- const status = await requestCalendarDisconnect(ctx);
1566
- out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
1567
- return status;
1568
+ return requireStudioHuman(options.configPath, "calendar disconnect", "calendar", options.env);
1568
1569
  }
1569
1570
  async function ensureCalendarConnected(ctx, options) {
1570
1571
  const out = options.stdout ?? console;
@@ -1572,7 +1573,7 @@ async function ensureCalendarConnected(ctx, options) {
1572
1573
  const connectOptions = connectionOptions(options, out);
1573
1574
  return await continueConnectedCalendar(ctx, current, connectOptions) ?? connectWithContext(ctx, connectOptions);
1574
1575
  }
1575
- async function lifecycleContext(options) {
1576
+ async function lifecycleContext(options, optionalProjectCapabilities = []) {
1576
1577
  const cfg = await loadProjectConfig(options.configPath);
1577
1578
  if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
1578
1579
  const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
@@ -1590,7 +1591,8 @@ async function lifecycleContext(options) {
1590
1591
  openApprovalUrl: options.openConsentUrl
1591
1592
  },
1592
1593
  doFetch,
1593
- out
1594
+ out,
1595
+ { optionalProjectCapabilities }
1594
1596
  );
1595
1597
  return { cfg, ctx: { platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch }, out };
1596
1598
  }
@@ -1800,11 +1802,17 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
1800
1802
  if (res.ok || res.status === 404) return;
1801
1803
  if (res.status === 403) {
1802
1804
  const detail = await safeText4(res);
1803
- if (errorCode(detail) === "human_session_required") {
1805
+ const code = errorCode(detail);
1806
+ if (code === "human_session_required") {
1804
1807
  throw new Error(
1805
1808
  `${env}: odla-db rejected the provision credential before checking ownership for "${cfg.app.id}" (tenant ${tenantId}): human_session_required. Retrying or changing app owners will not help; the deployed odla-db must accept owner-approved app.manage credentials on provisioning routes`
1806
1809
  );
1807
1810
  }
1811
+ if (code === "provision_approval_required") {
1812
+ throw new Error(
1813
+ `${env}: the agent credential does not carry the owner-reviewed app.manage grant required to provision "${cfg.app.id}" (tenant ${tenantId}). The human owner id on the token is accountability, not agent authority. Discard the cached or supplied token and run this command with the current CLI to approve one fresh exact-project provisioning handshake`
1814
+ );
1815
+ }
1808
1816
  throw new Error(
1809
1817
  `${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; re-run provision with a fresh owner-approved provision handshake. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
1810
1818
  );
@@ -3319,7 +3327,9 @@ async function secretsSet(options) {
3319
3327
  throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
3320
3328
  }
3321
3329
  const { cfg, tenantId, value: value2, doFetch, out } = await resolveVaultWrite(options);
3322
- const token = await getDeveloperToken(cfg, options, doFetch, out);
3330
+ const token = await getDeveloperToken(cfg, options, doFetch, out, {
3331
+ optionalProjectCapabilities: ["app.manage"]
3332
+ });
3323
3333
  try {
3324
3334
  await putSecret({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
3325
3335
  } catch (err) {
@@ -3336,7 +3346,9 @@ async function secretsSetClerkKey(options) {
3336
3346
  if (value2.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
3337
3347
  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)`);
3338
3348
  }
3339
- const token = await getDeveloperToken(cfg, options, doFetch, out);
3349
+ const token = await getDeveloperToken(cfg, options, doFetch, out, {
3350
+ optionalProjectCapabilities: ["app.manage"]
3351
+ });
3340
3352
  const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
3341
3353
  method: "POST",
3342
3354
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
@@ -7808,17 +7820,17 @@ async function runHostedSecurity(options) {
7808
7820
  allowNetwork: false
7809
7821
  }
7810
7822
  });
7811
- const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
7812
- await writeSecurityArtifacts(output, report5);
7813
- const reportDigest = await securityFingerprint(report5);
7823
+ const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
7824
+ await writeSecurityArtifacts(output, report4);
7825
+ const reportDigest = await securityFingerprint(report4);
7814
7826
  await hosted.complete({
7815
7827
  reportDigest,
7816
- coverageStatus: report5.coverageStatus,
7817
- confirmed: report5.metrics.confirmed,
7818
- candidates: report5.metrics.candidates
7828
+ coverageStatus: report4.coverageStatus,
7829
+ confirmed: report4.metrics.confirmed,
7830
+ candidates: report4.metrics.candidates
7819
7831
  }, { signal: options.signal });
7820
- printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
7821
- return Object.freeze({ report: report5, run: hosted.run, output });
7832
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
7833
+ return Object.freeze({ report: report4, run: hosted.run, output });
7822
7834
  }
7823
7835
  function selectEnv(requested, declared, configPath, rootDir) {
7824
7836
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -7844,14 +7856,14 @@ function profileFor(name, maxHuntTasks) {
7844
7856
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
7845
7857
  return { ...profile, maxHuntTasks };
7846
7858
  }
7847
- function printSummary(out, appId, env, run, report5, output) {
7848
- const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
7859
+ function printSummary(out, appId, env, run, report4, output) {
7860
+ const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
7849
7861
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
7850
7862
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
7851
7863
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
7852
- out.log(` coverage: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
7853
- if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
7854
- out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
7864
+ out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
7865
+ if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
7866
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
7855
7867
  out.log(` report: ${resolve9(output, "REPORT.md")}`);
7856
7868
  }
7857
7869
  function formatBudget(usage) {
@@ -8416,7 +8428,7 @@ async function agentCommand(parsed, deps = {}) {
8416
8428
  if (action2 !== "jobs" && action2 !== "retry") {
8417
8429
  throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
8418
8430
  }
8419
- assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action2 === "jobs" ? 2 : 3);
8431
+ assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
8420
8432
  if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
8421
8433
  throw new Error('--state and --limit are supported only by "agent jobs"');
8422
8434
  }
@@ -8424,17 +8436,17 @@ async function agentCommand(parsed, deps = {}) {
8424
8436
  const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
8425
8437
  const doFetch = deps.fetch ?? fetch;
8426
8438
  const out = deps.stdout ?? console;
8427
- const credential2 = await getDeveloperToken(
8428
- cfg,
8429
- {
8430
- configPath: cfg.configPath,
8431
- token: stringOpt(parsed.options.token),
8432
- email: stringOpt(parsed.options.email),
8433
- open: false
8434
- },
8435
- doFetch,
8436
- out
8437
- );
8439
+ const credential2 = stringOpt(parsed.options.token) ?? readCredentials(cfg.local.credentialsFile)?.envs[env]?.dbKey;
8440
+ if (!credential2) {
8441
+ throw new Error(
8442
+ `no ${env} app credential found; run \`odla-ai provision --write-dev-vars --yes\` or pass --token <ODLA_API_KEY>`
8443
+ );
8444
+ }
8445
+ if (credential2.startsWith("odla_dev_")) {
8446
+ throw new Error(
8447
+ "agent job administration requires an app credential (ODLA_API_KEY / odla_sk_\u2026), not a developer device token"
8448
+ );
8449
+ }
8438
8450
  const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
8439
8451
  const headers = { authorization: `Bearer ${credential2}` };
8440
8452
  if (action2 === "retry") {
@@ -8491,53 +8503,14 @@ function errorMessage(body) {
8491
8503
  }
8492
8504
 
8493
8505
  // src/app-export.ts
8494
- import { createWriteStream } from "fs";
8495
- import { Readable } from "stream";
8496
- import { pipeline } from "stream/promises";
8497
8506
  async function appExport(options) {
8498
- const cfg = await loadProjectConfig(options.configPath);
8499
- const out = options.stdout ?? console;
8500
- const doFetch = options.fetch ?? fetch;
8501
- const { tenant } = resolveTenant(cfg, options.env);
8502
- const token = await getDeveloperToken(
8503
- cfg,
8504
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
8505
- doFetch,
8506
- out
8507
- );
8508
- const auth = { authorization: `Bearer ${token}` };
8509
- const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
8510
- if (options.fresh) {
8511
- const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
8512
- const body = await res.json().catch(() => ({}));
8513
- if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
8514
- }
8515
- const list = await doFetch(`${base}/backups`, { headers: auth });
8516
- if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
8517
- const { backups } = await list.json();
8518
- const newest = backups[0];
8519
- if (!newest) {
8520
- throw new Error(
8521
- `${tenant} has no backups yet \u2014 run \`odla-ai app export --fresh\` to take one now (nightly snapshots appear after the first day with writes)`
8522
- );
8523
- }
8524
- const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
8525
- if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
8526
- const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
8527
- await pipeline(Readable.fromWeb(download.body), createWriteStream(file));
8528
- if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
8529
- else {
8530
- out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
8531
- out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
8532
- }
8533
- return { file, backup: newest };
8507
+ return requireStudioHuman(options.configPath, "database export", "database", options.env);
8534
8508
  }
8535
8509
 
8536
8510
  // src/app-import.ts
8537
8511
  import { readFileSync as readFileSync9 } from "fs";
8538
8512
  import {
8539
8513
  buildImportOps,
8540
- importMutationId,
8541
8514
  parseImport,
8542
8515
  planImportChunks
8543
8516
  } from "@odla-ai/db/import";
@@ -8556,7 +8529,6 @@ async function appImport(options) {
8556
8529
  const cfg = await loadProjectConfig(options.configPath);
8557
8530
  const out = options.stdout ?? console;
8558
8531
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
8559
- const doFetch = options.fetch ?? fetch;
8560
8532
  const { tenant } = resolveTenant(cfg, options.env);
8561
8533
  const text2 = options.file === "-" ? (options.readStdin ?? (() => readFileSync9(0, "utf8")))() : readFileSync9(options.file, "utf8");
8562
8534
  const { format, sources } = parseImport(text2, options.ns);
@@ -8584,100 +8556,18 @@ ${detail}${more}`);
8584
8556
  if (options.json) out.log(JSON.stringify(result, null, 2));
8585
8557
  return result;
8586
8558
  }
8587
- const token = await getDeveloperToken(
8588
- cfg,
8589
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
8590
- doFetch,
8591
- out
8592
- );
8593
- const runId = crypto.randomUUID();
8594
- const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
8595
- for (const chunk of chunks) {
8596
- const res = await doFetch(url, {
8597
- method: "POST",
8598
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
8599
- body: JSON.stringify({ mutationId: importMutationId(runId, chunk.index), ops: chunk.ops })
8600
- });
8601
- const body = await res.json().catch(() => ({}));
8602
- if (!res.ok) {
8603
- const code = body.error?.code ? ` (${body.error.code})` : "";
8604
- throw new Error(
8605
- `chunk ${chunk.index + 1} of ${chunks.length} failed${code}: ${body.error?.message ?? res.status}. ${result.committed} row(s) already committed; fix the input and re-run to import the rest.`
8606
- );
8607
- }
8608
- result.committed += chunk.ops.length;
8609
- if (typeof body.txId === "number") result.txIds.push(body.txId);
8610
- if (body.duplicate) result.duplicate++;
8611
- }
8612
- if (options.json) out.log(JSON.stringify(result, null, 2));
8613
- else {
8614
- const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
8615
- out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
8616
- }
8617
- return result;
8559
+ return requireStudioHuman(options.configPath, "database import", "database", options.env);
8618
8560
  }
8619
8561
 
8620
8562
  // src/app-owners.ts
8621
- var sink = (options) => options.stdout ?? console;
8622
- async function ownersRequest(method, suffix, options, body) {
8623
- const cfg = await loadProjectConfig(options.configPath);
8624
- const doFetch = options.fetch ?? fetch;
8625
- const token = await getDeveloperToken(
8626
- cfg,
8627
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
8628
- doFetch,
8629
- sink(options)
8630
- );
8631
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
8632
- method,
8633
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
8634
- body: body === void 0 ? void 0 : JSON.stringify(body)
8635
- });
8636
- const data = await res.json().catch(() => ({}));
8637
- if (!res.ok) {
8638
- throw new Error(
8639
- `owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
8640
- );
8641
- }
8642
- return data.owners ?? [];
8643
- }
8644
- function report(options, owners, headline) {
8645
- const out = sink(options);
8646
- if (options.json === true) {
8647
- out.log(JSON.stringify(owners, null, 2));
8648
- return;
8649
- }
8650
- if (headline) out.log(headline);
8651
- out.log(`owners (${owners.length}):`);
8652
- for (const o of owners) {
8653
- const name = o.email?.trim() || "Unnamed member";
8654
- out.log(
8655
- ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
8656
- );
8657
- }
8658
- }
8659
8563
  async function ownersList(options) {
8660
- report(options, await ownersRequest("GET", "", options));
8564
+ await requireStudioHuman(options.configPath, "listing app owners", "app");
8661
8565
  }
8662
8566
  async function ownersAdd(email, options) {
8663
- const owners = await ownersRequest("POST", "", options, { email });
8664
- report(
8665
- options,
8666
- owners,
8667
- `added ${email} as a co-owner \u2014 they share full access. They can now run \`odla-ai provision\` to mint their own credentials (no secret sharing).`
8668
- );
8567
+ await requireStudioHuman(options.configPath, `adding ${email} as an app owner`, "app");
8669
8568
  }
8670
8569
  async function ownersRemove(target, options) {
8671
- let ownerId = target;
8672
- if (target.includes("@")) {
8673
- const owners2 = await ownersRequest("GET", "", options);
8674
- const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
8675
- if (!match) throw new Error(`no co-owner with email ${target}`);
8676
- if (match.primary) throw new Error("can't remove the primary owner");
8677
- ownerId = match.ownerId;
8678
- }
8679
- const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
8680
- report(options, owners, `removed ${target}`);
8570
+ await requireStudioHuman(options.configPath, `removing ${target} as an app owner`, "app");
8681
8571
  }
8682
8572
  async function appOwnersCommand(parsed, dependencies = {}) {
8683
8573
  const sub = parsed.positionals[2] ?? "list";
@@ -8709,35 +8599,9 @@ async function appOwnersCommand(parsed, dependencies = {}) {
8709
8599
 
8710
8600
  // src/app-rename.ts
8711
8601
  async function appRename(name, options) {
8712
- const out = options.stdout ?? console;
8713
8602
  const trimmed = name.trim();
8714
8603
  if (!trimmed) throw new Error('"app rename" needs a name \u2014 try `odla-ai app rename "Acme Storefront"`.');
8715
- const cfg = await loadProjectConfig(options.configPath);
8716
- const doFetch = options.fetch ?? fetch;
8717
- const token = await getDeveloperToken(
8718
- cfg,
8719
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
8720
- doFetch,
8721
- out
8722
- );
8723
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/name`, {
8724
- method: "PUT",
8725
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
8726
- body: JSON.stringify({ name: trimmed })
8727
- });
8728
- const data = await res.json().catch(() => ({}));
8729
- if (!res.ok || !data.app) {
8730
- throw new Error(
8731
- `rename failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
8732
- );
8733
- }
8734
- if (options.json === true) {
8735
- out.log(JSON.stringify(data.app, null, 2));
8736
- return;
8737
- }
8738
- out.log(`renamed ${data.app.appId} \u2192 "${data.app.name}"`);
8739
- out.log("The app id is unchanged, so credentials, tenants, and URLs keep working.");
8740
- out.log(`Update the "name" in ${cfg.configPath} to match.`);
8604
+ await requireStudioHuman(options.configPath, `renaming the app to "${trimmed}"`, "app");
8741
8605
  }
8742
8606
  async function appRenameCommand(parsed, dependencies = {}) {
8743
8607
  assertArgs(parsed, ["config", "token", "email", "json"], parsed.positionals.length);
@@ -8753,172 +8617,23 @@ async function appRenameCommand(parsed, dependencies = {}) {
8753
8617
  }
8754
8618
 
8755
8619
  // src/app-transfer.ts
8756
- function endpointsFor(verb, tenants) {
8757
- const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
8758
- const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
8759
- if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
8760
- return { ...up, mode: "cutover" };
8761
- }
8762
- async function api(cfg, token, path, init, doFetch) {
8763
- const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
8764
- ...init,
8765
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
8766
- });
8767
- return { status: res.status, body: await res.json().catch(() => ({})) };
8768
- }
8769
- function describe(side) {
8770
- if (!side.exists) return "not provisioned";
8771
- if (side.maxTx === 0) return "empty \u2014 never written";
8772
- const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
8773
- const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
8774
- return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
8775
- }
8776
- function printPlan2(out, verb, pre, opts) {
8777
- const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
8778
- out.log(`${verb} (${arrow})`);
8779
- out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
8780
- out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
8781
- if (verb === "promote") {
8782
- out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
8783
- } else {
8784
- out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
8785
- out.log(` stays put: ${pre.staysPut.join(", ")}`);
8786
- const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
8787
- out.log(` identity: ${identity}`);
8788
- out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
8789
- }
8790
- for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
8791
- }
8792
8620
  async function appTransfer(options) {
8793
8621
  const cfg = await loadProjectConfig(options.configPath);
8794
- const out = options.stdout ?? console;
8795
- const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
8796
- const doFetch = options.fetch ?? fetch;
8797
- const tenants = bothTenants(cfg);
8798
- const route2 = endpointsFor(options.verb, tenants);
8799
- const token = await getDeveloperToken(
8800
- cfg,
8801
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
8802
- doFetch,
8803
- out
8804
- );
8805
- const pre = await api(
8806
- cfg,
8807
- token,
8808
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
8809
- void 0,
8810
- doFetch
8811
- );
8812
- if (pre.status !== 200) {
8813
- throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
8814
- }
8815
- const plan = pre.body;
8816
- printPlan2({ log: say }, options.verb, plan, options);
8817
- if (options.verb === "go-live" && !plan.targetEmpty) {
8818
- throw new Error(
8819
- `${route2.target} already has data \u2014 go-live has already happened. Use \`odla-ai app promote --yes\` to push schema and rules, or \`odla-ai app refresh-sandbox --yes\` to pull live back down.`
8820
- );
8821
- }
8822
- if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
8823
- if (options.dryRun || options.yes !== true) {
8824
- say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
8825
- if (options.json) out.log(JSON.stringify(plan, null, 2));
8826
- return { ok: true, plan };
8827
- }
8828
- if (options.verb === "promote") {
8829
- const res2 = await api(
8830
- cfg,
8831
- token,
8832
- `/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
8833
- { method: "POST", body: JSON.stringify({ from: "dev" }) },
8834
- doFetch
8835
- );
8836
- if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
8837
- if (options.json) out.log(JSON.stringify(res2.body, null, 2));
8838
- else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
8839
- return { ok: true, plan, result: res2.body };
8840
- }
8841
- const res = await api(
8842
- cfg,
8843
- token,
8844
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
8845
- {
8846
- method: "POST",
8847
- body: JSON.stringify({
8848
- from: route2.from,
8849
- mode: route2.mode,
8850
- ...options.includeIdentity ? { includeUsers: true } : {},
8851
- ...options.includeFiles ? { includeFiles: true } : {}
8852
- })
8853
- },
8854
- doFetch
8855
- );
8856
- if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
8857
- say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
8858
- if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
8859
- if (options.json) out.log(JSON.stringify(res.body, null, 2));
8860
- return { ok: true, plan, result: res.body };
8861
- }
8862
- async function copyFiles(cfg, token, route2, doFetch, out) {
8863
- for (let attempt = 1; attempt <= 20; attempt++) {
8864
- const res = await api(
8865
- cfg,
8866
- token,
8867
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
8868
- { method: "POST", body: JSON.stringify({ from: route2.from }) },
8869
- doFetch
8870
- );
8871
- if (res.status === 200) {
8872
- out.log(`${route2.target}: files copied`);
8873
- return;
8874
- }
8875
- if (!res.body.error?.retry) {
8876
- throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
8877
- }
8878
- out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
8879
- }
8880
- throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
8622
+ bothTenants(cfg);
8623
+ return requireStudioHuman(options.configPath, `app ${options.verb}`, "database");
8881
8624
  }
8882
8625
 
8883
8626
  // src/app-lifecycle.ts
8884
- async function lifecycleCall(action2, options) {
8885
- const cfg = await loadProjectConfig(options.configPath);
8886
- const out = options.stdout ?? console;
8887
- const doFetch = options.fetch ?? fetch;
8888
- const token = await getDeveloperToken(
8889
- cfg,
8890
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
8891
- doFetch,
8892
- out
8893
- );
8894
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
8895
- method: "POST",
8896
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
8897
- });
8898
- const body = await res.json().catch(() => ({}));
8899
- if (!res.ok || !body.ok) {
8900
- throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
8901
- }
8902
- return body;
8903
- }
8904
8627
  async function appArchive(options) {
8905
8628
  if (options.yes !== true) {
8906
8629
  throw new Error(
8907
8630
  "app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
8908
8631
  );
8909
8632
  }
8910
- const out = options.stdout ?? console;
8911
- const body = await lifecycleCall("archive", options);
8912
- if (options.json) out.log(JSON.stringify(body, null, 2));
8913
- else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
8914
- else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
8633
+ await requireStudioHuman(options.configPath, "app archive", "app");
8915
8634
  }
8916
8635
  async function appRestore(options) {
8917
- const out = options.stdout ?? console;
8918
- const body = await lifecycleCall("restore", options);
8919
- if (options.json) out.log(JSON.stringify(body, null, 2));
8920
- else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
8921
- else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
8636
+ await requireStudioHuman(options.configPath, "app restore", "app");
8922
8637
  }
8923
8638
  async function appCommand(parsed, dependencies = {}) {
8924
8639
  const sub = parsed.positionals[1];
@@ -9554,18 +9269,17 @@ Usage:
9554
9269
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
9555
9270
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
9556
9271
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
9557
- odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
9558
- odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
9559
- odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
9560
- odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
9272
+ odla-ai calendar disconnect [--env dev] --yes [continue in Studio; human session required]
9273
+ odla-ai app archive [--config odla.config.mjs] --yes [continue in Studio; human session required]
9274
+ odla-ai app restore [--config odla.config.mjs] [continue in Studio; human session required]
9275
+ odla-ai app export [--env dev] [continue in Studio; human session required]
9561
9276
  odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
9562
- odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
9563
- odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
9564
- odla-ai app promote [--dry-run] [--json] --yes
9565
- odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
9566
- odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
9567
- odla-ai app owners add <email> [--email <odla-account>] [--json]
9568
- odla-ai app owners remove <email> [--email <odla-account>] [--json]
9277
+ [dry-run is local; writes continue in Studio]
9278
+ odla-ai app refresh-sandbox [continue in Studio; human session required]
9279
+ odla-ai app go-live [continue in Studio; human session required]
9280
+ odla-ai app promote [continue in Studio; human session required]
9281
+ odla-ai app rename <name> [continue in Studio; human session required]
9282
+ odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
9569
9283
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
9570
9284
  odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
9571
9285
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -9600,8 +9314,8 @@ Usage:
9600
9314
  odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
9601
9315
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
9602
9316
  odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
9603
- odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
9604
- odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
9317
+ odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--token <ODLA_API_KEY>] [--json]
9318
+ odla-ai agent retry <job-id> [--env dev] [--token <ODLA_API_KEY>] [--json]
9605
9319
  odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
9606
9320
  odla-ai context list [--json]
9607
9321
  odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
@@ -9637,8 +9351,8 @@ Usage:
9637
9351
  odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
9638
9352
  odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
9639
9353
  odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
9640
- odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
9641
- odla-ai security github disconnect --source <id> [--env dev] [--yes]
9354
+ odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
9355
+ odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
9642
9356
  odla-ai security plan [--env dev] [--json]
9643
9357
  odla-ai security sources [--env dev] [--json]
9644
9358
  odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
@@ -10149,7 +9863,7 @@ async function discussWatch(ctx, topicId, parsed) {
10149
9863
  throw new WatchRemoteError(cursor, error);
10150
9864
  }
10151
9865
  if (deadline !== void 0 && now() >= deadline) {
10152
- const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
9866
+ const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
10153
9867
  throw new WatchTimeoutError(result.cursor);
10154
9868
  }
10155
9869
  const base = Math.min(intervalMs, 1e3);
@@ -10190,7 +9904,7 @@ async function discussWatch(ctx, topicId, parsed) {
10190
9904
  });
10191
9905
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
10192
9906
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
10193
- return report2(ctx, parsed, {
9907
+ return report(ctx, parsed, {
10194
9908
  found: true,
10195
9909
  cursor,
10196
9910
  events: matching,
@@ -10217,13 +9931,13 @@ async function discussWatch(ctx, topicId, parsed) {
10217
9931
  }
10218
9932
  if (page2.hasMore) continue;
10219
9933
  if (deadline !== void 0 && now() >= deadline) {
10220
- return report2(ctx, parsed, { found: false, cursor });
9934
+ return report(ctx, parsed, { found: false, cursor });
10221
9935
  }
10222
9936
  const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
10223
9937
  await sleep(wait2);
10224
9938
  }
10225
9939
  }
10226
- function report2(ctx, parsed, result) {
9940
+ function report(ctx, parsed, result) {
10227
9941
  if (ctx.json) {
10228
9942
  ctx.out.log(JSON.stringify(result, null, 2));
10229
9943
  } else if (parsed.options.jsonl !== true && result.found) {
@@ -10724,7 +10438,7 @@ function eventLabel(event) {
10724
10438
  const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
10725
10439
  return body || event.payload.entityId;
10726
10440
  }
10727
- function report3(ctx, parsed, result) {
10441
+ function report2(ctx, parsed, result) {
10728
10442
  if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
10729
10443
  else if (parsed.options.jsonl !== true && result.found) {
10730
10444
  for (const event of result.events ?? []) {
@@ -10784,7 +10498,7 @@ async function pmWatch(ctx, parsed) {
10784
10498
  });
10785
10499
  if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
10786
10500
  if (deadline !== void 0 && now() >= deadline) {
10787
- return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
10501
+ return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
10788
10502
  }
10789
10503
  const backoff = Math.min(
10790
10504
  MAX_BACKOFF_MS2,
@@ -10825,7 +10539,7 @@ async function pmWatch(ctx, parsed) {
10825
10539
  cursor,
10826
10540
  serverTime: current.serverTime
10827
10541
  });
10828
- return report3(ctx, parsed, { found: true, cursor, events: matching });
10542
+ return report2(ctx, parsed, { found: true, cursor, events: matching });
10829
10543
  }
10830
10544
  if (current.events.length > 0) {
10831
10545
  jsonl2(ctx, parsed, {
@@ -10844,7 +10558,7 @@ async function pmWatch(ctx, parsed) {
10844
10558
  }
10845
10559
  if (current.hasMore) continue;
10846
10560
  if (deadline !== void 0 && now() >= deadline) {
10847
- return report3(ctx, parsed, { found: false, cursor });
10561
+ return report2(ctx, parsed, { found: false, cursor });
10848
10562
  }
10849
10563
  await sleep(
10850
10564
  deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
@@ -12035,7 +11749,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
12035
11749
  return out;
12036
11750
  }
12037
11751
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
12038
- function report4(ctx, impacts) {
11752
+ function report3(ctx, impacts) {
12039
11753
  const covered = impacts.filter((i) => i.runbooks.length);
12040
11754
  ctx.out.log(
12041
11755
  `${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
@@ -12073,7 +11787,7 @@ async function runbookImpact(ctx, options, deps = {}) {
12073
11787
  }
12074
11788
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
12075
11789
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
12076
- report4(ctx, impacts);
11790
+ report3(ctx, impacts);
12077
11791
  }
12078
11792
 
12079
11793
  // src/runbook-lint.ts
@@ -12573,7 +12287,6 @@ async function runbookCommand(parsed, deps = {}) {
12573
12287
  }
12574
12288
 
12575
12289
  // src/security-command-context.ts
12576
- import { createInterface } from "readline/promises";
12577
12290
  async function hostedSecurityContext(parsed, dependencies) {
12578
12291
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
12579
12292
  const cfg = await loadProjectConfig(configPath);
@@ -12592,21 +12305,11 @@ async function hostedSecurityContext(parsed, dependencies) {
12592
12305
  cfg,
12593
12306
  { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
12594
12307
  doFetch,
12595
- stdout
12308
+ stdout,
12309
+ { optionalProjectCapabilities: ["app.manage"] }
12596
12310
  );
12597
12311
  return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
12598
12312
  }
12599
- async function interactiveConfirmation(message2, dependencies) {
12600
- if (dependencies.confirm) return dependencies.confirm(message2);
12601
- if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
12602
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
12603
- try {
12604
- const answer = await prompt.question(`${message2} [y/N] `);
12605
- return /^y(?:es)?$/i.test(answer.trim());
12606
- } finally {
12607
- prompt.close();
12608
- }
12609
- }
12610
12313
  function requiredSecurityPositional(parsed, index, label) {
12611
12314
  const value2 = parsed.positionals[index];
12612
12315
  if (!value2) throw new Error(`${label} is required`);
@@ -12672,31 +12375,31 @@ function printHostedJob(out, job, platform, appId) {
12672
12375
  url.searchParams.set("job", job.jobId);
12673
12376
  out.log(` Studio: ${url.toString()}`);
12674
12377
  }
12675
- function printHostedReport(out, report5) {
12676
- out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
12677
- out.log(` coverage: ${report5.coverageStatus} cells=${report5.metrics.coverageCells} shallow=${report5.metrics.shallowCells} blocked=${report5.metrics.blockedCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
12678
- out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
12679
- out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
12680
- out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
12681
- for (const finding of report5.findings) {
12378
+ function printHostedReport(out, report4) {
12379
+ out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
12380
+ out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
12381
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
12382
+ out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
12383
+ out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
12384
+ for (const finding of report4.findings) {
12682
12385
  const location = finding.locations[0];
12683
12386
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
12684
12387
  }
12685
- for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
12388
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
12686
12389
  }
12687
- function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
12390
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
12688
12391
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12689
12392
  const candidateValue = parsed.options["fail-on-candidates"];
12690
12393
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12691
12394
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
12692
- const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12693
- const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12694
- const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12395
+ const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12396
+ const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12397
+ const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12695
12398
  if (confirmed.length || leads.length || incomplete) {
12696
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
12399
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
12697
12400
  }
12698
12401
  if (emitSuccess) {
12699
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
12402
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
12700
12403
  }
12701
12404
  }
12702
12405
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -12800,13 +12503,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
12800
12503
  }
12801
12504
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
12802
12505
  }
12803
- const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12506
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12804
12507
  if (parsed.options.json === true) {
12805
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
12508
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
12806
12509
  } else {
12807
- printHostedReport(context.stdout, report5);
12510
+ printHostedReport(context.stdout, report4);
12808
12511
  }
12809
- enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
12512
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
12810
12513
  }
12811
12514
  async function runLocalSecurityCommand(parsed, dependencies) {
12812
12515
  if (parsed.options.source === true) {
@@ -12867,19 +12570,20 @@ async function runLocalSecurityCommand(parsed, dependencies) {
12867
12570
  cfg,
12868
12571
  { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
12869
12572
  doFetch,
12870
- out
12573
+ out,
12574
+ { optionalProjectCapabilities: ["app.manage"] }
12871
12575
  );
12872
12576
  }
12873
12577
  });
12874
12578
  enforceLocalGate(result.report, parsed);
12875
12579
  }
12876
- function enforceLocalGate(report5, parsed) {
12580
+ function enforceLocalGate(report4, parsed) {
12877
12581
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12878
12582
  const candidateValue = parsed.options["fail-on-candidates"];
12879
12583
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12880
- const confirmed = findingsAtOrAbove(report5, failOn);
12881
- const leads = failOnCandidates ? findingsAtOrAbove(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12882
- const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12584
+ const confirmed = findingsAtOrAbove(report4, failOn);
12585
+ const leads = failOnCandidates ? findingsAtOrAbove(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12586
+ const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12883
12587
  if (confirmed.length || leads.length || incomplete) {
12884
12588
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
12885
12589
  }
@@ -12918,9 +12622,9 @@ async function securityCommand(parsed, dependencies) {
12918
12622
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
12919
12623
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
12920
12624
  const context = await hostedSecurityContext(parsed, dependencies);
12921
- const report5 = await getHostedSecurityReport({ ...context, jobId });
12922
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
12923
- else printHostedReport(context.stdout, report5);
12625
+ const report4 = await getHostedSecurityReport({ ...context, jobId });
12626
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
12627
+ else printHostedReport(context.stdout, report4);
12924
12628
  return;
12925
12629
  }
12926
12630
  if (sub !== "run") {
@@ -12934,35 +12638,24 @@ async function githubSecurityCommand(parsed, dependencies) {
12934
12638
  const action2 = parsed.positionals[2];
12935
12639
  if (action2 === "disconnect") {
12936
12640
  assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
12937
- const context2 = await hostedSecurityContext(parsed, dependencies);
12938
12641
  const sourceId = requiredString(parsed.options.source, "--source");
12939
- const confirmed = parsed.options.yes === true || await interactiveConfirmation(
12940
- `Disconnect GitHub security source ${sourceId} from ${context2.appId}/${context2.env}?`,
12941
- dependencies
12642
+ return requireStudioHuman(
12643
+ stringOpt(parsed.options.config) ?? "odla.config.mjs",
12644
+ `disconnecting GitHub security source ${sourceId}`,
12645
+ "security",
12646
+ stringOpt(parsed.options.env)
12942
12647
  );
12943
- if (!confirmed) {
12944
- throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
12945
- }
12946
- await disconnectGitHubSecuritySource({ ...context2, sourceId });
12947
- context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
12948
- return;
12949
12648
  }
12950
12649
  if (action2 !== "connect") {
12951
12650
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
12952
12651
  }
12953
12652
  assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
12954
- const context = await hostedSecurityContext(parsed, dependencies);
12955
- const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin).catch(() => void 0);
12956
- const connection = await connectGitHubSecuritySource({
12957
- ...context,
12958
- ...repository === void 0 ? {} : { repository },
12959
- open: parsed.options.open !== false,
12960
- openInstallUrl: dependencies.openUrl ?? openUrl,
12961
- wait: dependencies.pollWait,
12962
- stdout: context.stdout
12963
- });
12964
- context.stdout.log(`github: connected ${connection.repository ?? repository ?? "app repository"} (${connection.sourceId ?? "source pending"})`);
12965
- context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
12653
+ await requireStudioHuman(
12654
+ stringOpt(parsed.options.config) ?? "odla.config.mjs",
12655
+ "connecting the GitHub security repository",
12656
+ "security",
12657
+ stringOpt(parsed.options.env)
12658
+ );
12966
12659
  }
12967
12660
  async function listSecuritySources(parsed, dependencies) {
12968
12661
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
@@ -13213,4 +12906,4 @@ export {
13213
12906
  exitCodeFor,
13214
12907
  runCli
13215
12908
  };
13216
- //# sourceMappingURL=chunk-OGZELVS5.js.map
12909
+ //# sourceMappingURL=chunk-ECRBOR66.js.map