okengine 0.23.1 → 0.23.2

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.
Files changed (83) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/ai/mcp.mdx +3 -3
  3. package/site/content/docs/elements/signal/broadcast.mdx +4 -2
  4. package/site/content/docs/reference/configuration.mdx +16 -0
  5. package/site/content/docs/reference/errors.mdx +31 -19
  6. package/src/compiler/aot.test.ts +3 -2
  7. package/src/compiler/aot.ts +45 -1
  8. package/src/compiler/dynamic.ts +1 -1
  9. package/src/compiler/http-parse.test.ts +101 -0
  10. package/src/compiler/http-parse.ts +188 -9
  11. package/src/compiler/interpret.ts +25 -2
  12. package/src/console/ui-next/dist/assets/{access-page-C_N4qvai.js → access-page-zBMhZnFm.js} +1 -1
  13. package/src/console/ui-next/dist/assets/{agent-disclosure-Cq1TxniW.js → agent-disclosure-CBj9f7Ju.js} +1 -1
  14. package/src/console/ui-next/dist/assets/{cache-glyph-BaI4uxZz.js → cache-glyph-D3MY49h3.js} +1 -1
  15. package/src/console/ui-next/dist/assets/{call-pii-button-DNyK7IIY.js → call-pii-button-L9JxdBhp.js} +1 -1
  16. package/src/console/ui-next/dist/assets/{collapsible-CRhBJMZR.js → collapsible-BvFaX-Zt.js} +1 -1
  17. package/src/console/ui-next/dist/assets/{decisions-page-D9twj8Xy.js → decisions-page-DgB76m-X.js} +1 -1
  18. package/src/console/ui-next/dist/assets/{duration-tone-CsHWlVPU.js → duration-tone-l1DSJ2Vz.js} +1 -1
  19. package/src/console/ui-next/dist/assets/{flows-page-ZaTJAhR9.js → flows-page-Bb9t1lu6.js} +1 -1
  20. package/src/console/ui-next/dist/assets/{highlighted-json-DQ-EMAfz.js → highlighted-json-DMvsi3Ti.js} +1 -1
  21. package/src/console/ui-next/dist/assets/{http-method-Cn_Fl79s.js → http-method-BwhJBTcQ.js} +1 -1
  22. package/src/console/ui-next/dist/assets/{index-CmRFeWdT.js → index-RhV2jT_7.js} +3 -3
  23. package/src/console/ui-next/dist/assets/{observability-page-Cn6R_dXz.js → observability-page-BRfUILBq.js} +1 -1
  24. package/src/console/ui-next/dist/assets/{replica-lag-Bx6EZhSe.js → replica-lag-BsceAzIG.js} +1 -1
  25. package/src/console/ui-next/dist/assets/{request-meta-DmdiUrfh.js → request-meta-C6lTyCXp.js} +1 -1
  26. package/src/console/ui-next/dist/assets/{store-page-8P6vGXnn.js → store-page-YmE806bJ.js} +1 -1
  27. package/src/console/ui-next/dist/assets/{trace-detail-sheet-BfGvpFSv.js → trace-detail-sheet-Di4irS49.js} +1 -1
  28. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CWdUHbc0.js → tree-expand-toggle-DYeL6Rye.js} +1 -1
  29. package/src/console/ui-next/dist/assets/{units-page-CZOFZzay.js → units-page-07agasRA.js} +1 -1
  30. package/src/console/ui-next/dist/assets/{vault-page-ki_ishmX.js → vault-page-CHBck4n_.js} +1 -1
  31. package/src/console/ui-next/dist/index.html +1 -1
  32. package/src/console/ui-next/seed-parked-approval.ts +5 -1
  33. package/src/drivers/journal-postgres.test.ts +84 -1
  34. package/src/drivers/journal-postgres.ts +282 -32
  35. package/src/drivers/postgres.test.ts +60 -0
  36. package/src/drivers/postgres.ts +79 -5
  37. package/src/drivers/signal-compete.test.ts +6 -2
  38. package/src/drivers/signal-postgres.test.ts +170 -0
  39. package/src/drivers/signal-postgres.ts +449 -196
  40. package/src/drivers/signal-redis.test.ts +224 -0
  41. package/src/drivers/signal-redis.ts +835 -150
  42. package/src/elements/ai/approval.test.ts +15 -3
  43. package/src/elements/ai/approval.ts +15 -9
  44. package/src/elements/channel/runtime.ts +31 -4
  45. package/src/elements/channel/sql-ledger.test.ts +404 -0
  46. package/src/elements/channel/sql-ledger.ts +177 -33
  47. package/src/elements/clock/chaos-child.ts +12 -2
  48. package/src/elements/clock/durable.ts +11 -0
  49. package/src/elements/store/cache-bus.test.ts +121 -0
  50. package/src/elements/store/cache-bus.ts +278 -0
  51. package/src/elements/store/cache.test.ts +59 -0
  52. package/src/elements/store/cache.ts +157 -12
  53. package/src/elements/store/runtime.ts +44 -2
  54. package/src/elements/store/sql-errors.test.ts +131 -0
  55. package/src/elements/store/sql-errors.ts +39 -16
  56. package/src/elements/store/sql-nested-tx.test.ts +344 -0
  57. package/src/elements/store/sql-session.ts +183 -3
  58. package/src/kernel/app.ts +176 -28
  59. package/src/kernel/auto-registry.test.ts +5 -1
  60. package/src/kernel/boot-bind/signal.ts +13 -2
  61. package/src/kernel/boot-bind/store.ts +16 -1
  62. package/src/kernel/boot.test.ts +2 -2
  63. package/src/kernel/boot.ts +41 -11
  64. package/src/kernel/boundary-contract.ts +7 -0
  65. package/src/kernel/builtin-errors.ts +2 -0
  66. package/src/kernel/call.test.ts +1 -0
  67. package/src/kernel/fx-sql-handle.ts +7 -1
  68. package/src/kernel/horizontal-child.ts +54 -1
  69. package/src/kernel/horizontal.integration.test.ts +131 -7
  70. package/src/kernel/idempotency.test.ts +5 -6
  71. package/src/kernel/journal-boot.test.ts +83 -6
  72. package/src/kernel/journal.test.ts +44 -0
  73. package/src/kernel/journal.ts +171 -26
  74. package/src/mcp/authorization.ts +5 -11
  75. package/src/mcp/confirmation.ts +203 -38
  76. package/src/mcp/docs-server.ts +7 -2
  77. package/src/mcp/index.ts +4 -0
  78. package/src/mcp/mcp.test.ts +321 -33
  79. package/src/mcp/server.ts +8 -3
  80. package/src/mcp/session.ts +7 -0
  81. package/src/mcp/tools.ts +68 -19
  82. package/src/runtime/bun.ts +5 -0
  83. package/src/runtime/types.ts +5 -0
