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
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { EffectExternal } from "./effects.ts";
7
7
  import { isDryRun, recordWouldHaveFired } from "./dry-run.ts";
8
+ import { requestSignal } from "./abort-scope.ts";
8
9
 
9
10
  /**
10
11
  * Hostname for `fx.fetch` capability / ledger resource.
@@ -48,7 +49,10 @@ export function runFxFetch(gated: Gated, url: string | URL, init?: RequestInit):
48
49
  recordWouldHaveFired("fetch", host);
49
50
  return new Response(null, { status: 204 });
50
51
  }
51
- return globalThis.fetch(url, init);
52
+ return globalThis.fetch(url, {
53
+ ...init,
54
+ signal: requestSignal(init?.signal),
55
+ });
52
56
  },
53
57
  { host, kind: "third-party" },
54
58
  );
@@ -483,9 +483,7 @@ describe("fx — wholesale swap", () => {
483
483
  async deliverOtp(opts) {
484
484
  return { ok: true as const, channel: opts.only ?? opts.channels[0] ?? "email" };
485
485
  },
486
- async ask() {
487
- return {};
488
- },
486
+ ask: (async () => ({})) as unknown as Fx["ask"],
489
487
  async embed() {
490
488
  return [];
491
489
  },
@@ -495,9 +493,18 @@ describe("fx — wholesale swap", () => {
495
493
  async fetch() {
496
494
  return new Response(null, { status: 204 });
497
495
  },
498
- async run() {
499
- return {};
496
+ async decide<T>(): Promise<T> {
497
+ return {} as T;
498
+ },
499
+ agent: {
500
+ async approve() {
501
+ return { ok: true as const };
502
+ },
503
+ async deny() {
504
+ return { ok: true as const };
505
+ },
500
506
  },
507
+ run: (async () => ({})) as unknown as Fx["run"],
501
508
  async *stream() {
502
509
  /* empty */
503
510
  },
package/src/kernel/fx.ts CHANGED
@@ -38,7 +38,10 @@ import type { SignalDecl, SignalRuntime } from "../elements/signal.ts";
38
38
  import type { DeadLetter, SignalEmitOptions } from "../drivers/signal-types.ts";
39
39
  import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.ts";
40
40
  import type { ChannelRuntime } from "../elements/channel.ts";
41
+ import type { AgentApprovalResolveResult } from "../elements/ai/approval.ts";
41
42
  import type { AiRuntime } from "../elements/ai.ts";
43
+ import type { AgUiEvent } from "../elements/ai/events.ts";
44
+ import type { AiMessage } from "../drivers/ai-types.ts";
42
45
  import { parseDurationMs } from "../elements/clock/duration.ts";
43
46
  import type { ApiKeyStore } from "../auth/api-keys.ts";
44
47
  import type { FxAuthIdentity, FxAuthKeyMethods } from "./fx-auth-keys.ts";
@@ -62,14 +65,14 @@ import {
62
65
  touchDryRunStore,
63
66
  } from "./dry-run.ts";
64
67
  import type { FailFn } from "./errors.ts";
65
- import { currentAbortSignal, linkAbort } from "./abort-scope.ts";
68
+ import { abortError, currentAbortSignal, linkAbort, withAbortSignal } from "./abort-scope.ts";
66
69
  import type { FxRetryOptions, FxThunk } from "./concurrency.ts";
67
70
  import { maskRedactedDeep, Redacted } from "./redacted.ts";
68
71
  import type { JournalSession, JournalStepOptions } from "./journal.ts";
69
72
 
70
73
  /** Options for {@link Fx.step} — see {@link JournalStepOptions}. */
71
74
  export type StepOptions<T> = JournalStepOptions<T>;
72
- import type { RunTelemetry } from "./run-telemetry.ts";
75
+ import { type RunTelemetry } from "./run-telemetry.ts";
73
76
  import type { RunsRuntime } from "../runs/runtime.ts";
74
77
  import type { RunsRow, WideEvent } from "../runs/types.ts";
