okengine 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) 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 +14 -10
  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/channel/index.mdx +4 -4
  13. package/site/content/docs/elements/channel/receipts.mdx +7 -5
  14. package/site/content/docs/elements/channel/sms.mdx +2 -1
  15. package/site/content/docs/elements/flow/index.mdx +22 -18
  16. package/site/content/docs/elements/gate/auth.mdx +4 -1
  17. package/site/content/docs/elements/signal/index.mdx +15 -14
  18. package/site/content/docs/elements/store/index.mdx +1 -1
  19. package/site/content/docs/elements/store/kv.mdx +5 -5
  20. package/site/content/docs/plugins/otp.mdx +3 -3
  21. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  22. package/site/content/docs/recipes/index.mdx +6 -6
  23. package/site/content/docs/recipes/postgres.mdx +12 -8
  24. package/site/content/docs/recipes/redis.mdx +4 -4
  25. package/site/content/docs/reference/cli.mdx +2 -1
  26. package/site/content/docs/reference/configuration.mdx +9 -7
  27. package/site/content/docs/reference/fx.mdx +19 -10
  28. package/src/auth/api-key-sql.test.ts +44 -0
  29. package/src/auth/api-key-sql.ts +1 -0
  30. package/src/auth/api-keys.ts +30 -12
  31. package/src/auth/config.ts +6 -2
  32. package/src/auth/gate-auth.test.ts +20 -0
  33. package/src/auth/tenants.ts +17 -0
  34. package/src/cli/agents-md.test.ts +25 -0
  35. package/src/cli/ask-vault-gaps.test.ts +38 -1
  36. package/src/cli/ask-vault-gaps.ts +35 -2
  37. package/src/cli/decide.ts +174 -0
  38. package/src/cli/decision-lock-watch.test.ts +45 -0
  39. package/src/cli/decision-lock-watch.ts +51 -0
  40. package/src/cli/dev-app-runner.ts +34 -8
  41. package/src/cli/dev-auth-secret.test.ts +49 -0
  42. package/src/cli/dev-auth-secret.ts +49 -0
  43. package/src/cli/dev-hot.test.ts +41 -0
  44. package/src/cli/dev-hot.ts +48 -0
  45. package/src/cli/dev.test.ts +1 -1
  46. package/src/cli/dev.ts +24 -6
  47. package/src/cli/doctor-diff.test.ts +29 -1
  48. package/src/cli/doctor-diff.ts +35 -0
  49. package/src/cli/eval.ts +142 -1
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/load-config.images.test.ts +2 -2
  52. package/src/cli/load-config.ts +1 -1
  53. package/src/cli/manifest-pr-diff.ts +16 -0
  54. package/src/cli/mcp-from-console.ts +48 -0
  55. package/src/cli/registry.ts +35 -1
  56. package/src/client/agent.ts +307 -0
  57. package/src/client/transport.test.ts +20 -0
  58. package/src/client/transport.ts +2 -1
  59. package/src/client/types.ts +1 -1
  60. package/src/client-react/index.ts +7 -1
  61. package/src/client-react/use-agent-run.test.ts +271 -0
  62. package/src/client-react/use-agent-run.ts +193 -0
  63. package/src/compiler/ai-approval.test.ts +36 -0
  64. package/src/compiler/ai-repair.test.ts +22 -0
  65. package/src/compiler/decisions.extract.test.ts +164 -0
  66. package/src/compiler/effects-fetch.test.ts +77 -0
  67. package/src/compiler/effects-infer.ts +114 -17
  68. package/src/compiler/effects-join.test.ts +48 -0
  69. package/src/compiler/extract.test.ts +28 -0
  70. package/src/compiler/extract.ts +279 -13
  71. package/src/compiler/fx-index.ts +58 -4
  72. package/src/compiler/response.ts +35 -2
  73. package/src/config/index.ts +21 -3
  74. package/src/console/index.ts +1 -1
  75. package/src/console/server/ai-runs-flows.ts +269 -0
  76. package/src/console/server/ai-runs.test.ts +323 -0
  77. package/src/console/server/ai-runs.ts +562 -0
  78. package/src/console/server/ai.ts +12 -1
  79. package/src/console/server/app.ts +10 -1
  80. package/src/console/server/decisions-flows.ts +157 -0
  81. package/src/console/server/decisions.test.ts +66 -0
  82. package/src/console/server/decisions.ts +229 -0
  83. package/src/console/server/flows.ts +50 -2
  84. package/src/console/server/invoke-user-flow.ts +9 -0
  85. package/src/console/server/runs-ingest.ts +16 -0
  86. package/src/console/server/serve.ts +5 -0
  87. package/src/console/server/state.ts +25 -1
  88. package/src/console/server/store-stats.test.ts +47 -0
  89. package/src/console/server/store-stats.ts +92 -18
  90. package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
  91. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
  92. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
  95. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  96. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  97. package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
  98. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  99. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  100. package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
  101. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  102. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  103. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  104. package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
  105. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
  107. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  108. package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
  109. package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
  110. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
  111. package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
  112. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  113. package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
  114. package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
  115. package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
  116. package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
  117. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  118. package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
  119. package/src/console/ui-next/dist/index.html +4 -3
  120. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  121. package/src/console/ui-next/src/client.ts +224 -1
  122. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  123. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  124. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  125. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  126. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  127. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  128. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  129. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  130. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  131. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  132. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  133. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  134. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  135. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  136. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  137. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  138. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  139. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  140. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  141. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  142. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  143. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  144. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  145. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  146. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  147. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  148. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  149. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  150. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  151. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  152. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  153. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  154. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  155. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  156. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  157. package/src/console/ui-next/src/router.tsx +17 -0
  158. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  159. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  160. package/src/console/ui-next/vite.config.ts +9 -0
  161. package/src/docker/recipes/dragonfly.ts +3 -2
  162. package/src/docker/recipes/redis.ts +1 -1
  163. package/src/drivers/ai-anthropic.ts +212 -21
  164. package/src/drivers/ai-mock.ts +34 -1
  165. package/src/drivers/ai-openai-compatible.ts +67 -26
  166. package/src/drivers/ai-preconnect.ts +18 -0
  167. package/src/drivers/ai-stream.test.ts +62 -0
  168. package/src/drivers/ai-types.ts +20 -0
  169. package/src/drivers/journal-postgres.test.ts +22 -1
  170. package/src/drivers/journal-postgres.ts +511 -13
  171. package/src/drivers/memory-ddl.test.ts +18 -0
  172. package/src/drivers/memory.ts +21 -1
  173. package/src/drivers/pg-rls.test.ts +31 -0
  174. package/src/drivers/pg-rls.ts +15 -0
  175. package/src/drivers/postgres.test.ts +9 -0
  176. package/src/drivers/postgres.ts +49 -11
  177. package/src/drivers/signal-compete.test.ts +234 -0
  178. package/src/drivers/signal-postgres.ts +39 -19
  179. package/src/drivers/signal-redis.ts +167 -11
  180. package/src/drivers/signal-types.ts +12 -0
  181. package/src/elements/ai/agent-event-slot.ts +27 -0
  182. package/src/elements/ai/approval-http.ts +207 -0
  183. package/src/elements/ai/approval.test.ts +741 -0
  184. package/src/elements/ai/approval.ts +228 -0
  185. package/src/elements/ai/decisions/bind.ts +163 -0
  186. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  187. package/src/elements/ai/decisions/certificate.ts +377 -0
  188. package/src/elements/ai/decisions/certify.ts +314 -0
  189. package/src/elements/ai/decisions/decide.test.ts +953 -0
  190. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  191. package/src/elements/ai/decisions/export.test.ts +120 -0
  192. package/src/elements/ai/decisions/export.ts +146 -0
  193. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  194. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  195. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  196. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  197. package/src/elements/ai/decisions/http.test.ts +154 -0
  198. package/src/elements/ai/decisions/http.ts +240 -0
  199. package/src/elements/ai/decisions/labels.test.ts +282 -0
  200. package/src/elements/ai/decisions/labels.ts +257 -0
  201. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  202. package/src/elements/ai/decisions/openrouter.ts +36 -0
  203. package/src/elements/ai/decisions/provider.ts +102 -0
  204. package/src/elements/ai/decisions/typesafe.ts +36 -0
  205. package/src/elements/ai/declare.ts +252 -1
  206. package/src/elements/ai/events.test.ts +379 -0
  207. package/src/elements/ai/events.ts +138 -0
  208. package/src/elements/ai/mcp-protocol.ts +5 -6
  209. package/src/elements/ai/run-events.test.ts +461 -0
  210. package/src/elements/ai/run-events.ts +522 -0
  211. package/src/elements/ai/runtime.ts +1187 -105
  212. package/src/elements/ai/stream-turn.ts +214 -0
  213. package/src/elements/ai/stream.live.test.ts +76 -0
  214. package/src/elements/ai/subagent.test.ts +136 -0
  215. package/src/elements/ai.test.ts +497 -3
  216. package/src/elements/ai.ts +16 -1
  217. package/src/elements/channel/runtime.ts +11 -0
  218. package/src/elements/channel/sql-ledger.ts +251 -0
  219. package/src/elements/clock/durable.ts +27 -1
  220. package/src/elements/signal/runtime.ts +9 -0
  221. package/src/elements/signal.test.ts +1 -0
  222. package/src/elements/store/cache.test.ts +120 -15
  223. package/src/elements/store/cache.ts +200 -39
  224. package/src/elements/store/prepare-row.test.ts +6 -2
  225. package/src/elements/store/sql-session.test.ts +53 -0
  226. package/src/elements/store/sql-session.ts +206 -10
  227. package/src/elements/store.ts +1 -0
  228. package/src/i18n/catalogs/ar.ts +5 -0
  229. package/src/i18n/catalogs/en.ts +5 -0
  230. package/src/index.ts +2 -0
  231. package/src/kernel/agent-event-store.ts +448 -0
  232. package/src/kernel/app.ts +135 -17
  233. package/src/kernel/auto-cache.test.ts +228 -0
  234. package/src/kernel/boot-bind/ai.ts +3 -0
  235. package/src/kernel/boot-bind/channel.ts +63 -2
  236. package/src/kernel/boot-bind/honor-config.test.ts +40 -10
  237. package/src/kernel/boot-bind/signal.ts +61 -25
  238. package/src/kernel/boot-bind/store.test.ts +26 -0
  239. package/src/kernel/boot-bind/store.ts +82 -6
  240. package/src/kernel/boot.ts +31 -6
  241. package/src/kernel/builtin-errors.ts +2 -0
  242. package/src/kernel/capability.ts +3 -0
  243. package/src/kernel/decision-label-store.ts +441 -0
  244. package/src/kernel/effects.test.ts +2 -1
  245. package/src/kernel/effects.ts +13 -1
  246. package/src/kernel/element-registries.ts +3 -0
  247. package/src/kernel/errors-compiler.ts +11 -0
  248. package/src/kernel/errors-text.ts +20 -0
  249. package/src/kernel/errors.ts +10 -0
  250. package/src/kernel/flow.test.ts +2 -3
  251. package/src/kernel/flow.ts +33 -3
  252. package/src/kernel/fx-call-types.test.ts +32 -0
  253. package/src/kernel/fx-decide.ts +765 -0
  254. package/src/kernel/fx-sql-handle.ts +46 -7
  255. package/src/kernel/fx.test.ts +13 -8
  256. package/src/kernel/fx.ts +339 -45
  257. package/src/kernel/http-frame.test.ts +94 -0
  258. package/src/kernel/http-frame.ts +203 -0
  259. package/src/kernel/idempotency.test.ts +2 -1
  260. package/src/kernel/journal.test.ts +114 -0
  261. package/src/kernel/journal.ts +471 -51
  262. package/src/kernel/json-result.ts +10 -0
  263. package/src/kernel/pipeline-tenant.ts +5 -1
  264. package/src/kernel/pipeline.test.ts +45 -0
  265. package/src/kernel/pipeline.ts +5 -0
  266. package/src/kernel/signal-tx.ts +28 -0
  267. package/src/kernel/sse-id.ts +32 -0
  268. package/src/kernel/store-transaction.test.ts +95 -0
  269. package/src/manifest/diff.ts +1 -0
  270. package/src/manifest/types.ts +29 -2
  271. package/src/mcp/ai-tools.test.ts +202 -0
  272. package/src/mcp/authorization.ts +66 -0
  273. package/src/mcp/docs-server.ts +19 -2
  274. package/src/mcp/mcp.test.ts +83 -0
  275. package/src/mcp/protocol.ts +8 -2
  276. package/src/mcp/server.ts +23 -2
  277. package/src/mcp/session.ts +3 -0
  278. package/src/mcp/tools.ts +168 -0
  279. package/src/mcp/versions.ts +60 -0
  280. package/src/plugins/index.ts +1 -0
  281. package/src/plugins/mena.ts +56 -0
  282. package/src/release/http-graph.test.ts +2 -0
  283. package/src/release/http-graph.ts +5 -0
  284. package/src/release/measure.ts +39 -14
  285. package/src/runs/collect.ts +4 -1
  286. package/src/runs/parquet.test.ts +27 -1
  287. package/src/runs/parquet.ts +19 -0
  288. package/src/runs/types.ts +31 -0
  289. package/src/runtime/json-code-block.test.ts +106 -47
  290. package/src/runtime/json-code-block.ts +1209 -46
  291. package/src/term.test.ts +13 -0
  292. package/src/term.ts +81 -49
  293. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  294. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  295. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  296. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  297. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  298. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  299. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  300. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  301. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  302. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  303. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  304. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  305. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  306. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  307. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  308. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  309. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  310. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  311. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  312. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -226,7 +226,7 @@ export default defineConfig({
226
226
  images: {
227
227
  store: {
228
228
  sql: "postgres:18-alpine",
229
- kv: "redis:8-alpine",
229
+ kv: "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
230
230
  files: "rustfs/rustfs:1.0.0",
231
231
  },
232
232
  },
@@ -544,11 +544,11 @@ writes (`set` · `delete`). Declare the same refs when you hand-write
544
544
 
545
545
  ## Drivers
546
546
 
547
- | Driver | Runs as | Best for |
548
- | -------------------------------------------- | -------------------------------- | ---------------------------------- |
549
- | `redis` | Docker Redis / Valkey-compatible | Dev + prod default |
550
- | `memory` | Process map | Test default |
551
- | Durable (`postgres` / `pglite` via `oke_kv`) | Shared SQL URL | Namespaces that must outlive Redis |
547
+ | Driver | Runs as | Best for |
548
+ | -------------------------------------------- | --------------------------------- | ---------------------------------- |
549
+ | `redis` | Dragonfly by default (Redis-wire) | Dev + prod default |
550
+ | `memory` | Process map | Test default |
551
+ | Durable (`postgres` / `pglite` via `oke_kv`) | Shared SQL URL | Namespaces that must outlive Redis |
552
552
 
553
553
  Defaults: `redis` / `memory` / `redis` (dev / test / prod). Pin overrides in
554
554
  `oke.config.ts`; image pins stay under `images.store.kv` — the driver id stays
@@ -22,9 +22,9 @@ app-owned delivery across the channels you declare.
22
22
  </Callout>
23
23
 
24
24
  <Callout type="info" title="fx.sendOtp vs otp()">
25
- `fx.sendOtp` / `fx.verifyOtp` are raw Channel capabilities — no `.plug()`. Direct use means you
26
- build routes, sessions, rates, and storage yourself. `otp()` provider mode wraps that path; skip
27
- it and call the raw methods in your own flow without losing the provider connection.
25
+ `fx.sendOtp` / `fx.verifyOtp` / `fx.deliverOtp` are supported Channel methods. Direct use means
26
+ you build routes, sessions, rates, and storage yourself. `otp()` provider mode wraps that path.
27
+ The `mena` plugin (`menaChannels`) is an additional way to open the same providers.
28
28
  </Callout>
29
29
 
30
30
  ## Quick start
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  title: "Dragonfly"
3
- description: "Multi-threaded Redis-wire runtime — OKE_STORE_KV_PASSWORD, memlock ulimit, no maxmemory-policy flag."
3
+ description: "Default store.kv image — multi-threaded Redis-wire runtime, memlock ulimit, OKE_STORE_KV_PASSWORD."
4
4
  icon: "Zap"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- Dragonfly is a multi-threaded, memory-efficient reimplementation of the Redis protocol —
9
- same `redis://` wire format, different engine. Slots behind the same `redis` driver with
10
- zero Flow changes.
8
+ Dragonfly is the default `store.kv` image in `dev` and `prod`: a multi-threaded Redis-protocol engine.
9
+ Same `redis://` wire format and the same `redis` driver, so Flows do not change.
10
+ Pin Redis or Valkey when you want those engines instead.
11
11
 
12
12
  <Callout title="The one rule">
13
13
  Driver id stays `redis`. Dragonfly is an image choice — every KV call through `fx.store` behaves
@@ -23,7 +23,7 @@ zero Flow changes.
23
23
 
24
24
  ```typescript title="oke.config.ts"
25
25
  images: {
26
- "store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly",
26
+ "store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
27
27
  },
28
28
  ```
29
29
 
@@ -51,15 +51,15 @@ Driver id stays `redis` for every image below.
51
51
 
52
52
  <Cards>
53
53
  <Card
54
- title="Redis"
55
- description="Default store.kv — requirepass + maxmemory."
56
- href="/docs/recipes/redis"
54
+ title="Dragonfly"
55
+ description="Default store.kv — multi-threaded Redis-wire runtime."
56
+ href="/docs/recipes/dragonfly"
57
57
  />
58
58
  <Card title="Valkey" description="BSD-licensed Redis-wire fork." href="/docs/recipes/valkey" />
59
59
  <Card
60
- title="Dragonfly"
61
- description="Multi-threaded Redis-wire runtime."
62
- href="/docs/recipes/dragonfly"
60
+ title="Redis"
61
+ description="Opt-in Redis Open Source — requirepass + maxmemory."
62
+ href="/docs/recipes/redis"
63
63
  />
64
64
  </Cards>
65
65
 
@@ -106,11 +106,11 @@ Scale out with [PgDog](/docs/recipes/pgdog) so `N × Bun.SQL pool` does not exha
106
106
  Recreate `store-sql` after a recipe change — the data volume can stay.
107
107
  </Callout>
108
108
 
109
- | Step | What you do |
110
- | ------- | ----------------------------------------------------------------- |
111
- | Preload | Recipe already sets `shared_preload_libraries=pg_stat_statements` |
112
- | Create | `CREATE EXTENSION IF NOT EXISTS pg_stat_statements` |
113
- | Console | Store → SQL band → **Performance** |
109
+ | Step | What you do |
110
+ | ------- | ---------------------------------------------------------------------- |
111
+ | Preload | Recipe already sets `shared_preload_libraries=pg_stat_statements` |
112
+ | Create | `oke db push` runs `CREATE EXTENSION IF NOT EXISTS pg_stat_statements` |
113
+ | Console | Store → SQL band → **Performance** |
114
114
 
115
115
  Default `postgres:18-alpine` does **not** ship `hypopg` / `index_advisor`. Pin the
116
116
  opt-in image when you want Suggest indexes:
@@ -137,8 +137,8 @@ set. Re-run `oke dev` so the stack writes `.env.local`, or export
137
137
  <Accordion title="PgStatStatementsNotPreloaded">
138
138
 
139
139
  The Console Performance view needs the library preloaded **and** the extension
140
- created. Recreate `store-sql` so the new `command` applies, then run
141
- `CREATE EXTENSION IF NOT EXISTS pg_stat_statements`.
140
+ created. Recreate `store-sql` so the new `command` applies, then run `oke db push`
141
+ (or open Performance again — it creates the extension when preload is already on).
142
142
 
143
143
  Existing clusters that started before this recipe change keep the old
144
144
  postmaster flags until recreate.
@@ -167,5 +167,9 @@ Postgres refusing auth.
167
167
  <Cards>
168
168
  <Card title="PgDog" description="Add pooling in front of Postgres." href="/docs/recipes/pgdog" />
169
169
  <Card title="Neon" description="A managed alternative." href="/docs/providers/neon" />
170
- <Card title="Redis" description="The default store.kv image." href="/docs/recipes/redis" />
170
+ <Card
171
+ title="Dragonfly"
172
+ description="The default store.kv image."
173
+ href="/docs/recipes/dragonfly"
174
+ />
171
175
  </Cards>
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  title: "Redis"
3
- description: "Default store.kv image — OKE_STORE_KV_PASSWORD, maxmemory policy, ephemeral data, REDIS_URL."
3
+ description: "Opt-in store.kv image — OKE_STORE_KV_PASSWORD, maxmemory policy, ephemeral data, REDIS_URL."
4
4
  icon: "Zap"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- Redis is the default `store.kv` image in `dev` and `prod`. `oke docker` matches image
9
- references containing `redis` or `keydb` and derives a password-required server — no
10
- unauthenticated instance.
8
+ Redis is an opt-in `store.kv` image. The default pin is [Dragonfly](/docs/recipes/dragonfly).
9
+ `oke docker` matches image references containing `redis` or `keydb` and derives a
10
+ password-required server — no unauthenticated instance.
11
11
 
12
12
  <Callout title="The one rule">
13
13
  The driver id stays `redis` for every Redis-wire image — Redis, Valkey, Dragonfly, or a managed
@@ -134,7 +134,8 @@ Override app / console / mcp listen ports in `oke.config.ts` `ports` — see
134
134
  | `docker` | Derive compose · clean leftover stacks | `--prod\|-p`, `--out`, `--config`, `--manifest`; sub `clean` |
135
135
  | `images` | List / pin digests | `list` · `pin` |
136
136
  | `build` | Tree-shaken bundle | `--target\|-t bun\|node\|edge`, `--entry`, `--outdir` |
137
- | `eval` | Prompt eval sets (CI gate) | `--manifest` |
137
+ | `eval` | Prompt eval sets (CI gate) | `--manifest`, `--certify` (decision seed certificate; prompt evals stay as they are) |
138
+ | `decide` | Write a decision lockfile from the operator candidate | `promote <name>` (`--origin`, `--lock`) |
138
139
  | `ai` | Configure AI driver + models | `setup` (`--provider`, `--chat`, `--vision`, `--embed`, `--yes`) |
139
140
  | `branch` | Fork journaled state | `<name>`, `--at\|-a` |
140
141
  | `replay` | Re-invoke a past Flow from Runs | `--request-id\|-r`, `--entry`, `--dry-run`, `--live` |
@@ -136,11 +136,13 @@ sql: {
136
136
  },
137
137
  ```
138
138
 
139
- | Field | Type | Meaning |
140
- | ---------- | ----------------- | ------------------------------------------- |
141
- | `url` | connection string | Overrides the env-var resolution |
142
- | `pool` | `{ max?, min? }` | SQL pool sizing |
143
- | `replicas` | `string[]` | Read-only routing targets (read flows only) |
139
+ | Field | Type | Meaning |
140
+ | ---------- | ----------------- | --------------------------------------------------------------------------------- |
141
+ | `url` | connection string | Overrides the env-var resolution |
142
+ | `pool` | `{ max?, min? }` | `max` is the Bun.SQL pool size. Unset keeps 8. `min` is accepted and not applied |
143
+ | `replicas` | `string[]` | Read-only flows use these URLs. Each replica opens its own pool at the same `max` |
144
+
145
+ A store `max` of 20 does not share a pool another part of the process already opened at 8. Writes stay on the primary.
144
146
 
145
147
  ## images
146
148
 
@@ -150,7 +152,7 @@ Nested pins by element role — vendor choice lives here, never in driver ids:
150
152
  images: {
151
153
  store: {
152
154
  sql: "postgres:18-alpine",
153
- kv: "redis:8-alpine",
155
+ kv: "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
154
156
  files: "rustfs/rustfs:1.0.0",
155
157
  },
156
158
  channel: { email: "axllent/mailpit:v1.31.2" },
@@ -161,7 +163,7 @@ images: {
161
163
 
162
164
  Omitted image keys mean no container for that role. When both `store.sql` and `pgdog` are pinned, `DATABASE_URL` points at PgDog — see [Store](/docs/elements/store#multiple-environments).
163
165
 
164
- For `store.kv`, pin Redis (default), Valkey, or Dragonfly — driver id stays `redis`.
166
+ For `store.kv`, pin Dragonfly (default), Redis, or Valkey — driver id stays `redis`.
165
167
  `{ durable: true }` KV lives in SQL (`oke_kv` on `DATABASE_URL`), not a second Redis image.
166
168
 
167
169
  Compose does not manage AI inference — [OpenRouter](/docs/recipes/openrouter) or BYO
@@ -158,7 +158,13 @@ on(
158
158
  | `fx.auth.upsertTenantRole({ tenantId, roleName, scopes })` | `write` `auth:tenants` | Application scopes only — `console:*` is unknown_scope |
159
159
 
160
160
  `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed) and propagates `fx.tenant.id`.
161
- Read `fx.principal` for audit — it does not copy into `fx.auth`.
161
+ Read `fx.principal` for audit — it does not copy into `fx.auth`. Passing a Flow handle types
162
+ `input` and the result. A string name stays `Promise<unknown>`.
163
+
164
+ `fx.store(db).transaction(async (tx) => { … })` pins one SQL connection. `fx.emit` inside the
165
+ callback stages on the signal outbox and publishes after commit. A throw rolls that batch back.
166
+ `fx.store(db).run(builder)` runs a Drizzle builder through `toSQL()`. Joins stay on that builder;
167
+ the hand-rolled chain does not grow a join API. Inference still records every joined table.
162
168
 
163
169
  <Callout>
164
170
  Gates never consult `fx.principal`. `return fx.fail(...)` is a `FlowFailure`; an unhandled `throw`
@@ -240,12 +246,13 @@ elements.
240
246
 
241
247
  ## AI
242
248
 
243
- | Signature | Records | Returns |
244
- | ----------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------- |
245
- | `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
246
- | `fx.run(agent, input?)` | `ask` | Agent result |
247
- | `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included) |
248
- | `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
249
+ | Signature | Records | Returns |
250
+ | ----------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
251
+ | `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
252
+ | `fx.run(agent, input?)` | `ask` | Agent result |
253
+ | `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included) |
254
+ | `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
255
+ | `fx.decide(decision, input)` | `decide` | `$.<question>.how` is `auto`, `reviewed`, or `abstained`. `audited` is a boolean flag, not a `how`. Abstain makes the value `null`. |
249
256
 
