okengine 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +13 -9
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/gate/auth.mdx +4 -1
  13. package/site/content/docs/elements/store/index.mdx +1 -1
  14. package/site/content/docs/elements/store/kv.mdx +5 -5
  15. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  16. package/site/content/docs/recipes/index.mdx +6 -6
  17. package/site/content/docs/recipes/postgres.mdx +12 -8
  18. package/site/content/docs/recipes/redis.mdx +4 -4
  19. package/site/content/docs/reference/cli.mdx +2 -1
  20. package/site/content/docs/reference/configuration.mdx +9 -7
  21. package/site/content/docs/reference/fx.mdx +7 -6
  22. package/src/auth/api-key-sql.test.ts +44 -0
  23. package/src/auth/api-key-sql.ts +1 -0
  24. package/src/auth/api-keys.ts +30 -12
  25. package/src/auth/config.ts +6 -2
  26. package/src/auth/gate-auth.test.ts +20 -0
  27. package/src/cli/agents-md.test.ts +25 -0
  28. package/src/cli/ask-vault-gaps.test.ts +38 -1
  29. package/src/cli/ask-vault-gaps.ts +35 -2
  30. package/src/cli/decide.ts +174 -0
  31. package/src/cli/decision-lock-watch.test.ts +45 -0
  32. package/src/cli/decision-lock-watch.ts +51 -0
  33. package/src/cli/dev-app-runner.ts +34 -8
  34. package/src/cli/dev-auth-secret.test.ts +49 -0
  35. package/src/cli/dev-auth-secret.ts +49 -0
  36. package/src/cli/dev-hot.test.ts +41 -0
  37. package/src/cli/dev-hot.ts +48 -0
  38. package/src/cli/dev.test.ts +1 -1
  39. package/src/cli/dev.ts +24 -6
  40. package/src/cli/eval.ts +142 -1
  41. package/src/cli/index.ts +5 -0
  42. package/src/cli/load-config.images.test.ts +2 -2
  43. package/src/cli/load-config.ts +1 -1
  44. package/src/cli/mcp-from-console.ts +48 -0
  45. package/src/cli/registry.ts +35 -1
  46. package/src/client/agent.ts +307 -0
  47. package/src/client/transport.test.ts +20 -0
  48. package/src/client/transport.ts +2 -1
  49. package/src/client/types.ts +1 -1
  50. package/src/client-react/index.ts +7 -1
  51. package/src/client-react/use-agent-run.test.ts +271 -0
  52. package/src/client-react/use-agent-run.ts +193 -0
  53. package/src/compiler/ai-approval.test.ts +36 -0
  54. package/src/compiler/ai-repair.test.ts +22 -0
  55. package/src/compiler/decisions.extract.test.ts +164 -0
  56. package/src/compiler/effects-infer.ts +27 -1
  57. package/src/compiler/extract.ts +268 -12
  58. package/src/compiler/response.ts +35 -2
  59. package/src/config/index.ts +21 -3
  60. package/src/console/server/ai-runs-flows.ts +269 -0
  61. package/src/console/server/ai-runs.test.ts +323 -0
  62. package/src/console/server/ai-runs.ts +562 -0
  63. package/src/console/server/ai.ts +12 -1
  64. package/src/console/server/app.ts +10 -1
  65. package/src/console/server/decisions-flows.ts +157 -0
  66. package/src/console/server/decisions.test.ts +66 -0
  67. package/src/console/server/decisions.ts +229 -0
  68. package/src/console/server/flows.ts +50 -2
  69. package/src/console/server/invoke-user-flow.ts +9 -0
  70. package/src/console/server/runs-ingest.ts +16 -0
  71. package/src/console/server/serve.ts +5 -0
  72. package/src/console/server/state.ts +25 -1
  73. package/src/console/server/store-stats.test.ts +47 -0
  74. package/src/console/server/store-stats.ts +92 -18
  75. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  76. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
  80. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  81. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  82. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  83. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  84. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  85. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  86. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  87. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  88. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  89. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  90. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
  92. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  93. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  94. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  95. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
  96. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  97. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  98. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  99. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  100. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  101. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  102. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  103. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  104. package/src/console/ui-next/dist/index.html +4 -3
  105. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  106. package/src/console/ui-next/src/client.ts +224 -1
  107. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  108. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  109. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  110. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  111. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  112. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  113. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  114. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  115. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  116. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  117. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  118. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  119. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  120. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  121. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  122. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  123. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  124. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  125. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  126. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  127. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  128. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  129. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  130. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  131. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  132. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  133. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  134. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  135. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  137. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  138. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  139. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  140. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  141. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  142. package/src/console/ui-next/src/router.tsx +17 -0
  143. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  144. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  145. package/src/console/ui-next/vite.config.ts +9 -0
  146. package/src/docker/recipes/dragonfly.ts +3 -2
  147. package/src/docker/recipes/redis.ts +1 -1
  148. package/src/drivers/ai-anthropic.ts +212 -21
  149. package/src/drivers/ai-mock.ts +34 -1
  150. package/src/drivers/ai-openai-compatible.ts +67 -26
  151. package/src/drivers/ai-preconnect.ts +18 -0
  152. package/src/drivers/ai-stream.test.ts +62 -0
  153. package/src/drivers/ai-types.ts +20 -0
  154. package/src/drivers/journal-postgres.ts +289 -2
  155. package/src/drivers/memory-ddl.test.ts +18 -0
  156. package/src/drivers/memory.ts +21 -1
  157. package/src/drivers/pg-rls.test.ts +31 -0
  158. package/src/drivers/pg-rls.ts +15 -0
  159. package/src/drivers/postgres.test.ts +9 -0
  160. package/src/drivers/postgres.ts +49 -11
  161. package/src/elements/ai/agent-event-slot.ts +27 -0
  162. package/src/elements/ai/approval-http.ts +207 -0
  163. package/src/elements/ai/approval.test.ts +741 -0
  164. package/src/elements/ai/approval.ts +228 -0
  165. package/src/elements/ai/decisions/bind.ts +163 -0
  166. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  167. package/src/elements/ai/decisions/certificate.ts +377 -0
  168. package/src/elements/ai/decisions/certify.ts +314 -0
  169. package/src/elements/ai/decisions/decide.test.ts +953 -0
  170. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  171. package/src/elements/ai/decisions/export.test.ts +120 -0
  172. package/src/elements/ai/decisions/export.ts +146 -0
  173. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  174. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  175. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  176. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  177. package/src/elements/ai/decisions/http.test.ts +154 -0
  178. package/src/elements/ai/decisions/http.ts +240 -0
  179. package/src/elements/ai/decisions/labels.test.ts +282 -0
  180. package/src/elements/ai/decisions/labels.ts +257 -0
  181. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  182. package/src/elements/ai/decisions/openrouter.ts +36 -0
  183. package/src/elements/ai/decisions/provider.ts +102 -0
  184. package/src/elements/ai/decisions/typesafe.ts +36 -0
  185. package/src/elements/ai/declare.ts +252 -1
  186. package/src/elements/ai/events.test.ts +379 -0
  187. package/src/elements/ai/events.ts +138 -0
  188. package/src/elements/ai/run-events.test.ts +461 -0
  189. package/src/elements/ai/run-events.ts +522 -0
  190. package/src/elements/ai/runtime.ts +1186 -105
  191. package/src/elements/ai/stream-turn.ts +214 -0
  192. package/src/elements/ai/stream.live.test.ts +76 -0
  193. package/src/elements/ai/subagent.test.ts +136 -0
  194. package/src/elements/ai.test.ts +497 -3
  195. package/src/elements/ai.ts +16 -1
  196. package/src/elements/clock/durable.ts +27 -1
  197. package/src/elements/store/sql-session.test.ts +51 -0
  198. package/src/elements/store/sql-session.ts +111 -1
  199. package/src/i18n/catalogs/ar.ts +5 -0
  200. package/src/i18n/catalogs/en.ts +5 -0
  201. package/src/index.ts +2 -0
  202. package/src/kernel/agent-event-store.ts +448 -0
  203. package/src/kernel/app.ts +103 -1
  204. package/src/kernel/boot-bind/ai.ts +3 -0
  205. package/src/kernel/boot-bind/store.test.ts +26 -0
  206. package/src/kernel/boot-bind/store.ts +82 -6
  207. package/src/kernel/boot.ts +15 -4
  208. package/src/kernel/builtin-errors.ts +2 -0
  209. package/src/kernel/capability.ts +3 -0
  210. package/src/kernel/decision-label-store.ts +441 -0
  211. package/src/kernel/effects.test.ts +2 -1
  212. package/src/kernel/effects.ts +13 -1
  213. package/src/kernel/element-registries.ts +3 -0
  214. package/src/kernel/errors-text.ts +4 -0
  215. package/src/kernel/errors.ts +2 -0
  216. package/src/kernel/flow.test.ts +2 -3
  217. package/src/kernel/fx-decide.ts +761 -0
  218. package/src/kernel/fx.test.ts +12 -5
  219. package/src/kernel/fx.ts +312 -29
  220. package/src/kernel/http-frame.test.ts +94 -0
  221. package/src/kernel/http-frame.ts +203 -0
  222. package/src/kernel/journal.ts +104 -0
  223. package/src/kernel/json-result.ts +10 -0
  224. package/src/kernel/pipeline.test.ts +45 -0
  225. package/src/kernel/sse-id.ts +32 -0
  226. package/src/manifest/diff.ts +1 -0
  227. package/src/manifest/types.ts +29 -2
  228. package/src/mcp/ai-tools.test.ts +202 -0
  229. package/src/mcp/authorization.ts +66 -0
  230. package/src/mcp/session.ts +3 -0
  231. package/src/mcp/tools.ts +168 -0
  232. package/src/release/http-graph.test.ts +2 -0
  233. package/src/release/http-graph.ts +5 -0
  234. package/src/release/measure.ts +39 -14
  235. package/src/runs/collect.ts +4 -1
  236. package/src/runs/parquet.test.ts +27 -1
  237. package/src/runs/parquet.ts +19 -0
  238. package/src/runs/types.ts +31 -0
  239. package/src/runtime/json-code-block.test.ts +106 -47
  240. package/src/runtime/json-code-block.ts +1209 -46
  241. package/src/term.test.ts +13 -0
  242. package/src/term.ts +81 -49
  243. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  244. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  245. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  246. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  247. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  248. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  249. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  250. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  251. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  252. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  253. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  254. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  255. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  256. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  257. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  258. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  259. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  260. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  261. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  262. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -21,6 +21,8 @@
