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
@@ -503,7 +503,7 @@ function createMemorySqlConnection(role: "primary" | "replica"): SqlConnection {
503
503
  }
504
504
 
505
505
  const create =
506
- /^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*\((.+)\)\s*$/i.exec(
506
+ /^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*\((.+)\)\s*$/is.exec(
507
507
  text,
508
508
  );
509
509
  if (create) {
@@ -519,6 +519,26 @@ function createMemorySqlConnection(role: "primary" | "replica"): SqlConnection {
519
519
  return { changes: 0 };
520
520
  }
521
521
 
522
+ const addColumn =
523
+ /^ALTER\s+TABLE\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s+ADD\s+COLUMN\s+IF\s+NOT\s+EXISTS\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\b/is.exec(
524
+ text,
525
+ );
526
+ if (addColumn) {
527
+ const name = parseIdent(addColumn[1]!);
528
+ const column = parseIdent(addColumn[2]!);
529
+ const table = tables.get(name);
530
+ if (table && !table.columns.includes(column)) table.columns.push(column);
531
+ return { changes: 0 };
532
+ }
533
+
534
+ // Scan results do not depend on indexes. Identity and vault DDL still
535
+ // issue CREATE INDEX IF NOT EXISTS on the shared memory connection.
536
+ const createIndex =
537
+ /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(?:"?[a-zA-Z_][a-zA-Z0-9_]*"?)\s+ON\s+/is.exec(
538
+ text,
539
+ );
540
+ if (createIndex) return { changes: 0 };
541
+
522
542
  const insert =
523
543
  /^INSERT\s+INTO\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*$/i.exec(
524
544
  text,
@@ -5,6 +5,7 @@ import {
5
5
  buildRlsIdentityPreludeSql,
6
6
  emitPgPolicySource,
7
7
  formatPolicyRole,
8
+ OKE_PG_STAT_STATEMENTS_SQL,
8
9
  OKE_RLS_HELPER_STATEMENTS,
9
10
  OKE_RLS_ROLE,
10
11
  parseSqlPolicySpec,
@@ -149,6 +150,36 @@ describe("OKE_RLS_HELPER_STATEMENTS", () => {
149
150
  expect(hasScope).not.toMatch(/jsonb\s+\?/);
150
151
  });
151
152
 
153
+ test("initial oke SQL creates pg_stat_statements before helpers", async () => {
154
+ const ran: string[] = [];
155
+ const { installOkeRlsHelpers } = await import("./pg-rls.ts");
156
+ await installOkeRlsHelpers(async (sql) => {
157
+ ran.push(sql);
158
+ if (sql.startsWith("CREATE EXTENSION")) return;
159
+ if (sql.includes("pg_vault") || sql.includes("vault.")) {
160
+ throw new Error("stop before vault attach");
161
+ }
162
+ }).catch(() => undefined);
163
+ expect(ran[0]).toBe(OKE_PG_STAT_STATEMENTS_SQL);
164
+ expect(ran[1]).toBe(OKE_RLS_HELPER_STATEMENTS[0]);
165
+ });
166
+
167
+ test("a rejected pg_stat_statements create still installs helpers", async () => {
168
+ const ran: string[] = [];
169
+ const { installOkeRlsHelpers } = await import("./pg-rls.ts");
170
+ await installOkeRlsHelpers(async (sql) => {
171
+ ran.push(sql);
172
+ if (sql.startsWith("CREATE EXTENSION")) {
173
+ throw new Error('extension "pg_stat_statements" is not available');
174
+ }
175
+ if (sql.includes("CREATE OR REPLACE FUNCTION oke.gate")) {
176
+ throw new Error("stop");
177
+ }
178
+ }).catch(() => undefined);
179
+ expect(ran[0]).toBe(OKE_PG_STAT_STATEMENTS_SQL);
180
+ expect(ran.some((sql) => sql.includes("CREATE SCHEMA IF NOT EXISTS oke"))).toBe(true);
181
+ });
182
+
152
183
  test("pins search_path to public so schema oke is not the table home", () => {
153
184
  expect(OKE_RLS_HELPER_STATEMENTS).toContain("SET search_path TO public, oke");
154
185
  expect(
@@ -135,12 +135,27 @@ $oke_app_search_path$`,
135
135
  */
136
136
  export const OKE_RLS_HELPER_SQL = `${OKE_RLS_HELPER_STATEMENTS.join(";\n")};`;
137
137
 
138
+ /**
139
+ * Enable query telemetry on postgres. Preload is still required
140
+ * (`shared_preload_libraries=pg_stat_statements`). PGlite and Cockroach
141
+ * reject this; callers swallow that failure so the rest of the oke SQL runs.
142
+ */
143
+ export const OKE_PG_STAT_STATEMENTS_SQL = "CREATE EXTENSION IF NOT EXISTS pg_stat_statements";
144
+
138
145
  /**
139
146
  * Install `oke.*` helpers one statement at a time.
140
147
  *
148
+ * `pg_stat_statements` is created first when the engine allows it, so Store
149
+ * performance does not depend on a later manual `CREATE EXTENSION`.
150
+ *
141
151
  * @param exec - Statement runner
142
152
  */
143
153
  export async function installOkeRlsHelpers(exec: (sql: string) => Promise<unknown>): Promise<void> {
154
+ try {
155
+ await exec(OKE_PG_STAT_STATEMENTS_SQL);
156
+ } catch {
157
+ // Not preloaded, or this engine has no pg_stat_statements (pglite, cockroach).
158
+ }
144
159
  for (const stmt of OKE_RLS_HELPER_STATEMENTS) {
145
160
  await exec(stmt);
146
161
  }
@@ -38,6 +38,15 @@ describe("toPostgresParams", () => {
38
38
  });
39
39
 
40
40
  describe("closeSharedPostgresClients", () => {
41
+ test("a different max is a different pool on the same URL", () => {
42
+ const url = "postgres://127.0.0.1:9/oke-pool-max-shared-test";
43
+ const small = sharedPostgresClient(url, 8);
44
+ const large = sharedPostgresClient(url, 20);
45
+ expect(large).not.toBe(small);
46
+ expect(sharedPostgresClient(url, 20)).toBe(large);
47
+ expect(sharedPostgresClient(url)).toBe(small);
48
+ });
49
+
41
50
  test("drops cached pools so the next checkout is a new client", async () => {
42
51
  const url = "postgres://127.0.0.1:9/oke-close-shared-test";
43
52
  const first = sharedPostgresClient(url);
@@ -43,7 +43,27 @@ type SharedPostgresEntry = {
43
43
  readonly guarded: PostgresClientLike;
44
44
  };
45
45
 
46
- const sharedClients = new Map<string, SharedPostgresEntry>();
46
+ /**
47
+ * Process-wide pool registry.
48
+ *
49
+ * `bun --hot` re-evaluates this module on each save. A module-local `Map`
50
+ * would drop the only handle to the previous Bun.SQL pools and leave them
51
+ * connected. PgDog then queues every checkout — Console login included —
52
+ * until its server pool times out.
53
+ */
54
+ const SHARED_POSTGRES_CLIENTS = Symbol.for("oke.sharedPostgresClients");
55
+
56
+ function sharedClientMap(): Map<string, SharedPostgresEntry> {
57
+ const g = globalThis as typeof globalThis & {
58
+ [SHARED_POSTGRES_CLIENTS]?: Map<string, SharedPostgresEntry>;
59
+ };
60
+ let map = g[SHARED_POSTGRES_CLIENTS];
61
+ if (!map) {
62
+ map = new Map();
63
+ g[SHARED_POSTGRES_CLIENTS] = map;
64
+ }
65
+ return map;
66
+ }
47
67
 
48
68
  /**
49
69
  * When true, shared pools stay closed and every query fails fast — no Bun.SQL
@@ -160,21 +180,38 @@ function guardSharedPostgresClient(raw: PostgresClientLike): PostgresClientLike
160
180
  }
161
181
 
162
182
  /**
163
- * One Bun.SQL pool per URL for store / journal / clock / vault / console.
183
+ * Bun.SQL pool size. Unset, non-finite, or below 1 keeps {@link POSTGRES_POOL_MAX}.
184
+ * A configured max is not capped — callers that need a larger pool ask for it.
185
+ *
186
+ * @param poolMax - Requested `max`
187
+ */
188
+ function postgresPoolMax(poolMax: number | undefined): number {
189
+ if (poolMax === undefined || !Number.isFinite(poolMax) || poolMax < 1) return POSTGRES_POOL_MAX;
190
+ return Math.floor(poolMax);
191
+ }
192
+
193
+ /**
194
+ * One Bun.SQL pool per URL and `max` for store / journal / clock / vault / console.
164
195
  *
165
196
  * Returns a pause-aware client — holders (including fleet store wrappers)
166
197
  * fail soft after {@link pauseSharedPostgresClients} instead of reconnecting.
167
198
  *
168
199
  * @param url - Connection URL
200
+ * @param poolMax - Bun.SQL `max`. Omitted uses {@link POSTGRES_POOL_MAX}. A
201
+ * different max is a different pool — a store pin of 20 must not reuse a
202
+ * pool another subsystem already opened at the default.
169
203
  */
170
- export function sharedPostgresClient(url?: string): PostgresClientLike {
204
+ export function sharedPostgresClient(url?: string, poolMax?: number): PostgresClientLike {
171
205
  assertSharedPostgresReady();
172
- const key = resolvePostgresUrl(url);
173
- const existing = sharedClients.get(key);
206
+ const resolved = resolvePostgresUrl(url);
207
+ const max = postgresPoolMax(poolMax);
208
+ const key = `${resolved}\0${String(max)}`;
209
+ const clients = sharedClientMap();
210
+ const existing = clients.get(key);
174
211
  if (existing) return existing.guarded;
175
- const raw = new Bun.SQL(key, { max: POSTGRES_POOL_MAX }) as unknown as PostgresClientLike;
212
+ const raw = new Bun.SQL(resolved, { max }) as unknown as PostgresClientLike;
176
213
  const guarded = guardSharedPostgresClient(raw);
177
- sharedClients.set(key, { raw, guarded });
214
+ clients.set(key, { raw, guarded });
178
215
  return guarded;
179
216
  }
180
217
 
@@ -185,8 +222,8 @@ export function sharedPostgresClient(url?: string): PostgresClientLike {
185
222
  * not stop Console from opening a fresh pool on the next poll.
186
223
  */
187
224
  export async function closeSharedPostgresClients(): Promise<void> {
188
- const clients = [...sharedClients.values()];
189
- sharedClients.clear();
225
+ const clients = [...sharedClientMap().values()];
226
+ sharedClientMap().clear();
190
227
  await Promise.all(
191
228
  clients.map(async (entry) => {
192
229
  try {
@@ -283,12 +320,13 @@ export async function connectPostgres(options: SqlConnectOptions = {}): Promise<
283
320
  // must not share a pooled client — Bun raises ERR_POSTGRES_UNSAFE_TRANSACTION
284
321
  // when raw statements interleave with open transaction state on the shared
285
322
  // pool. Opt in explicitly; default behavior is unchanged.
286
- const dedicated = !injected && options.pool?.max === 1;
323
+ const requestedMax = options.pool?.max;
324
+ const dedicated = !injected && requestedMax === 1;
287
325
  const client =
288
326
  injected ??
289
327
  (dedicated
290
328
  ? dedicatedPostgresClient(resolvePostgresUrl(options.url))
291
- : sharedPostgresClient(options.url));
329
+ : sharedPostgresClient(options.url, requestedMax));
292
330
  return wrapPostgresClient(client, role, {
293
331
  shared: injected === undefined && !dedicated,
294
332
  ...(options.url !== undefined ? { url: options.url } : {}),
@@ -115,4 +115,14 @@ describe("builtin vault driver", () => {
115
115
  await rm(dir, { recursive: true, force: true });
116
116
  }
117
117
  }, 15_000);
118
+
119
+ test("webhook audit without a URL fails open instead of degrading", async () => {
120
+ await expect(
121
+ builtinVaultDriver.open({
122
+ connection: sharedConn,
123
+ env: {},
124
+ audit: { sink: "webhook" },
125
+ }),
126
+ ).rejects.toThrow(/webhookUrl/);
127
+ });
118
128
  });
@@ -18,6 +18,7 @@ import {
18
18
  sqlConnectionAsExec,
19
19
  type BuiltinVaultAdapter,
20
20
  } from "../elements/vault/builtin-adapter.ts";
21
+ import { isVaultError } from "../elements/vault/errors.ts";
21
22
  import type { SqlConnection } from "./types.ts";
22
23
  import type { VaultBag, VaultDriver, VaultOpenOptions } from "./vault-types.ts";
23
24
 
@@ -39,6 +40,10 @@ export interface OpenBuiltinVaultOptions {
39
40
  readonly env?: Readonly<Record<string, string | undefined>>;
40
41
  /** DEKs re-wrapped per master-rotation batch. */
41
42
  readonly kekRewrapBatchSize?: number;
43
+ /** `vault.audit` from config. Omitted means the SQL hash chain. */
44
+ readonly audit?: import("../elements/vault/types.ts").VaultAuditConfig;
45
+ /** Fetch override for the webhook audit sink (tests). */
46
+ readonly fetch?: typeof globalThis.fetch;
42
47
  }
43
48
 
44
49
  /** An adapter plus the connection this module opened for it, if any. */
@@ -76,6 +81,8 @@ export async function openBuiltinVaultAdapter(
76
81
  ...(options.kekRewrapBatchSize === undefined
77
82
  ? {}
78
83
  : { kekRewrapBatchSize: options.kekRewrapBatchSize }),
84
+ ...(options.audit === undefined ? {} : { audit: options.audit }),
85
+ ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
79
86
  });
80
87
 
81
88
  const masterKey = options.masterKey ?? env[VAULT_MASTER_KEY_ENV];
@@ -114,6 +121,8 @@ export const builtinVaultDriver: VaultDriver = {
114
121
  opened = await openBuiltinVaultAdapter({
115
122
  env,
116
123
  ...(options.connection !== undefined ? { connection: options.connection } : { url }),
124
+ ...(options.audit === undefined ? {} : { audit: options.audit }),
125
+ ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
117
126
  });
118
127
  if (opened.adapter.getUnsealer() !== null) {
119
128
  for (const entry of await opened.adapter.list()) {
@@ -121,7 +130,10 @@ export const builtinVaultDriver: VaultDriver = {
121
130
  if (secret) map.set(entry.path, secret.value);
122
131
  }
123
132
  }
124
- } catch {
133
+ } catch (err) {
134
+ // A bad audit sink is a config error — do not hide it behind the
135
+ // uninitialized-backend fallback.
136
+ if (isVaultError(err, "MISSING_PEER")) throw err;
125
137
  // Uninitialized / sealed / unreachable: fall back to the seed bag so
126
138
  // boot reports missing contracts instead of a driver stack trace.
127
139
  await opened?.close().catch(() => undefined);
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { VaultError } from "../elements/vault/errors.ts";
11
+ import { requestSignal } from "../kernel/abort-scope.ts";
11
12
  import type { VaultBag } from "./vault-types.ts";
12
13
 
13
14
  /**
@@ -183,7 +184,10 @@ export async function vaultHttpJson(
183
184
  ): Promise<VaultHttpJsonResult> {
184
185
  let response: Response;
185
186
  try {
186
- response = await fetchFn(url, init);
187
+ response = await fetchFn(url, {
188
+ ...init,
189
+ signal: requestSignal(init.signal),
190
+ });
187
191
  } catch (error) {
188
192
  throw asRemoteVaultError(error, message);
189
193
  }
@@ -46,8 +46,13 @@ export interface VaultOpenOptions {
46
46
  readonly region?: string;
47
47
  /** Seed map for `memory` / `managed` drivers. */
48
48
  readonly secrets?: Readonly<Record<string, string>>;
49
- /** HTTP fetch override for remote drivers (tests). */
49
+ /** HTTP fetch override for remote drivers and the webhook audit sink (tests). */
50
50
  readonly fetch?: typeof globalThis.fetch;
51
+ /**
52
+ * Built-in vault audit block (`vault.audit`). Honored by the `vault` driver.
53
+ * Omitted means the SQL hash chain.
54
+ */
55
+ readonly audit?: import("../elements/vault/types.ts").VaultAuditConfig;
51
56
  }
52
57
 
53
58
  /** Resolved secret bag — names → cleartext values. Never logged. */
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Installed agent event log.
3
+ *
4
+ * The follow route and the AI runtime must share one slot. The log
5
+ * implementation stays in `run-events.ts` and loads on first use; this file
6
+ * is the only static edge so a `lazyRequire` copy cannot hide the log.
7
+ */
8
+
9
+ import type { AgentEventLog } from "./run-events.ts";
10
+
11
+ let activeLog: AgentEventLog | undefined;
12
+
13
+ /**
14
+ * Install the log the follow route reads. Boot and tests call this.
15
+ *
16
+ * @param log - Active log
17
+ */
18
+ export function setAgentEventLog(log: AgentEventLog | undefined): void {
19
+ activeLog = log;
20
+ }
21
+
22
+ /**
23
+ * Log installed by the running AI runtime.
24
+ */
25
+ export function getAgentEventLog(): AgentEventLog | undefined {
26
+ return activeLog;
27
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Built-in HTTP approve / deny for a parked agent tool.
3
+ *
4
+ * The route is public so the request can arrive. The tool's own gate is
5
+ * checked inside `fx.agent`. `Idempotency-Key` applies because the flow writes.
6
+ */
7
+
8
+ import { flow } from "../../kernel/flow.ts";
9
+ import type { Fx } from "../../kernel/fx.ts";
10
+ import { lazyRequire } from "../../kernel/lazy-require.ts";
11
+ import type { Binding } from "../../kernel/on.ts";
12
+ import { http } from "../../kernel/triggers.ts";
13
+ import type { GateRuntime } from "../gate/runtime.ts";
14
+ import type { StandardSchemaV1 } from "../../validation/standard-schema.ts";
15
+ import { journalLeaseBusyResponse } from "./approval.ts";
16
+ import { getAgentEventLog } from "./agent-event-slot.ts";
17
+ import type { AgentEventLog } from "./run-events.ts";
18
+
19
+ /** SSE frame helpers live on `fx.ts`. Load them on the first follow frame. */
20
+ function loadSseFrames(): typeof import("../../kernel/fx.ts") {
21
+ return lazyRequire(`${import.meta.dir}/../../kernel`, ["f", "x"].join(""));
22
+ }
23
+
24
+ /** Body the approve route accepts. `args` replaces the tool input. */
25
+ const approveIn: StandardSchemaV1<{ id: string; args?: unknown }> = {
26
+ "~standard": {
27
+ version: 1,
28
+ vendor: "okengine",
29
+ validate(value) {
30
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
31
+ return { issues: [{ message: "expected object", path: [] }] };
32
+ }
33
+ const body = value as { id?: unknown; args?: unknown };
34
+ if (typeof body.id !== "string" || body.id.length === 0) {
35
+ return { issues: [{ message: "id is required", path: ["id"] }] };
36
+ }
37
+ return { value: { id: body.id, ...(body.args !== undefined ? { args: body.args } : {}) } };
38
+ },
39
+ },
40
+ };
41
+
42
+ /** Body the deny route accepts. */
43
+ const denyIn: StandardSchemaV1<{ id: string; reason?: string }> = {
44
+ "~standard": {
45
+ version: 1,
46
+ vendor: "okengine",
47
+ validate(value) {
48
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
49
+ return { issues: [{ message: "expected object", path: [] }] };
50
+ }
51
+ const body = value as { id?: unknown; reason?: unknown };
52
+ if (typeof body.id !== "string" || body.id.length === 0) {
53
+ return { issues: [{ message: "id is required", path: ["id"] }] };
54
+ }
55
+ if (body.reason !== undefined && typeof body.reason !== "string") {
56
+ return { issues: [{ message: "reason must be a string", path: ["reason"] }] };
57
+ }
58
+ return {
59
+ value: {
60
+ id: body.id,
61
+ ...(typeof body.reason === "string" ? { reason: body.reason } : {}),
62
+ },
63
+ };
64
+ },
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Map a resolution to the HTTP failure the rest of the kernel already speaks.
70
+ *
71
+ * @param fx - Flow context
72
+ * @param result - Approval resolution
73
+ */
74
+ function approvalHttpResult(
75
+ fx: Fx,
76
+ result:
77
+ | { readonly ok: true }
78
+ | { readonly ok: false; readonly status: 403 | 404 }
79
+ | { readonly ok: false; readonly status: 409; readonly reason: "resolved" }
80
+ | {
81
+ readonly ok: false;
82
+ readonly status: 409;
83
+ readonly reason: "lease";
84
+ readonly retryAfterSeconds: number;
85
+ },
86
+ ): unknown {
87
+ if (result.ok) return { ok: true };
88
+ if (result.status === 409 && result.reason === "lease") {
89
+ return journalLeaseBusyResponse(result.retryAfterSeconds);
90
+ }
91
+ if (result.status === 409) return fx.fail.conflict();
92
+ if (result.status === 403) return fx.fail.forbidden();
93
+ return fx.fail.notFound();
94
+ }
95
+
96
+ /**
97
+ * Register the approve and deny routes on an app.
98
+ *
99
+ * @param adopt - App binding adopter
100
+ */
101
+ export function bindAgentApprovalFlows(adopt: (binding: Binding) => void): void {
102
+ adopt({
103
+ trigger: http.post("/agent/approvals/approve", { in: approveIn }).public(),
104
+ flow: flow("oke.agent.approve", {
105
+ effects: { writes: ["journal:runs"] },
106
+ do: async (input, fx) => {
107
+ const body = input as { id: string; args?: unknown };
108
+ const result = await fx.agent.approve(body.id, { args: body.args });
109
+ return approvalHttpResult(fx, result);
110
+ },
111
+ }),
112
+ });
113
+ adopt({
114
+ trigger: http.post("/agent/approvals/deny", { in: denyIn }).public(),
115
+ flow: flow("oke.agent.deny", {
116
+ effects: { writes: ["journal:runs"] },
117
+ do: async (input, fx) => {
118
+ const body = input as { id: string; reason?: string };
119
+ const result = await fx.agent.deny(body.id, { reason: body.reason });
120
+ return approvalHttpResult(fx, result);
121
+ },
122
+ }),
123
+ });
124
+ adopt({
125
+ trigger: http.get("/agent/runs/:runId/events").public(),
126
+ flow: flow("oke.agent.follow", {
127
+ do: async (input, fx) => followAgentRun(input, fx),
128
+ }),
129
+ });
130
+ }
131
+
132
+ let followGates: GateRuntime | undefined;
133
+
134
+ /**
135
+ * Gate runtime the follow route uses. Boot installs the app's gates.
136
+ *
137
+ * @param gates - Runtime that checked the calling Flow
138
+ */
139
+ export function setAgentFollowGates(gates: GateRuntime | undefined): void {
140
+ followGates = gates;
141
+ }
142
+
143
+ async function followAgentRun(input: unknown, fx: Fx): Promise<unknown> {
144
+ const body = (input ?? {}) as { runId?: unknown };
145
+ const runId = typeof body.runId === "string" ? body.runId : "";
146
+ const log = getAgentEventLog();
147
+ if (!log || !runId) return fx.fail.notFound();
148
+ const header = await log.header(runId);
149
+ if (!header) return fx.fail.notFound();
150
+ if ((header.tenant ?? null) !== (fx.tenant.id ?? null)) return fx.fail.notFound();
151
+ const followerIsOperator = fx.operator.id !== null;
152
+ const sameUser = (header.userId ?? null) === (fx.auth.userId ?? null);
153
+ const sameOperator = (header.operatorId ?? null) === (fx.operator.id ?? null);
154
+ const samePrincipal =
155
+ (header.userId === null && header.operatorId === null && sameUser && sameOperator) ||
156
+ (header.userId !== null && sameUser) ||
157
+ (header.operatorId !== null && sameOperator);
158
+ if (!followerIsOperator && !samePrincipal) return fx.fail.forbidden();
159
+ if (header.gates.length > 0) {
160
+ if (!followGates) return fx.fail.forbidden();
161
+ const checks = await followGates.check(header.gates, {
162
+ auth: fx.auth,
163
+ operator: fx.operator,
164
+ meta: {},
165
+ });
166
+ if (checks.some((check) => check.allowed === false)) return fx.fail.forbidden();
167
+ }
168
+ const after = Number(fx.lastEventId ?? "0");
169
+ const afterSeq = Number.isFinite(after) ? after : 0;
170
+ return fx.json.stream(frames(log, runId, afterSeq, fx.signal));
171
+ }
172
+
173
+ async function* frames(
174
+ log: AgentEventLog,
175
+ runId: string,
176
+ afterSeq: number,
177
+ signal: AbortSignal,
178
+ ): AsyncIterable<unknown> {
179
+ const iter = log.subscribe(runId, afterSeq, signal)[Symbol.asyncIterator]();
180
+ let pending = iter.next();
181
+ try {
182
+ for (;;) {
183
+ let timer: ReturnType<typeof setTimeout> | undefined;
184
+ const ping = new Promise<"ping">((resolve) => {
185
+ timer = setTimeout(() => resolve("ping"), 15_000);
186
+ });
187
+ const winner = await Promise.race([
188
+ pending.then((row) => ({ kind: "row" as const, row })),
189
+ ping.then((kind) => ({ kind })),
190
+ ]);
191
+ if (timer) clearTimeout(timer);
192
+ if (winner.kind === "ping") {
193
+ if (signal.aborted) return;
194
+ yield loadSseFrames().sseComment("keepalive");
195
+ continue;
196
+ }
197
+ pending = iter.next();
198
+ if (winner.row.done || winner.row.value === undefined) return;
199
+ const row = winner.row.value;
200
+ yield loadSseFrames().sseFrame(row.event, String(row.seq));
201
+ if (row.event.type === "RUN_ERROR") return;
202
+ if (row.event.type === "RUN_FINISHED" && row.event.outcome?.type !== "interrupt") return;
203
+ }
204
+ } finally {
205
+ await iter.return?.();
206
+ }
207
+ }