250
257
  AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them. `tools` are Flow refs — each model tool call goes through `fx.call` (same capability and Runs path).
251
258
 
@@ -280,9 +287,11 @@ Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400
280
287
 
281
288
  ## Cache
282
289
 
283
- Read-only flows cache automatically from inferred or ledgered `reads` — no
284
- `fx.cache` call and no `cache:` default on the flow. Writes invalidate those
285
- keys. Use `cache: false` to opt out, or `cache: "30s"` for a TTL.
290
+ Pure store-read flows cache by default. A send, emit, fetch, vault read, ask,
291
+ decide, or `fx.call` is never cached. The key includes tenant, locale, scopes,
292
+ and membership roles. Writes through `fx` invalidate those keys. `cache: false`
293
+ always disables. `oke({ cache: { auto: false } })` turns the default off.
294
+ Cross-instance invalidation and out-of-band writes are not covered.
286
295
 
287
296
  `fx.cache` is the manual (tier-3) surface:
288
297
 
@@ -44,4 +44,48 @@ describe("api-key SQL persist", () => {
44
44
  expect(other.keys.get("key_sql")?.creatorId).toBe("u1");
45
45
  expect(other.keys.get("key_sql")?.hash).toBe(created.row.hash);
46
46
  });
47
+
48
+ test("authenticate reloads an oke_ secret missing from memory", async () => {
49
+ const { authenticateApiKey } = await import("./api-keys.ts");
50
+ const rows = new Map<string, Record<string, unknown>>();
51
+ const sql: ApiKeySqlExec = {
52
+ async execute(_q, params) {
53
+ const id = String(params[0]);
54
+ rows.set(id, {
55
+ id,
56
+ plane: params[1],
57
+ hash: params[2],
58
+ name: params[3],
59
+ scopes: params[4],
60
+ expires_at: params[5],
61
+ rate_limit: params[6],
62
+ ip_allowlist: params[7],
63
+ creator_id: params[8],
64
+ creator_scopes: params[9],
65
+ created_at: params[10],
66
+ last_used_at: params[11],
67
+ revoked_at: params[12],
68
+ });
69
+ },
70
+ async all() {
71
+ return [...rows.values()];
72
+ },
73
+ };
74
+ const writer = createApiKeyStore({ pepper: "shared" });
75
+ bindApiKeySqlPersist(writer, sql);
76
+ const created = await createApiKey(writer, {
77
+ plane: "user",
78
+ name: "Auth Test",
79
+ scopes: ["member"],
80
+ creatorId: "user_admin",
81
+ creatorScopes: ["member"],
82
+ id: "key_live",
83
+ });
84
+ const reader = createApiKeyStore({ pepper: "shared" });
85
+ bindApiKeySqlPersist(reader, sql);
86
+ expect(reader.keys.size).toBe(0);
87
+ const row = await authenticateApiKey(reader, created.secret);
88
+ expect(row?.id).toBe("key_live");
89
+ expect(reader.keys.has("key_live")).toBe(true);
90
+ });
47
91
  });
