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
@@ -111,6 +111,11 @@
111
111
  "type": "string",
112
112
  "pattern": "^(sql|kv|files|index):.+$"
113
113
  },
114
+ "SignalResourceRef": {
115
+ "description": "Dead-letter read via fx.deadLetters — signal:<name>, not a store facet.",
116
+ "type": "string",
117
+ "pattern": "^signal:.+$"
118
+ },
114
119
  "SignalRef": {
115
120
  "type": "string",
116
121
  "minLength": 1
@@ -145,7 +150,8 @@
145
150
  {
146
151
  "const": "runs",
147
152
  "description": "Runs wide-event store via fx.runs (observability read)."
148
- }
153
+ },
154
+ { "$ref": "#/$defs/SignalResourceRef" }
149
155
  ]
150
156
  },
151
157
  "uniqueItems": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.13.0",
3
+ "version": "0.15.2",
4
4
  "description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -174,10 +174,9 @@
174
174
  "d3-array": "^3.2.4",
175
175
  "d3-shape": "^3.2.0",
176
176
  "date-fns": "^4.1.0",
177
- "drizzle-kit": "1.0.0-rc.4",
178
- "drizzle-orm": "1.0.0-rc.4",
177
+ "drizzle-kit": "1.0.0-rc.5-ab785fc",
178
+ "drizzle-orm": "1.0.0-rc.5-169397b",
179
179
  "drizzle-seed": "^0.3.1",
180
- "drizzle-zod": "^0.8.3",
181
180
  "gflows": "^1.2.1",
182
181
  "happy-dom": "^20.11.1",
183
182
  "hast-util-to-jsx-runtime": "^2.3.6",
@@ -271,6 +270,6 @@
271
270
  "react": "^19.2.8"
272
271
  },
273
272
  "engines": {
274
- "bun": ">=1.3.14"
273
+ "bun": ">=1.4.0"
275
274
  }