75
78
  import type { RunWindowStats, SloBreach } from "../runs/window.ts";
@@ -114,7 +117,33 @@ function loadFxLiveStream(): typeof import("./fx-live-stream.ts") {
114
117
  return lazyRequire(import.meta.dir, ["fx", "live", "stream"].join("-"));
115
118
  }
116
119
 
120
+ /**
121
+ * Split `fx.run` input into one message or a history.
122
+ *
123
+ * @param input - String, `{ message }`, or `{ messages }`
124
+ */
125
+ function agentTurn(input: unknown): { message?: string; messages?: readonly AiMessage[] } {
126
+ if (typeof input === "string") return { message: input };
127
+ if (input && typeof input === "object") {
128
+ const record = input as { message?: unknown; messages?: unknown };
129
+ const hasMessage = "message" in record && record.message !== undefined;
130
+ const hasMessages = "messages" in record && record.messages !== undefined;
131
+ if (hasMessage && hasMessages) {
132
+ throw new TypeError("fx.run: pass message or messages, not both");
133
+ }
134
+ if (hasMessages && Array.isArray(record.messages)) {
135
+ return { messages: record.messages as readonly AiMessage[] };
136
+ }
137
+ if (hasMessage) return { message: String(record.message) };
138
+ }
139
+ return { message: JSON.stringify(input ?? {}) };
140
+ }
141
+
117
142
  /** `fx.fetch` — keep host parsing + dry-run stub off cold edge / Store-only graphs. */
143
+ function loadFxDecide(): typeof import("./fx-decide.ts") {
144
+ return lazyRequire(import.meta.dir, ["fx", "decide"].join("-"));
145
+ }
146
+
118
147
  function loadFxFetch(): typeof import("./fx-fetch.ts") {
119
148
  return lazyRequire(import.meta.dir, ["fx", "fetch"].join("-"));
120
149
  }
@@ -394,6 +423,8 @@ export interface FxAskOptions {
394
423
  readonly tools?: readonly NamedRef[];
395
424
  /** Bound on tool invocations (default 6). */
396
425
  readonly maxSteps?: number;
426
+ /** Yield AG-UI events instead of resolving to the validated object. */
427
+ readonly stream?: boolean;
397
428
  }
398
429
 
399
430
  /** Options for {@link Fx.search}. */
@@ -406,11 +437,13 @@ export type { JsonResult, JsonStreamResult };
406
437
 
407
438
  const sseFrameBrand: unique symbol = Symbol.for("oke.sse.frame");
408
439
 
409
- /** One SSE frame — optional `id:` plus JSON `data:`. */
440
+ /** One SSE frame — optional `id:` plus JSON `data:`, or a comment line. */
410
441
  export interface SseFrame {
411
442
  readonly [sseFrameBrand]: true;
412
443
  readonly data: unknown;
413
444
  readonly id?: string;
445
+ /** Comment body. When set and `data` is undefined, the encoder emits `: comment`. */
446
+ readonly comment?: string;
414
447
  }
415
448
 
416
449
  /**
@@ -423,6 +456,15 @@ export function sseFrame(data: unknown, id?: string): SseFrame {
423
456
  return id !== undefined ? { [sseFrameBrand]: true, data, id } : { [sseFrameBrand]: true, data };
424
457
  }
425
458
 
459
+ /**
460
+ * SSE comment (`: keepalive`). Keeps a followed run from being idle-closed.
461
+ *
462
+ * @param text - Comment text
463
+ */
464
+ export function sseComment(text: string): SseFrame {
465
+ return { [sseFrameBrand]: true, data: undefined, comment: text };
466
+ }
467
+
426
468
  /**
427
469
  * True when `value` is a branded SSE frame.
428
470
  *
@@ -781,6 +823,11 @@ export interface Fx {
781
823
  * @param opts - Model routing opts
782
824
  */
783
825
  ask(prompt: NamedRef, input?: unknown, opts?: FxAskOptions): Promise<Record<string, unknown>>;
