okengine 0.13.0 → 0.15.2

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 (277) hide show
  1. package/manifest.v1.schema.json +7 -1
  2. package/package.json +4 -5
  3. package/site/content/docs/ai/mcp.mdx +27 -2
  4. package/site/content/docs/elements/ai.mdx +59 -11
  5. package/site/content/docs/elements/channel.mdx +1 -0
  6. package/site/content/docs/elements/clock.mdx +30 -12
  7. package/site/content/docs/elements/flow.mdx +9 -3
  8. package/site/content/docs/elements/gate.mdx +25 -22
  9. package/site/content/docs/elements/signal.mdx +71 -25
  10. package/site/content/docs/elements/store.mdx +87 -28
  11. package/site/content/docs/elements/vault.mdx +1 -0
  12. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  13. package/site/content/docs/get-started/index.mdx +5 -0
  14. package/site/content/docs/get-started/installation.mdx +2 -2
  15. package/site/content/docs/get-started/introduction.mdx +11 -3
  16. package/site/content/docs/get-started/meta.json +1 -1
  17. package/site/content/docs/get-started/project-structure.mdx +932 -0
  18. package/site/content/docs/reference/cli.md +13 -10
  19. package/site/content/docs/reference/client.mdx +25 -21
  20. package/site/content/docs/reference/fx.mdx +27 -22
  21. package/site/content/docs/reference/security.md +1 -1
  22. package/src/auth/bindings.ts +14 -5
  23. package/src/auth/cross-plane.ts +10 -3
  24. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  25. package/src/cli/ai-setup/apply.ts +32 -1
  26. package/src/cli/ai-setup/recommend.test.ts +25 -0
  27. package/src/cli/ai-setup/recommend.ts +8 -3
  28. package/src/cli/attach-host-console.test.ts +3 -2
  29. package/src/cli/attach-host-console.ts +3 -0
  30. package/src/cli/build.test.ts +4 -2
  31. package/src/cli/db-seed.ts +48 -15
  32. package/src/cli/db.test.ts +104 -0
  33. package/src/cli/db.ts +122 -10
  34. package/src/cli/dev-controls.test.ts +20 -0
  35. package/src/cli/dev-controls.ts +13 -0
  36. package/src/cli/dev.test.ts +15 -4
  37. package/src/cli/dev.ts +102 -29
  38. package/src/cli/ensure-drizzle-config.ts +2 -1
  39. package/src/cli/tui/DevLive.tsx +12 -2
  40. package/src/cli/tui/keys.test.ts +2 -1
  41. package/src/cli/tui/keys.ts +3 -2
  42. package/src/compiler/aot.test.ts +1 -1
  43. package/src/compiler/effects-infer.ts +58 -3
  44. package/src/compiler/extract.test.ts +458 -29
  45. package/src/compiler/extract.ts +387 -69
  46. package/src/compiler/flow-path.test.ts +96 -0
  47. package/src/compiler/flow-path.ts +173 -0
  48. package/src/compiler/generate-adopt.test.ts +112 -3
  49. package/src/compiler/generate-adopt.ts +280 -26
  50. package/src/compiler/response.ts +45 -1
  51. package/src/compiler/schema-from-ast.ts +609 -0
  52. package/src/console/server/ai.ts +5 -2
  53. package/src/console/server/bind.ts +2 -2
  54. package/src/console/server/console.test.ts +3 -0
  55. package/src/console/server/flows.ts +121 -40
  56. package/src/console/server/invoke-user-flow.test.ts +3 -1
  57. package/src/console/server/invoke-user-flow.ts +17 -2
  58. package/src/console/server/iso-at.test.ts +9 -0
  59. package/src/console/server/iso-at.ts +11 -0
  60. package/src/console/server/security-headers.ts +6 -3
  61. package/src/console/server/serve.ts +17 -2
  62. package/src/console/server/spa-proxy.test.ts +1 -0
  63. package/src/console/server/sql-catalog.ts +147 -7
  64. package/src/console/server/sql-rls.test.ts +16 -0
  65. package/src/console/server/state.ts +8 -0
  66. package/src/console/server/store.test.ts +7 -4
  67. package/src/console/server/store.ts +142 -58
  68. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  69. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
  70. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  71. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  72. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  73. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  74. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  75. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  80. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  81. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
  82. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  83. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  84. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  90. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  92. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  93. package/src/console/ui-next/dist/index.html +6 -6
  94. package/src/console/ui-next/seed-invoke-host.ts +2 -0
  95. package/src/console/ui-next/src/client.ts +35 -7
  96. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  97. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  98. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  99. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
  100. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
  101. package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
  102. package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
  103. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
  104. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  105. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  106. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
  107. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
  108. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  109. package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
  110. package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
  111. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  112. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  113. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  114. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  115. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  116. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  117. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  118. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  119. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  120. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  121. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  122. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  123. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  124. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  125. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  126. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  127. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  128. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  129. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  130. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  131. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  132. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  133. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  134. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  135. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  137. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
  138. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  139. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  140. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  141. package/src/docker/compose-health.test.ts +27 -0
  142. package/src/docker/compose-health.ts +19 -3
  143. package/src/docker/docker.test.ts +4 -1
  144. package/src/docker/dockerfile.ts +1 -1
  145. package/src/docker/recipes/pgdog.ts +2 -0
  146. package/src/drivers/ai-anthropic.ts +5 -0
  147. package/src/drivers/ai-ollama.ts +49 -30
  148. package/src/drivers/ai-openai-compatible.ts +57 -46
  149. package/src/drivers/ai-providers.test.ts +3 -0
  150. package/src/drivers/bun-native-completeness.test.ts +7 -9
  151. package/src/drivers/clock-postgres.ts +13 -9
  152. package/src/drivers/instances-postgres.ts +4 -6
  153. package/src/drivers/journal-postgres.ts +13 -9
  154. package/src/drivers/pg-rls.test.ts +87 -0
  155. package/src/drivers/pg-rls.ts +233 -7
  156. package/src/drivers/pglite.ts +26 -3
  157. package/src/drivers/postgres.test.ts +23 -0
  158. package/src/drivers/postgres.ts +133 -9
  159. package/src/drivers/redis.ts +11 -4
  160. package/src/drivers/signal-redis.ts +24 -14
  161. package/src/drivers/signal-types.ts +2 -1
  162. package/src/drivers/types.ts +10 -0
  163. package/src/elements/ai/declare.ts +109 -0
  164. package/src/elements/ai/errors.test.ts +5 -1
  165. package/src/elements/ai/errors.ts +30 -2
  166. package/src/elements/ai/eval.ts +4 -6
  167. package/src/elements/ai/mcp-client.test.ts +206 -0
  168. package/src/elements/ai/mcp-client.ts +362 -0
  169. package/src/elements/ai/mcp-http.ts +159 -0
  170. package/src/elements/ai/mcp-mock.ts +134 -0
  171. package/src/elements/ai/mcp-protocol.ts +234 -0
  172. package/src/elements/ai/mcp-stdio.test.ts +50 -0
  173. package/src/elements/ai/mcp-stdio.ts +212 -0
  174. package/src/elements/ai/mcp-transport.ts +70 -0
  175. package/src/elements/ai/runtime.ts +159 -29
  176. package/src/elements/ai.test.ts +139 -0
  177. package/src/elements/ai.ts +16 -0
  178. package/src/elements/clock/declare.ts +26 -1
  179. package/src/elements/clock/health.test.ts +43 -0
  180. package/src/elements/clock/runtime.ts +55 -2
  181. package/src/elements/clock/schedule.ts +71 -142
  182. package/src/elements/clock.test.ts +9 -40
  183. package/src/elements/clock.ts +6 -1
  184. package/src/elements/gate/boot.ts +5 -5
  185. package/src/elements/gate/declare.ts +36 -4
  186. package/src/elements/gate/flatten.ts +1 -1
  187. package/src/elements/gate/runtime.ts +1 -1
  188. package/src/elements/index.ts +2 -0
  189. package/src/elements/signal/declare.ts +2 -1
  190. package/src/elements/signal/runtime.ts +16 -1
  191. package/src/elements/signal.ts +1 -0
  192. package/src/elements/store/cache.test.ts +2 -0
  193. package/src/elements/store/cache.ts +3 -3
  194. package/src/elements/store/declare.ts +9 -6
  195. package/src/elements/store/emit-drizzle.ts +44 -8
  196. package/src/elements/store/resource.test.ts +1 -1
  197. package/src/elements/store/rls-identity.test.ts +95 -0
  198. package/src/elements/store/rls-identity.ts +116 -0
  199. package/src/elements/store/runtime.ts +4 -0
  200. package/src/elements/store/schema-decl.test.ts +37 -0
  201. package/src/elements/store/schema-decl.ts +161 -0
  202. package/src/elements/store/seed.ts +1 -1
  203. package/src/elements/store/sql-condition.test.ts +12 -0
  204. package/src/elements/store/sql-condition.ts +26 -2
  205. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  206. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  207. package/src/elements/store/sql-session.ts +81 -3
  208. package/src/elements/store/upsert-app.test.ts +1 -1
  209. package/src/elements/store.ts +14 -0
  210. package/src/elements/vault/builtin-adapter.ts +17 -0
  211. package/src/full.ts +2 -0
  212. package/src/http.ts +20 -3
  213. package/src/index.ts +14 -1
  214. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  215. package/src/kernel/adopt-routes.ts +30 -2
  216. package/src/kernel/app.ts +227 -77
  217. package/src/kernel/auto-cache.test.ts +8 -8
  218. package/src/kernel/auto-registry.test.ts +54 -9
  219. package/src/kernel/boot-bind/ai.ts +24 -0
  220. package/src/kernel/boot-bind/clock.ts +2 -0
  221. package/src/kernel/boot.test.ts +4 -4
  222. package/src/kernel/boot.ts +5 -3
  223. package/src/kernel/effects-stamping.test.ts +3 -3
  224. package/src/kernel/element-registries.ts +15 -5
  225. package/src/kernel/errors.ts +24 -0
  226. package/src/kernel/flow-units.ts +64 -0
  227. package/src/kernel/flow.ts +29 -17
  228. package/src/kernel/fx-dead-letters.test.ts +77 -0
  229. package/src/kernel/fx.test.ts +22 -0
  230. package/src/kernel/fx.ts +135 -6
  231. package/src/kernel/horizontal-child.ts +4 -4
  232. package/src/kernel/http-path-pending.ts +20 -0
  233. package/src/kernel/http-resource.ts +4 -0
  234. package/src/kernel/http-stream.test.ts +174 -0
  235. package/src/kernel/index.ts +2 -0
  236. package/src/kernel/pipeline.test.ts +2 -2
  237. package/src/kernel/plugin-elements.test.ts +1 -1
  238. package/src/kernel/plugin-needs.test.ts +1 -1
  239. package/src/kernel/ready.test.ts +2 -2
  240. package/src/kernel/resource-mount.test.ts +14 -0
  241. package/src/kernel/run-duration.test.ts +2 -2
  242. package/src/kernel/stamp-http.test.ts +79 -0
  243. package/src/kernel/stamp-http.ts +75 -0
  244. package/src/kernel/triggers.ts +52 -27
  245. package/src/manifest/mcp-ref.ts +88 -0
  246. package/src/manifest/types.ts +41 -2
  247. package/src/manifest/validate.test.ts +20 -0
  248. package/src/mcp/docs-server.ts +1 -1
  249. package/src/mcp/server.ts +1 -1
  250. package/src/plugins/compression.test.ts +21 -0
  251. package/src/plugins/compression.ts +1 -0
  252. package/src/release/measure.ts +2 -2
  253. package/src/runtime/bun.ts +41 -4
  254. package/src/runtime/json-code-block.test.ts +2 -2
  255. package/src/runtime/serve.test.ts +4 -4
  256. package/src/term.test.ts +52 -4
  257. package/src/term.ts +125 -17
  258. package/src/test/reset-element-registries.ts +10 -3
  259. package/src/cli/ask-seed.test.ts +0 -96
  260. package/src/cli/ask-seed.ts +0 -82
  261. package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
  262. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  263. package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
  264. package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
  265. package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
  266. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  267. package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
  268. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  269. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  270. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  271. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  272. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  273. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  274. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  275. package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
  276. package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
  277. package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
