@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 +18 -2
- package/REQUIREMENTS.md +5 -0
- package/dist/bin.cjs +42 -9
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-MZSU4YQL.js → chunk-LT56MKDK.js} +43 -10
- package/dist/chunk-LT56MKDK.js.map +1 -0
- package/dist/index.cjs +42 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +2 -1
- package/skills/odla/references/agent-identity.md +9 -2
- package/skills/odla/references/build.md +11 -1
- package/skills/odla/references/co-owners.md +11 -13
- package/dist/chunk-MZSU4YQL.js.map +0 -1
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, {
|
|
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(
|
|
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}:
|
|
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) {
|