826
+ ask(
827
+ prompt: NamedRef,
828
+ input: unknown,
829
+ opts: FxAskOptions & { readonly stream: true },
830
+ ): AsyncIterable<AgUiEvent>;
784
831
  /**
785
832
  * Embed text via a named model (records `embed` — distinct from `ask`).
786
833
  * Returns the vector; does not write an index.
@@ -808,13 +855,40 @@ export interface Fx {
808
855
  * @param init - Standard `RequestInit`
809
856
  */
810
857
  fetch(url: string | URL, init?: RequestInit): Promise<Response>;
858
+ /**
859
+ * Run one declared decision. `$` carries `how` and `audited`.
860
+ * Abstain mode makes each question value `T | null`.
861
+ */
862
+ decide<T>(decision: { name: string }, input: unknown): Promise<T>;
863
+ /**
864
+ * Resolve a pending agent tool approval (records nothing until the tool runs).
865
+ *
866
+ * First resolution wins. A later call returns a conflict failure.
867
+ */
868
+ readonly agent: {
869
+ /**
870
+ * @param id - Approval id from the interrupt
871
+ * @param opts - Optional replacement args
872
+ */
873
+ approve(id: string, opts?: { readonly args?: unknown }): Promise<AgentApprovalResolveResult>;
874
+ /**
875
+ * @param id - Approval id from the interrupt
876
+ * @param opts - Denial reason fed back to the model
877
+ */
878
+ deny(id: string, opts?: { readonly reason?: string }): Promise<AgentApprovalResolveResult>;
879
+ };
811
880
  /**
812
881
  * Run a bounded AI agent (records `ask`).
813
882
  *
814
883
  * @param agent - Agent name or handle
815
- * @param input - Agent input (`{ message }` or string)
884
+ * @param input - Agent input (`{ message }`, `{ messages }`, or string)
816
885
  */
817
886
  run(agent: NamedRef, input?: unknown): Promise<unknown>;
887
+ run(
888
+ agent: NamedRef,
889
+ input: unknown,
890
+ opts: { readonly stream: true; readonly threadId?: string },
891
+ ): AsyncIterable<AgUiEvent>;
818
892
  /**
819
893
  * Stream model tokens (records `ask`). Returns an async iterable of chunks.
820
894
  *
@@ -844,6 +918,8 @@ export interface Fx {
844
918
  t(key: AppMessageKey, values?: MessageValues): string;
845
919
  /** Active locale for {@link Fx.t} and default channel sends. */
846
920
  readonly locale: string;
921
+ /** HTTP `Last-Event-ID` on this request, when the client sent one. */
922
+ readonly lastEventId?: string;
847
923
  /** Generate a unique id (OKID). */
848
924
  id(): string;
849
925
  /** User-plane auth principal. */
@@ -1200,9 +1276,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1200
1276
  externalOf?:
1201
1277
  | EffectExternal
1202
1278
  | ((result: T | undefined, error: unknown) => EffectExternal | undefined),
1279
+ stamp?: (result: T | undefined) => { readonly agentRunId?: string } | undefined,
1203
1280
  ): Promise<T> {
1281
+ const signal = currentAbortSignal();
1282
+ if (signal.aborted) throw abortError(signal.reason);
1204
1283
  capability.assert(kind, resource);
1205
- const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf);
1284
+ const execute = () => recordEffect(ledger, kind, resource, now, body, externalOf, stamp);
1206
1285
  if (journal) {
1207
1286
  return journal.effect(kind, resource, execute);
1208
1287
  }
@@ -1420,6 +1499,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1420
1499
 
1421
1500
  const aiDisablesCache = options.aiRuntime?.autoCacheDisabled === true;
1422
1501
  const telemetry = options.runTelemetry;
1502
+ if (telemetry) {
1503
+ lazyRequire<{ bindDecisionLabelTelemetry(telemetry: RunTelemetry): void }>(
1504
+ `${import.meta.dir}/../elements/ai/decisions`,
1505
+ ["lab", "els"].join(""),
1506
+ ).bindDecisionLabelTelemetry(telemetry);
1507
+ }
1423
1508
 