@@ -62,6 +62,16 @@ export interface SqlConnection {
62
62
  * @param params - Bound parameters
63
63
  */
64
64
  exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
65
+ /**
66
+ * Pin `fn` to one backend connection for a single transaction.
67
+ *
68
+ * Required for `SET LOCAL` RLS stamps on pooled `postgres` (Bun.SQL /
69
+ * PgDog transaction mode). A standalone `BEGIN` via {@link exec} returns
70
+ * the connection to the pool still in a transaction and exhausts checkout.
71
+ *
72
+ * @param fn - Work that must share the reserved connection
73
+ */
74
+ transaction?<T>(fn: (tx: SqlConnection) => Promise<T>): Promise<T>;
65
75
  /** Close the connection. */
66
76
  close(): Promise<void>;
67
77
  }
@@ -7,9 +7,12 @@
7
7
  import {
8
8
  aiAgentRegistry,
9
9
  aiEmbedRegistry,
10
+ aiMcpServerRegistry,
10
11
  aiModelRegistry,
11
12
  aiPromptRegistry,
12
13
  } from "../../kernel/element-registries.ts";
14
+ import { mcpToolRef, type McpToolRef } from "../../manifest/mcp-ref.ts";
15
+ import type { VaultSecretDecl } from "../vault/declare.ts";
13
16
 
