@odla-ai/cli 0.27.12 → 0.27.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -359,6 +359,8 @@ npx odla-ai code connect --env prod --once # bounded enrollment + heartbeat proo
359
359
  # install SDKs, write the Worker, and create wrangler.jsonc before secret push
360
360
  npx odla-ai provision --dry-run
361
361
  npx odla-ai provision --email owner@example.com --write-dev-vars --push-secrets
362
+ # recovery when the selected agent credential has no live app.manage grant
363
+ npx odla-ai provision --request-grant --email owner@example.com --write-dev-vars --push-secrets
362
364
  npx odla-ai smoke --env dev
363
365
  npx odla-ai agent jobs --env dev --state dead_letter --json
364
366
  npx odla-ai agent retry <job-id> --env dev --json
@@ -472,7 +474,11 @@ shown-once credential.
472
474
  needed by the run; it does not permit ownership, rename/category, archive,
473
475
  restore, or purge. A cached or pending baseline handshake without
474
476
  `app.manage` is not reused for provision: the CLI starts a fresh request for
475
- owner review. If
477
+ owner review. An explicit `--token` or `ODLA_DEV_TOKEN` has no trustworthy
478
+ local grant metadata, so a server refusal cannot be repaired by retrying it.
479
+ Run `provision --request-grant --email <account>` to ignore the ambient and
480
+ cached credentials, open a new exact-project review, collect the approved
481
+ replacement, and continue the same provisioning run. If
476
482
  the exact project id does not exist yet, approval reserves that id for this
477
483
  credential instead of failing: the credential may create only that app once,
478
484
  and Registry binds its reviewed grant to the new app incarnation.
package/dist/bin.cjs CHANGED
@@ -318,23 +318,30 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
318
318
 
319
319
  // src/token.ts
320
320
  async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
321
- if (options.token) return options.token;
322
321
  const audience = platformAudience(cfg.platformUrl);
323
- if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
324
- const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
325
- if (declared) {
326
- if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
327
- } else if (audience !== "https://odla.ai") {
328
- throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
329
- }
330
- return import_node_process4.default.env.ODLA_DEV_TOKEN;
331
- }
332
322
  const optionalProjectCapabilities = grantRequest.optionalProjectCapabilities ?? [];
333
323
  const grantIntent = { projectIds: [cfg.app.id], optionalProjectCapabilities };
334
324
  const cached = readJsonFile(cfg.local.tokenFile);
335
- if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
336
- out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
337
- return cached.token;
325
+ if (!grantRequest.forceReview) {
326
+ if (options.token) return options.token;
327
+ if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
328
+ const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
329
+ if (declared) {
330
+ if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
331
+ } else if (audience !== "https://odla.ai") {
332
+ throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
333
+ }
334
+ return import_node_process4.default.env.ODLA_DEV_TOKEN;
335
+ }
336
+ if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
337
+ out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
338
+ return cached.token;
339
+ }
340
+ } else {
341
+ if (options.token) {
342
+ throw new Error("--request-grant cannot be combined with --token; remove --token so the approved replacement credential can be collected and cached");
343
+ }
344
+ out.error(`auth: requesting fresh owner review for app.manage on exact project "${cfg.app.id}"`);
338
345
  }
339
346
  const ctx = {
340
347
  cfg,
@@ -2831,11 +2838,11 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2831
2838
  }
2832
2839
  if (code === "provision_approval_required") {
2833
2840
  throw new Error(
2834
- `${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`
2841
+ `${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. Run "odla-ai provision --request-grant --email <odla-account>" to open one fresh exact-project owner review; do not change app ownership unless the human account itself is not an owner`
2835
2842
  );
2836
2843
  }
