okengine 0.1.4

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 (674) hide show
  1. package/AGENTS.md +91 -0
  2. package/README.md +327 -0
  3. package/docs/spec/complete-example.md +1188 -0
  4. package/docs/spec/console.md +739 -0
  5. package/docs/spec/four-applications.md +1188 -0
  6. package/docs/spec/unified-theory.md +489 -0
  7. package/package.json +152 -0
  8. package/spec/manifest.v1.schema.json +624 -0
  9. package/src/auth/api-keys.ts +168 -0
  10. package/src/auth/attenuation.ts +116 -0
  11. package/src/auth/auth.test.ts +273 -0
  12. package/src/auth/cross-plane.ts +98 -0
  13. package/src/auth/index.ts +112 -0
  14. package/src/auth/invites.ts +66 -0
  15. package/src/auth/invoke-as.ts +49 -0
  16. package/src/auth/operator.ts +178 -0
  17. package/src/auth/planes.ts +102 -0
  18. package/src/auth/plugin.ts +44 -0
  19. package/src/auth/roles.ts +84 -0
  20. package/src/auth/sessions.ts +418 -0
  21. package/src/auth/tables.ts +131 -0
  22. package/src/cli/args.ts +50 -0
  23. package/src/cli/branch.ts +75 -0
  24. package/src/cli/build.ts +86 -0
  25. package/src/cli/client-add.test.ts +140 -0
  26. package/src/cli/client-add.ts +164 -0
  27. package/src/cli/completion.test.ts +58 -0
  28. package/src/cli/completion.ts +296 -0
  29. package/src/cli/dev-app-runner.ts +77 -0
  30. package/src/cli/dev.test.ts +435 -0
  31. package/src/cli/dev.ts +594 -0
  32. package/src/cli/doc-drift.test.ts +146 -0
  33. package/src/cli/doc-drift.ts +397 -0
  34. package/src/cli/docker-cli.test.ts +221 -0
  35. package/src/cli/docker.ts +127 -0
  36. package/src/cli/doctor-diff-examples.ts +92 -0
  37. package/src/cli/doctor-diff.test.ts +91 -0
  38. package/src/cli/doctor-diff.ts +274 -0
  39. package/src/cli/doctor-pii.test.ts +98 -0
  40. package/src/cli/doctor-pii.ts +151 -0
  41. package/src/cli/doctor.test.ts +150 -0
  42. package/src/cli/doctor.ts +223 -0
  43. package/src/cli/eval.ts +122 -0
  44. package/src/cli/exit.ts +19 -0
  45. package/src/cli/gates-list.test.ts +47 -0
  46. package/src/cli/gates-list.ts +84 -0
  47. package/src/cli/images.ts +277 -0
  48. package/src/cli/index.ts +99 -0
  49. package/src/cli/json-out.test.ts +129 -0
  50. package/src/cli/load-config.ts +71 -0
  51. package/src/cli/mcp-from-console.ts +50 -0
  52. package/src/cli/privacy-erase.ts +73 -0
  53. package/src/cli/registry.ts +455 -0
  54. package/src/cli/safe-defaults.test.ts +92 -0
  55. package/src/cli/schema.ts +196 -0
  56. package/src/cli/stack.ts +82 -0
  57. package/src/cli/start.ts +106 -0
  58. package/src/cli/upgrade.ts +105 -0
  59. package/src/cli/vault-cmd.ts +140 -0
  60. package/src/client/budget-entry.ts +34 -0
  61. package/src/client/budget.test.ts +17 -0
  62. package/src/client/create.test.ts +159 -0
  63. package/src/client/create.ts +152 -0
  64. package/src/client/errors.ts +60 -0
  65. package/src/client/index.ts +35 -0
  66. package/src/client/notes-contract.test.ts +289 -0
  67. package/src/client/transport.test.ts +123 -0
  68. package/src/client/transport.ts +254 -0
  69. package/src/client/types.ts +282 -0
  70. package/src/compiler/aot.test.ts +248 -0
  71. package/src/compiler/aot.ts +182 -0
  72. package/src/compiler/differential.test.ts +241 -0
  73. package/src/compiler/dynamic.ts +54 -0
  74. package/src/compiler/effects-infer.ts +541 -0
  75. package/src/compiler/emit.ts +40 -0
  76. package/src/compiler/extract.test.ts +177 -0
  77. package/src/compiler/extract.ts +1368 -0
  78. package/src/compiler/fixtures/raw/raw-unannotated.ts +28 -0
  79. package/src/compiler/fixtures/skyport/oke.config.ts +24 -0
  80. package/src/compiler/fixtures/skyport/src/ai.ts +15 -0
  81. package/src/compiler/fixtures/skyport/src/app.ts +10 -0
  82. package/src/compiler/fixtures/skyport/src/channels.ts +6 -0
  83. package/src/compiler/fixtures/skyport/src/core.ts +3 -0
  84. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +57 -0
  85. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +11 -0
  86. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +14 -0
  87. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +20 -0
  88. package/src/compiler/fixtures/skyport/src/gates.ts +14 -0
  89. package/src/compiler/fixtures/skyport/src/journeys.ts +9 -0
  90. package/src/compiler/fixtures/skyport/src/schema.ts +3 -0
  91. package/src/compiler/fixtures/skyport/src/vault.ts +6 -0
  92. package/src/compiler/fixtures/skyport.expected.json +241 -0
  93. package/src/compiler/fixtures/triggers/five-triggers.ts +69 -0
  94. package/src/compiler/http-parse.ts +214 -0
  95. package/src/compiler/index.ts +67 -0
  96. package/src/compiler/response.ts +89 -0
  97. package/src/compiler/sucrose.ts +238 -0
  98. package/src/config/index.ts +167 -0
  99. package/src/console/budget.test.ts +73 -0
  100. package/src/console/index.ts +30 -0
  101. package/src/console/server/access.test.ts +370 -0
  102. package/src/console/server/access.ts +777 -0
  103. package/src/console/server/ai.test.ts +172 -0
  104. package/src/console/server/ai.ts +632 -0
  105. package/src/console/server/app.ts +294 -0
  106. package/src/console/server/auth-rate.test.ts +81 -0
  107. package/src/console/server/auth-rate.ts +72 -0
  108. package/src/console/server/bind.ts +25 -0
  109. package/src/console/server/channels.test.ts +179 -0
  110. package/src/console/server/channels.ts +469 -0
  111. package/src/console/server/claim.test.ts +66 -0
  112. package/src/console/server/claim.ts +134 -0
  113. package/src/console/server/clock.test.ts +239 -0
  114. package/src/console/server/clock.ts +390 -0
  115. package/src/console/server/console.test.ts +338 -0
  116. package/src/console/server/diff.test.ts +299 -0
  117. package/src/console/server/diff.ts +352 -0
  118. package/src/console/server/dry-run.audit.test.ts +118 -0
  119. package/src/console/server/flows-invoke.test.ts +148 -0
  120. package/src/console/server/flows.ts +3633 -0
  121. package/src/console/server/gates.test.ts +360 -0
  122. package/src/console/server/gates.ts +869 -0
  123. package/src/console/server/index.ts +141 -0
  124. package/src/console/server/live.test.ts +30 -0
  125. package/src/console/server/live.ts +109 -0
  126. package/src/console/server/plugin.ts +50 -0
  127. package/src/console/server/plugins.test.ts +131 -0
  128. package/src/console/server/plugins.ts +429 -0
  129. package/src/console/server/runs-pii.ts +98 -0
  130. package/src/console/server/security-headers.ts +97 -0
  131. package/src/console/server/security.gate.test.ts +286 -0
  132. package/src/console/server/serve.ts +319 -0
  133. package/src/console/server/signals.test.ts +88 -0
  134. package/src/console/server/signals.ts +282 -0
  135. package/src/console/server/state.ts +982 -0
  136. package/src/console/server/store.test.ts +233 -0
  137. package/src/console/server/store.ts +861 -0
  138. package/src/console/server/structural.ts +95 -0
  139. package/src/console/server/vault.test.ts +251 -0
  140. package/src/console/server/vault.ts +430 -0
  141. package/src/console/ui/access/AccessA11yView.tsx +237 -0
  142. package/src/console/ui/access/a11y.test.tsx +122 -0
  143. package/src/console/ui/access/acknowledgement.test.ts +13 -0
  144. package/src/console/ui/access/acknowledgement.ts +35 -0
  145. package/src/console/ui/access/blast-radius.test.ts +26 -0
  146. package/src/console/ui/access/blast-radius.ts +40 -0
  147. package/src/console/ui/access/confirmation.test.ts +38 -0
  148. package/src/console/ui/access/confirmation.ts +36 -0
  149. package/src/console/ui/access/fixture.ts +203 -0
  150. package/src/console/ui/access/grantable.test.ts +17 -0
  151. package/src/console/ui/access/grantable.ts +36 -0
  152. package/src/console/ui/access/hygiene.test.ts +12 -0
  153. package/src/console/ui/access/hygiene.ts +39 -0
  154. package/src/console/ui/access/index.ts +54 -0
  155. package/src/console/ui/access/provenance.test.ts +12 -0
  156. package/src/console/ui/access/provenance.ts +29 -0
  157. package/src/console/ui/access/search.test.ts +17 -0
  158. package/src/console/ui/access/search.ts +72 -0
  159. package/src/console/ui/access/types.ts +111 -0
  160. package/src/console/ui/ai/AiA11yView.tsx +265 -0
  161. package/src/console/ui/ai/a11y.test.tsx +99 -0
  162. package/src/console/ui/ai/findings.ts +41 -0
  163. package/src/console/ui/ai/fixture.ts +182 -0
  164. package/src/console/ui/ai/format.ts +90 -0
  165. package/src/console/ui/ai/group.test.ts +37 -0
  166. package/src/console/ui/ai/group.ts +88 -0
  167. package/src/console/ui/ai/index.ts +66 -0
  168. package/src/console/ui/ai/promotion.test.ts +77 -0
  169. package/src/console/ui/ai/promotion.ts +150 -0
  170. package/src/console/ui/ai/search.test.ts +40 -0
  171. package/src/console/ui/ai/search.ts +89 -0
  172. package/src/console/ui/ai/types.ts +139 -0
  173. package/src/console/ui/architecture/ArchitectureA11yView.tsx +163 -0
  174. package/src/console/ui/architecture/a11y.test.tsx +97 -0
  175. package/src/console/ui/architecture/boundary.ts +42 -0
  176. package/src/console/ui/architecture/declared.ts +165 -0
  177. package/src/console/ui/architecture/fixture.ts +74 -0
  178. package/src/console/ui/architecture/index.ts +84 -0
  179. package/src/console/ui/architecture/layers.ts +64 -0
  180. package/src/console/ui/architecture/layout.ts +68 -0
  181. package/src/console/ui/architecture/pathologies.ts +260 -0
  182. package/src/console/ui/architecture/search.ts +145 -0
  183. package/src/console/ui/architecture/traffic.ts +102 -0
  184. package/src/console/ui/architecture/types.ts +144 -0
  185. package/src/console/ui/architecture/view.test.ts +165 -0
  186. package/src/console/ui/architecture/view.ts +516 -0
  187. package/src/console/ui/channels/ChannelsA11yView.tsx +180 -0
  188. package/src/console/ui/channels/a11y.test.tsx +96 -0
  189. package/src/console/ui/channels/confirmation.test.ts +37 -0
  190. package/src/console/ui/channels/confirmation.ts +27 -0
  191. package/src/console/ui/channels/fallback.test.ts +15 -0
  192. package/src/console/ui/channels/fallback.ts +20 -0
  193. package/src/console/ui/channels/findings.ts +38 -0
  194. package/src/console/ui/channels/fixture.ts +134 -0
  195. package/src/console/ui/channels/group.ts +25 -0
  196. package/src/console/ui/channels/index.ts +57 -0
  197. package/src/console/ui/channels/locale.test.ts +17 -0
  198. package/src/console/ui/channels/locale.ts +31 -0
  199. package/src/console/ui/channels/mask.test.ts +14 -0
  200. package/src/console/ui/channels/mask.ts +12 -0
  201. package/src/console/ui/channels/search.ts +58 -0
  202. package/src/console/ui/channels/taxonomy.test.ts +17 -0
  203. package/src/console/ui/channels/taxonomy.ts +49 -0
  204. package/src/console/ui/channels/types.ts +117 -0
  205. package/src/console/ui/clock/ClockA11yView.tsx +182 -0
  206. package/src/console/ui/clock/a11y.test.tsx +99 -0
  207. package/src/console/ui/clock/confirmation.test.ts +32 -0
  208. package/src/console/ui/clock/confirmation.ts +34 -0
  209. package/src/console/ui/clock/findings.ts +40 -0
  210. package/src/console/ui/clock/fixture.ts +111 -0
  211. package/src/console/ui/clock/health.test.ts +25 -0
  212. package/src/console/ui/clock/health.ts +61 -0
  213. package/src/console/ui/clock/index.ts +49 -0
  214. package/src/console/ui/clock/search.test.ts +39 -0
  215. package/src/console/ui/clock/search.ts +76 -0
  216. package/src/console/ui/clock/timeline.test.ts +44 -0
  217. package/src/console/ui/clock/timeline.ts +41 -0
  218. package/src/console/ui/clock/types.ts +78 -0
  219. package/src/console/ui/clock/waiting-on.test.ts +34 -0
  220. package/src/console/ui/clock/waiting-on.ts +83 -0
  221. package/src/console/ui/confirmation.unify.test.ts +92 -0
  222. package/src/console/ui/diff/DiffA11yView.tsx +85 -0
  223. package/src/console/ui/diff/a11y.test.tsx +99 -0
  224. package/src/console/ui/diff/fixture.ts +85 -0
  225. package/src/console/ui/diff/group.test.ts +58 -0
  226. package/src/console/ui/diff/group.ts +97 -0
  227. package/src/console/ui/diff/index.ts +38 -0
  228. package/src/console/ui/diff/search.test.ts +32 -0
  229. package/src/console/ui/diff/search.ts +75 -0
  230. package/src/console/ui/diff/summary.ts +74 -0
  231. package/src/console/ui/diff/types.ts +48 -0
  232. package/src/console/ui/dist/assets/index-B71Yl_SS.js +10 -0
  233. package/src/console/ui/dist/assets/panel-access-Dd37LU2c.js +64 -0
  234. package/src/console/ui/dist/assets/panel-ai-CC7LR6-J.js +1 -0
  235. package/src/console/ui/dist/assets/panel-architecture-B5b3iKCz.js +1 -0
  236. package/src/console/ui/dist/assets/panel-channels-CeNjTKXp.js +1 -0
  237. package/src/console/ui/dist/assets/panel-clock-DgFTLoHV.js +1 -0
  238. package/src/console/ui/dist/assets/panel-diff-DxehccqB.js +1 -0
  239. package/src/console/ui/dist/assets/panel-flows-BtrVn-Eg.js +45 -0
  240. package/src/console/ui/dist/assets/panel-gates-Z9MKRGdH.js +1 -0
  241. package/src/console/ui/dist/assets/panel-overview-Bd48d9km.js +1 -0
  242. package/src/console/ui/dist/assets/panel-plugins-DWd0TowH.js +1 -0
  243. package/src/console/ui/dist/assets/panel-runs-BwsWqKeB.js +1 -0
  244. package/src/console/ui/dist/assets/panel-signals-9najbZY2.js +1 -0
  245. package/src/console/ui/dist/assets/panel-store-OHkP2pDp.js +1 -0
  246. package/src/console/ui/dist/assets/panel-traces-tn2JoY8U.js +1 -0
  247. package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +1 -0
  248. package/src/console/ui/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
  249. package/src/console/ui/dist/assets/style-Cnl7WLya.css +3 -0
  250. package/src/console/ui/dist/index.html +14 -0
  251. package/src/console/ui/flows/FlowsA11yView.tsx +155 -0
  252. package/src/console/ui/flows/a11y.test.tsx +105 -0
  253. package/src/console/ui/flows/buffer.test.ts +91 -0
  254. package/src/console/ui/flows/buffer.ts +118 -0
  255. package/src/console/ui/flows/confirmation.ts +131 -0
  256. package/src/console/ui/flows/contract.test.ts +89 -0
  257. package/src/console/ui/flows/contract.ts +353 -0
  258. package/src/console/ui/flows/fixture.ts +81 -0
  259. package/src/console/ui/flows/graph.test.ts +101 -0
  260. package/src/console/ui/flows/graph.ts +549 -0
  261. package/src/console/ui/flows/index.ts +87 -0
  262. package/src/console/ui/flows/save-as-test.test.ts +47 -0
  263. package/src/console/ui/flows/save-as-test.ts +99 -0
  264. package/src/console/ui/flows/search.ts +255 -0
  265. package/src/console/ui/flows/tiers.ts +140 -0
  266. package/src/console/ui/gates/GatesA11yView.tsx +192 -0
  267. package/src/console/ui/gates/a11y.test.tsx +116 -0
  268. package/src/console/ui/gates/audit.test.ts +40 -0
  269. package/src/console/ui/gates/audit.ts +63 -0
  270. package/src/console/ui/gates/denial.test.ts +36 -0
  271. package/src/console/ui/gates/denial.ts +51 -0
  272. package/src/console/ui/gates/findings.ts +19 -0
  273. package/src/console/ui/gates/fixture.ts +189 -0
  274. package/src/console/ui/gates/group.test.ts +30 -0
  275. package/src/console/ui/gates/group.ts +102 -0
  276. package/src/console/ui/gates/index.ts +47 -0
  277. package/src/console/ui/gates/search.test.ts +49 -0
  278. package/src/console/ui/gates/search.ts +115 -0
  279. package/src/console/ui/gates/types.ts +121 -0
  280. package/src/console/ui/overview/OverviewA11yView.tsx +152 -0
  281. package/src/console/ui/overview/a11y.test.tsx +113 -0
  282. package/src/console/ui/overview/busiest.ts +26 -0
  283. package/src/console/ui/overview/compose.ts +133 -0
  284. package/src/console/ui/overview/cost.test.ts +39 -0
  285. package/src/console/ui/overview/cost.ts +138 -0
  286. package/src/console/ui/overview/fixture.ts +161 -0
  287. package/src/console/ui/overview/golden.ts +57 -0
  288. package/src/console/ui/overview/index.ts +59 -0
  289. package/src/console/ui/overview/rank.test.ts +136 -0
  290. package/src/console/ui/overview/rank.ts +210 -0
  291. package/src/console/ui/overview/slo.test.ts +73 -0
  292. package/src/console/ui/overview/slo.ts +229 -0
  293. package/src/console/ui/overview/types.ts +121 -0
  294. package/src/console/ui/overview/verdict.ts +131 -0
  295. package/src/console/ui/plugins/PluginsA11yView.tsx +227 -0
  296. package/src/console/ui/plugins/a11y.test.tsx +97 -0
  297. package/src/console/ui/plugins/command.test.ts +22 -0
  298. package/src/console/ui/plugins/command.ts +27 -0
  299. package/src/console/ui/plugins/findings.ts +39 -0
  300. package/src/console/ui/plugins/fixture.ts +233 -0
  301. package/src/console/ui/plugins/group.test.ts +38 -0
  302. package/src/console/ui/plugins/group.ts +67 -0
  303. package/src/console/ui/plugins/index.ts +40 -0
  304. package/src/console/ui/plugins/search.ts +67 -0
  305. package/src/console/ui/plugins/types.ts +115 -0
  306. package/src/console/ui/runs/RunsA11yView.tsx +267 -0
  307. package/src/console/ui/runs/a11y.test.tsx +99 -0
  308. package/src/console/ui/runs/explain.test.ts +23 -0
  309. package/src/console/ui/runs/explain.ts +32 -0
  310. package/src/console/ui/runs/fixture.ts +189 -0
  311. package/src/console/ui/runs/group.test.ts +24 -0
  312. package/src/console/ui/runs/group.ts +85 -0
  313. package/src/console/ui/runs/histogram.test.ts +31 -0
  314. package/src/console/ui/runs/histogram.ts +85 -0
  315. package/src/console/ui/runs/index.ts +75 -0
  316. package/src/console/ui/runs/project.ts +122 -0
  317. package/src/console/ui/runs/query.test.ts +50 -0
  318. package/src/console/ui/runs/query.ts +283 -0
  319. package/src/console/ui/runs/search.test.ts +45 -0
  320. package/src/console/ui/runs/search.ts +141 -0
  321. package/src/console/ui/runs/trace-link.test.ts +30 -0
  322. package/src/console/ui/runs/trace-link.ts +99 -0
  323. package/src/console/ui/runs/types.ts +137 -0
  324. package/src/console/ui/shell/App.tsx +56 -0
  325. package/src/console/ui/shell/client.ts +1715 -0
  326. package/src/console/ui/shell/components/ui.tsx +110 -0
  327. package/src/console/ui/shell/index.html +12 -0
  328. package/src/console/ui/shell/layout/Shell.tsx +95 -0
  329. package/src/console/ui/shell/main.tsx +281 -0
  330. package/src/console/ui/shell/panels/Access.tsx +5 -0
  331. package/src/console/ui/shell/panels/Ai.tsx +5 -0
  332. package/src/console/ui/shell/panels/Architecture.tsx +5 -0
  333. package/src/console/ui/shell/panels/Channels.tsx +5 -0
  334. package/src/console/ui/shell/panels/Clock.tsx +5 -0
  335. package/src/console/ui/shell/panels/Diff.tsx +5 -0
  336. package/src/console/ui/shell/panels/Flows.tsx +5 -0
  337. package/src/console/ui/shell/panels/Gates.tsx +5 -0
  338. package/src/console/ui/shell/panels/Overview.tsx +6 -0
  339. package/src/console/ui/shell/panels/Plugins.tsx +5 -0
  340. package/src/console/ui/shell/panels/Runs.tsx +5 -0
  341. package/src/console/ui/shell/panels/Signals.tsx +5 -0
  342. package/src/console/ui/shell/panels/Store.tsx +5 -0
  343. package/src/console/ui/shell/panels/Traces.tsx +5 -0
  344. package/src/console/ui/shell/panels/Vault.tsx +5 -0
  345. package/src/console/ui/shell/panels/access/AccessPanel.tsx +813 -0
  346. package/src/console/ui/shell/panels/ai/AiPanel.tsx +563 -0
  347. package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +377 -0
  348. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +454 -0
  349. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +479 -0
  350. package/src/console/ui/shell/panels/diff/DiffPanel.tsx +275 -0
  351. package/src/console/ui/shell/panels/flows/ContractEditor.tsx +301 -0
  352. package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +350 -0
  353. package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +502 -0
  354. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +482 -0
  355. package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +428 -0
  356. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +413 -0
  357. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +649 -0
  358. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +840 -0
  359. package/src/console/ui/shell/panels/store/StorePanel.tsx +787 -0
  360. package/src/console/ui/shell/panels/traces/TracesPanel.tsx +539 -0
  361. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +449 -0
  362. package/src/console/ui/shell/plugin/Sandbox.tsx +29 -0
  363. package/src/console/ui/shell/setup/Wizard.tsx +189 -0
  364. package/src/console/ui/shell/styles.css +75 -0
  365. package/src/console/ui/signals/SignalsA11yView.tsx +276 -0
  366. package/src/console/ui/signals/a11y.test.tsx +99 -0
  367. package/src/console/ui/signals/confirmation.test.ts +39 -0
  368. package/src/console/ui/signals/confirmation.ts +63 -0
  369. package/src/console/ui/signals/dry-run.test.ts +18 -0
  370. package/src/console/ui/signals/dry-run.ts +36 -0
  371. package/src/console/ui/signals/durable.test.ts +22 -0
  372. package/src/console/ui/signals/durable.ts +41 -0
  373. package/src/console/ui/signals/findings.ts +33 -0
  374. package/src/console/ui/signals/fixture.ts +242 -0
  375. package/src/console/ui/signals/group.test.ts +32 -0
  376. package/src/console/ui/signals/group.ts +49 -0
  377. package/src/console/ui/signals/index.ts +52 -0
  378. package/src/console/ui/signals/monitor.test.ts +30 -0
  379. package/src/console/ui/signals/monitor.ts +79 -0
  380. package/src/console/ui/signals/schema-form.test.ts +34 -0
  381. package/src/console/ui/signals/schema-form.ts +112 -0
  382. package/src/console/ui/signals/search.ts +102 -0
  383. package/src/console/ui/signals/types.ts +71 -0
  384. package/src/console/ui/store/StoreA11yView.tsx +199 -0
  385. package/src/console/ui/store/a11y.test.tsx +99 -0
  386. package/src/console/ui/store/cache-view.test.ts +13 -0
  387. package/src/console/ui/store/cache-view.ts +36 -0
  388. package/src/console/ui/store/confirmation.test.ts +38 -0
  389. package/src/console/ui/store/confirmation.ts +55 -0
  390. package/src/console/ui/store/dry-run.test.ts +15 -0
  391. package/src/console/ui/store/dry-run.ts +29 -0
  392. package/src/console/ui/store/fixture.ts +163 -0
  393. package/src/console/ui/store/group.test.ts +21 -0
  394. package/src/console/ui/store/group.ts +41 -0
  395. package/src/console/ui/store/index.ts +40 -0
  396. package/src/console/ui/store/search.ts +78 -0
  397. package/src/console/ui/store/types.ts +65 -0
  398. package/src/console/ui/store/will-not-fire.test.ts +19 -0
  399. package/src/console/ui/store/will-not-fire.ts +40 -0
  400. package/src/console/ui/traces/TracesA11yView.tsx +200 -0
  401. package/src/console/ui/traces/a11y.test.tsx +97 -0
  402. package/src/console/ui/traces/chain.test.ts +61 -0
  403. package/src/console/ui/traces/chain.ts +144 -0
  404. package/src/console/ui/traces/critical-path.test.ts +24 -0
  405. package/src/console/ui/traces/critical-path.ts +59 -0
  406. package/src/console/ui/traces/filter.test.ts +78 -0
  407. package/src/console/ui/traces/filter.ts +112 -0
  408. package/src/console/ui/traces/fixture.ts +126 -0
  409. package/src/console/ui/traces/fold.test.ts +67 -0
  410. package/src/console/ui/traces/fold.ts +170 -0
  411. package/src/console/ui/traces/index.ts +64 -0
  412. package/src/console/ui/traces/mini.ts +65 -0
  413. package/src/console/ui/traces/replay.test.ts +24 -0
  414. package/src/console/ui/traces/replay.ts +35 -0
  415. package/src/console/ui/traces/sampling.test.ts +26 -0
  416. package/src/console/ui/traces/sampling.ts +66 -0
  417. package/src/console/ui/traces/search.ts +148 -0
  418. package/src/console/ui/traces/tier.ts +72 -0
  419. package/src/console/ui/traces/types.ts +125 -0
  420. package/src/console/ui/tsconfig.json +20 -0
  421. package/src/console/ui/vault/VaultA11yView.tsx +177 -0
  422. package/src/console/ui/vault/a11y.test.tsx +97 -0
  423. package/src/console/ui/vault/blast-radius.test.ts +35 -0
  424. package/src/console/ui/vault/blast-radius.ts +55 -0
  425. package/src/console/ui/vault/confirmation.test.ts +41 -0
  426. package/src/console/ui/vault/confirmation.ts +39 -0
  427. package/src/console/ui/vault/dormant.test.ts +37 -0
  428. package/src/console/ui/vault/dormant.ts +28 -0
  429. package/src/console/ui/vault/export-safe.test.ts +30 -0
  430. package/src/console/ui/vault/export-safe.ts +65 -0
  431. package/src/console/ui/vault/fixture.ts +75 -0
  432. package/src/console/ui/vault/group.test.ts +25 -0
  433. package/src/console/ui/vault/group.ts +50 -0
  434. package/src/console/ui/vault/index.ts +45 -0
  435. package/src/console/ui/vault/search.ts +62 -0
  436. package/src/console/ui/vault/types.ts +57 -0
  437. package/src/console/ui/vite.config.ts +84 -0
  438. package/src/console/xss.gate.test.ts +118 -0
  439. package/src/docker/compose.ts +226 -0
  440. package/src/docker/credentials.ts +25 -0
  441. package/src/docker/derive.ts +83 -0
  442. package/src/docker/docker.test.ts +185 -0
  443. package/src/docker/dockerfile.integration.test.ts +170 -0
  444. package/src/docker/dockerfile.ts +39 -0
  445. package/src/docker/helpers.ts +103 -0
  446. package/src/docker/index.ts +52 -0
  447. package/src/docker/pin.ts +90 -0
  448. package/src/docker/recipes/index.ts +33 -0
  449. package/src/docker/recipes/postgres.ts +18 -0
  450. package/src/docker/recipes/redis.ts +18 -0
  451. package/src/docker/stack.integration.test.ts +140 -0
  452. package/src/docker/stack.ts +62 -0
  453. package/src/docker/types.ts +134 -0
  454. package/src/drivers/ai-anthropic.ts +128 -0
  455. package/src/drivers/ai-mock.ts +82 -0
  456. package/src/drivers/ai-openai-compatible.ts +139 -0
  457. package/src/drivers/ai-providers.test.ts +163 -0
  458. package/src/drivers/ai-types.ts +83 -0
  459. package/src/drivers/bun-native-completeness.test.ts +155 -0
  460. package/src/drivers/channel-console.ts +118 -0
  461. package/src/drivers/channel-fcm.ts +96 -0
  462. package/src/drivers/channel-resend.ts +27 -0
  463. package/src/drivers/channel-smtp.ts +33 -0
  464. package/src/drivers/channel-types.ts +160 -0
  465. package/src/drivers/channel-unifonic.ts +79 -0
  466. package/src/drivers/channel-wa-cloud.ts +84 -0
  467. package/src/drivers/channel-webpush.ts +317 -0
  468. package/src/drivers/conformance.test.ts +89 -0
  469. package/src/drivers/conformance.ts +162 -0
  470. package/src/drivers/fs.ts +73 -0
  471. package/src/drivers/index.ts +194 -0
  472. package/src/drivers/kv-lua.ts +222 -0
  473. package/src/drivers/memory.ts +472 -0
  474. package/src/drivers/pgvector.ts +130 -0
  475. package/src/drivers/postgres.ts +221 -0
  476. package/src/drivers/redis.ts +254 -0
  477. package/src/drivers/s3.ts +129 -0
  478. package/src/drivers/signal-engine.ts +690 -0
  479. package/src/drivers/signal-memory.ts +27 -0
  480. package/src/drivers/signal-nats.ts +131 -0
  481. package/src/drivers/signal-postgres.ts +1089 -0
  482. package/src/drivers/signal-redis.ts +275 -0
  483. package/src/drivers/signal-types.ts +328 -0
  484. package/src/drivers/sqlite.ts +53 -0
  485. package/src/drivers/types.ts +334 -0
  486. package/src/drivers/vault-dotenv-parse.ts +56 -0
  487. package/src/drivers/vault-env.ts +101 -0
  488. package/src/drivers/vault-infisical.ts +61 -0
  489. package/src/drivers/vault-managed.ts +42 -0
  490. package/src/drivers/vault-memory.ts +38 -0
  491. package/src/drivers/vault-openbao.ts +80 -0
  492. package/src/drivers/vault-sops.ts +252 -0
  493. package/src/drivers/vault-types.ts +84 -0
  494. package/src/elements/ai/declare.ts +206 -0
  495. package/src/elements/ai/eval.ts +113 -0
  496. package/src/elements/ai/pii.ts +108 -0
  497. package/src/elements/ai/runtime.ts +540 -0
  498. package/src/elements/ai/schema.ts +194 -0
  499. package/src/elements/ai.test.ts +393 -0
  500. package/src/elements/ai.ts +56 -0
  501. package/src/elements/channel/consent.ts +81 -0
  502. package/src/elements/channel/costs.test.ts +62 -0
  503. package/src/elements/channel/costs.ts +130 -0
  504. package/src/elements/channel/declare.ts +141 -0
  505. package/src/elements/channel/email-auth.test.ts +47 -0
  506. package/src/elements/channel/email-auth.ts +109 -0
  507. package/src/elements/channel/locale.ts +97 -0
  508. package/src/elements/channel/mask.test.ts +19 -0
  509. package/src/elements/channel/mask.ts +60 -0
  510. package/src/elements/channel/mime.ts +23 -0
  511. package/src/elements/channel/outcomes.test.ts +40 -0
  512. package/src/elements/channel/outcomes.ts +126 -0
  513. package/src/elements/channel/receipts.ts +121 -0
  514. package/src/elements/channel/runtime.ts +500 -0
  515. package/src/elements/channel/suppression.ts +136 -0
  516. package/src/elements/channel.test.ts +232 -0
  517. package/src/elements/channel.ts +109 -0
  518. package/src/elements/clock/actions.ts +170 -0
  519. package/src/elements/clock/declare.ts +58 -0
  520. package/src/elements/clock/dst.ts +197 -0
  521. package/src/elements/clock/durable.ts +140 -0
  522. package/src/elements/clock/duration.ts +29 -0
  523. package/src/elements/clock/health.test.ts +103 -0
  524. package/src/elements/clock/health.ts +112 -0
  525. package/src/elements/clock/leader.ts +127 -0
  526. package/src/elements/clock/reconcile.ts +241 -0
  527. package/src/elements/clock/runtime.ts +243 -0
  528. package/src/elements/clock/schedule.ts +262 -0
  529. package/src/elements/clock/time-travel.ts +53 -0
  530. package/src/elements/clock.test.ts +365 -0
  531. package/src/elements/clock.ts +80 -0
  532. package/src/elements/gate/declare.ts +134 -0
  533. package/src/elements/gate/permissions.ts +104 -0
  534. package/src/elements/gate/runtime.ts +177 -0
  535. package/src/elements/gate/strategies.ts +388 -0
  536. package/src/elements/gate.test.ts +145 -0
  537. package/src/elements/gate.ts +41 -0
  538. package/src/elements/index.ts +169 -0
  539. package/src/elements/signal/chaos-child.ts +47 -0
  540. package/src/elements/signal/declare.ts +77 -0
  541. package/src/elements/signal/dry-run-replay.test.ts +95 -0
  542. package/src/elements/signal/dry-run-write-isolation.test.ts +94 -0
  543. package/src/elements/signal/reconcile.test.ts +23 -0
  544. package/src/elements/signal/reconcile.ts +122 -0
  545. package/src/elements/signal/runtime.ts +94 -0
  546. package/src/elements/signal.test.ts +457 -0
  547. package/src/elements/signal.ts +26 -0
  548. package/src/elements/store/cache.ts +197 -0
  549. package/src/elements/store/classify.ts +125 -0
  550. package/src/elements/store/declare.ts +200 -0
  551. package/src/elements/store/files-policy.test.ts +31 -0
  552. package/src/elements/store/files-policy.ts +70 -0
  553. package/src/elements/store/replica.ts +68 -0
  554. package/src/elements/store/runtime.ts +393 -0
  555. package/src/elements/store/sql-condition.ts +205 -0
  556. package/src/elements/store/sql-session.ts +579 -0
  557. package/src/elements/store/table.ts +282 -0
  558. package/src/elements/store.test.ts +370 -0
  559. package/src/elements/store.ts +99 -0
  560. package/src/elements/vault/declare.ts +143 -0
  561. package/src/elements/vault/fingerprint.ts +30 -0
  562. package/src/elements/vault/redact.ts +52 -0
  563. package/src/elements/vault/runtime.ts +461 -0
  564. package/src/elements/vault.test.ts +296 -0
  565. package/src/elements/vault.ts +44 -0
  566. package/src/index.ts +183 -0
  567. package/src/kernel/adopt-routes.ts +194 -0
  568. package/src/kernel/app.ts +1068 -0
  569. package/src/kernel/auth-resolve.ts +93 -0
  570. package/src/kernel/boot.test.ts +173 -0
  571. package/src/kernel/boot.ts +439 -0
  572. package/src/kernel/budget-entry.ts +55 -0
  573. package/src/kernel/budget.test.ts +19 -0
  574. package/src/kernel/call.test.ts +83 -0
  575. package/src/kernel/capability.ts +113 -0
  576. package/src/kernel/dry-run.test.ts +64 -0
  577. package/src/kernel/dry-run.ts +171 -0
  578. package/src/kernel/effects.test.ts +89 -0
  579. package/src/kernel/effects.ts +140 -0
  580. package/src/kernel/errors.registry.test.ts +39 -0
  581. package/src/kernel/errors.ts +225 -0
  582. package/src/kernel/flow.test.ts +154 -0
  583. package/src/kernel/flow.ts +290 -0
  584. package/src/kernel/fx.test.ts +245 -0
  585. package/src/kernel/fx.ts +996 -0
  586. package/src/kernel/hook-timing.test.ts +72 -0
  587. package/src/kernel/hook-timing.ts +190 -0
  588. package/src/kernel/hooks.test.ts +181 -0
  589. package/src/kernel/hooks.ts +275 -0
  590. package/src/kernel/index.ts +278 -0
  591. package/src/kernel/journal.ts +405 -0
  592. package/src/kernel/journey.ts +75 -0
  593. package/src/kernel/on.ts +74 -0
  594. package/src/kernel/pipeline.test.ts +349 -0
  595. package/src/kernel/pipeline.ts +245 -0
  596. package/src/kernel/plug.ts +82 -0
  597. package/src/kernel/plugin/capabilities.test.ts +84 -0
  598. package/src/kernel/plugin/conflicts.test.ts +106 -0
  599. package/src/kernel/plugin/decorate.test.ts +111 -0
  600. package/src/kernel/plugin/scoping.test.ts +138 -0
  601. package/src/kernel/plugin.ts +485 -0
  602. package/src/kernel/rate-limit.ts +25 -0
  603. package/src/kernel/registry.ts +398 -0
  604. package/src/kernel/router.test.ts +114 -0
  605. package/src/kernel/router.ts +556 -0
  606. package/src/kernel/routing-budget.test.ts +19 -0
  607. package/src/kernel/run-telemetry.ts +70 -0
  608. package/src/kernel/triggers.ts +324 -0
  609. package/src/kernel/unit.ts +88 -0
  610. package/src/manifest/diff.test.ts +866 -0
  611. package/src/manifest/diff.ts +1670 -0
  612. package/src/manifest/fixtures/base.manifest.json +112 -0
  613. package/src/manifest/fixtures/skyport.excerpt.json +51 -0
  614. package/src/manifest/fixtures/skyport.manifest.json +153 -0
  615. package/src/manifest/index.ts +27 -0
  616. package/src/manifest/types.ts +358 -0
  617. package/src/manifest/undeclared.test.ts +82 -0
  618. package/src/manifest/undeclared.ts +65 -0
  619. package/src/manifest/validate.test.ts +105 -0
  620. package/src/manifest/validate.ts +174 -0
  621. package/src/mcp/authorization.ts +340 -0
  622. package/src/mcp/confirmation.ts +174 -0
  623. package/src/mcp/data.ts +90 -0
  624. package/src/mcp/index.ts +80 -0
  625. package/src/mcp/injection.gate.test.ts +176 -0
  626. package/src/mcp/mcp.test.ts +348 -0
  627. package/src/mcp/protocol.ts +172 -0
  628. package/src/mcp/server.ts +283 -0
  629. package/src/mcp/session.ts +139 -0
  630. package/src/mcp/tools.ts +391 -0
  631. package/src/plugins/catalogue.test.ts +48 -0
  632. package/src/plugins/catalogue.ts +113 -0
  633. package/src/plugins/node-import-scan.test.ts +28 -0
  634. package/src/plugins/node-import-scan.ts +98 -0
  635. package/src/plugins/supply-chain.test.ts +48 -0
  636. package/src/plugins/supply-chain.ts +460 -0
  637. package/src/release/index.ts +30 -0
  638. package/src/release/limits.ts +20 -0
  639. package/src/release/measure.ts +443 -0
  640. package/src/release/publish.ts +56 -0
  641. package/src/runs/bench.test.ts +93 -0
  642. package/src/runs/collect.ts +144 -0
  643. package/src/runs/drivers/clickhouse.ts +108 -0
  644. package/src/runs/drivers/files.ts +201 -0
  645. package/src/runs/drivers/memory.ts +94 -0
  646. package/src/runs/drivers/postgres.ts +110 -0
  647. package/src/runs/duckdb.ts +99 -0
  648. package/src/runs/index.ts +82 -0
  649. package/src/runs/outlier.ts +180 -0
  650. package/src/runs/parquet.ts +210 -0
  651. package/src/runs/privacy.ts +49 -0
  652. package/src/runs/runs.test.ts +324 -0
  653. package/src/runs/runtime.ts +229 -0
  654. package/src/runs/shred.ts +243 -0
  655. package/src/runs/telemetry.ts +10 -0
  656. package/src/runs/types.ts +179 -0
  657. package/src/runtime/bun.ts +140 -0
  658. package/src/runtime/cold-start.bench.ts +20 -0
  659. package/src/runtime/index.ts +46 -0
  660. package/src/runtime/primitives.ts +242 -0
  661. package/src/runtime/security.test.ts +170 -0
  662. package/src/runtime/security.ts +168 -0
  663. package/src/runtime/serve.test.ts +223 -0
  664. package/src/runtime/types.ts +217 -0
  665. package/src/runtime/web-standard.ts +77 -0
  666. package/src/test/create-test-app.test.ts +161 -0
  667. package/src/test/create-test-app.ts +533 -0
  668. package/src/test/index.ts +21 -0
  669. package/src/test/provisions.integration.test.ts +174 -0
  670. package/src/upgrade/codemods.test.ts +58 -0
  671. package/src/upgrade/codemods.ts +83 -0
  672. package/src/validation/index.ts +23 -0
  673. package/src/validation/standard-schema.test.ts +96 -0
  674. package/src/validation/standard-schema.ts +316 -0