14
17
  /** Budget for a prompt or agent. */
15
18
  export interface AiBudgetDecl {
@@ -30,6 +33,11 @@ export interface AiModelOptions {
30
33
  readonly baseUrl?: string;
31
34
  /** Optional API key override for this binding (cloud providers). */
32
35
  readonly apiKey?: string;
36
+ /**
37
+ * Protocol driver for this binding (`anthropic`, `ollama`, …).
38
+ * When omitted, the app-level default driver is used.
39
+ */
40
+ readonly driverId?: string;
33
41
  }
34
42
 
35
43
  /**
@@ -77,6 +85,7 @@ export interface AiModelDecl {
77
85
  readonly model?: string;
78
86
  readonly baseUrl?: string;
79
87
  readonly apiKey?: string;
88
+ readonly driverId?: string;
80
89
  /**
81
90
  * Declare a versioned prompt artifact on this model.
82
91
  *
@@ -118,6 +127,54 @@ export interface AiAgentDecl {
118
127
  readonly budget?: AiBudgetDecl;
119
128
  }
120
129
 
130
+ /** Bearer auth for {@link ai.mcpServer} — secret contract, never a token literal. */
131
+ export interface AiMcpServerAuthOptions {
132
+ readonly bearer: VaultSecretDecl | string;
133
+ }
134
+
135
+ /** Options for {@link ai.mcpServer}. */
136
+ export interface AiMcpServerOptions {
137
+ /** Streamable HTTP endpoint. */
138
+ readonly url?: string;
139
+ /** stdio executable (no shell string). */
140
+ readonly command?: string;
141
+ /** Arguments for {@link command}. */
142
+ readonly args?: readonly string[];
143
+ /** Bearer secret contract (`vault.secret` handle or name). */
144
+ readonly auth?: AiMcpServerAuthOptions;
145
+ /**
146
+ * Required allowlist of tool names on this server.
147
+ * The runtime never offers whatever `tools/list` happens to expose.
148
+ */
149
+ readonly tools: readonly string[];
150
+ }
151
+
152
+ /** Named capability ref returned by {@link AiMcpServerDecl.tool}. */
153
+ export interface AiMcpToolRef {
154
+ readonly name: McpToolRef;
155
+ }
156
+
157
+ /**
158
+ * Declared external MCP server — tools join `fx.call` / `toolLoop` as
159
+ * `mcp:<server>/<tool>`.
160
+ */
161
+ export interface AiMcpServerDecl {
162
+ readonly kind: "mcp-server";
163
+ readonly name: string;
164
+ readonly url?: string;
165
+ readonly command?: string;
166
+ readonly args?: readonly string[];
167
+ /** Secret contract name when bearer auth is declared. */
168
+ readonly auth?: string;
169
+ readonly tools: readonly string[];
170
+ /**
171
+ * Capability ref for one allowlisted tool (`mcp:<server>/<tool>`).
172
+ *
173
+ * @param tool - Tool name on this server
174
+ */
175
+ tool(tool: string): AiMcpToolRef;
176
+ }
177
+
121
178
  /**
122
179
  * Resolve a tool ref to a flow name.
123
180
  *
@@ -152,6 +209,13 @@ export interface AiNamespace {
152
209
  * @param options - Tools / maxSteps / model / budget
153
210
  */
154
211
  agent(name: string, options?: AiAgentOptions): AiAgentDecl;
212
+ /**
213
+ * Declare an external MCP server whose allowlisted tools join `fx.call`.
214
+ *
215
+ * @param name - Server id (`github`, `linear`, …)
216
+ * @param options - Transport + required tool allowlist
217
+ */
218
+ mcpServer(name: string, options: AiMcpServerOptions): AiMcpServerDecl;
155
219
  }