@@ -8,6 +8,7 @@ import { describe, expect, test } from "bun:test";
8
8
  import { runDurable } from "../elements/clock/durable.ts";
9
9
  import { flow } from "./flow.ts";
10
10
  import {
11
+ clearJournalHeartbeats,
11
12
  createJournal,
12
13
  createMemoryJournalStore,
13
14
  hasJournalLease,
@@ -171,6 +172,34 @@ describe("journal lease (memory store)", () => {
171
172
  }
172
173
  });
173
174
 
175
+ test("heartbeat extends the lease, and a lost lease does not persist the step", async () => {
176
+ let t = 1_000;
177
+ const store = createMemoryJournalStore();
178
+ const a = createJournal({
179
+ store,
180
+ now: () => t,
181
+ lease: { instanceId: "a", leaseMs: 60 },
182
+ codeVersion: "v1",
183
+ });
184
+ const session = await a.start("charge");
185
+ t = 1_030;
186
+ await Bun.sleep(40);
187
+ expect((await store.get(session.runId))?.leaseExpiresAt).toBe(1_090);
188
+ t = 2_000;
189
+ const b = createJournal({
190
+ store,
191
+ now: () => t,
192
+ lease: { instanceId: "b", leaseMs: 60 },
193
+ codeVersion: "v1",
194
+ });
195
+ await b.resume(session.runId);
196
+ await Bun.sleep(40);
197
+ await expect(session.step("capture", () => 1)).rejects.toBeInstanceOf(OkeError);
198
+ expect((await store.get(session.runId))?.entries ?? []).toHaveLength(0);
199
+ expect((await store.get(session.runId))?.lockedBy).toBe("b");
200
+ clearJournalHeartbeats();
201
+ });
202
+
174
203
  test("replay throws when the next call does not match the journal", async () => {
175
204
  const store = createMemoryJournalStore();
176
205
  const journal = createJournal({ store, now: () => 1, codeVersion: "v1" });
@@ -198,6 +227,21 @@ describe("journal lease (memory store)", () => {
198
227
  }
199
228
  });
200
229
 
230
+ test("a 0.23.1 stamp resumes and is restamped when codeVersion is set", async () => {
231
+ const store = createMemoryJournalStore();
232
+ const started = await createJournal({ store, now: () => 1, codeVersion: "v1" }).start("charge");
233
+ const row = await store.get(started.runId);
234
+ if (row) {
235
+ row.codeVersion = "0.23.1";
236
+ await store.put(row);
237
+ }
238
+ const resumed = await createJournal({ store, now: () => 2, codeVersion: "v9" }).resume(
239
+ started.runId,
240
+ );
241
+ expect(resumed.run.codeVersion).toBe("app:v9");
242
+ expect((await store.get(started.runId))?.status).not.toBe("failed");
243
+ });
244
+
201
245
  test("resume rejects a run stamped with another code version", async () => {
202
246
  const store = createMemoryJournalStore();
203
247
  const started = await createJournal({ store, now: () => 1, codeVersion: "v1" }).start("charge");
@@ -7,11 +7,10 @@
7
7
  * (four-applications · Provisions).
8
8
  */
9
9
 
10
- import { readFileSync } from "node:fs";
11
10
  import { mkdir } from "node:fs/promises";
12
11
  import { dirname } from "node:path";
13
12
  import { okid } from "../okid.ts";
14
- import { throwOke } from "./errors.ts";
13
+ import { OkeError, throwOke } from "./errors.ts";
15
14
  import type { DecisionLabelStore } from "./decision-label-store.ts";
16
15
  import type { IdempotencyStore } from "./idempotency-store.ts";
17
16
  import { JournalSuspend } from "./journal-suspend.ts";
@@ -145,6 +144,15 @@ export interface JournalLeaseStore {
145
144
  * @param leaseMs - Lease duration
146
145
  */
147
146
  acquireLease(runId: string, instanceId: string, now: number, leaseMs: number): Promise<boolean>;
147
+ /**
148
+ * Extend `lease_expires_at` without changing the fencing token.
149
+ * Returns false when this holder no longer owns the run.
150
+ *
151
+ * @param runId - Run id
152
+ * @param expiresAt - New expiry, epoch-ms
153
+ * @param fence - Caller's lease fence
154
+ */
155
+ renewLeaseExpiry?(runId: string, expiresAt: number, fence: JournalWriteFence): Promise<boolean>;
148
156
  /**
149
157
  * Take the lease and write `update(run)` in that same hold.
150
158
  * `undefined` from `update` keeps the previous entries and still holds the lease.
@@ -259,6 +267,22 @@ export interface JournalStore extends Partial<JournalLeaseStore> {
259
267
  entry: JournalEntry,
260
268
  fence?: JournalWriteFence,
261
269
  ): Promise<void>;
270
+ /**
271
+ * Replace one existing entry under the caller's fence. Built-in stores
272
+ * implement this so approval and decide can correct a step without
273
+ * rewriting the run. Absent on a custom store — callers keep `put`.
274
+ *
275
+ * @param runId - Run id
276
+ * @param seq - Zero-based position
277
+ * @param entry - Replacement entry
278
+ * @param fence - Lease fence
279
+ */
280
+ updateEntry?(
281
+ runId: string,
282
+ seq: number,
283
+ entry: JournalEntry,
284
+ fence: JournalWriteFence,
285
+ ): Promise<void>;
262
286
  /** List all runs (test / console helper). */
263
287
  list(): Promise<readonly JournalRun[]>;
264
288
  }
@@ -296,6 +320,27 @@ export function isJournalLeaseBusy(err: unknown): err is JournalLeaseBusy {
296
320
  return err instanceof JournalLeaseBusy;
297
321
  }
298
322
 
323
+ /**
324
+ * A fenced write lost the lease (OKE1074). The holder must not compensate
325
+ * or record the step — another instance may already own the run.
326
+ *
327
+ * @param err - Caught error
328
+ */
329
+ export function isLostJournalLease(err: unknown): boolean {
330
+ return err instanceof OkeError && err.code === 1074;
331
+ }
332
+
333
+ const heartbeatTimers = new Set<ReturnType<typeof setInterval>>();
334
+
335
+ /**
336
+ * Stop every session heartbeat. `stop` and `close` call this so a shut
337
+ * down process does not keep renewing leases it no longer owns.
338
+ */
339
+ export function clearJournalHeartbeats(): void {
340
+ for (const timer of heartbeatTimers) clearInterval(timer);
341
+ heartbeatTimers.clear();
342
+ }
343
+
299
344
  /** Live lease = a holder with an unexpired expiry. */
300
345
  function hasLiveLease(run: JournalRun, now: number): boolean {
301
346
  return run.lockedBy !== undefined && run.leaseExpiresAt !== undefined && run.leaseExpiresAt > now;
@@ -350,6 +395,20 @@ function leaseMethods(
350
395
  release();
351
396
  }
352
397
  },
398
+ async renewLeaseExpiry(runId, expiresAt, fence) {
399
+ const map = await load();
400
+ const run = map.get(runId);
401
+ if (!run) return false;
402
+ const expired = run.leaseExpiresAt !== undefined && run.leaseExpiresAt <= fence.now;
403
+ if (run.lockedBy !== fence.lockedBy || run.leaseToken !== fence.leaseToken || expired) {
404
+ return false;
405
+ }
406
+ if (run.leaseExpiresAt === undefined || run.leaseExpiresAt < expiresAt) {
407
+ run.leaseExpiresAt = expiresAt;
408
+ }
409
+ await flush?.(map);
410
+ return true;
411
+ },
353
412
  async releaseLease(runId, instanceId) {
354
413
  const map = await load();
355
414
  const run = map.get(runId);
@@ -410,6 +469,14 @@ export function createMemoryJournalStore(seed?: readonly JournalRun[]): JournalS
410
469
  assertJournalFence(existing, fence);
411
470
  appendStoredEntry(existing, seq, entry);
412
471
  },
472
+ async updateEntry(runId, seq, entry, fence) {
473
+ const existing = runs.get(runId);
474
+ if (!existing) throwOke("JOURNAL_STALE_LEASE", { runId });
475
+ assertJournalFence(existing, fence);
476
+ const entries = existing.entries as JournalEntry[];
477
+ if (!entries[seq]) throwOke("JOURNAL_STALE_LEASE", { runId });
478
+ entries[seq] = structuredClone(entry);
479
+ },
413
480
  async list() {
414
481
  return [...runs.values()].map(cloneRun);
415
482
  },
@@ -479,6 +546,16 @@ export function createFileJournalStore(path: string): JournalStore {
479
546
  appendStoredEntry(existing, seq, entry);
480
547
  await flush(map);
481
548
  },
549
+ async updateEntry(runId, seq, entry, fence) {
550
+ const map = await load();
551
+ const existing = map.get(runId);
552
+ if (!existing) throwOke("JOURNAL_STALE_LEASE", { runId });
553
+ assertJournalFence(existing, fence);
554
+ const entries = existing.entries as JournalEntry[];
555
+ if (!entries[seq]) throwOke("JOURNAL_STALE_LEASE", { runId });
556
+ entries[seq] = structuredClone(entry);
557
+ await flush(map);
558
+ },
482
559
  async list() {
483
560
  const map = await load();
484
561
  return [...map.values()].map(cloneRun);
@@ -674,7 +751,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
674
751
  const now = options.now ?? (() => Date.now());
675
752
  const newId = options.id ?? (() => okid());
676
753
  const lease = options.lease;
677
- const codeVersion = options.codeVersion ?? packageVersion();
754
+ const codeVersion = stampAppVersion(options.codeVersion);
678
755
  const coordinated = lease !== undefined && hasJournalLease(options.store);
679
756
 
680
757
  function openSession(run: JournalRun, leased: boolean): JournalSession {
@@ -697,6 +774,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
697
774
  }
698
775
 
699
776
  async function persist(entry?: JournalEntry): Promise<void> {
777
+ if (lostLease) throwOke("JOURNAL_STALE_LEASE", { runId: run.id });
700
778
  run.updatedAt = now();
701
779
  // Natural heartbeat — a live holder renews on every journal write.
702
780
  if (leaseHeld && lease) {
@@ -742,8 +820,52 @@ export function createJournal(options: CreateJournalOptions): Journal {
742
820
  }
743
821
 
744
822
  /** Parking / terminal states must not hold a short lease across days. */
823
+ let heartbeat: ReturnType<typeof setInterval> | undefined;
824
+ let lostLease = false;
825
+ let activeStep = "";
826
+
827
+ function stopHeartbeat(): void {
828
+ if (!heartbeat) return;
829
+ clearInterval(heartbeat);
830
+ heartbeatTimers.delete(heartbeat);
831
+ heartbeat = undefined;
832
+ }
833
+
834
+ function startHeartbeat(): void {
835
+ if (!leased || !lease || heartbeat) return;
836
+ const every = Math.max(1, Math.floor((lease.leaseMs ?? JOURNAL_DEFAULT_LEASE_MS) / 3));
837
+ heartbeat = setInterval(() => {
838
+ void (async () => {
839
+ if (!leaseHeld || lostLease || !options.store.renewLeaseExpiry) return;
840
+ const writeFence = fence();
841
+ if (!writeFence) return;
842
+ const expiresAt = now() + (lease.leaseMs ?? JOURNAL_DEFAULT_LEASE_MS);
843
+ const ok = await options.store.renewLeaseExpiry(run.id, expiresAt, writeFence);
844
+ if (!ok) {
845
+ lostLease = true;
846
+ console.warn(
847
+ JSON.stringify({
848
+ event: "journal.lease.lost",
849
+ runId: run.id,
850
+ step: activeStep,
851
+ instanceId: lease.instanceId,
852
+ }),
853
+ );
854
+ return;
855
+ }
856
+ if (run.leaseExpiresAt === undefined || run.leaseExpiresAt < expiresAt) {
857
+ run.leaseExpiresAt = expiresAt;
858
+ }
859
+ })();
860
+ }, every);
861
+ heartbeat.unref?.();
862
+ heartbeatTimers.add(heartbeat);
863
+ }
864
+
745
865
  function releaseLeaseLocally(): void {
746
866
  leaseHeld = false;
867
+ lostLease = false;
868
+ stopHeartbeat();
747
869
  delete run.lockedBy;
748
870
  delete run.leaseExpiresAt;
749
871
  }
@@ -772,6 +894,8 @@ export function createJournal(options: CreateJournalOptions): Journal {
772
894
  }
773
895
  }
774
896
 
897
+ if (leased) startHeartbeat();
898
+
775
899
  return {
776
900
  runId: run.id,
777
901
  run,
@@ -792,6 +916,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
792
916
  return value;
793
917
  }
794
918
  assertCanAppendStep(name);
919
+ activeStep = name;
795
920
  stepDepth += 1;
796
921
  let stored: unknown;
797
922
  try {
@@ -799,6 +924,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
799
924
  } finally {
800
925
  stepDepth -= 1;
801
926
  }
927
+ if (lostLease) throwOke("JOURNAL_STALE_LEASE", { runId: run.id });
802
928
  const value = reviveJournalValue(stored) as T;
803
929
  const entry: JournalStepEntry = {
804
930
  kind: "step",
@@ -808,7 +934,13 @@ export function createJournal(options: CreateJournalOptions): Journal {
808
934
  };
809
935
  run.entries.push(entry);
810
936
  cursor = run.entries.length;
811
- await persist(entry);
937
+ try {
938
+ await persist(entry);
939
+ } catch (err) {
940
+ run.entries.pop();
941
+ cursor = run.entries.length;
942
+ throw err;
943
+ }
812
944
  if (!name.startsWith(JOURNAL_UNDO_PREFIX)) {
813
945
  registerUndo(name, value, opts);
814
946
  }
@@ -998,15 +1130,30 @@ export function createJournal(options: CreateJournalOptions): Journal {
998
1130
  }
999
1131
  throw new Error(`journal: run "${runId}" not found`);
1000
1132
  }
1001
- if (run.codeVersion !== undefined && run.codeVersion !== codeVersion) {
1002
- if (coordinated && lease) {
1003
- await options.store.releaseLease!(runId, lease.instanceId);
1133
+ if (
1134
+ codeVersion !== undefined &&
1135
+ run.codeVersion !== undefined &&
1136
+ run.codeVersion !== codeVersion
1137
+ ) {
1138
+ if (run.codeVersion.startsWith("app:")) {
1139
+ run.status = "failed";
1140
+ run.error = "OKE1076";
1141
+ run.updatedAt = now();
1142
+ const skewFence =
1143
+ coordinated && lease && run.leaseToken !== undefined && run.lockedBy !== undefined
1144
+ ? { lockedBy: run.lockedBy, leaseToken: run.leaseToken, now: now() }
1145
+ : undefined;
1146
+ await options.store.put(cloneRun(run), skewFence);
1147
+ if (coordinated && lease) {
1148
+ await options.store.releaseLease!(runId, lease.instanceId);
1149
+ }
1150
+ throwOke("JOURNAL_CODE_VERSION", {
1151
+ runId,
1152
+ expected: run.codeVersion,
1153
+ actual: codeVersion,
1154
+ });
1004
1155
  }
1005
- throwOke("JOURNAL_CODE_VERSION", {
1006
- runId,
1007
- expected: run.codeVersion,
1008
- actual: codeVersion,
1009
- });
1156
+ run.codeVersion = codeVersion;
1010
1157
  }
1011
1158
  // Leave status intact — the durable runner parks or continues.
1012
1159
  run.updatedAt = now();
@@ -1014,7 +1161,11 @@ export function createJournal(options: CreateJournalOptions): Journal {
1014
1161
  run.lockedBy = lease.instanceId;
1015
1162
  run.leaseExpiresAt = now() + (lease.leaseMs ?? JOURNAL_DEFAULT_LEASE_MS);
1016
1163
  }
1017
- await options.store.put(cloneRun(run));
1164
+ const resumeFence =
1165
+ coordinated && lease && run.leaseToken !== undefined && run.lockedBy !== undefined
1166
+ ? { lockedBy: run.lockedBy, leaseToken: run.leaseToken, now: now() }
1167
+ : undefined;
1168
+ await options.store.put(cloneRun(run), resumeFence);
1018
1169
  return openSession(run, coordinated);
1019
1170
  },
1020
1171
  };
@@ -1159,19 +1310,13 @@ function entryLabel(entry: JournalEntry): string {
1159
1310
  return `effect ${entry.effectKind} ${entry.resource}`;
1160
1311
  }
1161
1312
 
1162
- let cachedPackageVersion: string | undefined;
1163
-
1164
1313
  /**
1165
- * Package version used when a journal is not given an explicit code version.
1314
+ * Namespace an opt-in code version so an okengine package version stored by
1315
+ * 0.23.1 (`"0.23.1"`) is not compared with an app stamp (`"app:1"`).
1316
+ *
1317
+ * @param version - Caller version, or unset to skip the check
1166
1318
  */
1167
- function packageVersion(): string {
1168
- if (cachedPackageVersion !== undefined) return cachedPackageVersion;
1169
- try {
1170
- const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
1171
- const parsed = JSON.parse(raw) as { version?: string };
1172
- cachedPackageVersion = parsed.version && parsed.version.length > 0 ? parsed.version : "0";
1173
- } catch {
1174
- cachedPackageVersion = "0";
1175
- }
1176
- return cachedPackageVersion;
1319
+ function stampAppVersion(version: string | undefined): string | undefined {
1320
+ if (version === undefined || version.length === 0) return undefined;
1321
+ return version.startsWith("app:") ? version : `app:${version}`;
1177
1322
  }
@@ -225,25 +225,19 @@ export const MCP_TOOL_POLICIES: readonly McpToolPolicy[] = [
225
225
  name: "oke.action.confirm",
226
226
  scopes: ["mcp:action:invoke", "console:flows:invoke"],
227
227
  mutability: "read",
228
- description: "Request a single-use confirmation token for a write tool (no caching).",
228
+ description:
229
+ "Issue a single-use confirmation token for a pending write. The caller must be a different auth session than the requester (no caching).",
229
230
  inputSchema: {
230
231
  type: "object",
231
232
  properties: {
232
- tool: {
233
- type: "string",
234
- enum: ["oke.action.invoke", "oke.action.structural_propose"],
235
- },
236
- args: { type: "object" },
233
+ confirmationId: { type: "string", minLength: 1 },
237
234
  reason: { type: "string", minLength: 3 },
238
235
  },
239
- required: ["tool", "args", "reason"],
236
+ required: ["confirmationId", "reason"],
240
237
  additionalProperties: false,
241
238
  },
242
239
  params: [
243
- {
244
- name: "tool",
245
- enum: ["oke.action.invoke", "oke.action.structural_propose"],
246
- },
240
+ { name: "confirmationId", maxLength: 200 },
247
241
  { name: "reason", maxLength: 2000 },
248
242
  ],
249
243
  },