21
21
 
22
22
  import { resolve } from "node:path";
23
23
  import { pathToFileURL } from "node:url";
24
+ import { closeSharedPostgresClients } from "../drivers/postgres.ts";
25
+ import { installDevHotGeneration, takeDevHotGeneration } from "./dev-hot.ts";
24
26
  import { installGracefulShutdown, type GracefulShutdownApp } from "../kernel/graceful-shutdown.ts";
25
27
  import type { Manifest } from "../manifest/types.ts";
26
28
  import { createBunRuntime } from "../runtime/bun.ts";
@@ -67,22 +69,36 @@ if (manifestPath !== undefined && manifestPath.length > 0) {
67
69
  }
68
70
  }
69
71
 
72
+ // Soft reload re-enters this file in the same process. Stop the previous
73
+ // boot's 1s scheduler and close its Bun.SQL pools before opening new ones.
74
+ // Holder `close()` is a no-op on the shared pool, so this has to be explicit.
75
+ const previous = takeDevHotGeneration();
76
+ if (previous) {
77
+ try {
78
+ await previous.dispose();
79
+ } catch (err) {
80
+ console.error(
81
+ `oke dev: previous hot generation failed to dispose — ${
82
+ err instanceof Error ? err.message : String(err)
83
+ }`,
84
+ );
85
+ }
86
+ }
87
+ await closeSharedPostgresClients();
88
+
70
89
  const absoluteEntry = resolve(entry);