156
220
 
157
221
  /**
@@ -162,12 +226,14 @@ export function listAiDecls(): {
162
226
  readonly prompts: readonly AiPromptDecl[];
163
227
  readonly embeds: readonly AiEmbedDecl[];
164
228
  readonly agents: readonly AiAgentDecl[];
229
+ readonly mcpServers: readonly AiMcpServerDecl[];
165
230
  } {
166
231
  return {
167
232
  models: aiModelRegistry.slice(),
168
233
  prompts: aiPromptRegistry.slice(),
169
234
  embeds: aiEmbedRegistry.slice(),
170
235
  agents: aiAgentRegistry.slice(),
236
+ mcpServers: aiMcpServerRegistry.slice(),
171
237
  };
172
238
  }
173
239
 
@@ -179,6 +245,7 @@ export function resetAiDecls(): void {
179
245
  aiPromptRegistry.length = 0;
180
246
  aiEmbedRegistry.length = 0;
181
247
  aiAgentRegistry.length = 0;
248
+ aiMcpServerRegistry.length = 0;
182
249
  }
183
250
 
184
251
  /**
@@ -201,6 +268,7 @@ export const ai: AiNamespace = {
201
268
  ...(options.model !== undefined ? { model: options.model } : {}),
202
269
  ...(options.baseUrl !== undefined ? { baseUrl: options.baseUrl } : {}),
203
270
  ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
271
+ ...(options.driverId !== undefined ? { driverId: options.driverId } : {}),
204
272
  prompt(promptName, promptOpts = {}) {
205
273
  const promptDecl: AiPromptDecl = {
206
274
  kind: "prompt",
@@ -263,4 +331,45 @@ export const ai: AiNamespace = {
263
331
  aiAgentRegistry.push(decl);
264
332
  return decl;
265
333
  },
334
+
335
+ /**
336
+ * Declare an external MCP server whose allowlisted tools join `fx.call`.
337
+ *
338
+ * @param name - Server id
339
+ * @param options - Transport + required tool allowlist
340
+ */
341
+ mcpServer(name: string, options: AiMcpServerOptions): AiMcpServerDecl {
342
+ if (!name) throw new TypeError("ai.mcpServer: name is required");
343
+ if (name.includes("/") || name.includes("__")) {
344
+ throw new TypeError(`ai.mcpServer: name "${name}" must not contain "/" or "__"`);
345
+ }
346
+ if (!options.tools || !Array.isArray(options.tools)) {
347
+ throw new TypeError("ai.mcpServer: tools allowlist is required");
348
+ }
349
+ const hasUrl = typeof options.url === "string" && options.url.length > 0;
350
+ const hasCommand = typeof options.command === "string" && options.command.length > 0;
351
+ if (hasUrl === hasCommand) {
352
+ throw new TypeError("ai.mcpServer: declare exactly one of url or command");
353
+ }
354
+ const allow = new Set(options.tools);
355
+ const authName =
356
+ typeof options.auth?.bearer === "string" ? options.auth.bearer : options.auth?.bearer?.name;
357
+ const decl: AiMcpServerDecl = {
358
+ kind: "mcp-server",
359
+ name,
360
+ ...(hasUrl ? { url: options.url } : {}),
361
+ ...(hasCommand ? { command: options.command } : {}),
362
+ ...(options.args !== undefined ? { args: options.args } : {}),
363
+ ...(authName !== undefined ? { auth: authName } : {}),
364
+ tools: options.tools,
365
+ tool(tool: string): AiMcpToolRef {
366
+ if (!allow.has(tool)) {
367
+ throw new TypeError(`ai.mcpServer("${name}"): tool "${tool}" is not in the allowlist`);
368
+ }
369
+ return { name: mcpToolRef(name, tool) };
370
+ },
371
+ };
372
+ aiMcpServerRegistry.push(decl);
373
+ return decl;
374
+ },
266
375
  };