1424
1509
  const cache: FxCache = {
1425
1510
  async get<T = unknown>(key: string): Promise<T | undefined> {
@@ -1511,6 +1596,8 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1511
1596
  * @param body - Work to run under the gate
1512
1597
  */
1513
1598
  async function gatedSecret<T>(name: string, body: () => T | Promise<T>): Promise<T> {
1599
+ const signal = currentAbortSignal();
1600
+ if (signal.aborted) throw abortError(signal.reason);
1514
1601
  capability.assert("secret", name);
1515
1602
  const timestamp = now();
1516
1603
  const t0 = performance.now();
@@ -1659,6 +1746,37 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1659
1746
  },
1660
1747
  };
1661
1748
 
1749
+ async function resolveAgentDecision(
1750
+ id: string,
1751
+ decision: {
1752
+ readonly decision: "approve" | "deny";
1753
+ readonly args?: unknown;
1754
+ readonly reason?: string;
1755
+ },
1756
+ ): Promise<AgentApprovalResolveResult> {
1757
+ const runtime = options.aiRuntime;
1758
+ if (!runtime?.resolveApproval) return { ok: false, status: 404 };
1759
+ const approver = auth.userId ?? operator.id;
1760
+ return runtime.resolveApproval(
1761
+ id,
1762
+ {
1763
+ decision: decision.decision,
1764
+ tenant: tenant.id,
1765
+ ...(decision.args !== undefined ? { args: decision.args } : {}),
1766
+ ...(decision.reason !== undefined ? { reason: decision.reason } : {}),
1767
+ ...(approver ? { approver } : {}),
1768
+ },
1769
+ {
1770
+ auth: {
1771
+ userId: auth.userId,
1772
+ scopes: auth.scopes,
1773
+ verified: auth.verified,
1774
+ },
1775
+ operator: { id: operator.id },
1776
+ },
1777
+ );
1778
+ }
1779
+
1662
1780
  const fx: Fx = {
1663
1781
  store: storeHandle,
1664
1782
  runs: runsSurface,
@@ -1811,8 +1929,37 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1811
1929
  return { ok: true as const, channel: result.channel };
1812
1930
  });
1813
1931
  },