276
275
  }
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  title: "MCP"
3
- description: "The runtime MCP server on :6535 read tools for Manifest, schemas, effects, and traces, write actions gated by per-call human confirmation plus the read-only docs MCP on :6536."
3
+ description: "OKE serves MCP on :6535 so agents can operate a running app, and apps consume external MCP servers as allowlisted tools on the same fx.call path."
4
4
  icon: "Plug"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- OKE serves an [MCP](https://modelcontextprotocol.io) endpoint on port **6535** so an AI agent can operate your running app: read its Manifest and traces, and take a small set of safe actions. It speaks JSON-RPC over HTTP (MCP protocol `2024-11-05`), requires a Bearer token **even on localhost**, and never forwards that token upstream — adapters receive structured operator ids instead.
8
+ Two directions, same protocol. **Serve:** OKE exposes your running app on port **6535** so an agent can read the Manifest and take confirmed actions. **Consume:** your Flows call _external_ MCP servers as tools declared on `ai.mcpServer`, dispatched through `fx.call`.
9
+
10
+ The server on **6535** speaks JSON-RPC over HTTP (MCP protocol `2024-11-05`), requires a Bearer token **even on localhost**, and never forwards that token upstream — adapters receive structured operator ids instead.
9
11
 
10
12
  <Callout title="The one rule">
11
13
  MCP inherits the operator's capability and can never exceed it. Server-level controls alone are
@@ -96,6 +98,29 @@ Consumed tokens cannot be replayed; the next write needs a new confirmation. A m
96
98
 
97
99
  The docs content ships inside the `okengine` package, so the index your agent searches is exactly the version you have installed. If the surface cannot boot (missing content, busy port), `oke dev` prints `Docs MCP skipped — …` and continues — docs search never takes your dev session down.
98
100
 
101
+ ## Consume — `ai.mcpServer`
102
+
103
+ Your app can _call_ other MCP servers. Those tools are not a second loop — they join `fx.ask` / `ai.agent` the same way a Flow tool does.
104
+
105
+ ```typescript
106
+ export const github = ai.mcpServer("github", {
107
+ url: "https://mcp.example/github",
108
+ auth: { bearer: githubToken },
109
+ tools: ["create_issue"], // required allowlist
110
+ });
111
+
112
+ await fx.ask(triage, input, { tools: [github.tool("create_issue")] });
113
+ ```
114
+
115
+ | Rule | Meaning |
116
+ | ---------- | ------------------------------------------------------------------------------------------------- |
117
+ | Allowlist | `tools` is required. Extra names from `tools/list` are dropped. |
118
+ | Capability | `mcp:<server>/<tool>` on `effects.calls` — undeclared throws **OKE1007**. |
119
+ | Transport | `url` (Streamable HTTP) **or** `command` + `args` (stdio). Not both. |
120
+ | Cancel | HTTP aborts the fetch / SSE stream. stdio sends `notifications/cancelled` then kills the process. |
121
+
122
+ Console draws each declared server as one **AI** node on the flow graph; Units chips read `Call github → create_issue`; traces label the effect **MCP call**. There is no separate MCP page or connect UI.
123
+
99
124
  ## Learn more
100
125
 
101
126
  - [Agent contracts](/docs/ai/skills) — what agents are taught about the system they operate
@@ -68,14 +68,15 @@ export const triage = smart.prompt("ticket-triage", {
68
68
 
69
69
  </Steps>
70
70
 
71
- ## The four building blocks
71
+ ## The building blocks
72
72
 
73
- | Declaration | Produces |
74
- | -------------------------- | ------------------------------------------------------------------ |
75
- | `ai.model(name, opts)` | Logical model binding — provider / tier / concrete model id |
76
- | `model.prompt(name, opts)` | Versioned prompt with typed in/out, evals, budget |
77
- | `ai.embed(name, opts)` | Embedding pipeline into a `store.index` (searched via `fx.search`) |
78
- | `ai.agent(name, opts)` | Bounded agent whose tools are **your own flows** |
73
+ | Declaration | Produces |
74
+ | -------------------------- | ------------------------------------------------------------------------------- |
75
+ | `ai.model(name, opts)` | Logical model binding — provider / tier / model id / optional `driverId` |
76
+ | `model.prompt(name, opts)` | Versioned prompt with typed in/out, evals, budget |
77
+ | `ai.embed(name, opts)` | Embedding pipeline into a `store.index` (searched via `fx.search`) |
78
+ | `ai.agent(name, opts)` | Bounded agent whose tools are **your own flows** |
79
+ | `ai.mcpServer(name, opts)` | External MCP server — allowlisted tools join `fx.call` as `mcp:<server>/<tool>` |
79
80
 
80
81
  <AiBlocks />
81
82
 
@@ -105,9 +106,11 @@ const out = await fx.ask(summarizeNote, { title, body });
105
106
  // out.via is the logical model that answered
106
107
  ```
107
108
 
108
- Resolution: `fx.ask(…, { via })` overrides `prompt.via`, else the prompt’s bound model. On each model the runtime retries **once** for retryable failures (timeout / 429 / 5xx / network), then advances. Permanent failures (401 / other 4xx / schema invalid) stop the chain. When every eligible attempt fails, `fx.ask` throws — map that to a typed error in the flow (no silent text excerpt).
109
+ Resolution: `fx.ask(…, { via })` overrides `prompt.via`, else the prompt’s bound model. On each model the runtime retries **once** for retryable failures (timeout / 429 / 5xx / network), then advances.
109
110
 
110
- `timeout` uses the same duration vocabulary as Clock (`"30s"`, `"2m"`, or a millisecond number). Ask-time `fx.ask(…, { timeout: "10s" })` overrides the prompt. Omit = no artificial cap. Cost caps stay on `budget`.
111
+ Permanent failures (401 / other 4xx / schema invalid / client disconnect / `maxCostPerCall` exceeded) stop the chain. When every eligible attempt fails, `fx.ask` throws map that to a typed error in the flow (no silent text excerpt).
112
+
113
+ `timeout` uses the same duration vocabulary as Clock (`"30s"`, `"2m"`, or a millisecond number). Ask-time `fx.ask(…, { timeout: "10s" })` overrides the prompt. Omit = no artificial cap. `budget.maxCostPerCall` is enforced at runtime (not just observed). Set `driverId` on a model to open a different protocol than the app default.
111
114
 
112
115
  ## Declared guardrails
113
116
 
@@ -126,6 +129,22 @@ const result = await fx.ask(triage, input, {
126
129
  });
127
130
  ```
128
131
 
132
+ External MCP tools take the same path. Declare a server with a **required** allowlist, then pass `server.tool("…")` — never whatever `tools/list` happens to expose:
133
+
134
+ ```typescript
135
+ export const github = ai.mcpServer("github", {
136
+ url: "https://mcp.example/github",
137
+ auth: { bearer: githubToken },
138
+ tools: ["create_issue"],
139
+ });
140
+
141
+ await fx.ask(triage, input, {
142
+ tools: [github.tool("create_issue")],
143
+ });
144
+ ```
145
+
146
+ Capability refs are `mcp:<server>/<tool>`. An undeclared call throws **OKE1007**. See [MCP](/docs/ai/mcp).
147
+
129
148
  ## Agents with real guardrails
130
149
 
131
150
  An agent's tools are your flows — each carrying its own gates, effects, and typed errors, so the agent can never do anything a flow couldn't:
@@ -139,7 +158,7 @@ export const support = ai.agent("support", {
139
158
  });
140
159
  ```
141
160
 
142
- `maxSteps` bounds the loop; `budget.maxCostPerRun` bounds the spend. Both are declared, so "the agent ran away" is a violated contract, not a surprise. `fx.run(support, { message })` uses the same tool loop and the same `fx.call` dispatch as `fx.ask(…, { tools })`.
161
+ `maxSteps` bounds the loop; `budget.maxCostPerRun` is enforced after the run. Both are declared, so "the agent ran away" is a violated contract, not a surprise. `fx.run(support, { message })` uses the same tool loop and the same `fx.call` dispatch as `fx.ask(…, { tools })`.
143
162
 
144
163
  ## Rate limits via Gate
145
164
 
@@ -157,7 +176,20 @@ Use `keyBy: "ip"` on public unauthenticated AI edges. Cost caps stay on prompt/a
157
176
 
158
177
  ## Streaming
159
178
 
160
- `fx.stream(model, { prompt })` yields real provider tokens (Ollama NDJSON, OpenAI-compatible SSE). Cancel by aborting the ambient signal — the same one `fx.all` / `fx.race` already use. Drivers without `stream` fail loud (no stub echo).
179
+ `fx.stream(model, { prompt, via? })` yields real provider tokens (Ollama NDJSON, OpenAI-compatible SSE). Cancel by aborting the ambient signal — the same one `fx.all` / `fx.race` already use. Drivers without `stream` fail loud (no stub echo).
180
+
181
+ Return those chunks on HTTP with `fx.json.stream(...)`. The kernel answers `text/event-stream` (`data:` frames, then `data: [DONE]`). JSON flows stay buffered `{ data, error }`.
182
+
183
+ ```typescript
184
+ on(
185
+ http.post("/complete").public(),
186
+ flow("chat.complete", {
187
+ do: (input, fx) => fx.json.stream(fx.stream(smart, { prompt: input.prompt })),
188
+ }),
189
+ );
190
+ ```
191
+
192
+ Disconnect cancels the provider call automatically (`request.signal` → ambient abort). An `AbortError` from hang-up does not retry or advance `via`. Console Traces stay empty until the stream closes — then the run is one completed row whose duration covers the open stream, not just time-to-first-byte.
161
193
 
162
194
  ## PII cannot leak by accident
163
195
 
@@ -305,6 +337,11 @@ Capability pins and Call API pass `name@version`. `fx.ask` resolves the bare
305
337
  prompt id. A pin that does not match `prompt.version` throws
306
338
  `ai: unknown prompt "name@version"`.
307
339
 
340
+ </Accordion>
341
+ <Accordion title="OKE1007 when the model calls an MCP tool">
342
+
343
+ Add `mcp:<server>/<tool>` to that flow’s `effects.calls` — the same token as `fx.call`. The allowlist on `ai.mcpServer` is not a capability grant.
344
+
308
345
  </Accordion>
309
346
  <Accordion title="An agent looped and burned budget">
310
347
 
@@ -313,13 +350,24 @@ Bound it at declaration: `maxSteps` caps iterations, `budget.maxCostPerRun` caps
313
350
  `fx.ask` stamps `promptVersion` on the run. Cost is recorded only when the
314
351
  driver reports `usage.cost` — tokens land on the ask journal either way.
315
352
 
353
+ </Accordion>
354
+ <Accordion title="The client hung up but the model kept generating">
355
+
356
+ HTTP flows install `request.signal` as the ambient abort. `fx.ask` / `fx.stream` /
357
+ `fx.run` cancel the provider fetch. Return tokens with `fx.json.stream(fx.stream(...))`
358
+ so disconnect is observable.
359
+
360
+ A hang-up is `AbortError` — it does not retry or advance `via`.
361
+
316
362
  </Accordion>
317
363
  </Accordions>
318
364
 
319
365
  ## Learn more
320
366
 
367
+ - [Project structure](/docs/get-started/project-structure) — agent `tools` are Flow names (`notes.get`), not URLs
321
368
  - [Flow](/docs/elements/flow) — `fx.ask` and `fx.search` inside `do`
322
369
  - [Store](/docs/elements/store) — `store.index`, the home of embeddings
370
+ - [MCP](/docs/ai/mcp) — serve the app to agents, or consume external servers as tools
323
371
 
324
372
  ## Next
325
373
 
@@ -327,6 +327,7 @@ Confirm a visible OS notification titled `okengine webpush interop`.
327
327
 
328
328
  ## Learn more
329
329
 
330
+ - [Project structure](/docs/get-started/project-structure) — `flow({…})` names as `unit.export`; explicit `flow("unit.action")` still wins
330
331
  - [Flow](/docs/elements/flow) — `fx.send` inside `do`
331
332
  - [Environment Variables](/docs/reference/environment-variables) — Channel boot credentials
332
333
  - [Configuration](/docs/reference/configuration) — `drivers.channel.*` maps
@@ -28,7 +28,7 @@ export const purgeOld = on(
28
28
  every("1h"),
29
29
  flow("links.purgeOld", {
30
30
  do: async (_, fx) => {
31
- const cutoff = fx.clock.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
31
+ const cutoff = fx.clock.ago("30d");
32
32
  await fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
33
33
  },
34
34
  }),
@@ -52,6 +52,8 @@ export const dailyReport = clock("daily-report", {
52
52
  });
53
53
  ```
54
54
 
55
+ Import the module before `oke()` — named clocks auto-register. No `oke({ clocks })` list.
56
+
55
57
  ```typescript title="src/flows/reports/daily.ts"
56
58
  export const sendDaily = on(
57
59
  dailyReport,
@@ -68,12 +70,15 @@ export const sendDaily = on(
68
70
  <Step>
69
71
  ### Ask for time inside flows
70
72
 
71
- `fx.clock` is the only clock a flow knows:
73
+ `fx.clock` is the only clock a flow knows. Instants stay epoch-ms; spans use the same
74
+ duration strings as `every()` and `sleep()`:
72
75
 
73
76
  ```typescript
74
77
  do: async (input, fx) => {
75
- const now = fx.clock.now(); // epoch-ms, injectable
76
- await fx.clock.sleep("wait-for-payment", "7d"); // durable — survives restarts on a shared journal
78
+ const cutoff = fx.clock.ago("30d");
79
+ const expiresAt = fx.clock.fromNow("14d");
80
+ const window = input.createdAt + fx.clock.duration("7d");
81
+ await fx.clock.sleep("wait-for-payment", "7d");
77
82
  };
78
83
  ```
79
84
 
@@ -90,13 +95,25 @@ difference.
90
95
 
91
96
  ### `clock()` options
92
97
 
93
- | Option | Type | Default | Meaning |
94
- | ------------- | ------- | ------- | -------------------------------------------------------------- |
95
- | `cron` | string | — | Cron expression `m h dom mon dow` (this or `every` required) |
96
- | `every` | string | — | Fixed interval: `"30s"` · `"10m"` · `"1h"` · `"7d"` |
97
- | `timezone` | string | `"UTC"` | IANA timezone for cron evaluation |
98
- | `overridable` | boolean | `false` | Allow the Console to **edit** the schedule (pause is separate) |
99
- | `description` | string | — | Human title in the Console (falls back to the clock name) |
98
+ | Option | Type | Default | Meaning |
99
+ | ------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
100
+ | `cron` | string | — | Five-field crontab (`m h dom mon dow`), plus steps, ranges, lists, names, `@hourly`, `@daily` (this or `every` required) |
101
+ | `every` | string | — | Fixed interval: `"30s"` · `"10m"` · `"1h"` · `"7d"` |
102
+ | `timezone` | string | `"UTC"` | IANA timezone for cron evaluation |
103
+ | `overridable` | boolean | `false` | Allow the Console to **edit** the schedule (pause is separate) |
104
+ | `description` | string | — | Human title in the Console (falls back to the clock name) |
105
+
106
+ ### `fx.clock`
107
+
108
+ | Call | Returns | Meaning |
109
+ | -------------------- | ------- | ------------------------------------------------ |
110
+ | `now()` | instant | Injected epoch-ms |
111
+ | `ago("30d")` | instant | now − duration |
112
+ | `fromNow("14d")` | instant | now + duration |
113
+ | `duration("7d")` | span ms | Offset a stored instant (`createdAt + duration`) |
114
+ | `sleep(label, "7d")` | — | Durable sleep |
115
+
116
+ Same duration strings as `every()`: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400_000 ms, not a calendar day. Unknown strings parse as `0`.
100
117
 
101
118
  ## Sleeping inside a flow
102
119
 
@@ -136,7 +153,7 @@ Simple daily crons (`M H * * *`) in a DST-observing zone can hit a **gap** (spri
136
153
 
137
154
  Detection attaches `dstAmbiguity` on the cron row and the Console shows it. `oke doctor` does not check DST. The scheduler does not rewrite the expression.
138
155
 
139
- **Consequence:** fall-back days can list two civil instants an hour apart. The default leader lease (30s) is shorter than that gap, so a lease alone is not a “fire once on overlap” policy. Prefer UTC, or a wall time outside the transition window, when double-fire would hurt.
156
+ **Consequence:** firing follows crontab. A spring gap runs at the next valid instant (shifted). A fall overlap fires **once** (first occurrence). Prefer UTC when a shifted spring fire would hurt.
140
157
 
141
158
  ## Per-environment drivers
142
159
 
@@ -203,6 +220,7 @@ The registry is off in `test` and when there is no shared SQL URL. `dev`/`prod`
203
220
 
204
221
  ## Learn more
205
222
 
223
+ - [Project structure](/docs/get-started/project-structure) — clock flows in the tree get a name, never a URL
206
224
  - [Flow](/docs/elements/flow) — `on(trigger, flow)` and the `fx` surface
207
225
  - [fx · Runs](/docs/reference/fx#runs-observability-read) — native SLO checkers via `on(every(…))` + `fx.runs`
208
226
  - [Signal](/docs/elements/signal) — reacting to events instead of time
@@ -112,6 +112,8 @@ Built-in codes attach a localized `message` ([Errors](/docs/reference/errors));
112
112
 
113
113
  `http.get` · `http.post` · `http.put` · `http.patch` · `http.delete` · `http.head` · `http.options` · `http.query`. QUERY (RFC 10008) is a safe, idempotent read with a JSON body.
114
114
 
115
+ `http.get()` with no path is filled from the file tree (`flows/notes/[id]/get.ts` → `/notes/:id`). `flow({ do })` is named `unit.export`. Explicit `http.get("/me/tasks")` and `flow("notes.get")` still win. Folder conventions live on [Project structure](/docs/get-started/project-structure).
116
+
115
117
  `Content-Type: application/json` is required — missing → **400** `InvalidQuery`, anything else → **415** `UnsupportedMediaType` with `Accept-Query: "application/json"`. Invalid JSON is **400** (no sniffing). Responses advertise `Accept-Query`. okengine does not assign `Location` / `Content-Location` result URIs.
116
118
 
117
119
  A browser `GET` (`Accept` prefers `text/html`) paints the `{ data, error }` envelope in traces chrome — status, latency, and cache, Routes rail (static GET links), copy, line numbers, Pretty / Raw, Console on **6533**. `curl`, `Accept: application/json`, and `?format=json` stay JSON.
@@ -130,7 +132,9 @@ export const findOrder = on(
130
132
 
131
133
  ### signal — another flow emits
132
134
 
133
- The producer emits through `fx` (transactionally with its writes); the consumer is the same species — no `subscribe()`, no listener registration:
135
+ The producer emits through `fx`. For `once` and `broadcast` the consumer is the same species —
136
+ `on(signal, flow)`, no `subscribe()` registration. `live` replay is the exception: `bus.live()` on
137
+ the server, not a Flow.
134
138
 
135
139
  ```typescript
136
140
  await fx.emit(orderPlaced, { orderId: id }); // inside the producing flow
@@ -154,7 +158,7 @@ on(
154
158
  every("1h"),
155
159
  flow("sessions.sweepExpired", {
156
160
  do: async (_, fx) => {
157
- const cutoff = fx.clock.now() - 30 * 24 * 60 * 60 * 1000;
161
+ const cutoff = fx.clock.ago("30d");
158
162
  await fx.store(db).delete(sessions).where(lt(sessions.createdAt, cutoff));
159
163
  },
160
164
  }),
@@ -198,12 +202,13 @@ Everything a flow may touch, on one object:
198
202
  | ------------------------------------------- | --------------- | -------------------------------------------------- |
199
203
  | `fx.store(db).select/insert/…` | read / write | SQL, KV, files, index sessions |
200
204
  | `fx.emit(signal, payload)` | emit | Publish a signal (transactional with writes) |
205
+ | `fx.deadLetters(signal)` | read | Dead-lettered messages for that signal |
201
206
  | `fx.send(template, opts)` | send | Reach a human (email · SMS · …) |
202
207
  | `fx.ask(prompt, input)` | ask | Call a versioned AI prompt |
203
208
  | `fx.run(agent, input)` | ask | Run a bounded agent |
204
209
  | `fx.call(flow, input)` | call | Invoke another flow |
205
210
  | `fx.vault.get(contract)` | secret | Read a secret (`Redacted`; logs masked) |
206
- | `fx.clock.now()` / `.sleep(…)` | — | Injected time / durable sleep |
211
+ | `fx.clock.now/ago/fromNow/duration/sleep` | — | Injected instants, spans, durable sleep |
207
212
  | `fx.cache.get/set` | — | Shared cache with effect-aware invalidation |
208
213
  | `fx.step(name, fn, { undo? })` | — | Named durable step — optional LIFO undo on failure |
209
214
  | `fx.all` / `fx.race` / `fx.retry` | — | Structured concurrency + backoff retry |
@@ -366,6 +371,7 @@ a **hit**. A matching write turns the next read into a miss.
366
371
 
367
372
  ## Learn more
368
373
 
374
+ - [Project structure](/docs/get-started/project-structure) — folders are the URL; `unit.export` is the name
369
375
  - [fx](/docs/reference/fx) — full `fx` surface, including ICU `fx.t` / `fx.locale`
370
376
  - [Signal](/docs/elements/signal) — delivery physics (`once` · `broadcast` · `live`)
371
377
  - [Clock](/docs/elements/clock) — schedules and durable sleep
@@ -10,7 +10,7 @@ burned its minute quota? Attach gates to the HTTP trigger — the pipeline check
10
10
  store write, emit, or channel send runs.
11
11
 
12
12
  <Callout title="The one rule">
13
- Every HTTP trigger declares posture: attach a real gate, or attach `.gate.public`. Omitting both
13
+ Every HTTP trigger declares posture: attach a real gate, or attach `.public()`. Omitting both
14
14
  fails boot. Denial is a typed error value — never a thrown exception mid-`do`.
15
15
  </Callout>
16
16
 
@@ -19,10 +19,10 @@ store write, emit, or channel send runs.
19
19
  <Steps>
20
20
 
21
21
  <Step>
22
- ### Declare policies and register the Gate bag
22
+ ### Declare policies
23
23
 
24
- Policies are named predicates; rate limits are declarative budgets. Put the ones you reuse in
25
- `oke({ gate: { policies } })`:
24
+ Policies are named predicates; rate limits are declarative budgets. Import the module before
25
+ `oke()` — they auto-register. `oke({ gate: { policies } })` is still valid and additive.
26
26
 
27
27
  ```typescript title="src/gates.ts"
28
28
  import { gate } from "okengine";
@@ -43,14 +43,10 @@ export const fair = gate.rate({
43
43
  the scope checked.
44
44
 
45
45
  ```typescript title="src/app.ts"
46
+ import "./gates";
46
47
  import { oke } from "okengine";
47
- import { member, canBook, fair } from "./gates";
48
48
 
49
- export const app = oke({
50
- name: "shop",
51
- env: "dev",
52
- gate: { policies: [member, canBook, fair] },
53
- });
49
+ export const app = oke({ name: "shop", env: "dev" });
54
50
  ```
55
51
 
56
52
  </Step>
@@ -58,14 +54,14 @@ export const app = oke({
58
54
  <Step>
59
55
  ### Attach them to triggers
60
56
 
61
- `.gate(...)` composes left to right — first denial wins. Public routes must say so explicitly:
57
+ `.gate(...)` composes left to right — first denial wins. Public routes must say so explicitly. Path and name can come from the file tree — see [Project structure](/docs/get-started/project-structure).
62
58
 
63
59
  ```typescript title="src/flows/links/shorten.ts"
64
60
  import { on, flow, http, gate } from "okengine";
65
61
  import { member, canBook, fair } from "../../gates";
66
62
 
67
63
  export const health = on(
68
- http.get("/health").gate.public,
64
+ http.get("/health").public(),
69
65
  flow("health.check", {
70
66
  out: HealthOut,
71
67
  do: () => ({ ok: true as const }),
@@ -88,8 +84,8 @@ export const shorten = on(
88
84
  ```
89
85
 
90
86
  A single gate stays `.gate(member)`. Reuse two or more with `gate.all` — `.gate(write)`
91
- flattens. `http.resource(path, ops).gate(...)` stamps every verb. Do **not** put
92
- `gate.public` in `gate.policies` — attach it on the trigger only.
87
+ flattens. `http.resource(path, ops).gate(...)` stamps every verb. Public routes use
88
+ `.public()` — do **not** put `gate.public` in `gate.policies`.
93
89
 
94
90
  </Step>
95
91
 
@@ -131,16 +127,16 @@ gates are skipped. `do` runs only when every gate passed.
131
127
  | ------------------------------------- | ------------------------------------------- | ---------------------------------- |
132
128
  | `gate.policy(name, check \| options)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
133
129
  | `gate.scope(name)` | Does `auth.scopes` contain this exact name? | `auth.scopes` |
134
- | `gate.public` / `.gate.public` | Intentionally unauthenticated surface | always allows |
130
+ | `gate.public` / `.public()` | Intentionally unauthenticated surface | always allows |
135
131
  | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
136
132
  | `gate.all(...members)` | Reuse this chain on many triggers | flattens; each member is evaluated |
137
133
 
138
134
  Pass `{ check, description }` on `gate.policy` when you want a human title in the Console (falls
139
- back to the policy name). The name `"public"` is reserved — use `gate.public`, not
140
- `gate.policy("public", …)`.
135
+ back to the policy name). The name `"public"` is reserved — mark the trigger with
136
+ `.public()`, not `gate.policy("public", …)`.
141
137
 
142
138
  `gate.all()` with no members throws. Nested `all` handles flatten.
143
- `oke({ gate: { policies: [write] } })` registers the members.
139
+ Members auto-register when declared; `oke({ gate: { policies: [write] } })` is additive.
144
140
 
145
141
  ### Policy context
146
142
 
@@ -178,11 +174,11 @@ driver in tests. Boot opens a dedicated `oke:gates` namespace on **`drivers.stor
178
174
 
179
175
  ## Auth posture at boot
180
176
 
181
- Every HTTP trigger must carry a gate or `.gate.public`. Omitting both fails boot with
177
+ Every HTTP trigger must carry a gate or `.public()`. Omitting both fails boot with
182
178
  `GateBootError` — message starts with `gate boot failed —` and lists every gap.
183
179
 
184
180
  ```typescript
185
- on(http.get("/health").gate.public, flow("health.check", {/* … */}));
181
+ on(http.get("/health").public(), flow("health.check", {/* … */}));
186
182
  on(http.post("/bookings").gate(member, canBook), flow("bookings.create", {/* … */}));
187
183
  ```
188
184
 
@@ -191,7 +187,7 @@ on(http.post("/bookings").gate(member, canBook), flow("bookings.create", {/* …
191
187
  posture but does **not** authenticate.
192
188
 
193
189
  `gate: { unguardedHttp: "allow" }` skips the audit **only** when `env === "test"`.
194
- In `dev` / `prod` migrate with per-trigger `.gate.public`.
190
+ In `dev` / `prod` migrate with per-trigger `.public()`.
195
191
 
196
192
  ## Built-in auth (`gate.auth`)
197
193
 
@@ -270,12 +266,18 @@ Pass **and** deny land on the run's `gates` dimension. Console (`:6533` → `/fl
270
266
  chain on the flow contract. Rate `overridable` is a catalog flag only — Clock schedules are what the Console can
271
267
  edit live today.
272
268
 
269
+ <Callout title="RLS reads the stamped principal">
270
+ After HTTP Gate passes, user-plane `fx.store` stamps `oke.gate()`, `oke.user()`, and
271
+ `oke.has_scope()`. Rate, cron, CDC, and signal stay unstamped. Helpers:
272
+ [Store](/docs/elements/store).
273
+ </Callout>
274
+
273
275
  ## Troubleshooting
274
276
 
275
277
  <Accordions>
276
278
  <Accordion title="Boot: gate boot failed — N HTTP trigger(s) missing auth posture">
277
279
 
278
- Empty `.gate(...)` chain. Attach a policy/rate gate, or `.gate.public`. First `fetch`
280
+ Empty `.gate(...)` chain. Attach a policy/rate gate, or `.public()`. First `fetch`
279
281
  auto-boots — do not rely on `gate.unguardedHttp: "allow"` outside `env: "test"`.
280
282
 
281
283
  </Accordion>
@@ -313,6 +315,7 @@ Keying an authenticated endpoint by IP punishes shared NAT.
313
315
 
314
316
  ## Learn more
315
317
 
318
+ - [Project structure](/docs/get-started/project-structure) — `http.get()` and `flow({…})` fill path and name from the file tree
316
319
  - [Flow](/docs/elements/flow) — the trigger pipeline gates plug into
317
320
  - [fx](/docs/reference/fx) — `fx.auth`, `fx.operator`, `fx.principal`
318
321
  - [Client](/docs/reference/client) — Bearer `createClient` + `okengine/client/auth`