@@ -0,0 +1,3633 @@
1
+ /**
2
+ * Console flows — every action is a real operator-plane flow through `fx`.
3
+ *
4
+ * The audit log is the trace (console §1 · §6).
5
+ */
6
+
7
+ import { z } from "zod";
8
+ import {
9
+ authenticateOperator,
10
+ createOperator,
11
+ issueSession,
12
+ scopesForRoles,
13
+ userPrincipal,
14
+ type IssuedSession,
15
+ } from "../../auth/index.ts";
16
+ import { DryRunWriteIsolationError } from "../../kernel/dry-run.ts";
17
+ import { fail, flow, http, type AnyFlowDef, type Binding } from "../../kernel/index.ts";
18
+ import type { Flow as ManifestFlow, ResourceRef } from "../../manifest/types.ts";
19
+ import type { WideEvent } from "../../runs/types.ts";
20
+ import { bindHttp } from "./bind.ts";
21
+ import { touchLoginRateLimit } from "./auth-rate.ts";
22
+ import { verifyClaimCode } from "./claim.ts";
23
+ import {
24
+ maskWideEventForConsole,
25
+ piiFieldNamesFromManifest,
26
+ } from "./runs-pii.ts";
27
+ import {
28
+ ClockResourceNotFoundError,
29
+ ScheduleNotOverridableError,
30
+ } from "./clock.ts";
31
+ import { createFileDiff, emitStructuralDiff } from "./structural.ts";
32
+ import type { ConsoleState } from "./state.ts";
33
+ import { tenancyDeclared } from "./store.ts";
34
+
35
+ /** Flows that may run without an operator session. */
36
+ export const PUBLIC_CONSOLE_FLOWS = new Set([
37
+ "console.setup.status",
38
+ "console.setup.claim",
39
+ "console.session.login",
40
+ ]);
41
+
42
+ const SetupStatusOut = z.object({
43
+ setupClosed: z.boolean(),
44
+ claimRequired: z.boolean(),
45
+ });
46
+
47
+ const ClaimIn = z.object({
48
+ claimCode: z.string().min(1),
49
+ email: z.string().email(),
50
+ name: z.string().min(1),
51
+ password: z.string().min(8),
52
+ });
53
+
54
+ const SessionOut = z.object({
55
+ operatorId: z.string(),
56
+ email: z.string(),
57
+ name: z.string(),
58
+ accessToken: z.string(),
59
+ refreshToken: z.string(),
60
+ accessExpiresAt: z.number(),
61
+ });
62
+
63
+ const LoginIn = z.object({
64
+ email: z.string().email(),
65
+ password: z.string().min(1),
66
+ });
67
+
68
+ const MeOut = z.object({
69
+ operatorId: z.string(),
70
+ email: z.string(),
71
+ name: z.string(),
72
+ setupClosed: z.boolean(),
73
+ });
74
+
75
+ const ManifestOut = z.object({
76
+ manifest: z.unknown().nullable(),
77
+ });
78
+
79
+ const EffectEntryOut = z.object({
80
+ kind: z.enum([
81
+ "read",
82
+ "write",
83
+ "emit",
84
+ "send",
85
+ "ask",
86
+ "secret",
87
+ "call",
88
+ ]),
89
+ resource: z.string(),
90
+ timestamp: z.number(),
91
+ duration: z.number(),
92
+ reversibility: z.enum([
93
+ "none",
94
+ "reversible",
95
+ "deferred",
96
+ "irreversible",
97
+ "capability",
98
+ "portal",
99
+ ]),
100
+ });
101
+
102
+ const LogLineOut = z.object({
103
+ level: z.enum(["debug", "info", "warn", "error"]),
104
+ message: z.string(),
105
+ data: z.record(z.string(), z.unknown()).optional(),
106
+ at: z.number(),
107
+ });
108
+
109
+ /** Wide-event projection — Runs · Traces · Overview share one store (console §9.11). */
110
+ const RunsListOut = z.object({
111
+ runs: z.array(
112
+ z.object({
113
+ id: z.string(),
114
+ parentId: z.string().nullable(),
115
+ flow: z.string(),
116
+ unit: z.string().nullable(),
117
+ trigger: z.string(),
118
+ plane: z.string(),
119
+ tenant: z.string().nullable(),
120
+ principal: z.string().nullable(),
121
+ gates: z.array(z.string()),
122
+ cache: z.enum(["hit", "miss", "none"]),
123
+ replica: z.enum(["primary", "replica"]).nullable(),
124
+ replicaLagMs: z.number().nullable(),
125
+ cost: z.number().nullable(),
126
+ promptVersion: z.number().nullable(),
127
+ buildVersion: z.string().nullable(),
128
+ startedAt: z.number(),
129
+ endedAt: z.number(),
130
+ durationMs: z.number(),
131
+ error: z.string().nullable(),
132
+ sampled: z.enum(["full", "error", "sample", "boost"]),
133
+ effects: z.array(EffectEntryOut),
134
+ /** `fx.log` lines — a field on the run, not a parallel stream. */
135
+ logs: z.array(LogLineOut),
136
+ /** All queryable dimensions for population analysis. */
137
+ dimensions: z.record(
138
+ z.string(),
139
+ z.union([z.string(), z.number(), z.boolean(), z.null()]),
140
+ ),
141
+ }),
142
+ ),
143
+ });
144
+
145
+ const TracesReplayIn = z.object({
146
+ rootId: z.string().min(1),
147
+ dryRun: z.boolean(),
148
+ });
149
+
150
+ const TracesReplayOut = z.object({
151
+ ok: z.literal(true),
152
+ rootId: z.string(),
153
+ dryRun: z.boolean(),
154
+ at: z.number(),
155
+ });
156
+
157
+ const SignalEndpointOut = z.object({
158
+ flowId: z.string(),
159
+ durable: z.boolean(),
160
+ external: z.boolean(),
161
+ peakTier: z.enum([
162
+ "none",
163
+ "reads",
164
+ "writes",
165
+ "emits",
166
+ "external",
167
+ "capabilities",
168
+ ]),
169
+ });
170
+
171
+ const DeadLetterOut = z.object({
172
+ id: z.string(),
173
+ signal: z.string(),
174
+ payload: z.unknown(),
175
+ delivery: z.enum(["once", "broadcast", "live"]),
176
+ attempts: z.number(),
177
+ failures: z.array(
178
+ z.object({
179
+ code: z.string(),
180
+ message: z.string(),
181
+ at: z.number(),
182
+ attempt: z.number(),
183
+ }),
184
+ ),
185
+ createdAt: z.number(),
186
+ availableAt: z.number(),
187
+ status: z.literal("dead"),
188
+ causeRunId: z.string().optional(),
189
+ causeFlow: z.string().optional(),
190
+ });
191
+
192
+ const SignalsListOut = z.object({
193
+ signals: z.array(
194
+ z.object({
195
+ name: z.string(),
196
+ delivery: z.enum(["once", "broadcast", "live"]),
197
+ retries: z.number(),
198
+ deadLetterEnabled: z.boolean(),
199
+ orphaned: z.boolean(),
200
+ pending: z.number(),
201
+ inflight: z.number(),
202
+ dead: z.number(),
203
+ delivered: z.number(),
204
+ outboxLagMs: z.number().nullable(),
205
+ connections: z.number(),
206
+ throughputPerSec: z.number(),
207
+ schema: z.unknown().optional(),
208
+ subscribers: z.array(
209
+ z.object({
210
+ id: z.string(),
211
+ lag: z.number(),
212
+ errorCount: z.number(),
213
+ }),
214
+ ),
215
+ recentLive: z.array(z.unknown()),
216
+ deadLetters: z.array(DeadLetterOut),
217
+ producers: z.array(SignalEndpointOut),
218
+ consumers: z.array(SignalEndpointOut),
219
+ consumersDurable: z.boolean().nullable(),
220
+ }),
221
+ ),
222
+ });
223
+
224
+ const SignalsReplayIn = z.object({
225
+ signal: z.string().min(1),
226
+ messageIds: z.array(z.string()).optional(),
227
+ subscriberId: z.string().optional(),
228
+ ratePerSec: z.number().min(1).max(1_000).default(10),
229
+ dryRun: z.boolean(),
230
+ payloads: z.record(z.string(), z.unknown()).optional(),
231
+ /** Typed confirmation phrase for irreversible production replay. */
232
+ confirmation: z.string().optional(),
233
+ /** Recorded reason for irreversible production replay. */
234
+ reason: z.string().optional(),
235
+ });
236
+
237
+ const SignalsReplayOut = z.object({
238
+ ok: z.literal(true),
239
+ attempted: z.number(),
240
+ succeeded: z.number(),
241
+ failed: z.number(),
242
+ dryRun: z.boolean(),
243
+ results: z.array(
244
+ z.object({
245
+ id: z.string(),
246
+ ok: z.boolean(),
247
+ error: z
248
+ .object({ code: z.string(), message: z.string() })
249
+ .optional(),
250
+ }),
251
+ ),
252
+ wouldHaveFired: z.array(
253
+ z.object({
254
+ kind: z.enum(["send", "ask"]),
255
+ resource: z.string(),
256
+ messageId: z.string().optional(),
257
+ }),
258
+ ),
259
+ at: z.number(),
260
+ });
261
+
262
+ const SignalsDiscardIn = z.object({
263
+ signal: z.string().min(1),
264
+ messageIds: z.array(z.string()).min(1),
265
+ confirmation: z.string().optional(),
266
+ reason: z.string().optional(),
267
+ });
268
+
269
+ const SignalsDiscardOut = z.object({
270
+ ok: z.literal(true),
271
+ discarded: z.number(),
272
+ at: z.number(),
273
+ });
274
+
275
+ const ActionPingIn = z.object({
276
+ note: z.string().optional(),
277
+ });
278
+
279
+ const ActionPingOut = z.object({
280
+ ok: z.literal(true),
281
+ note: z.string().optional(),
282
+ at: z.number(),
283
+ });
284
+
285
+ const StructuralIn = z.object({
286
+ title: z.string().min(1),
287
+ relativePath: z.string().min(1),
288
+ contents: z.string(),
289
+ reason: z.string().min(1),
290
+ });
291
+
292
+ const StructuralOut = z.object({
293
+ id: z.string(),
294
+ path: z.string(),
295
+ applied: z.literal(false),
296
+ });
297
+
298
+ const IdentitiesOut = z.object({
299
+ identities: z.array(
300
+ z.object({
301
+ id: z.string(),
302
+ email: z.string(),
303
+ name: z.string(),
304
+ status: z.enum(["active", "disabled"]),
305
+ scopes: z.array(z.string()),
306
+ }),
307
+ ),
308
+ });
309
+
310
+ const InvokeIn = z.object({
311
+ flowId: z.string().min(1),
312
+ body: z.unknown(),
313
+ asUserId: z.string().min(1),
314
+ /** Typed confirmation phrase for irreversible production invokes. */
315
+ confirmation: z.string().optional(),
316
+ /** Recorded reason for irreversible production invokes. */
317
+ reason: z.string().optional(),
318
+ });
319
+
320
+ const InvokeOut = z.object({
321
+ ok: z.literal(true),
322
+ flowId: z.string(),
323
+ asUserId: z.string(),
324
+ trigger: z.enum(["http", "signal", "clock", "internal", "durable"]),
325
+ response: z.unknown(),
326
+ peakTier: z.enum([
327
+ "none",
328
+ "reads",
329
+ "writes",
330
+ "emits",
331
+ "external",
332
+ "capabilities",
333
+ ]),
334
+ auditedAt: z.number(),
335
+ });
336
+
337
+ const SetupClosed = z.object({ reason: z.string() });
338
+ const ClaimFailed = z.object({ reason: z.string() });
339
+ const AuthFailed = z.object({});
340
+ const AuthRateLimited = z.object({ reason: z.string() });
341
+ const NotFound = z.object({ flowId: z.string() });
342
+ const InvokeDenied = z.object({ reason: z.string() });
343
+ const ConfirmRequired = z.object({
344
+ phrase: z.enum([
345
+ "INVOKE",
346
+ "REPLAY",
347
+ "DISCARD",
348
+ "EDIT",
349
+ "DELETE",
350
+ "PURGE",
351
+ "SET",
352
+ "ROTATE",
353
+ "REVOKE",
354
+ "RUN",
355
+ "SEND",
356
+ ]),
357
+ reason: z.string(),
358
+ });
359
+
360
+ const ChannelNotFound = z.object({ template: z.string() });
361
+
362
+ const ChannelOutcomeOut = z.object({
363
+ state: z.enum([
364
+ "suppressed/opted-out",
365
+ "suppressed/prior-bounce",
366
+ "blocked/invalid-address",
367
+ "soft-bounce",
368
+ "hard-bounce",
369
+ "provider-error",
370
+ "delivered-then-complained",
371
+ ]),
372
+ count: z.number(),
373
+ verdict: z.enum(["correct", "retry", "suppress", "review"]),
374
+ weight: z.number(),
375
+ });
376
+
377
+ const ChannelsListOut = z.object({
378
+ face: z.enum(["inbox", "deliverability"]),
379
+ production: z.boolean(),
380
+ templates: z.array(
381
+ z.object({
382
+ name: z.string(),
383
+ medium: z.string(),
384
+ locales: z.array(z.string()),
385
+ from: z.string().nullable(),
386
+ schema: z.unknown(),
387
+ }),
388
+ ),
389
+ outcomes: z.array(ChannelOutcomeOut),
390
+ fallback: z.object({
391
+ template: z.string().nullable(),
392
+ chainExample: z.string(),
393
+ fallbackRate: z.number(),
394
+ fallbackCount: z.number(),
395
+ totalCount: z.number(),
396
+ weeklyDeltaUsd: z.number(),
397
+ primaryMedium: z.string(),
398
+ fallbackMedium: z.string(),
399
+ summary: z.string(),
400
+ }),
401
+ inbox: z.array(
402
+ z.object({
403
+ id: z.string(),
404
+ medium: z.string(),
405
+ toMasked: z.string(),
406
+ subject: z.string().nullable(),
407
+ text: z.string().nullable(),
408
+ html: z.string().nullable(),
409
+ template: z.string().nullable(),
410
+ locale: z.string().nullable(),
411
+ at: z.number(),
412
+ }),
413
+ ),
414
+ receipts: z.array(
415
+ z.object({
416
+ id: z.string(),
417
+ template: z.string(),
418
+ toMasked: z.string(),
419
+ medium: z.string(),
420
+ locale: z.string().nullable(),
421
+ localeChain: z.array(z.string()),
422
+ status: z.string(),
423
+ chain: z.string(),
424
+ messageId: z.string().nullable(),
425
+ at: z.number(),
426
+ error: z.string().nullable(),
427
+ }),
428
+ ),
429
+ suppression: z.array(
430
+ z.object({
431
+ subjectMasked: z.string(),
432
+ medium: z.string(),
433
+ reason: z.enum(["opted-out", "prior-bounce"]),
434
+ at: z.number(),
435
+ }),
436
+ ),
437
+ });
438
+
439
+ const ChannelPreviewIn = z.object({
440
+ template: z.string().min(1),
441
+ locale: z.string().optional(),
442
+ profileLocale: z.string().optional(),
443
+ acceptLanguage: z.string().optional(),
444
+ data: z.record(z.string(), z.unknown()).optional(),
445
+ });
446
+
447
+ const ChannelPreviewOut = z.object({
448
+ template: z.string(),
449
+ locale: z.string(),
450
+ localeChain: z.array(z.string()),
451
+ dir: z.enum(["ltr", "rtl"]),
452
+ subject: z.string().nullable(),
453
+ text: z.string().nullable(),
454
+ html: z.string().nullable(),
455
+ });
456
+
457
+ const ChannelVerifyAuthIn = z.object({
458
+ from: z.string().min(1),
459
+ });
460
+
461
+ const ChannelVerifyAuthOut = z.object({
462
+ domain: z.string(),
463
+ spf: z.enum(["pass", "fail", "missing"]),
464
+ dkim: z.enum(["pass", "fail", "missing"]),
465
+ dmarc: z.enum(["pass", "fail", "missing"]),
466
+ checkedAt: z.number(),
467
+ });
468
+
469
+ const ChannelRevealIn = z.object({
470
+ id: z.string().min(1),
471
+ });
472
+
473
+ const ChannelRevealOut = z.object({
474
+ ok: z.literal(true),
475
+ id: z.string(),
476
+ to: z.string(),
477
+ at: z.number(),
478
+ });
479
+
480
+ const ChannelSendTestIn = z.object({
481
+ template: z.string().min(1),
482
+ to: z.string().min(1),
483
+ locale: z.string().optional(),
484
+ data: z.record(z.string(), z.unknown()).optional(),
485
+ confirmation: z.string().optional(),
486
+ reason: z.string().optional(),
487
+ });
488
+
489
+ const ChannelSendTestOut = z.object({
490
+ ok: z.boolean(),
491
+ messageId: z.string(),
492
+ status: z.string(),
493
+ chain: z.string(),
494
+ at: z.number(),
495
+ });
496
+
497
+ const VaultNotFound = z.object({ name: z.string() });
498
+ const ClockNotFound = z.object({ kind: z.enum(["cron", "run"]), id: z.string() });
499
+ const ScheduleNotOverridable = z.object({ name: z.string() });
500
+
501
+ const CronHealthOut = z.object({
502
+ driftMs: z.number().nullable(),
503
+ overdue: z.boolean(),
504
+ missedRuns: z.number(),
505
+ catchUp: z.literal("one"),
506
+ leaderInstanceId: z.string().optional(),
507
+ leaderLeaseUntil: z.number().optional(),
508
+ });
509
+
510
+ const ClockCronOut = z.object({
511
+ name: z.string(),
512
+ status: z.enum(["active", "paused", "orphaned"]),
513
+ timezone: z.string(),
514
+ overridable: z.boolean(),
515
+ declaredCron: z.string().optional(),
516
+ declaredEvery: z.string().optional(),
517
+ effectiveCron: z.string().optional(),
518
+ effectiveEvery: z.string().optional(),
519
+ lastRunAt: z.number().optional(),
520
+ nextRunAt: z.number().optional(),
521
+ health: CronHealthOut,
522
+ dstAmbiguity: z
523
+ .object({
524
+ kind: z.enum(["gap", "overlap"]),
525
+ reason: z.string(),
526
+ on: z.string(),
527
+ localTime: z.string(),
528
+ })
529
+ .nullable(),
530
+ external: z.boolean(),
531
+ flowIds: z.array(z.string()),
532
+ });
533
+
534
+ const WaitingOnOut = z.object({
535
+ runId: z.string(),
536
+ flow: z.string(),
537
+ label: z.string(),
538
+ wakeAt: z.number(),
539
+ wakeInMs: z.number(),
540
+ step: z.string().nullable(),
541
+ });
542
+
543
+ const ClockListOut = z.object({
544
+ now: z.number(),
545
+ crons: z.array(ClockCronOut),
546
+ waitingOn: z.array(WaitingOnOut),
547
+ waitingOnCounts: z.array(
548
+ z.object({ label: z.string(), count: z.number() }),
549
+ ),
550
+ timeline: z.array(
551
+ z.object({
552
+ at: z.number(),
553
+ kind: z.enum(["cron", "wake"]),
554
+ name: z.string(),
555
+ meta: z.string().optional(),
556
+ }),
557
+ ),
558
+ });
559
+
560
+ const ClockRunNowIn = z.object({
561
+ name: z.string().min(1),
562
+ confirmation: z.string().optional(),
563
+ reason: z.string().optional(),
564
+ });
565
+
566
+ const ClockRunNowOut = z.object({
567
+ ok: z.literal(true),
568
+ name: z.string(),
569
+ ran: z.boolean(),
570
+ at: z.number(),
571
+ });
572
+
573
+ const ClockPauseIn = z.object({ name: z.string().min(1) });
574
+ const ClockPauseOut = z.object({
575
+ ok: z.literal(true),
576
+ name: z.string(),
577
+ status: z.string(),
578
+ at: z.number(),
579
+ });
580
+
581
+ const ClockEditIn = z.object({
582
+ name: z.string().min(1),
583
+ cron: z.string().optional(),
584
+ every: z.string().optional(),
585
+ });
586
+
587
+ const ClockEditOut = z.object({
588
+ ok: z.literal(true),
589
+ name: z.string(),
590
+ effectiveCron: z.string().optional(),
591
+ effectiveEvery: z.string().optional(),
592
+ at: z.number(),
593
+ });
594
+
595
+ const ClockWakeEarlyIn = z.object({ runId: z.string().min(1) });
596
+ const ClockWakeEarlyOut = z.object({
597
+ ok: z.literal(true),
598
+ runId: z.string(),
599
+ wakeAt: z.number(),
600
+ resumed: z.boolean(),
601
+ at: z.number(),
602
+ });
603
+
604
+ const VaultResolutionStepOut = z.object({
605
+ source: z.enum([
606
+ "process.env",
607
+ ".env.local",
608
+ ".env.stack",
609
+ "driver",
610
+ "dev-fallback",
611
+ ]),
612
+ present: z.boolean(),
613
+ won: z.boolean(),
614
+ });
615
+
616
+ const VaultBlastRadiusOut = z.object({
617
+ count: z.number(),
618
+ longestWakeAt: z.number().nullable(),
619
+ longestOutstandingMs: z.number().nullable(),
620
+ runIds: z.array(z.string()),
621
+ });
622
+
623
+ const VaultRowOut = z.object({
624
+ name: z.string(),
625
+ kind: z.enum(["secret", "config"]),
626
+ sensitive: z.boolean(),
627
+ description: z.string().optional(),
628
+ rotate: z.string().optional(),
629
+ fingerprints: z.record(z.string(), z.string()),
630
+ fingerprint: z.string().nullable(),
631
+ cleartext: z.string().nullable(),
632
+ winner: z
633
+ .enum([
634
+ "process.env",
635
+ ".env.local",
636
+ ".env.stack",
637
+ "driver",
638
+ "dev-fallback",
639
+ ])
640
+ .nullable(),
641
+ resolution: z.array(VaultResolutionStepOut),
642
+ readers: z.array(z.string()),
643
+ blastRadius: VaultBlastRadiusOut,
644
+ lastReadAt: z.number().nullable(),
645
+ sharedFingerprintEnvs: z.array(z.string()),
646
+ });
647
+
648
+ const VaultListOut = z.object({
649
+ secrets: z.array(VaultRowOut),
650
+ env: z.string(),
651
+ });
652
+
653
+ const VaultWriteIn = z.object({
654
+ name: z.string().min(1),
655
+ value: z.string().min(1),
656
+ confirmation: z.string().optional(),
657
+ reason: z.string().optional(),
658
+ });
659
+
660
+ const VaultWriteOut = z.object({
661
+ ok: z.literal(true),
662
+ name: z.string(),
663
+ fingerprint: z.string().nullable(),
664
+ at: z.number(),
665
+ });
666
+
667
+ const AiMetricOut = z.object({
668
+ samples: z.array(z.number()),
669
+ mean: z.number(),
670
+ p50: z.number(),
671
+ p95: z.number(),
672
+ buckets: z.array(
673
+ z.object({
674
+ min: z.number(),
675
+ max: z.number(),
676
+ count: z.number(),
677
+ }),
678
+ ),
679
+ });
680
+
681
+ const AiListOut = z.object({
682
+ prompts: z.array(
683
+ z.object({
684
+ name: z.string(),
685
+ version: z.number().optional(),
686
+ model: z.string().optional(),
687
+ evals: z.string().optional(),
688
+ budgetMaxCostPerCall: z.number().nullable(),
689
+ manifestDiffPath: z.string(),
690
+ }),
691
+ ),
692
+ agents: z.array(
693
+ z.object({
694
+ name: z.string(),
695
+ tools: z.array(z.string()),
696
+ maxSteps: z.number().optional(),
697
+ model: z.string().optional(),
698
+ budgetMaxCostPerRun: z.number().nullable(),
699
+ }),
700
+ ),
701
+ versions: z.array(
702
+ z.object({
703
+ prompt: z.string(),
704
+ version: z.number(),
705
+ sampleCount: z.number(),
706
+ cost: AiMetricOut,
707
+ latencyMs: AiMetricOut,
708
+ evalScore: AiMetricOut,
709
+ schemaInvalidRate: z.number(),
710
+ providerErrorRate: z.number(),
711
+ okRate: z.number(),
712
+ overBudgetRate: z.number(),
713
+ budgetMaxCostPerCall: z.number().nullable(),
714
+ outcomeCounts: z.object({
715
+ ok: z.number(),
716
+ provider_error: z.number(),
717
+ schema_invalid: z.number(),
718
+ }),
719
+ }),
720
+ ),
721
+ allowPii: z.array(
722
+ z.object({
723
+ flowId: z.string(),
724
+ asks: z.array(z.string()),
725
+ pii: z.enum(["masked", "allow", "denied"]).nullable(),
726
+ allowPii: z.boolean(),
727
+ source: z.string().nullable(),
728
+ }),
729
+ ),
730
+ fallbackChains: z.array(
731
+ z.object({
732
+ prompt: z.string(),
733
+ version: z.number().optional(),
734
+ attempts: z.array(
735
+ z.object({
736
+ model: z.string(),
737
+ ok: z.boolean(),
738
+ error: z.string().optional(),
739
+ cost: z.number().optional(),
740
+ latencyMs: z.number().optional(),
741
+ at: z.number(),
742
+ }),
743
+ ),
744
+ actualCost: z.number(),
745
+ primaryOnlyCost: z.number().nullable(),
746
+ costConsequence: z.number().nullable(),
747
+ at: z.number(),
748
+ }),
749
+ ),
750
+ agentRuns: z.array(
751
+ z.object({
752
+ id: z.string(),
753
+ agent: z.string(),
754
+ message: z.string(),
755
+ ok: z.boolean(),
756
+ steps: z.number(),
757
+ cost: z.number(),
758
+ at: z.number(),
759
+ trail: z.array(
760
+ z.object({
761
+ tool: z.string(),
762
+ status: z.enum(["ok", "denied"]),
763
+ effects: z.array(
764
+ z.object({
765
+ kind: z.enum([
766
+ "read",
767
+ "write",
768
+ "emit",
769
+ "send",
770
+ "ask",
771
+ "secret",
772
+ "call",
773
+ ]),
774
+ resource: z.string(),
775
+ }),
776
+ ),
777
+ denial: z
778
+ .object({
779
+ agent: z.string(),
780
+ tool: z.string(),
781
+ gate: z.string(),
782
+ reason: z.string(),
783
+ at: z.number(),
784
+ })
785
+ .nullable(),
786
+ at: z.number(),
787
+ }),
788
+ ),
789
+ denials: z.array(
790
+ z.object({
791
+ agent: z.string(),
792
+ tool: z.string(),
793
+ gate: z.string(),
794
+ reason: z.string(),
795
+ at: z.number(),
796
+ }),
797
+ ),
798
+ }),
799
+ ),
800
+ denials: z.array(
801
+ z.object({
802
+ agent: z.string(),
803
+ tool: z.string(),
804
+ gate: z.string(),
805
+ reason: z.string(),
806
+ at: z.number(),
807
+ }),
808
+ ),
809
+ });
810
+
811
+ const GatesListOut = z.object({
812
+ moduleActions: z.array(z.string()),
813
+ flows: z.array(
814
+ z.object({
815
+ flowId: z.string(),
816
+ plane: z.enum(["user", "operator"]),
817
+ gates: z.array(z.string()),
818
+ unguarded: z.boolean(),
819
+ }),
820
+ ),
821
+ gates: z.array(
822
+ z.object({
823
+ name: z.string(),
824
+ kind: z.enum(["policy", "rate"]),
825
+ scopes: z.array(z.string()),
826
+ roles: z.array(z.string()),
827
+ strategy: z.string().optional(),
828
+ max: z.number().optional(),
829
+ per: z.string().optional(),
830
+ keyBy: z.string().optional(),
831
+ overridable: z.boolean(),
832
+ attachedTo: z.array(z.string()),
833
+ }),
834
+ ),
835
+ principals: z.array(
836
+ z.object({
837
+ kind: z.enum(["role", "key", "user"]),
838
+ id: z.string(),
839
+ name: z.string(),
840
+ plane: z.enum(["user", "operator"]),
841
+ scopes: z.array(z.string()),
842
+ memberCount: z.number().optional(),
843
+ email: z.string().optional(),
844
+ }),
845
+ ),
846
+ violations: z.array(
847
+ z.object({
848
+ kind: z.literal("operator-application-scope"),
849
+ operatorId: z.string(),
850
+ name: z.string(),
851
+ email: z.string(),
852
+ applicationScopes: z.array(z.string()),
853
+ }),
854
+ ),
855
+ audit: z.object({
856
+ unguardedFlows: z.array(z.string()),
857
+ orphanPermissions: z.array(z.string()),
858
+ emptyRoles: z.array(z.string()),
859
+ unattachedGates: z.array(z.string()),
860
+ }),
861
+ widenings: z.array(
862
+ z.object({
863
+ path: z.string(),
864
+ category: z.string(),
865
+ kind: z.string(),
866
+ summary: z.string(),
867
+ before: z.unknown().optional(),
868
+ after: z.unknown().optional(),
869
+ }),
870
+ ),
871
+ });
872
+
873
+ const DiffChangeOut = z.object({
874
+ path: z.string(),
875
+ category: z.enum([
876
+ "contract-breaking",
877
+ "permission-widening",
878
+ "effect-widening",
879
+ "no-impact",
880
+ ]),
881
+ kind: z.enum(["added", "removed", "changed"]),
882
+ summary: z.string(),
883
+ before: z.unknown().optional(),
884
+ after: z.unknown().optional(),
885
+ flowName: z.string().nullable(),
886
+ runCountLastWeek: z.number(),
887
+ blastLine: z.string().nullable(),
888
+ weeklyDeltaUsd: z.number().nullable(),
889
+ weeklyBillLine: z.string().nullable(),
890
+ ciGate: z.enum(["blocked", "acknowledged"]).nullable(),
891
+ });
892
+
893
+ const DiffListOut = z.object({
894
+ hasBaseline: z.boolean(),
895
+ severity: z
896
+ .enum([
897
+ "contract-breaking",
898
+ "permission-widening",
899
+ "effect-widening",
900
+ "no-impact",
901
+ ])
902
+ .nullable(),
903
+ blockedCount: z.number(),
904
+ acknowledgedCount: z.number(),
905
+ changes: z.array(DiffChangeOut),
906
+ });
907
+
908
+ const SupplySignalStates = z.enum([
909
+ "pass",
910
+ "fail",
911
+ "hold",
912
+ "not-applicable",
913
+ "unknown",
914
+ "clean",
915
+ "conflict",
916
+ ]);
917
+
918
+ const PluginsListOut = z.object({
919
+ stateDerivation: z.string(),
920
+ plugins: z.array(
921
+ z.object({
922
+ id: z.string(),
923
+ origin: z.enum(["core", "local", "community"]),
924
+ state: z.enum(["on", "off"]),
925
+ version: z.string().nullable(),
926
+ summary: z.string().nullable(),
927
+ scopes: z.array(
928
+ z.object({
929
+ kind: z.enum(["app", "unit", "flow"]),
930
+ name: z.string().optional(),
931
+ }),
932
+ ),
933
+ declares: z.array(z.string()),
934
+ intercepts: z.array(
935
+ z.object({
936
+ stage: z.string(),
937
+ meanMs: z.number().nullable(),
938
+ count: z.number(),
939
+ }),
940
+ ),
941
+ hookCost: z
942
+ .object({
943
+ count: z.number(),
944
+ meanMs: z.number(),
945
+ p50Ms: z.number(),
946
+ p95Ms: z.number(),
947
+ lastMs: z.number().nullable(),
948
+ })
949
+ .nullable(),
950
+ supplyChain: z.object({
951
+ lifecycleScripts: z.object({
952
+ state: SupplySignalStates,
953
+ scripts: z.array(z.string()),
954
+ detail: z.string(),
955
+ }),
956
+ releaseCooldown: z.object({
957
+ state: SupplySignalStates,
958
+ publishedAt: z.number().nullable(),
959
+ holdUntil: z.number().nullable(),
960
+ detail: z.string(),
961
+ }),
962
+ nodeImportScan: z.object({
963
+ state: SupplySignalStates,
964
+ findings: z.array(
965
+ z.object({
966
+ source: z.string(),
967
+ specifier: z.string(),
968
+ line: z.number().nullable(),
969
+ }),
970
+ ),
971
+ detail: z.string(),
972
+ }),
973
+ npmProvenance: z.object({
974
+ state: SupplySignalStates,
975
+ detail: z.string(),
976
+ }),
977
+ bootConflicts: z.object({
978
+ state: SupplySignalStates,
979
+ conflicts: z.array(z.string()),
980
+ detail: z.string(),
981
+ }),
982
+ }),
983
+ capabilityDiff: z.array(
984
+ z.object({
985
+ path: z.string(),
986
+ category: z.string(),
987
+ kind: z.string(),
988
+ summary: z.string(),
989
+ }),
990
+ ),
991
+ installCommand: z.string().nullable(),
992
+ enableHint: z.string().nullable(),
993
+ packageName: z.string().nullable(),
994
+ }),
995
+ ),
996
+ });
997
+
998
+ const GatesSimulateIn = z.object({
999
+ flowId: z.string().min(1),
1000
+ principal: z.object({
1001
+ kind: z.enum(["role", "key", "user"]),
1002
+ id: z.string().min(1),
1003
+ }),
1004
+ meta: z
1005
+ .object({
1006
+ ip: z.string().optional(),
1007
+ })
1008
+ .optional(),
1009
+ });
1010
+
1011
+ const GatesSimulateOut = z.object({
1012
+ flowId: z.string(),
1013
+ gates: z.array(z.string()),
1014
+ evaluations: z.array(
1015
+ z.object({
1016
+ name: z.string(),
1017
+ allowed: z.boolean(),
1018
+ kind: z.enum(["policy", "rate"]),
1019
+ remaining: z.number().optional(),
1020
+ retryAfterMs: z.number().optional(),
1021
+ reason: z.string().optional(),
1022
+ }),
1023
+ ),
1024
+ deniedAt: z.string().nullable(),
1025
+ denial: z
1026
+ .object({
1027
+ code: z.enum(["Unauthorized", "Forbidden", "RateLimited"]),
1028
+ data: z.record(z.string(), z.unknown()),
1029
+ status: z.union([z.literal(401), z.literal(403), z.literal(429)]),
1030
+ })
1031
+ .nullable(),
1032
+ allowed: z.boolean(),
1033
+ });
1034
+
1035
+ const GatesPowersIn = z.object({
1036
+ kind: z.enum(["role", "key", "user"]),
1037
+ id: z.string().min(1),
1038
+ });
1039
+
1040
+ const GatesPowersOut = z.object({
1041
+ scopes: z.array(z.string()),
1042
+ allowedFlowIds: z.array(z.string()),
1043
+ deniedFlowIds: z.array(z.string()),
1044
+ });
1045
+
1046
+ const AccessPlaneSection = z.object({
1047
+ plane: z.enum(["user", "operator"]),
1048
+ operators: z
1049
+ .array(
1050
+ z.object({
1051
+ id: z.string(),
1052
+ email: z.string(),
1053
+ name: z.string(),
1054
+ status: z.enum(["active", "suspended", "invited"]),
1055
+ roles: z.array(z.string()),
1056
+ scopes: z.array(z.string()),
1057
+ lastSeenAt: z.number().nullable(),
1058
+ neverSignedIn: z.boolean(),
1059
+ }),
1060
+ )
1061
+ .optional(),
1062
+ users: z
1063
+ .array(
1064
+ z.object({
1065
+ id: z.string(),
1066
+ email: z.string(),
1067
+ name: z.string(),
1068
+ status: z.enum(["active", "disabled"]),
1069
+ roles: z.array(z.string()),
1070
+ scopes: z.array(z.string()),
1071
+ }),
1072
+ )
1073
+ .optional(),
1074
+ roles: z.array(
1075
+ z.object({
1076
+ id: z.string(),
1077
+ name: z.string(),
1078
+ plane: z.enum(["user", "operator"]),
1079
+ description: z.string(),
1080
+ scopes: z.array(z.string()),
1081
+ memberCount: z.number(),
1082
+ }),
1083
+ ),
1084
+ keys: z.array(
1085
+ z.object({
1086
+ id: z.string(),
1087
+ name: z.string(),
1088
+ plane: z.enum(["user", "operator"]),
1089
+ scopes: z.array(z.string()),
1090
+ createdAt: z.number(),
1091
+ lastUsedAt: z.number().nullable(),
1092
+ expiresAt: z.number().nullable(),
1093
+ revokedAt: z.number().nullable(),
1094
+ rateLimit: z
1095
+ .object({ max: z.number(), per: z.string() })
1096
+ .nullable(),
1097
+ ipAllowlist: z.array(z.string()),
1098
+ unused90d: z.boolean(),
1099
+ }),
1100
+ ),
1101
+ invites: z
1102
+ .array(
1103
+ z.object({
1104
+ id: z.string(),
1105
+ email: z.string(),
1106
+ invitedBy: z.string(),
1107
+ createdAt: z.number(),
1108
+ expiresAt: z.number(),
1109
+ expired: z.boolean(),
1110
+ }),
1111
+ )
1112
+ .optional(),
1113
+ grantableScopes: z.array(z.string()),
1114
+ });
1115
+
1116
+ const AccessListOut = z.object({
1117
+ operatorPlane: AccessPlaneSection,
1118
+ userPlane: AccessPlaneSection,
1119
+ hygiene: z.object({
1120
+ unusedKeys: z.array(
1121
+ z.object({
1122
+ id: z.string(),
1123
+ name: z.string(),
1124
+ plane: z.enum(["user", "operator"]),
1125
+ scopes: z.array(z.string()),
1126
+ createdAt: z.number(),
1127
+ lastUsedAt: z.number().nullable(),
1128
+ expiresAt: z.number().nullable(),
1129
+ revokedAt: z.number().nullable(),
1130
+ rateLimit: z
1131
+ .object({ max: z.number(), per: z.string() })
1132
+ .nullable(),
1133
+ ipAllowlist: z.array(z.string()),
1134
+ unused90d: z.boolean(),
1135
+ }),
1136
+ ),
1137
+ neverSignedInOperators: z.array(
1138
+ z.object({
1139
+ id: z.string(),
1140
+ email: z.string(),
1141
+ name: z.string(),
1142
+ status: z.enum(["active", "suspended", "invited"]),
1143
+ roles: z.array(z.string()),
1144
+ scopes: z.array(z.string()),
1145
+ lastSeenAt: z.number().nullable(),
1146
+ neverSignedIn: z.boolean(),
1147
+ }),
1148
+ ),
1149
+ expiredInvitations: z.array(
1150
+ z.object({
1151
+ id: z.string(),
1152
+ email: z.string(),
1153
+ invitedBy: z.string(),
1154
+ createdAt: z.number(),
1155
+ expiresAt: z.number(),
1156
+ expired: z.boolean(),
1157
+ }),
1158
+ ),
1159
+ }),
1160
+ accessTtlMs: z.number(),
1161
+ catalog: z.array(z.string()),
1162
+ });
1163
+
1164
+ const AccessEffectiveIn = z.object({
1165
+ kind: z.enum(["operator", "user", "role", "key"]),
1166
+ id: z.string().min(1),
1167
+ });
1168
+
1169
+ const AccessEffectiveOut = z.object({
1170
+ kind: z.enum(["operator", "user", "role", "key"]),
1171
+ id: z.string(),
1172
+ plane: z.enum(["user", "operator"]),
1173
+ scopes: z.array(
1174
+ z.object({
1175
+ scope: z.string(),
1176
+ sources: z.array(
1177
+ z.object({
1178
+ kind: z.enum(["role", "direct"]),
1179
+ id: z.string(),
1180
+ name: z.string(),
1181
+ }),
1182
+ ),
1183
+ }),
1184
+ ),
1185
+ });
1186
+
1187
+ const AccessKeyBlastIn = z.object({
1188
+ keyId: z.string().min(1),
1189
+ });
1190
+
1191
+ const AccessKeyBlastOut = z.object({
1192
+ callVolume: z.number(),
1193
+ lastUsedAt: z.number().nullable(),
1194
+ sourceAddresses: z.array(z.string()),
1195
+ accessTtlMs: z.number(),
1196
+ residualAccessNote: z.string(),
1197
+ });
1198
+
1199
+ const AccessCreateKeyIn = z.object({
1200
+ plane: z.enum(["user", "operator"]),
1201
+ name: z.string().min(1),
1202
+ scopes: z.array(z.string()),
1203
+ expiresAt: z.number().nullable().optional(),
1204
+ rateLimit: z
1205
+ .object({ max: z.number(), per: z.string() })
1206
+ .nullable()
1207
+ .optional(),
1208
+ ipAllowlist: z.array(z.string()).optional(),
1209
+ });
1210
+
1211
+ const AccessCreateKeyOut = z.object({
1212
+ key: z.object({
1213
+ id: z.string(),
1214
+ name: z.string(),
1215
+ plane: z.enum(["user", "operator"]),
1216
+ scopes: z.array(z.string()),
1217
+ createdAt: z.number(),
1218
+ lastUsedAt: z.number().nullable(),
1219
+ expiresAt: z.number().nullable(),
1220
+ revokedAt: z.number().nullable(),
1221
+ rateLimit: z.object({ max: z.number(), per: z.string() }).nullable(),
1222
+ ipAllowlist: z.array(z.string()),
1223
+ unused90d: z.boolean(),
1224
+ }),
1225
+ /** Raw secret — returned exactly once. */
1226
+ secret: z.string(),
1227
+ });
1228
+
1229
+ const AccessRevokeKeyIn = z.object({
1230
+ keyId: z.string().min(1),
1231
+ confirmation: z.string().optional(),
1232
+ reason: z.string().optional(),
1233
+ });
1234
+
1235
+ const AccessRevokeKeyOut = z.object({
1236
+ key: z.object({
1237
+ id: z.string(),
1238
+ name: z.string(),
1239
+ plane: z.enum(["user", "operator"]),
1240
+ scopes: z.array(z.string()),
1241
+ createdAt: z.number(),
1242
+ lastUsedAt: z.number().nullable(),
1243
+ expiresAt: z.number().nullable(),
1244
+ revokedAt: z.number().nullable(),
1245
+ rateLimit: z.object({ max: z.number(), per: z.string() }).nullable(),
1246
+ ipAllowlist: z.array(z.string()),
1247
+ unused90d: z.boolean(),
1248
+ }),
1249
+ blastRadius: AccessKeyBlastOut,
1250
+ });
1251
+
1252
+ const AccessRotateKeyIn = z.object({
1253
+ keyId: z.string().min(1),
1254
+ confirmation: z.string().optional(),
1255
+ reason: z.string().optional(),
1256
+ });
1257
+
1258
+ const AccessRotateKeyOut = z.object({
1259
+ key: AccessCreateKeyOut.shape.key,
1260
+ secret: z.string(),
1261
+ blastRadius: AccessKeyBlastOut,
1262
+ });
1263
+
1264
+ const AccessSetRoleGrantsIn = z.object({
1265
+ roleId: z.string().min(1),
1266
+ scopes: z.array(z.string()),
1267
+ });
1268
+
1269
+ const AccessSetRoleGrantsOut = z.object({
1270
+ roleId: z.string(),
1271
+ scopes: z.array(z.string()),
1272
+ });
1273
+
1274
+ const AccessGrantDenied = z.object({ reason: z.string() });
1275
+ const AccessKeyNotFound = z.object({ keyId: z.string() });
1276
+
1277
+ const SignalNotFound = z.object({ signal: z.string() });
1278
+ const DryRunUnsafe = z.object({
1279
+ signal: z.string().optional(),
1280
+ ref: z.string().optional(),
1281
+ reason: z.string(),
1282
+ });
1283
+
1284
+ const TenantRequired = z.object({
1285
+ reason: z.string(),
1286
+ });
1287
+
1288
+ const StoreNotFound = z.object({ ref: z.string() });
1289
+
1290
+ const WillNotFireOut = z.object({
1291
+ writerFlowIds: z.array(z.string()),
1292
+ signals: z.array(z.string()),
1293
+ channels: z.array(z.string()),
1294
+ });
1295
+
1296
+ const StoreListOut = z.object({
1297
+ tenancyDeclared: z.boolean(),
1298
+ tenants: z.array(z.string()),
1299
+ stores: z.array(
1300
+ z.object({
1301
+ ref: z.string(),
1302
+ facet: z.enum(["sql", "kv", "files", "index"]),
1303
+ name: z.string(),
1304
+ children: z.array(
1305
+ z.object({
1306
+ name: z.string(),
1307
+ effectRef: z.string(),
1308
+ writers: z.array(z.string()),
1309
+ readers: z.array(z.string()),
1310
+ cache: z.object({
1311
+ producedByRead: z.string(),
1312
+ invalidatedByWrites: z.array(z.string()),
1313
+ invalidatingFlowIds: z.array(z.string()),
1314
+ }),
1315
+ willNotFire: WillNotFireOut,
1316
+ piiColumns: z.array(z.string()),
1317
+ }),
1318
+ ),
1319
+ replicaLagMs: z.number().nullable(),
1320
+ migrationDrift: z
1321
+ .object({
1322
+ declared: z.string(),
1323
+ applied: z.string().nullable(),
1324
+ drifted: z.boolean(),
1325
+ })
1326
+ .nullable(),
1327
+ contentAddressed: z.boolean(),
1328
+ warnings: z.array(
1329
+ z.object({
1330
+ code: z.string(),
1331
+ message: z.string(),
1332
+ key: z.string(),
1333
+ }),
1334
+ ),
1335
+ }),
1336
+ ),
1337
+ });
1338
+
1339
+ const StoreQueryIn = z.object({
1340
+ ref: z.string().min(1),
1341
+ child: z.string().optional(),
1342
+ tenant: z.string().optional(),
1343
+ prefix: z.string().optional(),
1344
+ limit: z.number().min(1).max(500).optional(),
1345
+ vector: z.array(z.number()).optional(),
1346
+ topK: z.number().min(1).max(100).optional(),
1347
+ });
1348
+
1349
+ const StoreQueryOut = z.object({
1350
+ facet: z.enum(["sql", "kv", "files", "index"]),
1351
+ rows: z.array(z.record(z.string(), z.unknown())).optional(),
1352
+ keys: z
1353
+ .array(
1354
+ z.object({
1355
+ key: z.string(),
1356
+ value: z.unknown().optional(),
1357
+ warnings: z
1358
+ .array(z.object({ code: z.string(), message: z.string() }))
1359
+ .optional(),
1360
+ }),
1361
+ )
1362
+ .optional(),
1363
+ hits: z
1364
+ .array(
1365
+ z.object({
1366
+ id: z.string(),
1367
+ score: z.number(),
1368
+ meta: z.record(z.string(), z.unknown()).optional(),
1369
+ }),
1370
+ )
1371
+ .optional(),
1372
+ masked: z.boolean(),
1373
+ routedRole: z.enum(["primary", "replica"]).optional(),
1374
+ });
1375
+
1376
+ const StoreRevealIn = z.object({
1377
+ ref: z.string().min(1),
1378
+ child: z.string().optional(),
1379
+ tenant: z.string().optional(),
1380
+ id: z.string().min(1),
1381
+ column: z.string().min(1),
1382
+ });
1383
+
1384
+ const StoreRevealOut = z.object({
1385
+ ok: z.literal(true),
1386
+ value: z.unknown(),
1387
+ at: z.number(),
1388
+ });
1389
+
1390
+ const StoreEditIn = z.object({
1391
+ ref: z.string().min(1),
1392
+ child: z.string().optional(),
1393
+ tenant: z.string().optional(),
1394
+ id: z.string().optional(),
1395
+ key: z.string().optional(),
1396
+ patch: z.record(z.string(), z.unknown()),
1397
+ confirmation: z.string().optional(),
1398
+ reason: z.string().optional(),
1399
+ /** When false/omitted, returns willNotFire without applying. */
1400
+ commit: z.boolean().optional(),
1401
+ });
1402
+
1403
+ const StoreEditOut = z.object({
1404
+ ok: z.literal(true),
1405
+ dryRun: z.boolean(),
1406
+ applied: z.boolean(),
1407
+ willNotFire: WillNotFireOut,
1408
+ wouldHaveFired: z.array(
1409
+ z.object({
1410
+ kind: z.enum(["send", "ask"]),
1411
+ resource: z.string(),
1412
+ }),
1413
+ ),
1414
+ at: z.number(),
1415
+ });
1416
+
1417
+ const StoreDeleteIn = z.object({
1418
+ ref: z.string().min(1),
1419
+ child: z.string().optional(),
1420
+ tenant: z.string().optional(),
1421
+ ids: z.array(z.string()).optional(),
1422
+ keys: z.array(z.string()).optional(),
1423
+ confirmation: z.string().optional(),
1424
+ reason: z.string().optional(),
1425
+ });
1426
+
1427
+ const StoreDeleteOut = z.object({
1428
+ ok: z.literal(true),
1429
+ deleted: z.number(),
1430
+ at: z.number(),
1431
+ });
1432
+
1433
+ const StorePurgeIn = z.object({
1434
+ resource: z.string().min(1),
1435
+ confirmation: z.string().optional(),
1436
+ reason: z.string().optional(),
1437
+ });
1438
+
1439
+ const StorePurgeOut = z.object({
1440
+ ok: z.literal(true),
1441
+ keys: z.array(z.string()),
1442
+ at: z.number(),
1443
+ });
1444
+
1445
+ const StoreSqlIn = z.object({
1446
+ ref: z.string().min(1),
1447
+ sql: z.string().min(1),
1448
+ tenant: z.string().optional(),
1449
+ allowWrite: z.boolean().optional(),
1450
+ });
1451
+
1452
+ const StoreSqlOut = z.object({
1453
+ rows: z.array(z.record(z.string(), z.unknown())),
1454
+ masked: z.boolean(),
1455
+ routedRole: z.enum(["primary", "replica"]),
1456
+ });
1457
+
1458
+ const StorePreviewIn = z.object({
1459
+ ref: z.string().min(1),
1460
+ child: z.string().optional(),
1461
+ tenant: z.string().optional(),
1462
+ id: z.string().optional(),
1463
+ key: z.string().optional(),
1464
+ patch: z.record(z.string(), z.unknown()),
1465
+ });
1466
+
1467
+ const StorePreviewOut = z.object({
1468
+ ok: z.literal(true),
1469
+ dryRun: z.literal(true),
1470
+ willNotFire: WillNotFireOut,
1471
+ wouldHaveFired: z.array(
1472
+ z.object({
1473
+ kind: z.enum(["send", "ask"]),
1474
+ resource: z.string(),
1475
+ }),
1476
+ ),
1477
+ at: z.number(),
1478
+ });
1479
+
1480
+ /**
1481
+ * Build all Console HTTP bindings against shared state.
1482
+ *
1483
+ * @param state - Console state
1484
+ */
1485
+ export function createConsoleBindings(state: ConsoleState): {
1486
+ readonly bindings: Binding[];
1487
+ readonly routes: {
1488
+ readonly setup: {
1489
+ readonly status: AnyFlowDef;
1490
+ readonly claim: AnyFlowDef;
1491
+ };
1492
+ readonly session: {
1493
+ readonly login: AnyFlowDef;
1494
+ readonly me: AnyFlowDef;
1495
+ readonly logout: AnyFlowDef;
1496
+ };
1497
+ readonly manifest: { readonly get: AnyFlowDef };
1498
+ readonly runs: { readonly list: AnyFlowDef };
1499
+ readonly action: { readonly ping: AnyFlowDef };
1500
+ readonly structural: {
1501
+ readonly propose: AnyFlowDef;
1502
+ };
1503
+ readonly flows: {
1504
+ readonly identities: AnyFlowDef;
1505
+ readonly invoke: AnyFlowDef;
1506
+ };
1507
+ readonly traces: {
1508
+ readonly replay: AnyFlowDef;
1509
+ };
1510
+ readonly signals: {
1511
+ readonly list: AnyFlowDef;
1512
+ readonly replay: AnyFlowDef;
1513
+ readonly dryRunReplay: AnyFlowDef;
1514
+ readonly discard: AnyFlowDef;
1515
+ };
1516
+ readonly store: {
1517
+ readonly list: AnyFlowDef;
1518
+ readonly query: AnyFlowDef;
1519
+ readonly reveal: AnyFlowDef;
1520
+ readonly edit: AnyFlowDef;
1521
+ readonly delete: AnyFlowDef;
1522
+ readonly purgeCache: AnyFlowDef;
1523
+ readonly sql: AnyFlowDef;
1524
+ readonly preview: AnyFlowDef;
1525
+ };
1526
+ readonly vault: {
1527
+ readonly list: AnyFlowDef;
1528
+ readonly set: AnyFlowDef;
1529
+ readonly rotate: AnyFlowDef;
1530
+ };
1531
+ readonly ai: {
1532
+ readonly list: AnyFlowDef;
1533
+ };
1534
+ readonly gates: {
1535
+ readonly list: AnyFlowDef;
1536
+ readonly simulate: AnyFlowDef;
1537
+ readonly powers: AnyFlowDef;
1538
+ };
1539
+ readonly access: {
1540
+ readonly list: AnyFlowDef;
1541
+ readonly effective: AnyFlowDef;
1542
+ readonly keyBlast: AnyFlowDef;
1543
+ readonly createKey: AnyFlowDef;
1544
+ readonly revokeKey: AnyFlowDef;
1545
+ readonly rotateKey: AnyFlowDef;
1546
+ readonly setRoleGrants: AnyFlowDef;
1547
+ };
1548
+ readonly diff: {
1549
+ readonly list: AnyFlowDef;
1550
+ };
1551
+ readonly plugin: {
1552
+ readonly list: AnyFlowDef;
1553
+ };
1554
+ readonly clock: {
1555
+ readonly list: AnyFlowDef;
1556
+ readonly runNow: AnyFlowDef;
1557
+ readonly pause: AnyFlowDef;
1558
+ readonly editSchedule: AnyFlowDef;
1559
+ readonly wakeEarly: AnyFlowDef;
1560
+ };
1561
+ readonly channel: {
1562
+ readonly list: AnyFlowDef;
1563
+ readonly preview: AnyFlowDef;
1564
+ readonly verifyAuth: AnyFlowDef;
1565
+ readonly reveal: AnyFlowDef;
1566
+ readonly sendTest: AnyFlowDef;
1567
+ };
1568
+ };
1569
+ } {
1570
+ const setupStatus = createSetupStatus(state);
1571
+ const setupClaim = createSetupClaim(state);
1572
+ const sessionLogin = createSessionLogin(state);
1573
+ const sessionMe = createSessionMe(state);
1574
+ const sessionLogout = createSessionLogout();
1575
+ const manifestGet = createManifestGet(state);
1576
+ const runsList = createRunsList(state);
1577
+ const actionPing = createActionPing(state);
1578
+ const structuralPropose = createStructuralPropose(state);
1579
+ const flowsIdentities = createFlowsIdentities(state);
1580
+ const flowsInvoke = createFlowsInvoke(state);
1581
+ const tracesReplay = createTracesReplay(state);
1582
+ const signalsList = createSignalsList(state);
1583
+ const signalsReplay = createSignalsReplay(state);
1584
+ const signalsDryRunReplay = createSignalsDryRunReplay(state);
1585
+ const signalsDiscard = createSignalsDiscard(state);
1586
+ const storeList = createStoreList(state);
1587
+ const storeQuery = createStoreQuery(state);
1588
+ const storeReveal = createStoreReveal(state);
1589
+ const storeEdit = createStoreEdit(state);
1590
+ const storeDelete = createStoreDelete(state);
1591
+ const storePurgeCache = createStorePurgeCache(state);
1592
+ const storeSql = createStoreSql(state);
1593
+ const storePreview = createStorePreview(state);
1594
+ const vaultList = createVaultList(state);
1595
+ const vaultSet = createVaultSet(state);
1596
+ const vaultRotate = createVaultRotate(state);
1597
+ const aiList = createAiList(state);
1598
+ const gatesList = createGatesList(state);
1599
+ const gatesSimulate = createGatesSimulate(state);
1600
+ const gatesPowers = createGatesPowers(state);
1601
+ const accessList = createAccessList(state);
1602
+ const accessEffective = createAccessEffective(state);
1603
+ const accessKeyBlast = createAccessKeyBlast(state);
1604
+ const accessCreateKeyFlow = createAccessCreateKey(state);
1605
+ const accessRevokeKeyFlow = createAccessRevokeKey(state);
1606
+ const accessRotateKeyFlow = createAccessRotateKey(state);
1607
+ const accessSetRoleGrantsFlow = createAccessSetRoleGrants(state);
1608
+ const diffList = createDiffList(state);
1609
+ const pluginsList = createPluginsList(state);
1610
+ const clockList = createClockList(state);
1611
+ const clockRunNow = createClockRunNow(state);
1612
+ const clockPause = createClockPause(state);
1613
+ const clockEditSchedule = createClockEditSchedule(state);
1614
+ const clockWakeEarly = createClockWakeEarly(state);
1615
+ const channelsList = createChannelsList(state);
1616
+ const channelPreview = createChannelPreview(state);
1617
+ const channelVerifyAuth = createChannelVerifyAuth(state);
1618
+ const channelReveal = createChannelReveal(state);
1619
+ const channelSendTest = createChannelSendTest(state);
1620
+
1621
+ const bindings: Binding[] = [
1622
+ bindHttp(http.get("/console/setup/status"), setupStatus),
1623
+ bindHttp(http.post("/console/setup/claim"), setupClaim),
1624
+ bindHttp(http.post("/console/session/login"), sessionLogin),
1625
+ bindHttp(http.get("/console/session/me"), sessionMe),
1626
+ bindHttp(http.post("/console/session/logout"), sessionLogout),
1627
+ bindHttp(http.get("/console/manifest"), manifestGet),
1628
+ bindHttp(http.get("/console/runs"), runsList),
1629
+ bindHttp(http.post("/console/action/ping"), actionPing),
1630
+ bindHttp(http.post("/console/structural/propose"), structuralPropose),
1631
+ bindHttp(http.get("/console/flows/identities"), flowsIdentities),
1632
+ bindHttp(http.post("/console/flows/invoke"), flowsInvoke),
1633
+ bindHttp(http.post("/console/traces/replay"), tracesReplay),
1634
+ bindHttp(http.get("/console/signals"), signalsList),
1635
+ bindHttp(http.post("/console/signals/replay"), signalsReplay),
1636
+ bindHttp(http.post("/console/signals/dry-run-replay"), signalsDryRunReplay),
1637
+ bindHttp(http.post("/console/signals/discard"), signalsDiscard),
1638
+ bindHttp(http.get("/console/store"), storeList),
1639
+ bindHttp(http.post("/console/store/query"), storeQuery),
1640
+ bindHttp(http.post("/console/store/reveal"), storeReveal),
1641
+ bindHttp(http.post("/console/store/edit"), storeEdit),
1642
+ bindHttp(http.post("/console/store/delete"), storeDelete),
1643
+ bindHttp(http.post("/console/store/purge-cache"), storePurgeCache),
1644
+ bindHttp(http.post("/console/store/sql"), storeSql),
1645
+ bindHttp(http.post("/console/store/preview"), storePreview),
1646
+ bindHttp(http.get("/console/vault"), vaultList),
1647
+ bindHttp(http.post("/console/vault/set"), vaultSet),
1648
+ bindHttp(http.post("/console/vault/rotate"), vaultRotate),
1649
+ bindHttp(http.get("/console/ai"), aiList),
1650
+ bindHttp(http.get("/console/gates"), gatesList),
1651
+ bindHttp(http.post("/console/gates/simulate"), gatesSimulate),
1652
+ bindHttp(http.post("/console/gates/powers"), gatesPowers),
1653
+ bindHttp(http.get("/console/access"), accessList),
1654
+ bindHttp(http.post("/console/access/effective"), accessEffective),
1655
+ bindHttp(http.post("/console/access/key-blast"), accessKeyBlast),
1656
+ bindHttp(http.post("/console/access/keys"), accessCreateKeyFlow),
1657
+ bindHttp(http.post("/console/access/keys/revoke"), accessRevokeKeyFlow),
1658
+ bindHttp(http.post("/console/access/keys/rotate"), accessRotateKeyFlow),
1659
+ bindHttp(
1660
+ http.post("/console/access/roles/grants"),
1661
+ accessSetRoleGrantsFlow,
1662
+ ),
1663
+ bindHttp(http.get("/console/diff"), diffList),
1664
+ bindHttp(http.get("/console/plugins"), pluginsList),
1665
+ bindHttp(http.get("/console/clock"), clockList),
1666
+ bindHttp(http.post("/console/clock/run-now"), clockRunNow),
1667
+ bindHttp(http.post("/console/clock/pause"), clockPause),
1668
+ bindHttp(http.post("/console/clock/edit-schedule"), clockEditSchedule),
1669
+ bindHttp(http.post("/console/clock/wake-early"), clockWakeEarly),
1670
+ bindHttp(http.get("/console/channels"), channelsList),
1671
+ bindHttp(http.post("/console/channels/preview"), channelPreview),
1672
+ bindHttp(http.post("/console/channels/verify-auth"), channelVerifyAuth),
1673
+ bindHttp(http.post("/console/channels/reveal"), channelReveal),
1674
+ bindHttp(http.post("/console/channels/send-test"), channelSendTest),
1675
+ ];
1676
+
1677
+ return {
1678
+ bindings,
1679
+ routes: {
1680
+ setup: { status: setupStatus, claim: setupClaim },
1681
+ session: { login: sessionLogin, me: sessionMe, logout: sessionLogout },
1682
+ manifest: { get: manifestGet },
1683
+ runs: { list: runsList },
1684
+ action: { ping: actionPing },
1685
+ structural: { propose: structuralPropose },
1686
+ flows: { identities: flowsIdentities, invoke: flowsInvoke },
1687
+ traces: { replay: tracesReplay },
1688
+ signals: {
1689
+ list: signalsList,
1690
+ replay: signalsReplay,
1691
+ dryRunReplay: signalsDryRunReplay,
1692
+ discard: signalsDiscard,
1693
+ },
1694
+ store: {
1695
+ list: storeList,
1696
+ query: storeQuery,
1697
+ reveal: storeReveal,
1698
+ edit: storeEdit,
1699
+ delete: storeDelete,
1700
+ purgeCache: storePurgeCache,
1701
+ sql: storeSql,
1702
+ preview: storePreview,
1703
+ },
1704
+ vault: {
1705
+ list: vaultList,
1706
+ set: vaultSet,
1707
+ rotate: vaultRotate,
1708
+ },
1709
+ ai: { list: aiList },
1710
+ gates: {
1711
+ list: gatesList,
1712
+ simulate: gatesSimulate,
1713
+ powers: gatesPowers,
1714
+ },
1715
+ access: {
1716
+ list: accessList,
1717
+ effective: accessEffective,
1718
+ keyBlast: accessKeyBlast,
1719
+ createKey: accessCreateKeyFlow,
1720
+ revokeKey: accessRevokeKeyFlow,
1721
+ rotateKey: accessRotateKeyFlow,
1722
+ setRoleGrants: accessSetRoleGrantsFlow,
1723
+ },
1724
+ diff: { list: diffList },
1725
+ plugin: { list: pluginsList },
1726
+ clock: {
1727
+ list: clockList,
1728
+ runNow: clockRunNow,
1729
+ pause: clockPause,
1730
+ editSchedule: clockEditSchedule,
1731
+ wakeEarly: clockWakeEarly,
1732
+ },
1733
+ channel: {
1734
+ list: channelsList,
1735
+ preview: channelPreview,
1736
+ verifyAuth: channelVerifyAuth,
1737
+ reveal: channelReveal,
1738
+ sendTest: channelSendTest,
1739
+ },
1740
+ },
1741
+ };
1742
+ }
1743
+
1744
+ function createSetupStatus(state: ConsoleState) {
1745
+ return flow({
1746
+ name: "console.setup.status",
1747
+ unit: "console",
1748
+ plane: "operator",
1749
+ out: SetupStatusOut,
1750
+ do: () => ({
1751
+ setupClosed: state.setupClosed,
1752
+ claimRequired: !state.setupClosed,
1753
+ }),
1754
+ });
1755
+ }
1756
+
1757
+ function createSetupClaim(state: ConsoleState) {
1758
+ return flow({
1759
+ name: "console.setup.claim",
1760
+ unit: "console",
1761
+ plane: "operator",
1762
+ in: ClaimIn,
1763
+ out: SessionOut,
1764
+ errors: { SetupClosed, ClaimFailed },
1765
+ do: async (input: z.infer<typeof ClaimIn>, fx) => {
1766
+ if (state.setupClosed) {
1767
+ return fail("SetupClosed", { reason: "first operator already exists" });
1768
+ }
1769
+ const verified = verifyClaimCode(state.claim, input.claimCode, state.now);
1770
+ if (!verified.ok) {
1771
+ return fail("ClaimFailed", { reason: verified.reason });
1772
+ }
1773
+ const op = await createOperator(state.operators, {
1774
+ email: input.email,
1775
+ name: input.name,
1776
+ password: input.password,
1777
+ });
1778
+ const issued = await issueOperatorSession(state, op.id);
1779
+ fx.log.info("console.setup.claim", { operatorId: op.id });
1780
+ return sessionPayload(op.id, op.email, op.name, issued);
1781
+ },
1782
+ });
1783
+ }
1784
+
1785
+ function createSessionLogin(state: ConsoleState) {
1786
+ return flow({
1787
+ name: "console.session.login",
1788
+ unit: "console",
1789
+ plane: "operator",
1790
+ in: LoginIn,
1791
+ out: SessionOut,
1792
+ errors: { AuthFailed, AuthRateLimited },
1793
+ do: async (input: z.infer<typeof LoginIn>, fx) => {
1794
+ if (
1795
+ touchLoginRateLimit(state.loginAttempts, input.email, state.now()) ===
1796
+ "rate_limited"
1797
+ ) {
1798
+ return fail("AuthRateLimited", {
1799
+ reason: "too many login attempts; retry after 60s",
1800
+ });
1801
+ }
1802
+ const op = await authenticateOperator(
1803
+ state.operators,
1804
+ input.email,
1805
+ input.password,
1806
+ );
1807
+ if (!op) return fail("AuthFailed", {});
1808
+ const issued = await issueOperatorSession(state, op.id);
1809
+ fx.log.info("console.session.login", { operatorId: op.id });
1810
+ return sessionPayload(op.id, op.email, op.name, issued);
1811
+ },
1812
+ });
1813
+ }
1814
+
1815
+ function createSessionMe(state: ConsoleState) {
1816
+ return flow({
1817
+ name: "console.session.me",
1818
+ unit: "console",
1819
+ plane: "operator",
1820
+ out: MeOut,
1821
+ errors: { AuthFailed },
1822
+ do: (_input, fx) => {
1823
+ const id = fx.operator.id;
1824
+ if (!id) return fail("AuthFailed", {});
1825
+ const op = state.operators.operators.get(id);
1826
+ if (!op) return fail("AuthFailed", {});
1827
+ return {
1828
+ operatorId: op.id,
1829
+ email: op.email,
1830
+ name: op.name,
1831
+ setupClosed: state.setupClosed,
1832
+ };
1833
+ },
1834
+ });
1835
+ }
1836
+
1837
+ function createSessionLogout() {
1838
+ return flow({
1839
+ name: "console.session.logout",
1840
+ unit: "console",
1841
+ plane: "operator",
1842
+ out: z.object({ ok: z.literal(true) }),
1843
+ do: (_input, fx) => {
1844
+ fx.log.info("console.session.logout", {
1845
+ operatorId: fx.operator.id,
1846
+ });
1847
+ return { ok: true as const };
1848
+ },
1849
+ });
1850
+ }
1851
+
1852
+ function createManifestGet(state: ConsoleState) {
1853
+ return flow({
1854
+ name: "console.manifest.get",
1855
+ unit: "console",
1856
+ plane: "operator",
1857
+ out: ManifestOut,
1858
+ errors: { AuthFailed },
1859
+ do: (_input, fx) => {
1860
+ if (!fx.operator.id) return fail("AuthFailed", {});
1861
+ return { manifest: state.manifest };
1862
+ },
1863
+ });
1864
+ }
1865
+
1866
+ function createRunsList(state: ConsoleState) {
1867
+ return flow({
1868
+ name: "console.runs.list",
1869
+ unit: "console",
1870
+ plane: "operator",
1871
+ out: RunsListOut,
1872
+ errors: { AuthFailed },
1873
+ do: async (_input, fx) => {
1874
+ if (!fx.operator.id) return fail("AuthFailed", {});
1875
+ const all = await state.listRuns();
1876
+ const piiFields = piiFieldNamesFromManifest(state.manifest);
1877
+ return {
1878
+ runs: all.map((r) => projectRun(r, piiFields)),
1879
+ };
1880
+ },
1881
+ });
1882
+ }
1883
+
1884
+ /**
1885
+ * Project a wide event for GET /console/runs (Runs · Traces · Overview).
1886
+ *
1887
+ * PII-classified fields are masked centrally here so every panel that reads
1888
+ * the shared wide-event store sees the same redaction as Store's row browser.
1889
+ *
1890
+ * @param r - Stored wide event
1891
+ * @param piiFields - Classified field names from the Manifest
1892
+ */
1893
+ export function projectRun(
1894
+ r: WideEvent,
1895
+ piiFields: ReadonlySet<string> = new Set(),
1896
+ ) {
1897
+ const masked = maskWideEventForConsole(r, piiFields);
1898
+ const dimensions: Record<string, string | number | boolean | null> = {};
1899
+ for (const [k, v] of Object.entries(masked.dimensions)) {
1900
+ if (v === undefined) continue;
1901
+ dimensions[k] = v;
1902
+ }
1903
+ return {
1904
+ id: r.id,
1905
+ parentId: r.parentId ?? null,
1906
+ flow: r.flow,
1907
+ unit: r.unit ?? null,
1908
+ trigger: r.trigger,
1909
+ plane: r.plane,
1910
+ tenant: r.tenant ?? null,
1911
+ principal: r.principal ?? null,
1912
+ gates: [...r.gates],
1913
+ cache: r.cache,
1914
+ replica: r.replica ?? null,
1915
+ replicaLagMs: r.replicaLagMs ?? null,
1916
+ cost: r.cost ?? null,
1917
+ promptVersion: r.promptVersion ?? null,
1918
+ buildVersion: r.buildVersion ?? null,
1919
+ startedAt: r.startedAt,
1920
+ endedAt: r.endedAt,
1921
+ durationMs: r.durationMs,
1922
+ error: masked.error?.code ?? null,
1923
+ sampled: masked.error ? ("error" as const) : ("sample" as const),
1924
+ effects: masked.effects.map((e) => ({
1925
+ kind: e.kind,
1926
+ resource: e.resource,
1927
+ timestamp: e.timestamp,
1928
+ duration: e.duration,
1929
+ reversibility: e.reversibility,
1930
+ })),
1931
+ logs: masked.logs.map((line) => ({
1932
+ level: line.level,
1933
+ message: line.message,
1934
+ ...(line.data !== undefined ? { data: line.data } : {}),
1935
+ at: line.at,
1936
+ })),
1937
+ dimensions,
1938
+ };
1939
+ }
1940
+
1941
+ function createTracesReplay(state: ConsoleState) {
1942
+ return flow({
1943
+ name: "console.traces.replay",
1944
+ unit: "console",
1945
+ plane: "operator",
1946
+ in: TracesReplayIn,
1947
+ out: TracesReplayOut,
1948
+ errors: { AuthFailed, NotFound },
1949
+ do: async (input: z.infer<typeof TracesReplayIn>, fx) => {
1950
+ if (!fx.operator.id) return fail("AuthFailed", {});
1951
+ const all = await state.listRuns();
1952
+ const root = all.find((r) => r.id === input.rootId);
1953
+ if (!root) return fail("NotFound", { flowId: input.rootId });
1954
+ fx.log.info("console.traces.replay", {
1955
+ operatorId: fx.operator.id,
1956
+ rootId: input.rootId,
1957
+ dryRun: input.dryRun,
1958
+ flow: root.flow,
1959
+ });
1960
+ return {
1961
+ ok: true as const,
1962
+ rootId: input.rootId,
1963
+ dryRun: input.dryRun,
1964
+ at: Date.now(),
1965
+ };
1966
+ },
1967
+ });
1968
+ }
1969
+
1970
+ function createSignalsList(state: ConsoleState) {
1971
+ return flow({
1972
+ name: "console.signals.list",
1973
+ unit: "console",
1974
+ plane: "operator",
1975
+ out: SignalsListOut,
1976
+ errors: { AuthFailed },
1977
+ do: async (_input, fx) => {
1978
+ if (!fx.operator.id) return fail("AuthFailed", {});
1979
+ const signals = await state.listSignals();
1980
+ return { signals: [...signals] };
1981
+ },
1982
+ });
1983
+ }
1984
+
1985
+ function createSignalsReplay(state: ConsoleState) {
1986
+ return flow({
1987
+ name: "console.signals.replay",
1988
+ unit: "console",
1989
+ plane: "operator",
1990
+ in: SignalsReplayIn,
1991
+ out: SignalsReplayOut,
1992
+ errors: { AuthFailed, SignalNotFound, ConfirmRequired },
1993
+ do: async (input: z.infer<typeof SignalsReplayIn>, fx) => {
1994
+ if (!fx.operator.id) return fail("AuthFailed", {});
1995
+ return runSignalReplay(state, fx, { ...input, dryRun: false });
1996
+ },
1997
+ });
1998
+ }
1999
+
2000
+ function createSignalsDryRunReplay(state: ConsoleState) {
2001
+ return flow({
2002
+ name: "console.signals.dryRunReplay",
2003
+ unit: "console",
2004
+ plane: "operator",
2005
+ in: SignalsReplayIn.omit({ dryRun: true }),
2006
+ out: SignalsReplayOut,
2007
+ errors: { AuthFailed, SignalNotFound, DryRunUnsafe },
2008
+ do: async (
2009
+ input: Omit<z.infer<typeof SignalsReplayIn>, "dryRun">,
2010
+ fx,
2011
+ ) => {
2012
+ if (!fx.operator.id) return fail("AuthFailed", {});
2013
+ return runSignalReplay(state, fx, { ...input, dryRun: true });
2014
+ },
2015
+ });
2016
+ }
2017
+
2018
+ function createSignalsDiscard(state: ConsoleState) {
2019
+ return flow({
2020
+ name: "console.signals.discard",
2021
+ unit: "console",
2022
+ plane: "operator",
2023
+ in: SignalsDiscardIn,
2024
+ out: SignalsDiscardOut,
2025
+ errors: { AuthFailed, SignalNotFound, ConfirmRequired },
2026
+ do: async (input: z.infer<typeof SignalsDiscardIn>, fx) => {
2027
+ if (!fx.operator.id) return fail("AuthFailed", {});
2028
+ const rows = await state.listSignals();
2029
+ const row = rows.find((s) => s.name === input.signal);
2030
+ if (!row) return fail("SignalNotFound", { signal: input.signal });
2031
+
2032
+ const irreversible =
2033
+ state.production &&
2034
+ row.consumers.some((c) => c.external || !c.durable);
2035
+ if (irreversible) {
2036
+ if (
2037
+ input.confirmation !== "DISCARD" ||
2038
+ (input.reason?.trim().length ?? 0) < 3
2039
+ ) {
2040
+ return fail("ConfirmRequired", {
2041
+ phrase: "DISCARD" as const,
2042
+ reason: "discarding dead letters requires typed confirmation",
2043
+ });
2044
+ }
2045
+ }
2046
+
2047
+ const result = await state.discardSignals({
2048
+ signal: input.signal,
2049
+ messageIds: input.messageIds,
2050
+ });
2051
+ fx.log.info("console.signals.discard", {
2052
+ operatorId: fx.operator.id,
2053
+ signal: input.signal,
2054
+ discarded: result.discarded,
2055
+ reason: input.reason,
2056
+ });
2057
+ return {
2058
+ ok: true as const,
2059
+ discarded: result.discarded,
2060
+ at: state.now(),
2061
+ };
2062
+ },
2063
+ });
2064
+ }
2065
+
2066
+ async function runSignalReplay(
2067
+ state: ConsoleState,
2068
+ fx: {
2069
+ operator: { id: string | null };
2070
+ log: { info: (m: string, data?: Record<string, unknown>) => void };
2071
+ },
2072
+ input: z.infer<typeof SignalsReplayIn>,
2073
+ ) {
2074
+ const rows = await state.listSignals();
2075
+ const row = rows.find((s) => s.name === input.signal);
2076
+ if (!row) return fail("SignalNotFound", { signal: input.signal });
2077
+
2078
+ if (input.dryRun) {
2079
+ // Same refusal spirit as Traces: if we cannot offer a safe stubbed
2080
+ // dry run for this consumer shape, refuse rather than risk a side effect.
2081
+ const safety = dryRunSafety(row);
2082
+ if (!safety.ok) {
2083
+ return fail("DryRunUnsafe", {
2084
+ signal: input.signal,
2085
+ reason: safety.reason,
2086
+ });
2087
+ }
2088
+ } else {
2089
+ const retriggersExternal =
2090
+ state.production &&
2091
+ row.consumers.some((c) => c.external) &&
2092
+ row.consumersDurable !== true;
2093
+ if (retriggersExternal) {
2094
+ if (
2095
+ input.confirmation !== "REPLAY" ||
2096
+ (input.reason?.trim().length ?? 0) < 3
2097
+ ) {
2098
+ return fail("ConfirmRequired", {
2099
+ phrase: "REPLAY" as const,
2100
+ reason:
2101
+ "replay re-triggers an external effect; typed confirmation required",
2102
+ });
2103
+ }
2104
+ }
2105
+ }
2106
+
2107
+ const result = await state.replaySignals({
2108
+ signal: input.signal,
2109
+ messageIds: input.messageIds,
2110
+ subscriberId: input.subscriberId,
2111
+ ratePerSec: input.ratePerSec,
2112
+ dryRun: input.dryRun,
2113
+ payloads: input.payloads,
2114
+ });
2115
+ if (result.refused) {
2116
+ return fail("DryRunUnsafe", {
2117
+ signal: input.signal,
2118
+ reason: result.refused.reason,
2119
+ });
2120
+ }
2121
+ fx.log.info(
2122
+ input.dryRun
2123
+ ? "console.signals.dryRunReplay"
2124
+ : "console.signals.replay",
2125
+ {
2126
+ operatorId: fx.operator.id,
2127
+ signal: input.signal,
2128
+ attempted: result.attempted,
2129
+ succeeded: result.succeeded,
2130
+ failed: result.failed,
2131
+ dryRun: result.dryRun,
2132
+ ratePerSec: input.ratePerSec,
2133
+ reason: input.reason,
2134
+ wouldHaveFired: result.wouldHaveFired.length,
2135
+ },
2136
+ );
2137
+ return {
2138
+ ok: true as const,
2139
+ attempted: result.attempted,
2140
+ succeeded: result.succeeded,
2141
+ failed: result.failed,
2142
+ dryRun: result.dryRun,
2143
+ results: [...result.results],
2144
+ wouldHaveFired: [...result.wouldHaveFired],
2145
+ at: state.now(),
2146
+ };
2147
+ }
2148
+
2149
+ /**
2150
+ * Whether a dry-run can be offered safely for this signal.
2151
+ *
2152
+ * Orphaned / unknown consumers cannot be stubbed with confidence — refuse,
2153
+ * the same way Traces refuses a live replay when it cannot offer a safe dry run.
2154
+ *
2155
+ * @param row - Projected signal row
2156
+ */
2157
+ function dryRunSafety(row: {
2158
+ readonly orphaned: boolean;
2159
+ readonly consumers: ReadonlyArray<{ readonly flowId: string }>;
2160
+ }): { readonly ok: true } | { readonly ok: false; readonly reason: string } {
2161
+ if (row.orphaned) {
2162
+ return {
2163
+ ok: false,
2164
+ reason:
2165
+ "Orphaned signal — consumer shape unknown; dry-run refused rather than risk a side effect.",
2166
+ };
2167
+ }
2168
+ if (row.consumers.length === 0) {
2169
+ return {
2170
+ ok: false,
2171
+ reason:
2172
+ "No Manifest consumer — dry-run refused rather than invoke an unknown handler unsafely.",
2173
+ };
2174
+ }
2175
+ return { ok: true };
2176
+ }
2177
+
2178
+ function createActionPing(state: ConsoleState) {
2179
+ return flow({
2180
+ name: "console.action.ping",
2181
+ unit: "console",
2182
+ plane: "operator",
2183
+ in: ActionPingIn,
2184
+ out: ActionPingOut,
2185
+ errors: { AuthFailed },
2186
+ do: (input: z.infer<typeof ActionPingIn>, fx) => {
2187
+ if (!fx.operator.id) return fail("AuthFailed", {});
2188
+ fx.log.info("console.action.ping", {
2189
+ operatorId: fx.operator.id,
2190
+ note: input.note,
2191
+ });
2192
+ return { ok: true as const, note: input.note, at: state.now() };
2193
+ },
2194
+ });
2195
+ }
2196
+
2197
+ function createStructuralPropose(state: ConsoleState) {
2198
+ return flow({
2199
+ name: "console.structural.propose",
2200
+ unit: "console",
2201
+ plane: "operator",
2202
+ in: StructuralIn,
2203
+ out: StructuralOut,
2204
+ errors: { AuthFailed },
2205
+ do: async (input: z.infer<typeof StructuralIn>, fx) => {
2206
+ if (!fx.operator.id) return fail("AuthFailed", {});
2207
+ const proposal = await emitStructuralDiff({
2208
+ cwd: state.cwd,
2209
+ title: input.title,
2210
+ relativePath: input.relativePath,
2211
+ diff: createFileDiff(input.relativePath, input.contents),
2212
+ actorId: fx.operator.id,
2213
+ reason: input.reason,
2214
+ now: state.now,
2215
+ });
2216
+ fx.log.info("console.structural.propose", {
2217
+ id: proposal.id,
2218
+ path: proposal.path,
2219
+ });
2220
+ return { id: proposal.id, path: proposal.path, applied: false as const };
2221
+ },
2222
+ });
2223
+ }
2224
+
2225
+ function createFlowsIdentities(state: ConsoleState) {
2226
+ return flow({
2227
+ name: "console.flows.identities",
2228
+ unit: "console",
2229
+ plane: "operator",
2230
+ out: IdentitiesOut,
2231
+ errors: { AuthFailed },
2232
+ do: (_input, fx) => {
2233
+ if (!fx.operator.id) return fail("AuthFailed", {});
2234
+ return {
2235
+ identities: state.identities.map((i) => ({
2236
+ id: i.id,
2237
+ email: i.email,
2238
+ name: i.name,
2239
+ status: i.status,
2240
+ scopes: [...i.scopes],
2241
+ })),
2242
+ };
2243
+ },
2244
+ });
2245
+ }
2246
+
2247
+ function createFlowsInvoke(state: ConsoleState) {
2248
+ return flow({
2249
+ name: "console.flows.invoke",
2250
+ unit: "console",
2251
+ plane: "operator",
2252
+ in: InvokeIn,
2253
+ out: InvokeOut,
2254
+ errors: { AuthFailed, NotFound, InvokeDenied, ConfirmRequired },
2255
+ do: (input: z.infer<typeof InvokeIn>, fx) => {
2256
+ if (!fx.operator.id) return fail("AuthFailed", {});
2257
+ const manifest = state.manifest;
2258
+ const declared = manifest?.flows?.[input.flowId];
2259
+ if (!declared) {
2260
+ return fail("NotFound", { flowId: input.flowId });
2261
+ }
2262
+
2263
+ const identity = state.identities.find((i) => i.id === input.asUserId);
2264
+ if (!identity || identity.status !== "active") {
2265
+ return fail("InvokeDenied", { reason: "identity not found or disabled" });
2266
+ }
2267
+
2268
+ // Operators hold no application scopes — `console:flows:invoke-as`
2269
+ // (covered by session `console:*`) is the grant to assume a user principal.
2270
+ const assumed = userPrincipal({
2271
+ userId: identity.id,
2272
+ scopes: identity.scopes,
2273
+ verified: true,
2274
+ });
2275
+
2276
+ const peakTier = peakTierOf(declared);
2277
+ if (peakTier === "external" && state.production) {
2278
+ if (input.confirmation !== "INVOKE" || (input.reason?.trim().length ?? 0) < 3) {
2279
+ return fail("ConfirmRequired", {
2280
+ phrase: "INVOKE" as const,
2281
+ reason: "irreversible production invoke requires typed confirmation",
2282
+ });
2283
+ }
2284
+ }
2285
+
2286
+ const trigger = triggerKindOf(declared);
2287
+ const response = stubResponse(declared, input.body);
2288
+ fx.log.info("console.flows.invoke", {
2289
+ operatorId: fx.operator.id,
2290
+ flowId: input.flowId,
2291
+ asUserId: assumed.userId,
2292
+ scopes: [...assumed.scopes],
2293
+ peakTier,
2294
+ reason: input.reason,
2295
+ });
2296
+
2297
+ return {
2298
+ ok: true as const,
2299
+ flowId: input.flowId,
2300
+ asUserId: input.asUserId,
2301
+ trigger,
2302
+ response,
2303
+ peakTier,
2304
+ auditedAt: state.now(),
2305
+ };
2306
+ },
2307
+ });
2308
+ }
2309
+
2310
+ function peakTierOf(
2311
+ flow: ManifestFlow,
2312
+ ): "none" | "reads" | "writes" | "emits" | "external" | "capabilities" {
2313
+ const e = flow.effects;
2314
+ if (!e) return "none";
2315
+ if ((e.sends?.length ?? 0) > 0 || (e.asks?.length ?? 0) > 0) return "external";
2316
+ if ((e.secrets?.length ?? 0) > 0) return "capabilities";
2317
+ if ((e.emits?.length ?? 0) > 0) return "emits";
2318
+ if ((e.writes?.length ?? 0) > 0) return "writes";
2319
+ if ((e.reads?.length ?? 0) > 0) return "reads";
2320
+ return "none";
2321
+ }
2322
+
2323
+ function triggerKindOf(
2324
+ flow: ManifestFlow,
2325
+ ): "http" | "signal" | "clock" | "internal" | "durable" {
2326
+ if (flow.durable) return "durable";
2327
+ if (flow.trigger?.http) return "http";
2328
+ if (flow.trigger?.signal) return "signal";
2329
+ if (flow.trigger?.cron || flow.trigger?.every) return "clock";
2330
+ return "internal";
2331
+ }
2332
+
2333
+ /**
2334
+ * Deterministic stub response for Console invoke — echoes the request under
2335
+ * `echo` and fills required `out` string fields when declared.
2336
+ *
2337
+ * @param flow - Manifest flow
2338
+ * @param body - Request body
2339
+ */
2340
+ function stubResponse(flow: ManifestFlow, body: unknown): unknown {
2341
+ const out = flow.out;
2342
+ if (out && typeof out === "object" && !Array.isArray(out)) {
2343
+ const props = (out.properties ?? {}) as Record<string, unknown>;
2344
+ const required = Array.isArray(out.required)
2345
+ ? (out.required as string[])
2346
+ : Object.keys(props);
2347
+ const result: Record<string, unknown> = { echo: body };
2348
+ for (const key of required) {
2349
+ if (key === "id") result.id = `inv_${hashShort(body)}`;
2350
+ else if (!(key in result)) result[key] = null;
2351
+ }
2352
+ return result;
2353
+ }
2354
+ return { echo: body, ok: true };
2355
+ }
2356
+
2357
+ function hashShort(value: unknown): string {
2358
+ const text = JSON.stringify(value) ?? "";
2359
+ let h = 0;
2360
+ for (let i = 0; i < text.length; i++) {
2361
+ h = (h * 31 + text.charCodeAt(i)) >>> 0;
2362
+ }
2363
+ return h.toString(16).padStart(8, "0");
2364
+ }
2365
+
2366
+ async function issueOperatorSession(
2367
+ state: ConsoleState,
2368
+ operatorId: string,
2369
+ ): Promise<IssuedSession> {
2370
+ const op = state.operators.operators.get(operatorId);
2371
+ if (op) op.lastSeenAt = state.now();
2372
+ return issueSession(
2373
+ state.sessions,
2374
+ {
2375
+ secret: state.secret,
2376
+ now: state.now,
2377
+ accessTtlMs: state.accessTtlMs,
2378
+ },
2379
+ {
2380
+ id: operatorId,
2381
+ plane: "operator",
2382
+ scopes: ["console:*"],
2383
+ },
2384
+ );
2385
+ }
2386
+
2387
+ function sessionPayload(
2388
+ operatorId: string,
2389
+ email: string,
2390
+ name: string,
2391
+ issued: IssuedSession,
2392
+ ) {
2393
+ return {
2394
+ operatorId,
2395
+ email,
2396
+ name,
2397
+ accessToken: issued.accessToken,
2398
+ refreshToken: issued.refreshToken,
2399
+ accessExpiresAt: issued.accessExpiresAt,
2400
+ };
2401
+ }
2402
+
2403
+ /**
2404
+ * Require tenant when `manifest.tenancy` is declared (off by default).
2405
+ *
2406
+ * @param state - Console state
2407
+ * @param tenant - Optional tenant from the request
2408
+ */
2409
+ function requireTenantIfDeclared(
2410
+ state: ConsoleState,
2411
+ tenant: string | undefined,
2412
+ ): ReturnType<typeof fail> | null {
2413
+ if (!tenancyDeclared(state.manifest)) return null;
2414
+ if (tenant !== undefined && tenant.length > 0) return null;
2415
+ if (!state.production) return null;
2416
+ return fail("TenantRequired", {
2417
+ reason:
2418
+ "tenancy is declared — tenant selector is required (compliance boundary)",
2419
+ });
2420
+ }
2421
+
2422
+ function createStoreList(state: ConsoleState) {
2423
+ return flow({
2424
+ name: "console.store.list",
2425
+ unit: "console",
2426
+ plane: "operator",
2427
+ out: StoreListOut,
2428
+ errors: { AuthFailed },
2429
+ do: async (_input, fx) => {
2430
+ if (!fx.operator.id) return fail("AuthFailed", {});
2431
+ return state.listStores();
2432
+ },
2433
+ });
2434
+ }
2435
+
2436
+ function createStoreQuery(state: ConsoleState) {
2437
+ return flow({
2438
+ name: "console.store.query",
2439
+ unit: "console",
2440
+ plane: "operator",
2441
+ in: StoreQueryIn,
2442
+ out: StoreQueryOut,
2443
+ errors: { AuthFailed, TenantRequired, StoreNotFound },
2444
+ do: async (input: z.infer<typeof StoreQueryIn>, fx) => {
2445
+ if (!fx.operator.id) return fail("AuthFailed", {});
2446
+ const tenantFail = requireTenantIfDeclared(state, input.tenant);
2447
+ if (tenantFail) return tenantFail;
2448
+ return state.queryStore({
2449
+ ref: input.ref as ResourceRef,
2450
+ child: input.child,
2451
+ tenant: input.tenant,
2452
+ prefix: input.prefix,
2453
+ limit: input.limit,
2454
+ vector: input.vector,
2455
+ topK: input.topK,
2456
+ });
2457
+ },
2458
+ });
2459
+ }
2460
+
2461
+ function createStoreReveal(state: ConsoleState) {
2462
+ return flow({
2463
+ name: "console.store.reveal",
2464
+ unit: "console",
2465
+ plane: "operator",
2466
+ in: StoreRevealIn,
2467
+ out: StoreRevealOut,
2468
+ errors: { AuthFailed, TenantRequired, StoreNotFound },
2469
+ do: async (input: z.infer<typeof StoreRevealIn>, fx) => {
2470
+ if (!fx.operator.id) return fail("AuthFailed", {});
2471
+ const tenantFail = requireTenantIfDeclared(state, input.tenant);
2472
+ if (tenantFail) return tenantFail;
2473
+ const result = await state.queryStore({
2474
+ ref: input.ref as ResourceRef,
2475
+ child: input.child,
2476
+ tenant: input.tenant,
2477
+ revealPii: true,
2478
+ limit: 500,
2479
+ });
2480
+ const row = (result.rows ?? []).find(
2481
+ (r) => String(r.id ?? r.Id) === input.id,
2482
+ );
2483
+ if (!row) return fail("StoreNotFound", { ref: input.ref });
2484
+ fx.log.info("console.store.reveal", {
2485
+ operatorId: fx.operator.id,
2486
+ ref: input.ref,
2487
+ child: input.child,
2488
+ id: input.id,
2489
+ column: input.column,
2490
+ tenant: input.tenant,
2491
+ });
2492
+ return {
2493
+ ok: true as const,
2494
+ value: row[input.column],
2495
+ at: state.now(),
2496
+ };
2497
+ },
2498
+ });
2499
+ }
2500
+
2501
+ function createStoreEdit(state: ConsoleState) {
2502
+ return flow({
2503
+ name: "console.store.edit",
2504
+ unit: "console",
2505
+ plane: "operator",
2506
+ in: StoreEditIn,
2507
+ out: StoreEditOut,
2508
+ errors: { AuthFailed, TenantRequired, ConfirmRequired, StoreNotFound },
2509
+ do: async (input: z.infer<typeof StoreEditIn>, fx) => {
2510
+ if (!fx.operator.id) return fail("AuthFailed", {});
2511
+ const tenantFail = requireTenantIfDeclared(state, input.tenant);
2512
+ if (tenantFail) return tenantFail;
2513
+
2514
+ if (!input.commit) {
2515
+ // Return will-not-fire payload without applying — informational confirm.
2516
+ const preview = await state.editStore(
2517
+ {
2518
+ ref: input.ref as ResourceRef,
2519
+ child: input.child,
2520
+ tenant: input.tenant,
2521
+ id: input.id,
2522
+ key: input.key,
2523
+ patch: input.patch,
2524
+ },
2525
+ { dryRun: true },
2526
+ );
2527
+ return {
2528
+ ok: true as const,
2529
+ dryRun: true,
2530
+ applied: false,
2531
+ willNotFire: preview.willNotFire,
2532
+ wouldHaveFired: [...preview.wouldHaveFired],
2533
+ at: state.now(),
2534
+ };
2535
+ }
2536
+
2537
+ if (state.production) {
2538
+ if (
2539
+ input.confirmation !== "EDIT" ||
2540
+ (input.reason?.trim().length ?? 0) < 3
2541
+ ) {
2542
+ return fail("ConfirmRequired", {
2543
+ phrase: "EDIT" as const,
2544
+ reason:
2545
+ "direct edit is not a flow execution — typed confirmation required",
2546
+ });
2547
+ }
2548
+ }
2549
+
2550
+ const result = await state.editStore(
2551
+ {
2552
+ ref: input.ref as ResourceRef,
2553
+ child: input.child,
2554
+ tenant: input.tenant,
2555
+ id: input.id,
2556
+ key: input.key,
2557
+ patch: input.patch,
2558
+ },
2559
+ { dryRun: false },
2560
+ );
2561
+ fx.log.info("console.store.edit", {
2562
+ operatorId: fx.operator.id,
2563
+ ref: input.ref,
2564
+ child: input.child,
2565
+ willNotFire: result.willNotFire,
2566
+ reason: input.reason,
2567
+ });
2568
+ return {
2569
+ ok: true as const,
2570
+ dryRun: false,
2571
+ applied: result.applied,
2572
+ willNotFire: result.willNotFire,
2573
+ wouldHaveFired: [...result.wouldHaveFired],
2574
+ at: state.now(),
2575
+ };
2576
+ },
2577
+ });
2578
+ }
2579
+
2580
+ function createStoreDelete(state: ConsoleState) {
2581
+ return flow({
2582
+ name: "console.store.delete",
2583
+ unit: "console",
2584
+ plane: "operator",
2585
+ in: StoreDeleteIn,
2586
+ out: StoreDeleteOut,
2587
+ errors: { AuthFailed, TenantRequired, ConfirmRequired },
2588
+ do: async (input: z.infer<typeof StoreDeleteIn>, fx) => {
2589
+ if (!fx.operator.id) return fail("AuthFailed", {});
2590
+ const tenantFail = requireTenantIfDeclared(state, input.tenant);
2591
+ if (tenantFail) return tenantFail;
2592
+ if (state.production) {
2593
+ if (
2594
+ input.confirmation !== "DELETE" ||
2595
+ (input.reason?.trim().length ?? 0) < 3
2596
+ ) {
2597
+ return fail("ConfirmRequired", {
2598
+ phrase: "DELETE" as const,
2599
+ reason: "destructive store delete requires typed confirmation",
2600
+ });
2601
+ }
2602
+ }
2603
+ const result = await state.deleteStore({
2604
+ ref: input.ref as ResourceRef,
2605
+ child: input.child,
2606
+ tenant: input.tenant,
2607
+ ids: input.ids,
2608
+ keys: input.keys,
2609
+ });
2610
+ fx.log.info("console.store.delete", {
2611
+ operatorId: fx.operator.id,
2612
+ ref: input.ref,
2613
+ deleted: result.deleted,
2614
+ reason: input.reason,
2615
+ });
2616
+ return {
2617
+ ok: true as const,
2618
+ deleted: result.deleted,
2619
+ at: state.now(),
2620
+ };
2621
+ },
2622
+ });
2623
+ }
2624
+
2625
+ function createStorePurgeCache(state: ConsoleState) {
2626
+ return flow({
2627
+ name: "console.store.purgeCache",
2628
+ unit: "console",
2629
+ plane: "operator",
2630
+ in: StorePurgeIn,
2631
+ out: StorePurgeOut,
2632
+ errors: { AuthFailed, ConfirmRequired },
2633
+ do: async (input: z.infer<typeof StorePurgeIn>, fx) => {
2634
+ if (!fx.operator.id) return fail("AuthFailed", {});
2635
+ if (state.production) {
2636
+ if (
2637
+ input.confirmation !== "PURGE" ||
2638
+ (input.reason?.trim().length ?? 0) < 3
2639
+ ) {
2640
+ return fail("ConfirmRequired", {
2641
+ phrase: "PURGE" as const,
2642
+ reason: "cache purge requires typed confirmation",
2643
+ });
2644
+ }
2645
+ }
2646
+ const result = await state.purgeStoreCache(input.resource as ResourceRef);
2647
+ fx.log.info("console.store.purgeCache", {
2648
+ operatorId: fx.operator.id,
2649
+ resource: input.resource,
2650
+ keys: result.keys,
2651
+ reason: input.reason,
2652
+ });
2653
+ return {
2654
+ ok: true as const,
2655
+ keys: [...result.keys],
2656
+ at: state.now(),
2657
+ };
2658
+ },
2659
+ });
2660
+ }
2661
+
2662
+ function createStoreSql(state: ConsoleState) {
2663
+ return flow({
2664
+ name: "console.store.sql",
2665
+ unit: "console",
2666
+ plane: "operator",
2667
+ in: StoreSqlIn,
2668
+ out: StoreSqlOut,
2669
+ errors: { AuthFailed, TenantRequired, StoreNotFound },
2670
+ do: async (input: z.infer<typeof StoreSqlIn>, fx) => {
2671
+ if (!fx.operator.id) return fail("AuthFailed", {});
2672
+ const tenantFail = requireTenantIfDeclared(state, input.tenant);
2673
+ if (tenantFail) return tenantFail;
2674
+ try {
2675
+ const result = await state.runStoreSql(
2676
+ input.ref as ResourceRef,
2677
+ input.sql,
2678
+ {
2679
+ allowWrite: input.allowWrite === true,
2680
+ tenant: input.tenant,
2681
+ },
2682
+ );
2683
+ fx.log.info("console.store.sql", {
2684
+ operatorId: fx.operator.id,
2685
+ ref: input.ref,
2686
+ allowWrite: input.allowWrite === true,
2687
+ rowCount: result.rows.length,
2688
+ });
2689
+ return result;
2690
+ } catch (err) {
2691
+ return fail("StoreNotFound", {
2692
+ ref: `${input.ref}: ${err instanceof Error ? err.message : String(err)}`,
2693
+ });
2694
+ }
2695
+ },
2696
+ });
2697
+ }
2698
+
2699
+ function createStorePreview(state: ConsoleState) {
2700
+ return flow({
2701
+ name: "console.store.preview",
2702
+ unit: "console",
2703
+ plane: "operator",
2704
+ in: StorePreviewIn,
2705
+ out: StorePreviewOut,
2706
+ errors: { AuthFailed, TenantRequired, DryRunUnsafe },
2707
+ do: async (input: z.infer<typeof StorePreviewIn>, fx) => {
2708
+ if (!fx.operator.id) return fail("AuthFailed", {});
2709
+ const tenantFail = requireTenantIfDeclared(state, input.tenant);
2710
+ if (tenantFail) return tenantFail;
2711
+ try {
2712
+ const preview = await state.editStore(
2713
+ {
2714
+ ref: input.ref as ResourceRef,
2715
+ child: input.child,
2716
+ tenant: input.tenant,
2717
+ id: input.id,
2718
+ key: input.key,
2719
+ patch: input.patch,
2720
+ },
2721
+ { dryRun: true },
2722
+ );
2723
+ fx.log.info("console.store.preview", {
2724
+ operatorId: fx.operator.id,
2725
+ ref: input.ref,
2726
+ willNotFire: preview.willNotFire,
2727
+ });
2728
+ return {
2729
+ ok: true as const,
2730
+ dryRun: true as const,
2731
+ willNotFire: preview.willNotFire,
2732
+ wouldHaveFired: [...preview.wouldHaveFired],
2733
+ at: state.now(),
2734
+ };
2735
+ } catch (err) {
2736
+ if (err instanceof DryRunWriteIsolationError) {
2737
+ return fail("DryRunUnsafe", {
2738
+ ref: input.ref,
2739
+ reason: err.message,
2740
+ });
2741
+ }
2742
+ return fail("DryRunUnsafe", {
2743
+ ref: input.ref,
2744
+ reason: err instanceof Error ? err.message : String(err),
2745
+ });
2746
+ }
2747
+ },
2748
+ });
2749
+ }
2750
+
2751
+ function createVaultList(state: ConsoleState) {
2752
+ return flow({
2753
+ name: "console.vault.list",
2754
+ unit: "console",
2755
+ plane: "operator",
2756
+ out: VaultListOut,
2757
+ errors: { AuthFailed },
2758
+ do: async (_input, fx) => {
2759
+ if (!fx.operator.id) return fail("AuthFailed", {});
2760
+ return state.listVault();
2761
+ },
2762
+ });
2763
+ }
2764
+
2765
+ function createVaultSet(state: ConsoleState) {
2766
+ return flow({
2767
+ name: "console.vault.set",
2768
+ unit: "console",
2769
+ plane: "operator",
2770
+ in: VaultWriteIn,
2771
+ out: VaultWriteOut,
2772
+ errors: { AuthFailed, VaultNotFound, ConfirmRequired },
2773
+ do: async (input: z.infer<typeof VaultWriteIn>, fx) => {
2774
+ if (!fx.operator.id) return fail("AuthFailed", {});
2775
+ if (!(await vaultNameKnown(state, input.name))) {
2776
+ return fail("VaultNotFound", { name: input.name });
2777
+ }
2778
+ if (state.production) {
2779
+ if (input.confirmation !== "SET" || !input.reason || input.reason.length < 3) {
2780
+ return fail("ConfirmRequired", {
2781
+ phrase: "SET" as const,
2782
+ reason: "Type SET and provide a reason to write a secret in production",
2783
+ });
2784
+ }
2785
+ }
2786
+ try {
2787
+ const result = await state.setVault({
2788
+ name: input.name,
2789
+ value: input.value,
2790
+ });
2791
+ // Never log the value — name + fingerprint only.
2792
+ fx.log.info("console.vault.set", {
2793
+ operatorId: fx.operator.id,
2794
+ name: result.name,
2795
+ fingerprint: result.fingerprint,
2796
+ reason: input.reason,
2797
+ });
2798
+ return {
2799
+ ok: true as const,
2800
+ name: result.name,
2801
+ fingerprint: result.fingerprint,
2802
+ at: state.now(),
2803
+ };
2804
+ } catch (err) {
2805
+ return fail("VaultNotFound", {
2806
+ name: `${input.name}: ${err instanceof Error ? err.message : String(err)}`,
2807
+ });
2808
+ }
2809
+ },
2810
+ });
2811
+ }
2812
+
2813
+ function createVaultRotate(state: ConsoleState) {
2814
+ return flow({
2815
+ name: "console.vault.rotate",
2816
+ unit: "console",
2817
+ plane: "operator",
2818
+ in: VaultWriteIn,
2819
+ out: VaultWriteOut,
2820
+ errors: { AuthFailed, VaultNotFound, ConfirmRequired },
2821
+ do: async (input: z.infer<typeof VaultWriteIn>, fx) => {
2822
+ if (!fx.operator.id) return fail("AuthFailed", {});
2823
+ if (!(await vaultNameKnown(state, input.name))) {
2824
+ return fail("VaultNotFound", { name: input.name });
2825
+ }
2826
+ // Rotate always requires typed confirm (console §6 · §9.8).
2827
+ if (
2828
+ input.confirmation !== "ROTATE" ||
2829
+ !input.reason ||
2830
+ input.reason.length < 3
2831
+ ) {
2832
+ return fail("ConfirmRequired", {
2833
+ phrase: "ROTATE" as const,
2834
+ reason:
2835
+ "Type ROTATE and provide a reason — in-flight durable runs may wake holding the new key",
2836
+ });
2837
+ }
2838
+ try {
2839
+ const result = await state.rotateVault({
2840
+ name: input.name,
2841
+ value: input.value,
2842
+ });
2843
+ fx.log.info("console.vault.rotate", {
2844
+ operatorId: fx.operator.id,
2845
+ name: result.name,
2846
+ fingerprint: result.fingerprint,
2847
+ reason: input.reason,
2848
+ });
2849
+ return {
2850
+ ok: true as const,
2851
+ name: result.name,
2852
+ fingerprint: result.fingerprint,
2853
+ at: state.now(),
2854
+ };
2855
+ } catch (err) {
2856
+ return fail("VaultNotFound", {
2857
+ name: `${input.name}: ${err instanceof Error ? err.message : String(err)}`,
2858
+ });
2859
+ }
2860
+ },
2861
+ });
2862
+ }
2863
+
2864
+ /**
2865
+ * Whether a vault contract name is known to Manifest or the live runtime.
2866
+ *
2867
+ * @param state - Console state
2868
+ * @param name - Contract name
2869
+ */
2870
+ async function vaultNameKnown(
2871
+ state: ConsoleState,
2872
+ name: string,
2873
+ ): Promise<boolean> {
2874
+ if (state.manifest?.vault?.[name]) return true;
2875
+ if (state.vaultRuntime?.contracts.has(name)) return true;
2876
+ if (state.vaultRuntime?.names().includes(name)) return true;
2877
+ const listed = await state.listVault();
2878
+ return listed.secrets.some((s) => s.name === name);
2879
+ }
2880
+
2881
+ function createAiList(state: ConsoleState) {
2882
+ return flow({
2883
+ name: "console.ai.list",
2884
+ unit: "console",
2885
+ plane: "operator",
2886
+ out: AiListOut,
2887
+ errors: { AuthFailed },
2888
+ do: async (_input, fx) => {
2889
+ if (!fx.operator.id) return fail("AuthFailed", {});
2890
+ const projection = await state.listAi();
2891
+ return {
2892
+ prompts: projection.prompts.map((p) => ({
2893
+ name: p.name,
2894
+ ...(p.version !== undefined ? { version: p.version } : {}),
2895
+ ...(p.model !== undefined ? { model: p.model } : {}),
2896
+ ...(p.evals !== undefined ? { evals: p.evals } : {}),
2897
+ budgetMaxCostPerCall: p.budgetMaxCostPerCall,
2898
+ manifestDiffPath: p.manifestDiffPath,
2899
+ })),
2900
+ agents: projection.agents.map((a) => ({
2901
+ name: a.name,
2902
+ tools: [...a.tools],
2903
+ ...(a.maxSteps !== undefined ? { maxSteps: a.maxSteps } : {}),
2904
+ ...(a.model !== undefined ? { model: a.model } : {}),
2905
+ budgetMaxCostPerRun: a.budgetMaxCostPerRun,
2906
+ })),
2907
+ versions: projection.versions.map((v) => ({
2908
+ ...v,
2909
+ outcomeCounts: { ...v.outcomeCounts },
2910
+ })),
2911
+ allowPii: projection.allowPii.map((r) => ({
2912
+ ...r,
2913
+ asks: [...r.asks],
2914
+ })),
2915
+ fallbackChains: projection.fallbackChains.map((c) => ({
2916
+ prompt: c.prompt,
2917
+ ...(c.version !== undefined ? { version: c.version } : {}),
2918
+ attempts: c.attempts.map((a) => ({ ...a })),
2919
+ actualCost: c.actualCost,
2920
+ primaryOnlyCost: c.primaryOnlyCost,
2921
+ costConsequence: c.costConsequence,
2922
+ at: c.at,
2923
+ })),
2924
+ agentRuns: projection.agentRuns.map((r) => ({
2925
+ ...r,
2926
+ trail: r.trail.map((t) => ({
2927
+ ...t,
2928
+ effects: t.effects.map((e) => ({ ...e })),
2929
+ denial: t.denial ? { ...t.denial } : null,
2930
+ })),
2931
+ denials: r.denials.map((d) => ({ ...d })),
2932
+ })),
2933
+ denials: projection.denials.map((d) => ({ ...d })),
2934
+ };
2935
+ },
2936
+ });
2937
+ }
2938
+
2939
+ function createGatesList(state: ConsoleState) {
2940
+ return flow({
2941
+ name: "console.gates.list",
2942
+ unit: "console",
2943
+ plane: "operator",
2944
+ out: GatesListOut,
2945
+ errors: { AuthFailed },
2946
+ do: async (_input, fx) => {
2947
+ if (!fx.operator.id) return fail("AuthFailed", {});
2948
+ const projection = await state.listGates();
2949
+ return {
2950
+ moduleActions: [...projection.moduleActions],
2951
+ flows: projection.flows.map((f) => ({
2952
+ flowId: f.flowId,
2953
+ plane: f.plane,
2954
+ gates: [...f.gates],
2955
+ unguarded: f.unguarded,
2956
+ })),
2957
+ gates: projection.gates.map((g) => ({
2958
+ name: g.name,
2959
+ kind: g.kind,
2960
+ scopes: [...g.scopes],
2961
+ roles: [...g.roles],
2962
+ ...(g.strategy !== undefined ? { strategy: g.strategy } : {}),
2963
+ ...(g.max !== undefined ? { max: g.max } : {}),
2964
+ ...(g.per !== undefined ? { per: g.per } : {}),
2965
+ ...(g.keyBy !== undefined ? { keyBy: g.keyBy } : {}),
2966
+ overridable: g.overridable,
2967
+ attachedTo: [...g.attachedTo],
2968
+ })),
2969
+ principals: projection.principals.map((p) => ({
2970
+ kind: p.kind,
2971
+ id: p.id,
2972
+ name: p.name,
2973
+ plane: p.plane,
2974
+ scopes: [...p.scopes],
2975
+ ...(p.memberCount !== undefined
2976
+ ? { memberCount: p.memberCount }
2977
+ : {}),
2978
+ ...(p.email !== undefined ? { email: p.email } : {}),
2979
+ })),
2980
+ violations: projection.violations.map((v) => ({
2981
+ kind: v.kind,
2982
+ operatorId: v.operatorId,
2983
+ name: v.name,
2984
+ email: v.email,
2985
+ applicationScopes: [...v.applicationScopes],
2986
+ })),
2987
+ audit: {
2988
+ unguardedFlows: [...projection.audit.unguardedFlows],
2989
+ orphanPermissions: [...projection.audit.orphanPermissions],
2990
+ emptyRoles: [...projection.audit.emptyRoles],
2991
+ unattachedGates: [...projection.audit.unattachedGates],
2992
+ },
2993
+ widenings: projection.widenings.map((w) => ({
2994
+ path: w.path,
2995
+ category: w.category,
2996
+ kind: w.kind,
2997
+ summary: w.summary,
2998
+ ...(w.before !== undefined ? { before: w.before } : {}),
2999
+ ...(w.after !== undefined ? { after: w.after } : {}),
3000
+ })),
3001
+ };
3002
+ },
3003
+ });
3004
+ }
3005
+
3006
+ function createDiffList(state: ConsoleState) {
3007
+ return flow({
3008
+ name: "console.diff.list",
3009
+ unit: "console",
3010
+ plane: "operator",
3011
+ out: DiffListOut,
3012
+ errors: { AuthFailed },
3013
+ do: async (_input, fx) => {
3014
+ if (!fx.operator.id) return fail("AuthFailed", {});
3015
+ const projection = await state.listDiff();
3016
+ return {
3017
+ hasBaseline: projection.hasBaseline,
3018
+ severity: projection.severity,
3019
+ blockedCount: projection.blockedCount,
3020
+ acknowledgedCount: projection.acknowledgedCount,
3021
+ changes: projection.changes.map((c) => ({
3022
+ path: c.path,
3023
+ category: c.category,
3024
+ kind: c.kind,
3025
+ summary: c.summary,
3026
+ ...(c.before !== undefined ? { before: c.before } : {}),
3027
+ ...(c.after !== undefined ? { after: c.after } : {}),
3028
+ flowName: c.flowName,
3029
+ runCountLastWeek: c.runCountLastWeek,
3030
+ blastLine: c.blastLine,
3031
+ weeklyDeltaUsd: c.weeklyDeltaUsd,
3032
+ weeklyBillLine: c.weeklyBillLine,
3033
+ ciGate: c.ciGate,
3034
+ })),
3035
+ };
3036
+ },
3037
+ });
3038
+ }
3039
+
3040
+ function createPluginsList(state: ConsoleState) {
3041
+ return flow({
3042
+ name: "console.plugin.list",
3043
+ unit: "console",
3044
+ plane: "operator",
3045
+ out: PluginsListOut,
3046
+ errors: { AuthFailed },
3047
+ do: async (_input, fx) => {
3048
+ if (!fx.operator.id) return fail("AuthFailed", {});
3049
+ return state.listPlugins();
3050
+ },
3051
+ });
3052
+ }
3053
+
3054
+ function createGatesSimulate(state: ConsoleState) {
3055
+ return flow({
3056
+ name: "console.gates.simulate",
3057
+ unit: "console",
3058
+ plane: "operator",
3059
+ in: GatesSimulateIn,
3060
+ out: GatesSimulateOut,
3061
+ errors: { AuthFailed },
3062
+ do: async (input: z.infer<typeof GatesSimulateIn>, fx) => {
3063
+ if (!fx.operator.id) return fail("AuthFailed", {});
3064
+ const result = await state.simulateGates(input);
3065
+ return {
3066
+ flowId: result.flowId,
3067
+ gates: [...result.gates],
3068
+ evaluations: result.evaluations.map((e) => ({
3069
+ name: e.name,
3070
+ allowed: e.allowed,
3071
+ kind: e.kind,
3072
+ ...(e.remaining !== undefined ? { remaining: e.remaining } : {}),
3073
+ ...(e.retryAfterMs !== undefined
3074
+ ? { retryAfterMs: e.retryAfterMs }
3075
+ : {}),
3076
+ ...(e.reason !== undefined ? { reason: e.reason } : {}),
3077
+ })),
3078
+ deniedAt: result.deniedAt,
3079
+ denial: result.denial
3080
+ ? {
3081
+ code: result.denial.code,
3082
+ data: { ...result.denial.data },
3083
+ status: result.denial.status,
3084
+ }
3085
+ : null,
3086
+ allowed: result.allowed,
3087
+ };
3088
+ },
3089
+ });
3090
+ }
3091
+
3092
+ function createGatesPowers(state: ConsoleState) {
3093
+ return flow({
3094
+ name: "console.gates.powers",
3095
+ unit: "console",
3096
+ plane: "operator",
3097
+ in: GatesPowersIn,
3098
+ out: GatesPowersOut,
3099
+ errors: { AuthFailed },
3100
+ do: async (input: z.infer<typeof GatesPowersIn>, fx) => {
3101
+ if (!fx.operator.id) return fail("AuthFailed", {});
3102
+ const result = await state.powersForPrincipal(input);
3103
+ return {
3104
+ scopes: [...result.scopes],
3105
+ allowedFlowIds: [...result.allowedFlowIds],
3106
+ deniedFlowIds: [...result.deniedFlowIds],
3107
+ };
3108
+ },
3109
+ });
3110
+ }
3111
+
3112
+ /**
3113
+ * Actor grant ceiling — role scopes plus session `console:*`.
3114
+ *
3115
+ * @param state - Console state
3116
+ * @param operatorId - Acting operator
3117
+ */
3118
+ function actorScopesOf(state: ConsoleState, operatorId: string): string[] {
3119
+ const roleIds = state.operators.roles.get(operatorId) ?? [];
3120
+ const fromRoles = [...scopesForRoles(state.roles, roleIds, "operator")];
3121
+ // Console sessions mint `console:*` (see issueOperatorSession).
3122
+ return [...new Set([...fromRoles, "console:*"])];
3123
+ }
3124
+
3125
+ function createAccessList(state: ConsoleState) {
3126
+ return flow({
3127
+ name: "console.access.list",
3128
+ unit: "console",
3129
+ plane: "operator",
3130
+ out: AccessListOut,
3131
+ errors: { AuthFailed },
3132
+ do: async (_input, fx) => {
3133
+ if (!fx.operator.id) return fail("AuthFailed", {});
3134
+ return state.listAccess(actorScopesOf(state, fx.operator.id));
3135
+ },
3136
+ });
3137
+ }
3138
+
3139
+ function createAccessEffective(state: ConsoleState) {
3140
+ return flow({
3141
+ name: "console.access.effective",
3142
+ unit: "console",
3143
+ plane: "operator",
3144
+ in: AccessEffectiveIn,
3145
+ out: AccessEffectiveOut,
3146
+ errors: { AuthFailed, NotFound },
3147
+ do: async (input: z.infer<typeof AccessEffectiveIn>, fx) => {
3148
+ if (!fx.operator.id) return fail("AuthFailed", {});
3149
+ const result = await state.accessEffective(input);
3150
+ if (!result) {
3151
+ return fail("NotFound", { flowId: `${input.kind}:${input.id}` });
3152
+ }
3153
+ return {
3154
+ kind: result.kind,
3155
+ id: result.id,
3156
+ plane: result.plane,
3157
+ scopes: result.scopes.map((s) => ({
3158
+ scope: s.scope,
3159
+ sources: s.sources.map((src) => ({ ...src })),
3160
+ })),
3161
+ };
3162
+ },
3163
+ });
3164
+ }
3165
+
3166
+ function createAccessKeyBlast(state: ConsoleState) {
3167
+ return flow({
3168
+ name: "console.access.keyBlast",
3169
+ unit: "console",
3170
+ plane: "operator",
3171
+ in: AccessKeyBlastIn,
3172
+ out: AccessKeyBlastOut,
3173
+ errors: { AuthFailed, AccessKeyNotFound },
3174
+ do: async (input: z.infer<typeof AccessKeyBlastIn>, fx) => {
3175
+ if (!fx.operator.id) return fail("AuthFailed", {});
3176
+ if (!state.apiKeys.keys.has(input.keyId)) {
3177
+ return fail("AccessKeyNotFound", { keyId: input.keyId });
3178
+ }
3179
+ return state.accessKeyBlast(input.keyId);
3180
+ },
3181
+ });
3182
+ }
3183
+
3184
+ function createAccessCreateKey(state: ConsoleState) {
3185
+ return flow({
3186
+ name: "console.access.createKey",
3187
+ unit: "console",
3188
+ plane: "operator",
3189
+ in: AccessCreateKeyIn,
3190
+ out: AccessCreateKeyOut,
3191
+ errors: { AuthFailed, AccessGrantDenied },
3192
+ do: async (input: z.infer<typeof AccessCreateKeyIn>, fx) => {
3193
+ if (!fx.operator.id) return fail("AuthFailed", {});
3194
+ try {
3195
+ const created = await state.accessCreateKey({
3196
+ plane: input.plane,
3197
+ name: input.name,
3198
+ scopes: input.scopes,
3199
+ creatorId: fx.operator.id,
3200
+ creatorScopes: actorScopesOf(state, fx.operator.id),
3201
+ expiresAt: input.expiresAt,
3202
+ rateLimit: input.rateLimit,
3203
+ ipAllowlist: input.ipAllowlist,
3204
+ });
3205
+ fx.log.info("console.access.createKey", {
3206
+ keyId: created.row.id,
3207
+ plane: created.row.plane,
3208
+ operatorId: fx.operator.id,
3209
+ });
3210
+ return { key: { ...created.row }, secret: created.secret };
3211
+ } catch (err) {
3212
+ return fail("AccessGrantDenied", {
3213
+ reason: err instanceof Error ? err.message : "grant denied",
3214
+ });
3215
+ }
3216
+ },
3217
+ });
3218
+ }
3219
+
3220
+ function createAccessRevokeKey(state: ConsoleState) {
3221
+ return flow({
3222
+ name: "console.access.revokeKey",
3223
+ unit: "console",
3224
+ plane: "operator",
3225
+ in: AccessRevokeKeyIn,
3226
+ out: AccessRevokeKeyOut,
3227
+ errors: { AuthFailed, AccessKeyNotFound, ConfirmRequired },
3228
+ do: async (input: z.infer<typeof AccessRevokeKeyIn>, fx) => {
3229
+ if (!fx.operator.id) return fail("AuthFailed", {});
3230
+ if (
3231
+ input.confirmation !== "REVOKE" ||
3232
+ (input.reason?.trim().length ?? 0) < 3
3233
+ ) {
3234
+ return fail("ConfirmRequired", {
3235
+ phrase: "REVOKE" as const,
3236
+ reason:
3237
+ "Type REVOKE and provide a reason — revocation is irreversible",
3238
+ });
3239
+ }
3240
+ const blastRadius = await state.accessKeyBlast(input.keyId);
3241
+ const key = await state.accessRevokeKey(input.keyId);
3242
+ if (!key) {
3243
+ return fail("AccessKeyNotFound", { keyId: input.keyId });
3244
+ }
3245
+ fx.log.info("console.access.revokeKey", {
3246
+ keyId: key.id,
3247
+ operatorId: fx.operator.id,
3248
+ reason: input.reason,
3249
+ callVolume: blastRadius.callVolume,
3250
+ });
3251
+ return { key: { ...key }, blastRadius };
3252
+ },
3253
+ });
3254
+ }
3255
+
3256
+ function createAccessRotateKey(state: ConsoleState) {
3257
+ return flow({
3258
+ name: "console.access.rotateKey",
3259
+ unit: "console",
3260
+ plane: "operator",
3261
+ in: AccessRotateKeyIn,
3262
+ out: AccessRotateKeyOut,
3263
+ errors: { AuthFailed, AccessKeyNotFound, ConfirmRequired },
3264
+ do: async (input: z.infer<typeof AccessRotateKeyIn>, fx) => {
3265
+ if (!fx.operator.id) return fail("AuthFailed", {});
3266
+ if (
3267
+ input.confirmation !== "ROTATE" ||
3268
+ (input.reason?.trim().length ?? 0) < 3
3269
+ ) {
3270
+ return fail("ConfirmRequired", {
3271
+ phrase: "ROTATE" as const,
3272
+ reason:
3273
+ "Type ROTATE and provide a reason — the old secret dies immediately",
3274
+ });
3275
+ }
3276
+ const blastRadius = await state.accessKeyBlast(input.keyId);
3277
+ const rotated = await state.accessRotateKey(input.keyId);
3278
+ if (!rotated) {
3279
+ return fail("AccessKeyNotFound", { keyId: input.keyId });
3280
+ }
3281
+ fx.log.info("console.access.rotateKey", {
3282
+ keyId: rotated.row.id,
3283
+ operatorId: fx.operator.id,
3284
+ reason: input.reason,
3285
+ });
3286
+ return {
3287
+ key: { ...rotated.row },
3288
+ secret: rotated.secret,
3289
+ blastRadius,
3290
+ };
3291
+ },
3292
+ });
3293
+ }
3294
+
3295
+ function createAccessSetRoleGrants(state: ConsoleState) {
3296
+ return flow({
3297
+ name: "console.access.setRoleGrants",
3298
+ unit: "console",
3299
+ plane: "operator",
3300
+ in: AccessSetRoleGrantsIn,
3301
+ out: AccessSetRoleGrantsOut,
3302
+ errors: { AuthFailed, AccessGrantDenied },
3303
+ do: async (input: z.infer<typeof AccessSetRoleGrantsIn>, fx) => {
3304
+ if (!fx.operator.id) return fail("AuthFailed", {});
3305
+ try {
3306
+ await state.accessSetRoleGrants({
3307
+ roleId: input.roleId,
3308
+ scopes: input.scopes,
3309
+ actorScopes: actorScopesOf(state, fx.operator.id),
3310
+ });
3311
+ fx.log.info("console.access.setRoleGrants", {
3312
+ roleId: input.roleId,
3313
+ operatorId: fx.operator.id,
3314
+ scopes: input.scopes,
3315
+ });
3316
+ return { roleId: input.roleId, scopes: [...input.scopes] };
3317
+ } catch (err) {
3318
+ return fail("AccessGrantDenied", {
3319
+ reason: err instanceof Error ? err.message : "grant denied",
3320
+ });
3321
+ }
3322
+ },
3323
+ });
3324
+ }
3325
+
3326
+ function createClockList(state: ConsoleState) {
3327
+ return flow({
3328
+ name: "console.clock.list",
3329
+ unit: "console",
3330
+ plane: "operator",
3331
+ out: ClockListOut,
3332
+ errors: { AuthFailed },
3333
+ do: async (_input, fx) => {
3334
+ if (!fx.operator.id) return fail("AuthFailed", {});
3335
+ return state.listClocks();
3336
+ },
3337
+ });
3338
+ }
3339
+
3340
+ function createClockRunNow(state: ConsoleState) {
3341
+ return flow({
3342
+ name: "console.clock.runNow",
3343
+ unit: "console",
3344
+ plane: "operator",
3345
+ in: ClockRunNowIn,
3346
+ out: ClockRunNowOut,
3347
+ errors: { AuthFailed, ClockNotFound, ConfirmRequired },
3348
+ do: async (input: z.infer<typeof ClockRunNowIn>, fx) => {
3349
+ if (!fx.operator.id) return fail("AuthFailed", {});
3350
+ const list = await state.listClocks();
3351
+ const cron = list.crons.find((c) => c.name === input.name);
3352
+ if (!cron) {
3353
+ return fail("ClockNotFound", { kind: "cron" as const, id: input.name });
3354
+ }
3355
+ if (state.production && cron.external) {
3356
+ if (
3357
+ input.confirmation !== "RUN" ||
3358
+ !input.reason ||
3359
+ input.reason.length < 3
3360
+ ) {
3361
+ return fail("ConfirmRequired", {
3362
+ phrase: "RUN" as const,
3363
+ reason:
3364
+ "Type RUN and provide a reason — this cron has an external effect",
3365
+ });
3366
+ }
3367
+ }
3368
+ try {
3369
+ const result = await state.runCronNow(input.name);
3370
+ fx.log.info("console.clock.runNow", {
3371
+ operatorId: fx.operator.id,
3372
+ name: input.name,
3373
+ ran: result.ran,
3374
+ reason: input.reason,
3375
+ });
3376
+ return {
3377
+ ok: true as const,
3378
+ name: input.name,
3379
+ ran: result.ran,
3380
+ at: state.now(),
3381
+ };
3382
+ } catch (err) {
3383
+ if (err instanceof ClockResourceNotFoundError) {
3384
+ return fail("ClockNotFound", { kind: err.kind, id: err.id });
3385
+ }
3386
+ throw err;
3387
+ }
3388
+ },
3389
+ });
3390
+ }
3391
+
3392
+ function createClockPause(state: ConsoleState) {
3393
+ return flow({
3394
+ name: "console.clock.pause",
3395
+ unit: "console",
3396
+ plane: "operator",
3397
+ in: ClockPauseIn,
3398
+ out: ClockPauseOut,
3399
+ errors: { AuthFailed, ClockNotFound },
3400
+ do: async (input: z.infer<typeof ClockPauseIn>, fx) => {
3401
+ if (!fx.operator.id) return fail("AuthFailed", {});
3402
+ try {
3403
+ const result = await state.pauseCron(input.name);
3404
+ fx.log.info("console.clock.pause", {
3405
+ operatorId: fx.operator.id,
3406
+ name: result.name,
3407
+ });
3408
+ return {
3409
+ ok: true as const,
3410
+ name: result.name,
3411
+ status: result.status,
3412
+ at: state.now(),
3413
+ };
3414
+ } catch (err) {
3415
+ if (err instanceof ClockResourceNotFoundError) {
3416
+ return fail("ClockNotFound", { kind: err.kind, id: err.id });
3417
+ }
3418
+ throw err;
3419
+ }
3420
+ },
3421
+ });
3422
+ }
3423
+
3424
+ function createClockEditSchedule(state: ConsoleState) {
3425
+ return flow({
3426
+ name: "console.clock.editSchedule",
3427
+ unit: "console",
3428
+ plane: "operator",
3429
+ in: ClockEditIn,
3430
+ out: ClockEditOut,
3431
+ errors: { AuthFailed, ClockNotFound, ScheduleNotOverridable },
3432
+ do: async (input: z.infer<typeof ClockEditIn>, fx) => {
3433
+ if (!fx.operator.id) return fail("AuthFailed", {});
3434
+ try {
3435
+ const result = await state.editSchedule({
3436
+ name: input.name,
3437
+ cron: input.cron,
3438
+ every: input.every,
3439
+ });
3440
+ fx.log.info("console.clock.editSchedule", {
3441
+ operatorId: fx.operator.id,
3442
+ name: result.name,
3443
+ });
3444
+ return {
3445
+ ok: true as const,
3446
+ name: result.name,
3447
+ effectiveCron: result.effectiveCron,
3448
+ effectiveEvery: result.effectiveEvery,
3449
+ at: state.now(),
3450
+ };
3451
+ } catch (err) {
3452
+ if (err instanceof ScheduleNotOverridableError) {
3453
+ return fail("ScheduleNotOverridable", { name: err.cronName });
3454
+ }
3455
+ if (err instanceof ClockResourceNotFoundError) {
3456
+ return fail("ClockNotFound", { kind: err.kind, id: err.id });
3457
+ }
3458
+ throw err;
3459
+ }
3460
+ },
3461
+ });
3462
+ }
3463
+
3464
+ function createClockWakeEarly(state: ConsoleState) {
3465
+ return flow({
3466
+ name: "console.clock.wakeEarly",
3467
+ unit: "console",
3468
+ plane: "operator",
3469
+ in: ClockWakeEarlyIn,
3470
+ out: ClockWakeEarlyOut,
3471
+ errors: { AuthFailed, ClockNotFound },
3472
+ do: async (input: z.infer<typeof ClockWakeEarlyIn>, fx) => {
3473
+ if (!fx.operator.id) return fail("AuthFailed", {});
3474
+ try {
3475
+ const result = await state.wakeEarly(input.runId);
3476
+ fx.log.info("console.clock.wakeEarly", {
3477
+ operatorId: fx.operator.id,
3478
+ runId: result.runId,
3479
+ resumed: result.resumed,
3480
+ });
3481
+ return {
3482
+ ok: true as const,
3483
+ runId: result.runId,
3484
+ wakeAt: result.wakeAt,
3485
+ resumed: result.resumed,
3486
+ at: state.now(),
3487
+ };
3488
+ } catch (err) {
3489
+ if (err instanceof ClockResourceNotFoundError) {
3490
+ return fail("ClockNotFound", { kind: err.kind, id: err.id });
3491
+ }
3492
+ throw err;
3493
+ }
3494
+ },
3495
+ });
3496
+ }
3497
+
3498
+ function createChannelsList(state: ConsoleState) {
3499
+ return flow({
3500
+ name: "console.channel.list",
3501
+ unit: "console",
3502
+ plane: "operator",
3503
+ out: ChannelsListOut,
3504
+ errors: { AuthFailed },
3505
+ do: async (_input, fx) => {
3506
+ if (!fx.operator.id) return fail("AuthFailed", {});
3507
+ return state.listChannels();
3508
+ },
3509
+ });
3510
+ }
3511
+
3512
+ function createChannelPreview(state: ConsoleState) {
3513
+ return flow({
3514
+ name: "console.channel.preview",
3515
+ unit: "console",
3516
+ plane: "operator",
3517
+ in: ChannelPreviewIn,
3518
+ out: ChannelPreviewOut,
3519
+ errors: { AuthFailed, ChannelNotFound },
3520
+ do: async (input: z.infer<typeof ChannelPreviewIn>, fx) => {
3521
+ if (!fx.operator.id) return fail("AuthFailed", {});
3522
+ if (
3523
+ state.manifest?.channels &&
3524
+ !(input.template in state.manifest.channels) &&
3525
+ !state.channelRuntime?.templates.has(input.template)
3526
+ ) {
3527
+ return fail("ChannelNotFound", { template: input.template });
3528
+ }
3529
+ return state.previewChannel(input);
3530
+ },
3531
+ });
3532
+ }
3533
+
3534
+ function createChannelVerifyAuth(state: ConsoleState) {
3535
+ return flow({
3536
+ name: "console.channel.verifyAuth",
3537
+ unit: "console",
3538
+ plane: "operator",
3539
+ in: ChannelVerifyAuthIn,
3540
+ out: ChannelVerifyAuthOut,
3541
+ errors: { AuthFailed },
3542
+ do: async (input: z.infer<typeof ChannelVerifyAuthIn>, fx) => {
3543
+ if (!fx.operator.id) return fail("AuthFailed", {});
3544
+ return state.verifyChannelAuth(input.from);
3545
+ },
3546
+ });
3547
+ }
3548
+
3549
+ function createChannelReveal(state: ConsoleState) {
3550
+ return flow({
3551
+ name: "console.channel.reveal",
3552
+ unit: "console",
3553
+ plane: "operator",
3554
+ in: ChannelRevealIn,
3555
+ out: ChannelRevealOut,
3556
+ errors: { AuthFailed, ChannelNotFound },
3557
+ do: async (input: z.infer<typeof ChannelRevealIn>, fx) => {
3558
+ if (!fx.operator.id) return fail("AuthFailed", {});
3559
+ const revealed = await state.revealChannel(input.id);
3560
+ if (!revealed) {
3561
+ return fail("ChannelNotFound", { template: input.id });
3562
+ }
3563
+ fx.log.info("console.channel.reveal", {
3564
+ operatorId: fx.operator.id,
3565
+ id: revealed.id,
3566
+ });
3567
+ return {
3568
+ ok: true as const,
3569
+ id: revealed.id,
3570
+ to: revealed.to,
3571
+ at: state.now(),
3572
+ };
3573
+ },
3574
+ });
3575
+ }
3576
+
3577
+ function createChannelSendTest(state: ConsoleState) {
3578
+ return flow({
3579
+ name: "console.channel.sendTest",
3580
+ unit: "console",
3581
+ plane: "operator",
3582
+ in: ChannelSendTestIn,
3583
+ out: ChannelSendTestOut,
3584
+ errors: { AuthFailed, ChannelNotFound, ConfirmRequired },
3585
+ do: async (input: z.infer<typeof ChannelSendTestIn>, fx) => {
3586
+ if (!fx.operator.id) return fail("AuthFailed", {});
3587
+ if (state.production) {
3588
+ if (
3589
+ input.confirmation !== "SEND" ||
3590
+ (input.reason?.trim().length ?? 0) < 3
3591
+ ) {
3592
+ return fail("ConfirmRequired", {
3593
+ phrase: "SEND" as const,
3594
+ reason:
3595
+ "send test is a real external send — typed confirmation required",
3596
+ });
3597
+ }
3598
+ }
3599
+ if (!state.channelRuntime) {
3600
+ return fail("ChannelNotFound", { template: input.template });
3601
+ }
3602
+ if (
3603
+ !state.channelRuntime.templates.has(input.template) &&
3604
+ !(state.manifest?.channels && input.template in state.manifest.channels)
3605
+ ) {
3606
+ return fail("ChannelNotFound", { template: input.template });
3607
+ }
3608
+ try {
3609
+ const result = await state.sendChannelTest({
3610
+ template: input.template,
3611
+ to: input.to,
3612
+ locale: input.locale,
3613
+ data: input.data,
3614
+ });
3615
+ fx.log.info("console.channel.sendTest", {
3616
+ operatorId: fx.operator.id,
3617
+ template: input.template,
3618
+ messageId: result.messageId,
3619
+ ok: result.ok,
3620
+ reason: input.reason,
3621
+ });
3622
+ return {
3623
+ ...result,
3624
+ at: state.now(),
3625
+ };
3626
+ } catch (err) {
3627
+ return fail("ChannelNotFound", {
3628
+ template: `${input.template}: ${err instanceof Error ? err.message : String(err)}`,
3629
+ });
3630
+ }
3631
+ },
3632
+ });
3633
+ }