@odla-ai/cli 0.27.7 → 0.27.9

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
@@ -467,6 +467,15 @@ shown-once credential.
467
467
  capability request without mutation controls. The owner approves that exact
468
468
  request or declines it with an explanation which the CLI returns to the
469
469
  calling agent; a different identity or access set requires a new request.
470
+ `provision` includes `app.manage` for the configured exact app. That permits
471
+ the non-lifecycle app configuration and sibling-service tenant administration
472
+ needed by the run; it does not permit ownership, rename/category, archive,
473
+ restore, or purge. A cached or pending baseline handshake without
474
+ `app.manage` is not reused for provision: the CLI starts a fresh request for
475
+ owner review. If
476
+ the exact project id does not exist yet, approval reserves that id for this
477
+ credential instead of failing: the credential may create only that app once,
478
+ and Registry binds its reviewed grant to the new app incarnation.
470
479
  Every real CLI handshake prints exactly one canonical `/studio?code=…` URL
471
480
  and attempts to open it — including from CI, SSH, display-less, scripted,
472
481
  and agent-driven shells. Only `--no-open` suppresses the attempt. Browser
@@ -477,7 +486,8 @@ shown-once credential.
477
486
  Outside an interactive terminal the approval wait is capped (90 seconds by
478
487
  default; `--wait <seconds>` overrides), and a still-pending handshake exits
479
488
  with code 75 — approve at the URL, then re-run to collect the token.
480
- 2. Creates the platform app if needed.
489
+ 2. Creates the platform app if needed. For a new id, this consumes the exact-id
490
+ reservation approved in step 1; it is not ambient project-creation authority.
481
491
  3. Enables configured services in every configured environment. Calendar
482
492
  config is normalized per env and fixed to Google read-only access.
483
493
  4. Mints or reuses each env's configured service credentials: an odla-db app
@@ -554,7 +564,13 @@ or approved-but-uncollected request can be active for that user.
554
564
  For an ordinary request, Studio also requires at least one exact project
555
565
  selection. The credential receives project metadata, PM, discussion, and
556
566
  brand-candidate editing grants only for those projects, never ambient owner
557
- access, app administration, or brand approval.
567
+ access, app administration, or brand approval. A selected id that does not yet
568
+ exist is a one-time bootstrap reservation: the credential may create that exact
569
+ app, after which the grant is bound to its new lifetime. A 404 from the approve
570
+ action is a routing regression; missing, archived, foreign, changed, or raced
571
+ requests must produce a specific conflict or denial. The reservation authorizes
572
+ only that bootstrap creation; later service, configuration, and administrative
573
+ mutations still require their normal capabilities and human checkpoints.
558
574
 
559
575
  Developer grants are tracked by id, owner, label, scopes, creation/expiry,
560
576
  last use, and revocation state. The plaintext token is delivered once to the
package/REQUIREMENTS.md CHANGED
@@ -46,6 +46,11 @@ Agnacl, but none should mention or special-case Agnacl.
46
46
  controls. Approval accepts the request digest exactly; denial requires a
47
47
  bounded, non-secret explanation returned to the polling agent. Any changed
48
48
  scope, project, or capability requires a fresh request.
49
+ - An approved exact project id may be absent at review time. Collection must
50
+ reserve that id for the approved credential, which may create only that app
51
+ once; creation binds the reviewed grant to the new app incarnation. Existing
52
+ foreign or archived ids remain unapprovable. Approval precondition failures
53
+ must return an actionable conflict—never a route-like 404.
49
54
  - Only one claimed pending or approved-but-uncollected request may be active per
50
55
  user. Unclaimed issued codes must not occupy that slot.
51
56
  - Every real CLI handshake must attempt to open that canonical approval URL,
package/dist/bin.cjs CHANGED
@@ -269,17 +269,24 @@ function handshakeFile(cfg) {
269
269
  return (0, import_node_path2.join)((0, import_node_path2.dirname)(cfg.local.tokenFile), "handshake.local.json");
270
270
  }
271
271
  var RESUME_MARGIN_MS = 5e3;
272
- function readPendingHandshake(path, platform, email) {
272
+ function readPendingHandshake(path, platform, email, requiredGrant) {
273
273
  const pending = readJsonFile(path);
274
274
  if (!pending || pending.platform !== platform || pending.email !== email) return null;
275
275
  if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
276
276
  if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
277
+ if (requiredGrant && !grantCovers(pending, requiredGrant)) return null;
277
278
  return {
278
279
  interval: typeof pending.interval === "number" ? pending.interval : 3,
279
280
  ...pending,
280
281
  approvalUrl: handshakeUrl(platform, pending.userCode)
281
282
  };
282
283
  }
284
+ function grantCovers(stored, required) {
285
+ if (required.optionalProjectCapabilities.length === 0) return true;
286
+ return required.projectIds.every((id) => stored.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
287
+ (capability) => stored.optionalProjectCapabilities?.includes(capability)
288
+ );
289
+ }
283
290
  function writePendingHandshake(path, pending) {
284
291
  writePrivateJson(path, pending);
285
292
  }
@@ -310,7 +317,7 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
310
317
  }
