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
@@ -5,27 +5,82 @@
5
5
  * re-validating is how tool poisoning and rug pulls persist. Every write
6
6
  * (or otherwise sensitive) tool invocation must carry a fresh confirmation
7
7
  * token that was issued for that exact tool + arguments digest.
8
+ *
9
+ * The requester and the issuer are different auth sessions (`claims.sid`),
10
+ * even when they are the same principal. A write without a token returns an
11
+ * opaque confirmation id bound to the requester. `oke.action.confirm` mints
12
+ * the token only from another session. Only the requester session can
13
+ * consume it, and the issuer session cannot.
14
+ *
15
+ * Pending rows live in this process only. A token issued on another process
16
+ * is absent here and fails closed with `"unknown"`. Do not add shared storage.
8
17
  */
9
18
 
10
19
  /** Typed confirmation phrase for irreversible MCP writes. */
11
20
  export const MCP_CONFIRM_PHRASE = "CONFIRM" as const;
12
21
 
13
- /** Pending confirmation that has not yet been consumed. */
22
+ /**
23
+ * Auth session a confirmation is bound to.
24
+ *
25
+ * `sid` is `McpRequester.sessionId` (`claims.sid`), not the MCP transport
26
+ * session id from `initialize`.
27
+ */
28
+ export interface ConfirmationTarget {
29
+ /** Operator principal id (`claims.sub`). */
30
+ readonly principalId: string;
31
+ /** Auth session id (`claims.sid`). */
32
+ readonly sid: string;
33
+ }
34
+
35
+ /**
36
+ * Confirmation after a different session has issued its token.
37
+ *
38
+ * The write rejection that opens a confirmation never carries `token`.
39
+ */
14
40
  export interface PendingConfirmation {
15
- /** Cryptographically random token. */
41
+ /** Opaque id returned to the requester. Not a token. */
42
+ readonly confirmationId: string;
43
+ /** Cryptographically random token. Present only after issue. */
16
44
  readonly token: string;
17
45
  /** Tool name the token authorises. */
18
46
  readonly tool: string;
19
47
  /** SHA-256 of canonical JSON arguments. */
20
48
  readonly argsDigest: string;
21
- /** Operator / principal id that requested it. */
22
- readonly principalId: string;
49
+ /** Requester the write is bound to. */
50
+ readonly target: ConfirmationTarget;
51
+ /** Auth session that issued the token. Distinct from {@link ConfirmationTarget.sid}. */
52
+ readonly issuerSid: string;
23
53
  /** Expiry epoch-ms. */
24
54
  readonly expiresAt: number;
25
- /** Human reason recorded at request time. */
55
+ /** Human reason recorded when the token was issued. */
26
56
  readonly reason: string;
27
57
  }
28
58
 
59
+ /**
60
+ * Opaque handle for a write that still needs another session to confirm.
61
+ *
62
+ * This is not a token and must not be accepted by {@link ConfirmationGate.consume}.
63
+ */
64
+ export interface OpenConfirmation {
65
+ /** Id to pass to `oke.action.confirm`. */
66
+ readonly confirmationId: string;
67
+ /** Expiry epoch-ms of the pending row. */
68
+ readonly expiresAt: number;
69
+ }
70
+
71
+ /** Result of issuing a confirmation token from a second session. */
72
+ export type ConfirmIssueResult =
73
+ | {
74
+ readonly ok: true;
75
+ readonly token: string;
76
+ readonly tool: string;
77
+ readonly expiresAt: number;
78
+ }
79
+ | {
80
+ readonly ok: false;
81
+ readonly reason: "unknown" | "expired" | "same-session" | "reason-short" | "already-issued";
82
+ };
83
+
29
84
  /** Result of consuming a confirmation. */
30
85
  export type ConfirmConsumeResult =
31
86
  | { readonly ok: true; readonly pending: PendingConfirmation }
@@ -37,7 +92,8 @@ export type ConfirmConsumeResult =
37
92
  | "expired"
38
93
  | "tool-mismatch"
