okengine 0.22.0 → 0.23.0

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 (262) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +13 -9
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/gate/auth.mdx +4 -1
  13. package/site/content/docs/elements/store/index.mdx +1 -1
  14. package/site/content/docs/elements/store/kv.mdx +5 -5
  15. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  16. package/site/content/docs/recipes/index.mdx +6 -6
  17. package/site/content/docs/recipes/postgres.mdx +12 -8
  18. package/site/content/docs/recipes/redis.mdx +4 -4
  19. package/site/content/docs/reference/cli.mdx +2 -1
  20. package/site/content/docs/reference/configuration.mdx +9 -7
  21. package/site/content/docs/reference/fx.mdx +7 -6
  22. package/src/auth/api-key-sql.test.ts +44 -0
  23. package/src/auth/api-key-sql.ts +1 -0
  24. package/src/auth/api-keys.ts +30 -12
  25. package/src/auth/config.ts +6 -2
  26. package/src/auth/gate-auth.test.ts +20 -0
  27. package/src/cli/agents-md.test.ts +25 -0
  28. package/src/cli/ask-vault-gaps.test.ts +38 -1
  29. package/src/cli/ask-vault-gaps.ts +35 -2
  30. package/src/cli/decide.ts +174 -0
  31. package/src/cli/decision-lock-watch.test.ts +45 -0
  32. package/src/cli/decision-lock-watch.ts +51 -0
  33. package/src/cli/dev-app-runner.ts +34 -8
  34. package/src/cli/dev-auth-secret.test.ts +49 -0
  35. package/src/cli/dev-auth-secret.ts +49 -0
  36. package/src/cli/dev-hot.test.ts +41 -0
  37. package/src/cli/dev-hot.ts +48 -0
  38. package/src/cli/dev.test.ts +1 -1
  39. package/src/cli/dev.ts +24 -6
  40. package/src/cli/eval.ts +142 -1
  41. package/src/cli/index.ts +5 -0
  42. package/src/cli/load-config.images.test.ts +2 -2
  43. package/src/cli/load-config.ts +1 -1
  44. package/src/cli/mcp-from-console.ts +48 -0
  45. package/src/cli/registry.ts +35 -1
  46. package/src/client/agent.ts +307 -0
  47. package/src/client/transport.test.ts +20 -0
  48. package/src/client/transport.ts +2 -1
  49. package/src/client/types.ts +1 -1
  50. package/src/client-react/index.ts +7 -1
  51. package/src/client-react/use-agent-run.test.ts +271 -0
  52. package/src/client-react/use-agent-run.ts +193 -0
  53. package/src/compiler/ai-approval.test.ts +36 -0
  54. package/src/compiler/ai-repair.test.ts +22 -0
  55. package/src/compiler/decisions.extract.test.ts +164 -0
  56. package/src/compiler/effects-infer.ts +27 -1
  57. package/src/compiler/extract.ts +268 -12
  58. package/src/compiler/response.ts +35 -2
  59. package/src/config/index.ts +21 -3
  60. package/src/console/server/ai-runs-flows.ts +269 -0
  61. package/src/console/server/ai-runs.test.ts +323 -0
  62. package/src/console/server/ai-runs.ts +562 -0
  63. package/src/console/server/ai.ts +12 -1
  64. package/src/console/server/app.ts +10 -1
  65. package/src/console/server/decisions-flows.ts +157 -0
  66. package/src/console/server/decisions.test.ts +66 -0
  67. package/src/console/server/decisions.ts +229 -0
  68. package/src/console/server/flows.ts +50 -2
  69. package/src/console/server/invoke-user-flow.ts +9 -0
  70. package/src/console/server/runs-ingest.ts +16 -0
  71. package/src/console/server/serve.ts +5 -0
  72. package/src/console/server/state.ts +25 -1
  73. package/src/console/server/store-stats.test.ts +47 -0
  74. package/src/console/server/store-stats.ts +92 -18
  75. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  76. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
  80. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  81. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  82. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  83. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  84. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  85. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  86. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  87. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  88. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  89. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  90. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
  92. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  93. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  94. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  95. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
  96. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  97. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  98. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  99. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  100. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  101. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  102. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  103. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  104. package/src/console/ui-next/dist/index.html +4 -3
  105. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  106. package/src/console/ui-next/src/client.ts +224 -1
  107. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  108. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  109. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  110. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  111. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  112. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  113. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  114. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  115. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  116. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  117. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  118. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  119. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  120. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  121. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  122. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  123. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  124. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  125. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  126. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  127. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  128. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  129. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  130. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  131. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  132. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  133. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  134. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  135. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  137. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  138. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  139. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  140. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  141. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  142. package/src/console/ui-next/src/router.tsx +17 -0
  143. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  144. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  145. package/src/console/ui-next/vite.config.ts +9 -0
  146. package/src/docker/recipes/dragonfly.ts +3 -2
  147. package/src/docker/recipes/redis.ts +1 -1
  148. package/src/drivers/ai-anthropic.ts +212 -21
  149. package/src/drivers/ai-mock.ts +34 -1
  150. package/src/drivers/ai-openai-compatible.ts +67 -26
  151. package/src/drivers/ai-preconnect.ts +18 -0
  152. package/src/drivers/ai-stream.test.ts +62 -0
  153. package/src/drivers/ai-types.ts +20 -0
  154. package/src/drivers/journal-postgres.ts +289 -2
  155. package/src/drivers/memory-ddl.test.ts +18 -0
  156. package/src/drivers/memory.ts +21 -1
  157. package/src/drivers/pg-rls.test.ts +31 -0
  158. package/src/drivers/pg-rls.ts +15 -0
  159. package/src/drivers/postgres.test.ts +9 -0
  160. package/src/drivers/postgres.ts +49 -11
  161. package/src/elements/ai/agent-event-slot.ts +27 -0
  162. package/src/elements/ai/approval-http.ts +207 -0
  163. package/src/elements/ai/approval.test.ts +741 -0
  164. package/src/elements/ai/approval.ts +228 -0
  165. package/src/elements/ai/decisions/bind.ts +163 -0
  166. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  167. package/src/elements/ai/decisions/certificate.ts +377 -0
  168. package/src/elements/ai/decisions/certify.ts +314 -0
  169. package/src/elements/ai/decisions/decide.test.ts +953 -0
  170. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  171. package/src/elements/ai/decisions/export.test.ts +120 -0
  172. package/src/elements/ai/decisions/export.ts +146 -0
  173. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  174. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  175. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  176. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  177. package/src/elements/ai/decisions/http.test.ts +154 -0
  178. package/src/elements/ai/decisions/http.ts +240 -0
  179. package/src/elements/ai/decisions/labels.test.ts +282 -0
  180. package/src/elements/ai/decisions/labels.ts +257 -0
  181. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  182. package/src/elements/ai/decisions/openrouter.ts +36 -0
  183. package/src/elements/ai/decisions/provider.ts +102 -0
  184. package/src/elements/ai/decisions/typesafe.ts +36 -0
  185. package/src/elements/ai/declare.ts +252 -1
  186. package/src/elements/ai/events.test.ts +379 -0
  187. package/src/elements/ai/events.ts +138 -0
  188. package/src/elements/ai/run-events.test.ts +461 -0
  189. package/src/elements/ai/run-events.ts +522 -0
  190. package/src/elements/ai/runtime.ts +1186 -105
  191. package/src/elements/ai/stream-turn.ts +214 -0
  192. package/src/elements/ai/stream.live.test.ts +76 -0
  193. package/src/elements/ai/subagent.test.ts +136 -0
  194. package/src/elements/ai.test.ts +497 -3
  195. package/src/elements/ai.ts +16 -1
  196. package/src/elements/clock/durable.ts +27 -1
  197. package/src/elements/store/sql-session.test.ts +51 -0
  198. package/src/elements/store/sql-session.ts +111 -1
  199. package/src/i18n/catalogs/ar.ts +5 -0
  200. package/src/i18n/catalogs/en.ts +5 -0
  201. package/src/index.ts +2 -0
  202. package/src/kernel/agent-event-store.ts +448 -0
  203. package/src/kernel/app.ts +103 -1
  204. package/src/kernel/boot-bind/ai.ts +3 -0
  205. package/src/kernel/boot-bind/store.test.ts +26 -0
  206. package/src/kernel/boot-bind/store.ts +82 -6
  207. package/src/kernel/boot.ts +15 -4
  208. package/src/kernel/builtin-errors.ts +2 -0
  209. package/src/kernel/capability.ts +3 -0
  210. package/src/kernel/decision-label-store.ts +441 -0
  211. package/src/kernel/effects.test.ts +2 -1
  212. package/src/kernel/effects.ts +13 -1
  213. package/src/kernel/element-registries.ts +3 -0
  214. package/src/kernel/errors-text.ts +4 -0
  215. package/src/kernel/errors.ts +2 -0
  216. package/src/kernel/flow.test.ts +2 -3
  217. package/src/kernel/fx-decide.ts +761 -0
  218. package/src/kernel/fx.test.ts +12 -5
  219. package/src/kernel/fx.ts +312 -29
  220. package/src/kernel/http-frame.test.ts +94 -0
  221. package/src/kernel/http-frame.ts +203 -0
  222. package/src/kernel/journal.ts +104 -0
  223. package/src/kernel/json-result.ts +10 -0
  224. package/src/kernel/pipeline.test.ts +45 -0
  225. package/src/kernel/sse-id.ts +32 -0
  226. package/src/manifest/diff.ts +1 -0
  227. package/src/manifest/types.ts +29 -2
  228. package/src/mcp/ai-tools.test.ts +202 -0
  229. package/src/mcp/authorization.ts +66 -0
  230. package/src/mcp/session.ts +3 -0
  231. package/src/mcp/tools.ts +168 -0
  232. package/src/release/http-graph.test.ts +2 -0
  233. package/src/release/http-graph.ts +5 -0
  234. package/src/release/measure.ts +39 -14
  235. package/src/runs/collect.ts +4 -1
  236. package/src/runs/parquet.test.ts +27 -1
  237. package/src/runs/parquet.ts +19 -0
  238. package/src/runs/types.ts +31 -0
  239. package/src/runtime/json-code-block.test.ts +106 -47
  240. package/src/runtime/json-code-block.ts +1209 -46
  241. package/src/term.test.ts +13 -0
  242. package/src/term.ts +81 -49
  243. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  244. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  245. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  246. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  247. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  248. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  249. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  250. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  251. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  252. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  253. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  254. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  255. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  256. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  257. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  258. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  259. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  260. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  261. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  262. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -0,0 +1,203 @@