311
318
 
312
319
  // src/token.ts
313
- async function getDeveloperToken(cfg, options, doFetch, out) {
320
+ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
314
321
  if (options.token) return options.token;
315
322
  const audience = platformAudience(cfg.platformUrl);
316
323
  if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
@@ -322,8 +329,10 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
322
329
  }
323
330
  return import_node_process4.default.env.ODLA_DEV_TOKEN;
324
331
  }
332
+ const optionalProjectCapabilities = grantRequest.optionalProjectCapabilities ?? [];
333
+ const grantIntent = { projectIds: [cfg.app.id], optionalProjectCapabilities };
325
334
  const cached = readJsonFile(cfg.local.tokenFile);
326
- if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
335
+ if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
327
336
  out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
328
337
  return cached.token;
329
338
  }
@@ -334,17 +343,30 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
334
343
  out,
335
344
  audience,
336
345
  email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
337
- pendingFile: handshakeFile(cfg)
346
+ pendingFile: handshakeFile(cfg),
347
+ grantIntent
338
348
  };
339
349
  const waitMs = handshakeWaitMs(options.wait);
340
350
  const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
341
351
  clearPendingHandshake(ctx.pendingFile);
342
- writePrivateJson(cfg.local.tokenFile, { platform: audience, email: ctx.email, token, expiresAt });
352
+ writePrivateJson(cfg.local.tokenFile, {
353
+ platform: audience,
354
+ email: ctx.email,
355
+ projectIds: grantIntent.projectIds,
356
+ optionalProjectCapabilities,
357
+ token,
358
+ expiresAt
359
+ });
343
360
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
344
361
  return token;
345
362
  }
346
363
  async function resumePendingHandshake(ctx, waitMs) {
347
- const pending = readPendingHandshake(ctx.pendingFile, ctx.audience, ctx.email);
364
+ const pending = readPendingHandshake(
365
+ ctx.pendingFile,
366
+ ctx.audience,
367
+ ctx.email,
368
+ ctx.grantIntent
369
+ );
348
370
  if (!pending) return null;
349
371
  ctx.out.error("");
350
372
  ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
@@ -389,6 +411,7 @@ async function freshHandshake(ctx, waitMs) {
389
411
  label: `${ctx.cfg.app.id} provisioner`,
390
412
  agentHandle: projectAgentHandle(ctx.cfg.app.id),
391
413
  projectIds: [ctx.cfg.app.id],
414
+ optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities,
392
415
  fetch: ctx.doFetch,
393
416
  waitMs,
394
417
  onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
@@ -400,7 +423,9 @@ async function freshHandshake(ctx, waitMs) {
400
423
  deviceCode,
401
424
  approvalUrl,
402
425
  interval,
403
- expiresAt: Date.now() + expiresIn * 1e3
426
+ expiresAt: Date.now() + expiresIn * 1e3,
427
+ projectIds: ctx.grantIntent.projectIds,
428
+ optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities
404
429
  };
405
430
  writePendingHandshake(ctx.pendingFile, started);
406
431
  await presentHandshakeApproval(ctx.out, {
@@ -423,6 +448,12 @@ async function freshHandshake(ctx, waitMs) {
423
448
  stopReminder?.();
424
449
  }
425
450
  }
451
+ function cachedGrantCovers(cached, required) {
452
+ if (required.optionalProjectCapabilities.length === 0) return true;
453
+ return required.projectIds.every((id) => cached.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
454
+ (capability) => cached.optionalProjectCapabilities?.includes(capability)
455
+ );
456
+ }
426
457
  function projectAgentHandle(appId) {
427
458
  const candidate = /^[a-z]/.test(appId) ? appId : `app-${appId}`;
428
459
  if (candidate.length <= 32) return candidate;
@@ -3086,7 +3117,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
3086
3117
  if (res.ok || res.status === 404) return;
3087
3118
  if (res.status === 403) {
3088
3119
  throw new Error(
3089
- `${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
3120
+ `${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`
3090
3121
  );
3091
3122
  }
3092
3123
  throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
@@ -10923,7 +10954,9 @@ async function provision(options) {
10923
10954
  out.log("secrets: Wrangler config and login preflight passed");
10924
10955
  }
10925
10956
  const doFetch = options.fetch ?? fetch;
10926
- const token = await getDeveloperToken(cfg, options, doFetch, out);
10957
+ const token = await getDeveloperToken(cfg, options, doFetch, out, {
10958
+ optionalProjectCapabilities: ["app.manage"]
10959
+ });
10927
10960
  const apps = (0, import_apps12.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
10928
10961
  const existing = await apps.resolveApp(cfg.app.id);
10929
10962
  if (existing) {