1814
- ask(prompt, input, opts) {
1932
+ ask: function (prompt, input, opts) {
1815
1933
  const name = resolveName(prompt);
1934
+ if (opts?.stream) {
1935
+ return (async function* () {
1936
+ await gated("ask", name, async () => undefined);
1937
+ if (isDryRun()) {
1938
+ recordWouldHaveFired("ask", name);
1939
+ return;
1940
+ }
1941
+ if (!options.aiRuntime) {
1942
+ throw new Error(`fx.ask: AI runtime is not configured for prompt "${name}"`);
1943
+ }
1944
+ const local = new AbortController();
1945
+ const unlink = linkAbort(currentAbortSignal(), local);
1946
+ try {
1947
+ const events = await withAbortSignal(local.signal, () =>
1948
+ options.aiRuntime!.streamAsk(name, input, {
1949
+ via: opts.via?.map(resolveName),
1950
+ ...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
1951
+ tools: opts.tools?.map(resolveName),
1952
+ maxSteps: opts.maxSteps,
1953
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
1954
+ }),
1955
+ );
1956
+ yield* events;
1957
+ } finally {
1958
+ unlink();
1959
+ if (!local.signal.aborted) local.abort();
1960
+ }
1961
+ })();
1962
+ }
1816
1963
  return gated(
1817
1964
  "ask",
1818
1965
  name,
@@ -1840,7 +1987,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1840
1987
  },
1841
1988
  () => options.aiRuntime?.lastExternal,
1842
1989
  );
1843
- },
1990
+ } as Fx["ask"],
1844
1991
  embed(model, text) {
1845
1992
  const name = resolveName(model);
1846
1993
  return gated(
@@ -1886,29 +2033,158 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1886
2033
  fetch(url, init) {
1887
2034
  return loadFxFetch().runFxFetch(gated, url, init);
1888
2035
  },
1889
- run(agent, input) {
2036
+ decide<T>(decision: { name: string }, input: unknown) {
2037
+ return loadFxDecide().runFxDecide({
2038
+ gated,
2039
+ journal: options.journal,
2040
+ signal: currentAbortSignal(),
2041
+ now,
2042
+ runId: options.runId,
2043
+ tenantId: tenant.id,
2044
+ getSecret: async (secretName) => {
2045
+ const value = await gatedSecret(secretName, async () => secrets[secretName]);
2046
+ return typeof value === "string" && value.length > 0 ? value : undefined;
2047
+ },
2048
+ decision,
2049
+ input,
2050
+ }) as Promise<T>;
2051
+ },
2052
+ agent: {
2053
+ approve(id, opts) {
2054
+ return resolveAgentDecision(id, { decision: "approve", args: opts?.args });
2055
+ },
2056
+ deny(id, opts) {
2057
+ return resolveAgentDecision(id, { decision: "deny", reason: opts?.reason });
2058
+ },
2059
+ },
2060
+ run: function (agent, input, opts?: { readonly stream?: boolean; readonly threadId?: string }) {
1890
2061
  const name = resolveName(agent);
1891
- return gated("ask", name, async () => {
1892
- if (options.aiRuntime) {
1893
- const message =
1894
- typeof input === "string"
1895
- ? input
1896
- : input && typeof input === "object" && "message" in input
1897
- ? String((input as { message: unknown }).message)
1898
- : JSON.stringify(input ?? {});
1899
- return options.aiRuntime.runAgent(name, {
1900
- message,
1901
- auth: {
2062
+ const turn = agentTurn(input);
2063
+ if (opts?.stream) {
2064
+ let inner: (AsyncIterable<AgUiEvent> & { readonly result?: Promise<unknown> }) | undefined;
2065
+ let heldRunId: string | undefined;
2066
+ let markRun: () => void = () => undefined;
2067
+ const heldReady = new Promise<void>((resolve) => {
2068
+ markRun = resolve;
2069
+ });
2070
+ const gen = (async function* () {
2071
+ if (!options.aiRuntime) {
2072
+ await gated("ask", name, async () => {
2073
+ throw new Error(`fx.run: AI runtime is not configured for agent "${name}"`);
2074
+ });
2075
+ return;
2076
+ }
2077
+ const local = new AbortController();
2078
+ const unlink = linkAbort(currentAbortSignal(), local);
2079
+ try {
2080
+ await withAbortSignal(local.signal, async () => {
2081
+ // The inner tool loop journals each step. Recording the whole
2082
+ // stream as one ask effect would replay the run id and skip
2083
+ // the parked tool.
2084
+ capability.assert("ask", name);
2085
+ const events = await options.aiRuntime!.streamAgent(name, {
2086
+ ...turn,
2087
+ ...(opts.threadId !== undefined ? { threadId: opts.threadId } : {}),
2088
+ gates: (options.rlsGateNames ?? []).filter((gate) => gate !== "public"),
2089
+ userId: auth.userId,
2090
+ operatorId: operator.id,
2091
+ ...(options.journal ? { journal: options.journal } : {}),
2092
+ ...(options.flow !== undefined ? { flow: options.flow } : {}),
2093
+ tenantId: tenant.id,
2094
+ auth: {
2095
+ userId: auth.userId,
2096
+ scopes: auth.scopes,
2097
+ verified: auth.verified,
2098
+ },
2099
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
2100
+ recordCall: (tool, runId) => {
2101
+ void gated(
2102
+ "call",
2103
+ tool,
2104
+ async () => runId,
2105
+ undefined,
2106
+ (id) =>
2107
+ typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined,
2108
+ );
2109
+ },
2110
+ onRunId(id) {
2111
+ heldRunId = id;
2112
+ markRun();
2113
+ },
2114
+ });
2115
+ inner = events as AsyncIterable<AgUiEvent> & {
2116
+ readonly result?: Promise<unknown>;
2117
+ };
2118
+ const settled = inner.result?.then(
2119
+ () => undefined,
2120
+ () => undefined,
2121
+ );
2122
+ await Promise.race([heldReady, settled ?? Promise.resolve()]);
2123
+ await recordEffect(
2124
+ ledger,
2125
+ "ask",
2126
+ name,
2127
+ now,
2128
+ async () => heldRunId,
2129
+ undefined,
2130
+ (id) => (typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined),
2131
+ );
2132
+ });
2133
+ if (inner) yield* inner;
2134
+ } finally {
2135
+ unlink();
2136
+ if (!local.signal.aborted) local.abort();
2137
+ }
2138
+ })();
2139
+ Object.defineProperty(gen, "result", {
2140
+ enumerable: true,
2141
+ get() {
2142
+ return inner?.result;
2143
+ },
2144
+ });
2145
+ return gen;
2146
+ }
2147
+ return gated(
2148
+ "ask",
2149
+ name,
2150
+ async () => {
2151
+ if (options.aiRuntime) {
2152
+ return options.aiRuntime.runAgent(name, {
2153
+ ...turn,
2154
+ gates: (options.rlsGateNames ?? []).filter((gate) => gate !== "public"),
1902
2155
  userId: auth.userId,
1903
- scopes: auth.scopes,
1904
- verified: auth.verified,
1905
- },
1906
- callTool: (tool, toolInput) => fx.call(tool, toolInput),
1907
- });
1908
- }
1909
- return { ok: true, steps: 0, denials: [], output: input };
1910
- });
1911
- },
2156
+ operatorId: operator.id,
2157
+ ...(options.journal ? { journal: options.journal } : {}),
2158
+ ...(options.flow !== undefined ? { flow: options.flow } : {}),
2159
+ tenantId: tenant.id,
2160
+ auth: {
2161
+ userId: auth.userId,
2162
+ scopes: auth.scopes,
2163
+ verified: auth.verified,
2164
+ },
2165
+ callTool: (tool, toolInput) => fx.call(tool, toolInput),
2166
+ recordCall: (tool, runId) => {
2167
+ void gated(
2168
+ "call",
2169
+ tool,
2170
+ async () => runId,
2171
+ undefined,
2172
+ (id) =>
2173
+ typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined,
2174
+ );
2175
+ },
2176
+ });
2177
+ }
2178
+ return { ok: true, steps: 0, denials: [], output: input };
2179
+ },
2180
+ undefined,
2181
+ (result) => {
2182
+ if (!result || typeof result !== "object" || !("runId" in result)) return undefined;
2183
+ const id = (result as { runId?: unknown }).runId;
2184
+ return typeof id === "string" && id.length > 0 ? { agentRunId: id } : undefined;
2185
+ },
2186
+ );
2187
+ } as Fx["run"],
1912
2188
  stream(model, opts) {
1913
2189
  const name = resolveName(model);
1914
2190
  const chunks = (async function* () {
@@ -1953,6 +2229,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1953
2229
  });
1954
2230
  },
1955
2231
  locale,
2232
+ lastEventId: options.lastEventId,
1956
2233
  id() {
1957
2234
  return okid();
1958
2235
  },
@@ -1979,12 +2256,22 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1979
2256
  with: jsonWith,
1980
2257
  withQuery: jsonWithQuery,
1981
2258
  stream(chunks) {
1982
- return {
2259
+ const carrier = {
1983
2260
  [jsonResultBrand]: true,
1984
2261
  kind: "stream" as const,
1985
2262
  status: 200 as const,
1986
2263
  chunks,
1987
2264
  };
2265
+ Object.defineProperty(carrier, "result", {
2266
+ enumerable: true,
2267
+ get() {
2268
+ if (chunks && typeof chunks === "object" && "result" in chunks) {
2269
+ return (chunks as { readonly result?: Promise<unknown> }).result;
2270
+ }
2271
+ return undefined;
2272
+ },
2273
+ });
2274
+ return carrier as JsonStreamResult;
1988
2275
  },
1989
2276
  },
1990
2277
  async step<T>(name: string, fn: () => T | Promise<T>, opts?: StepOptions<T>): Promise<T> {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * HTTP wire-frame capture redacts credentials and keeps the rest.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ HTTP_FRAME_REDACTED,
8
+ captureHttpFrame,
9
+ redactHttpFrame,
10
+ requestForHttpInvoke,
11
+ } from "./http-frame.ts";
12
+
13
+ describe("requestForHttpInvoke", () => {
14
+ test("GET puts scalar input on the query string and keeps accept", () => {
15
+ const request = requestForHttpInvoke(
16
+ { method: "GET", path: "/tasks/:id" },
17
+ { pathParams: { id: "tsk_1" }, input: { id: "tsk_1", limit: 20 } },
18
+ );
19
+ expect(request.method).toBe("GET");
20
+ expect(new URL(request.url).pathname).toBe("/tasks/tsk_1");
21
+ expect(new URL(request.url).searchParams.get("limit")).toBe("20");
22
+ expect(new URL(request.url).searchParams.has("id")).toBe(false);
23
+ expect(request.headers.get("accept")).toBe("application/json");
24
+ expect(request.headers.get("x-oke-client")).toBe("console");
25
+ expect(request.headers.get("content-type")).toBeNull();
26
+ });
27
+ });
28
+
29
+ describe("captureHttpFrame", () => {
30
+ test("keeps method, path, query, and headers and redacts credentials", () => {
31
+ const request = new Request("http://localhost/tasks?limit=20&token=sekrit", {
32
+ method: "GET",
33
+ headers: {
34
+ accept: "application/json",
35
+ authorization: "Bearer super-secret",
36
+ cookie: "oke_session=abc",
37
+ "x-request-id": "req_1",
38
+ },
39
+ });
40
+ const response = new Response(JSON.stringify({ ok: true }), {
41
+ status: 200,
42
+ headers: {
43
+ "content-type": "application/json",
44
+ "set-cookie": "oke_session=next; HttpOnly",
45
+ },
46
+ });
47
+
48
+ expect(captureHttpFrame(request, response)).toEqual({
49
+ request: {
50
+ method: "GET",
51
+ path: "/tasks",
52
+ query: { limit: "20", token: HTTP_FRAME_REDACTED },
53
+ headers: {
54
+ accept: "application/json",
55
+ authorization: HTTP_FRAME_REDACTED,
56
+ cookie: HTTP_FRAME_REDACTED,
57
+ "x-request-id": "req_1",
58
+ },
59
+ },
60
+ response: {
61
+ status: 200,
62
+ headers: {
63
+ "content-type": "application/json",
64
+ "set-cookie": HTTP_FRAME_REDACTED,
65
+ },
66
+ },
67
+ });
68
+ });
69
+
70
+ test("redactHttpFrame masks a frame that skipped capture", () => {
71
+ expect(
72
+ redactHttpFrame({
73
+ request: {
74
+ method: "POST",
75
+ path: "/tasks",
76
+ query: {},
77
+ headers: { authorization: "Bearer leak", host: "localhost" },
78
+ },
79
+ response: { status: 201, headers: { "x-api-key": "leak" } },
80
+ }),
81
+ ).toEqual({
82
+ request: {
83
+ method: "POST",
84
+ path: "/tasks",
85
+ query: {},
86
+ headers: { authorization: HTTP_FRAME_REDACTED, host: "localhost" },
87
+ },
88
+ response: {
89
+ status: 201,
90
+ headers: { "x-api-key": HTTP_FRAME_REDACTED },
91
+ },
92
+ });
93
+ });
94
+ });