39
94
  | "args-mismatch"
40
- | "principal-mismatch"
95
+ | "session-mismatch"
96
+ | "issuer-mismatch"
41
97
  | "phrase-mismatch"
42
98
  | "reason-short";
43
99
  };
@@ -50,51 +106,128 @@ export interface ConfirmationGateOptions {
50
106
  }
51
107
 
52
108
  /**
53
- * Create a confirmation gate with **no session-level cache**.
54
- * Tokens are single-use and bound to tool + args + principal.
109
+ * In-process confirmation gate with **no session-level cache**.
110
+ *
111
+ * Tokens are single-use and bound to tool + args digest + target session.
112
+ * The map is per process: an id or token issued elsewhere fails closed
113
+ * with `"unknown"`. Do not add shared storage.
55
114
  */
56
- export function createConfirmationGate(options: ConfirmationGateOptions = {}): {
57
- readonly request: (input: {
115
+ export interface ConfirmationGate {
116
+ /**
117
+ * Bind a write attempt to the requester.
118
+ *
119
+ * Returns an opaque confirmation id and never a token.
120
+ *
121
+ * @param input - Tool, arguments, and requester session
122
+ */
123
+ readonly open: (input: {
58
124
  readonly tool: string;
59
125
  readonly args: unknown;
60
- readonly principalId: string;
126
+ readonly target: ConfirmationTarget;
127
+ }) => OpenConfirmation;
128
+ /**
129
+ * Issue a single-use token for an open confirmation.
130
+ *
131
+ * Succeeds only when `callerSid` is not the target session.
132
+ *
133
+ * @param input - Confirmation id, reason, and issuer session
134
+ */
135
+ readonly issue: (input: {
136
+ readonly confirmationId: string;
61
137
  readonly reason: string;
62
- }) => PendingConfirmation | { readonly error: "reason-short" };
138
+ readonly callerSid: string;
139
+ }) => ConfirmIssueResult;
140
+ /**
141
+ * Consume a token for one write.
142
+ *
143
+ * Requires `consumerSid === target.sid` and `issuerSid !== consumerSid`,
144
+ * plus single-use, tool, args digest, expiry, and phrase checks.
145
+ *
146
+ * @param input - Write attempt presenting the token
147
+ */
63
148
  readonly consume: (input: {
64
149
  readonly tool: string;
65
150
  readonly args: unknown;
66
- readonly principalId: string;
151
+ readonly consumerSid: string;
67
152
  readonly token: string;
68
153
  readonly phrase: string;
69
154
  readonly reason: string;
70
155
  }) => ConfirmConsumeResult;
71
156
  /** Test helper — pending count (never used for consent caching). */
72
157
  readonly size: () => number;
73
- } {
158
+ }
159
+
160
+ /** Mutable row stored in the per-process maps. */
161
+ interface ConfirmationRecord {
162
+ confirmationId: string;
163
+ token: string | null;
164
+ tool: string;
165
+ argsDigest: string;
166
+ target: ConfirmationTarget;
167
+ issuerSid: string | null;
168
+ expiresAt: number;
169
+ reason: string;
170
+ }
171
+
172
+ /**
173
+ * Create a confirmation gate with **no session-level cache**.
174
+ *
175
+ * @param options - Clock and TTL
176
+ */
177
+ export function createConfirmationGate(options: ConfirmationGateOptions = {}): ConfirmationGate {
74
178
  const now = options.now ?? (() => Date.now());
75
179
  const ttlMs = options.ttlMs ?? 2 * 60 * 1000;
76
- const pending = new Map<string, PendingConfirmation>();
180
+ // Per-process only. A token issued on another process is not in these maps
181
+ // and fails closed with "unknown". Do not add shared storage.
182
+ const byId = new Map<string, ConfirmationRecord>();
183
+ const byToken = new Map<string, ConfirmationRecord>();
77
184
 
78
185
  return {
79
- request(input) {
80
- if (input.reason.trim().length < 3) {
81
- return { error: "reason-short" as const };
82
- }
83
- prune(pending, now());
84
- const token = `mcp_c_${cryptoRandomHex(24)}`;
85
- const entry: PendingConfirmation = {
86
- token,
186
+ open(input) {
187
+ prune(byId, byToken, now());
188
+ const confirmationId = `mcp_id_${cryptoRandomHex(24)}`;
189
+ const entry: ConfirmationRecord = {
190
+ confirmationId,
191
+ token: null,
87
192
  tool: input.tool,
88
193
  argsDigest: digestArgs(input.args),
89
- principalId: input.principalId,
194
+ target: input.target,
195
+ issuerSid: null,
90
196
  expiresAt: now() + ttlMs,
91
- reason: input.reason.trim(),
197
+ reason: "",
92
198
  };
93
- pending.set(token, entry);
94
- return entry;
199
+ byId.set(confirmationId, entry);
200
+ return { confirmationId, expiresAt: entry.expiresAt };
201
+ },
202
+ issue(input) {
203
+ if (input.reason.trim().length < 3) {
204
+ return { ok: false, reason: "reason-short" };
205
+ }
206
+ prune(byId, byToken, now());
207
+ const entry = byId.get(input.confirmationId);
208
+ if (!entry) {
209
+ return { ok: false, reason: "unknown" };
210
+ }
211
+ if (entry.expiresAt <= now()) {
212
+ drop(byId, byToken, entry);
213
+ return { ok: false, reason: "expired" };
214
+ }
215
+ // Same auth session cannot request, confirm, and invoke.
216
+ if (input.callerSid === entry.target.sid) {
217
+ return { ok: false, reason: "same-session" };
218
+ }
219
+ if (entry.token !== null) {
220
+ return { ok: false, reason: "already-issued" };
221
+ }
222
+ const token = `mcp_c_${cryptoRandomHex(24)}`;
223
+ entry.token = token;
224
+ entry.issuerSid = input.callerSid;
225
+ entry.reason = input.reason.trim();
226
+ byToken.set(token, entry);
227
+ return { ok: true, token, tool: entry.tool, expiresAt: entry.expiresAt };
95
228
  },
96
229
  consume(input) {
97
- prune(pending, now());
230
+ prune(byId, byToken, now());
98
231
  if (!input.token) {
99
232
  return { ok: false, reason: "missing" };
100
233
  }
@@ -104,12 +237,13 @@ export function createConfirmationGate(options: ConfirmationGateOptions = {}): {
104
237
  if (input.reason.trim().length < 3) {
105
238
  return { ok: false, reason: "reason-short" };
106
239
  }
107
- const entry = pending.get(input.token);
240
+ const entry = byToken.get(input.token);
108
241
  if (!entry) {
242
+ // Missing locally — including a token issued on another process.
109
243
  return { ok: false, reason: "unknown" };
110
244
  }
111
245
  // Single-use: remove before further checks so replay fails.
112
- pending.delete(input.token);
246
+ drop(byId, byToken, entry);
113
247
  if (entry.expiresAt <= now()) {
114
248
  return { ok: false, reason: "expired" };
115
249
  }
@@ -119,14 +253,32 @@ export function createConfirmationGate(options: ConfirmationGateOptions = {}): {
119
253
  if (entry.argsDigest !== digestArgs(input.args)) {
120
254
  return { ok: false, reason: "args-mismatch" };
121
255
  }
122
- if (entry.principalId !== input.principalId) {
123
- return { ok: false, reason: "principal-mismatch" };
256
+ if (entry.target.sid !== input.consumerSid) {
257
+ return { ok: false, reason: "session-mismatch" };
124
258
  }
125
- return { ok: true, pending: entry };
259
+ if (entry.issuerSid === null || entry.issuerSid === input.consumerSid) {
260
+ return { ok: false, reason: "issuer-mismatch" };
261
+ }
262
+ if (entry.token === null) {
263
+ return { ok: false, reason: "unknown" };
264
+ }
265
+ return {
266
+ ok: true,
267
+ pending: {
268
+ confirmationId: entry.confirmationId,
269
+ token: entry.token,
270
+ tool: entry.tool,
271
+ argsDigest: entry.argsDigest,
272
+ target: entry.target,
273
+ issuerSid: entry.issuerSid,
274
+ expiresAt: entry.expiresAt,
275
+ reason: entry.reason,
276
+ },
277
+ };
126
278
  },
127
279
  size() {
128
- prune(pending, now());
129
- return pending.size;
280
+ prune(byId, byToken, now());
281
+ return byId.size;
130
282
  },
131
283
  };
132
284
  }
@@ -142,6 +294,15 @@ export function digestArgs(args: unknown): string {
142
294
  return hasher.digest("hex");
143
295
  }
144
296
 
297
+ function drop(
298
+ byId: Map<string, ConfirmationRecord>,
299
+ byToken: Map<string, ConfirmationRecord>,
300
+ entry: ConfirmationRecord,
301
+ ): void {
302
+ byId.delete(entry.confirmationId);
303
+ if (entry.token !== null) byToken.delete(entry.token);
304
+ }
305
+
145
306
  function cryptoRandomHex(byteLength: number): string {
146
307
  const bytes = new Uint8Array(byteLength);
147
308
  crypto.getRandomValues(bytes);
@@ -160,8 +321,12 @@ function canonicalJson(value: unknown): string {
160
321
  return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
161
322
  }
162
323
 
163
- function prune(pending: Map<string, PendingConfirmation>, t: number): void {
164
- for (const [token, entry] of pending) {
165
- if (entry.expiresAt <= t) pending.delete(token);
324
+ function prune(
325
+ byId: Map<string, ConfirmationRecord>,
326
+ byToken: Map<string, ConfirmationRecord>,
327
+ t: number,
328
+ ): void {
329
+ for (const entry of byId.values()) {
330
+ if (entry.expiresAt <= t) drop(byId, byToken, entry);
166
331
  }
167
332
  }
@@ -11,6 +11,7 @@ import {
11
11
  forbiddenResponse,
12
12
  resolveAllowedHosts,
13
13
  } from "../runtime/security.ts";
14
+ import { HttpBodyRejected, parseBody } from "../compiler/http-parse.ts";
14
15
  import { DOCS_MCP_PORT, type ServerHandle } from "../runtime/types.ts";
15
16
  import { asData } from "./data.ts";
16
17
  import { defaultDocsContentDir, loadDocsIndex, type DocsIndex } from "./docs-index.ts";
@@ -81,8 +82,11 @@ export async function createDocsMcpServer(
81
82
 
82
83
  let body: unknown;
83
84
  try {
84
- body = await request.json();
85
- } catch {
85
+ body = await parseBody(request);
86
+ } catch (err) {
87
+ if (err instanceof HttpBodyRejected && err.code === "PayloadTooLarge") {
88
+ return new Response("Payload Too Large", { status: 413 });
89
+ }
86
90
  return jsonRpcHttp(rpcError(null, RpcErrorCode.parse, "invalid JSON body"), 400);
87
91
  }
88
92
 
@@ -202,6 +206,7 @@ export async function serveDocsMcp(
202
206
  const server = Bun.serve({
203
207
  hostname,
204
208
  port,
209
+ maxRequestBodySize: 1_048_576,
205
210
  fetch: mcp.fetch,
206
211
  });
207
212
  const boundPort = server.port ?? port;
package/src/mcp/index.ts CHANGED
@@ -32,8 +32,12 @@ export {
32
32
  createConfirmationGate,
33
33
  digestArgs,
34
34
  MCP_CONFIRM_PHRASE,
35
+ type ConfirmationGate,
36
+ type ConfirmationTarget,
35
37
  type ConfirmConsumeResult,
38
+ type ConfirmIssueResult,
36
39
  type ConfirmationGateOptions,
40
+ type OpenConfirmation,
37
41
  type PendingConfirmation,
38
42
  } from "./confirmation.ts";
39
43