@odla-ai/cli 0.25.22 → 0.26.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/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  CODE_BUILD_RECIPES: () => CODE_BUILD_RECIPES,
37
37
  CODE_PI_IMAGE: () => CODE_PI_IMAGE,
38
38
  COMMAND_SURFACE: () => COMMAND_SURFACE,
39
+ ConfigOperationCommandError: () => ConfigOperationCommandError,
39
40
  GOOGLE_CALENDAR_EVENTS_SCOPE: () => GOOGLE_CALENDAR_EVENTS_SCOPE,
40
41
  SYSTEM_AI_PURPOSES: () => SYSTEM_AI_PURPOSES,
41
42
  acceptedAfter: () => acceptedAfter,
@@ -47,7 +48,10 @@ __export(index_exports, {
47
48
  calendarServiceConfig: () => calendarServiceConfig,
48
49
  calendarStatus: () => calendarStatus,
49
50
  codeConnect: () => codeConnect,
51
+ configApply: () => configApply,
50
52
  configDiff: () => configDiff,
53
+ configOperationGet: () => configOperationGet,
54
+ configOperationWait: () => configOperationWait,
51
55
  configPlan: () => configPlan,
52
56
  connectGitHubSecuritySource: () => connectGitHubSecuritySource,
53
57
  describeProblem: () => describeProblem,
@@ -246,10 +250,10 @@ function isManagedDevVar(line) {
246
250
  const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
247
251
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
248
252
  }
249
- function writePrivateText(path, text) {
253
+ function writePrivateText(path, text2) {
250
254
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
251
255
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
252
- (0, import_node_fs.writeFileSync)(temporary, text, { mode: 384 });
256
+ (0, import_node_fs.writeFileSync)(temporary, text2, { mode: 384 });
253
257
  (0, import_node_fs.chmodSync)(temporary, 384);
254
258
  (0, import_node_fs.renameSync)(temporary, path);
255
259
  }
@@ -556,7 +560,9 @@ function audienceBoundEnvToken(token, platform) {
556
560
  }
557
561
  var SCOPE_PURPOSE = {
558
562
  "platform:status:read": "read the platform fleet health and deployment snapshot",
563
+ "platform:chat:credential:write": "rotate the built-in Discussion responder credential",
559
564
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
565
+ "app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
560
566
  "platform:runbook:write": "add or edit odla's operational runbooks",
561
567
  "platform:ai:policy:write": "change System AI model routing",
562
568
  "platform:ai:policy:read": "read System AI model routing",
@@ -614,6 +620,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
614
620
  return token;
615
621
  }
616
622
 
623
+ // src/principal-presentation.ts
624
+ function unresolvedPrincipalLabel(credentialKind2, principalId) {
625
+ const kind = typeof credentialKind2 === "string" ? credentialKind2.trim() : "";
626
+ const id = typeof principalId === "string" ? principalId.trim() : "";
627
+ const audit = kind && id ? `${kind}:${id}` : kind || id;
628
+ return `Unknown principal${audit ? ` [${audit}]` : ""}`;
629
+ }
630
+
617
631
  // src/admin-ai-audit.ts
618
632
  function adminAiAuditQuery(filters) {
619
633
  if (filters.limit === void 0) return "";
@@ -643,17 +657,17 @@ async function readAdminAiAudit(request2) {
643
657
  String(event.changeKind ?? ""),
644
658
  String(event.purpose ?? event.provider ?? ""),
645
659
  route2,
646
- `${String(event.actorType ?? "")}:${String(event.actorId ?? "")}`
660
+ unresolvedPrincipalLabel(event.actorType, event.actorId)
647
661
  ].join(" "));
648
662
  }
649
663
  }
650
664
  async function responseBody(response2) {
651
- const text = await response2.text();
652
- if (!text) return {};
665
+ const text2 = await response2.text();
666
+ if (!text2) return {};
653
667
  try {
654
- return JSON.parse(text);
668
+ return JSON.parse(text2);
655
669
  } catch {
656
- return { message: text.slice(0, 300) };
670
+ return { message: text2.slice(0, 300) };
657
671
  }
658
672
  }
659
673
  function apiError(status, body) {
@@ -734,7 +748,7 @@ when app/env run actor purpose/role route / policy tokens cost status`);
734
748
  timestamp2(event.created_at),
735
749
  `${String(event.app_id ?? "")}/${String(event.env ?? "")}`,
736
750
  String(event.run_id ?? ""),
737
- `${String(event.actor_type ?? "")}:${String(event.actor_id ?? "")}`,
751
+ unresolvedPrincipalLabel(event.actor_type, event.actor_id),
738
752
  `${String(event.purpose ?? "")}/${String(event.role ?? "")}`,
739
753
  `${String(event.provider ?? "")}/${String(event.model ?? "")}@v${String(event.policy_version ?? "unknown")}`,
740
754
  String(input + output),
@@ -755,12 +769,12 @@ function timestamp2(value2) {
755
769
  return Number.isFinite(date.valueOf()) ? date.toISOString() : "";
756
770
  }
757
771
  async function responseBody2(res) {
758
- const text = await res.text();
759
- if (!text) return {};
772
+ const text2 = await res.text();
773
+ if (!text2) return {};
760
774
  try {
761
- return JSON.parse(text);
775
+ return JSON.parse(text2);
762
776
  } catch {
763
- return { message: text.slice(0, 300) };
777
+ return { message: text2.slice(0, 300) };
764
778
  }
765
779
  }
766
780
  function apiError2(action2, status, body) {
@@ -936,12 +950,12 @@ function catalogModels(body) {
936
950
  return body.catalog.models.filter((value2) => isRecord3(value2) && typeof value2.id === "string" && typeof value2.provider === "string");
937
951
  }
938
952
  async function responseBody3(res) {
939
- const text = await res.text();
940
- if (!text) return {};
953
+ const text2 = await res.text();
954
+ if (!text2) return {};
941
955
  try {
942
- return JSON.parse(text);
956
+ return JSON.parse(text2);
943
957
  } catch {
944
- return { message: text.slice(0, 300) };
958
+ return { message: text2.slice(0, 300) };
945
959
  }
946
960
  }
947
961
  function apiError3(action2, status, body) {
@@ -1112,6 +1126,7 @@ function unique(values) {
1112
1126
  var DEFAULT_PLATFORM = "https://odla.ai";
1113
1127
  var DEFAULT_ENVS = ["dev"];
1114
1128
  var DEFAULT_SERVICES = ["db", "ai"];
1129
+ var configImportSerial = 0;
1115
1130
  var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
1116
1131
  async function loadProjectConfig(configPath = "odla.config.mjs") {
1117
1132
  const resolved = (0, import_node_path4.resolve)(configPath);
@@ -1319,7 +1334,8 @@ function validId2(value2) {
1319
1334
  }
1320
1335
  async function loadConfigModule(path) {
1321
1336
  if (path.endsWith(".json")) return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
1322
- const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?t=${Date.now()}`);
1337
+ const nonce = `${Date.now()}-${configImportSerial++}`;
1338
+ const mod = await import(`${(0, import_node_url.pathToFileURL)(path).href}?reload=${nonce}`);
1323
1339
  const value2 = mod.default ?? mod.config;
1324
1340
  if (typeof value2 === "function") return await value2();
1325
1341
  return value2;
@@ -1783,8 +1799,8 @@ async function appImport(options) {
1783
1799
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1784
1800
  const doFetch = options.fetch ?? fetch;
1785
1801
  const { tenant } = resolveTenant(cfg, options.env);
1786
- const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1787
- const { format, sources } = (0, import_import.parseImport)(text, options.ns);
1802
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1803
+ const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
1788
1804
  if (format === "namespace-map" && options.ns) {
1789
1805
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
1790
1806
  }
@@ -1875,7 +1891,10 @@ function report(options, owners, headline) {
1875
1891
  if (headline) out.log(headline);
1876
1892
  out.log(`owners (${owners.length}):`);
1877
1893
  for (const o of owners) {
1878
- out.log(` ${o.primary ? "\u2605" : "\xB7"} ${o.email ?? o.ownerId}${o.primary ? " (primary)" : ""}`);
1894
+ const name = o.email?.trim() || "Unnamed member";
1895
+ out.log(
1896
+ ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
1897
+ );
1879
1898
  }
1880
1899
  }
1881
1900
  async function ownersList(options) {
@@ -2703,7 +2722,8 @@ var CAPABILITIES = {
2703
2722
  "save and explicitly select non-secret named operator contexts with isolated credential caches; resolve and explain platform, app, environment, and credential provenance without authenticating; then run PM, Discussions, o11y, runbook, and identity operations outside a project checkout",
2704
2723
  "read one versioned o11y status envelope spanning application RED, exact Worker versions and Cloudflare colos observed in traffic, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, account-scoped Durable Object, D1, and R2 evidence under odla-db, and a bounded machine verdict",
2705
2724
  "read one canonical platform fleet snapshot over private service bindings, including release identities, probe latency, Cloudflare load/runtime freshness, explicit unknowns, and stable next actions through a read-only capability",
2706
- "compare one project's checked-in Registry intent with live owner-visible state and freeze a secret-free plan digest through an exact app:config:read capability",
2725
+ "compare one project's checked-in Registry intent with live owner-visible state, freeze a secret-free Registry-revision-bound plan through app:config:read, and conditionally apply checkpoint-free actions through exact app:config:write operation routes",
2726
+ "inspect or bounded-wait one exact durable config-operation journal entry and verify every terminal receipt digest before returning it to remote automation",
2707
2727
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
2708
2728
  "run app-attributed hosted security discovery and independent validation without provider keys",
2709
2729
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -2750,10 +2770,31 @@ function printGroup(out, heading, items) {
2750
2770
  out.log("");
2751
2771
  }
2752
2772
 
2753
- // src/config-reconcile-command.ts
2773
+ // src/config-operation-command.ts
2754
2774
  var import_apps6 = require("@odla-ai/apps");
2755
2775
  var import_node_path7 = require("path");
2756
2776
 
2777
+ // src/version.ts
2778
+ var import_node_fs9 = require("fs");
2779
+ function cliVersion() {
2780
+ const pkg = JSON.parse((0, import_node_fs9.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
2781
+ return pkg.version ?? "unknown";
2782
+ }
2783
+
2784
+ // src/config-operation-error.ts
2785
+ var ConfigOperationCommandError = class extends Error {
2786
+ constructor(message2, code) {
2787
+ super(message2);
2788
+ this.code = code;
2789
+ this.name = "ConfigOperationCommandError";
2790
+ }
2791
+ code;
2792
+ };
2793
+
2794
+ // src/config-operation-validate.ts
2795
+ var import_apps3 = require("@odla-ai/apps");
2796
+ var import_node_fs10 = require("fs");
2797
+
2757
2798
  // src/config-reconcile-digest.ts
2758
2799
  var import_node_crypto = require("crypto");
2759
2800
  function canonicalJson(value2) {
@@ -2763,27 +2804,160 @@ function configDigest(value2) {
2763
2804
  return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
2764
2805
  }
2765
2806
  function canonicalValue(value2) {
2807
+ if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") return value2;
2808
+ if (typeof value2 === "number") {
2809
+ if (!Number.isFinite(value2)) throw new TypeError("canonical JSON rejects non-finite numbers");
2810
+ return value2;
2811
+ }
2766
2812
  if (Array.isArray(value2)) return value2.map(canonicalValue);
2767
2813
  if (value2 && typeof value2 === "object") {
2814
+ const record11 = value2;
2768
2815
  return Object.fromEntries(
2769
- Object.entries(value2).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalValue(entry)])
2816
+ Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, canonicalValue(record11[key])])
2770
2817
  );
2771
2818
  }
2772
- return value2;
2819
+ throw new TypeError("canonical JSON rejects unsupported values");
2820
+ }
2821
+
2822
+ // src/config-operation-validate.ts
2823
+ var DIGEST = /^sha256:[0-9a-f]{64}$/;
2824
+ var REVISION = /^registry:[1-9][0-9]*$/;
2825
+ var OPERATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2826
+ var ACTION_ID = /^action-[0-9a-f]{16}$/;
2827
+ var ENV = /^[a-z0-9]{2,12}$/;
2828
+ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
2829
+ function readPlan(path) {
2830
+ let value2;
2831
+ try {
2832
+ const raw = (0, import_node_fs10.readFileSync)(path, "utf8");
2833
+ if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
2834
+ value2 = JSON.parse(raw);
2835
+ } catch (error) {
2836
+ throw new ConfigOperationCommandError(
2837
+ `cannot read --plan ${path}: ${error instanceof Error ? error.message : String(error)}`,
2838
+ "invalid_plan"
2839
+ );
2840
+ }
2841
+ if (!record2(value2) || value2.schemaVersion !== "odla.config-plan/v2") invalidPlan("unsupported plan schema");
2842
+ if (!record2(value2.scope) || typeof value2.scope.appId !== "string" || typeof value2.scope.platformUrl !== "string") {
2843
+ invalidPlan("plan scope is invalid");
2844
+ }
2845
+ if (!DIGEST.test(String(value2.desiredRevision)) || !DIGEST.test(String(value2.observedRevision))) {
2846
+ invalidPlan("plan content revisions are invalid");
2847
+ }
2848
+ if (!REVISION.test(String(value2.registryRevision)) || !DIGEST.test(String(value2.planDigest))) {
2849
+ invalidPlan("plan Registry revision or digest is invalid");
2850
+ }
2851
+ if (!Array.isArray(value2.actions) || !value2.actions.length || value2.actions.length > 32) {
2852
+ invalidPlan("plan actions must contain 1-32 entries");
2853
+ }
2854
+ assertActions(value2.actions);
2855
+ const plan = value2;
2856
+ const digest = configDigest({
2857
+ schemaVersion: plan.schemaVersion,
2858
+ desiredRevision: plan.desiredRevision,
2859
+ observedRevision: plan.observedRevision,
2860
+ registryRevision: plan.registryRevision,
2861
+ actions: plan.actions
2862
+ });
2863
+ if (digest !== plan.planDigest) invalidPlan("plan digest does not bind these revisions and actions");
2864
+ return plan;
2865
+ }
2866
+ function assertPlanContext(plan, appId, platformUrl) {
2867
+ if (plan.scope.appId !== appId || plan.scope.platformUrl.replace(/\/$/, "") !== platformUrl.replace(/\/$/, "")) {
2868
+ throw new ConfigOperationCommandError("plan scope does not match the selected project config", "checkpoint_required");
2869
+ }
2870
+ }
2871
+ function verifyReceipt(receipt, appId, operationId) {
2872
+ if (receipt.appId !== appId || operationId && receipt.operationId !== operationId) {
2873
+ throw new ConfigOperationCommandError("Registry returned a receipt outside the requested scope", "invalid_receipt");
2874
+ }
2875
+ if (receipt.receiptDigest) {
2876
+ const { receiptDigest, ...fields } = receipt;
2877
+ if (configDigest(fields) !== receiptDigest) {
2878
+ throw new ConfigOperationCommandError("config operation receipt digest is invalid", "invalid_receipt");
2879
+ }
2880
+ } else if (receipt.state === "succeeded" || receipt.state === "conflict" || receipt.state === "failed" && !receipt.error?.retryable) {
2881
+ throw new ConfigOperationCommandError("terminal config operation receipt is not digest-authenticated", "invalid_receipt");
2882
+ }
2883
+ }
2884
+ function assertOperationId(value2) {
2885
+ if (!OPERATION_ID.test(value2)) {
2886
+ throw new ConfigOperationCommandError("operation id must be a UUID", "invalid_operation_id");
2887
+ }
2888
+ }
2889
+ function assertActions(actions) {
2890
+ const ids = /* @__PURE__ */ new Set();
2891
+ for (const action2 of actions) {
2892
+ if (!record2(action2)) invalidPlan("every plan action must be an object");
2893
+ const id = String(action2.id ?? "");
2894
+ if (!ACTION_ID.test(id) || ids.has(id)) invalidPlan("plan action ids must be unique frozen ids");
2895
+ ids.add(id);
2896
+ if (typeof action2.path !== "string" || typeof action2.reason !== "string" || !action2.reason || action2.reason.length > 500 || typeof action2.requiresApproval !== "boolean" || !["low", "medium", "high"].includes(String(action2.risk)) || !["provision", "command", "studio"].includes(String(action2.applySupport))) {
2897
+ invalidPlan("plan action metadata is invalid");
2898
+ }
2899
+ if (["rename_app", "enable_service", "configure_service", "set_link"].includes(String(action2.kind))) {
2900
+ assertConditionalAction(action2);
2901
+ }
2902
+ }
2903
+ }
2904
+ function assertConditionalAction(action2) {
2905
+ if (action2.kind === "rename_app") {
2906
+ if (action2.path !== "app.name" || action2.applySupport !== "command" || typeof action2.before !== "string" || typeof action2.after !== "string" || action2.after !== action2.after.trim() || !action2.after || action2.after.length > 80) invalidPlan("rename action is invalid");
2907
+ return;
2908
+ }
2909
+ if (!action2.env || !ENV.test(action2.env)) invalidPlan("conditional action env is invalid");
2910
+ if (action2.kind === "set_link") {
2911
+ if (action2.path !== `environments.${action2.env}.link` || action2.applySupport !== "provision" || !linkState(action2.before) || !linkState(action2.after)) invalidPlan("link action is invalid");
2912
+ return;
2913
+ }
2914
+ if (!action2.service || !SERVICE.test(action2.service) || !(0, import_apps3.appServiceDefinition)(action2.service)) {
2915
+ invalidPlan("service action names an unknown service");
2916
+ }
2917
+ const base = `environments.${action2.env}.services.${action2.service}`;
2918
+ if (action2.path !== (action2.kind === "configure_service" ? `${base}.config` : base)) {
2919
+ invalidPlan("service action path is invalid");
2920
+ }
2921
+ if (action2.applySupport !== "provision" || !record2(action2.after)) {
2922
+ invalidPlan("service action payload is invalid");
2923
+ }
2924
+ if (action2.kind === "enable_service") {
2925
+ if (action2.after.enabled !== true || action2.before !== null && !record2(action2.before)) {
2926
+ invalidPlan("service enable action is invalid");
2927
+ }
2928
+ } else if (!record2(action2.before)) {
2929
+ invalidPlan("service configure action is invalid");
2930
+ }
2931
+ }
2932
+ function linkState(value2) {
2933
+ if (value2 === null) return true;
2934
+ if (typeof value2 !== "string") return false;
2935
+ try {
2936
+ const url = new URL(value2.trim());
2937
+ return url.protocol === "http:" || url.protocol === "https:";
2938
+ } catch {
2939
+ return false;
2940
+ }
2941
+ }
2942
+ function invalidPlan(message2) {
2943
+ throw new ConfigOperationCommandError(message2, "invalid_plan");
2944
+ }
2945
+ function record2(value2) {
2946
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
2773
2947
  }
2774
2948
 
2775
2949
  // src/config-reconcile-desired.ts
2776
- var import_apps4 = require("@odla-ai/apps");
2950
+ var import_apps5 = require("@odla-ai/apps");
2777
2951
 
2778
2952
  // src/provision-helpers.ts
2779
2953
  var import_ai = require("@odla-ai/ai");
2780
- var import_apps3 = require("@odla-ai/apps");
2954
+ var import_apps4 = require("@odla-ai/apps");
2781
2955
  function defaultSecretName(provider) {
2782
2956
  const names = import_ai.DEFAULT_SECRET_NAMES;
2783
2957
  return names[provider] ?? `${provider}_api_key`;
2784
2958
  }
2785
2959
  async function assertTenantAdminAccess(doFetch, cfg, env, token) {
2786
- const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
2960
+ const tenantId = (0, import_apps4.tenantIdFor)(cfg.app.id, env);
2787
2961
  const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
2788
2962
  headers: { authorization: `Bearer ${token}` }
2789
2963
  });
@@ -2825,7 +2999,7 @@ async function safeText3(res) {
2825
2999
  // src/config-reconcile-desired.ts
2826
3000
  function desiredRegistryState(cfg) {
2827
3001
  const environments = {};
2828
- const services = (0, import_apps4.orderAppServices)(cfg.services);
3002
+ const services = (0, import_apps5.orderAppServices)(cfg.services);
2829
3003
  for (const env of cfg.envs) {
2830
3004
  const desiredServices = {};
2831
3005
  for (const service of services) {
@@ -2865,8 +3039,208 @@ function managedServiceConfig(cfg, env, service) {
2865
3039
  return {};
2866
3040
  }
2867
3041
 
3042
+ // src/config-reconcile-support.ts
3043
+ var CONDITIONAL_KINDS = /* @__PURE__ */ new Set([
3044
+ "rename_app",
3045
+ "enable_service",
3046
+ "configure_service",
3047
+ "set_link"
3048
+ ]);
3049
+ function configApplySupport(reconciliation) {
3050
+ if (!reconciliation.observedRevision || !reconciliation.registryRevision) {
3051
+ return {
3052
+ supported: false,
3053
+ checkpointRequired: false,
3054
+ reason: "the app must already exist in a revision-aware Registry"
3055
+ };
3056
+ }
3057
+ if (!reconciliation.actions.length) {
3058
+ return {
3059
+ supported: false,
3060
+ checkpointRequired: false,
3061
+ reason: "there are no managed changes to apply"
3062
+ };
3063
+ }
3064
+ const blocked = reconciliation.actions.filter(
3065
+ (action2) => !CONDITIONAL_KINDS.has(action2.kind) || action2.risk !== "low" || action2.requiresApproval || action2.applySupport === "studio" || action2.env === "prod" || action2.env === "production"
3066
+ );
3067
+ if (blocked.length) {
3068
+ return {
3069
+ supported: false,
3070
+ checkpointRequired: blocked.some(
3071
+ (action2) => action2.risk === "high" || action2.requiresApproval || action2.applySupport === "studio" || action2.env === "prod" || action2.env === "production"
3072
+ ),
3073
+ reason: `${blocked.length} action${blocked.length === 1 ? "" : "s"} remain outside conditional apply`
3074
+ };
3075
+ }
3076
+ return {
3077
+ supported: true,
3078
+ checkpointRequired: false,
3079
+ reason: "all actions are low-risk, checkpoint-free Registry changes"
3080
+ };
3081
+ }
3082
+
3083
+ // src/config-operation-command.ts
3084
+ var IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/;
3085
+ var DEFAULT_WAIT_SECONDS = 60;
3086
+ var DEFAULT_INTERVAL_SECONDS = 2;
3087
+ async function configApply(options) {
3088
+ const plan = readPlan(options.planPath);
3089
+ const cfg = await loadProjectConfig(options.configPath);
3090
+ assertPlanContext(plan, cfg.app.id, cfg.platformUrl);
3091
+ const support = configApplySupport(plan);
3092
+ if (!support.supported) {
3093
+ throw new ConfigOperationCommandError(
3094
+ support.reason,
3095
+ support.checkpointRequired ? "checkpoint_required" : "invalid_plan"
3096
+ );
3097
+ }
3098
+ if (configDigest(desiredRegistryState(cfg)) !== plan.desiredRevision) {
3099
+ throw new ConfigOperationCommandError(
3100
+ "project config changed after this plan was frozen; generate and review a fresh plan",
3101
+ "checkpoint_required"
3102
+ );
3103
+ }
3104
+ const idempotencyKey = options.idempotencyKey ?? `cli:${plan.planDigest.slice(7)}`;
3105
+ if (!IDEMPOTENCY_KEY.test(idempotencyKey)) {
3106
+ throw new ConfigOperationCommandError("--idempotency-key is not a safe 1-120 character key", "invalid_plan");
3107
+ }
3108
+ const client = await operationClient(cfg, options, "apply");
3109
+ const request2 = {
3110
+ schemaVersion: "odla.config-operation-request/v1",
3111
+ expectedRevision: plan.registryRevision,
3112
+ desiredRevision: plan.desiredRevision,
3113
+ observedRevision: plan.observedRevision,
3114
+ planDigest: plan.planDigest,
3115
+ idempotencyKey,
3116
+ source: { kind: "cli", version: cliVersion() },
3117
+ actions: plan.actions
3118
+ };
3119
+ let receipt;
3120
+ try {
3121
+ receipt = await client.applyConfigOperation(cfg.app.id, request2);
3122
+ } catch (error) {
3123
+ const retained = retainedReceipt(error);
3124
+ if (retained) {
3125
+ verifyReceipt(retained, cfg.app.id);
3126
+ printReceipt(retained, options);
3127
+ throw failureForReceipt(retained);
3128
+ }
3129
+ throw normalizeRequestError(error);
3130
+ }
3131
+ verifyReceipt(receipt, cfg.app.id);
3132
+ printReceipt(receipt, options);
3133
+ assertApplyCompleted(receipt);
3134
+ return receipt;
3135
+ }
3136
+ async function configOperationGet(options) {
3137
+ assertOperationId(options.operationId);
3138
+ const cfg = await loadProjectConfig(options.configPath);
3139
+ const client = await operationClient(cfg, options, "read");
3140
+ const receipt = await client.getConfigOperation(cfg.app.id, options.operationId).catch((error) => {
3141
+ throw normalizeRequestError(error);
3142
+ });
3143
+ if (!receipt) {
3144
+ throw new ConfigOperationCommandError("config operation not found for this app", "operation_not_found");
3145
+ }
3146
+ verifyReceipt(receipt, cfg.app.id, options.operationId);
3147
+ printReceipt(receipt, options);
3148
+ return receipt;
3149
+ }
3150
+ async function configOperationWait(options) {
3151
+ assertOperationId(options.operationId);
3152
+ const cfg = await loadProjectConfig(options.configPath);
3153
+ const client = await operationClient(cfg, options, "wait");
3154
+ const wait2 = options.pollWait ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
3155
+ const now = () => (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
3156
+ const deadline = now() + (options.timeoutSeconds ?? DEFAULT_WAIT_SECONDS) * 1e3;
3157
+ const interval = (options.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS) * 1e3;
3158
+ let receipt = null;
3159
+ for (; ; ) {
3160
+ receipt = await client.getConfigOperation(cfg.app.id, options.operationId).catch((error) => {
3161
+ throw normalizeRequestError(error);
3162
+ });
3163
+ if (!receipt) {
3164
+ throw new ConfigOperationCommandError("config operation not found for this app", "operation_not_found");
3165
+ }
3166
+ verifyReceipt(receipt, cfg.app.id, options.operationId);
3167
+ if (receipt.state !== "running") break;
3168
+ if (now() >= deadline) {
3169
+ printReceipt(receipt, options);
3170
+ throw new ConfigOperationCommandError("config operation is still running", "operation_pending");
3171
+ }
3172
+ await wait2(Math.min(interval, Math.max(0, deadline - now())));
3173
+ }
3174
+ printReceipt(receipt, options);
3175
+ if (receipt.state !== "succeeded") throw failureForReceipt(receipt);
3176
+ return receipt;
3177
+ }
3178
+ async function operationClient(cfg, options, purpose) {
3179
+ const doFetch = options.fetch ?? fetch;
3180
+ const out = options.stdout ?? console;
3181
+ const token = await resolveAdminPlatformToken({
3182
+ platform: cfg.platformUrl,
3183
+ scope: "app:config:write",
3184
+ token: options.token,
3185
+ tokenFile: (0, import_node_path7.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3186
+ rootDir: cfg.rootDir,
3187
+ email: options.email,
3188
+ open: options.open,
3189
+ fetch: doFetch,
3190
+ stdout: out,
3191
+ openApprovalUrl: options.openApprovalUrl,
3192
+ label: `odla CLI (${cfg.app.id} config operation ${purpose})`
3193
+ });
3194
+ return (0, import_apps6.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
3195
+ }
3196
+ function printReceipt(receipt, options) {
3197
+ const out = options.stdout ?? console;
3198
+ if (options.json) out.log(JSON.stringify(receipt, null, 2));
3199
+ else {
3200
+ out.log(`config operation ${receipt.operationId}: ${receipt.state}`);
3201
+ out.log(`revision: ${receipt.expectedRevision} \u2192 ${receipt.currentRevision}`);
3202
+ for (const step of receipt.progress) out.log(` ${step.state} ${step.actionId} attempts=${step.attempts}`);
3203
+ out.log(`receipt: ${receipt.receiptDigest ?? "pending"}`);
3204
+ out.log(`studio: ${receipt.studioUrl}`);
3205
+ }
3206
+ }
3207
+ function assertApplyCompleted(receipt) {
3208
+ if (receipt.state === "succeeded") return;
3209
+ throw receipt.state === "running" ? new ConfigOperationCommandError("config operation is still running", "operation_pending") : failureForReceipt(receipt);
3210
+ }
3211
+ function failureForReceipt(receipt) {
3212
+ if (receipt.state === "conflict") return new ConfigOperationCommandError(
3213
+ receipt.error?.message ?? "config operation conflicted with current Registry state",
3214
+ "checkpoint_required"
3215
+ );
3216
+ if (receipt.error?.retryable) return new ConfigOperationCommandError(receipt.error.message, "remote_unavailable");
3217
+ return new ConfigOperationCommandError(receipt.error?.message ?? `config operation ${receipt.state}`, "config_operation_failed");
3218
+ }
3219
+ function retainedReceipt(error) {
3220
+ if (!(error instanceof import_apps6.AppsError) || !record3(error.details)) return null;
3221
+ return record3(error.details.operation) ? error.details.operation : null;
3222
+ }
3223
+ function normalizeRequestError(error) {
3224
+ if (!(error instanceof import_apps6.AppsError)) return error instanceof Error ? error : new Error(String(error));
3225
+ if (error.status === 401 || error.status === 403) {
3226
+ return new ConfigOperationCommandError(error.message, "auth_failed");
3227
+ }
3228
+ if (error.status === 409) return new ConfigOperationCommandError(error.message, "checkpoint_required");
3229
+ if (error.status === 429 || error.status >= 500) {
3230
+ return new ConfigOperationCommandError(error.message, "remote_unavailable");
3231
+ }
3232
+ return new ConfigOperationCommandError(error.message, error.code || "config_operation_failed");
3233
+ }
3234
+ function record3(value2) {
3235
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
3236
+ }
3237
+
3238
+ // src/config-reconcile-command.ts
3239
+ var import_apps8 = require("@odla-ai/apps");
3240
+ var import_node_path8 = require("path");
3241
+
2868
3242
  // src/config-reconcile.ts
2869
- var import_apps5 = require("@odla-ai/apps");
3243
+ var import_apps7 = require("@odla-ai/apps");
2870
3244
 
2871
3245
  // src/config-reconcile-values.ts
2872
3246
  function difference(path, desired, observed, status, reason, desiredSource, observedSource, env, service) {
@@ -2967,6 +3341,7 @@ function reconcileConfig(input) {
2967
3341
  sources: { desired: desiredSource, observed: observedSource },
2968
3342
  desiredRevision,
2969
3343
  observedRevision,
3344
+ registryRevision: input.observed?.configRevision ?? null,
2970
3345
  status: different ? "different" : unmanaged ? "unmanaged" : "in_sync",
2971
3346
  summary: {
2972
3347
  differences: different,
@@ -3021,7 +3396,7 @@ function compareEnvironments(desired, observed, desiredSource, observedSource, a
3021
3396
  function compareServices(env, desired, observed, desiredSource, observedSource, add) {
3022
3397
  const wanted = desired.environments[env]?.services ?? {};
3023
3398
  const live = observed?.environments[env] ?? {};
3024
- const knownOrder = (0, import_apps5.orderAppServices)((0, import_apps5.appServiceIds)());
3399
+ const knownOrder = (0, import_apps7.orderAppServices)((0, import_apps7.appServiceIds)());
3025
3400
  const services = [.../* @__PURE__ */ new Set([...knownOrder, ...Object.keys(wanted), ...Object.keys(live)])];
3026
3401
  for (const service of services) {
3027
3402
  const next = wanted[service];
@@ -3132,20 +3507,19 @@ async function configDiff(options) {
3132
3507
  }
3133
3508
  async function configPlan(options) {
3134
3509
  const reconciliation = await inspectConfig(options);
3510
+ const apply = configApplySupport(reconciliation);
3135
3511
  const planDigest = configDigest({
3136
- schemaVersion: "odla.config-plan/v1",
3512
+ schemaVersion: "odla.config-plan/v2",
3137
3513
  desiredRevision: reconciliation.desiredRevision,
3138
3514
  observedRevision: reconciliation.observedRevision,
3515
+ registryRevision: reconciliation.registryRevision,
3139
3516
  actions: reconciliation.actions
3140
3517
  });
3141
3518
  const document2 = {
3142
- schemaVersion: "odla.config-plan/v1",
3519
+ schemaVersion: "odla.config-plan/v2",
3143
3520
  ...reconciliation,
3144
3521
  planDigest,
3145
- apply: {
3146
- supported: false,
3147
- reason: "conditional, resumable config apply is not available yet; use the exact reviewed commands below"
3148
- },
3522
+ apply,
3149
3523
  nextActions: planNextActions(reconciliation, options.configPath)
3150
3524
  };
3151
3525
  printPlan2(document2, options);
@@ -3159,7 +3533,7 @@ async function inspectConfig(options) {
3159
3533
  platform: cfg.platformUrl,
3160
3534
  scope: "app:config:read",
3161
3535
  token: options.token,
3162
- tokenFile: (0, import_node_path7.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3536
+ tokenFile: (0, import_node_path8.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
3163
3537
  rootDir: cfg.rootDir,
3164
3538
  email: options.email,
3165
3539
  open: options.open,
@@ -3168,7 +3542,7 @@ async function inspectConfig(options) {
3168
3542
  openApprovalUrl: options.openApprovalUrl,
3169
3543
  label: `odla CLI (${cfg.app.id} config read)`
3170
3544
  });
3171
- const client = (0, import_apps6.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
3545
+ const client = (0, import_apps8.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
3172
3546
  const observed = await client.resolveApp(cfg.app.id);
3173
3547
  return reconcileConfig({
3174
3548
  desired: desiredRegistryState(cfg),
@@ -3204,7 +3578,7 @@ function printPlan2(document2, options) {
3204
3578
  }
3205
3579
  if (!document2.actions.length) out.log(" no managed changes");
3206
3580
  out.log(`plan digest: ${document2.planDigest}`);
3207
- out.log(`apply: unsupported \u2014 ${document2.apply.reason}`);
3581
+ out.log(`apply: ${document2.apply.supported ? "supported" : "blocked"} \u2014 ${document2.apply.reason}`);
3208
3582
  printNext(out, document2.nextActions);
3209
3583
  }
3210
3584
  function printHeader(out, kind, document2) {
@@ -3212,6 +3586,7 @@ function printHeader(out, kind, document2) {
3212
3586
  out.log(`platform: ${document2.scope.platformUrl}`);
3213
3587
  out.log(`desired: ${document2.desiredRevision}`);
3214
3588
  out.log(`observed: ${document2.observedRevision ?? "absent"}`);
3589
+ out.log(`registry: ${document2.registryRevision ?? "absent"}`);
3215
3590
  out.log(
3216
3591
  `summary: ${document2.summary.differences} different, ${document2.summary.unmanaged} unmanaged, ${document2.summary.actions} planned actions`
3217
3592
  );
@@ -3244,6 +3619,13 @@ function diffNextActions(reconciliation, configPath) {
3244
3619
  }
3245
3620
  function planNextActions(reconciliation, configPath) {
3246
3621
  const next = [];
3622
+ if (configApplySupport(reconciliation).supported) {
3623
+ next.push({
3624
+ code: "apply_frozen_plan",
3625
+ command: "odla-ai config apply --plan <saved-plan.json> --json",
3626
+ description: "Save this JSON document, then conditionally apply these exact revision-bound actions."
3627
+ });
3628
+ }
3247
3629
  if (reconciliation.actions.some((action2) => action2.applySupport === "provision")) {
3248
3630
  next.push({
3249
3631
  code: "review_provision",
@@ -3257,35 +3639,39 @@ function planNextActions(reconciliation, configPath) {
3257
3639
  }
3258
3640
  }
3259
3641
  if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
3260
- const { appId, platformUrl } = reconciliation.scope;
3261
3642
  next.push({
3262
3643
  code: "review_destructive",
3263
- command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
3644
+ command: studioSettingsUrl(reconciliation),
3264
3645
  description: "Review service disablement in the owning Studio scope; this plan will not apply it."
3265
3646
  });
3266
3647
  }
3267
3648
  if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
3268
3649
  next.push({
3269
3650
  code: "declare_or_accept_runtime_state",
3270
- command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
3651
+ command: studioSettingsUrl(reconciliation),
3271
3652
  description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
3272
3653
  });
3273
3654
  }
3274
3655
  return next;
3275
3656
  }
3657
+ function studioSettingsUrl(reconciliation) {
3658
+ const environment2 = reconciliation.actions.map((action2) => action2.env).concat(reconciliation.scope.environments).find((candidate) => candidate === "dev" || candidate === "prod") ?? "dev";
3659
+ const origin = reconciliation.scope.platformUrl.replace(/\/$/, "");
3660
+ return `${origin}${(0, import_apps8.studioAppSettingsPath)(reconciliation.scope.appId, environment2, "environment")}`;
3661
+ }
3276
3662
  function quoteArg2(value2) {
3277
3663
  return `'${value2.replace(/'/g, `'\\''`)}'`;
3278
3664
  }
3279
3665
 
3280
3666
  // src/doctor-checks.ts
3281
3667
  var import_node_child_process3 = require("child_process");
3282
- var import_node_fs10 = require("fs");
3283
- var import_node_path9 = require("path");
3668
+ var import_node_fs12 = require("fs");
3669
+ var import_node_path10 = require("path");
3284
3670
 
3285
3671
  // src/wrangler.ts
3286
3672
  var import_node_child_process2 = require("child_process");
3287
- var import_node_fs9 = require("fs");
3288
- var import_node_path8 = require("path");
3673
+ var import_node_fs11 = require("fs");
3674
+ var import_node_path9 = require("path");
3289
3675
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
3290
3676
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
3291
3677
  let stdout = "";
@@ -3299,28 +3685,28 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
3299
3685
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
3300
3686
  function findWranglerConfig(rootDir) {
3301
3687
  for (const name of WRANGLER_CONFIG_FILES) {
3302
- const path = (0, import_node_path8.join)(rootDir, name);
3303
- if ((0, import_node_fs9.existsSync)(path)) return path;
3688
+ const path = (0, import_node_path9.join)(rootDir, name);
3689
+ if ((0, import_node_fs11.existsSync)(path)) return path;
3304
3690
  }
3305
3691
  return null;
3306
3692
  }
3307
3693
  function readWranglerConfig(path) {
3308
3694
  if (path.endsWith(".toml")) return null;
3309
3695
  try {
3310
- return JSON.parse(stripJsonComments((0, import_node_fs9.readFileSync)(path, "utf8")));
3696
+ return JSON.parse(stripJsonComments((0, import_node_fs11.readFileSync)(path, "utf8")));
3311
3697
  } catch {
3312
3698
  return null;
3313
3699
  }
3314
3700
  }
3315
- function stripJsonComments(text) {
3701
+ function stripJsonComments(text2) {
3316
3702
  let result = "";
3317
3703
  let inString = false;
3318
- for (let i = 0; i < text.length; i++) {
3319
- const ch = text[i];
3704
+ for (let i = 0; i < text2.length; i++) {
3705
+ const ch = text2[i];
3320
3706
  if (inString) {
3321
3707
  result += ch;
3322
3708
  if (ch === "\\") {
3323
- result += text[i + 1] ?? "";
3709
+ result += text2[i + 1] ?? "";
3324
3710
  i++;
3325
3711
  } else if (ch === '"') {
3326
3712
  inString = false;
@@ -3332,14 +3718,14 @@ function stripJsonComments(text) {
3332
3718
  result += ch;
3333
3719
  continue;
3334
3720
  }
3335
- if (ch === "/" && text[i + 1] === "/") {
3336
- while (i < text.length && text[i] !== "\n") i++;
3721
+ if (ch === "/" && text2[i + 1] === "/") {
3722
+ while (i < text2.length && text2[i] !== "\n") i++;
3337
3723
  result += "\n";
3338
3724
  continue;
3339
3725
  }
3340
- if (ch === "/" && text[i + 1] === "*") {
3726
+ if (ch === "/" && text2[i + 1] === "*") {
3341
3727
  i += 2;
3342
- while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
3728
+ while (i < text2.length && !(text2[i] === "*" && text2[i + 1] === "/")) i++;
3343
3729
  i++;
3344
3730
  continue;
3345
3731
  }
@@ -3356,7 +3742,14 @@ async function wranglerLoggedIn(run, cwd) {
3356
3742
  }
3357
3743
  }
3358
3744
  function wranglerPutSecret(run, opts) {
3359
- const args = ["wrangler", "secret", "put", opts.name, ...opts.env ? ["--env", opts.env] : []];
3745
+ const args = [
3746
+ "wrangler",
3747
+ "secret",
3748
+ "put",
3749
+ opts.name,
3750
+ ...opts.env ? ["--env", opts.env] : [],
3751
+ ...opts.configPath ? ["--config", opts.configPath] : []
3752
+ ];
3360
3753
  return run("npx", args, { input: opts.value, cwd: opts.cwd });
3361
3754
  }
3362
3755
 
@@ -3405,10 +3798,10 @@ function wranglerWarnings(rootDir) {
3405
3798
  for (const { label, block } of blocks) {
3406
3799
  const assets = block.assets;
3407
3800
  if (assets?.directory) {
3408
- const dir = (0, import_node_path9.resolve)(rootDir, assets.directory);
3409
- if (dir === (0, import_node_path9.resolve)(rootDir)) {
3801
+ const dir = (0, import_node_path10.resolve)(rootDir, assets.directory);
3802
+ if (dir === (0, import_node_path10.resolve)(rootDir)) {
3410
3803
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
3411
- } else if ((0, import_node_fs10.existsSync)((0, import_node_path9.join)(dir, "node_modules"))) {
3804
+ } else if ((0, import_node_fs12.existsSync)((0, import_node_path10.join)(dir, "node_modules"))) {
3412
3805
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
3413
3806
  }
3414
3807
  }
@@ -3443,13 +3836,13 @@ function o11yProjectWarnings(rootDir) {
3443
3836
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
3444
3837
  return warnings;
3445
3838
  }
3446
- const main = typeof config.main === "string" ? (0, import_node_path9.resolve)(rootDir, config.main) : null;
3447
- if (!main || !(0, import_node_fs10.existsSync)(main)) {
3839
+ const main = typeof config.main === "string" ? (0, import_node_path10.resolve)(rootDir, config.main) : null;
3840
+ if (!main || !(0, import_node_fs12.existsSync)(main)) {
3448
3841
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
3449
3842
  } else {
3450
3843
  let source = "";
3451
3844
  try {
3452
- source = (0, import_node_fs10.readFileSync)(main, "utf8");
3845
+ source = (0, import_node_fs12.readFileSync)(main, "utf8");
3453
3846
  } catch {
3454
3847
  }
3455
3848
  if (!/\bwithObservability\b/.test(source)) {
@@ -3473,7 +3866,7 @@ function calendarProjectWarnings(rootDir) {
3473
3866
  }
3474
3867
  function readPackageJson(rootDir) {
3475
3868
  try {
3476
- return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path9.join)(rootDir, "package.json"), "utf8"));
3869
+ return JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path10.join)(rootDir, "package.json"), "utf8"));
3477
3870
  } catch {
3478
3871
  return null;
3479
3872
  }
@@ -3700,14 +4093,14 @@ function harnessOption(value2, flag) {
3700
4093
  }
3701
4094
 
3702
4095
  // src/init.ts
3703
- var import_node_fs11 = require("fs");
3704
- var import_node_path10 = require("path");
3705
- var import_apps7 = require("@odla-ai/apps");
4096
+ var import_node_fs13 = require("fs");
4097
+ var import_node_path11 = require("path");
4098
+ var import_apps9 = require("@odla-ai/apps");
3706
4099
  function initProject(options) {
3707
4100
  const out = options.stdout ?? console;
3708
- const rootDir = (0, import_node_path10.resolve)(options.rootDir ?? process.cwd());
3709
- const configPath = (0, import_node_path10.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
3710
- if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
4101
+ const rootDir = (0, import_node_path11.resolve)(options.rootDir ?? process.cwd());
4102
+ const configPath = (0, import_node_path11.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4103
+ if ((0, import_node_fs13.existsSync)(configPath) && !options.force) {
3711
4104
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
3712
4105
  }
3713
4106
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -3716,27 +4109,27 @@ function initProject(options) {
3716
4109
  const envs = options.envs?.length ? options.envs : ["dev"];
3717
4110
  const services = options.services?.length ? options.services : ["db", "ai"];
3718
4111
  for (const service of services) {
3719
- const definition = (0, import_apps7.appServiceDefinition)(service);
3720
- if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps7.appServiceIds)().join(", ")})`);
4112
+ const definition = (0, import_apps9.appServiceDefinition)(service);
4113
+ if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps9.appServiceIds)().join(", ")})`);
3721
4114
  for (const dependency of definition.requires) {
3722
4115
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
3723
4116
  }
3724
4117
  }
3725
4118
  const aiProvider = options.aiProvider ?? "anthropic";
3726
- (0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(configPath), { recursive: true });
3727
- (0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, "src/odla"), { recursive: true });
3728
- (0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, ".odla"), { recursive: true });
3729
- (0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
3730
- writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
3731
- writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4119
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.dirname)(configPath), { recursive: true });
4120
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.resolve)(rootDir, "src/odla"), { recursive: true });
4121
+ (0, import_node_fs13.mkdirSync)((0, import_node_path11.resolve)(rootDir, ".odla"), { recursive: true });
4122
+ (0, import_node_fs13.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4123
+ writeIfMissing((0, import_node_path11.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4124
+ writeIfMissing((0, import_node_path11.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
3732
4125
  ensureGitignore(rootDir);
3733
4126
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
3734
4127
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
3735
4128
  out.log("updated .gitignore for local odla credentials");
3736
4129
  }
3737
- function writeIfMissing(path, text) {
3738
- if ((0, import_node_fs11.existsSync)(path)) return;
3739
- (0, import_node_fs11.writeFileSync)(path, text);
4130
+ function writeIfMissing(path, text2) {
4131
+ if ((0, import_node_fs13.existsSync)(path)) return;
4132
+ (0, import_node_fs13.writeFileSync)(path, text2);
3740
4133
  }
3741
4134
  function configTemplate(input) {
3742
4135
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -3911,7 +4304,7 @@ function assertWranglerConfig(cfg) {
3911
4304
 
3912
4305
  // src/secrets-set.ts
3913
4306
  var import_ai2 = require("@odla-ai/ai");
3914
- var import_apps8 = require("@odla-ai/apps");
4307
+ var import_apps10 = require("@odla-ai/apps");
3915
4308
  var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
3916
4309
  async function secretsSet(options) {
3917
4310
  const name = (options.name ?? "").trim();
@@ -3944,13 +4337,13 @@ async function secretsSetClerkKey(options) {
3944
4337
  body: JSON.stringify({ value: value2 })
3945
4338
  });
3946
4339
  if (!res.ok) {
3947
- const text = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
3948
- throw new Error(`store Clerk secret key failed (${res.status}): ${text || "request failed"}`);
4340
+ const text2 = scrubValue((await res.text().catch(() => "")).slice(0, 300), value2);
4341
+ throw new Error(`store Clerk secret key failed (${res.status}): ${text2 || "request failed"}`);
3949
4342
  }
3950
4343
  out.log(`Clerk secret key stored for ${tenantId} ($clerk_secret, reserved + write-only; the value was never echoed)`);
3951
4344
  }
3952
- function scrubValue(text, value2) {
3953
- return redactSecrets(text).split(value2).join("[value redacted]");
4345
+ function scrubValue(text2, value2) {
4346
+ return redactSecrets(text2).split(value2).join("[value redacted]");
3954
4347
  }
3955
4348
  async function resolveVaultWrite(options) {
3956
4349
  const out = options.stdout ?? console;
@@ -3963,13 +4356,13 @@ async function resolveVaultWrite(options) {
3963
4356
  throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
3964
4357
  }
3965
4358
  const value2 = await secretInputValue(options, "secret");
3966
- return { cfg, tenantId: (0, import_apps8.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
4359
+ return { cfg, tenantId: (0, import_apps10.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
3967
4360
  }
3968
4361
 
3969
4362
  // src/skill.ts
3970
- var import_node_fs12 = require("fs");
4363
+ var import_node_fs14 = require("fs");
3971
4364
  var import_node_os2 = require("os");
3972
- var import_node_path11 = require("path");
4365
+ var import_node_path12 = require("path");
3973
4366
  var import_node_url2 = require("url");
3974
4367
 
3975
4368
  // src/skill-adapters.ts
@@ -4048,8 +4441,8 @@ function installSkill(options = {}) {
4048
4441
  const files = listFiles(sourceDir);
4049
4442
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
4050
4443
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
4051
- const root = (0, import_node_path11.resolve)(options.dir ?? process.cwd());
4052
- const home = (0, import_node_path11.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
4444
+ const root = (0, import_node_path12.resolve)(options.dir ?? process.cwd());
4445
+ const home = (0, import_node_path12.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
4053
4446
  const plans = /* @__PURE__ */ new Map();
4054
4447
  const targets = /* @__PURE__ */ new Map();
4055
4448
  const rememberTarget = (harness, target) => {
@@ -4063,48 +4456,48 @@ function installSkill(options = {}) {
4063
4456
  plans.set(target, { target, content: content2, boundary, managedMerge });
4064
4457
  };
4065
4458
  const planSkillTree = (targetDir2, boundary = root) => {
4066
- for (const rel of files) plan((0, import_node_path11.join)(targetDir2, rel), (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, rel), "utf8"), false, boundary);
4459
+ for (const rel of files) plan((0, import_node_path12.join)(targetDir2, rel), (0, import_node_fs14.readFileSync)((0, import_node_path12.join)(sourceDir, rel), "utf8"), false, boundary);
4067
4460
  };
4068
4461
  let targetDir;
4069
4462
  if (options.global) {
4070
- const claudeRoot = (0, import_node_path11.join)(home, ".claude", "skills");
4071
- const codexRoot = (0, import_node_path11.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path11.join)(home, ".codex"), "skills");
4463
+ const claudeRoot = (0, import_node_path12.join)(home, ".claude", "skills");
4464
+ const codexRoot = (0, import_node_path12.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path12.join)(home, ".codex"), "skills");
4072
4465
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
4073
4466
  for (const harness of harnesses) {
4074
4467
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
4075
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path11.dirname)((0, import_node_path11.dirname)(codexRoot)));
4468
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path12.dirname)((0, import_node_path12.dirname)(codexRoot)));
4076
4469
  rememberTarget(harness, skillRoot);
4077
4470
  }
4078
4471
  } else {
4079
- const sharedRoot = (0, import_node_path11.join)(root, ".agents", "skills");
4472
+ const sharedRoot = (0, import_node_path12.join)(root, ".agents", "skills");
4080
4473
  planSkillTree(sharedRoot);
4081
- const claudeRoot = (0, import_node_path11.join)(root, ".claude", "skills");
4474
+ const claudeRoot = (0, import_node_path12.join)(root, ".claude", "skills");
4082
4475
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
4083
4476
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
4084
4477
  if (harnesses.includes("claude")) {
4085
4478
  for (const skill of skillNames(files)) {
4086
- const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, skill, "SKILL.md"), "utf8");
4087
- plan((0, import_node_path11.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
4479
+ const canonical = (0, import_node_fs14.readFileSync)((0, import_node_path12.join)(sourceDir, skill, "SKILL.md"), "utf8");
4480
+ plan((0, import_node_path12.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
4088
4481
  }
4089
4482
  rememberTarget("claude", claudeRoot);
4090
4483
  }
4091
4484
  if (harnesses.includes("cursor")) {
4092
- const cursorRule = (0, import_node_path11.join)(root, ".cursor", "rules", "odla.mdc");
4485
+ const cursorRule = (0, import_node_path12.join)(root, ".cursor", "rules", "odla.mdc");
4093
4486
  plan(cursorRule, CURSOR_RULE);
4094
4487
  rememberTarget("cursor", cursorRule);
4095
4488
  }
4096
4489
  if (harnesses.includes("agents")) {
4097
- const agentsFile = (0, import_node_path11.join)(root, "AGENTS.md");
4490
+ const agentsFile = (0, import_node_path12.join)(root, "AGENTS.md");
4098
4491
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4099
4492
  rememberTarget("agents", agentsFile);
4100
4493
  }
4101
4494
  if (harnesses.includes("copilot")) {
4102
- const copilotFile = (0, import_node_path11.join)(root, ".github", "copilot-instructions.md");
4495
+ const copilotFile = (0, import_node_path12.join)(root, ".github", "copilot-instructions.md");
4103
4496
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4104
4497
  rememberTarget("copilot", copilotFile);
4105
4498
  }
4106
4499
  if (harnesses.includes("gemini")) {
4107
- const geminiFile = (0, import_node_path11.join)(root, "GEMINI.md");
4500
+ const geminiFile = (0, import_node_path12.join)(root, "GEMINI.md");
4108
4501
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
4109
4502
  rememberTarget("gemini", geminiFile);
4110
4503
  }
@@ -4118,11 +4511,11 @@ function installSkill(options = {}) {
4118
4511
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
4119
4512
  continue;
4120
4513
  }
4121
- if (!(0, import_node_fs12.existsSync)(file.target)) {
4514
+ if (!(0, import_node_fs14.existsSync)(file.target)) {
4122
4515
  writtenPaths.add(file.target);
4123
4516
  continue;
4124
4517
  }
4125
- const current = (0, import_node_fs12.readFileSync)(file.target, "utf8");
4518
+ const current = (0, import_node_fs14.readFileSync)(file.target, "utf8");
4126
4519
  if (current === file.content) {
4127
4520
  unchangedPaths.add(file.target);
4128
4521
  } else if (file.managedMerge || options.force) {
@@ -4139,9 +4532,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
4139
4532
  );
4140
4533
  }
4141
4534
  for (const file of plans.values()) {
4142
- if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
4143
- (0, import_node_fs12.mkdirSync)((0, import_node_path11.dirname)(file.target), { recursive: true });
4144
- (0, import_node_fs12.writeFileSync)(file.target, file.content);
4535
+ if (!(0, import_node_fs14.existsSync)(file.target) || (0, import_node_fs14.readFileSync)(file.target, "utf8") !== file.content) {
4536
+ (0, import_node_fs14.mkdirSync)((0, import_node_path12.dirname)(file.target), { recursive: true });
4537
+ (0, import_node_fs14.writeFileSync)(file.target, file.content);
4145
4538
  }
4146
4539
  }
4147
4540
  const skills = skillNames(files);
@@ -4160,7 +4553,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
4160
4553
  };
4161
4554
  }
4162
4555
  function pathsUnder(root, paths) {
4163
- return [...paths].map((path) => (0, import_node_path11.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path11.sep}`) && !(0, import_node_path11.isAbsolute)(path)).sort();
4556
+ return [...paths].map((path) => (0, import_node_path12.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path12.sep}`) && !(0, import_node_path12.isAbsolute)(path)).sort();
4164
4557
  }
4165
4558
  function normalizeHarnesses(values, global) {
4166
4559
  const requested = values?.length ? values : ["claude"];
@@ -4182,9 +4575,9 @@ function normalizeHarnesses(values, global) {
4182
4575
  function managedFileContent(path, block, force, boundary) {
4183
4576
  const symlink = symlinkedComponent(boundary, path);
4184
4577
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
4185
- if (!(0, import_node_fs12.existsSync)(path)) return `${block}
4578
+ if (!(0, import_node_fs14.existsSync)(path)) return `${block}
4186
4579
  `;
4187
- const current = (0, import_node_fs12.readFileSync)(path, "utf8");
4580
+ const current = (0, import_node_fs14.readFileSync)(path, "utf8");
4188
4581
  const start = "<!-- odla-ai agent setup:start -->";
4189
4582
  const end = "<!-- odla-ai agent setup:end -->";
4190
4583
  const startAt = current.indexOf(start);
@@ -4205,15 +4598,15 @@ function managedFileContent(path, block, force, boundary) {
4205
4598
  return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
4206
4599
  }
4207
4600
  function symlinkedComponent(boundary, target) {
4208
- const rel = (0, import_node_path11.relative)(boundary, target);
4209
- if (rel === ".." || rel.startsWith(`..${import_node_path11.sep}`) || (0, import_node_path11.isAbsolute)(rel)) {
4601
+ const rel = (0, import_node_path12.relative)(boundary, target);
4602
+ if (rel === ".." || rel.startsWith(`..${import_node_path12.sep}`) || (0, import_node_path12.isAbsolute)(rel)) {
4210
4603
  throw new Error(`agent setup target escapes its install root: ${target}`);
4211
4604
  }
4212
4605
  let current = boundary;
4213
- for (const part of rel.split(import_node_path11.sep).filter(Boolean)) {
4214
- current = (0, import_node_path11.join)(current, part);
4606
+ for (const part of rel.split(import_node_path12.sep).filter(Boolean)) {
4607
+ current = (0, import_node_path12.join)(current, part);
4215
4608
  try {
4216
- if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
4609
+ if ((0, import_node_fs14.lstatSync)(current).isSymbolicLink()) return current;
4217
4610
  } catch (error) {
4218
4611
  if (error.code !== "ENOENT") throw error;
4219
4612
  }
@@ -4224,13 +4617,13 @@ function skillNames(files) {
4224
4617
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
4225
4618
  }
4226
4619
  function listFiles(dir) {
4227
- if (!(0, import_node_fs12.existsSync)(dir)) return [];
4620
+ if (!(0, import_node_fs14.existsSync)(dir)) return [];
4228
4621
  const results = [];
4229
4622
  const walk = (current) => {
4230
- for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
4231
- const path = (0, import_node_path11.join)(current, entry.name);
4623
+ for (const entry of (0, import_node_fs14.readdirSync)(current, { withFileTypes: true })) {
4624
+ const path = (0, import_node_path12.join)(current, entry.name);
4232
4625
  if (entry.isDirectory()) walk(path);
4233
- else results.push((0, import_node_path11.relative)(dir, path));
4626
+ else results.push((0, import_node_path12.relative)(dir, path));
4234
4627
  }
4235
4628
  };
4236
4629
  walk(dir);
@@ -4425,10 +4818,14 @@ async function secretsCommand(parsed, deps) {
4425
4818
  async function projectCommand(command, parsed, deps) {
4426
4819
  if (command === "config") {
4427
4820
  const sub = parsed.positionals[1];
4428
- if (sub !== "diff" && sub !== "plan") {
4821
+ if (sub !== "diff" && sub !== "plan" && sub !== "apply") {
4429
4822
  throw new Error(`unknown config subcommand "${sub ?? ""}". Try "odla-ai config diff --json".`);
4430
4823
  }
4431
- assertArgs(parsed, ["config", "token", "email", "open", "json"], 2);
4824
+ assertArgs(
4825
+ parsed,
4826
+ sub === "apply" ? ["config", "plan", "idempotency-key", "token", "email", "open", "json"] : ["config", "token", "email", "open", "json"],
4827
+ 2
4828
+ );
4432
4829
  const options = {
4433
4830
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
4434
4831
  token: stringOpt(parsed.options.token),
@@ -4440,7 +4837,42 @@ async function projectCommand(command, parsed, deps) {
4440
4837
  stdout: deps.stdout
4441
4838
  };
4442
4839
  if (sub === "diff") await configDiff(options);
4443
- else await configPlan(options);
4840
+ else if (sub === "plan") await configPlan(options);
4841
+ else await configApply({
4842
+ ...options,
4843
+ planPath: requiredString(parsed.options.plan, "--plan"),
4844
+ idempotencyKey: stringOpt(parsed.options["idempotency-key"])
4845
+ });
4846
+ return true;
4847
+ }
4848
+ if (command === "operations") {
4849
+ const sub = parsed.positionals[1];
4850
+ if (sub !== "get" && sub !== "wait") {
4851
+ throw new Error(`unknown operations subcommand "${sub ?? ""}". Try "odla-ai operations get <operation-id> --json".`);
4852
+ }
4853
+ assertArgs(
4854
+ parsed,
4855
+ sub === "wait" ? ["config", "token", "email", "open", "json", "interval", "timeout"] : ["config", "token", "email", "open", "json"],
4856
+ 3
4857
+ );
4858
+ const operationId = parsed.positionals[2];
4859
+ if (!operationId) throw new Error(`operation id is required`);
4860
+ const options = {
4861
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
4862
+ operationId,
4863
+ token: stringOpt(parsed.options.token),
4864
+ email: stringOpt(parsed.options.email),
4865
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
4866
+ json: parsed.options.json === true,
4867
+ fetch: deps.fetch,
4868
+ openApprovalUrl: deps.openUrl,
4869
+ stdout: deps.stdout,
4870
+ pollWait: deps.pollWait,
4871
+ intervalSeconds: numberOpt(parsed.options.interval, "--interval"),
4872
+ timeoutSeconds: numberOpt(parsed.options.timeout, "--timeout")
4873
+ };
4874
+ if (sub === "get") await configOperationGet(options);
4875
+ else await configOperationWait(options);
4444
4876
  return true;
4445
4877
  }
4446
4878
  if (command === "init") {
@@ -4501,9 +4933,9 @@ async function projectCommand(command, parsed, deps) {
4501
4933
  }
4502
4934
 
4503
4935
  // src/code-connect.ts
4504
- var import_node_fs14 = require("fs");
4936
+ var import_node_fs15 = require("fs");
4505
4937
  var import_node_os4 = require("os");
4506
- var import_node_path13 = require("path");
4938
+ var import_node_path14 = require("path");
4507
4939
 
4508
4940
  // ../harness/dist/chunk-QTUEF2HZ.js
4509
4941
  var HARNESS_PROTOCOL_VERSION = 1;
@@ -4513,7 +4945,7 @@ var CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
4513
4945
  var HarnessProtocolError = class extends Error {
4514
4946
  name = "HarnessProtocolError";
4515
4947
  };
4516
- function record2(value2) {
4948
+ function record4(value2) {
4517
4949
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
4518
4950
  }
4519
4951
  function boundedText(value2, label, max) {
@@ -4530,7 +4962,7 @@ function parseAgentOutput(line) {
4530
4962
  } catch {
4531
4963
  throw new HarnessProtocolError("agent emitted invalid JSON");
4532
4964
  }
4533
- const message2 = record2(value2);
4965
+ const message2 = record4(value2);
4534
4966
  if (!message2 || message2.protocolVersion !== HARNESS_PROTOCOL_VERSION) {
4535
4967
  throw new HarnessProtocolError(`agent protocolVersion must be ${HARNESS_PROTOCOL_VERSION}`);
4536
4968
  }
@@ -4543,7 +4975,7 @@ function parseAgentOutput(line) {
4543
4975
  };
4544
4976
  }
4545
4977
  if (message2.type === "inference.request") {
4546
- const call2 = record2(message2.call);
4978
+ const call2 = record4(message2.call);
4547
4979
  if (!call2 || !Array.isArray(call2.messages) || !Number.isSafeInteger(call2.maxTokens)) {
4548
4980
  throw new HarnessProtocolError("inference.request.call requires messages and maxTokens");
4549
4981
  }
@@ -4555,7 +4987,7 @@ function parseAgentOutput(line) {
4555
4987
  };
4556
4988
  }
4557
4989
  if (message2.type === "tool.request") {
4558
- const input = record2(message2.input);
4990
+ const input = record4(message2.input);
4559
4991
  const tool = String(message2.tool);
4560
4992
  if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
4561
4993
  throw new HarnessProtocolError("tool.request requires a registered tool and object input");
@@ -4739,8 +5171,8 @@ async function runContainerAttempt(options) {
4739
5171
  let stopped = false;
4740
5172
  let exited = false;
4741
5173
  child.stderr.setEncoding("utf8");
4742
- child.stderr.on("data", (text) => {
4743
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
5174
+ child.stderr.on("data", (text2) => {
5175
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
4744
5176
  });
4745
5177
  const stop = (reason) => {
4746
5178
  if (stopped || exited) return;
@@ -4898,8 +5330,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
4898
5330
  const maxFiles = options.maxFiles ?? 2e4;
4899
5331
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
4900
5332
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
4901
- const entries = inventory.flatMap((record8) => {
4902
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record8);
5333
+ const entries = inventory.flatMap((record11) => {
5334
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record11);
4903
5335
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
4904
5336
  });
4905
5337
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -5147,8 +5579,8 @@ function normalize(value2) {
5147
5579
  if (Array.isArray(value2)) return value2.map(normalize);
5148
5580
  if (value2 instanceof Uint8Array) return { $bytes: [...value2] };
5149
5581
  if (typeof value2 === "object") {
5150
- const record8 = value2;
5151
- return Object.fromEntries(Object.keys(record8).filter((key) => record8[key] !== void 0).sort().map((key) => [key, normalize(record8[key])]));
5582
+ const record11 = value2;
5583
+ return Object.fromEntries(Object.keys(record11).filter((key) => record11[key] !== void 0).sort().map((key) => [key, normalize(record11[key])]));
5152
5584
  }
5153
5585
  throw new CamelError("state_conflict", "Canonical JSON rejects unsupported values.");
5154
5586
  }
@@ -5227,7 +5659,7 @@ function normalizeReaders(readers) {
5227
5659
  }
5228
5660
 
5229
5661
  // ../camel/dist/code.js
5230
- var DIGEST = /^sha256:[0-9a-f]{64}$/;
5662
+ var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
5231
5663
  var SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
5232
5664
  var ID = /^[A-Za-z0-9._:-]{1,160}$/;
5233
5665
  async function digestCodeVerificationReceipt(fields) {
@@ -5262,18 +5694,18 @@ async function digestCodeVerificationReceipt(fields) {
5262
5694
  return `sha256:${await sha256Hex(canonicalJson2(canonical))}`;
5263
5695
  }
5264
5696
  function validate(fields) {
5265
- if (fields.schemaVersion !== 1 || typeof fields.verificationId !== "string" || !ID.test(fields.verificationId) || typeof fields.trustedBaseCommitSha !== "string" || !SHA.test(fields.trustedBaseCommitSha) || !DIGEST.test(fields.trustedBaseDigest) || !DIGEST.test(fields.patchDigest) || !DIGEST.test(fields.candidateDigest) || !DIGEST.test(fields.sourceDigest) || !DIGEST.test(fields.policyDigest) || !DIGEST.test(fields.changedTestSetDigest) || !Number.isSafeInteger(fields.changedTestCount) || fields.changedTestCount < 0 || fields.changedTestCount > 1e4 || fields.changedTestsRequireReview !== fields.changedTestCount > 0 || fields.recipes.length < 1 || fields.recipes.length > 64) {
5697
+ if (fields.schemaVersion !== 1 || typeof fields.verificationId !== "string" || !ID.test(fields.verificationId) || typeof fields.trustedBaseCommitSha !== "string" || !SHA.test(fields.trustedBaseCommitSha) || !DIGEST2.test(fields.trustedBaseDigest) || !DIGEST2.test(fields.patchDigest) || !DIGEST2.test(fields.candidateDigest) || !DIGEST2.test(fields.sourceDigest) || !DIGEST2.test(fields.policyDigest) || !DIGEST2.test(fields.changedTestSetDigest) || !Number.isSafeInteger(fields.changedTestCount) || fields.changedTestCount < 0 || fields.changedTestCount > 1e4 || fields.changedTestsRequireReview !== fields.changedTestCount > 0 || fields.recipes.length < 1 || fields.recipes.length > 64) {
5266
5698
  throw new CamelError("state_conflict", "Code verification receipt is malformed or outside its bounds.");
5267
5699
  }
5268
5700
  const ids = /* @__PURE__ */ new Set();
5269
5701
  for (const recipe2 of fields.recipes) {
5270
- if (typeof recipe2.recipeId !== "string" || !ID.test(recipe2.recipeId) || ids.has(recipe2.recipeId) || typeof recipe2.recipeDigest !== "string" || !DIGEST.test(recipe2.recipeDigest) || !["passed", "failed", "timed_out", "output_limited"].includes(recipe2.status) || !Number.isSafeInteger(recipe2.exitCode) || recipe2.exitCode < 0 || recipe2.exitCode > 255 || !Number.isSafeInteger(recipe2.durationMs) || recipe2.durationMs < 0 || recipe2.durationMs > 30 * 6e4) {
5702
+ if (typeof recipe2.recipeId !== "string" || !ID.test(recipe2.recipeId) || ids.has(recipe2.recipeId) || typeof recipe2.recipeDigest !== "string" || !DIGEST2.test(recipe2.recipeDigest) || !["passed", "failed", "timed_out", "output_limited"].includes(recipe2.status) || !Number.isSafeInteger(recipe2.exitCode) || recipe2.exitCode < 0 || recipe2.exitCode > 255 || !Number.isSafeInteger(recipe2.durationMs) || recipe2.durationMs < 0 || recipe2.durationMs > 30 * 6e4) {
5271
5703
  throw new CamelError("state_conflict", "Code verification recipe receipt is malformed or outside its bounds.");
5272
5704
  }
5273
5705
  const artifactIds = /* @__PURE__ */ new Set();
5274
5706
  if (recipe2.artifacts.length > 64) throw new CamelError("state_conflict", "Code verification has too many artifacts.");
5275
5707
  for (const artifact of recipe2.artifacts) {
5276
- if (typeof artifact.artifactId !== "string" || !ID.test(artifact.artifactId) || artifactIds.has(artifact.artifactId) || !["verified", "missing", "invalid", "too_large"].includes(artifact.status) || artifact.bytes !== null && (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0) || artifact.digest !== null && !DIGEST.test(artifact.digest) || artifact.status === "verified" !== (artifact.bytes !== null && artifact.digest !== null) || ["missing", "invalid"].includes(artifact.status) && (artifact.bytes !== null || artifact.digest !== null) || artifact.status === "too_large" && (artifact.bytes === null || artifact.digest !== null)) {
5708
+ if (typeof artifact.artifactId !== "string" || !ID.test(artifact.artifactId) || artifactIds.has(artifact.artifactId) || !["verified", "missing", "invalid", "too_large"].includes(artifact.status) || artifact.bytes !== null && (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0) || artifact.digest !== null && !DIGEST2.test(artifact.digest) || artifact.status === "verified" !== (artifact.bytes !== null && artifact.digest !== null) || ["missing", "invalid"].includes(artifact.status) && (artifact.bytes !== null || artifact.digest !== null) || artifact.status === "too_large" && (artifact.bytes === null || artifact.digest !== null)) {
5277
5709
  throw new CamelError("state_conflict", "Code verification artifact receipt is malformed.");
5278
5710
  }
5279
5711
  artifactIds.add(artifact.artifactId);
@@ -5289,7 +5721,7 @@ function validate(fields) {
5289
5721
  }
5290
5722
  }
5291
5723
  var SHA2 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
5292
- var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
5724
+ var DIGEST22 = /^sha256:[0-9a-f]{64}$/;
5293
5725
  var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
5294
5726
  var MAX_PATCH_BYTES = 256 * 1024;
5295
5727
  var MAX_STATE_BYTES = 64 * 1024;
@@ -5334,14 +5766,14 @@ function normalizeState(value2) {
5334
5766
  ]);
5335
5767
  const planCursor = state2.planCursor;
5336
5768
  const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
5337
- const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
5338
- if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state2.planningInputDigest !== "string" || !DIGEST2.test(state2.planningInputDigest) || typeof state2.buildPolicyDigest !== "string" || !DIGEST2.test(state2.buildPolicyDigest) || state2.dependencyLayerDigest !== null && (typeof state2.dependencyLayerDigest !== "string" || !DIGEST2.test(state2.dependencyLayerDigest)) || state2.verificationDigest !== null && (typeof state2.verificationDigest !== "string" || !DIGEST2.test(state2.verificationDigest)) || state2.reviewDigest !== null && (typeof state2.reviewDigest !== "string" || !DIGEST2.test(state2.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state2.trustStatus)) || !Array.isArray(state2.completedEffects) || state2.completedEffects.length > 256) {
5769
+ const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST22, 256, true);
5770
+ if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state2.planningInputDigest !== "string" || !DIGEST22.test(state2.planningInputDigest) || typeof state2.buildPolicyDigest !== "string" || !DIGEST22.test(state2.buildPolicyDigest) || state2.dependencyLayerDigest !== null && (typeof state2.dependencyLayerDigest !== "string" || !DIGEST22.test(state2.dependencyLayerDigest)) || state2.verificationDigest !== null && (typeof state2.verificationDigest !== "string" || !DIGEST22.test(state2.verificationDigest)) || state2.reviewDigest !== null && (typeof state2.reviewDigest !== "string" || !DIGEST22.test(state2.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state2.trustStatus)) || !Array.isArray(state2.completedEffects) || state2.completedEffects.length > 256) {
5339
5771
  throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
5340
5772
  }
5341
5773
  const effects = state2.completedEffects.map((item) => {
5342
5774
  const effect = object(item, "completed effect");
5343
5775
  exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
5344
- if (typeof effect.effectId !== "string" || !ID2.test(effect.effectId) || typeof effect.actionDigest !== "string" || !DIGEST2.test(effect.actionDigest) || typeof effect.receiptDigest !== "string" || !DIGEST2.test(effect.receiptDigest)) {
5776
+ if (typeof effect.effectId !== "string" || !ID2.test(effect.effectId) || typeof effect.actionDigest !== "string" || !DIGEST22.test(effect.actionDigest) || typeof effect.receiptDigest !== "string" || !DIGEST22.test(effect.receiptDigest)) {
5345
5777
  throw invalid2("Portable checkpoint effect receipt is malformed.");
5346
5778
  }
5347
5779
  return {
@@ -5572,8 +6004,8 @@ function boundedInteger(value2, spec) {
5572
6004
  }
5573
6005
  function boundedNumber(value2, spec) {
5574
6006
  if (spec.kind !== "finite_number" || typeof value2 !== "number" || !Number.isFinite(value2) || value2 < spec.minimum || value2 > spec.maximum) throw new CamelError("conversion_rejected", "Finite-number conversion rejected the structured value.");
5575
- const text = String(value2);
5576
- if (/e/i.test(text) || (text.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
6007
+ const text2 = String(value2);
6008
+ if (/e/i.test(text2) || (text2.split(".")[1]?.length ?? 0) > spec.maximumDecimalPlaces) throw new CamelError("conversion_rejected", "Finite-number conversion rejected a non-canonical decimal.");
5577
6009
  return value2;
5578
6010
  }
5579
6011
  function enumMember(value2, spec) {
@@ -5729,8 +6161,8 @@ function validateUnsafeSelector(path, value2, tool) {
5729
6161
  return void 0;
5730
6162
  }
5731
6163
  function looksLikeDestination(value2) {
5732
- const text = value2.trim();
5733
- return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
6164
+ const text2 = value2.trim();
6165
+ return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text2) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text2);
5734
6166
  }
5735
6167
 
5736
6168
  // ../harness/dist/chunk-GMVZ4LZH.js
@@ -5871,7 +6303,7 @@ function createCodeRuntimeControlClient(options) {
5871
6303
  }
5872
6304
  const value2 = await response2.json().catch(() => null);
5873
6305
  if (!response2.ok) {
5874
- const problem = record3(record3(value2)?.error);
6306
+ const problem = record5(record5(value2)?.error);
5875
6307
  throw new CodeRuntimeControlError(
5876
6308
  typeof problem?.message === "string" ? problem.message : `Code runtime request failed (${response2.status})`,
5877
6309
  response2.status,
@@ -5893,12 +6325,12 @@ function createCodeRuntimeControlClient(options) {
5893
6325
  await call2(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
5894
6326
  ),
5895
6327
  infer: async (sessionId, inference) => {
5896
- const value2 = record3(await call2(
6328
+ const value2 = record5(await call2(
5897
6329
  `/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
5898
6330
  inference,
5899
6331
  modelRequestTimeoutMs
5900
6332
  ));
5901
- if (!value2 || value2.requestId !== inference.requestId || !record3(value2.response) || !record3(value2.receipt)) {
6333
+ if (!value2 || value2.requestId !== inference.requestId || !record5(value2.response) || !record5(value2.receipt)) {
5902
6334
  throw new CodeRuntimeControlError("invalid Code inference response", 502, "invalid_response");
5903
6335
  }
5904
6336
  return value2;
@@ -5956,12 +6388,12 @@ function validateHeartbeat(version, capabilities) {
5956
6388
  }
5957
6389
  }
5958
6390
  function parseSnapshot(value2) {
5959
- const root = record3(value2);
5960
- const host = record3(root?.host);
6391
+ const root = record5(value2);
6392
+ const host = record5(root?.host);
5961
6393
  if (!host || typeof host.hostId !== "string" || typeof host.runtimeVersion !== "string" || !Number.isSafeInteger(host.lastSeenAt) || host.revokedAt !== null || !Array.isArray(root?.bindings) || root.bindings.length > 1024 || !Array.isArray(root?.commands) || root.commands.length > 64) throw invalid("heartbeat");
5962
6394
  const bindingIds = /* @__PURE__ */ new Set();
5963
6395
  const bindings = root.bindings.map((item) => {
5964
- const binding = record3(item);
6396
+ const binding = record5(item);
5965
6397
  if (!binding || typeof binding.bindingId !== "string" || typeof binding.appId !== "string" || binding.env !== "dev" && binding.env !== "prod" || typeof binding.offerId !== "string" || binding.hostId !== host.hostId || !Number.isSafeInteger(binding.generation) || Number(binding.generation) < 1 || binding.revokedAt !== null || bindingIds.has(binding.bindingId)) {
5966
6398
  throw invalid("binding");
5967
6399
  }
@@ -5971,10 +6403,10 @@ function parseSnapshot(value2) {
5971
6403
  const commandIds = /* @__PURE__ */ new Set();
5972
6404
  const commandSequences = /* @__PURE__ */ new Set();
5973
6405
  const commands = root.commands.map((item) => {
5974
- const command = record3(item);
6406
+ const command = record5(item);
5975
6407
  const binding = bindings.find((candidate) => candidate.bindingId === command?.bindingId);
5976
6408
  const sequenceKey = `${String(command?.instanceId)}:${String(command?.sequence)}`;
5977
- if (!command || typeof command.commandId !== "string" || !/^ccmd_[0-9a-f]{32}$/.test(command.commandId) || typeof command.instanceId !== "string" || typeof command.sessionId !== "string" || !/^csess_[0-9a-f]{32}$/.test(command.sessionId) || typeof command.appId !== "string" || command.env !== "dev" && command.env !== "prod" || command.hostId !== host.hostId || !binding || binding.appId !== command.appId || binding.generation !== command.bindingGeneration || !Number.isSafeInteger(command.sequence) || Number(command.sequence) < 1 || commandIds.has(command.commandId) || commandSequences.has(sequenceKey) || !["start", "prompt", "checkpoint_stop", "resume"].includes(String(command.kind)) || !record3(command.payload) || !Number.isSafeInteger(command.createdAt)) throw invalid("command");
6409
+ if (!command || typeof command.commandId !== "string" || !/^ccmd_[0-9a-f]{32}$/.test(command.commandId) || typeof command.instanceId !== "string" || typeof command.sessionId !== "string" || !/^csess_[0-9a-f]{32}$/.test(command.sessionId) || typeof command.appId !== "string" || command.env !== "dev" && command.env !== "prod" || command.hostId !== host.hostId || !binding || binding.appId !== command.appId || binding.generation !== command.bindingGeneration || !Number.isSafeInteger(command.sequence) || Number(command.sequence) < 1 || commandIds.has(command.commandId) || commandSequences.has(sequenceKey) || !["start", "prompt", "checkpoint_stop", "resume"].includes(String(command.kind)) || !record5(command.payload) || !Number.isSafeInteger(command.createdAt)) throw invalid("command");
5978
6410
  commandIds.add(command.commandId);
5979
6411
  commandSequences.add(sequenceKey);
5980
6412
  return command;
@@ -5982,10 +6414,10 @@ function parseSnapshot(value2) {
5982
6414
  return { host, bindings, commands };
5983
6415
  }
5984
6416
  async function parseSource(value2) {
5985
- const snapshot = record3(record3(value2)?.snapshot);
6417
+ const snapshot = record5(record5(value2)?.snapshot);
5986
6418
  if (!snapshot || typeof snapshot.repository !== "string" || typeof snapshot.commitSha !== "string" || typeof snapshot.treeDigest !== "string" || !Array.isArray(snapshot.files)) throw invalid("source");
5987
6419
  const files = snapshot.files.map((value22) => {
5988
- const file = record3(value22);
6420
+ const file = record5(value22);
5989
6421
  if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("source file");
5990
6422
  return { path: file.path, content: file.content };
5991
6423
  });
@@ -5994,11 +6426,11 @@ async function parseSource(value2) {
5994
6426
  const aliases = /* @__PURE__ */ new Set();
5995
6427
  const references = [];
5996
6428
  for (const item of referencesValue) {
5997
- const reference = record3(item);
6429
+ const reference = record5(item);
5998
6430
  if (!reference || typeof reference.alias !== "string" || !/^[a-z][a-z0-9-]{0,39}$/.test(reference.alias) || aliases.has(reference.alias) || reference.alias === "primary" || typeof reference.repository !== "string" || typeof reference.commitSha !== "string" || typeof reference.treeDigest !== "string" || !Array.isArray(reference.files)) throw invalid("reference source");
5999
6431
  aliases.add(reference.alias);
6000
6432
  const referenceFiles = reference.files.map((entry) => {
6001
- const file = record3(entry);
6433
+ const file = record5(entry);
6002
6434
  if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("reference source file");
6003
6435
  return { path: file.path, content: file.content };
6004
6436
  });
@@ -6013,18 +6445,18 @@ async function parseSource(value2) {
6013
6445
  return { ...source, treeDigest: digest, ...references.length ? { references } : {} };
6014
6446
  }
6015
6447
  function parseReview(value2) {
6016
- const review = record3(record3(value2)?.review);
6448
+ const review = record5(record5(value2)?.review);
6017
6449
  if (!review || !["approved", "rejected"].includes(String(review.verdict)) || typeof review.reviewDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(review.reviewDigest) || typeof review.provider !== "string" || !review.provider || typeof review.model !== "string" || !review.model || !Number.isSafeInteger(review.policyVersion) || Number(review.policyVersion) < 1) throw invalid("review");
6018
6450
  return review;
6019
6451
  }
6020
6452
  function parseCandidate(value2) {
6021
- const candidate = record3(record3(value2)?.candidate);
6453
+ const candidate = record5(record5(value2)?.candidate);
6022
6454
  if (!candidate || typeof candidate.candidateId !== "string" || !/^ccand_[0-9a-f]{32}$/.test(candidate.candidateId) || !["submitted", "approved", "published", "failed"].includes(String(candidate.status))) {
6023
6455
  throw invalid("candidate");
6024
6456
  }
6025
6457
  return { candidateId: candidate.candidateId, status: candidate.status };
6026
6458
  }
6027
- var record3 = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
6459
+ var record5 = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
6028
6460
  var invalid = (part) => new CodeRuntimeControlError(`invalid Code runtime ${part} response`, 502, "invalid_response");
6029
6461
  var RESERVED = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_modules", "dist", "coverage"]);
6030
6462
  var SECRET = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
@@ -6099,8 +6531,8 @@ function gitApply(cwd, patch2, check) {
6099
6531
  });
6100
6532
  let stderr = "";
6101
6533
  child.stderr.setEncoding("utf8");
6102
- child.stderr.on("data", (text) => {
6103
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
6534
+ child.stderr.on("data", (text2) => {
6535
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
6104
6536
  });
6105
6537
  child.once("error", reject);
6106
6538
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(`patch did not apply: ${stderr.trim().slice(0, 500)}`)));
@@ -7271,10 +7703,10 @@ var CodePiRuntimeEngine = class {
7271
7703
  task: lease.task,
7272
7704
  limits: this.options.limits,
7273
7705
  signal: active.abort.signal,
7274
- onStderr: (text) => this.#event(command, {
7706
+ onStderr: (text2) => this.#event(command, {
7275
7707
  type: "message",
7276
7708
  actor: "system",
7277
- body: text.slice(0, 4e3)
7709
+ body: text2.slice(0, 4e3)
7278
7710
  }, active.conversationRefs),
7279
7711
  onMessage: async (output) => {
7280
7712
  if (output.type === "inference.request") {
@@ -7387,13 +7819,6 @@ var CodePiRuntimeEngine = class {
7387
7819
  }
7388
7820
  };
7389
7821
 
7390
- // src/version.ts
7391
- var import_node_fs13 = require("fs");
7392
- function cliVersion() {
7393
- const pkg = JSON.parse((0, import_node_fs13.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
7394
- return pkg.version ?? "unknown";
7395
- }
7396
-
7397
7822
  // src/security-hosted-github.ts
7398
7823
  var import_node_child_process4 = require("child_process");
7399
7824
  var import_node_util2 = require("util");
@@ -7669,7 +8094,7 @@ var import_node_child_process6 = require("child_process");
7669
8094
  var import_node_crypto3 = require("crypto");
7670
8095
  var import_promises10 = require("fs/promises");
7671
8096
  var import_node_os3 = require("os");
7672
- var import_node_path12 = require("path");
8097
+ var import_node_path13 = require("path");
7673
8098
  var import_node_url3 = require("url");
7674
8099
 
7675
8100
  // src/code-runtime-config.ts
@@ -7755,10 +8180,10 @@ async function embeddedPiImageName() {
7755
8180
  return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex")}`;
7756
8181
  }
7757
8182
  async function buildEmbeddedPiImage(engine, image, run) {
7758
- const context = await (0, import_promises10.mkdtemp)((0, import_node_path12.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
8183
+ const context = await (0, import_promises10.mkdtemp)((0, import_node_path13.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
7759
8184
  try {
7760
- await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path12.join)(context, "pi-agent.js"));
7761
- await (0, import_promises10.writeFile)((0, import_node_path12.join)(context, "Dockerfile"), [
8185
+ await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path13.join)(context, "pi-agent.js"));
8186
+ await (0, import_promises10.writeFile)((0, import_node_path13.join)(context, "Dockerfile"), [
7762
8187
  `FROM ${CODE_NODE_IMAGE}`,
7763
8188
  "COPY pi-agent.js /opt/odla/pi-agent.js",
7764
8189
  "WORKDIR /workspace",
@@ -7774,8 +8199,8 @@ async function buildEmbeddedPiImage(engine, image, run) {
7774
8199
  // src/code-connect.ts
7775
8200
  async function codeConnect(options) {
7776
8201
  const cwd = options.cwd ?? process.cwd();
7777
- const configPath = (0, import_node_path13.resolve)(cwd, options.configPath);
7778
- const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
8202
+ const configPath = (0, import_node_path14.resolve)(cwd, options.configPath);
8203
+ const cfg = (0, import_node_fs15.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
7779
8204
  const requestedAppId = options.appId?.trim();
7780
8205
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
7781
8206
  throw new Error("--app-id must be a valid odla app id");
@@ -7933,20 +8358,20 @@ async function runCodeRuntime(input) {
7933
8358
  }
7934
8359
  }
7935
8360
  function parseConnection(value2, appId, appEnv) {
7936
- const root = record4(value2);
7937
- const host = record4(root?.host);
7938
- const offer = record4(root?.offer);
7939
- const binding = record4(root?.binding);
8361
+ const root = record6(value2);
8362
+ const host = record6(root?.host);
8363
+ const offer = record6(root?.offer);
8364
+ const binding = record6(root?.binding);
7940
8365
  if (!root || typeof root.token !== "string" || !/^odla_code_host_[0-9a-f]{64}$/.test(root.token) || typeof root.resumed !== "boolean" || !host || !/^chost_[0-9a-f]{32}$/.test(String(host.hostId)) || typeof host.name !== "string" || !offer || !Number.isSafeInteger(offer.slots) || !binding || typeof binding.appId !== "string" || !binding.appId || appId && binding.appId !== appId || binding.env !== appEnv || !Number.isSafeInteger(binding.generation)) {
7941
8366
  throw new Error("connect Code host returned an invalid response");
7942
8367
  }
7943
8368
  return root;
7944
8369
  }
7945
8370
  function apiFailure(action2, status, value2) {
7946
- const message2 = record4(record4(value2)?.error)?.message;
8371
+ const message2 = record6(record6(value2)?.error)?.message;
7947
8372
  return `${action2} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
7948
8373
  }
7949
- function record4(value2) {
8374
+ function record6(value2) {
7950
8375
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
7951
8376
  }
7952
8377
 
@@ -8161,8 +8586,10 @@ Usage:
8161
8586
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8162
8587
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
8163
8588
  odla-ai doctor [--config odla.config.mjs]
8164
- odla-ai config diff [--config odla.config.mjs] [--email <odla-account>] [--json]
8165
- odla-ai config plan [--config odla.config.mjs] [--email <odla-account>] [--json]
8589
+ odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
8590
+ odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
8591
+ odla-ai operations get <operation-id> [--json]
8592
+ odla-ai operations wait <operation-id> [--interval <seconds>] [--timeout <seconds>] [--json]
8166
8593
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8167
8594
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8168
8595
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
@@ -8213,6 +8640,7 @@ Usage:
8213
8640
  odla-ai context remove <name> --yes [--json]
8214
8641
  odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
8215
8642
  odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8643
+ odla-ai platform chat-credentials rotate [--context <name>] [--email <odla-account>] [--wrangler-config <path>] [--expected-version <id>] [--json] --yes
8216
8644
  odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
8217
8645
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
8218
8646
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -8276,9 +8704,9 @@ Commands:
8276
8704
  does and what it guarantees is JSDoc, rendered per package at
8277
8705
  https://odla.ai/docs and shipped in the installed .d.ts. Answering
8278
8706
  a question usually needs both.
8279
- whoami Report who this terminal is authenticated as, and whether it holds
8280
- platform admin. A handshake-minted device token is never admin,
8281
- however the human who approved it is configured.
8707
+ whoami Report the actual principal name, handle and kind, its manager,
8708
+ credential kind, accountable owner, and platform-admin status.
8709
+ A manager relationship never supplies authority by itself.
8282
8710
  context Explain selected config, platform, app, environment, and
8283
8711
  developer-token provenance without printing credentials or
8284
8712
  starting a device handshake. Operator work can run outside a
@@ -8287,8 +8715,8 @@ Commands:
8287
8715
  setup Install offline odla runbooks for common coding-agent harnesses.
8288
8716
  init Create a generic odla.config.mjs plus starter schema/rules files.
8289
8717
  doctor Validate and summarize the project config without network calls.
8290
- config Read-only diff and revision-bound plan for checked-in Registry
8291
- intent; runtime-owned fields and excluded coverage stay explicit.
8718
+ config Diff Registry intent, freeze a CAS-bound plan, and conditionally apply its safe actions.
8719
+ operations Inspect or wait on one exact, durable config-operation receipt.
8292
8720
  calendar Inspect, connect, or disconnect the live Google booking connection.
8293
8721
  app Archive (suspend, data retained), restore, export, import, or
8294
8722
  manage the co-owners of the app. Archiving takes every
@@ -8398,6 +8826,58 @@ Safety:
8398
8826
  `);
8399
8827
  }
8400
8828
 
8829
+ // src/discuss-principals.ts
8830
+ function mergeDiscussPrincipals(target, source) {
8831
+ Object.assign(target.authors, source.authors ?? {});
8832
+ Object.assign(target.principals, source.principals ?? {});
8833
+ }
8834
+ var withAt = (handle) => handle.startsWith("@") ? handle : `@${handle}`;
8835
+ function discussPrincipalLabel(principalId, authorKind, projection) {
8836
+ const profile = projection.principals?.[principalId];
8837
+ if (profile) {
8838
+ const identity = `${profile.displayName} (${withAt(profile.handle)})`;
8839
+ if (profile.kind === "agent") {
8840
+ return profile.managerDisplayName ? `${identity} \u2014 agent managed by ${profile.managerDisplayName}` : `${identity} \u2014 agent`;
8841
+ }
8842
+ return profile.kind === "service" ? `${identity} \u2014 service` : identity;
8843
+ }
8844
+ const legacyName = projection.authors?.[principalId]?.trim();
8845
+ if (legacyName) {
8846
+ return authorKind === "bot" ? `${legacyName} \u2014 agent` : legacyName;
8847
+ }
8848
+ return authorKind === "bot" ? "Unnamed agent" : "Unnamed member";
8849
+ }
8850
+
8851
+ // src/discuss-read-render.ts
8852
+ function discussBodyWithRefs(post) {
8853
+ if (!post.refs || post.refs.length === 0) return post.body;
8854
+ let out = "";
8855
+ let cursor = 0;
8856
+ for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8857
+ if (ref.start < cursor || ref.end > post.body.length) continue;
8858
+ out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8859
+ cursor = ref.end;
8860
+ }
8861
+ return out + post.body.slice(cursor);
8862
+ }
8863
+ function renderDiscussRead(ctx, topic, posts, projection) {
8864
+ const status = topic.resolved ? "resolved" : "open";
8865
+ ctx.out.log(`${topic.subject} [${status}] ${topic.appId ?? ""}`);
8866
+ for (const post of posts) {
8867
+ const who = discussPrincipalLabel(
8868
+ post.authorId,
8869
+ post.authorKind,
8870
+ projection
8871
+ );
8872
+ ctx.out.log(`
8873
+ \u2014 ${who}`);
8874
+ ctx.out.log(discussBodyWithRefs(post));
8875
+ for (const file of post.attachments ?? []) {
8876
+ ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8877
+ }
8878
+ }
8879
+ }
8880
+
8401
8881
  // src/discuss-actions.ts
8402
8882
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8403
8883
  async function request(ctx, method, path, body) {
@@ -8416,17 +8896,6 @@ function emit(ctx, value2, human) {
8416
8896
  else human();
8417
8897
  }
8418
8898
  var state = (topic) => topic.resolved ? "resolved" : "open";
8419
- function bodyWithRefs(post) {
8420
- if (!post.refs || post.refs.length === 0) return post.body;
8421
- let out = "";
8422
- let cursor = 0;
8423
- for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
8424
- if (ref.start < cursor || ref.end > post.body.length) continue;
8425
- out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
8426
- cursor = ref.end;
8427
- }
8428
- return out + post.body.slice(cursor);
8429
- }
8430
8899
  function content(parsed) {
8431
8900
  const markup = stringOpt(parsed.options.markup);
8432
8901
  if (markup) return { markup };
@@ -8481,11 +8950,19 @@ async function discussRead(ctx, id, parsed) {
8481
8950
  offset: requestedOffset ?? "0"
8482
8951
  });
8483
8952
  const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
8484
- emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
8953
+ emit(
8954
+ ctx,
8955
+ page,
8956
+ () => renderDiscussRead(ctx, page.topic, page.posts, page)
8957
+ );
8485
8958
  return;
8486
8959
  }
8487
8960
  for (let scan = 0; scan < 3; scan++) {
8488
8961
  const posts = /* @__PURE__ */ new Map();
8962
+ const projection = {
8963
+ authors: {},
8964
+ principals: {}
8965
+ };
8489
8966
  let topic = null;
8490
8967
  let offset = 0;
8491
8968
  for (; ; ) {
@@ -8496,6 +8973,7 @@ async function discussRead(ctx, id, parsed) {
8496
8973
  );
8497
8974
  topic = page.topic;
8498
8975
  for (const post of page.posts) posts.set(post.id, post);
8976
+ mergeDiscussPrincipals(projection, page);
8499
8977
  if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
8500
8978
  if (!page.page?.hasMore) break;
8501
8979
  if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
@@ -8508,24 +8986,18 @@ async function discussRead(ctx, id, parsed) {
8508
8986
  );
8509
8987
  const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
8510
8988
  if (ordered.length === expected) {
8511
- const result = { topic, posts: ordered };
8512
- emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
8989
+ const result = { topic, posts: ordered, ...projection };
8990
+ emit(
8991
+ ctx,
8992
+ result,
8993
+ () => renderDiscussRead(ctx, result.topic, result.posts, result)
8994
+ );
8513
8995
  return;
8514
8996
  }
8515
8997
  if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
8516
8998
  }
8517
8999
  throw new Error("discuss read failed: conversation changed during every complete-read attempt");
8518
9000
  }
8519
- function renderRead(ctx, topic, posts) {
8520
- ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
8521
- for (const post of posts) {
8522
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
8523
- ctx.out.log(`
8524
- \u2014 ${who}`);
8525
- ctx.out.log(bodyWithRefs(post));
8526
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
8527
- }
8528
- }
8529
9001
  async function discussPost(ctx, parsed) {
8530
9002
  const appId = stringOpt(parsed.options.app) ?? ctx.appId;
8531
9003
  if (!appId) throw new Error("discuss post needs --app <appId>");
@@ -8750,6 +9222,8 @@ async function discussWatch(ctx, topicId, parsed) {
8750
9222
  found: true,
8751
9223
  cursor,
8752
9224
  events: matching,
9225
+ ...page.authors ? { authors: page.authors } : {},
9226
+ ...page.principals ? { principals: page.principals } : {},
8753
9227
  ...posts && posts.length > 0 ? { posts } : {},
8754
9228
  ...topics && topics.length > 0 ? { topics } : {}
8755
9229
  });
@@ -8782,7 +9256,11 @@ function report2(ctx, parsed, result) {
8782
9256
  ctx.out.log(JSON.stringify(result, null, 2));
8783
9257
  } else if (parsed.options.jsonl !== true && result.found) {
8784
9258
  for (const post of result.posts ?? []) {
8785
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
9259
+ const who = discussPrincipalLabel(
9260
+ post.authorId,
9261
+ post.authorKind,
9262
+ result
9263
+ );
8786
9264
  ctx.out.log(`\u2014 ${who}
8787
9265
  ${post.body}`);
8788
9266
  }
@@ -8927,8 +9405,8 @@ function collectFields(parsed, allowClear) {
8927
9405
  if (allowClear) out[spec.key] = null;
8928
9406
  continue;
8929
9407
  }
8930
- const text = stringOpt(value2);
8931
- out[spec.key] = spec.num ? Number(text) : text;
9408
+ const text2 = stringOpt(value2);
9409
+ out[spec.key] = spec.num ? Number(text2) : text2;
8932
9410
  }
8933
9411
  return out;
8934
9412
  }
@@ -8995,8 +9473,8 @@ async function pmAdd(ctx, entity, parsed) {
8995
9473
  emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
8996
9474
  }
8997
9475
  async function pmGet(ctx, entity, id) {
8998
- const { record: record8 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
8999
- emit2(ctx, record8, () => printRecord(ctx, entity, record8));
9476
+ const { record: record11 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
9477
+ emit2(ctx, record11, () => printRecord(ctx, entity, record11));
9000
9478
  }
9001
9479
  async function pmSet(ctx, entity, id, parsed) {
9002
9480
  const patch2 = collectEntityFields(entity, parsed, true);
@@ -9041,9 +9519,9 @@ async function pmHandoff(ctx, parsed) {
9041
9519
  ]);
9042
9520
  const handoff = {
9043
9521
  appId,
9044
- unmetGoals: goals.filter((record8) => record8.status !== "met"),
9045
- activeTasks: tasks.filter((record8) => record8.column !== "done"),
9046
- openBugs: bugs.filter((record8) => record8.status !== "fixed" && record8.status !== "wontfix")
9522
+ unmetGoals: goals.filter((record11) => record11.status !== "met"),
9523
+ activeTasks: tasks.filter((record11) => record11.column !== "done"),
9524
+ openBugs: bugs.filter((record11) => record11.status !== "fixed" && record11.status !== "wontfix")
9047
9525
  };
9048
9526
  const result = {
9049
9527
  ...handoff,
@@ -9062,10 +9540,10 @@ async function pmHandoff(ctx, parsed) {
9062
9540
  ]) {
9063
9541
  ctx.out.log(`${label}:`);
9064
9542
  if (!records.length) ctx.out.log("- (none)");
9065
- else for (const record8 of records) printRecord(
9543
+ else for (const record11 of records) printRecord(
9066
9544
  ctx,
9067
9545
  label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
9068
- record8
9546
+ record11
9069
9547
  );
9070
9548
  }
9071
9549
  });
@@ -9256,19 +9734,180 @@ function age(input) {
9256
9734
  return `${Math.round(input / 6e4)}m`;
9257
9735
  }
9258
9736
 
9737
+ // src/platform-chat-credential-command.ts
9738
+ var import_node_process10 = __toESM(require("process"), 1);
9739
+ async function rotatePlatformChatCredential(parsed, deps) {
9740
+ assertArgs(
9741
+ parsed,
9742
+ [
9743
+ "config",
9744
+ "context",
9745
+ "platform",
9746
+ "token",
9747
+ "email",
9748
+ "open",
9749
+ "json",
9750
+ "yes",
9751
+ "wrangler-config",
9752
+ "expected-version"
9753
+ ],
9754
+ 3
9755
+ );
9756
+ if (parsed.options.yes !== true) {
9757
+ throw new Error(
9758
+ "platform chat-credentials rotate changes the production chat secret; pass --yes"
9759
+ );
9760
+ }
9761
+ const context = await resolveOperatorContext(parsed, {
9762
+ allowMissingConfig: true
9763
+ });
9764
+ const platform = context.platform.value;
9765
+ const doFetch = deps.fetch ?? fetch;
9766
+ const out = deps.stdout ?? console;
9767
+ const run = deps.runner ?? defaultRunner;
9768
+ const cwd = import_node_process10.default.cwd();
9769
+ const wranglerConfig = stringOpt(parsed.options["wrangler-config"]) ?? "packages/chat-agent/wrangler.jsonc";
9770
+ if (!await wranglerLoggedIn(run, cwd)) {
9771
+ throw new Error(
9772
+ 'Wrangler is not authenticated; run "npx wrangler login" and retry'
9773
+ );
9774
+ }
9775
+ const priorHealth = await doFetch(`${platform}/health/services/chat`);
9776
+ const priorVersion = priorHealth.headers.get("x-odla-worker-version-id");
9777
+ await priorHealth.body?.cancel().catch(() => {
9778
+ });
9779
+ if (!priorVersion) {
9780
+ throw new Error(
9781
+ "chat health did not identify its current Worker version; refusing to rotate"
9782
+ );
9783
+ }
9784
+ const expectedVersion = stringOpt(parsed.options["expected-version"]);
9785
+ if (expectedVersion && priorVersion !== expectedVersion) {
9786
+ throw new Error(
9787
+ `chat health answered from version ${priorVersion}; expected ${expectedVersion}`
9788
+ );
9789
+ }
9790
+ const token = await resolveAdminPlatformToken({
9791
+ platform,
9792
+ scope: "platform:chat:credential:write",
9793
+ token: stringOpt(parsed.options.token),
9794
+ tokenFile: context.credentials.scopedTokenFile,
9795
+ email: stringOpt(parsed.options.email),
9796
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9797
+ fetch: doFetch,
9798
+ stdout: out,
9799
+ openApprovalUrl: deps.openUrl,
9800
+ label: "odla CLI (rotate built-in Discussion responder)"
9801
+ });
9802
+ const mintedResponse = await doFetch(
9803
+ `${platform}/registry/platform/chat-credentials/rotate`,
9804
+ {
9805
+ method: "POST",
9806
+ headers: { authorization: `Bearer ${token}` }
9807
+ }
9808
+ );
9809
+ const minted = await mintedResponse.json().catch(() => null);
9810
+ if (!mintedResponse.ok) {
9811
+ throw new Error(
9812
+ `mint Discussion credential failed (HTTP ${mintedResponse.status}): ${apiMessage(minted)}`
9813
+ );
9814
+ }
9815
+ if (!isPlatformChatCredential(minted)) {
9816
+ throw new Error(
9817
+ "platform returned an invalid odla.platform-chat-credential/v1 envelope"
9818
+ );
9819
+ }
9820
+ const put = await wranglerPutSecret(run, {
9821
+ name: "ODLA_BOT_TOKENS",
9822
+ value: JSON.stringify({
9823
+ [minted.appId]: { [minted.principalId]: minted.key }
9824
+ }),
9825
+ configPath: wranglerConfig,
9826
+ cwd
9827
+ });
9828
+ if (put.code !== 0) {
9829
+ throw new Error(
9830
+ "Wrangler could not replace ODLA_BOT_TOKENS; the new scoped key was not activated"
9831
+ );
9832
+ }
9833
+ const version = await waitForRotatedChatHealth({
9834
+ fetch: doFetch,
9835
+ platform,
9836
+ priorVersion,
9837
+ wait: deps.pollWait
9838
+ });
9839
+ const result = {
9840
+ schemaVersion: "odla.platform-chat-credential-rotation/v1",
9841
+ appId: minted.appId,
9842
+ appIncarnation: minted.appIncarnation,
9843
+ principalId: minted.principalId,
9844
+ health: { ok: true, service: "odla-chat-agent", version }
9845
+ };
9846
+ if (parsed.options.json === true) {
9847
+ out.log(JSON.stringify(result, null, 2));
9848
+ } else {
9849
+ out.log(
9850
+ `rotated ${result.appId} ${result.principalId} ${result.health.version}`
9851
+ );
9852
+ }
9853
+ }
9854
+ async function waitForRotatedChatHealth(opts) {
9855
+ const wait2 = opts.wait ?? ((milliseconds) => new Promise((resolve12) => setTimeout(resolve12, milliseconds)));
9856
+ let lastStatus = 0;
9857
+ let lastVersion = null;
9858
+ let lastError = "private_service_unready";
9859
+ for (let attempt = 0; attempt < 60; attempt++) {
9860
+ const response2 = await opts.fetch(
9861
+ `${opts.platform}/health/services/chat`
9862
+ );
9863
+ const body = await response2.json().catch(() => null);
9864
+ const version = response2.headers.get("x-odla-worker-version-id");
9865
+ if (response2.ok && record7(body) && body.ok === true && body.service === "odla-chat-agent" && version && version !== opts.priorVersion) {
9866
+ return version;
9867
+ }
9868
+ lastStatus = response2.status;
9869
+ lastVersion = version;
9870
+ lastError = record7(body) && typeof body.error === "string" ? body.error : "private_service_unready";
9871
+ if (attempt < 59) await wait2(5e3);
9872
+ }
9873
+ throw new Error(
9874
+ `chat health did not converge on the rotated Worker deployment (HTTP ${lastStatus}, version ${lastVersion ?? "unknown"}, ${lastError})`
9875
+ );
9876
+ }
9877
+ function isPlatformChatCredential(value2) {
9878
+ return record7(value2) && value2.schemaVersion === "odla.platform-chat-credential/v1" && value2.appId === "odla-pm" && typeof value2.appIncarnation === "string" && value2.appIncarnation.length > 0 && value2.principalId === "agent_odla" && typeof value2.key === "string" && value2.key.startsWith("odla_sk_");
9879
+ }
9880
+ function apiMessage(value2) {
9881
+ if (!record7(value2)) return "request failed";
9882
+ const error = record7(value2.error) ? value2.error : value2;
9883
+ return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
9884
+ }
9885
+ function record7(value2) {
9886
+ return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
9887
+ }
9888
+
9259
9889
  // src/platform-command.ts
9260
9890
  async function platformCommand(parsed, deps = {}) {
9891
+ const action2 = parsed.positionals[1];
9892
+ if (action2 === "status") {
9893
+ return platformStatus(parsed, deps);
9894
+ }
9895
+ if (action2 === "chat-credentials" && parsed.positionals[2] === "rotate") {
9896
+ return rotatePlatformChatCredential(parsed, deps);
9897
+ }
9898
+ throw new Error(
9899
+ `unknown platform action "${[
9900
+ action2,
9901
+ parsed.positionals[2]
9902
+ ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
9903
+ );
9904
+ }
9905
+ async function platformStatus(parsed, deps) {
9261
9906
  assertArgs(
9262
9907
  parsed,
9263
9908
  ["config", "context", "platform", "token", "email", "open", "json"],
9264
9909
  2
9265
9910
  );
9266
- const action2 = parsed.positionals[1];
9267
- if (action2 !== "status") {
9268
- throw new Error(
9269
- `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
9270
- );
9271
- }
9272
9911
  const context = await resolveOperatorContext(parsed, {
9273
9912
  allowMissingConfig: true
9274
9913
  });
@@ -9293,7 +9932,7 @@ async function platformCommand(parsed, deps = {}) {
9293
9932
  const body = await response2.json().catch(() => null);
9294
9933
  if (!response2.ok) {
9295
9934
  throw new Error(
9296
- `read platform status failed (HTTP ${response2.status}): ${apiMessage(body)}`
9935
+ `read platform status failed (HTTP ${response2.status}): ${apiMessage2(body)}`
9297
9936
  );
9298
9937
  }
9299
9938
  if (!isPlatformStatus(body)) {
@@ -9306,17 +9945,17 @@ async function platformCommand(parsed, deps = {}) {
9306
9945
  }
9307
9946
  }
9308
9947
  function isPlatformStatus(value2) {
9309
- if (!record5(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
9310
- if (!record5(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
9311
- if (!record5(value2.catalog) || !record5(value2.summary)) return false;
9948
+ if (!record8(value2) || value2.schemaVersion !== "odla.platform-status/v1") return false;
9949
+ if (!record8(value2.verdict) || !Array.isArray(value2.verdict.reasons)) return false;
9950
+ if (!record8(value2.catalog) || !record8(value2.summary)) return false;
9312
9951
  return Array.isArray(value2.services) && Array.isArray(value2.nextActions);
9313
9952
  }
9314
- function apiMessage(value2) {
9315
- if (!record5(value2)) return "request failed";
9316
- const error = record5(value2.error) ? value2.error : value2;
9953
+ function apiMessage2(value2) {
9954
+ if (!record8(value2)) return "request failed";
9955
+ const error = record8(value2.error) ? value2.error : value2;
9317
9956
  return typeof error.message === "string" ? error.message : typeof error.code === "string" ? error.code : "request failed";
9318
9957
  }
9319
- function record5(value2) {
9958
+ function record8(value2) {
9320
9959
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
9321
9960
  }
9322
9961
 
@@ -9357,7 +9996,7 @@ function statusVerdict(reads) {
9357
9996
  severity: "degraded"
9358
9997
  });
9359
9998
  }
9360
- const performance = record6(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
9999
+ const performance = record9(reads.liveSync.body.performance) ? reads.liveSync.body.performance : null;
9361
10000
  if (performance?.status === "unavailable") {
9362
10001
  reasons.push({
9363
10002
  source: "liveSync",
@@ -9438,7 +10077,7 @@ function statusVerdict(reads) {
9438
10077
  reasons
9439
10078
  };
9440
10079
  }
9441
- function record6(value2) {
10080
+ function record9(value2) {
9442
10081
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
9443
10082
  }
9444
10083
  function numeric2(value2) {
@@ -9466,7 +10105,7 @@ function printO11yStatus(status, out) {
9466
10105
  out.log(
9467
10106
  `o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
9468
10107
  );
9469
- const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record7) : [];
10108
+ const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record10) : [];
9470
10109
  const requests = routes.reduce(
9471
10110
  (total, row) => total + numeric3(row.requests),
9472
10111
  0
@@ -9478,39 +10117,39 @@ function printO11yStatus(status, out) {
9478
10117
  out.log(
9479
10118
  `application ${status.application.httpStatus} ${requests} requests ${errors} errors`
9480
10119
  );
9481
- const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record7) : [];
10120
+ const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record10) : [];
9482
10121
  out.log(
9483
10122
  `application-versions ${status.applicationVersions.httpStatus} ${versions.length ? versions.slice(0, 5).map(
9484
10123
  (row) => `${String(row.value || "(unattributed)")}:${numeric3(row.requests)}`
9485
10124
  ).join(", ") : "none observed"}`
9486
10125
  );
9487
10126
  out.log(liveSyncLine(status.liveSync));
9488
- const canaryDurations = record7(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
10127
+ const canaryDurations = record10(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
9489
10128
  out.log(
9490
10129
  `canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
9491
10130
  );
9492
- const collectorIngest = record7(status.collector.body.ingest) ? status.collector.body.ingest : {};
9493
- const collectorStorage = record7(collectorIngest.storage) ? collectorIngest.storage : {};
10131
+ const collectorIngest = record10(status.collector.body.ingest) ? status.collector.body.ingest : {};
10132
+ const collectorStorage = record10(collectorIngest.storage) ? collectorIngest.storage : {};
9494
10133
  out.log(
9495
10134
  `collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
9496
10135
  );
9497
- const providerMetrics = record7(status.provider.body.metrics) ? status.provider.body.metrics : {};
9498
- const providerCapacity = record7(status.provider.body.capacity) ? status.provider.body.capacity : {};
9499
- const workerMemory = record7(providerCapacity.memory) ? providerCapacity.memory : {};
10136
+ const providerMetrics = record10(status.provider.body.metrics) ? status.provider.body.metrics : {};
10137
+ const providerCapacity = record10(status.provider.body.capacity) ? status.provider.body.capacity : {};
10138
+ const workerMemory = record10(providerCapacity.memory) ? providerCapacity.memory : {};
9500
10139
  out.log(
9501
10140
  `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
9502
10141
  );
9503
10142
  for (const line of providerCapacityLines(status.providerCapacity)) {
9504
10143
  out.log(line);
9505
10144
  }
9506
- const coverage = record7(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
9507
- const coverageCounts = record7(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
9508
- const coverageBudget = record7(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
10145
+ const coverage = record10(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
10146
+ const coverageCounts = record10(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
10147
+ const coverageBudget = record10(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
9509
10148
  out.log(
9510
10149
  `request-coverage ${status.providerReconciliation.httpStatus} ${String(status.providerReconciliation.body.status ?? status.providerReconciliation.body.error ?? "unavailable")} ${optionalPercent(coverage.applicationCoverage)} application/provider ${numeric3(coverageCounts.applicationRequests)}/${numeric3(coverageCounts.providerRequests)} requests \xB1${optionalPercent(coverageBudget.maxRelativeError)} budget`
9511
10150
  );
9512
10151
  const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
9513
- const providerFreshness = record7(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
10152
+ const providerFreshness = record10(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
9514
10153
  out.log(
9515
10154
  `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
9516
10155
  );
@@ -9519,17 +10158,17 @@ function printO11yStatus(status, out) {
9519
10158
  );
9520
10159
  }
9521
10160
  function providerCapacityLines(read3) {
9522
- const resources = record7(read3.body.resources) ? read3.body.resources : {};
9523
- const durableObjects = record7(resources.durableObjects) ? resources.durableObjects : {};
9524
- const periodic = record7(durableObjects.periodic) ? durableObjects.periodic : {};
9525
- const storage = record7(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
9526
- const d1 = record7(resources.d1) ? resources.d1 : {};
9527
- const d1Activity = record7(d1.activity) ? d1.activity : {};
9528
- const d1Storage = record7(d1.storage) ? d1.storage : {};
9529
- const d1Latency = record7(d1Activity.latency) ? d1Activity.latency : {};
9530
- const r2 = record7(resources.r2) ? resources.r2 : {};
9531
- const r2Operations = record7(r2.operations) ? r2.operations : {};
9532
- const r2Storage = record7(r2.storage) ? r2.storage : {};
10161
+ const resources = record10(read3.body.resources) ? read3.body.resources : {};
10162
+ const durableObjects = record10(resources.durableObjects) ? resources.durableObjects : {};
10163
+ const periodic = record10(durableObjects.periodic) ? durableObjects.periodic : {};
10164
+ const storage = record10(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
10165
+ const d1 = record10(resources.d1) ? resources.d1 : {};
10166
+ const d1Activity = record10(d1.activity) ? d1.activity : {};
10167
+ const d1Storage = record10(d1.storage) ? d1.storage : {};
10168
+ const d1Latency = record10(d1Activity.latency) ? d1Activity.latency : {};
10169
+ const r2 = record10(resources.r2) ? resources.r2 : {};
10170
+ const r2Operations = record10(r2.operations) ? r2.operations : {};
10171
+ const r2Storage = record10(r2.storage) ? r2.storage : {};
9533
10172
  const status = String(
9534
10173
  read3.body.status ?? read3.body.error ?? "unavailable"
9535
10174
  );
@@ -9540,11 +10179,11 @@ function providerCapacityLines(read3) {
9540
10179
  ];
9541
10180
  }
9542
10181
  function liveSyncLine(read3) {
9543
- const performance = record7(read3.body.performance) ? read3.body.performance : {};
9544
- const commitToSend = record7(performance.commitToSend) ? performance.commitToSend : {};
10182
+ const performance = record10(read3.body.performance) ? read3.body.performance : {};
10183
+ const commitToSend = record10(performance.commitToSend) ? performance.commitToSend : {};
9545
10184
  return `live-sync ${read3.httpStatus} ${String(read3.body.status ?? read3.body.error ?? "unavailable")} ${numeric3(read3.body.activeConnections)} active ${optionalNumeric(commitToSend.p95)} commit-to-send p95 ${numeric3(performance.sendFailures)} send failures`;
9546
10185
  }
9547
- function record7(value2) {
10186
+ function record10(value2) {
9548
10187
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
9549
10188
  }
9550
10189
  function numeric3(value2) {
@@ -9713,23 +10352,23 @@ function statusMinutes(value2) {
9713
10352
  }
9714
10353
  async function read2(url, headers, doFetch) {
9715
10354
  const response2 = await doFetch(url, { headers });
9716
- const text = await response2.text();
10355
+ const text2 = await response2.text();
9717
10356
  let body = {};
9718
- if (text) {
10357
+ if (text2) {
9719
10358
  try {
9720
- const value2 = JSON.parse(text);
10359
+ const value2 = JSON.parse(text2);
9721
10360
  body = value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : { value: value2 };
9722
10361
  } catch {
9723
- body = { message: text.slice(0, 300) };
10362
+ body = { message: text2.slice(0, 300) };
9724
10363
  }
9725
10364
  }
9726
10365
  return { httpStatus: response2.status, body };
9727
10366
  }
9728
10367
 
9729
10368
  // src/provision.ts
9730
- var import_apps10 = require("@odla-ai/apps");
10369
+ var import_apps12 = require("@odla-ai/apps");
9731
10370
  var import_ai3 = require("@odla-ai/ai");
9732
- var import_node_process10 = __toESM(require("process"), 1);
10371
+ var import_node_process11 = __toESM(require("process"), 1);
9733
10372
 
9734
10373
  // src/integration-provision.ts
9735
10374
  var import_db3 = require("@odla-ai/db");
@@ -9784,9 +10423,9 @@ async function responseText(res) {
9784
10423
  }
9785
10424
 
9786
10425
  // src/provision-credentials.ts
9787
- var import_apps9 = require("@odla-ai/apps");
10426
+ var import_apps11 = require("@odla-ai/apps");
9788
10427
  async function provisionEnvCredentials(opts) {
9789
- const tenantId = (0, import_apps9.tenantIdFor)(opts.cfg.app.id, opts.env);
10428
+ const tenantId = (0, import_apps11.tenantIdFor)(opts.cfg.app.id, opts.env);
9790
10429
  const prior = opts.credentials?.envs[opts.env];
9791
10430
  let credentials = opts.credentials;
9792
10431
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -9948,7 +10587,7 @@ async function provision(options) {
9948
10587
  }
9949
10588
  const doFetch = options.fetch ?? fetch;
9950
10589
  const token = await getDeveloperToken(cfg, options, doFetch, out);
9951
- const apps = (0, import_apps10.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
10590
+ const apps = (0, import_apps12.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
9952
10591
  const existing = await apps.resolveApp(cfg.app.id);
9953
10592
  if (existing) {
9954
10593
  out.log(`app: ${cfg.app.id} already exists`);
@@ -9959,7 +10598,7 @@ async function provision(options) {
9959
10598
  for (const env of cfg.envs) {
9960
10599
  await assertTenantAdminAccess(doFetch, cfg, env, token);
9961
10600
  }
9962
- const serviceOrder = (0, import_apps10.orderAppServices)(cfg.services);
10601
+ const serviceOrder = (0, import_apps12.orderAppServices)(cfg.services);
9963
10602
  for (const env of cfg.envs) {
9964
10603
  for (const service of serviceOrder) {
9965
10604
  if (service === "ai") {
@@ -9992,7 +10631,7 @@ async function provision(options) {
9992
10631
  }
9993
10632
  }
9994
10633
  for (const env of cfg.envs) {
9995
- const tenantId = (0, import_apps10.tenantIdFor)(cfg.app.id, env);
10634
+ const tenantId = (0, import_apps12.tenantIdFor)(cfg.app.id, env);
9996
10635
  credentials = await provisionEnvCredentials({
9997
10636
  cfg,
9998
10637
  env,
@@ -10036,7 +10675,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10036
10675
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10037
10676
  }
10038
10677
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10039
- const key = import_node_process10.default.env[cfg.ai.keyEnv];
10678
+ const key = import_node_process11.default.env[cfg.ai.keyEnv];
10040
10679
  if (key) {
10041
10680
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10042
10681
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10077,8 +10716,8 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10077
10716
  }
10078
10717
 
10079
10718
  // src/record.ts
10080
- var import_node_fs15 = require("fs");
10081
- var import_node_process11 = __toESM(require("process"), 1);
10719
+ var import_node_fs16 = require("fs");
10720
+ var import_node_process12 = __toESM(require("process"), 1);
10082
10721
 
10083
10722
  // src/surface.ts
10084
10723
  var PM_ACTIONS = {
@@ -10126,7 +10765,7 @@ var COMMAND_SURFACE = {
10126
10765
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
10127
10766
  capabilities: {},
10128
10767
  code: { connect: {} },
10129
- config: { diff: {}, plan: {} },
10768
+ config: { diff: {}, plan: {}, apply: {} },
10130
10769
  context: { show: {}, list: {}, save: {}, remove: {} },
10131
10770
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
10132
10771
  discuss: {
@@ -10144,7 +10783,11 @@ var COMMAND_SURFACE = {
10144
10783
  help: {},
10145
10784
  init: {},
10146
10785
  o11y: { status: {} },
10147
- platform: { status: {} },
10786
+ operations: { get: {}, wait: {} },
10787
+ platform: {
10788
+ status: {},
10789
+ "chat-credentials": { rotate: {} }
10790
+ },
10148
10791
  pm: {
10149
10792
  ...PM_ENTITIES,
10150
10793
  handoff: {}
@@ -10232,7 +10875,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
10232
10875
 
10233
10876
  // src/record.ts
10234
10877
  function recordInvocation(parsed) {
10235
- const file = import_node_process11.default.env.ODLA_CLI_RECORD;
10878
+ const file = import_node_process12.default.env.ODLA_CLI_RECORD;
10236
10879
  if (!file) return;
10237
10880
  try {
10238
10881
  const entry = {
@@ -10240,14 +10883,14 @@ function recordInvocation(parsed) {
10240
10883
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
10241
10884
  };
10242
10885
  if (!entry.path.length) return;
10243
- (0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
10886
+ (0, import_node_fs16.appendFileSync)(file, `${JSON.stringify(entry)}
10244
10887
  `);
10245
10888
  } catch {
10246
10889
  }
10247
10890
  }
10248
10891
 
10249
10892
  // src/runbook-actions.ts
10250
- var import_node_fs16 = require("fs");
10893
+ var import_node_fs17 = require("fs");
10251
10894
 
10252
10895
  // src/runbook-requires.ts
10253
10896
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -10332,7 +10975,7 @@ async function bySlug(ctx, slug) {
10332
10975
  function readBody(file, inline) {
10333
10976
  if (inline !== void 0) return inline;
10334
10977
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
10335
- return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
10978
+ return (0, import_node_fs17.readFileSync)(file === "-" ? 0 : file, "utf8");
10336
10979
  }
10337
10980
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
10338
10981
  async function runbookList(ctx, all, query) {
@@ -10362,12 +11005,17 @@ async function runbookNew(ctx, slug, title, body, summary, requires) {
10362
11005
  });
10363
11006
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
10364
11007
  }
10365
- async function runbookEdit(ctx, slug, body, note, requires) {
11008
+ async function runbookEdit(ctx, slug, body, note, requires, expectedVersion) {
10366
11009
  const runbook = await bySlug(ctx, slug);
10367
11010
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
10368
11011
  // An empty --requires clears the declaration; omitting the flag leaves
10369
11012
  // whatever is there, so an ordinary body edit never drops it.
10370
- patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
11013
+ patch: {
11014
+ body,
11015
+ expectedVersion: expectedVersion ?? runbook.version,
11016
+ ...note ? { note } : {},
11017
+ ...requires === void 0 ? {} : { requires: requires || null }
11018
+ }
10371
11019
  });
10372
11020
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
10373
11021
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -10419,14 +11067,14 @@ async function runbookRemove(ctx, slug) {
10419
11067
  }
10420
11068
 
10421
11069
  // src/runbook-import.ts
10422
- var import_node_fs17 = require("fs");
10423
- var import_node_path14 = require("path");
10424
- function parseRunbook(text, slug) {
10425
- let rest = text;
11070
+ var import_node_fs18 = require("fs");
11071
+ var import_node_path15 = require("path");
11072
+ function parseRunbook(text2, slug) {
11073
+ let rest = text2;
10426
11074
  const meta = {};
10427
- const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
11075
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text2);
10428
11076
  if (fm) {
10429
- rest = text.slice(fm[0].length);
11077
+ rest = text2.slice(fm[0].length);
10430
11078
  for (const line of fm[1].split(/\r?\n/)) {
10431
11079
  const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
10432
11080
  if (!pair) continue;
@@ -10445,12 +11093,12 @@ function parseRunbook(text, slug) {
10445
11093
  };
10446
11094
  }
10447
11095
  function readRunbookDir(dir) {
10448
- if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
10449
- const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
11096
+ if (!(0, import_node_fs18.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
11097
+ const files = (0, import_node_fs18.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
10450
11098
  if (!files.length) throw new Error(`no .md files in ${dir}`);
10451
11099
  return files.map((file) => {
10452
- const slug = (0, import_node_path14.basename)(file, ".md");
10453
- const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path14.join)(dir, file), "utf8"), slug);
11100
+ const slug = (0, import_node_path15.basename)(file, ".md");
11101
+ const parsed = parseRunbook((0, import_node_fs18.readFileSync)((0, import_node_path15.join)(dir, file), "utf8"), slug);
10454
11102
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
10455
11103
  });
10456
11104
  }
@@ -10507,6 +11155,7 @@ async function upsert(ctx, r, visibility) {
10507
11155
  patch: {
10508
11156
  title: r.title,
10509
11157
  body: r.body,
11158
+ expectedVersion: found.version,
10510
11159
  ...r.summary ? { summary: r.summary } : {},
10511
11160
  ...r.tags ? { tags: r.tags } : {},
10512
11161
  note: `imported from ${r.file}`
@@ -10522,8 +11171,8 @@ async function upsert(ctx, r, visibility) {
10522
11171
 
10523
11172
  // src/runbook-impact.ts
10524
11173
  var import_node_child_process7 = require("child_process");
10525
- var import_node_fs18 = require("fs");
10526
- var import_node_path15 = require("path");
11174
+ var import_node_fs19 = require("fs");
11175
+ var import_node_path16 = require("path");
10527
11176
 
10528
11177
  // src/runbook-impact-scan.ts
10529
11178
  var DECL = /^[+-]\s*export\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
@@ -10692,10 +11341,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
10692
11341
  }
10693
11342
  function manifestLabeller(root) {
10694
11343
  return (workspace) => {
10695
- const manifest = (0, import_node_path15.join)(root, workspace, "package.json");
10696
- if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
11344
+ const manifest = (0, import_node_path16.join)(root, workspace, "package.json");
11345
+ if (!(0, import_node_fs19.existsSync)(manifest)) return void 0;
10697
11346
  try {
10698
- const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
11347
+ const name = JSON.parse((0, import_node_fs19.readFileSync)(manifest, "utf8")).name;
10699
11348
  return typeof name === "string" ? name : void 0;
10700
11349
  } catch {
10701
11350
  return void 0;
@@ -10762,7 +11411,7 @@ function report3(ctx, impacts) {
10762
11411
  async function runbookImpact(ctx, options, deps = {}) {
10763
11412
  const cwd = deps.cwd ?? process.cwd();
10764
11413
  const runGit = deps.runGit ?? gitRunner(cwd);
10765
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path15.join)(cwd, path), "utf8"));
11414
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs19.readFileSync)((0, import_node_path16.join)(cwd, path), "utf8"));
10766
11415
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
10767
11416
  if (!surfaces.length) {
10768
11417
  return ctx.out.log(
@@ -10895,12 +11544,12 @@ async function runbookComment(ctx, slug, body) {
10895
11544
 
10896
11545
  // src/runbook-editor.ts
10897
11546
  var import_node_child_process8 = require("child_process");
10898
- var import_node_fs19 = require("fs");
11547
+ var import_node_fs20 = require("fs");
10899
11548
  var import_node_os5 = require("os");
10900
- var import_node_path16 = require("path");
10901
- var import_node_process12 = __toESM(require("process"), 1);
11549
+ var import_node_path17 = require("path");
11550
+ var import_node_process13 = __toESM(require("process"), 1);
10902
11551
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
10903
- function resolveEditor(env = import_node_process12.default.env) {
11552
+ function resolveEditor(env = import_node_process13.default.env) {
10904
11553
  for (const name of EDITOR_ENV) {
10905
11554
  const value2 = env[name];
10906
11555
  if (value2 && value2.trim()) return value2.trim();
@@ -10914,8 +11563,8 @@ function defaultRun(command, path) {
10914
11563
  return result.status ?? 0;
10915
11564
  }
10916
11565
  function editText(initial, slug, deps = {}) {
10917
- const env = deps.env ?? import_node_process12.default.env;
10918
- const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
11566
+ const env = deps.env ?? import_node_process13.default.env;
11567
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
10919
11568
  const editor = resolveEditor(env);
10920
11569
  if (!editor)
10921
11570
  throw new Error(
@@ -10923,16 +11572,16 @@ function editText(initial, slug, deps = {}) {
10923
11572
  );
10924
11573
  if (!interactive())
10925
11574
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
10926
- const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
10927
- const file = (0, import_node_path16.join)(dir, `${slug}.md`);
11575
+ const dir = (0, import_node_fs20.mkdtempSync)((0, import_node_path17.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
11576
+ const file = (0, import_node_path17.join)(dir, `${slug}.md`);
10928
11577
  try {
10929
- (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
11578
+ (0, import_node_fs20.writeFileSync)(file, initial, { mode: 384 });
10930
11579
  const code = defaultRunOrInjected(deps)(editor, file);
10931
11580
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
10932
- const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
11581
+ const edited = (0, import_node_fs20.readFileSync)(file, "utf8");
10933
11582
  return edited === initial ? null : edited;
10934
11583
  } finally {
10935
- (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
11584
+ (0, import_node_fs20.rmSync)(dir, { recursive: true, force: true });
10936
11585
  }
10937
11586
  }
10938
11587
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -10947,27 +11596,90 @@ async function editRunbook(ctx, slug, deps = {}) {
10947
11596
  const found = page.records[0];
10948
11597
  if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
10949
11598
  ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
10950
- return editText(found.body, slug, deps);
11599
+ const body = await editText(found.body, slug, deps);
11600
+ return body === null ? null : { body, expectedVersion: found.version };
10951
11601
  }
10952
11602
 
10953
11603
  // src/whoami-command.ts
11604
+ var text = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
11605
+ function principalKind(value2, machine) {
11606
+ return value2 === "human" || value2 === "agent" || value2 === "service" ? value2 : machine ? "service" : "human";
11607
+ }
11608
+ function credentialKind(value2, machine, scopes) {
11609
+ if (value2 === "machine" || value2 === "device" || value2 === "clerk") return value2;
11610
+ if (machine) return "machine";
11611
+ if (scopes.length) return "device";
11612
+ return "unknown";
11613
+ }
11614
+ function managerOf(value2) {
11615
+ if (!value2 || typeof value2 !== "object") return null;
11616
+ const row = value2;
11617
+ const principalId = text(row.principalId);
11618
+ if (!principalId) return null;
11619
+ return {
11620
+ principalId,
11621
+ displayName: text(row.displayName) ?? "Unnamed member",
11622
+ handle: text(row.handle) ?? ""
11623
+ };
11624
+ }
11625
+ function unnamedPrincipal(kind) {
11626
+ if (kind === "agent") return "Unnamed agent";
11627
+ if (kind === "service") return "Unnamed service";
11628
+ return "Unnamed member";
11629
+ }
10954
11630
  async function fetchIdentity(platformUrl, token, doFetch) {
10955
11631
  const res = await doFetch(`${platformUrl.replace(/\/$/, "")}/registry/me`, {
10956
11632
  headers: { authorization: `Bearer ${token}` }
10957
11633
  });
10958
11634
  if (!res.ok) throw new Error(`could not resolve identity (HTTP ${res.status})`);
10959
11635
  const body = await res.json();
11636
+ const developerId = text(body.developerId) ?? "";
11637
+ const machine = body.machine === true;
11638
+ const scopes = Array.isArray(body.scopes) ? body.scopes.map(String) : [];
11639
+ const principalId = text(body.principalId) ?? developerId;
11640
+ const email = text(body.email);
11641
+ const kind = principalKind(body.principalKind, machine);
11642
+ const displayName = text(body.displayName) ?? email ?? unnamedPrincipal(kind);
11643
+ const handle = text(body.handle) ?? "";
11644
+ const credential2 = body.credential && typeof body.credential === "object" ? body.credential : {};
10960
11645
  return {
10961
- developerId: String(body.developerId ?? ""),
10962
- email: body.email ?? null,
11646
+ developerId,
11647
+ principalId,
11648
+ principalKind: kind,
11649
+ displayName,
11650
+ handle,
11651
+ manager: managerOf(body.manager),
11652
+ credential: {
11653
+ id: text(credential2.id),
11654
+ kind: credentialKind(credential2.kind, machine, scopes)
11655
+ },
11656
+ email,
10963
11657
  admin: body.admin === true,
10964
- machine: body.machine === true,
10965
- scopes: Array.isArray(body.scopes) ? body.scopes.map(String) : []
11658
+ machine,
11659
+ scopes
10966
11660
  };
10967
11661
  }
10968
- function credentialKind(identity) {
10969
- if (identity.machine) return "machine (platform admin secret)";
10970
- return identity.scopes.length ? "device token (scoped)" : "device token or session";
11662
+ function credentialLabel(identity) {
11663
+ if (identity.credential.kind === "machine") return "machine (platform admin secret)";
11664
+ if (identity.credential.kind === "device")
11665
+ return identity.scopes.length ? "device (scoped)" : "device";
11666
+ if (identity.credential.kind === "clerk") return "clerk";
11667
+ return "unknown (legacy server)";
11668
+ }
11669
+ function namedPrincipal(identity) {
11670
+ return identity.handle && identity.handle !== identity.displayName ? `${identity.displayName} (@${identity.handle})` : identity.displayName;
11671
+ }
11672
+ function namedManager(manager) {
11673
+ return manager.handle && manager.handle !== manager.displayName ? `${manager.displayName} (@${manager.handle})` : manager.displayName;
11674
+ }
11675
+ function accountableOwner(identity) {
11676
+ if (identity.principalKind === "agent" && identity.manager) {
11677
+ return namedManager(identity.manager);
11678
+ }
11679
+ if (identity.principalKind === "human" && identity.principalId === identity.developerId) {
11680
+ return namedPrincipal(identity);
11681
+ }
11682
+ return identity.email ?? "Unnamed member";
10971
11683
  }
10972
11684
  async function whoamiCommand(parsed, deps = {}) {
10973
11685
  assertArgs(
@@ -10998,9 +11710,19 @@ async function whoamiCommand(parsed, deps = {}) {
10998
11710
  return;
10999
11711
  }
11000
11712
  out.log(`platform: ${cfg.platformUrl}`);
11001
- out.log(`developer: ${identity.developerId}`);
11713
+ out.log(`principal: ${namedPrincipal(identity)}`);
11714
+ out.log(`principal id: ${identity.principalId}`);
11715
+ out.log(`kind: ${identity.principalKind}`);
11716
+ if (identity.principalKind === "agent")
11717
+ out.log(
11718
+ `manager: ${identity.manager ? namedManager(identity.manager) : "(unknown)"}`
11719
+ );
11720
+ out.log(`owner: ${accountableOwner(identity)}`);
11721
+ out.log(`owner id: ${identity.developerId}`);
11002
11722
  out.log(`email: ${identity.email ?? "(none)"}`);
11003
- out.log(`credential: ${credentialKind(identity)}`);
11723
+ out.log(`credential: ${credentialLabel(identity)}`);
11724
+ if (identity.credential.id)
11725
+ out.log(`credential id: ${identity.credential.id}`);
11004
11726
  out.log(`admin: ${identity.admin ? "yes" : "no"}`);
11005
11727
  if (identity.scopes.length) out.log(`scopes: ${identity.scopes.join(", ")}`);
11006
11728
  if (!identity.admin) {
@@ -11149,14 +11871,16 @@ async function runbookCommand(parsed, deps = {}) {
11149
11871
  const name = requireSlug(slug, "edit");
11150
11872
  const file = stringOpt(parsed.options.file);
11151
11873
  const inline = stringOpt(parsed.options.body);
11152
- const body = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11153
- if (body === null) return ctx.out.log(`${name} unchanged; nothing written`);
11874
+ const edited = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
11875
+ if (edited === null) return ctx.out.log(`${name} unchanged; nothing written`);
11876
+ const body = typeof edited === "string" ? edited : edited.body;
11154
11877
  return runbookEdit(
11155
11878
  ctx,
11156
11879
  name,
11157
11880
  body,
11158
11881
  stringOpt(parsed.options.note),
11159
- parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
11882
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? "",
11883
+ typeof edited === "string" ? void 0 : edited.expectedVersion
11160
11884
  );
11161
11885
  }
11162
11886
  case "import": {
@@ -11344,7 +12068,7 @@ function hostedSeverity(value2, flag) {
11344
12068
  var import_security2 = require("@odla-ai/security");
11345
12069
 
11346
12070
  // src/security.ts
11347
- var import_node_path17 = require("path");
12071
+ var import_node_path18 = require("path");
11348
12072
  var import_security = require("@odla-ai/security");
11349
12073
  var import_node3 = require("@odla-ai/security/node");
11350
12074
  async function runHostedSecurity(options) {
@@ -11356,9 +12080,9 @@ async function runHostedSecurity(options) {
11356
12080
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
11357
12081
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
11358
12082
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
11359
- const target = (0, import_node_path17.resolve)(options.target ?? cfg?.rootDir ?? ".");
11360
- const output = (0, import_node_path17.resolve)(options.out ?? (0, import_node_path17.resolve)(target, ".odla/security/hosted"));
11361
- const outputRelative = (0, import_node_path17.relative)(target, output).split(import_node_path17.sep).join("/");
12083
+ const target = (0, import_node_path18.resolve)(options.target ?? cfg?.rootDir ?? ".");
12084
+ const output = (0, import_node_path18.resolve)(options.out ?? (0, import_node_path18.resolve)(target, ".odla/security/hosted"));
12085
+ const outputRelative = (0, import_node_path18.relative)(target, output).split(import_node_path18.sep).join("/");
11362
12086
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
11363
12087
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
11364
12088
  const tokenRequest = {
@@ -11370,7 +12094,7 @@ async function runHostedSecurity(options) {
11370
12094
  };
11371
12095
  const token = await injectedToken(options, tokenRequest);
11372
12096
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
11373
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path17.isAbsolute)(outputRelative) ? [outputRelative] : []
12097
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path18.isAbsolute)(outputRelative) ? [outputRelative] : []
11374
12098
  });
11375
12099
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
11376
12100
  platform,
@@ -11388,7 +12112,7 @@ async function runHostedSecurity(options) {
11388
12112
  });
11389
12113
  const harness = (0, import_security.createSecurityHarness)({
11390
12114
  profile,
11391
- store: new import_node3.FileRunStore((0, import_node_path17.resolve)(output, "state")),
12115
+ store: new import_node3.FileRunStore((0, import_node_path18.resolve)(output, "state")),
11392
12116
  discoveryReasoner: hosted.discoveryReasoner,
11393
12117
  validationReasoner: hosted.validationReasoner,
11394
12118
  policy: {
@@ -11412,7 +12136,7 @@ async function runHostedSecurity(options) {
11412
12136
  function selectEnv(requested, declared, configPath, rootDir) {
11413
12137
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
11414
12138
  if (!env || !declared.includes(env)) {
11415
- const shown = (0, import_node_path17.relative)(rootDir, configPath) || configPath;
12139
+ const shown = (0, import_node_path18.relative)(rootDir, configPath) || configPath;
11416
12140
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
11417
12141
  }
11418
12142
  return env;
@@ -11441,7 +12165,7 @@ function printSummary(out, appId, env, run, report4, output) {
11441
12165
  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}`);
11442
12166
  if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
11443
12167
  out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
11444
- out.log(` report: ${(0, import_node_path17.resolve)(output, "REPORT.md")}`);
12168
+ out.log(` report: ${(0, import_node_path18.resolve)(output, "REPORT.md")}`);
11445
12169
  }
11446
12170
  function formatBudget(usage) {
11447
12171
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
@@ -11892,11 +12616,11 @@ async function securityStatus(parsed, dependencies) {
11892
12616
  // src/cli.ts
11893
12617
  function exitCodeFor(err) {
11894
12618
  const code = err?.code;
11895
- if (code === "handshake_pending" || code === "watch_timeout") return 75;
12619
+ if (code === "handshake_pending" || code === "watch_timeout" || code === "operation_pending") return 75;
11896
12620
  if (code === "checkpoint_required") return 3;
11897
12621
  if (code === "remote_unavailable") return 6;
11898
12622
  if (code === "auth_failed") return 5;
11899
- if (code === "invalid_cursor") return 2;
12623
+ if (code === "invalid_cursor" || code === "invalid_plan" || code === "invalid_operation_id") return 2;
11900
12624
  return 1;
11901
12625
  }
11902
12626
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
@@ -12043,6 +12767,7 @@ async function calendarCommand(parsed, dependencies) {
12043
12767
  CODE_BUILD_RECIPES,
12044
12768
  CODE_PI_IMAGE,
12045
12769
  COMMAND_SURFACE,
12770
+ ConfigOperationCommandError,
12046
12771
  GOOGLE_CALENDAR_EVENTS_SCOPE,
12047
12772
  SYSTEM_AI_PURPOSES,
12048
12773
  acceptedAfter,
@@ -12054,7 +12779,10 @@ async function calendarCommand(parsed, dependencies) {
12054
12779
  calendarServiceConfig,
12055
12780
  calendarStatus,
12056
12781
  codeConnect,
12782
+ configApply,
12057
12783
  configDiff,
12784
+ configOperationGet,
12785
+ configOperationWait,
12058
12786
  configPlan,
12059
12787
  connectGitHubSecuritySource,
12060
12788
  describeProblem,