@@ -22,7 +22,11 @@ describe("isRetryableAiError", () => {
22
22
  expect(isRetryableAiError(new Error("openai-compatible HTTP 401"))).toBe(false);
23
23
  const abort = new Error("aborted");
24
24
  abort.name = "AbortError";
25
- expect(isRetryableAiError(abort)).toBe(true);
25
+ expect(isRetryableAiError(abort)).toBe(false);
26
+ const timeout = new Error("deadline");
27
+ timeout.name = "TimeoutError";
28
+ expect(isRetryableAiError(timeout)).toBe(true);
29
+ expect(isRetryableAiError(new Error("request timeout"))).toBe(true);
26
30
  });
27
31
  });
28
32
 
@@ -74,8 +74,10 @@ export function isRetryableAiError(err: unknown): boolean {
74
74
  if (code >= 400 && code <= 499) return false;
75
75
  }
76
76
 
77
- if (name === "AbortError" || name === "TimeoutError") return true;
78
- if (lower.includes("aborterror") || lower.includes("timeout")) return true;
77
+ if (name === "TimeoutError" || isTimeoutAbort(err)) return true;
78
+ // Client disconnect / explicit cancel must not retry or advance `via`.
79
+ if (name === "AbortError") return false;
80
+ if (name === "AiBudgetExceededError") return false;
79
81
  if (
80
82
  lower.includes("econnreset") ||
81
83
  lower.includes("econnrefused") ||
@@ -103,6 +105,32 @@ export function aiHttpError(message: string, status: number): Error {
103
105
  return err;
104
106
  }
105
107
 
108
+ /**
109
+ * True when an abort came from a deadline (`AbortSignal.timeout`), not a
110
+ * client disconnect / explicit cancel.
111
+ *
112
+ * @param err - Thrown value
113
+ */
114
+ function isTimeoutAbort(err: unknown): boolean {
115
+ if (err == null || typeof err !== "object") return false;
116
+ const name = err instanceof Error ? err.name : "";
117
+ const message = err instanceof Error ? err.message.toLowerCase() : "";
118
+ if (name === "TimeoutError") return true;
119
+ if (message.includes("timeout")) return true;
120
+ const cause = "cause" in err ? (err as { cause: unknown }).cause : undefined;
121
+ if (cause instanceof Error && (cause.name === "TimeoutError" || /timeout/i.test(cause.message))) {
122
+ return true;
123
+ }
124
+ const reason = "reason" in err ? (err as { reason: unknown }).reason : undefined;
125
+ if (
126
+ reason instanceof Error &&
127
+ (reason.name === "TimeoutError" || /timeout/i.test(reason.message))
128
+ ) {
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+
106
134
  function readStatus(err: unknown): number | undefined {
107
135
  if (typeof err !== "object" || err === null) return undefined;
108
136
  const status = (err as AiErrorFields).status;
@@ -124,11 +124,9 @@ export async function runPromptEvals(options: RunPromptEvalsOptions): Promise<Ev
124
124
  * @param text - File text
125
125
  */
126
126
  export function parseEvalJsonl(text: string): EvalCase[] {
127
- const cases: EvalCase[] = [];
128
- for (const line of text.split("\n")) {
129
- const trimmed = line.trim();
130
- if (!trimmed) continue;
131
- cases.push(JSON.parse(trimmed) as EvalCase);
127
+ const jsonl = (Bun as typeof Bun & { JSONL?: { parse: (t: string) => unknown[] } }).JSONL;
128
+ if (!jsonl) {
129
+ throw new Error("ai.eval: Bun.JSONL is required (Bun >= 1.4.0)");
132
130
  }
133
- return cases;
131
+ return jsonl.parse(text) as EvalCase[];
134
132
  }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * MCP client — allowlist, era fallback, abort, capability.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { createMockAiDriver } from "../../drivers/ai-mock.ts";
7
+ import { createFx } from "../../kernel/fx.ts";
8
+ import {
9
+ mcpCapabilityRefFromName,
10
+ mcpModelToolName,
11
+ parseMcpToolRef,
12
+ } from "../../manifest/mcp-ref.ts";
13
+ import { ai } from "./declare.ts";
14
+ import { createMcpClient } from "./mcp-client.ts";
15
+ import { createMockMcpTransport } from "./mcp-mock.ts";
16
+ import { createAiRuntime } from "./runtime.ts";
17
+
18
+ describe("mcp refs", () => {
19
+ test("parses capability and model names", () => {
20
+ expect(parseMcpToolRef("mcp:github/create_issue")).toEqual({
21
+ server: "github",
22
+ tool: "create_issue",
23
+ });
24
+ expect(mcpModelToolName("github", "create_issue")).toBe("github__create_issue");
25
+ expect(mcpCapabilityRefFromName("github__create_issue")).toBe("mcp:github/create_issue");
26
+ expect(parseMcpToolRef("github/create_issue")).toBeNull();
27
+ });
28
+ });
29
+
30
+ describe("mcp client allowlist", () => {
31
+ test("drops extras from tools/list and calls only allowlisted tools", async () => {
32
+ const github = ai.mcpServer("github", {
33
+ url: "https://mcp.example/github",
34
+ tools: ["create_issue"],
35
+ });
36
+ const called: string[] = [];
37
+ const transport = createMockMcpTransport({
38
+ tools: [
39
+ { name: "create_issue", description: "Open an issue" },
40
+ { name: "delete_repo", description: "should be dropped" },
41
+ ],
42
+ extraTools: [{ name: "admin_wipe" }],
43
+ onCall: (name) => {
44
+ called.push(name);
45
+ return { content: [{ type: "text", text: "opened" }] };
46
+ },
47
+ });
48
+ const client = createMcpClient({
49
+ servers: [github],
50
+ transports: { github: transport },
51
+ });
52
+ const listed = await client.listedTool({ server: "github", tool: "create_issue" });
53
+ expect(listed?.name).toBe("create_issue");
54
+ expect(await client.listedTool({ server: "github", tool: "delete_repo" })).toBeUndefined();
55
+ await client.call("mcp:github/create_issue", { title: "bug" });
56
+ expect(called).toEqual(["create_issue"]);
57
+ await expect(client.call("mcp:github/delete_repo", {})).rejects.toThrow(/not allowlisted/);
58
+ });
59
+ });
60
+
61
+ describe("mcp era fallback", () => {
62
+ test("400 + UnsupportedProtocolVersion stays modern (no initialize)", async () => {
63
+ const methods: string[] = [];
64
+ const github = ai.mcpServer("github", {
65
+ url: "https://mcp.example/github",
66
+ tools: ["create_issue"],
67
+ });
68
+ const transport = createMockMcpTransport({
69
+ tools: [{ name: "create_issue" }],
70
+ httpStatusOnFirst: 400,
71
+ firstError: { code: -32022, message: "UnsupportedProtocolVersion" },
72
+ });
73
+ const wrapped = {
74
+ kind: "mock" as const,
75
+ async request(req: Parameters<typeof transport.request>[0]) {
76
+ methods.push(req.method);
77
+ return transport.request(req);
78
+ },
79
+ close: () => transport.close(),
80
+ };
81
+ const client = createMcpClient({
82
+ servers: [github],
83
+ transports: { github: wrapped },
84
+ });
85
+ await expect(client.listedTool({ server: "github", tool: "create_issue" })).rejects.toThrow(
86
+ /UnsupportedProtocolVersion/,
87
+ );
88
+ expect(methods).toEqual(["tools/list"]);
89
+ });
90
+
91
+ test("400 without a modern body falls back to initialize", async () => {
92
+ const methods: string[] = [];
93
+ const github = ai.mcpServer("github", {
94
+ url: "https://mcp.example/github",
95
+ tools: ["create_issue"],
96
+ });
97
+ const transport = createMockMcpTransport({
98
+ era: "legacy",
99
+ tools: [{ name: "create_issue" }],
100
+ httpStatusOnFirst: 400,
101
+ firstBody: "bad request",
102
+ });
103
+ const wrapped = {
104
+ kind: "mock" as const,
105
+ async request(req: Parameters<typeof transport.request>[0]) {
106
+ methods.push(req.method);
107
+ return transport.request(req);
108
+ },
109
+ close: () => transport.close(),
110
+ };
111
+ const client = createMcpClient({
112
+ servers: [github],
113
+ transports: { github: wrapped },
114
+ });
115
+ const listed = await client.listedTool({ server: "github", tool: "create_issue" });
116
+ expect(listed?.name).toBe("create_issue");
117
+ expect(methods).toEqual(["tools/list", "initialize", "tools/list"]);
118
+ });
119
+ });
120
+
121
+ describe("mcp abort", () => {
122
+ test("HTTP cancel aborts an in-flight mock request", async () => {
123
+ const github = ai.mcpServer("github", {
124
+ url: "https://mcp.example/github",
125
+ tools: ["create_issue"],
126
+ });
127
+ const transport = createMockMcpTransport({
128
+ tools: [{ name: "create_issue" }],
129
+ delayMs: 5_000,
130
+ });
131
+ const client = createMcpClient({
132
+ servers: [github],
133
+ transports: { github: transport },
134
+ });
135
+ const ctrl = new AbortController();
136
+ const pending = client.call("mcp:github/create_issue", {}, ctrl.signal);
137
+ ctrl.abort();
138
+ await expect(pending).rejects.toMatchObject({ name: "AbortError" });
139
+ });
140
+ });
141
+
142
+ describe("mcp capability + toolLoop", () => {
143
+ test("undeclared MCP tool throws OKE1007", async () => {
144
+ const github = ai.mcpServer("github", {
145
+ url: "https://mcp.example/github",
146
+ tools: ["create_issue"],
147
+ });
148
+ const runtime = createAiRuntime({
149
+ mcpServers: [github],
150
+ mcpTransports: { github: createMockMcpTransport({ tools: [{ name: "create_issue" }] }) },
151
+ });
152
+ const fx = createFx({
153
+ flow: "support.triage",
154
+ effects: { asks: ["triage"] },
155
+ aiRuntime: runtime,
156
+ });
157
+ try {
158
+ await fx.call(github.tool("create_issue"), { title: "x" });
159
+ throw new Error("expected OKE1007");
160
+ } catch (err) {
161
+ expect(err).toMatchObject({ code: 1007 });
162
+ }
163
+ });
164
+
165
+ test("model-facing server__tool dispatches through fx.call as mcp: ref", async () => {
166
+ const github = ai.mcpServer("github", {
167
+ url: "https://mcp.example/github",
168
+ tools: ["create_issue"],
169
+ });
170
+ const called: string[] = [];
171
+ const runtime = createAiRuntime({
172
+ models: [ai.model("smart")],
173
+ prompts: [ai.model("smart").prompt("triage")],
174
+ mcpServers: [github],
175
+ mcpTransports: {
176
+ github: createMockMcpTransport({
177
+ tools: [{ name: "create_issue", description: "Open an issue" }],
178
+ onCall: (name) => {
179
+ called.push(name);
180
+ return { content: [{ type: "text", text: "ok" }] };
181
+ },
182
+ }),
183
+ },
184
+ defaultDriver: createMockAiDriver({
185
+ "*": {
186
+ __toolCalls: [{ id: "c1", name: "github__create_issue", arguments: { title: "bug" } }],
187
+ },
188
+ }),
189
+ });
190
+ const fx = createFx({
191
+ flow: "support.triage",
192
+ effects: { asks: ["triage"], calls: ["mcp:github/create_issue"] },
193
+ aiRuntime: runtime,
194
+ });
195
+ await runtime.ask(
196
+ "triage",
197
+ { q: "open" },
198
+ {
199
+ tools: ["mcp:github/create_issue"],
200
+ maxSteps: 1,
201
+ callTool: (name, input) => fx.call(name, input),
202
+ },
203
+ );
204
+ expect(called).toEqual(["create_issue"]);
205
+ });
206
+ });