okengine 0.21.1 → 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 (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
package/src/kernel/app.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * pipeline, and wires `fx.call` to untriggered (and triggered) flows.
7
7
  */
8
8
 
9
+ import { bindAgentApprovalFlows } from "../elements/ai/approval-http.ts";
9
10
  import { compileRoute } from "../compiler/dynamic.ts";
10
11
  import type { CompiledRoute } from "../compiler/aot.ts";
11
12
  import { encodeExecuteResult, encodeFailure } from "../compiler/response.ts";
@@ -43,7 +44,7 @@ import { parseAcceptLanguage } from "../elements/channel/locale.ts";
43
44
  import { runWithLocale } from "../i18n/locale-context.ts";
44
45
  import { isFlow, type AnyFlowDef } from "./flow.ts";
45
46
  import { failureFromUnknown, runCompensationPhase } from "./compensate.ts";
46
- import { withAbortSignal } from "./abort-scope.ts";
47
+ import { runDetached, withAbortSignal } from "./abort-scope.ts";
47
48
  import { withCdcMutationId } from "../elements/store/sql-session.ts";
48
49
  import { MUTATION_ID_HEADER } from "./mutation-id.ts";
49
50
  import { fxRetry } from "./concurrency.ts";
@@ -67,6 +68,7 @@ import {
67
68
  import { GATE_PUBLIC_NAME } from "../elements/gate/flatten.ts";
68
69
  import type { JsonCodeAuth } from "../runtime/json-code-block.ts";
69
70
  import { resolveDurationMs } from "./elapsed.ts";
71
+ import { captureHttpFrame } from "./http-frame.ts";
70
72
  import { fail, throwOke, OkeError } from "./errors.ts";
71
73
  import { FLOW_NAME_DUPLICATE, FLOW_UNNAMED } from "./errors-flow-name.ts";
72
74
  import { ONCE_SIGNAL_MULTI_FLOW } from "./errors-once-signal.ts";
@@ -83,6 +85,7 @@ import {
83
85
  } from "./hooks.ts";
84
86
  import {
85
87
  createJournal,
88
+ createJournalSlot,
86
89
  createMemoryJournalStore,
87
90
  hasJournalLease,
88
91
  isJournalLeaseBusy,
@@ -91,6 +94,7 @@ import {
91
94
  type JournalSession,
92
95
  type JournalStore,
93
96
  } from "./journal.ts";
97
+ import type { IdempotencyAttempt } from "./idempotency.ts";
94
98
  import { releaseInstanceLeases } from "./graceful-shutdown.ts";
95
99
  import { mintInstanceId } from "./instance-id.ts";
96
100
  import type { JournalRuntime } from "./boot-bind/journal.ts";
@@ -181,6 +185,11 @@ function loadFx(): {
181
185
  return lazyRequire(import.meta.dir, ["fx", "runtime"].join("-"));
182
186
  }
183
187
 
188
+ /** Idempotency policy — off the edge graph until a request actually claims. */
189
+ function loadIdempotency(): typeof import("./idempotency.ts") {
190
+ return lazyRequire(import.meta.dir, ["idem", "potency"].join(""));
191
+ }
192
+
184
193
  /** App-shell tenancy wiring — loaded only when `gate.auth.tenant` is on. */
185
194
  function loadAppTenant(): { w: (kind: number, ...args: unknown[]) => unknown } {
186
195
  return lazyRequire(import.meta.dir, ["app", "tenant"].join("-"));
@@ -210,6 +219,39 @@ function loadClientDescriptor(): typeof import("./client-descriptor.ts") {
210
219
  return lazyRequire(import.meta.dir, ["client", "descriptor"].join("-"));
211
220
  }
212
221
 
222
+ /** Decision HTTP flows. A static `import()` still lands on every HTTP bundle. */
223
+ function loadDecisionBind(): typeof import("../elements/ai/decisions/bind.ts") {
224
+ return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["bi", "nd"].join(""));
225
+ }
226
+
227
+ /** Decision lockfile. Loaded only when the manifest declares decisions. */
228
+ function loadDecisionCertificate(): typeof import("../elements/ai/decisions/certificate.ts") {
229
+ return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["certificate"].join(""));
230
+ }
231
+
232
+ /** Decision label opener. Loaded only when the manifest declares decisions. */
233
+ function loadDecisionLabels(): typeof import("../elements/ai/decisions/labels.ts") {
234
+ return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["lab", "els"].join(""));
235
+ }
236
+
237
+ /** Drift-flag registration. Loaded only when the manifest declares decisions. */
238
+ function loadDecisionLabelStore(): typeof import("./decision-label-store.ts") {
239
+ return lazyRequire(import.meta.dir, ["decision", "label", "store"].join("-"));
240
+ }
241
+
242
+ /** `fx.decide` gate hook. Loaded only when the manifest declares decisions. */
243
+ function loadFxDecideModule(): typeof import("./fx-decide.ts") {
244
+ return lazyRequire(import.meta.dir, ["fx", "decide"].join("-"));
245
+ }
246
+
247
+ /**
248
+ * Full boot graph (drivers, elements). A static `import()` string is still
249
+ * bundled into every HTTP app; the stem stays computed.
250
+ */
251
+ function loadBoot(): typeof import("./boot.ts") {
252
+ return lazyRequire(import.meta.dir, ["bo", "ot"].join(""));
253
+ }
254
+
213
255
  /** Options for {@link oke}. */
214
256
  export interface OkeOptions {
215
257
  /** Application name (Manifest `app`). */
@@ -1088,6 +1130,8 @@ export function oke(options: OkeOptions): OkeApp {
1088
1130
  // contribute auth-method Bindings before traffic (plan Phase 2).
1089
1131
  const router: Router<Binding> = smart;
1090
1132
 
1133
+ bindAgentApprovalFlows(adoptBinding);
1134
+
1091
1135
  function adoptBinding(b: Binding): void {
1092
1136
  adopted.push(b);
1093
1137
  registerFlow(b.flow);
@@ -1108,6 +1152,24 @@ export function oke(options: OkeOptions): OkeApp {
1108
1152
 
1109
1153
  /** Once-per-app: system `_oke_search_embed_*` CDC bindings from Manifest. */
1110
1154
  let searchEmbedFlowsBound = false;
1155
+ let decisionFlowsBound = false;
1156
+
1157
+ /**
1158
+ * Register the decision aggregate, drift monitor, and candidate route
1159
+ * when any decision declares autonomy.
1160
+ *
1161
+ * @param manifest - Boot manifest
1162
+ */
1163
+ async function ensureDecisionFlows(manifest: BootOptions["manifest"] | undefined): Promise<void> {
1164
+ if (decisionFlowsBound || !manifest) return;
1165
+ const { bindDecisionFlows, manifestHasDecisionAutonomy } = loadDecisionBind();
1166
+ if (!manifestHasDecisionAutonomy(manifest)) {
1167
+ decisionFlowsBound = true;
1168
+ return;
1169
+ }
1170
+ bindDecisionFlows(adoptBinding, manifest);
1171
+ decisionFlowsBound = true;
1172
+ }
1111
1173
  /**
1112
1174
  * Auto-register hybrid-search embed CDC flows when any `.embed()` column
1113
1175
  * exists. Lazy-loads search-bind so edge graphs without search stay lean.
@@ -1332,6 +1394,19 @@ export function oke(options: OkeOptions): OkeApp {
1332
1394
  ...durableResumeFx(),
1333
1395
  ...(gateConfig.auth?.tenant ? (loadAppTenant().w(5, gateConfig.auth) as object) : {}),
1334
1396
  ...(existing?.tenant ? { tenant: { id: existing.tenant } } : {}),
1397
+ callHandler: async (name, callInput) => {
1398
+ const target = flowsByName.get(name);
1399
+ if (!target) return undefined;
1400
+ const inner = await execute(
1401
+ target,
1402
+ callInput,
1403
+ { kind: "internal" } satisfies InternalTrigger,
1404
+ { parentId: runId },
1405
+ );
1406
+ if (inner.failure) return inner.failure;
1407
+ if (inner.ctx.error !== undefined) throw inner.ctx.error;
1408
+ return inner.output;
1409
+ },
1335
1410
  },
1336
1411
  });
1337
1412
  } catch (err) {
@@ -1360,7 +1435,7 @@ export function oke(options: OkeOptions): OkeApp {
1360
1435
  }
1361
1436
 
1362
1437
  async function doBoot(overrides?: Partial<BootOptions>): Promise<BootResult> {
1363
- const { bootApplication, resolveElementNeeds } = await import("./boot.ts");
1438
+ const { bootApplication, resolveElementNeeds } = loadBoot();
1364
1439
  const { assertHttpGatePosture } = await import("../elements/gate/boot.ts");
1365
1440
  const { assertPluginNeeds, buildAvailableNeedTokens } = await import("./plugin-needs.ts");
1366
1441
  const dockerFlag =
@@ -1381,6 +1456,7 @@ export function oke(options: OkeOptions): OkeApp {
1381
1456
  }
1382
1457
 
1383
1458
  const unguardedHttp = overrides?.unguardedHttp ?? gateConfig.unguardedHttp ?? "deny";
1459
+ await ensureDecisionFlows(overrides?.manifest ?? options.manifest);
1384
1460
  // Auth bindings are in `adopted` — missing posture fails with GateBootError.
1385
1461
  assertHttpGatePosture(adopted, { unguardedHttp, env: bootEnv });
1386
1462
 
@@ -1550,6 +1626,26 @@ export function oke(options: OkeOptions): OkeApp {
1550
1626
  // Built-in hybrid search — auto-register durable embed CDC flows when the
1551
1627
  // Manifest declares any `.embed()` column (writer flows stay embed-free).
1552
1628
  await ensureSearchEmbedFlows(overrides?.manifest ?? options.manifest);
1629
+ const decisionManifest = overrides?.manifest ?? options.manifest;
1630
+ const declaredDecisions = decisionManifest?.ai?.decisions;
1631
+ if (declaredDecisions && Object.keys(declaredDecisions).length > 0) {
1632
+ const { loadDecisionLockfile, setDecisionDrift } = loadDecisionCertificate();
1633
+ const { openDecisionLabelStore } = loadDecisionLabels();
1634
+ const { setDeclaredDriftDecisions } = loadDecisionLabelStore();
1635
+ const { setDecisionGateAllow } = loadFxDecideModule();
1636
+ const root = overrides?.rootDir ?? options.rootDir ?? process.env["OKE_ROOT_DIR"];
1637
+ if (!root) {
1638
+ throw new Error(
1639
+ "oke boot: decisions are declared but rootDir and OKE_ROOT_DIR are unset, so the decision lockfile cannot load.",
1640
+ );
1641
+ }
1642
+ await loadDecisionLockfile(root);
1643
+ setDeclaredDriftDecisions(Object.keys(declaredDecisions));
1644
+ if (result.journal && result.journal.store.decisions) {
1645
+ await openDecisionLabelStore(result.journal.store, setDecisionDrift);
1646
+ }
1647
+ if (result.gate) setDecisionGateAllow((names, ctx) => result.gate!.allow(names, ctx));
1648
+ }
1553
1649
  // otp() provider / app mode capability — fail loud at boot, never silent downgrade.
1554
1650
  if (result.channel) {
1555
1651
  const { assertOtpPluginCapability } = await import("../auth/otp-capability.ts");
@@ -1837,23 +1933,61 @@ export function oke(options: OkeOptions): OkeApp {
1837
1933
  );
1838
1934
 
1839
1935
  capability = booted.capabilities.get(flowDef.name);
1840
-
1841
- if (flowDef.durable) {
1842
- const { store, instanceId, leaseMs } = activeJournal();
1843
- const journal = createJournal({
1844
- store,
1845
- now,
1846
- id: () => runId,
1847
- // Hold the run lease for the request's lifetime — a crash mid-run
1848
- // leaves an expired lease another instance can reclaim and resume.
1849
- ...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
1850
- });
1851
- journalSession = await journal.start(flowDef.name, input);
1852
- inflightRuns.add(journalSession.runId);
1853
- }
1854
1936
  }
1855
1937
 
1856
1938
  const effects = flowDef.effects ?? capability?.declared;
1939
+ const requestForIdem = extras?.request;
1940
+ const rawIdemHeader = requestForIdem?.headers.get("idempotency-key") ?? null;
1941
+ const idemRequired =
1942
+ flowDef.idempotency === "required" ||
1943
+ (typeof flowDef.idempotency === "object" && flowDef.idempotency.required === true) ||
1944
+ flowDef.resolvedIdempotency?.mode === "required";
1945
+ let idemAttempt: IdempotencyAttempt | undefined;
1946
+ if (requestForIdem && (rawIdemHeader !== null || idemRequired)) {
1947
+ const method = trigger.kind === "http" ? trigger.method : requestForIdem.method;
1948
+ const live =
1949
+ flowDef.live !== undefined || (trigger.kind === "http" && trigger.liveSignal !== undefined);
1950
+ idemAttempt = loadIdempotency().prepareIdempotencyAttempt({
1951
+ hasRequest: true,
1952
+ method,
1953
+ stream: flowDef.stream === true,
1954
+ live,
1955
+ effects,
1956
+ usesRaw: flowDef.usesRaw === true,
1957
+ option: flowDef.idempotency,
1958
+ resolved: flowDef.resolvedIdempotency,
1959
+ header: rawIdemHeader,
1960
+ });
1961
+ }
1962
+ const claiming = idemAttempt?.claiming === true;
1963
+
1964
+ const openJournal = (): ReturnType<typeof createJournal> => {
1965
+ const { store, instanceId, leaseMs } = activeJournal();
1966
+ return createJournal({
1967
+ store,
1968
+ now,
1969
+ id: () => runId,
1970
+ // Hold the run lease for the request's lifetime — a crash mid-run
1971
+ // leaves an expired lease another instance can reclaim and resume.
1972
+ ...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
1973
+ });
1974
+ };
1975
+
1976
+ // A claim starts the journal only after it wins, so a replay or a 409
1977
+ // does not open a run. Auto with no header keeps the start here.
1978
+ if (booted && flowDef.durable && !claiming) {
1979
+ journalSession = await openJournal().start(flowDef.name, input);
1980
+ inflightRuns.add(journalSession.runId);
1981
+ }
1982
+
1983
+ let journalSlot: { session?: JournalSession } | undefined;
1984
+ let journalFacade: JournalSession | undefined;
1985
+ if (booted && flowDef.durable && claiming) {
1986
+ const created = createJournalSlot();
1987
+ journalSlot = created.slot;
1988
+ journalFacade = created.facade;
1989
+ }
1990
+ const journalForFx = journalSession ?? journalFacade;
1857
1991
  const { fx, ledger } = loadFx().createFxContext({
1858
1992
  ...options.fx,
1859
1993
  flow: flowDef.name,
@@ -1906,7 +2040,7 @@ export function oke(options: OkeOptions): OkeApp {
1906
2040
  }
1907
2041
  : {}),
1908
2042
  runsRuntime: booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined),
1909
- ...(journalSession ? { journal: journalSession, durable: true } : {}),
2043
+ ...(journalForFx ? { journal: journalForFx, durable: true } : {}),
1910
2044
  callHandler: async (name, callInput) => {
1911
2045
  const target = flowsByName.get(name);
1912
2046
  if (!target) {
@@ -2038,13 +2172,141 @@ export function oke(options: OkeOptions): OkeApp {
2038
2172
  }
2039
2173
  return output;
2040
2174
  };
2175
+ const runClaimed = async (validated: unknown): Promise<unknown> => {
2176
+ if (!claiming || idemAttempt === undefined) return invoke(validated);
2177
+ const policy = loadIdempotency();
2178
+ const classified = policy.classifyIdempotencyKey(rawIdemHeader);
2179
+ if (classified === "missing") {
2180
+ return policy.idempotencyErrorResponse("IdempotencyKeyMissing");
2181
+ }
2182
+ if (classified === "invalid") {
2183
+ return policy.idempotencyErrorResponse("IdempotencyKeyInvalid");
2184
+ }
2185
+ const idemStore = activeJournal().store.idempotency;
2186
+ if (!idemStore) return invoke(validated);
2187
+ const scope = {
2188
+ tenant: fx.tenant.id ?? "",
2189
+ principal: policy.idempotencyPrincipal(fx.auth),
2190
+ flow: flowDef.name,
2191
+ key: classified,
2192
+ };
2193
+ const claimToken = crypto.randomUUID();
2194
+ const leaseMs = activeJournal().leaseMs;
2195
+ const claimed = await idemStore.claim({
2196
+ scope,
2197
+ fingerprint: policy.idempotencyFingerprint(flowDef.name, validated),
2198
+ claimToken,
2199
+ now: now(),
2200
+ leaseMs,
2201
+ ttlMs: idemAttempt.ttlMs,
2202
+ });
2203
+ if (claimed.kind === "mismatch") {
2204
+ return policy.idempotencyErrorResponse("IdempotencyKeyReused");
2205
+ }
2206
+ if (claimed.kind === "in_progress") {
2207
+ return policy.idempotencyErrorResponse(
2208
+ "IdempotencyInProgress",
2209
+ claimed.retryAfterSeconds,
2210
+ );
2211
+ }
2212
+ if (claimed.kind === "replay") return policy.replayResponse(claimed.row);
2213
+
2214
+ if (booted && flowDef.durable) {
2215
+ const journal = openJournal();
2216
+ const resumeId = claimed.kind === "reclaimed" ? claimed.row.runId : undefined;
2217
+ try {
2218
+ const session = resumeId
2219
+ ? await journal.resume(resumeId)
2220
+ : await journal.start(flowDef.name, validated);
2221
+ if (journalSlot) journalSlot.session = session;
2222
+ journalSession = session;
2223
+ inflightRuns.add(session.runId);
2224
+ const tenantId = principals?.tenant.id ?? fx.tenant.id;
2225
+ if (tenantId) await session.stampTenant(tenantId);
2226
+ await idemStore.attachRun(scope, claimToken, session.runId);
2227
+ } catch (err) {
2228
+ if (isJournalLeaseBusy(err)) {
2229
+ await idemStore.forfeit(scope, claimToken);
2230
+ return policy.idempotencyErrorResponse("IdempotencyInProgress", 1);
2231
+ }
2232
+ await idemStore.remove(scope, claimToken);
2233
+ throw err;
2234
+ }
2235
+ }
2236
+
2237
+ const timer = setInterval(
2238
+ () => {
2239
+ void idemStore.renew(scope, claimToken, now() + leaseMs);
2240
+ },
2241
+ Math.max(5, Math.floor(leaseMs / 3)),
2242
+ );
2243
+ if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
2244
+
2245
+ const storeEncoded = async (encoded: {
2246
+ readonly status: number;
2247
+ readonly headers: { get(name: string): string | null };
2248
+ text(): Promise<string>;
2249
+ }): Promise<void> => {
2250
+ const stored = await policy.storedFromResponse(encoded);
2251
+ if (!stored) {
2252
+ await idemStore.remove(scope, claimToken);
2253
+ return;
2254
+ }
2255
+ await idemStore.complete(scope, claimToken, stored, journalSession?.runId);
2256
+ };
2257
+ try {
2258
+ const output = await runDetached(() => invoke(validated));
2259
+ if (loadFx().isJsonStreamResult(output)) {
2260
+ await idemStore.remove(scope, claimToken);
2261
+ return output;
2262
+ }
2263
+ if (output instanceof Response) {
2264
+ const contentType = output.headers.get("content-type") ?? "";
2265
+ const buffered =
2266
+ output.status === 204 ||
2267
+ contentType.includes("json") ||
2268
+ contentType.startsWith("text/");
2269
+ if (!buffered || contentType.includes("text/event-stream")) {
2270
+ await idemStore.remove(scope, claimToken);
2271
+ return output;
2272
+ }
2273
+ await storeEncoded(output.clone());
2274
+ return output;
2275
+ }
2276
+ await storeEncoded(
2277
+ await encodeExecuteResult({
2278
+ output: isFlowFailure(output) ? undefined : output,
2279
+ failure: isFlowFailure(output) ? output : undefined,
2280
+ }),
2281
+ );
2282
+ return output;
2283
+ } catch (err) {
2284
+ if (flowDef.durable && isJournalSuspend(err)) {
2285
+ await idemStore.complete(
2286
+ scope,
2287
+ claimToken,
2288
+ { status: 204, body: "" },
2289
+ journalSession?.runId,
2290
+ );
2291
+ throw err;
2292
+ }
2293
+ if (policy.attemptedMutation(ledger.entries) || idemAttempt.usesRaw) {
2294
+ await storeEncoded(encodeFailure(fail("InternalError", {})));
2295
+ } else {
2296
+ await idemStore.remove(scope, claimToken);
2297
+ }
2298
+ throw err;
2299
+ } finally {
2300
+ clearInterval(timer);
2301
+ }
2302
+ };
2041
2303
  if (!alreadyValidated) {
2042
2304
  const parsed = await validate(flowDef.in, ctx.input);
2043
2305
  if (!parsed.ok) return parsed.failure;
2044
2306
  ctx.input = parsed.value;
2045
- return await invoke(parsed.value);
2307
+ return await runClaimed(parsed.value);
2046
2308
  }
2047
- return await invoke(ctx.input);
2309
+ return await runClaimed(ctx.input);
2048
2310
  } catch (err) {
2049
2311
  if (isFlowFailure(err)) return err;
2050
2312
  // A durable sleep that has not yet elapsed suspends the run — this
@@ -2084,6 +2346,14 @@ export function oke(options: OkeOptions): OkeApp {
2084
2346
  : durationMs;
2085
2347
  if (journalSession) {
2086
2348
  inflightRuns.delete(journalSession.runId);
2349
+ const parked = streamOut?.parked;
2350
+ if (parked) {
2351
+ ctx.state.sleeping = {
2352
+ wakeAt: parked.wakeAt,
2353
+ label: parked.label,
2354
+ runId: journalSession.runId,
2355
+ };
2356
+ }
2087
2357
  const sleeping = ctx.state.sleeping as
2088
2358
  | { readonly wakeAt: number; readonly label: string; readonly runId: string }
2089
2359
  | undefined;
@@ -2104,6 +2374,8 @@ export function oke(options: OkeOptions): OkeApp {
2104
2374
  fx,
2105
2375
  error: terminalErr,
2106
2376
  });
2377
+ } else if (parked && sleeping) {
2378
+ await journalSession.commit("sleeping", { wakeAt: sleeping.wakeAt });
2107
2379
  } else if (!sleeping) {
2108
2380
  await journalSession.commit("completed", {
2109
2381
  output: streamOut ? { streamed: true } : result.output,
@@ -2136,6 +2408,9 @@ export function oke(options: OkeOptions): OkeApp {
2136
2408
  ? { output: streamOut ? { streamed: true } : result.output }
2137
2409
  : {}),
2138
2410
  ...(parentId !== undefined ? { parentId } : {}),
2411
+ ...(trigger.kind === "http" && extras?.request
2412
+ ? { http: captureHttpFrame(extras.request, result.response) }
2413
+ : {}),
2139
2414
  },
2140
2415
  archiveCleartext,
2141
2416
  );
@@ -58,7 +58,7 @@ describe("automatic tier-1 cache from effects", () => {
58
58
  stores: [db],
59
59
  gate: { policies: [gate.public] },
60
60
  }).adopt({ list });
61
- const t = await createTestApp(app);
61
+ const t = await createTestApp(app, { capability: "open" });
62
62
  const notesApi = requireNotesApi(t);
63
63
 
64
64
  const first = await notesApi.list({});
@@ -90,7 +90,7 @@ describe("automatic tier-1 cache from effects", () => {
90
90
  stores: [db],
91
91
  gate: { policies: [gate.public] },
92
92
  }).adopt({ list });
93
- const t = await createTestApp(app);
93
+ const t = await createTestApp(app, { capability: "open" });
94
94
  const trigger = list.triggers[0];
95
95
  if (trigger === undefined) throw new Error("notes.list has no trigger");
96
96
 
@@ -128,7 +128,7 @@ describe("automatic tier-1 cache from effects", () => {
128
128
  stores: [db],
129
129
  gate: { policies: [gate.public] },
130
130
  }).adopt({ list });
131
- const t = await createTestApp(app);
131
+ const t = await createTestApp(app, { capability: "open" });
132
132
  const notesApi = requireNotesApi(t);
133
133
  await notesApi.list({});
134
134
  await notesApi.list({});
@@ -158,7 +158,7 @@ describe("automatic tier-1 cache from effects", () => {
158
158
  stores: [db],
159
159
  gate: { policies: [gate.public] },
160
160
  }).adopt({ list });
161
- const t = await createTestApp(app);
161
+ const t = await createTestApp(app, { capability: "open" });
162
162
  const notesApi = requireNotesApi(t);
163
163
 
164
164
  await notesApi.list({});
@@ -209,7 +209,7 @@ describe("automatic tier-1 cache from effects", () => {
209
209
  stores: [db],
210
210
  gate: { policies: [gate.public] },
211
211
  }).adopt({ list, create });
212
- const t = await createTestApp(app);
212
+ const t = await createTestApp(app, { capability: "open" });
213
213
  const notesApi = requireNotesApi(t);
214
214
 
215
215
  await notesApi.list({});
@@ -251,7 +251,7 @@ describe("automatic tier-1 cache from effects", () => {
251
251
  stores: [db],
252
252
  gate: { policies: [gate.public] },
253
253
  }).adopt({ list, create });
254
- const t = await createTestApp(app);
254
+ const t = await createTestApp(app, { capability: "open" });
255
255
  const notesApi = requireNotesApi(t);
256
256
 
257
257
  await notesApi.list({});
@@ -14,6 +14,7 @@ import type { AiDriver, AiOpenOptions } from "../../drivers/ai-types.ts";
14
14
  import { createAiRuntime, type AiRuntime } from "../../elements/ai.ts";
15
15
  import type { GateRuntime } from "../../elements/gate.ts";
16
16
  import type { VaultRuntime } from "../../elements/vault.ts";
17
+ import type { JournalStore } from "../journal.ts";
17
18
  import type { BootOptions } from "../boot.ts";
18
19
 
19
20
  /**
@@ -33,6 +34,7 @@ export function bindAi(
33
34
  env: ConfigEnv = "test",
34
35
  docker = false,
35
36
  vault?: VaultRuntime,
37
+ journalStore?: JournalStore,
36
38
  ): AiRuntime {
37
39
  const id = resolveAiDriverId(options, env, docker);
38
40
  const driver =
@@ -48,6 +50,7 @@ export function bindAi(
48
50
  },
49
51
  gates: options.ai?.gates ?? gate,
50
52
  now,
53
+ ...(journalStore ? { journalStore } : {}),
51
54
  });
52
55
  }
53
56
 
@@ -13,6 +13,7 @@ import {
13
13
  resolveIndexDriverId,
14
14
  resolveKvDriverId,
15
15
  resolveSqlDriverId,
16
+ sqlBindingForDecl,
16
17
  } from "./store.ts";
17
18
 
18
19
  describe("bindStore driver resolution", () => {
@@ -99,6 +100,31 @@ describe("bindStore driver resolution", () => {
99
100
  });
100
101
  });
101
102
 
103
+ describe("sqlBindingForDecl", () => {
104
+ test("pool.max and replica URLs land on the binding; min is ignored", () => {
105
+ const binding = sqlBindingForDecl("db", "postgres://primary/db", {
106
+ driver: "postgres",
107
+ pool: { max: 20, min: 2 },
108
+ replicas: ["postgres://ro-1/db", "postgres://ro-2/db"],
109
+ });
110
+ expect(binding).toEqual({
111
+ name: "db",
112
+ primary: { url: "postgres://primary/db", pool: { max: 20 } },
113
+ replicas: [
114
+ { url: "postgres://ro-1/db", pool: { max: 20 } },
115
+ { url: "postgres://ro-2/db", pool: { max: 20 } },
116
+ ],
117
+ });
118
+ });
119
+
120
+ test("a string pin keeps the URL and omits pool and replicas", () => {
121
+ expect(sqlBindingForDecl("db", "memory://", "memory")).toEqual({
122
+ name: "db",
123
+ primary: { url: "memory://" },
124
+ });
125
+ });
126
+ });
127
+
102
128
  describe("bindStore durable KV routing", () => {
103
129
  const prev = {
104
130
  redis: process.env.REDIS_URL,
@@ -2,7 +2,14 @@
2
2
  * Lazy store binder — loaded only when Store is declared.
3
3
  */
4
4
 
5
- import { resolveDomainDdlMode, resolveDriverId, type ConfigEnv } from "../../config/index.ts";
5
+ import {
6
+ driverRefId,
7
+ resolveDomainDdlMode,
8
+ resolveDriverId,
9
+ resolveDriverRef,
10
+ type ConfigEnv,
11
+ type DriverRef,
12
+ } from "../../config/index.ts";
6
13
  import {
7
14
  STORE_FILES_DEFAULTS,
8
15
  STORE_KV_DEFAULTS,
@@ -18,6 +25,7 @@ import { redisDriver } from "../../drivers/redis.ts";
18
25
  import { s3Driver } from "../../drivers/s3.ts";
19
26
  import type { FilesDriver, IndexDriver, KvDriver, SqlDriver } from "../../drivers/types.ts";
20
27
  import { createStoreRuntime, type StoreRuntime } from "../../elements/store.ts";
28
+ import type { SqlRuntimeBinding } from "../../elements/store/runtime.ts";
21
29
  import type { StoreDecl } from "../../elements/store/declare.ts";
22
30
  import { emitBootWarn } from "../../runtime/boot-warn.ts";
23
31
  import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runtime
@@ -129,17 +137,15 @@ export function bindStore(
129
137
  warnKvMemoryDurable(durableKvDecls.map((d) => d.name));
130
138
  }
131
139
 
132
- const sqlBindings: Record<string, { name: string; primary: { url: string } }> = {};
140
+ const sqlRef = resolveSqlDriverRef(options, env, docker);
141
+ const sqlBindings: Record<string, SqlRuntimeBinding> = {};
133
142
  const kvBindings: Record<string, { url?: string }> = {};
134
143
  const filesBindings: Record<string, { root?: string }> = {};
135
144
  const indexBindings: Record<string, { url?: string; apiKey?: string }> = {};
136
145
 
137
146
  for (const decl of options.stores ?? []) {
138
147
  if (isSqlDecl(decl)) {
139
- sqlBindings[decl.name] = {
140
- name: decl.name,
141
- primary: { url: sqlUrl },
142
- };
148
+ sqlBindings[decl.name] = sqlBindingForDecl(decl.name, sqlUrl, sqlRef);
143
149
  } else if (isKvDecl(decl)) {
144
150
  if (decl.durable === true) {
145
151
  kvBindings[decl.name] = {};
@@ -210,6 +216,76 @@ export function resolveSqlDriverId(options: BootOptions, env: ConfigEnv, docker:
210
216
  return resolveDriverId(options.config?.drivers?.store?.sql, env, STORE_SQL_DEFAULTS)!;
211
217
  }
212
218
 
219
+ /**
220
+ * Rich SQL driver ref for the active env. A docker `OKE_SQL_DRIVER` override
221
+ * that changes the protocol drops `pool` / `replicas` — those belong to the
222
+ * configured driver, not the override.
223
+ *
224
+ * @param options - Boot options
225
+ * @param env - Active environment
226
+ * @param docker - Docker mode
227
+ */
228
+ function resolveSqlDriverRef(
229
+ options: BootOptions,
230
+ env: ConfigEnv,
231
+ docker: boolean,
232
+ ): DriverRef | undefined {
233
+ const configured = resolveDriverRef(options.config?.drivers?.store?.sql, env, STORE_SQL_DEFAULTS);
234
+ const fromEnv = process.env.OKE_SQL_DRIVER?.trim();
235
+ if (docker && fromEnv && fromEnv !== driverRefId(configured)) return fromEnv;
236
+ return configured;
237
+ }
238
+
239
+ /**
240
+ * Primary URL plus documented `pool.max` and read-replica URLs.
241
+ *
242
+ * `pool.min` is ignored — Bun.SQL has no minimum pool size. Unset `max`
243
+ * leaves pool sizing to the postgres driver default of 8.
244
+ *
245
+ * @param name - Store declaration name
246
+ * @param url - Primary connection URL
247
+ * @param ref - Active driver pin (string or rich object)
248
+ */
249
+ export function sqlBindingForDecl(
250
+ name: string,
251
+ url: string,
252
+ ref: DriverRef | undefined,
253
+ ): SqlRuntimeBinding {
254
+ const max = driverPoolMax(ref);
255
+ const pool = max !== undefined ? { pool: { max } } : {};
256
+ const replicas = driverReplicaUrls(ref).map((replicaUrl) => ({
257
+ url: replicaUrl,
258
+ ...pool,
259
+ }));
260
+ return {
261
+ name,
262
+ primary: { url, ...pool },
263
+ ...(replicas.length > 0 ? { replicas } : {}),
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Positive finite `pool.max` from a rich driver ref.
269
+ *
270
+ * @param ref - Driver pin
271
+ */
272
+ function driverPoolMax(ref: DriverRef | undefined): number | undefined {
273
+ if (ref === undefined || typeof ref === "string") return undefined;
274
+ const max = ref.pool?.max;
275
+ if (typeof max !== "number" || !Number.isFinite(max) || max < 1) return undefined;
276
+ return Math.floor(max);
277
+ }
278
+
279
+ /**
280
+ * Replica connection strings from a rich driver ref. Non-strings are dropped.
281
+ *
282
+ * @param ref - Driver pin
283
+ */
284
+ function driverReplicaUrls(ref: DriverRef | undefined): readonly string[] {
285
+ if (ref === undefined || typeof ref === "string" || ref.replicas === undefined) return [];
286
+ return ref.replicas.filter((item): item is string => typeof item === "string" && item.length > 0);
287
+ }
288
+
213
289
  /**
214
290
  * @param options - Boot options
215
291
  * @param env - Active env