@@ -97,6 +97,7 @@ export function bindApiKeySqlPersist(
97
97
  table: string = AUTH_TABLES.apiKeys,
98
98
  ): void {
99
99
  store.persist = (row) => persistApiKeyRow(sql, row, table);
100
+ store.reload = () => hydrateApiKeyStore(sql, store, table);
100
101
  }
101
102
 
102
103
  /** Runtime that can open the app's shared primary SQL connection. */
@@ -85,6 +85,11 @@ export interface ApiKeyStore {
85
85
  pepper?: string;
86
86
  /** Optional write-through persist (SQL). */
87
87
  persist?: (row: ApiKeyRow) => void | Promise<void>;
88
+ /**
89
+ * Reload rows from the durable table. `oke dev` keeps Console and the app
90
+ * in separate processes; a miss on an `oke_` secret refreshes this cache.
91
+ */
92
+ reload?: () => void | Promise<void>;
88
93
  /** Sliding-window hits for per-key rateLimit (subject = key id). */
89
94
  rateHits?: Map<string, number[]>;
90
95
  }
@@ -202,20 +207,33 @@ export async function authenticateApiKey(
202
207
  const now = options.now ?? (() => Date.now());
203
208
  const pepper = store.pepper ?? DEFAULT_PEPPER;
204
209
  const hash = await hashApiKeySecret(secret, pepper);
205
- for (const row of store.keys.values()) {
206
- if (row.hash !== hash) continue;
207
- if (row.revokedAt !== null) return null;
208
- if (row.expiresAt !== null && row.expiresAt <= now()) return null;
209
- if (row.ipAllowlist.length > 0) {
210
- const allowed = await allowlistAllowsIp(row.ipAllowlist, options.ip, options.lookup);
211
- if (!allowed) return null;
210
+ let row = matchApiKeyHash(store, hash);
211
+ if (!row && secret.startsWith("oke_") && store.reload) {
212
+ try {
213
+ await store.reload();
214
+ } catch {
215
+ // Durable read missed — the in-memory miss still stands.
212
216
  }
213
- if (row.rateLimit && !takeKeyRate(store, row.id, row.rateLimit, now())) return null;
214
- row.lastUsedAt = now();
215
- await persistRow(store, row);
216
- return row;
217
+ row = matchApiKeyHash(store, hash);
218
+ }
219
+ if (!row) return null;
220
+ if (row.revokedAt !== null) return null;
221
+ if (row.expiresAt !== null && row.expiresAt <= now()) return null;
222
+ if (row.ipAllowlist.length > 0) {
223
+ const allowed = await allowlistAllowsIp(row.ipAllowlist, options.ip, options.lookup);
224
+ if (!allowed) return null;
225
+ }
226
+ if (row.rateLimit && !takeKeyRate(store, row.id, row.rateLimit, now())) return null;
227
+ row.lastUsedAt = now();
228
+ await persistRow(store, row);
229
+ return row;
230
+ }
231
+
232
+ function matchApiKeyHash(store: ApiKeyStore, hash: string): ApiKeyRow | undefined {
233
+ for (const row of store.keys.values()) {
234
+ if (row.hash === hash) return row;
217
235
  }
218
- return null;
236
+ return undefined;
219
237
  }
220
238
 
221
239
  /**
@@ -4,6 +4,7 @@
4
4
  * Better Auth is inspiration for shape only; this is OKE-native.
5
5
  */
6
6
 
7
+ import { createEnv } from "../runtime/primitives.ts";
7
8
  import type { PasswordHashOptions } from "../runtime/types.ts";
8
9
  import type { SessionStore } from "./sessions.ts";
9
10
  import { createApiKeyStore, type ApiKeyStore } from "./api-keys.ts";
@@ -226,9 +227,12 @@ export function mintDevAuthSecret(): string {
226
227
  export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAuth {
227
228
  const { auth, env } = options;
228
229
  const isProd = env === "prod" || env === "production";
229
- let secret = auth.secret;
230
+ let secret = auth.secret?.trim() ?? "";
230
231
  let secretMinted = false;
231
- if (!secret || secret.length === 0) {
232
+ if (secret.length === 0) {
233
+ secret = createEnv().get("OKE_AUTH_SECRET")?.trim() ?? "";
234
+ }
235
+ if (secret.length === 0) {
232
236
  if (isProd) {
233
237
  throw new Error(
234
238
  "gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET)",
@@ -21,13 +21,33 @@ afterEach(() => {
21
21
  });
22
22
 
23
23
  describe("gate.auth — config", () => {
24
+ const prevAuthSecret = process.env.OKE_AUTH_SECRET;
25
+
26
+ afterEach(() => {
27
+ if (prevAuthSecret === undefined) delete process.env.OKE_AUTH_SECRET;
28
+ else process.env.OKE_AUTH_SECRET = prevAuthSecret;
29
+ });
30
+
24
31
  test("prod without secret fails", () => {
32
+ delete process.env.OKE_AUTH_SECRET;
25
33
  expect(() =>
26
34
  resolveGateAuth({ auth: { emailAndPassword: { enabled: true } }, env: "prod" }),
27
35
  ).toThrow(/secret is required/);
28
36
  });
29
37
 
38
+ test("OKE_AUTH_SECRET is the pepper when gate.auth.secret is omitted", () => {
39
+ process.env.OKE_AUTH_SECRET = "shared-dev-pepper";
40
+ const resolved = resolveGateAuth({
41
+ auth: { emailAndPassword: { enabled: true } },
42
+ env: "dev",
43
+ });
44
+ expect(resolved.secret).toBe("shared-dev-pepper");
45
+ expect(resolved.secretMinted).toBe(false);
46
+ expect(resolved.apiKeyStore.pepper).toBe("shared-dev-pepper");
47
+ });
48
+
30
49
  test("dev mints secret", () => {
50
+ delete process.env.OKE_AUTH_SECRET;
31
51
  const resolved = resolveGateAuth({
32
52
  auth: { emailAndPassword: { enabled: true } },
33
53
  env: "dev",
@@ -359,3 +359,20 @@ export function tenantScopesForMember(
359
359
  const role = store.roles.get(roleKey(tenantId, member.role));
360
360
  return role?.scopes ?? [];
361
361
  }
362
+
363
+ /**
364
+ * Membership role name for the tier-1 cache key.
365
+ *
366
+ * Same member lookup as {@link tenantScopesForMember}. Not an authorization input.
367
+ *
368
+ * @param store - Tenant store
369
+ * @param tenantId - Tenant id
370
+ * @param userId - User id
371
+ */
372
+ export function tenantRoleForMember(
373
+ store: TenantStore,
374
+ tenantId: string,
375
+ userId: string,
376
+ ): string | null {
377
+ return getMember(store, tenantId, userId)?.role ?? null;
378
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Commands named in the agent contract must exist on the CLI.
3
+ */
4
+
5
+ import { readFileSync } from "node:fs";
6
+ import { describe, expect, test } from "bun:test";
7
+ import { commandNames } from "./registry.ts";
8
+
9
+ describe("AGENTS.md commands", () => {
10
+ test("every oke command mentioned in AGENTS.md is registered", () => {
11
+ const text = readFileSync(new URL("../../AGENTS.md", import.meta.url), "utf8");
12
+ const registered = new Set(commandNames());
13
+ const mentioned = new Set<string>();
14
+ for (const match of text.matchAll(/`oke ([a-z][a-z0-9-]*)/g)) {
15
+ const name = match[1];
16
+ if (name) mentioned.add(name);
17
+ }
18
+ expect(mentioned.has("eval")).toBe(true);
19
+ expect(mentioned.has("decide")).toBe(true);
20
+ expect(text).not.toContain("oke decide certify");
21
+ for (const name of mentioned) {
22
+ expect(registered.has(name)).toBe(true);
23
+ }
24
+ });
25
+ });
@@ -3,12 +3,14 @@
3
3
  */
4
4
 
5
5
  import { afterEach, describe, expect, test } from "bun:test";
6
+ import { EventEmitter } from "node:events";
6
7
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
8
  import { tmpdir } from "node:os";
8
9
  import { join, resolve } from "node:path";
9
10
  import { resetRequiredEnvNames, resetSecrets } from "../elements/vault/declare.ts";
10
11
  import { resetBindings } from "../kernel/on.ts";
11
- import { maybeAskVaultGaps, probeVaultGaps } from "./ask-vault-gaps.ts";
12
+ import { maybeAskVaultGaps, probeVaultGaps, secretPromptWrite } from "./ask-vault-gaps.ts";
13
+ import { promptHidden } from "./vault-secure-input.ts";
12
14
 
13
15
  const OKE = resolve(import.meta.dir, "../index.ts");
14
16
 
@@ -93,6 +95,41 @@ describe("maybeAskVaultGaps", () => {
93
95
  });
94
96
  });
95
97
 
98
+ describe("secretPromptWrite", () => {
99
+ test("keeps the mask on the prompt line", async () => {
100
+ const stdin = new EventEmitter() as EventEmitter & {
101
+ setRawMode?: (mode: boolean) => void;
102
+ off?: (event: string, listener: (...args: unknown[]) => void) => EventEmitter;
103
+ };
104
+ stdin.setRawMode = () => undefined;
105
+ stdin.off = (event: string | symbol, listener: (...args: unknown[]) => void) => {
106
+ stdin.removeListener(event, listener);
107
+ return stdin;
108
+ };
109
+ const shown: string[] = [];
110
+ const echoed: string[] = [];
111
+ const pending = promptHidden("Enter value for OPENROUTER_API_KEY: ", {
112
+ stdin,
113
+ write: secretPromptWrite(
114
+ (text) => shown.push(text),
115
+ (text) => echoed.push(text),
116
+ ),
117
+ exit: () => {
118
+ throw new Error("exit should not run");
119
+ },
120
+ });
121
+ await Promise.resolve();
122
+ stdin.emit("data", "sk");
123
+ stdin.emit("data", "\r");
124
+ expect(await pending).toBe("sk");
125
+ expect(shown).toHaveLength(1);
126
+ expect(shown[0]).toContain("Enter value for OPENROUTER_API_KEY: ");
127
+ expect(shown[0]?.endsWith("\n")).toBe(false);
128
+ expect(echoed.join("")).toBe("**\n");
129
+ expect(shown.join("")).not.toContain("*");
130
+ });
131
+ });
132
+
96
133
  describe("probeVaultGaps", () => {
97
134
  test("sees secrets on oke({ secrets }) after consume drains the registry", async () => {
98
135
  const dir = mkdtempSync(join(tmpdir(), "probe-vault-"));
@@ -14,7 +14,7 @@ import { buildVaultBootChain, normalizeVaultDriverId } from "../elements/vault/b
14
14
  import { createVaultRuntime, VaultBootError, type VaultGap } from "../elements/vault/runtime.ts";
15
15
  import type { OkeOptions } from "../kernel/app.ts";
16
16
  import { requiredEnvRegistry, secretRegistry } from "../kernel/element-registries.ts";
17
- import { formatCliChrome } from "../term.ts";
17
+ import { formatCliChrome, formatStatusLine } from "../term.ts";
18
18
  import { loadOkeConfig } from "./load-config.ts";
19
19
  import { openEnvStore } from "./vault-cmd.ts";
20
20
  import { promptHidden } from "./vault-secure-input.ts";
@@ -147,6 +147,35 @@ function unionSecrets(
147
147
  return out;
148
148
  }
149
149
 
150
+ /**
151
+ * Writer for {@link promptHidden} during `oke dev`.
152
+ *
153
+ * The question is one chrome row with no trailing newline, so mask
154
+ * characters stay on that row. Echo (`*`, backspace, Enter) goes straight
155
+ * to stdout: `formatCliChrome` would turn each `*` into its own `│` line,
156
+ * and the anchored board would count each of those writes as a new row.
157
+ *
158
+ * @param sink - Line-oriented stdout (the dev board wrapper)
159
+ * @param echo - Raw echo. Defaults to `process.stdout.write`
160
+ */
161
+ export function secretPromptWrite(
162
+ sink: (text: string) => void,
163
+ echo: (text: string) => void = (text) => {
164
+ process.stdout.write(text);
165
+ },
166
+ ): (text: string) => void {
167
+ let opened = false;
168
+ return (text: string) => {
169
+ if (!opened) {
170
+ opened = true;
171
+ const line = formatStatusLine(text);
172
+ sink(line.endsWith("\n") ? line.slice(0, -1) : line);
173
+ return;
174
+ }
175
+ echo(text);
176
+ };
177
+ }
178
+
150
179
  /**
151
180
  * After compose hydrate (and usually schema push), fill Vault boot gaps
152
181
  * one-by-one into `.env.local`. Skips when non-TTY or no gaps.
@@ -175,7 +204,11 @@ export async function maybeAskVaultGaps(options: AskVaultGapsOptions): Promise<n
175
204
  const envPath = resolve(cwd, options.envFile ?? ".env.local");
176
205
  const store = await openEnvStore(envPath);
177
206
  const read =
178
- options.readSecret ?? ((prompt: string) => promptHidden(prompt, { write: chromeWrite }));
207
+ options.readSecret ??
208
+ ((prompt: string) =>
209
+ promptHidden(prompt, {
210
+ write: secretPromptWrite(write),
211
+ }));
179
212
 
180
213
  const remaining: VaultGap[] = [];
181
214
  for (const gap of gaps) {