1
+ /**
2
+ * HTTP wire frame stored on a wide event — query, headers, status.
3
+ *
4
+ * Credential headers are replaced before the row is written. The body stays
5
+ * on `input` / `output`; this frame is the rest of the message.
6
+ */
7
+
8
+ import type { RunHttpFrame } from "../runs/types.ts";
9
+
10
+ /** Replacement for credential header and query values. */
11
+ export const HTTP_FRAME_REDACTED = "[redacted]";
12
+
13
+ const EXACT_HEADERS = new Set([
14
+ "authorization",
15
+ "proxy-authorization",
16
+ "cookie",
17
+ "set-cookie",
18
+ "x-api-key",
19
+ "dpop",
20
+ ]);
21
+
22
+ /**
23
+ * Build the Request a Console invoke would have sent for an HTTP flow.
24
+ *
25
+ * Call API runs `execute` directly, so there is no socket request. The frame
26
+ * still needs a method, path, query, and the headers the invoke actually
27
+ * uses (`accept`, `x-oke-client: console`, and `content-type` when there is a body).
28
+ * The operator's browser user-agent is not copied onto the flow.
29
+ *
30
+ * @param trigger - HTTP method and path template
31
+ * @param options - Path params and the assembled flow input
32
+ */
33
+ export function requestForHttpInvoke(
34
+ trigger: { readonly method: string; readonly path: string },
35
+ options: {
36
+ readonly pathParams?: Readonly<Record<string, string>>;
37
+ readonly input?: unknown;
38
+ } = {},
39
+ ): Request {
40
+ let path = trigger.path;
41
+ const params = options.pathParams ?? {};
42
+ for (const [key, value] of Object.entries(params)) {
43
+ path = path.replaceAll(`:${key}`, encodeURIComponent(value));
44
+ }
45
+ const url = new URL(path, "http://127.0.0.1");
46
+ const method = trigger.method.toUpperCase();
47
+ const headers = new Headers({ accept: "application/json", "x-oke-client": "console" });
48
+ if (!methodCarriesBody(method)) {
49
+ appendScalarQuery(url, options.input, new Set(Object.keys(params)));
50
+ return new Request(url.href, { method, headers });
51
+ }
52
+ headers.set("content-type", "application/json");
53
+ const body = options.input === undefined ? undefined : JSON.stringify(options.input);
54
+ return new Request(url.href, {
55
+ method,
56
+ headers,
57
+ ...(body !== undefined ? { body } : {}),
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Build the wire frame for an HTTP run.
63
+ *
64
+ * Does not read the body. Safe to call after the handler has consumed it,
65
+ * including streamed responses.
66
+ *
67
+ * @param request - Incoming request
68
+ * @param response - Final response after `onResponse`, when one exists
69
+ */
70
+ export function captureHttpFrame(request: Request, response: Response | undefined): RunHttpFrame {
71
+ const url = new URL(request.url);
72
+ const query: Record<string, string> = {};
73
+ for (const [key, value] of url.searchParams) {
74
+ query[key] = sensitiveQuery(key) ? HTTP_FRAME_REDACTED : value;
75
+ }
76
+ return redactHttpFrame({
77
+ request: {
78
+ method: request.method,
79
+ path: url.pathname,
80
+ query,
81
+ headers: headerMap(request.headers),
82
+ },
83
+ ...(response !== undefined
84
+ ? { response: { status: response.status, headers: headerMap(response.headers) } }
85
+ : {}),
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Mask credential headers and query values on a stored frame.
91
+ *
92
+ * Applied again at Console projection so a row that skipped capture still
93
+ * does not show secrets.
94
+ *
95
+ * @param frame - Stored HTTP frame
96
+ */
97
+ export function redactHttpFrame(frame: RunHttpFrame): RunHttpFrame {
98
+ return {
99
+ request: {
100
+ method: frame.request.method,
101
+ path: frame.request.path,
102
+ query: redactQuery(frame.request.query),
103
+ headers: redactHeaders(frame.request.headers),
104
+ },
105
+ ...(frame.response !== undefined
106
+ ? {
107
+ response: {
108
+ status: frame.response.status,
109
+ headers: redactHeaders(frame.response.headers),
110
+ },
111
+ }
112
+ : {}),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * True when a header name carries a credential.
118
+ *
119
+ * @param name - Lower-case header name
120
+ */
121
+ export function sensitiveHeader(name: string): boolean {
122
+ const key = name.toLowerCase();
123
+ if (EXACT_HEADERS.has(key)) return true;
124
+ if (key === "idempotency-key") return false;
125
+ return (
126
+ key.includes("token") ||
127
+ key.includes("secret") ||
128
+ key.includes("api-key") ||
129
+ key.endsWith("-key") ||
130
+ key.endsWith("_key")
131
+ );
132
+ }
133
+
134
+ function methodCarriesBody(method: string): boolean {
135
+ switch (method) {
136
+ case "GET":
137
+ case "HEAD":
138
+ case "DELETE":
139
+ return false;
140
+ default:
141
+ return true;
142
+ }
143
+ }
144
+
145
+ function appendScalarQuery(url: URL, input: unknown, skip: ReadonlySet<string>): void {
146
+ if (input === null || typeof input !== "object" || Array.isArray(input)) return;
147
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
148
+ if (skip.has(key)) continue;
149
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
150
+ url.searchParams.set(key, String(value));
151
+ }
152
+ }
153
+ }
154
+
155
+ function sensitiveQuery(name: string): boolean {
156
+ const key = name.toLowerCase();
157
+ return (
158
+ key === "token" ||
159
+ key.endsWith("_token") ||
160
+ key.endsWith("-token") ||
161
+ key === "api_key" ||
162
+ key === "apikey" ||
163
+ key === "api-key" ||
164
+ key === "secret" ||
165
+ key === "password" ||
166
+ key === "access_token" ||
167
+ key === "refresh_token"
168
+ );
169
+ }
170
+
171
+ function headerMap(headers: Headers): Record<string, string> {
172
+ const out: Record<string, string> = {};
173
+ headers.forEach((value, key) => {
174
+ const name = key.toLowerCase();
175
+ if (sensitiveHeader(name)) {
176
+ out[name] = HTTP_FRAME_REDACTED;
177
+ return;
178
+ }
179
+ const prev = out[name];
180
+ out[name] = prev === undefined ? value : `${prev}, ${value}`;
181
+ });
182
+ return sortRecord(out);
183
+ }
184
+
185
+ function redactHeaders(headers: Readonly<Record<string, string>>): Record<string, string> {
186
+ const out: Record<string, string> = {};
187
+ for (const [key, value] of Object.entries(headers)) {
188
+ out[key.toLowerCase()] = sensitiveHeader(key) ? HTTP_FRAME_REDACTED : value;
189
+ }
190
+ return sortRecord(out);
191
+ }
192
+
193
+ function redactQuery(query: Readonly<Record<string, string>>): Record<string, string> {
194
+ const out: Record<string, string> = {};
195
+ for (const [key, value] of Object.entries(query)) {
196
+ out[key] = sensitiveQuery(key) ? HTTP_FRAME_REDACTED : value;
197
+ }
198
+ return sortRecord(out);
199
+ }
200
+
201
+ function sortRecord(record: Record<string, string>): Record<string, string> {
202
+ return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
203
+ }
@@ -10,6 +10,7 @@
10
10
  import { mkdir } from "node:fs/promises";
11
11
  import { dirname } from "node:path";
12
12
  import { okid } from "../okid.ts";
13
+ import type { DecisionLabelStore } from "./decision-label-store.ts";
13
14
  import type { IdempotencyStore } from "./idempotency-store.ts";
14
15
  import { JournalSuspend } from "./journal-suspend.ts";
15
16
  import { lazyRequire } from "./lazy-require.ts";
@@ -128,6 +129,24 @@ export interface JournalLeaseStore {
128
129
  * @param leaseMs - Lease duration
129
130
  */
130
131
  acquireLease(runId: string, instanceId: string, now: number, leaseMs: number): Promise<boolean>;
132
+ /**
133
+ * Take the lease and write `update(run)` in that same hold.
134
+ * `undefined` from `update` keeps the previous entries and still holds the lease.
135
+ * `"lease"` means another holder won. `"missing"` means no run.
136
+ *
137
+ * @param runId - Run id
138
+ * @param instanceId - Claimant instance
139
+ * @param now - Epoch-ms
140
+ * @param leaseMs - Lease duration
141
+ * @param update - Next snapshot, or undefined to leave entries
142
+ */
143
+ cas(
144
+ runId: string,
145
+ instanceId: string,
146
+ now: number,
147
+ leaseMs: number,
148
+ update: (run: JournalRun) => JournalRun | undefined,
149
+ ): Promise<"ok" | "missing" | { readonly lease: true; readonly leaseExpiresAt?: number }>;
131
150
  /**
132
151
  * Release a lease held by `instanceId` (no-op for other holders).
133
152
  *
@@ -161,6 +180,20 @@ function loadIdempotencyStore(): typeof import("./idempotency-store.ts") {
161
180
  return lazyRequire(import.meta.dir, ["idempotency", "store"].join("-"));
162
181
  }
163
182
 
183
+ /**
184
+ * Load the decision-label table without a static import.
185
+ */
186
+ function loadDecisionLabelStore(): typeof import("./decision-label-store.ts") {
187
+ return lazyRequire(import.meta.dir, ["decision", "label", "store"].join("-"));
188
+ }
189
+
190
+ /**
191
+ * Load the agent-event table without a static import.
192
+ */
193
+ function loadAgentEventStore(): typeof import("./agent-event-store.ts") {
194
+ return lazyRequire(import.meta.dir, ["agent", "event", "store"].join("-"));
195
+ }
196
+
164
197
  /** Persistence backend for journal runs. */
165
198
  export interface JournalStore extends Partial<JournalLeaseStore> {
166
199
  /**
@@ -168,6 +201,16 @@ export interface JournalStore extends Partial<JournalLeaseStore> {
168
201
  * only implements run `get` / `put`.
169
202
  */
170
203
  readonly idempotency?: IdempotencyStore;
204
+ /**
205
+ * Decision labels and the drift flag on this same driver. Opened only when
206
+ * the app declares decisions.
207
+ */
208
+ readonly decisions?: DecisionLabelStore;
209
+ /**
210
+ * Agent-run event log on this same driver. Followers resume from these rows
211
+ * after a restart and from another instance.
212
+ */
213
+ readonly agentEvents?: import("./agent-event-store.ts").AgentEventStore;
171
214
  /**
172
215
  * Load a run by id.
173
216
  *
@@ -234,6 +277,7 @@ function leaseMethods(
234
277
  load: () => Promise<Map<string, JournalRun>>,
235
278
  flush?: (map: Map<string, JournalRun>) => Promise<void>,
236
279
  ): JournalLeaseStore {
280
+ let gate: Promise<void> = Promise.resolve();
237
281
  return {
238
282
  async acquireLease(runId, instanceId, now, leaseMs) {
239
283
  const map = await load();
@@ -244,6 +288,31 @@ function leaseMethods(
244
288
  await flush?.(map);
245
289
  return true;
246
290
  },
291
+ async cas(runId, instanceId, now, leaseMs, update) {
292
+ let release!: () => void;
293
+ const slot = new Promise<void>((resolve) => {
294
+ release = resolve;
295
+ });
296
+ const prev = gate;
297
+ gate = slot;
298
+ await prev;
299
+ try {
300
+ const map = await load();
301
+ const run = map.get(runId);
302
+ if (!run) return "missing";
303
+ if (!claimable(run, instanceId, now)) {
304
+ return { lease: true, leaseExpiresAt: run.leaseExpiresAt };
305
+ }
306
+ const next = update(cloneRun(run)) ?? run;
307
+ next.lockedBy = instanceId;
308
+ next.leaseExpiresAt = now + leaseMs;
309
+ map.set(runId, next);
310
+ await flush?.(map);
311
+ return "ok";
312
+ } finally {
313
+ release();
314
+ }
315
+ },
247
316
  async releaseLease(runId, instanceId) {
248
317
  const map = await load();
249
318
  const run = map.get(runId);
@@ -306,6 +375,16 @@ export function createMemoryJournalStore(seed?: readonly JournalRun[]): JournalS
306
375
  Object.defineProperty(this, "idempotency", { value: created });
307
376
  return created;
308
377
  },
378
+ get decisions(): DecisionLabelStore {
379
+ const created = loadDecisionLabelStore().createMemoryDecisionLabelStore();
380
+ Object.defineProperty(this, "decisions", { value: created });
381
+ return created;
382
+ },
383
+ get agentEvents(): import("./agent-event-store.ts").AgentEventStore {
384
+ const created = loadAgentEventStore().createMemoryAgentEventStore();
385
+ Object.defineProperty(this, "agentEvents", { value: created });
386
+ return created;
387
+ },
309
388
  };
310
389
  }
311
390
 
@@ -359,6 +438,20 @@ export function createFileJournalStore(path: string): JournalStore {
359
438
  Object.defineProperty(this, "idempotency", { value: created });
360
439
  return created;
361
440
  },
441
+ get decisions(): DecisionLabelStore {
442
+ const created = loadDecisionLabelStore().createFileDecisionLabelStore(
443
+ `${dirname(path)}/decision-labels.json`,
444
+ );
445
+ Object.defineProperty(this, "decisions", { value: created });
446
+ return created;
447
+ },
448
+ get agentEvents(): import("./agent-event-store.ts").AgentEventStore {
449
+ const created = loadAgentEventStore().createFileAgentEventStore(
450
+ `${dirname(path)}/agent-events`,
451
+ );
452
+ Object.defineProperty(this, "agentEvents", { value: created });
453
+ return created;
454
+ },
362
455
  };
363
456
  }
364
457
 
@@ -432,6 +525,11 @@ export interface JournalSession {
432
525
  * instead of treating the cursor as past them. Clears the in-memory undo stack
433
526
  * so frames re-bind on the next walk.
434
527
  */
528
+ /**
529
+ * Increments on {@link JournalSession.rewind}. Agent run ids key off this
530
+ * so `flow.retry` replays the same id.
531
+ */
532
+ readonly epoch: number;
435
533
  rewind(): void;
436
534
  /** Registered per-step undos in persist/replay order (LIFO compensate). */
437
535
  undoStack(): readonly JournalUndoFrame[];
@@ -471,6 +569,7 @@ export function createJournalSlot(): {
471
569
  const session = slot.session;
472
570
  if (!session) {
473
571
  if (prop === "runId") return "";
572
+ if (prop === "epoch") return 0;
474
573
  if (prop === "stampTenant") return async () => undefined;
475
574
  if (prop === "rewind") return () => undefined;
476
575
  return undefined;
@@ -516,6 +615,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
516
615
  function openSession(run: JournalRun, leased: boolean): JournalSession {
517
616
  /** Next entry index to consume on replay. */
518
617
  let cursor = 0;
618
+ let epoch = 0;
519
619
  let leaseHeld = leased;
520
620
  let registrationPass = false;
521
621
  let undoExecution = false;
@@ -665,9 +765,13 @@ export function createJournal(options: CreateJournalOptions): Journal {
665
765
  await persist();
666
766
  return value;
667
767
  },
768
+ get epoch() {
769
+ return epoch;
770
+ },
668
771
  rewind() {
669
772
  cursor = 0;
670
773
  undos.length = 0;
774
+ epoch += 1;
671
775
  },
672
776
  undoStack() {
673
777
  return undos;
@@ -28,6 +28,16 @@ export interface JsonStreamResult {
28
28
  ready?: () => Promise<void>;
29
29
  /** Set by the kernel to commit journal / Runs after the stream settles. */
30
30
  finalize?: () => Promise<void>;
31
+ /**
32
+ * Agent result once a streamed `fx.run` settles.
33
+ * Absent for text and live streams.
34
+ */
35
+ result?: Promise<unknown>;
36
+ /**
37
+ * Set when iteration parked a durable run, after `[DONE]` is queued.
38
+ * Finalize commits the journal as sleeping from this.
39
+ */
40
+ parked?: { wakeAt: number; label: string };
31
41
  }
32
42
 
33
43
  /**
@@ -14,6 +14,7 @@ import { createClockRuntime } from "../elements/clock.ts";
14
14
  import { gate } from "../elements/gate.ts";
15
15
  import { createRunsRuntime, memoryRunsDriver } from "../runs/index.ts";
16
16
  import { oke } from "./app.ts";
17
+ import { HTTP_FRAME_REDACTED } from "./http-frame.ts";
17
18
  import { flow, resetFlowSeq } from "./flow.ts";
18
19
  import { on, resetBindings } from "./on.ts";
19
20
  import { http } from "./triggers.ts";
@@ -196,6 +197,50 @@ describe("pipeline — Unauthorized for anonymous", () => {
196
197
  });
197
198
  });
198
199
 
200
+ describe("pipeline — HTTP wire frame", () => {
201
+ test("fetch stores query, headers, and status beside the body", async () => {
202
+ resetBindings();
203
+ resetFlowSeq();
204
+
205
+ on(
206
+ http.get("/tasks", { out: z.object({ ok: z.literal(true) }) }).public(),
207
+ flow("tasks.list", {
208
+ do: () => ({ ok: true as const }),
209
+ }),
210
+ );
211
+
212
+ const runs = createRunsRuntime({ driver: memoryRunsDriver });
213
+ await runs.open();
214
+ const app = oke({
215
+ name: "http-frame",
216
+ gate: { policies: [gate.public] },
217
+ runs,
218
+ env: "test",
219
+ });
220
+ await app.boot({ env: "test", gates: [gate.public], runs });
221
+
222
+ const res = await app.fetch(
223
+ new Request("http://localhost/tasks?limit=20", {
224
+ headers: { accept: "application/json", cookie: "sid=secret" },
225
+ }),
226
+ );
227
+ expect(res.status).toBe(200);
228
+
229
+ const event = (await runs.all()).find((row) => row.flow === "tasks.list");
230
+ expect(event?.input).toBeUndefined();
231
+ expect(event?.http).toMatchObject({
232
+ request: {
233
+ method: "GET",
234
+ path: "/tasks",
235
+ query: { limit: "20" },
236
+ headers: { cookie: HTTP_FRAME_REDACTED, accept: "application/json" },
237
+ },
238
+ response: { status: 200 },
239
+ });
240
+ await app.bootResult?.close();
241
+ });
242
+ });
243
+
199
244
  describe("pipeline — evaluated gates on the run", () => {
200
245
  test("recorded run contains evaluated chain, not just declared names", async () => {
201
246
  resetBindings();
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SSE id carried on a live agent event without changing its JSON payload.
3
+ *
4
+ * The follow log stores a seq. The live `fx.run` frame for that row uses the
5
+ * same seq as `id:` so a client can resume with `Last-Event-ID`.
6
+ */
7
+
8
+ const sseIdBrand: unique symbol = Symbol.for("oke.sse.id");
9
+
10
+ /**
11
+ * Copy `value` and attach an SSE id the encoder writes as `id:`.
12
+ * The id is not enumerable, so it is not part of the JSON payload.
13
+ *
14
+ * @param value - Event object
15
+ * @param id - SSE id (the stored log seq)
16
+ */
17
+ export function withSseId<T extends object>(value: T, id: string): T {
18
+ const copy: T = { ...value };
19
+ Object.defineProperty(copy, sseIdBrand, { value: id, enumerable: false });
20
+ return copy;
21
+ }
22
+
23
+ /**
24
+ * SSE id attached by {@link withSseId}, if any.
25
+ *
26
+ * @param value - Stream chunk
27
+ */
28
+ export function readSseId(value: unknown): string | undefined {
29
+ if (typeof value !== "object" || value === null) return undefined;
30
+ const id = (value as { readonly [sseIdBrand]?: unknown })[sseIdBrand];
31
+ return typeof id === "string" ? id : undefined;
32
+ }
@@ -52,6 +52,7 @@ const EFFECT_KEYS = [
52
52
  "secrets",
53
53
  "calls",
54
54
  "fetches",
55
+ "decides",
55
56
  ] as const;
56
57
 
57
58
  /** Isolation strength: higher = stronger tenant separation. */
@@ -58,6 +58,12 @@ export type AuthApiKeysResourceRef = "auth:api-keys";
58
58
  */
59
59
  export type AuthTenantsResourceRef = "auth:tenants";
60
60
 
61
+ /**
62
+ * Agent-run journal capability for approval routes.
63
+ * Not a store facet — the journal driver owns the rows.
64
+ */
65
+ export type JournalRunsResourceRef = "journal:runs";
66
+
61
67
  /**
62
68
  * Dead-letter / live-stream read capability — not a store facet.
63
69
  * Declare on `effects.reads` (never `writes`).
@@ -101,8 +107,10 @@ export interface Effects {
101
107
  | AuthApiKeysResourceRef
102
108
  | AuthTenantsResourceRef
103
109
  >;
104
- /** Store writes, plus `"auth:api-keys"` / `"auth:tenants"` for `fx.auth` mutations. */
105
- writes?: Array<ResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef>;
110
+ /** Store writes, plus `"auth:api-keys"` / `"auth:tenants"` and `"journal:runs"`. */
111
+ writes?: Array<
112
+ ResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef | JournalRunsResourceRef
113
+ >;
106
114
  /** Emitted signals. */
107
115
  emits?: SignalRef[];
108
116
  /** Channel template sends (irreversible). */
@@ -123,6 +131,8 @@ export interface Effects {
123
131
  * Hostname only (e.g. `"api.stripe.com"`), never a full URL.
124
132
  */
125
133
  fetches?: FetchHostRef[];
134
+ /** Decision refs from `fx.decide` (irreversible, nondeterministic). */
135
+ decides?: string[];
126
136
  }
127
137
 
128
138
  /** Embedding model ref for `effects.embeds` / `fx.embed` (model name). */
@@ -475,6 +485,8 @@ export interface AiPrompt {
475
485
  version?: number;
476
486
  evals?: string;
477
487
  budget?: AiBudget;
488
+ /** `1` sends one schema-mismatch follow-up. Default `0`. */
489
+ repair?: 0 | 1;
478
490
  /** Ordered recovery chain of logical model names. */
479
491
  via?: string[];
480
492
  /** Per-command deadline (`"30s"` or milliseconds). */
@@ -487,7 +499,10 @@ export interface AiPrompt {
487
499
  /** Bounded agent whose tools are flows. */
488
500
  export interface AiAgent {
489
501
  tools?: FlowRef[];
502
+ /** Tools that park for human approval. The predicate stays in author code. */
503
+ approvals?: Record<string, true>;
490
504
  maxSteps?: number;
505
+ maxDepth?: number;
491
506
  model?: string;
492
507
  budget?: AiBudget;
493
508
  }
@@ -514,9 +529,21 @@ export interface Ai {
514
529
  models?: Record<string, AiModel>;
515
530
  prompts?: Record<string, AiPrompt>;
516
531
  agents?: Record<string, AiAgent>;
532
+ decisions?: Record<string, AiDecision>;
517
533
  mcpServers?: Record<string, AiMcpServer>;
518
534
  }
519
535
 
536
+ /** A declared decision. The predicate and locale function stay in author code. */
537
+ export interface AiDecision {
538
+ mode: "review" | "abstain";
539
+ review?: string;
540
+ model?: string;
541
+ driverId?: "openrouter" | "typesafe";
542
+ questions: string[];
543
+ evals?: string;
544
+ autonomy?: { maxError: number; audit: number; risk?: number };
545
+ }
546
+
520
547
  /** Per-table metadata on a Manifest {@link Plugin}. */
521
548
  export interface PluginTable {
522
549
  plane?: string;