@odla-ai/cli 0.36.1 → 0.37.1

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/dist/bin.cjs CHANGED
@@ -838,6 +838,7 @@ var init_admin_ai_auth = __esm({
838
838
  "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
839
839
  "platform:runbook:write": "read and edit all of odla's operational runbooks, including admin-visible content",
840
840
  "app:device:enroll": "enrol this machine so it can mint its own short-lived credentials without asking you again",
841
+ "platform:device:enroll:extended": "enrol this machine with a credential that outlives the seven-day owner ceiling \u2014 a durable secret on a machine you are vouching for",
841
842
  "platform:ai:policy:write": "change System AI model routing",
842
843
  "platform:ai:policy:read": "read System AI model routing",
843
844
  "platform:ai:credential:write": "replace a stored AI provider key",
@@ -7646,7 +7647,7 @@ function createCodeRuntimeControlClient(options) {
7646
7647
  throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
7647
7648
  }
7648
7649
  const request3 = options.fetch ?? fetch;
7649
- const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
7650
+ const call3 = async (path, body, timeoutMs = requestTimeoutMs) => {
7650
7651
  const timeout = AbortSignal.timeout(timeoutMs);
7651
7652
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
7652
7653
  let response2;
@@ -7676,17 +7677,17 @@ function createCodeRuntimeControlClient(options) {
7676
7677
  return {
7677
7678
  heartbeat: async (version, capabilities) => {
7678
7679
  validateHeartbeat(version, capabilities);
7679
- return parseSnapshot(await call2("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7680
+ return parseSnapshot(await call3("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
7680
7681
  },
7681
7682
  acknowledge: async (commandId, result) => {
7682
7683
  if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
7683
- await call2(`/registry/code/runtime/commands/${commandId}/ack`, result);
7684
+ await call3(`/registry/code/runtime/commands/${commandId}/ack`, result);
7684
7685
  },
7685
7686
  source: async (sessionId) => parseSource(
7686
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7687
+ await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
7687
7688
  ),
7688
7689
  infer: async (sessionId, inference) => {
7689
- const value2 = record5(await call2(
7690
+ const value2 = record5(await call3(
7690
7691
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
7691
7692
  inference,
7692
7693
  modelRequestTimeoutMs
@@ -7697,11 +7698,11 @@ function createCodeRuntimeControlClient(options) {
7697
7698
  return value2;
7698
7699
  },
7699
7700
  review: async (sessionId, review) => parseReview(
7700
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7701
+ await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
7701
7702
  ),
7702
7703
  submitCandidate: async (sessionId, checkpointId, verification) => {
7703
7704
  if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
7704
- return parseCandidate(await call2(
7705
+ return parseCandidate(await call3(
7705
7706
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
7706
7707
  { checkpointId, verification }
7707
7708
  ));
@@ -7711,21 +7712,21 @@ function createCodeRuntimeControlClient(options) {
7711
7712
  if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
7712
7713
  throw new TypeError("invalid Code session event");
7713
7714
  }
7714
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7715
+ await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
7715
7716
  },
7716
7717
  recallMemories: async (sessionId, subjects, limit) => {
7717
- const response2 = await call2(
7718
+ const response2 = await call3(
7718
7719
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
7719
7720
  { subjects: [...subjects], limit }
7720
7721
  );
7721
7722
  return Array.isArray(response2.memories) ? response2.memories : [];
7722
7723
  },
7723
7724
  rememberMemory: async (sessionId, memory) => {
7724
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7725
+ await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
7725
7726
  },
7726
7727
  reportSessionFailure: async (sessionId, message2) => {
7727
7728
  if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
7728
- await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7729
+ await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
7729
7730
  }
7730
7731
  };
7731
7732
  }
@@ -8511,7 +8512,7 @@ async function materializeCommandWorkspace(input) {
8511
8512
  }
8512
8513
  function codeSkill(opts) {
8513
8514
  let seq = 0;
8514
- const call2 = async (tool, input, signal) => {
8515
+ const call3 = async (tool, input, signal) => {
8515
8516
  const startedAt = Date.now();
8516
8517
  const response2 = await opts.broker.execute(
8517
8518
  { lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
@@ -8533,7 +8534,7 @@ function codeSkill(opts) {
8533
8534
  },
8534
8535
  additionalProperties: false
8535
8536
  },
8536
- handler: (input, ctx) => call2("sandbox.read", input, ctx.signal)
8537
+ handler: (input, ctx) => call3("sandbox.read", input, ctx.signal)
8537
8538
  };
8538
8539
  const applyPatch = {
8539
8540
  name: "odla_apply_git_diff",
@@ -8544,7 +8545,7 @@ function codeSkill(opts) {
8544
8545
  properties: { patch: { type: "string", minLength: 1, maxLength: 262144 } },
8545
8546
  additionalProperties: false
8546
8547
  },
8547
- handler: (input, ctx) => call2("sandbox.apply_patch", input, ctx.signal)
8548
+ handler: (input, ctx) => call3("sandbox.apply_patch", input, ctx.signal)
8548
8549
  };
8549
8550
  const runRecipe = {
8550
8551
  name: "odla_run_recipe",
@@ -8555,7 +8556,7 @@ function codeSkill(opts) {
8555
8556
  properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
8556
8557
  additionalProperties: false
8557
8558
  },
8558
- handler: (input, ctx) => call2("sandbox.run_recipe", input, ctx.signal)
8559
+ handler: (input, ctx) => call3("sandbox.run_recipe", input, ctx.signal)
8559
8560
  };
8560
8561
  const listFiles2 = {
8561
8562
  name: "odla_list",
@@ -8568,7 +8569,7 @@ function codeSkill(opts) {
8568
8569
  },
8569
8570
  additionalProperties: false
8570
8571
  },
8571
- handler: (input, ctx) => call2("sandbox.list", input, ctx.signal)
8572
+ handler: (input, ctx) => call3("sandbox.list", input, ctx.signal)
8572
8573
  };
8573
8574
  const searchFiles = {
8574
8575
  name: "odla_search",
@@ -8584,7 +8585,7 @@ function codeSkill(opts) {
8584
8585
  },
8585
8586
  additionalProperties: false
8586
8587
  },
8587
- handler: (input, ctx) => call2("sandbox.search", input, ctx.signal)
8588
+ handler: (input, ctx) => call3("sandbox.search", input, ctx.signal)
8588
8589
  };
8589
8590
  const graphTool = (name, tool, description, required) => ({
8590
8591
  name,
@@ -8595,7 +8596,7 @@ function codeSkill(opts) {
8595
8596
  properties: { query: { type: "string", maxLength: 512 } },
8596
8597
  additionalProperties: false
8597
8598
  },
8598
- handler: (input, ctx) => call2(tool, input, ctx.signal)
8599
+ handler: (input, ctx) => call3(tool, input, ctx.signal)
8599
8600
  });
8600
8601
  const orientation = [
8601
8602
  graphTool(
@@ -8634,9 +8635,9 @@ async function runCodeAgent(options) {
8634
8635
  lease: options.lease,
8635
8636
  workspaceDir: options.workspaceDir,
8636
8637
  surface,
8637
- onToolCall: (call2) => {
8638
- toolCalls.push(call2);
8639
- options.onToolCall?.(call2);
8638
+ onToolCall: (call3) => {
8639
+ toolCalls.push(call3);
8640
+ options.onToolCall?.(call3);
8640
8641
  }
8641
8642
  });
8642
8643
  const compaction = options.compaction === void 0 ? (0, import_ai4.keepRecentExchanges)({ whenInputTokensExceed: 12e4, keep: 3 }) : options.compaction;
@@ -11184,7 +11185,7 @@ Usage:
11184
11185
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
11185
11186
  odla-ai security run [target] --self --ack-redacted-source
11186
11187
  odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
11187
- odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--email <odla-account>] [--no-open] [--json]
11188
+ odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
11188
11189
  odla-ai device list [--email <odla-account>] [--json]
11189
11190
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
11190
11191
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
@@ -13865,9 +13866,15 @@ async function provisionEnvCredentials(opts) {
13865
13866
  if (o11yToken) {
13866
13867
  opts.stdout.log(`${opts.env}: reusing local o11y ingest token`);
13867
13868
  } else {
13868
- o11yToken = await issueO11yToken(opts);
13869
- credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
13870
- opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
13869
+ o11yToken = await issueO11yToken(opts) ?? void 0;
13870
+ if (o11yToken) {
13871
+ credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
13872
+ opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
13873
+ } else {
13874
+ opts.stdout.log(
13875
+ `${opts.env}: o11y token already exists and its shown-once value is not on this machine \u2014 leaving it alone (a co-owner provisioned this env). Nothing was rotated, and this env's running Worker keeps its token. To replace it deliberately \u2014 which invalidates the token on whatever is deployed \u2014 run "odla-ai provision --rotate-o11y-token --push-secrets".`
13876
+ );
13877
+ }
13871
13878
  }
13872
13879
  }
13873
13880
  return save(opts, credentials, tenantId, {
@@ -13921,11 +13928,7 @@ async function issueO11yToken(opts) {
13921
13928
  `${opts.cfg.platformUrl}/o11y/${encodeURIComponent(opts.cfg.app.id)}/token${suffix}?env=${encodeURIComponent(opts.env)}`,
13922
13929
  { method: "POST", headers: { authorization: `Bearer ${opts.developerToken}` } }
13923
13930
  );
13924
- if (res.status === 409 && !opts.rotateO11y) {
13925
- throw new Error(
13926
- `o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --push-secrets" to install a separate runtime credential without rotating siblings`
13927
- );
13928
- }
13931
+ if (res.status === 409 && !opts.rotateO11y) return null;
13929
13932
  if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
13930
13933
  const body = await res.json();
13931
13934
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
@@ -14546,6 +14549,31 @@ var init_advisory_output = __esm({
14546
14549
  }
14547
14550
  });
14548
14551
 
14552
+ // src/device-ttl.ts
14553
+ function parseDeviceTtl(raw) {
14554
+ if (raw === void 0 || raw === true) return void 0;
14555
+ const text3 = String(raw).trim().toLowerCase();
14556
+ if (!text3) return void 0;
14557
+ if (text3 === "forever" || text3 === "never") return 100 * 365 * DAY_MS;
14558
+ const match = /^(\d+)\s*([dwy])$/.exec(text3);
14559
+ if (!match) {
14560
+ throw new Error(
14561
+ `--device-ttl expects a duration like 30d, 6w, 2y, or "forever" (got "${raw}")`
14562
+ );
14563
+ }
14564
+ return Number(match[1]) * UNITS[match[2]];
14565
+ }
14566
+ var OWNER_DEVICE_TTL_MS, DAY_MS, UNITS;
14567
+ var init_device_ttl = __esm({
14568
+ "src/device-ttl.ts"() {
14569
+ "use strict";
14570
+ init_cjs_shims();
14571
+ OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
14572
+ DAY_MS = 24 * 60 * 60 * 1e3;
14573
+ UNITS = { d: DAY_MS, w: 7 * DAY_MS, y: 365 * DAY_MS };
14574
+ }
14575
+ });
14576
+
14549
14577
  // src/device-command.ts
14550
14578
  async function deviceCommand(parsed, deps) {
14551
14579
  const action2 = parsed.positionals[1] ?? "";
@@ -14562,7 +14590,17 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14562
14590
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
14563
14591
  const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
14564
14592
  if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
14565
- const token = await scopedToken2(parsed, deps, cfg, doFetch, out, `odla CLI (enroll ${name})`);
14593
+ const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
14594
+ const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
14595
+ const token = await scopedToken2(
14596
+ parsed,
14597
+ deps,
14598
+ cfg,
14599
+ doFetch,
14600
+ out,
14601
+ `odla CLI (enroll ${name})`,
14602
+ extended ? "platform:device:enroll:extended" : "app:device:enroll"
14603
+ );
14566
14604
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
14567
14605
  method: "POST",
14568
14606
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
@@ -14570,7 +14608,8 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14570
14608
  name,
14571
14609
  platform: import_node_process15.default.platform,
14572
14610
  appIds: apps,
14573
- ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {}
14611
+ ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
14612
+ ...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
14574
14613
  })
14575
14614
  });
14576
14615
  const body = await response2.json().catch(() => ({}));
@@ -14623,12 +14662,12 @@ async function revoke(parsed, deps, cfg, doFetch, out, json) {
14623
14662
  out.error(`device: revoked ${deviceId}; every credential it minted is revoked with it`);
14624
14663
  if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
14625
14664
  }
14626
- async function scopedToken2(parsed, deps, cfg, doFetch, out, label) {
14665
+ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app:device:enroll") {
14627
14666
  const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
14628
14667
  const scopedTokenFile = credentials.scopedTokenFile;
14629
14668
  return getScopedPlatformToken({
14630
14669
  platform: cfg.platformUrl,
14631
- scope: "app:device:enroll",
14670
+ scope,
14632
14671
  email: stringOpt(parsed.options.email),
14633
14672
  label,
14634
14673
  fetch: doFetch,
@@ -14648,6 +14687,7 @@ var init_device_command = __esm({
14648
14687
  "src/device-command.ts"() {
14649
14688
  "use strict";
14650
14689
  init_cjs_shims();
14690
+ init_device_ttl();
14651
14691
  import_node_fs20 = require("fs");
14652
14692
  import_node_path18 = require("path");
14653
14693
  import_node_process15 = __toESM(require("process"), 1);
@@ -14860,12 +14900,7 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
14860
14900
  );
14861
14901
  }
14862
14902
  async function upsert(ctx, r, visibility) {
14863
- const page2 = await call(
14864
- ctx,
14865
- "GET",
14866
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
14867
- );
14868
- const found = page2.records[0];
14903
+ const found = await bySlug(ctx, r.slug).catch(() => null);
14869
14904
  if (!found) {
14870
14905
  await call(ctx, "POST", "/runbook", {
14871
14906
  appId: ctx.appId,
@@ -15303,13 +15338,7 @@ async function runbookAsk(ctx, question, all) {
15303
15338
  ctx.out.log(JSDOC_POINTER);
15304
15339
  }
15305
15340
  async function runbookComment(ctx, slug, body) {
15306
- const page2 = await call(
15307
- ctx,
15308
- "GET",
15309
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
15310
- );
15311
- const found = page2.records[0];
15312
- if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
15341
+ const found = await bySlug(ctx, slug);
15313
15342
  await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
15314
15343
  ctx.out.log(`commented on ${slug} (v${found.version})`);
15315
15344
  }
@@ -15376,13 +15405,7 @@ var init_runbook_editor = __esm({
15376
15405
 
15377
15406
  // src/runbook-edit-flow.ts
15378
15407
  async function editRunbook(ctx, slug, deps = {}) {
15379
- const page2 = await call(
15380
- ctx,
15381
- "GET",
15382
- `/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
15383
- );
15384
- const found = page2.records[0];
15385
- if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
15408
+ const found = await bySlug(ctx, slug);
15386
15409
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
15387
15410
  const body = await editText(found.body, slug, deps);
15388
15411
  return body === null ? null : { body, expectedVersion: found.version };