2837
2844
  throw new Error(
2838
- `${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`
2845
+ `${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; run "odla-ai provision --request-grant --email <odla-account>" to open a fresh owner review. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
2839
2846
  );
2840
2847
  }
2841
2848
  throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
@@ -8517,7 +8524,7 @@ Usage:
8517
8524
  odla-ai security report <job-id> [--json]
8518
8525
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8519
8526
  odla-ai security run [target] --self --ack-redacted-source
8520
- odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
8527
+ odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
8521
8528
  odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8522
8529
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8523
8530
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
@@ -8652,6 +8659,13 @@ Safety:
8652
8659
  The email is a non-secret identity hint: never provide a password or session
8653
8660
  token. The matching account must already exist, be signed in, explicitly
8654
8661
  review the exact code, and finish any current request before claiming another.
8662
+ If provision reports that the current agent principal has no live app.manage
8663
+ grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
8664
+ the local cache, prints and opens a fresh exact-project owner-review URL, then
8665
+ continues provisioning with the approved replacement credential.
8666
+ Before a non-dry-run provision, the executable checks npm's current CLI
8667
+ release. A confirmed stale client stops with a safe npx rerun command; a
8668
+ workspace-linked client also identifies the worktree that must be updated.
8655
8669
  Run Code from a GitHub checkout already connected to an app in Studio; an
8656
8670
  odla.config.mjs may select the app explicitly but is not required. Code host
8657
8671
  approval and credential hashes live in odla-ai/db. The host
@@ -10617,14 +10631,25 @@ async function provision(options) {
10617
10631
  }
10618
10632
  const doFetch = options.fetch ?? fetch;
10619
10633
  const token = await getDeveloperToken(cfg, options, doFetch, out, {
10620
- optionalProjectCapabilities: ["app.manage"]
10634
+ optionalProjectCapabilities: ["app.manage"],
10635
+ forceReview: options.requestGrant
10621
10636
  });
10622
10637
  const apps = (0, import_apps12.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
10623
10638
  const existing = await apps.resolveApp(cfg.app.id);
10624
10639
  if (existing) {
10625
10640
  out.log(`app: ${cfg.app.id} already exists`);
10626
10641
  } else {
10627
- await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
10642
+ try {
10643
+ await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
10644
+ } catch (error) {
10645
+ if (error instanceof import_apps12.AppsError && error.status === 403) {
10646
+ throw new Error(
10647
+ `app "${cfg.app.id}" does not exist, and this authenticated agent credential has no owner-reviewed app.manage bootstrap grant for that exact id. Run "odla-ai provision --request-grant --email <odla-account>" to open the review URL and continue; developer ownership alone is not agent authority`,
10648
+ { cause: error }
10649
+ );
10650
+ }
10651
+ throw error;
10652
+ }
10628
10653
  out.log(`app: created ${cfg.app.id}`);
10629
10654
  }
10630
10655
  for (const env of cfg.envs) {
@@ -12723,6 +12748,7 @@ async function provisionCommand(parsed, dependencies) {
12723
12748
  "write-credentials",
12724
12749
  "write-dev-vars",
12725
12750
  "token",
12751
+ "request-grant",
12726
12752
  "email",
12727
12753
  "open",
12728
12754
  "wait",
@@ -12738,6 +12764,7 @@ async function provisionCommand(parsed, dependencies) {
12738
12764
  writeCredentials: parsed.options["write-credentials"] !== false,
12739
12765
  writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
12740
12766
  token: stringOpt(parsed.options.token),
12767
+ requestGrant: parsed.options["request-grant"] === true,
12741
12768
  email: stringOpt(parsed.options.email),
12742
12769
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
12743
12770
  wait: numberOpt(parsed.options.wait, "--wait"),
@@ -12774,8 +12801,80 @@ async function calendarCommand(parsed, dependencies) {
12774
12801
  else await calendarDisconnect(options);
12775
12802
  }
12776
12803
 
12804
+ // src/cli-update.ts
12805
+ var import_node_fs20 = require("fs");
12806
+ var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org/@odla-ai%2fcli/latest";
12807
+ var VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
12808
+ async function requireCurrentCliForProvision(argv, options = {}) {
12809
+ if (argv[0] !== "provision" || argv.includes("--dry-run")) return;
12810
+ const current = options.currentVersion ?? cliVersion();
12811
+ if (!VERSION.test(current)) return;
12812
+ const latest = await fetchLatestCliVersion(options);
12813
+ if (!latest || compareVersions(current, latest) >= 0) return;
12814
+ const entryPath = resolvedEntryPath(options.entryPath ?? process.argv[1]);
12815
+ const workspace = isWorkspaceCli(entryPath);
12816
+ const rerun = renderReleasedProvisionCommand(latest, argv);
12817
+ const source = workspace ? ` This executable resolves to the workspace build at ${entryPath}; update/rebase that worktree and rebuild it before using the linked CLI again.` : " Update the installed dependency before using its CLI again.";
12818
+ throw new Error(
12819
+ `provision blocked: @odla-ai/cli ${current} is older than the released ${latest}. Provisioning grant requests are security-sensitive, and a stale client can omit required authority.${source}
12820
+ Run the current release now:
12821
+ ${rerun}`
12822
+ );
12823
+ }
12824
+ async function fetchLatestCliVersion(options) {
12825
+ const controller = new AbortController();
12826
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 2500);
12827
+ timeout.unref?.();
12828
+ try {
12829
+ const response2 = await (options.fetch ?? fetch)(
12830
+ options.registryUrl ?? process.env.ODLA_CLI_REGISTRY_URL ?? DEFAULT_REGISTRY_URL,
12831
+ {
12832
+ headers: {
12833
+ accept: "application/json",
12834
+ "user-agent": `odla-ai-cli/${options.currentVersion ?? cliVersion()}`
12835
+ },
12836
+ signal: controller.signal
12837
+ }
12838
+ );
12839
+ if (!response2.ok) return null;
12840
+ const body = await response2.json();
12841
+ return typeof body.version === "string" && VERSION.test(body.version) ? body.version : null;
12842
+ } catch {
12843
+ return null;
12844
+ } finally {
12845
+ clearTimeout(timeout);
12846
+ }
12847
+ }
12848
+ function resolvedEntryPath(entryPath) {
12849
+ if (!entryPath) return "unknown executable";
12850
+ try {
12851
+ return (0, import_node_fs20.realpathSync)(entryPath);
12852
+ } catch {
12853
+ return entryPath;
12854
+ }
12855
+ }
12856
+ function isWorkspaceCli(entryPath) {
12857
+ const normalized = entryPath.replaceAll("\\", "/");
12858
+ return normalized.includes("/packages/cli/dist/bin.") && !normalized.includes("/node_modules/");
12859
+ }
12860
+ function renderReleasedProvisionCommand(latest, argv) {
12861
+ const safeArgs = [];
12862
+ for (let index = 0; index < argv.length; index++) {
12863
+ const value2 = argv[index];
12864
+ safeArgs.push(value2);
12865
+ if (value2 === "--token" && index + 1 < argv.length) {
12866
+ safeArgs.push("<redacted-token>");
12867
+ index++;
12868
+ }
12869
+ }
12870
+ return ["npx", "--yes", `@odla-ai/cli@${latest}`, ...safeArgs].map(shellQuote).join(" ");
12871
+ }
12872
+ function shellQuote(value2) {
12873
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value2) ? value2 : `'${value2.replaceAll("'", `'"'"'`)}'`;
12874
+ }
12875
+
12777
12876
  // src/bin.ts
12778
- runCli().catch((err) => {
12877
+ requireCurrentCliForProvision(process.argv.slice(2)).then(() => runCli()).catch((err) => {
12779
12878
  console.error(redactSecrets(`odla-ai: ${err instanceof Error ? err.message : String(err)}`));
12780
12879
  process.exitCode = exitCodeFor(err);
12781
12880
  });