71
90
  const mod = (await import(pathToFileURL(absoluteEntry).href)) as {
72
91
  app?: BootableApp;
73
92
  };
93
+ const app = mod.app;
74
94
 
75
- if (
76
- mod.app === undefined ||
77
- typeof mod.app.boot !== "function" ||
78
- typeof mod.app.fetch !== "function"
79
- ) {
95
+ if (app === undefined || typeof app.boot !== "function" || typeof app.fetch !== "function") {
80
96
  console.error(`oke dev: ${absoluteEntry} must export app with boot() and fetch()`);
81
97
  process.exit(1);
82
98
  }
83
99
 
84
100
  try {
85
- await mod.app.boot({
101
+ await app.boot({
86
102
  rootDir: process.env["OKE_ROOT_DIR"],
87
103
  ...(manifest !== undefined ? { manifest } : {}),
88
104
  });
@@ -94,12 +110,22 @@ try {
94
110
  console.error(formatFatalError(err));
95
111
  process.exit(1);
96
112
  }
97
- const handle = createBunRuntime().serve(mod.app, {
113
+ const handle = createBunRuntime().serve(app, {
98
114
  port,
99
115
  hostname,
100
116
  id: DEV_APP_SERVE_ID,
101
117
  });
102
- installGracefulShutdown({ app: mod.app, handle });
118
+ const { watchDecisionLockfile } = await import("./decision-lock-watch.ts");
119
+ const decisionLockWatch = watchDecisionLockfile(process.env.OKE_ROOT_DIR ?? process.cwd());
120
+ const removeSignals = installGracefulShutdown({ app, handle });
121
+ installDevHotGeneration({
122
+ async dispose() {
123
+ decisionLockWatch.close();
124
+ removeSignals();
125
+ await app.stop();
126
+ await closeSharedPostgresClients();
127
+ },
128
+ });
103
129
 
104
130
  const appUrl = `http://${formatHostForUrl(hostname)}:${handle.port}`;
105
131
  // Parent board (claim + Docker) stays. Bun `--no-clear-screen` + no child wipe.
@@ -0,0 +1,49 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { ensureDevAuthSecret } from "./dev-auth-secret.ts";
6
+
7
+ describe("ensureDevAuthSecret", () => {
8
+ const dirs: string[] = [];
9
+ const prev = process.env.OKE_AUTH_SECRET;
10
+
11
+ afterEach(async () => {
12
+ if (prev === undefined) delete process.env.OKE_AUTH_SECRET;
13
+ else process.env.OKE_AUTH_SECRET = prev;
14
+ await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
15
+ });
16
+
17
+ test("reuses process.env and does not write a file", async () => {
18
+ process.env.OKE_AUTH_SECRET = "from-env";
19
+ const dir = await mkdtemp(join(tmpdir(), "oke-auth-secret-"));
20
+ dirs.push(dir);
21
+ expect(await ensureDevAuthSecret(dir)).toBe("from-env");
22
+ expect(await Bun.file(join(dir, ".env.local")).exists()).toBe(false);
23
+ });
24
+
25
+ test("reads .env.local before minting", async () => {
26
+ delete process.env.OKE_AUTH_SECRET;
27
+ const dir = await mkdtemp(join(tmpdir(), "oke-auth-secret-"));
28
+ dirs.push(dir);
29
+ await Bun.write(
30
+ join(dir, ".env.local"),
31
+ "DATABASE_URL=postgres://localhost/oke\nOKE_AUTH_SECRET=from-file\n",
32
+ );
33
+ expect(await ensureDevAuthSecret(dir)).toBe("from-file");
34
+ });
35
+
36
+ test("mints once and appends to .env.local", async () => {
37
+ delete process.env.OKE_AUTH_SECRET;
38
+ const dir = await mkdtemp(join(tmpdir(), "oke-auth-secret-"));
39
+ dirs.push(dir);
40
+ await Bun.write(join(dir, ".env.local"), "DATABASE_URL=postgres://localhost/oke\n");
41
+ const first = await ensureDevAuthSecret(dir);
42
+ expect(first.startsWith("oke_dev_")).toBe(true);
43
+ const text = await readFile(join(dir, ".env.local"), "utf8");
44
+ expect(text).toContain(`OKE_AUTH_SECRET=${first}`);
45
+ expect(text.startsWith("DATABASE_URL=")).toBe(true);
46
+ delete process.env.OKE_AUTH_SECRET;
47
+ expect(await ensureDevAuthSecret(dir)).toBe(first);
48
+ });
49
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * One HMAC secret for `oke dev`.
3
+ *
4
+ * Console boots the app in the parent; the HTTP server is a child. Each
5
+ * process would otherwise mint its own `gate.auth` secret, and a Bearer API
6
+ * key hashed in Console would not verify on the app. This stores one
7
+ * `OKE_AUTH_SECRET` in `.env.local` and returns it for both.
8
+ */
9
+
10
+ import { join } from "node:path";
11
+ import { mintDevAuthSecret } from "../auth/config.ts";
12
+ import { escapeDotenvValue, parseDotenv } from "../drivers/vault-dotenv-parse.ts";
13
+
14
+ /**
15
+ * Resolve the shared dev auth secret.
16
+ *
17
+ * Order: `process.env.OKE_AUTH_SECRET`, then `.env.local`, then a minted
18
+ * value appended to `.env.local`. Does not write `process.env`.
19
+ *
20
+ * @param cwd - Project root
21
+ */
22
+ export async function ensureDevAuthSecret(cwd: string): Promise<string> {
23
+ const current = process.env.OKE_AUTH_SECRET?.trim();
24
+ if (current) return current;
25
+
26
+ const envPath = join(cwd, ".env.local");
27
+ let text = "";
28
+ const file = Bun.file(envPath);
29
+ if (await file.exists()) {
30
+ try {
31
+ text = await file.text();
32
+ } catch {
33
+ text = "";
34
+ }
35
+ }
36
+ const fromFile = parseDotenv(text).get("OKE_AUTH_SECRET")?.trim();
37
+ if (fromFile) return fromFile;
38
+
39
+ const secret = mintDevAuthSecret();
40
+ const line = `OKE_AUTH_SECRET=${escapeDotenvValue(secret)}`;
41
+ const next =
42
+ text.length === 0
43
+ ? `${line}\n`
44
+ : text.endsWith("\n")
45
+ ? `${text}${line}\n`
46
+ : `${text}\n${line}\n`;
47
+ await Bun.write(envPath, next);
48
+ return secret;
49
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Soft-reload slot: one live generation, disposed before the next boot.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { installDevHotGeneration, takeDevHotGeneration } from "./dev-hot.ts";
7
+
8
+ describe("dev hot generation", () => {
9
+ test("take returns the installed generation once", async () => {
10
+ let disposed = 0;
11
+ installDevHotGeneration({
12
+ async dispose() {
13
+ disposed += 1;
14
+ },
15
+ });
16
+ const first = takeDevHotGeneration();
17
+ expect(first).toBeDefined();
18
+ await first!.dispose();
19
+ expect(disposed).toBe(1);
20
+ expect(takeDevHotGeneration()).toBeUndefined();
21
+ });
22
+
23
+ test("install replaces the previous slot without disposing it", () => {
24
+ installDevHotGeneration({
25
+ async dispose() {
26
+ throw new Error("stale");
27
+ },
28
+ });
29
+ let disposed = 0;
30
+ installDevHotGeneration({
31
+ async dispose() {
32
+ disposed += 1;
33
+ },
34
+ });
35
+ const current = takeDevHotGeneration();
36
+ expect(current).toBeDefined();
37
+ return current!.dispose().then(() => {
38
+ expect(disposed).toBe(1);
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * One live `bun --hot` generation for `oke dev`.
3
+ *
4
+ * Soft reload re-runs the app runner in the same process. The previous
5
+ * boot's scheduler and Postgres pools stay open unless this slot disposes
6
+ * them before the next `boot()`.
7
+ */
8
+
9
+ const DEV_HOT_SLOT = Symbol.for("oke.dev.hot.generation");
10
+
11
+ /** Work the previous runner evaluation must undo before the next boot. */
12
+ export type DevHotGeneration = {
13
+ /** Stop schedulers, drop signal handlers, and close this generation's pools. */
14
+ dispose(): Promise<void>;
15
+ };
16
+
17
+ type HotBox = { current?: DevHotGeneration };
18
+
19
+ function hotBox(): HotBox {
20
+ const g = globalThis as typeof globalThis & { [DEV_HOT_SLOT]?: HotBox };
21
+ let box = g[DEV_HOT_SLOT];
22
+ if (!box) {
23
+ box = {};
24
+ g[DEV_HOT_SLOT] = box;
25
+ }
26
+ return box;
27
+ }
28
+
29
+ /**
30
+ * Take the previous generation, if a soft reload left one.
31
+ *
32
+ * Clears the slot so a failed dispose cannot be retried against a half-stopped app.
33
+ */
34
+ export function takeDevHotGeneration(): DevHotGeneration | undefined {
35
+ const box = hotBox();
36
+ const prev = box.current;
37
+ box.current = undefined;
38
+ return prev;
39
+ }
40
+
41
+ /**
42
+ * Remember this evaluation so the next soft reload can dispose it.
43
+ *
44
+ * @param generation - Stop handle for the boot that just started
45
+ */
46
+ export function installDevHotGeneration(generation: DevHotGeneration): void {
47
+ hotBox().current = generation;
48
+ }
@@ -1191,7 +1191,7 @@ describe("oke dev Console vault config", () => {
1191
1191
  test("writes OKE_MANIFEST_PATH for the app child when a Manifest is available", async () => {
1192
1192
  const runner = await Bun.file(new URL("./dev-app-runner.ts", import.meta.url)).text();
1193
1193
  expect(runner).toContain("OKE_MANIFEST_PATH");
1194
- expect(runner).toMatch(/mod\.app\.boot\(\{/);
1194
+ expect(runner).toMatch(/\bapp\.boot\(\{/);
1195
1195
 
1196
1196
  const dir = await mkdtemp(join(tmpdir(), "oke-dev-manifest-handoff-"));
1197
1197
  await Bun.write(join(dir, "src/app.ts"), "export {}\n");
package/src/cli/dev.ts CHANGED
@@ -1036,6 +1036,13 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1036
1036
  setProcessEnv("OKE_KV_DRIVER", stackKvDriver);
1037
1037
  }
1038
1038
 
1039
+ // One secret for Console (this process) and the app child. Must land
1040
+ // before the first import of the app entry.
1041
+ {
1042
+ const { ensureDevAuthSecret } = await import("./dev-auth-secret.ts");
1043
+ setProcessEnv("OKE_AUTH_SECRET", await ensureDevAuthSecret(cwd));
1044
+ }
1045
+
1039
1046
  // One-shot schema sync for the compose (`dev`) profile: emits
1040
1047
  // drizzle/ for the active dialect, then pushes via drizzle-kit.
1041
1048
  let schemaPushOk = options.noDbPush === true;
@@ -1063,12 +1070,20 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1063
1070
  // Vault gaps before seed / app — missing contracts (e.g. OPENROUTER_API_KEY).
1064
1071
  {
1065
1072
  const { maybeAskVaultGaps } = await import("./ask-vault-gaps.ts");
1066
- const vaultCode = await maybeAskVaultGaps({
1067
- cwd,
1068
- entry,
1069
- write,
1070
- stdinIsTTY: options.stdinIsTTY ?? process.stdin.isTTY,
1071
- });
1073
+ // The secret prompt owns an open cursor line. A board repaint would
1074
+ // move that cursor and split the mask.
1075
+ bootBoard.hold();
1076
+ let vaultCode = 0;
1077
+ try {
1078
+ vaultCode = await maybeAskVaultGaps({
1079
+ cwd,
1080
+ entry,
1081
+ write,
1082
+ stdinIsTTY: options.stdinIsTTY ?? process.stdin.isTTY,
1083
+ });
1084
+ } finally {
1085
+ bootBoard.release();
1086
+ }
1072
1087
  if (vaultCode !== 0) {
1073
1088
  bootBoard.stop();
1074
1089
  restoreProcessEnv();
@@ -1443,6 +1458,8 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1443
1458
  autoPushRunner.trigger();
1444
1459
  }
1445
1460
  });
1461
+ const { watchDecisionLockfile } = await import("./decision-lock-watch.ts");
1462
+ const decisionLockWatch = watchDecisionLockfile(cwd);
1446
1463
 
1447
1464
  let stopped = false;
1448
1465
  const stop = () => {
@@ -1458,6 +1475,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1458
1475
  clientRegen.cancel();
1459
1476
  manifestRefresh.cancel();
1460
1477
  watcher.close();
1478
+ decisionLockWatch.close();
1461
1479
  const host = attachedHost;
1462
1480
  attachedHost = null;
1463
1481
  void host?.stop();
package/src/cli/eval.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { resolve } from "node:path";
6
6
  import { parseEvalJsonl, runPromptEvals, type EvalCase } from "../elements/ai/eval.ts";
7
+ import type { DecisionResponse } from "../elements/ai/decisions/provider.ts";
7
8
  import type { Manifest } from "../manifest/types.ts";
8
9
 
9
10
  /** Options for {@link runOkeEval}. */
@@ -96,17 +97,157 @@ export async function runOkeEval(options: OkeEvalOptions = {}): Promise<number>
96
97
  */
97
98
  export async function evalCli(args: string[]): Promise<number> {
98
99
  let manifestPath: string | undefined;
100
+ let certify = false;
99
101
  for (let i = 0; i < args.length; i++) {
100
102
  const a = args[i]!;
101
103
  if (a === "--manifest" || a === "-m") {
102
104
  manifestPath = args[++i];
105
+ } else if (a === "--certify") {
106
+ certify = true;
103
107
  } else if (a === "--help" || a === "-h") {
104
- console.log(`oke eval [--manifest oke.manifest.json]
108
+ console.log(`oke eval [--manifest oke.manifest.json] [--certify]
105
109
 
106
110
  Run prompt eval sets declared in the Manifest. Fails CI on regression.
111
+ --certify builds a decision certificate from each decision seed file.
107
112
  `);
108
113
  return 0;
109
114
  }
110
115
  }
116
+ if (certify) return runOkeCertify({ manifestPath });
111
117
  return runOkeEval({ manifestPath });
112
118
  }
119
+
120
+ /**
121
+ * Build certificates from decision seed files. Prompt evals are not run.
122
+ *
123
+ * @param options - Manifest path
124
+ */
125
+ export async function runOkeCertify(
126
+ options: {
127
+ readonly manifestPath?: string;
128
+ readonly manifest?: Manifest;
129
+ readonly root?: string;
130
+ readonly evaluate?: (input: unknown) => Promise<DecisionResponse>;
131
+ } = {},
132
+ ): Promise<number> {
133
+ const { certifySeed } = await import("../elements/ai/decisions/certify.ts");
134
+ const { aiDecisionRegistry } = await import("../kernel/element-registries.ts");
135
+ const { DECISION_LOCK_FILENAME, parseDecisionLockfile } =
136
+ await import("../elements/ai/decisions/certificate.ts");
137
+ const { resolveStartEntry } = await import("./start.ts");
138
+ const root = resolve(options.root ?? process.env["OKE_ROOT_DIR"] ?? ".");
139
+ process.env["OKE_ROOT_DIR"] ??= root;
140
+ let manifest = options.manifest;
141
+ if (!manifest) {
142
+ try {
143
+ const entry = await resolveStartEntry(root);
144
+ await import(entry);
145
+ } catch (err) {
146
+ console.error(
147
+ `oke eval: failed to load the app: ${err instanceof Error ? err.message : String(err)}`,
148
+ );
149
+ return 1;
150
+ }
151
+ const path = resolve(root, options.manifestPath ?? "oke.manifest.json");
152
+ const file = Bun.file(path);
153
+ if (!(await file.exists())) {
154
+ console.error(`oke eval: manifest not found: ${path}`);
155
+ return 1;
156
+ }
157
+ manifest = (await file.json()) as Manifest;
158
+ }
159
+ const decisions = manifest.ai?.decisions ?? {};
160
+ const names = Object.keys(decisions);
161
+ if (names.length === 0) {
162
+ process.stdout.write("oke eval: no decisions declared\n");
163
+ return 0;
164
+ }
165
+ const injected = options.evaluate;
166
+ const lockPath = resolve(root, DECISION_LOCK_FILENAME);
167
+ const existingFile = Bun.file(lockPath);
168
+ const current = (await existingFile.exists())
169
+ ? parseDecisionLockfile(await existingFile.json())
170
+ : undefined;
171
+ const next = { decisions: { ...(current?.decisions ?? {}) } };
172
+ for (const name of names) {
173
+ const decision = decisions[name];
174
+ if (!decision?.evals) {
175
+ process.stdout.write(`oke eval: skip ${name} (no evals)\n`);
176
+ continue;
177
+ }
178
+ const decl = aiDecisionRegistry.find((item) => item.name === name);
179
+ if (!decl) {
180
+ console.error(`oke eval: decision "${name}" is not loaded`);
181
+ return 1;
182
+ }
183
+ const text = await Bun.file(resolve(root, decision.evals)).text();
184
+ const evaluate = injected ?? (await providerForDecision(decl));
185
+ next.decisions[name] = await certifySeed({
186
+ model: decl.model ?? decision.model ?? "",
187
+ maxError: decision.autonomy?.maxError ?? 0.05,
188
+ delta: decision.autonomy?.risk ?? decl.autonomy?.risk ?? 0.1,
189
+ jsonl: text,
190
+ ask: decl.ask,
191
+ evaluate,
192
+ });
193
+ }
194
+ await Bun.write(lockPath, `${JSON.stringify(next, null, 2)}\n`);
195
+ const { setDecisionDrift } = await import("../elements/ai/decisions/certificate.ts");
196
+ const { persistDecisionDrift } = await import("../elements/ai/decisions/labels.ts");
197
+ for (const name of names) {
198
+ setDecisionDrift(name, false);
199
+ persistDecisionDrift(name, false);
200
+ }
201
+ return 0;
202
+ }
203
+
204
+ async function providerForDecision(
205
+ decl: import("../elements/ai/declare.ts").AiDecisionDecl,
206
+ ): Promise<(input: unknown) => Promise<DecisionResponse>> {
207
+ const typesafe = decl.driverId === "typesafe";
208
+ const keyName = typesafe ? "TYPESAFE_API_KEY" : "OPENROUTER_API_KEY";
209
+ const apiKey = process.env[keyName];
210
+ if (!apiKey) throw new Error(`oke eval: secret "${keyName}" is not configured`);
211
+ const { createTypesafeDecisionProvider, TYPESAFE_JEV_MODEL } =
212
+ await import("../elements/ai/decisions/typesafe.ts");
213
+ const { createOpenRouterDecisionProvider, OPENROUTER_JEV_MODEL } =
214
+ await import("../elements/ai/decisions/openrouter.ts");
215
+ const provider = typesafe
216
+ ? createTypesafeDecisionProvider(apiKey)
217
+ : createOpenRouterDecisionProvider(apiKey);
218
+ const model = decl.model ?? (typesafe ? TYPESAFE_JEV_MODEL : OPENROUTER_JEV_MODEL);
219
+ return (input) =>
220
+ provider.evaluate({
221
+ model,
222
+ state: input,
223
+ questions: wireFromDecl(decl),
224
+ });
225
+ }
226
+
227
+ function wireFromDecl(
228
+ decl: import("../elements/ai/declare.ts").AiDecisionDecl,
229
+ ): import("../elements/ai/decisions/provider.ts").DecisionRequest["questions"] {
230
+ const questions: Record<string, import("../elements/ai/decisions/provider.ts").WireQuestion> = {};
231
+ for (const [id, question] of Object.entries(decl.ask)) {
232
+ if (question.kind === "choice") {
233
+ questions[id] = {
234
+ type: "choice",
235
+ instructions: question.instructions,
236
+ criteria: { ...question.options, none_of_these: null },
237
+ };
238
+ } else if (question.kind === "score") {
239
+ questions[id] = {
240
+ type: "score",
241
+ instructions: question.instructions,
242
+ criteria: question.levels,
243
+ };
244
+ } else {
245
+ questions[id] = {
246
+ type: "noul",
247
+ instructions: question.instructions,
248
+ ...(question.criteria !== undefined ? { criteria: question.criteria } : {}),
249
+ };
250
+ }
251
+ }
252
+ return questions;
253
+ }
package/src/cli/index.ts CHANGED
@@ -16,6 +16,7 @@ import { dbCli } from "./db.ts";
16
16
  import { devCli } from "./dev.ts";
17
17
  import { doctorCli } from "./doctor.ts";
18
18
  import { dockerCli } from "./docker.ts";
19
+ import { decideCli } from "./decide.ts";
19
20
  import { evalCli } from "./eval.ts";
20
21
  import { EXIT_CODE_HELP, EXIT_OK, EXIT_USAGE } from "./exit.ts";
21
22
  import { gatesListCli } from "./gates-list.ts";
@@ -117,6 +118,10 @@ if (cmd === "eval") {
117
118
  process.exit(await evalCli(sub ? [sub, ...rest] : rest));
118
119
  }
119
120
 
121
+ if (cmd === "decide") {
122
+ process.exit(await decideCli(sub ? [sub, ...rest] : rest));
123
+ }
124
+
120
125
  if (cmd === "ai") {
121
126
  process.exit(await aiCli(sub ? [sub, ...rest] : rest));
122
127
  }
@@ -54,7 +54,7 @@ describe("defaultImagesFromConfig", () => {
54
54
  expect(images).toEqual({
55
55
  "store.sql": "postgres:18-alpine",
56
56
  pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
57
- "store.kv": "redis:8-alpine",
57
+ "store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
58
58
  });
59
59
  });
60
60
 
@@ -129,7 +129,7 @@ describe("resolveImages", () => {
129
129
  },
130
130
  });
131
131
  expect(images["store.sql"]).toBe("postgres:18-alpine");
132
- expect(images["store.kv"]).toBe("redis:8-alpine");
132
+ expect(images["store.kv"]).toBe("docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0");
133
133
  expect(images["store.kv.durable"]).toBeUndefined();
134
134
  });
135
135
  });
@@ -16,7 +16,7 @@ import type { Manifest } from "../manifest/types.ts";
16
16
  const DEFAULT_SQL_IMAGE = "postgres:18-alpine";
17
17
  const DEFAULT_PGVECTOR_IMAGE = "pgvector/pgvector:pg17";
18
18
  const DEFAULT_PGDOG_IMAGE = "ghcr.io/pgdogdev/pgdog:v0.1.59";
19
- const DEFAULT_KV_IMAGE = "redis:8-alpine";
19
+ const DEFAULT_KV_IMAGE = "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0";
20
20
 
21
21
  /**
22
22
  * Prefer `dev` then `prod` for a driver map (compose infra ≈ production).
@@ -22,6 +22,10 @@ export interface McpConsoleSurface {
22
22
  readonly cwd: string;
23
23
  /** Clock shared with Console. */
24
24
  readonly now: () => number;
25
+ /** Journal that holds agent events and parked approvals. */
26
+ readonly journalStore?: import("../kernel/journal.ts").JournalStore | null;
27
+ /** In-process agent ledger. The follow log is the durable source. */
28
+ readonly aiRuntime?: import("../elements/ai.ts").AiRuntime | null;
25
29
  }
26
30
 
27
31
  /**
@@ -33,6 +37,50 @@ export function mcpContextFromConsole(state: McpConsoleSurface): McpContext {
33
37
  return {
34
38
  getManifest: () => state.manifest,
35
39
  listRuns: () => state.listRuns(),
40
+ listAgentRuns: async () => {
41
+ const { loadConsoleAgentRuns } = await import("../console/server/ai-runs.ts");
42
+ return loadConsoleAgentRuns({
43
+ runtime: state.aiRuntime ?? null,
44
+ store: state.journalStore ?? null,
45
+ now: state.now(),
46
+ });
47
+ },
48
+ getAgentRun: async (runId) => {
49
+ const { loadConsoleAgentRun } = await import("../console/server/ai-runs.ts");
50
+ const run = await loadConsoleAgentRun(
51
+ {
52
+ runtime: state.aiRuntime ?? null,
53
+ store: state.journalStore ?? null,
54
+ now: state.now(),
55
+ },
56
+ runId,
57
+ );
58
+ if (!run) return undefined;
59
+ return { run, events: run.events };
60
+ },
61
+ listApprovals: async () => {
62
+ const { loadApprovalQueue } = await import("../console/server/ai-runs.ts");
63
+ return loadApprovalQueue(state.journalStore ?? null, state.now());
64
+ },
65
+ listDecisions: async (tenant) => {
66
+ const { loadDecisionQueue, projectMcpDecisions, candidateMetrics } =
67
+ await import("../console/server/decisions.ts");
68
+ const { listDecisionCandidates, loadDecisionCandidate } =
69
+ await import("../elements/ai/decisions/labels.ts");
70
+ const names = await listDecisionCandidates();
71
+ const fitted: Record<string, Record<string, number>> = {};
72
+ for (const name of names) {
73
+ const metrics = candidateMetrics(await loadDecisionCandidate(name));
74
+ if (metrics) fitted[name] = metrics;
75
+ }
76
+ const queue = await loadDecisionQueue(state.journalStore ?? null, state.now(), tenant);
77
+ const pending: Record<string, number> = {};
78
+ for (const row of queue) {
79
+ if (row.status !== "pending" || row.labelOnly) continue;
80
+ pending[row.decision] = (pending[row.decision] ?? 0) + 1;
81
+ }
82
+ return projectMcpDecisions(state.manifest, pending, fitted);
83
+ },
36
84
  proposeStructural: async (input) =>
37
85
  emitStructuralDiff({
38
86
  cwd: state.cwd,
@@ -550,7 +550,41 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
550
550
  name: "eval",
551
551
  summary: "prompt eval sets (CI gate)",
552
552
  leaf: true,
553
- flags: [MANIFEST, HELP],
553
+ flags: [
554
+ MANIFEST,
555
+ {
556
+ long: "--certify",
557
+ takesValue: false,
558
+ summary: "Build a decision certificate from the seed file",
559
+ },
560
+ HELP,
561
+ ],
562
+ },
563
+ {
564
+ name: "decide",
565
+ summary: "promote a decision certificate",
566
+ subcommands: [
567
+ {
568
+ name: "promote",
569
+ summary: "Fetch the operator candidate and write the app lockfile",
570
+ positionals: "<name>",
571
+ flags: [
572
+ {
573
+ long: "--origin",
574
+ takesValue: true,
575
+ valueName: "url",
576
+ summary: "App origin that serves the candidate",
577
+ },
578
+ {
579
+ long: "--lock",
580
+ takesValue: true,
581
+ valueName: "path",
582
+ summary: "Lockfile path (default oke-decisions.lock.json)",
583
+ },
584
+ HELP,
585
+ ],
586
+ },
587
+ ],
554
588
  },
555
589
  {
556
590
  name: "ai",