@anchrd/intel 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/README.md +496 -0
  2. package/bin/intel.mjs +134 -0
  3. package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
  4. package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
  5. package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
  6. package/dist/adapters/cloudflare/cloudflare.js +416 -0
  7. package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
  8. package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
  9. package/dist/adapters/content/content.d.ts +3 -0
  10. package/dist/adapters/content/content.js +128 -0
  11. package/dist/adapters/content/content.types.d.ts +38 -0
  12. package/dist/adapters/content/content.types.js +1 -0
  13. package/dist/adapters/db/db-audit.d.ts +62 -0
  14. package/dist/adapters/db/db-audit.js +150 -0
  15. package/dist/adapters/db/db-boards.d.ts +6 -0
  16. package/dist/adapters/db/db-boards.js +324 -0
  17. package/dist/adapters/db/db-feed.d.ts +30 -0
  18. package/dist/adapters/db/db-feed.js +276 -0
  19. package/dist/adapters/db/db-flows.d.ts +42 -0
  20. package/dist/adapters/db/db-flows.js +1311 -0
  21. package/dist/adapters/db/db-grants.d.ts +133 -0
  22. package/dist/adapters/db/db-grants.js +269 -0
  23. package/dist/adapters/db/db-indexing.d.ts +3 -0
  24. package/dist/adapters/db/db-indexing.js +200 -0
  25. package/dist/adapters/db/db-oauth.d.ts +3 -0
  26. package/dist/adapters/db/db-oauth.js +26 -0
  27. package/dist/adapters/db/db-prompts.d.ts +46 -0
  28. package/dist/adapters/db/db-prompts.js +144 -0
  29. package/dist/adapters/db/db.d.ts +6 -0
  30. package/dist/adapters/db/db.js +1687 -0
  31. package/dist/adapters/db/db.types.d.ts +16 -0
  32. package/dist/adapters/db/db.types.js +1 -0
  33. package/dist/adapters/document-converter/document-converter.d.ts +2 -0
  34. package/dist/adapters/document-converter/document-converter.js +26 -0
  35. package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
  36. package/dist/adapters/document-converter/document-converter.types.js +1 -0
  37. package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
  38. package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
  39. package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
  40. package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
  41. package/dist/adapters/index-queue/index-queue.d.ts +10 -0
  42. package/dist/adapters/index-queue/index-queue.js +12 -0
  43. package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
  44. package/dist/adapters/index-queue/index-queue.types.js +1 -0
  45. package/dist/adapters/json-schema/json-schema.d.ts +2 -0
  46. package/dist/adapters/json-schema/json-schema.js +77 -0
  47. package/dist/adapters/openid/openid.d.ts +5 -0
  48. package/dist/adapters/openid/openid.js +301 -0
  49. package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
  50. package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
  51. package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
  52. package/dist/adapters/remote-tools/remote-tools.js +129 -0
  53. package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
  54. package/dist/adapters/semantic-index/semantic-index.js +139 -0
  55. package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
  56. package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
  57. package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
  58. package/dist/adapters/session-cookie/session-cookie.js +87 -0
  59. package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
  60. package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
  61. package/dist/audit/audit.d.ts +2 -0
  62. package/dist/audit/audit.js +87 -0
  63. package/dist/audit/audit.types.d.ts +23 -0
  64. package/dist/audit/audit.types.js +1 -0
  65. package/dist/auth/auth.d.ts +2 -0
  66. package/dist/auth/auth.js +284 -0
  67. package/dist/auth/auth.types.d.ts +108 -0
  68. package/dist/auth/auth.types.js +1 -0
  69. package/dist/boards/boards.d.ts +44 -0
  70. package/dist/boards/boards.js +447 -0
  71. package/dist/boards/boards.types.d.ts +117 -0
  72. package/dist/boards/boards.types.js +1 -0
  73. package/dist/build/build.d.ts +2 -0
  74. package/dist/build/build.js +192 -0
  75. package/dist/build/build.types.d.ts +17 -0
  76. package/dist/build/build.types.js +1 -0
  77. package/dist/bundle/bundle.d.ts +4 -0
  78. package/dist/bundle/bundle.js +1194 -0
  79. package/dist/bundle/bundle.types.d.ts +33 -0
  80. package/dist/bundle/bundle.types.js +1 -0
  81. package/dist/cli/cli.d.ts +4 -0
  82. package/dist/cli/cli.js +262 -0
  83. package/dist/cli/cli.types.d.ts +14 -0
  84. package/dist/cli/cli.types.js +1 -0
  85. package/dist/contract/audit.d.ts +50 -0
  86. package/dist/contract/audit.js +72 -0
  87. package/dist/contract/board.d.ts +150 -0
  88. package/dist/contract/board.js +262 -0
  89. package/dist/contract/bundle.d.ts +147 -0
  90. package/dist/contract/bundle.js +139 -0
  91. package/dist/contract/contract.d.ts +112 -0
  92. package/dist/contract/contract.js +143 -0
  93. package/dist/contract/feed.d.ts +126 -0
  94. package/dist/contract/feed.js +149 -0
  95. package/dist/contract/flow-run.d.ts +346 -0
  96. package/dist/contract/flow-run.js +194 -0
  97. package/dist/contract/flow.d.ts +1057 -0
  98. package/dist/contract/flow.js +552 -0
  99. package/dist/contract/node.d.ts +551 -0
  100. package/dist/contract/node.js +456 -0
  101. package/dist/contract/share.d.ts +216 -0
  102. package/dist/contract/share.js +134 -0
  103. package/dist/contract/table.d.ts +171 -0
  104. package/dist/contract/table.js +117 -0
  105. package/dist/contract/tool.d.ts +122 -0
  106. package/dist/contract/tool.js +172 -0
  107. package/dist/feed/feed.d.ts +2 -0
  108. package/dist/feed/feed.js +77 -0
  109. package/dist/feed/feed.types.d.ts +23 -0
  110. package/dist/feed/feed.types.js +1 -0
  111. package/dist/flows/flows.d.ts +41 -0
  112. package/dist/flows/flows.js +2216 -0
  113. package/dist/flows/flows.types.d.ts +339 -0
  114. package/dist/flows/flows.types.js +1 -0
  115. package/dist/http/http.d.ts +9 -0
  116. package/dist/http/http.js +874 -0
  117. package/dist/http/http.types.d.ts +22 -0
  118. package/dist/http/http.types.js +1 -0
  119. package/dist/indexing/indexing.d.ts +6 -0
  120. package/dist/indexing/indexing.js +239 -0
  121. package/dist/indexing/indexing.types.d.ts +11 -0
  122. package/dist/indexing/indexing.types.js +1 -0
  123. package/dist/intel/intel.d.ts +3 -0
  124. package/dist/intel/intel.js +194 -0
  125. package/dist/intel/intel.types.d.ts +24 -0
  126. package/dist/intel/intel.types.js +1 -0
  127. package/dist/mcp/mcp.d.ts +2 -0
  128. package/dist/mcp/mcp.js +1292 -0
  129. package/dist/mcp/mcp.types.d.ts +25 -0
  130. package/dist/mcp/mcp.types.js +1 -0
  131. package/dist/nodes/document-links/document-links.d.ts +20 -0
  132. package/dist/nodes/document-links/document-links.js +57 -0
  133. package/dist/nodes/nodes.d.ts +2 -0
  134. package/dist/nodes/nodes.js +1667 -0
  135. package/dist/nodes/nodes.types.d.ts +511 -0
  136. package/dist/nodes/nodes.types.js +1 -0
  137. package/dist/prepare/prepare.d.ts +4 -0
  138. package/dist/prepare/prepare.js +16 -0
  139. package/dist/prepare/prepare.types.d.ts +9 -0
  140. package/dist/prepare/prepare.types.js +1 -0
  141. package/dist/prompts/prompts.d.ts +2 -0
  142. package/dist/prompts/prompts.js +65 -0
  143. package/dist/prompts/prompts.types.d.ts +71 -0
  144. package/dist/prompts/prompts.types.js +1 -0
  145. package/dist/shared/callers/callers.d.ts +28 -0
  146. package/dist/shared/callers/callers.js +30 -0
  147. package/dist/shared/csv/csv.d.ts +13 -0
  148. package/dist/shared/csv/csv.js +85 -0
  149. package/dist/shared/document-text/document-text.d.ts +21 -0
  150. package/dist/shared/document-text/document-text.js +31 -0
  151. package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
  152. package/dist/shared/gate-authorization/gate-authorization.js +26 -0
  153. package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
  154. package/dist/shared/grant-expiry/grant-expiry.js +26 -0
  155. package/dist/shared/intel-error/intel-error.d.ts +22 -0
  156. package/dist/shared/intel-error/intel-error.js +27 -0
  157. package/dist/shared/plain-title/plain-title.d.ts +16 -0
  158. package/dist/shared/plain-title/plain-title.js +45 -0
  159. package/dist/shared/problem-details/problem-details.d.ts +30 -0
  160. package/dist/shared/problem-details/problem-details.js +18 -0
  161. package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
  162. package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
  163. package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
  164. package/dist/shared/safe-return-path/safe-return-path.js +11 -0
  165. package/dist/shared/sha256/sha256.d.ts +2 -0
  166. package/dist/shared/sha256/sha256.js +14 -0
  167. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  168. package/dist/tools/tool-servers/tool-servers.js +177 -0
  169. package/dist/tools/tools.d.ts +2 -0
  170. package/dist/tools/tools.js +207 -0
  171. package/dist/tools/tools.types.d.ts +56 -0
  172. package/dist/tools/tools.types.js +1 -0
  173. package/examples/branding/favicon.svg +5 -0
  174. package/examples/branding/logo.svg +5 -0
  175. package/examples/branding/theme.css +16 -0
  176. package/examples/dev.vars.example +7 -0
  177. package/examples/intel.json +9 -0
  178. package/migrations/0000_intel_foundation.sql +203 -0
  179. package/migrations/0001_portal_native_tools.sql +21 -0
  180. package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
  181. package/migrations/0003_folder_permissions.sql +211 -0
  182. package/migrations/0004_subflow_runs.sql +14 -0
  183. package/migrations/0005_flow_node_cleanup.sql +28 -0
  184. package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
  185. package/migrations/0006_links_are_written_in_the_text.sql +20 -0
  186. package/migrations/0007_no_node_waits.sql +69 -0
  187. package/migrations/0008_three_layers.sql +130 -0
  188. package/migrations/0009_no_context_policy.sql +48 -0
  189. package/migrations/0010_no_run_waits_a_year.sql +27 -0
  190. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  191. package/migrations/0012_table_snapshots.sql +29 -0
  192. package/migrations/0017_a_vector_per_card.sql +38 -0
  193. package/migrations/0018_no_context_policy_at_last.sql +97 -0
  194. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  195. package/migrations/0020_cascade_purge_replay.sql +11 -0
  196. package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
  197. package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
  198. package/migrations/0023_a_board_and_its_tasks.sql +116 -0
  199. package/migrations/0024_the_archive_is_a_column.sql +33 -0
  200. package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
  201. package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
  202. package/migrations/0027_the_runs_of_every_flow.sql +27 -0
  203. package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
  204. package/package.json +118 -0
  205. package/src/contract/CLAUDE.md +152 -0
  206. package/src/contract/README.md +102 -0
  207. package/src/contract/audit.ts +85 -0
  208. package/src/contract/board.ts +306 -0
  209. package/src/contract/bundle.ts +157 -0
  210. package/src/contract/contract.ts +161 -0
  211. package/src/contract/feed.ts +165 -0
  212. package/src/contract/flow-run.ts +241 -0
  213. package/src/contract/flow.ts +675 -0
  214. package/src/contract/node.ts +583 -0
  215. package/src/contract/share.ts +196 -0
  216. package/src/contract/table.ts +178 -0
  217. package/src/contract/tool.ts +202 -0
  218. package/ui/README.md +110 -0
  219. package/ui/components.json +21 -0
  220. package/ui/index.html +13 -0
  221. package/ui/package.json +11 -0
  222. package/ui/scripts/lint-tokens.mjs +187 -0
  223. package/ui/src/access-summary/access-summary.tsx +385 -0
  224. package/ui/src/app/action-slot/action-slot.tsx +27 -0
  225. package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
  226. package/ui/src/app/app-tree/app-tree.tsx +1175 -0
  227. package/ui/src/app/app.tsx +87 -0
  228. package/ui/src/app/header-search/header-search.tsx +294 -0
  229. package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
  230. package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
  231. package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
  232. package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
  233. package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
  234. package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
  235. package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
  236. package/ui/src/app/tree-move/tree-move.tsx +427 -0
  237. package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
  238. package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
  239. package/ui/src/app/user-footer/user-footer.tsx +188 -0
  240. package/ui/src/app/view-menu/view-menu.tsx +115 -0
  241. package/ui/src/app-root/app-root.tsx +109 -0
  242. package/ui/src/app-root/app-root.types.ts +27 -0
  243. package/ui/src/archive/archive.tsx +417 -0
  244. package/ui/src/attachment-detail/attachment-detail.ts +52 -0
  245. package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
  246. package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
  247. package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
  248. package/ui/src/board/board-assignee/board-assignee.ts +48 -0
  249. package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
  250. package/ui/src/board/board-chip/board-chip.tsx +46 -0
  251. package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
  252. package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
  253. package/ui/src/board/board-data/board-data.ts +195 -0
  254. package/ui/src/board/board-data/board-data.types.ts +38 -0
  255. package/ui/src/board/board-dates/board-dates.ts +40 -0
  256. package/ui/src/board/board-dates/board-dates.tsx +105 -0
  257. package/ui/src/board/board-kanban/board-kanban.ts +162 -0
  258. package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
  259. package/ui/src/board/board-open-task/board-open-task.ts +32 -0
  260. package/ui/src/board/board-panel/board-panel.tsx +259 -0
  261. package/ui/src/board/board-settings/board-settings.tsx +226 -0
  262. package/ui/src/board/board-status/board-status.tsx +60 -0
  263. package/ui/src/board/board-stripes/board-stripes.ts +128 -0
  264. package/ui/src/board/board-table/board-table.ts +53 -0
  265. package/ui/src/board/board-table/board-table.tsx +610 -0
  266. package/ui/src/board/board-task/board-task.ts +108 -0
  267. package/ui/src/board/board-task/board-task.tsx +335 -0
  268. package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
  269. package/ui/src/branding/branding.tsx +17 -0
  270. package/ui/src/branding/custom-logo.ts +1 -0
  271. package/ui/src/branding/favicon.default.svg +5 -0
  272. package/ui/src/branding/favicon.svg +5 -0
  273. package/ui/src/components/ui/avatar.tsx +39 -0
  274. package/ui/src/components/ui/button.tsx +64 -0
  275. package/ui/src/components/ui/collapsible.tsx +20 -0
  276. package/ui/src/components/ui/command.tsx +160 -0
  277. package/ui/src/components/ui/dialog.tsx +143 -0
  278. package/ui/src/components/ui/dropdown-menu.tsx +187 -0
  279. package/ui/src/components/ui/input.tsx +21 -0
  280. package/ui/src/components/ui/popover.tsx +41 -0
  281. package/ui/src/components/ui/select.tsx +163 -0
  282. package/ui/src/components/ui/separator.tsx +26 -0
  283. package/ui/src/components/ui/sheet.tsx +136 -0
  284. package/ui/src/components/ui/sidebar.tsx +693 -0
  285. package/ui/src/components/ui/skeleton.tsx +13 -0
  286. package/ui/src/components/ui/switch.tsx +25 -0
  287. package/ui/src/components/ui/table.tsx +82 -0
  288. package/ui/src/components/ui/tabs.tsx +52 -0
  289. package/ui/src/components/ui/tooltip.tsx +51 -0
  290. package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
  291. package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
  292. package/ui/src/data/query-client.ts +18 -0
  293. package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
  294. package/ui/src/data/request-refusal/request-refusal.ts +51 -0
  295. package/ui/src/data/sign-in/sign-in.ts +97 -0
  296. package/ui/src/data/sign-in/sign-in.types.ts +15 -0
  297. package/ui/src/document-link/document-link.tsx +114 -0
  298. package/ui/src/document-markdown/document-markdown.ts +195 -0
  299. package/ui/src/editor-schema/editor-schema.ts +23 -0
  300. package/ui/src/entry-picker/entry-picker.tsx +238 -0
  301. package/ui/src/feed/feed.tsx +344 -0
  302. package/ui/src/file-preview/file-preview-view.tsx +496 -0
  303. package/ui/src/file-preview/file-preview.tsx +28 -0
  304. package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
  305. package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
  306. package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
  307. package/ui/src/file-preview/word-file-preview.tsx +5 -0
  308. package/ui/src/flow-runs/flow-runs.tsx +238 -0
  309. package/ui/src/flows/flows.tsx +1646 -0
  310. package/ui/src/flows/node-icon/node-icon.ts +34 -0
  311. package/ui/src/flows/node-palette/node-palette.tsx +214 -0
  312. package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
  313. package/ui/src/folder-contents/folder-contents.tsx +321 -0
  314. package/ui/src/folder-index/folder-index.ts +55 -0
  315. package/ui/src/frontmatter/frontmatter.tsx +193 -0
  316. package/ui/src/graph-pane/graph-pane.tsx +53 -0
  317. package/ui/src/hooks/use-mobile.ts +19 -0
  318. package/ui/src/i18n/custom.json +4 -0
  319. package/ui/src/i18n/de.json +609 -0
  320. package/ui/src/i18n/en.json +609 -0
  321. package/ui/src/i18n/es.json +609 -0
  322. package/ui/src/i18n/i18n-context.tsx +65 -0
  323. package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
  324. package/ui/src/i18n/i18n.ts +32 -0
  325. package/ui/src/i18n/i18n.types.ts +49 -0
  326. package/ui/src/kind-icon.ts +97 -0
  327. package/ui/src/lib/utils.ts +6 -0
  328. package/ui/src/main.tsx +68 -0
  329. package/ui/src/modal/modal.tsx +53 -0
  330. package/ui/src/node-details/node-details.tsx +545 -0
  331. package/ui/src/node-editor/node-editor.tsx +304 -0
  332. package/ui/src/node-graph/graph-notice.tsx +21 -0
  333. package/ui/src/node-graph/node-graph.ts +214 -0
  334. package/ui/src/node-graph/node-graph.tsx +295 -0
  335. package/ui/src/node-import/node-import.tsx +418 -0
  336. package/ui/src/node-save-error.ts +46 -0
  337. package/ui/src/node-source/node-source.tsx +128 -0
  338. package/ui/src/node-table/node-table.tsx +107 -0
  339. package/ui/src/nodes/nodes.tsx +240 -0
  340. package/ui/src/rename-resource/rename-resource.ts +80 -0
  341. package/ui/src/resolved-names/resolved-names.tsx +67 -0
  342. package/ui/src/resource-error.ts +33 -0
  343. package/ui/src/resource-menu/resource-menu.tsx +1156 -0
  344. package/ui/src/router/router-context.ts +6 -0
  345. package/ui/src/router/router.tsx +67 -0
  346. package/ui/src/router/router.types.ts +8 -0
  347. package/ui/src/router/selection-search.ts +88 -0
  348. package/ui/src/save-button/save-button.tsx +94 -0
  349. package/ui/src/section-hint/section-hint.tsx +40 -0
  350. package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
  351. package/ui/src/styles.css +189 -0
  352. package/ui/src/table-columns/table-columns.tsx +379 -0
  353. package/ui/src/theme/custom.css +1 -0
  354. package/ui/src/theme/theme-context.tsx +89 -0
  355. package/ui/src/theme/theme.ts +70 -0
  356. package/ui/src/time/relative-time.tsx +41 -0
  357. package/ui/src/time/time-context.tsx +78 -0
  358. package/ui/src/time/time.ts +165 -0
  359. package/ui/src/title-row/title-row.tsx +185 -0
  360. package/ui/src/tools/tools.tsx +546 -0
  361. package/ui/src/user-name/user-name.ts +61 -0
  362. package/ui/src/verbatim/verbatim.tsx +68 -0
  363. package/ui/tsconfig.json +17 -0
  364. package/ui/vite.config.ts +129 -0
@@ -0,0 +1,874 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import { AuditListRequest } from "../contract/audit.js";
4
+ import { BoardAssigneeSearchInput, BoardGetInput, BoardTaskCreateInput, BoardTaskUpdateInput, BoardUpdateInput, DirectoryResolveInput, } from "../contract/board.js";
5
+ import { FeedListRequest } from "../contract/feed.js";
6
+ import { ArchiveFlowInput, CreateFlowInput, GetFlowInput, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, PurgeFlowInput, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput, } from "../contract/flow.js";
7
+ import { CancelFlowRunInput, CompleteFlowRunStepInput, GetFlowRunInput, ListFlowRunsInput, StartFlowRunInput, } from "../contract/flow-run.js";
8
+ import { ArchiveNodeInput, CreateNodeInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, MaxAttachmentBytes, NodeGraphInput, PurgeNodeInput, PurgeNodePreviewInput, ResolveNodeLinksInput, SaveAttachmentBytesInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "../contract/node.js";
9
+ import { RevokeFlowGrantInput, RevokeGrantInput, ShareFlowInput, ShareInput, } from "../contract/share.js";
10
+ import { AppendTableRowsInput, DefineTableInput, DeleteTableRowsInput, RedefineTableInput, UpdateTableRowsInput, } from "../contract/table.js";
11
+ import { ExecuteToolInput, TestToolInput } from "../contract/tool.js";
12
+ import { authorizeBearer, bearer, permits, } from "../shared/gate-authorization/gate-authorization.js";
13
+ import { IntelError } from "../shared/intel-error/intel-error.js";
14
+ import { intelProblem, problemDetails as problem, } from "../shared/problem-details/problem-details.js";
15
+ import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
16
+ // A query string is a door to the outside like a body is, so what arrives through it is closed
17
+ // rather than tolerated: `z.strictObject` refuses an unknown field, and this is that refusal for the
18
+ // half of the input Zod never sees. A silently ignored parameter is how a caller believes it asked
19
+ // for something it did not get — a mistyped `limit=` that quietly draws everything, for instance.
20
+ //
21
+ // ⚠️ Not the same decision as `selectionSearch` in the UI, which deliberately passes unknown search
22
+ // parameters through so the portal's return marker survives a redirect. That is a browser location;
23
+ // this is an API route.
24
+ function requireKnownQuery(url, allowed) {
25
+ const unknown = [];
26
+ url.searchParams.forEach((_value, key) => {
27
+ if (!allowed.includes(key))
28
+ unknown.push(key);
29
+ });
30
+ if (unknown.length) {
31
+ throw new IntelError(400, "invalid_request", `Unknown query parameters: ${unknown.join(", ")}`);
32
+ }
33
+ }
34
+ function asActor(authorization) {
35
+ return {
36
+ id: authorization.identity.id,
37
+ email: authorization.identity.email,
38
+ isAdmin: authorization.can("intel", "admin"),
39
+ };
40
+ }
41
+ // The exporting caller: the node actor plus whether flows may ride along in the bundle at all.
42
+ // Gate's `flows/read` is asked here once, so the bundle service never re-derives a capability.
43
+ function asBundleActor(authorization) {
44
+ return {
45
+ id: authorization.identity.id,
46
+ email: authorization.identity.email,
47
+ isAdmin: authorization.can("intel", "admin"),
48
+ canReadFlows: permits(authorization, "flows", "read"),
49
+ canCreateFlows: permits(authorization, "flows", "create"),
50
+ };
51
+ }
52
+ // The idempotency key of an import travels as a header: the body is the zip itself, so there is no
53
+ // JSON envelope to put it in, and a query string would drag it into logs (#137).
54
+ const ImportIdempotencyKey = z.string().min(8).max(200);
55
+ // A bundle export streamed as a download. `content-length` is deliberately absent: the zip is
56
+ // written while it leaves, which is the whole point of streaming it (#136).
57
+ function zipResponse(bundle) {
58
+ return new Response(bundle.stream, {
59
+ headers: {
60
+ "cache-control": "private, no-store",
61
+ "content-disposition": contentDisposition(bundle.filename),
62
+ "content-type": "application/zip",
63
+ "x-content-type-options": "nosniff",
64
+ },
65
+ });
66
+ }
67
+ function asToolActor(authorization) {
68
+ return {
69
+ id: authorization.identity.id,
70
+ email: authorization.identity.email,
71
+ canExecute: permits(authorization, "tools", "execute"),
72
+ };
73
+ }
74
+ // RFC 5987 ext-value: encodeURIComponent leaves ' * ! ( ) unescaped, and a bare apostrophe is a
75
+ // delimiter that would make the header ambiguous. The ASCII filename stays as a fallback.
76
+ function contentDisposition(title) {
77
+ const encoded = encodeURIComponent(title).replace(/['()*!]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
78
+ const ascii = title.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_");
79
+ return `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`;
80
+ }
81
+ function asFlowActor(authorization) {
82
+ return {
83
+ id: authorization.identity.id,
84
+ email: authorization.identity.email,
85
+ canRun: permits(authorization, "flows", "run"),
86
+ isAdmin: authorization.can("intel", "admin"),
87
+ };
88
+ }
89
+ export function createHttp(deps) {
90
+ const app = new Hono();
91
+ async function authenticated(headers) {
92
+ const token = bearer(headers) ?? (await deps.auth?.resolve(headers))?.bearer ?? null;
93
+ if (!token)
94
+ return null;
95
+ const authorization = await authorizeBearer(deps.gate, token, deps.resource);
96
+ return authorization ? { authorization, token } : null;
97
+ }
98
+ app.onError((error, context) => {
99
+ if (error instanceof IntelError) {
100
+ return context.json(intelProblem(error), error.status);
101
+ }
102
+ if (error instanceof z.ZodError) {
103
+ return context.json(problem(400, "invalid_request", "Request validation failed", z.prettifyError(error)), 400);
104
+ }
105
+ // Same rule as the outer app: an expected refusal explains itself, an unknown exception must
106
+ // leave a trace — otherwise the 500 is a fact without a reason anywhere (#93).
107
+ reportUnexpectedError(error);
108
+ return context.json(problem(500, "internal_error", "Internal server error"), 500);
109
+ });
110
+ app.use("*", async (context, next) => {
111
+ const session = await authenticated(context.req.raw.headers);
112
+ if (!session) {
113
+ context.header("WWW-Authenticate", `Bearer resource_metadata="${deps.resourceMetadataUrl}"`);
114
+ return context.json(problem(401, "authentication_required", "Authentication required"), 401);
115
+ }
116
+ context.set("authorization", session.authorization);
117
+ context.set("token", session.token);
118
+ await next();
119
+ });
120
+ function requireCapability(context, handle, fn) {
121
+ const authorization = context.get("authorization");
122
+ if (!permits(authorization, handle, fn)) {
123
+ throw new IntelError(403, "permission_required", "Permission required");
124
+ }
125
+ return authorization;
126
+ }
127
+ // The caller's own identity, so the shell can name who is signed in. It needs no capability —
128
+ // the answer is the token's own subject — and it returns nothing but id, email, and name.
129
+ app.get("/session", (context) => {
130
+ const authorization = context.get("authorization");
131
+ const { identity } = authorization;
132
+ return context.json({
133
+ id: identity.id,
134
+ email: identity.email,
135
+ name: identity.name ?? null,
136
+ // ⚠️ What the shell may DRAW, never what it may do (#416). Every admin route below still asks
137
+ // `requireCapability` for itself, and a browser that lied here would gain nothing but a
138
+ // button that answers 403.
139
+ isAdmin: authorization.can("intel", "admin"),
140
+ });
141
+ });
142
+ app.get("/nodes", async (context) => {
143
+ const auth = requireCapability(context, "nodes", "read");
144
+ const url = new URL(context.req.url);
145
+ const unknownQuery = [];
146
+ url.searchParams.forEach((_value, key) => {
147
+ if (key !== "parentId" && key !== "includeArchived" && key !== "archivedOnly") {
148
+ unknownQuery.push(key);
149
+ }
150
+ });
151
+ if (unknownQuery.length) {
152
+ throw new IntelError(400, "invalid_request", `Unknown query parameters: ${unknownQuery.join(", ")}`);
153
+ }
154
+ const input = ListNodesInput.parse({
155
+ parentId: url.searchParams.get("parentId"),
156
+ includeArchived: url.searchParams.get("includeArchived") === "true",
157
+ ...(url.searchParams.get("archivedOnly") === "true" ? { archivedOnly: true } : {}),
158
+ });
159
+ return context.json(await deps.nodes.list(asActor(auth), input));
160
+ });
161
+ app.get("/nodes/graph", async (context) => {
162
+ const auth = requireCapability(context, "nodes", "read");
163
+ const url = new URL(context.req.url);
164
+ const input = NodeGraphInput.parse({
165
+ limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
166
+ });
167
+ return context.json(await deps.nodes.graph(asActor(auth), input));
168
+ });
169
+ // ⚠️ Before `/nodes/:nodeId`, or the router would read "export" as a node ID. The root has no
170
+ // node to address, so the whole installation — as this caller may read it — exports here (#136).
171
+ app.get("/nodes/export", async (context) => {
172
+ const auth = requireCapability(context, "nodes", "read");
173
+ return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), null));
174
+ });
175
+ // The reverse door (#137): a zip bundle — or a naked zipped folder — lands as a new subtree.
176
+ // The body IS the zip, streamed to the service rather than read whole here; `knowledge/create`
177
+ // like every other way of making nodes, and the resource-level write check is the service's.
178
+ async function importBundle(context, targetNodeId) {
179
+ const auth = requireCapability(context, "nodes", "create");
180
+ const idempotencyKey = ImportIdempotencyKey.parse(context.req.header("idempotency-key"));
181
+ const body = context.req.raw.body;
182
+ if (body === null) {
183
+ throw new IntelError(400, "invalid_request", "A zip request body is required");
184
+ }
185
+ const result = await deps.bundle.importBundle(asBundleActor(auth), {
186
+ targetNodeId,
187
+ zip: body,
188
+ idempotencyKey,
189
+ });
190
+ // A replay answers 200: nothing was created by THIS request, and the summary is the earlier
191
+ // import's. 201 stays the word for rows that exist because of this call.
192
+ return context.json(result, result.replayed ? 200 : 201);
193
+ }
194
+ // ⚠️ Before `/nodes/:nodeId`, or the router would read "import" as a node ID.
195
+ app.post("/nodes/import", async (context) => await importBundle(context, null));
196
+ app.get("/nodes/:nodeId", async (context) => {
197
+ const auth = requireCapability(context, "nodes", "read");
198
+ return context.json(await deps.nodes.get(asActor(auth), context.req.param("nodeId")));
199
+ });
200
+ app.post("/nodes/:nodeId/import", async (context) => {
201
+ const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
202
+ return await importBundle(context, input.nodeId);
203
+ });
204
+ // One subtree — a folder with everything beneath it, or a single node — as a zip bundle (#136).
205
+ app.get("/nodes/:nodeId/export", async (context) => {
206
+ const auth = requireCapability(context, "nodes", "read");
207
+ const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
208
+ return zipResponse(await deps.bundle.exportSubtree(asBundleActor(auth), input.nodeId));
209
+ });
210
+ // The change journal, read forward from a stable position (#620).
211
+ //
212
+ // ⚠️ **The capability follows the KIND asked for** (#774). An audit row says the same thing its
213
+ // resource says, so the journal asks what reading that resource asks: `nodes/read` for `node`,
214
+ // `flows/read` for `flow` and `flow-run` — which is what every other flow-delivering route in
215
+ // this file requires. Until the enum was widened one capability covered the one kind there was;
216
+ // leaving it at `nodes/read` afterwards would have dropped the first of the three checks for
217
+ // exactly the two kinds this ticket opened, and left the Intel ACL as the only filter.
218
+ //
219
+ // ⚠️ The parse therefore runs BEFORE the capability check, which is the one place in this file
220
+ // where it does. That is not a loosening of "capability first, so a refusal costs no storage
221
+ // read": parsing touches the query string and nothing else, and the kind cannot be known any
222
+ // earlier than this. What it costs is that a malformed `resourceType` answers 400 rather than
223
+ // 403 — a sentence about the caller's own parameter, which names nothing of Intel's.
224
+ //
225
+ // ⚠️ GET with the cursor in the query string, because the verb is one of the four in the grammar
226
+ // and the method carries it. `limit` arrives as a string and is coerced here — the contract
227
+ // wants a number, and a query string has none.
228
+ app.get("/audit", async (context) => {
229
+ const limit = context.req.query("limit");
230
+ const after = context.req.query("after");
231
+ const input = AuditListRequest.parse({
232
+ resourceType: context.req.query("resourceType"),
233
+ ...(after === undefined ? {} : { after }),
234
+ ...(limit === undefined ? {} : { limit: Number(limit) }),
235
+ });
236
+ const auth = requireCapability(context, input.resourceType === "node" ? "nodes" : "flows", "read");
237
+ return context.json(await deps.audit.list(asActor(auth), input));
238
+ });
239
+ // The same journal read the other way round: newest first, for a person rather than for a
240
+ // consumer catching up (#740).
241
+ //
242
+ // ⚠️ `nodes/read` for the whole answer, and here that CANNOT follow the kind the way `/audit`
243
+ // above does: one page mixes node and flow cards on purpose, so there is no single kind to pick a
244
+ // capability from. It has been that way since flow cards existed and #767 only added two actions
245
+ // to them, so nothing about it is new — but it does mean somebody holding `nodes/read` without
246
+ // `flows/read` still sees flow activity here, filtered by the Intel ACL alone. Narrowing it means
247
+ // filtering cards by capability inside the feed service, which is a change to the answer rather
248
+ // than to the door: #790.
249
+ //
250
+ // ⚠️ A SECOND door and not an option on `/audit`. That one is the contract `anchrd/signals` will
251
+ // build on, cursor and direction included; a shared signature would put both readers on one
252
+ // shape, and the next change to it would reach a consumer nobody had in mind.
253
+ app.get("/feed", async (context) => {
254
+ const auth = requireCapability(context, "nodes", "read");
255
+ const limit = context.req.query("limit");
256
+ const before = context.req.query("before");
257
+ const actor = context.req.query("actor");
258
+ const input = FeedListRequest.parse({
259
+ ...(actor === undefined ? {} : { actor }),
260
+ ...(before === undefined ? {} : { before }),
261
+ ...(limit === undefined ? {} : { limit: Number(limit) }),
262
+ });
263
+ return context.json(await deps.feed.list(asActor(auth), input));
264
+ });
265
+ // The whole board in one answer (#648). `nodes/read` and nothing new: a board is a node, and a
266
+ // card is a node — a second capability would be a second answer to one question.
267
+ //
268
+ // ⚠️ Every filter is a query parameter and every one of them reaches the WHERE. Filtering after
269
+ // the read would have carried other people's cards over the wire first.
270
+ app.get("/boards/:boardId", async (context) => {
271
+ const auth = requireCapability(context, "nodes", "read");
272
+ const query = context.req.query();
273
+ const input = BoardGetInput.parse({
274
+ boardId: context.req.param("boardId"),
275
+ ...(query.status === undefined ? {} : { status: query.status }),
276
+ ...(query.assigneeId === undefined ? {} : { assigneeId: query.assigneeId }),
277
+ ...(query.dueBefore === undefined ? {} : { dueBefore: query.dueBefore }),
278
+ ...(query.dependsOn === undefined ? {} : { dependsOn: query.dependsOn }),
279
+ ...(query.includeArchived === undefined
280
+ ? {}
281
+ : { includeArchived: query.includeArchived === "true" }),
282
+ });
283
+ return context.json(await deps.boards.get(asActor(auth), input));
284
+ });
285
+ /**
286
+ * Who this card may be given to (#700, D70).
287
+ *
288
+ * ⚠️ `nodes/read`, not a capability of its own and certainly not `nodes/write`. Offering an
289
+ * assignee is something everyone who can open the board does; a separate permission would be one
290
+ * more thing to grant before a picker works, and nothing would be safer for it — the answer is
291
+ * already narrower than what gate hands any signed-in person at its own door.
292
+ */
293
+ app.get("/boards/:boardId/assignees", async (context) => {
294
+ const auth = requireCapability(context, "nodes", "read");
295
+ const input = BoardAssigneeSearchInput.parse({
296
+ boardId: context.req.param("boardId"),
297
+ query: context.req.query("q") ?? "",
298
+ });
299
+ return context.json(await deps.boards.searchAssignees(asActor(auth), input));
300
+ });
301
+ /**
302
+ * What the people already on cards are called (#258, #700).
303
+ *
304
+ * ⚠️ A POST although it reads, and for the same reason gate's own door is one: the ids travel in
305
+ * the body. A list of a hundred identifiers in a query string lands in proxy logs and in browser
306
+ * history, and it would hit the length limit long before the hundredth.
307
+ */
308
+ app.post("/directory/resolve", async (context) => {
309
+ const auth = requireCapability(context, "nodes", "read");
310
+ const input = DirectoryResolveInput.parse(await context.req.json().catch(() => null));
311
+ return context.json(await deps.boards.resolveNames(asActor(auth), input));
312
+ });
313
+ app.patch("/boards/:boardId", async (context) => {
314
+ const auth = requireCapability(context, "nodes", "write");
315
+ const input = BoardUpdateInput.parse(await context.req.json().catch(() => null));
316
+ if (input.boardId !== context.req.param("boardId")) {
317
+ throw new IntelError(400, "board_id_mismatch", "Path and body board IDs differ");
318
+ }
319
+ return context.json(await deps.boards.update(asActor(auth), input));
320
+ });
321
+ app.post("/boards/:boardId/tasks", async (context) => {
322
+ const auth = requireCapability(context, "nodes", "create");
323
+ const input = BoardTaskCreateInput.parse(await context.req.json().catch(() => null));
324
+ if (input.boardId !== context.req.param("boardId")) {
325
+ throw new IntelError(400, "board_id_mismatch", "Path and body board IDs differ");
326
+ }
327
+ return context.json(await deps.boards.createTask(asActor(auth), input), 201);
328
+ });
329
+ // ⚠️ `PATCH` on the task, not `POST …/move`. Moving a card IS an update of `status` and
330
+ // `position`; a second address for it would be a second way to write one row.
331
+ app.patch("/boards/:boardId/tasks/:taskId", async (context) => {
332
+ const auth = requireCapability(context, "nodes", "write");
333
+ const input = BoardTaskUpdateInput.parse(await context.req.json().catch(() => null));
334
+ if (input.taskId !== context.req.param("taskId")) {
335
+ throw new IntelError(400, "task_id_mismatch", "Path and body task IDs differ");
336
+ }
337
+ return context.json(await deps.boards.updateTask(asActor(auth), input));
338
+ });
339
+ app.get("/nodes/:nodeId/versions", async (context) => {
340
+ const auth = requireCapability(context, "nodes", "read");
341
+ return context.json(await deps.nodes.listVersions(asActor(auth), context.req.param("nodeId")));
342
+ });
343
+ // The content behind one row of the list above (#147): what a version-pinned citation points at.
344
+ // Read capability like every other node read; the resource ACL and the node/version pairing are
345
+ // the service's to refuse.
346
+ app.get("/nodes/:nodeId/versions/:versionId/content", async (context) => {
347
+ const auth = requireCapability(context, "nodes", "read");
348
+ const input = GetNodeVersionInput.parse({
349
+ nodeId: context.req.param("nodeId"),
350
+ versionId: context.req.param("versionId"),
351
+ });
352
+ return context.json(await deps.nodes.getVersion(asActor(auth), input.nodeId, input.versionId));
353
+ });
354
+ // The metadata half of an attachment (#148): what `node_attachment_get` answers an agent, for
355
+ // HTTP callers who so far could only stream the bytes. Without the `resourceUri` — an MCP
356
+ // resource address says nothing to an HTTP client, whose address for the bytes is the sibling
357
+ // route below.
358
+ app.get("/nodes/:nodeId/attachment/meta", async (context) => {
359
+ const auth = requireCapability(context, "nodes", "read");
360
+ const input = GetNodeInput.parse({ nodeId: context.req.param("nodeId") });
361
+ const { node, version } = await deps.nodes.getAttachment(asActor(auth), input.nodeId);
362
+ return context.json({ node, version });
363
+ });
364
+ app.get("/nodes/:nodeId/attachment", async (context) => {
365
+ const auth = requireCapability(context, "nodes", "read");
366
+ const { attachment, body } = await deps.nodes.readAttachment(asActor(auth), context.req.param("nodeId"));
367
+ return new Response(body, {
368
+ headers: {
369
+ "cache-control": "private, no-store",
370
+ "content-disposition": contentDisposition(attachment.node.title),
371
+ "content-type": attachment.version.mediaType,
372
+ "x-content-type-options": "nosniff",
373
+ },
374
+ });
375
+ });
376
+ app.get("/nodes/:nodeId/table", async (context) => {
377
+ const auth = requireCapability(context, "nodes", "read");
378
+ return context.json(await deps.nodes.getTable(asActor(auth), context.req.param("nodeId")));
379
+ });
380
+ // Defining the header and appending rows are two routes because they are two decisions: the
381
+ // header is written once and is the contract, an append is the everyday write (#40).
382
+ app.post("/nodes/:nodeId/table", async (context) => {
383
+ const auth = requireCapability(context, "nodes", "write");
384
+ const input = DefineTableInput.parse(await context.req.json().catch(() => null));
385
+ if (input.nodeId !== context.req.param("nodeId")) {
386
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
387
+ }
388
+ return context.json(await deps.nodes.defineTable(asActor(auth), input), 201);
389
+ });
390
+ app.post("/nodes/:nodeId/table/rows", async (context) => {
391
+ const auth = requireCapability(context, "nodes", "write");
392
+ const input = AppendTableRowsInput.parse(await context.req.json().catch(() => null));
393
+ if (input.nodeId !== context.req.param("nodeId")) {
394
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
395
+ }
396
+ return context.json(await deps.nodes.appendTableRows(asActor(auth), input), 201);
397
+ });
398
+ // The three mutations beside the append (#135). Action-style POSTs like the archive and revoke
399
+ // routes, because a row has no address of its own to PUT to: positions live in the body, next to
400
+ // the `baseVersionId` that says which state they mean. Each writes one snapshot version, so the
401
+ // 201 is the same statement the append's 201 makes.
402
+ app.post("/nodes/:nodeId/table/rows/update", async (context) => {
403
+ const auth = requireCapability(context, "nodes", "write");
404
+ const input = UpdateTableRowsInput.parse(await context.req.json().catch(() => null));
405
+ if (input.nodeId !== context.req.param("nodeId")) {
406
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
407
+ }
408
+ return context.json(await deps.nodes.updateTableRows(asActor(auth), input), 201);
409
+ });
410
+ app.post("/nodes/:nodeId/table/rows/delete", async (context) => {
411
+ const auth = requireCapability(context, "nodes", "write");
412
+ const input = DeleteTableRowsInput.parse(await context.req.json().catch(() => null));
413
+ if (input.nodeId !== context.req.param("nodeId")) {
414
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
415
+ }
416
+ return context.json(await deps.nodes.deleteTableRows(asActor(auth), input), 201);
417
+ });
418
+ app.patch("/nodes/:nodeId/table", async (context) => {
419
+ const auth = requireCapability(context, "nodes", "write");
420
+ const input = RedefineTableInput.parse(await context.req.json().catch(() => null));
421
+ if (input.nodeId !== context.req.param("nodeId")) {
422
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
423
+ }
424
+ return context.json(await deps.nodes.redefineTable(asActor(auth), input), 201);
425
+ });
426
+ // ⚠️ There is deliberately no route for the sixth board operation. `board_repair_task_ids`
427
+ // (anchrd/intel#341) is MCP-only until a screen calls it: the routes here are what the browser
428
+ // reaches, nothing in `intel-data-provider` asks for it, and a route with no caller is the thing
429
+ // YAGNI is about. The screen is anchrd/intel#358, and it brings this route with it.
430
+ app.get("/nodes/:nodeId/links", async (context) => {
431
+ const auth = requireCapability(context, "nodes", "read");
432
+ return context.json(await deps.nodes.listLinks(asActor(auth), context.req.param("nodeId")));
433
+ });
434
+ // A document link's name, for whoever is reading the text (#41). A POST because the list of IDs
435
+ // is the request body — putting them in a query string would put the tree's identifiers into
436
+ // logs and referrers. The `knowledge/read` capability and nothing more: it answers with what this reader may
437
+ // already see, and stays silent about the rest rather than saying that there is a rest.
438
+ //
439
+ // There is no route to create or delete a link any more. A relationship is written where it is
440
+ // meant, in the text, and `POST /nodes/:nodeId/versions` is what records it.
441
+ app.post("/nodes/links/resolve", async (context) => {
442
+ const auth = requireCapability(context, "nodes", "read");
443
+ const input = ResolveNodeLinksInput.parse(await context.req.json().catch(() => null));
444
+ return context.json(await deps.nodes.resolveLinks(asActor(auth), input));
445
+ });
446
+ app.post("/nodes/search", async (context) => {
447
+ const auth = requireCapability(context, "nodes", "read");
448
+ const input = SearchInput.parse(await context.req.json().catch(() => null));
449
+ return context.json(await deps.nodes.search(asActor(auth), input));
450
+ });
451
+ app.post("/nodes/reindex", async (context) => {
452
+ const auth = requireCapability(context, "intel", "admin");
453
+ z.strictObject({}).parse(await context.req.json().catch(() => null));
454
+ return context.json(await deps.nodes.reindex(asActor(auth)), 202);
455
+ });
456
+ app.post("/nodes", async (context) => {
457
+ const auth = requireCapability(context, "nodes", "create");
458
+ const input = CreateNodeInput.parse(await context.req.json().catch(() => null));
459
+ return context.json(await deps.nodes.create(asActor(auth), input), 201);
460
+ });
461
+ app.post("/nodes/:nodeId/versions", async (context) => {
462
+ const auth = requireCapability(context, "nodes", "write");
463
+ const input = SaveNodeVersionInput.parse(await context.req.json().catch(() => null));
464
+ if (input.nodeId !== context.req.param("nodeId")) {
465
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
466
+ }
467
+ return context.json(await deps.nodes.save(asActor(auth), input), 201);
468
+ });
469
+ /**
470
+ * The streaming door for a file (#821): the body IS the bytes, and everything about them travels
471
+ * in the headers the request already has.
472
+ *
473
+ * ⚠️ **It has no MCP twin on purpose**, and it is the one capability in this file that does not.
474
+ * MCP carries JSON, so a tool cannot hand over a body; `node_attachment_create` remains the MCP
475
+ * form of the same act, with the base64 size limit that door brings with it. What must not happen
476
+ * is a second set of RULES here — the checks are the service's, and both doors call the same one.
477
+ *
478
+ * ⚠️ `intel-base-version-id` carries what `baseVersionId` carries in the JSON door, and the word
479
+ * `none` is how a first upload says "there is no version yet". An absent header is refused rather
480
+ * than read as `none`: a caller who merely forgot it would otherwise overwrite the newest version
481
+ * instead of being told that somebody else wrote first — which is the whole point of the field.
482
+ *
483
+ * ⚠️ The length is refused BEFORE the body is read, from `content-length`. A file over the limit
484
+ * has to end in a sentence the reader gets, and a request that has already been read to its end
485
+ * cannot be answered with one on every client.
486
+ */
487
+ app.post("/nodes/:nodeId/attachment/bytes", async (context) => {
488
+ const auth = requireCapability(context, "nodes", "write");
489
+ const baseVersionHeader = context.req.header("intel-base-version-id");
490
+ if (baseVersionHeader === undefined) {
491
+ throw new IntelError(400, "invalid_request", 'intel-base-version-id takes the current version id, or "none" for the first upload');
492
+ }
493
+ const declared = Number(context.req.header("content-length"));
494
+ if (!Number.isSafeInteger(declared) || declared <= 0) {
495
+ throw new IntelError(411, "length_required", "An upload has to announce its length in content-length");
496
+ }
497
+ if (declared > MaxAttachmentBytes) {
498
+ throw new IntelError(413, "attachment_too_large", `An attachment may carry at most ${MaxAttachmentBytes} bytes`);
499
+ }
500
+ const input = SaveAttachmentBytesInput.parse({
501
+ nodeId: context.req.param("nodeId"),
502
+ baseVersionId: baseVersionHeader === "none" ? null : baseVersionHeader,
503
+ mediaType: context.req.header("content-type") ?? "",
504
+ size: declared,
505
+ idempotencyKey: ImportIdempotencyKey.parse(context.req.header("idempotency-key")),
506
+ });
507
+ const body = context.req.raw.body;
508
+ if (body === null) {
509
+ throw new IntelError(400, "invalid_request", "A request body is required");
510
+ }
511
+ return context.json(await deps.nodes.saveAttachmentBytes(asActor(auth), input, body), 201);
512
+ });
513
+ app.post("/nodes/:nodeId/attachment", async (context) => {
514
+ const auth = requireCapability(context, "nodes", "write");
515
+ const input = SaveAttachmentInput.parse(await context.req.json().catch(() => null));
516
+ if (input.nodeId !== context.req.param("nodeId")) {
517
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
518
+ }
519
+ return context.json(await deps.nodes.saveAttachment(asActor(auth), input), 201);
520
+ });
521
+ app.patch("/nodes/:nodeId", async (context) => {
522
+ const auth = requireCapability(context, "nodes", "write");
523
+ const input = UpdateNodeInput.parse(await context.req.json().catch(() => null));
524
+ if (input.nodeId !== context.req.param("nodeId")) {
525
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
526
+ }
527
+ return context.json(await deps.nodes.update(asActor(auth), input));
528
+ });
529
+ app.post("/nodes/:nodeId/archive", async (context) => {
530
+ const auth = requireCapability(context, "nodes", "write");
531
+ const input = ArchiveNodeInput.parse(await context.req.json().catch(() => null));
532
+ if (input.nodeId !== context.req.param("nodeId")) {
533
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
534
+ }
535
+ // The caller's own bearer, for the one thing archiving may have to do in Gate: switch off the
536
+ // Application behind an agent (#182). Every other kind never reaches it.
537
+ return context.json(await deps.nodes.archive(asActor(auth), input));
538
+ });
539
+ // ⚠️ DELETE, and it means it (#457). Every other "delete" in this surface is `archived_at`; this
540
+ // one leaves nothing behind. The same capability as archiving — `nodes.write` — because the
541
+ // structural guard is elsewhere: only an archived node can be purged, so the destructive way has
542
+ // two steps and the first one is reversible.
543
+ app.delete("/nodes/:nodeId", async (context) => {
544
+ const auth = requireCapability(context, "nodes", "write");
545
+ const input = PurgeNodeInput.parse(await context.req.json().catch(() => null));
546
+ if (input.nodeId !== context.req.param("nodeId")) {
547
+ throw new IntelError(400, "node_id_mismatch", "Path and body node IDs differ");
548
+ }
549
+ return context.json(await deps.nodes.purge(asActor(auth), input));
550
+ });
551
+ app.get("/nodes/:nodeId/purge-preview", async (context) => {
552
+ const auth = requireCapability(context, "nodes", "write");
553
+ const input = PurgeNodePreviewInput.parse({ nodeId: context.req.param("nodeId") });
554
+ return context.json(await deps.nodes.purgePreview(asActor(auth), input));
555
+ });
556
+ app.get("/nodes/:nodeId/grants", async (context) => {
557
+ const auth = requireCapability(context, "nodes", "share");
558
+ return context.json(await deps.nodes.listGrants(asActor(auth), context.req.param("nodeId")));
559
+ });
560
+ app.get("/nodes/:nodeId/effective-access", async (context) => {
561
+ const auth = requireCapability(context, "nodes", "share");
562
+ return context.json(await deps.nodes.listEffectiveAccess(asActor(auth), context.req.param("nodeId")));
563
+ });
564
+ app.post("/nodes/:nodeId/grants", async (context) => {
565
+ const auth = requireCapability(context, "nodes", "share");
566
+ const input = ShareInput.parse(await context.req.json().catch(() => null));
567
+ if (input.resourceId !== context.req.param("nodeId")) {
568
+ throw new IntelError(400, "node_id_mismatch", "Path and body resource IDs differ");
569
+ }
570
+ return context.json(await deps.nodes.share(asActor(auth), input), 201);
571
+ });
572
+ app.post("/nodes/:nodeId/grants/:grantId/revoke", async (context) => {
573
+ const auth = requireCapability(context, "nodes", "share");
574
+ const input = RevokeGrantInput.parse(await context.req.json().catch(() => null));
575
+ if (input.resourceId !== context.req.param("nodeId") ||
576
+ input.grantId !== context.req.param("grantId")) {
577
+ throw new IntelError(400, "grant_id_mismatch", "Path and body grant IDs differ");
578
+ }
579
+ return context.json(await deps.nodes.revokeGrant(asActor(auth), input));
580
+ });
581
+ app.get("/flows", async (context) => {
582
+ const auth = requireCapability(context, "flows", "read");
583
+ const url = new URL(context.req.url);
584
+ requireKnownQuery(url, ["parentId", "includeArchived", "archivedOnly"]);
585
+ // Absent means "every flow I may see"; present but empty means the root of the shared tree.
586
+ // Without that distinction a tree level and a full list would be the same request.
587
+ const parentId = url.searchParams.get("parentId");
588
+ // Same rule for the archive: absent means "without it". Spelling the default out would put a
589
+ // field into every call that asks nothing about the archive.
590
+ const input = ListFlowsInput.parse({
591
+ ...(url.searchParams.has("parentId") ? { parentId: parentId === "" ? null : parentId } : {}),
592
+ ...(url.searchParams.get("includeArchived") === "true" ? { includeArchived: true } : {}),
593
+ ...(url.searchParams.get("archivedOnly") === "true" ? { archivedOnly: true } : {}),
594
+ });
595
+ return context.json(await deps.flows.list(asFlowActor(auth), input));
596
+ });
597
+ // ⚠️ Before `/flows/:flowId`, or the router would read "graph" as a flow ID. Same order and same
598
+ // reason as `/nodes/graph`.
599
+ app.get("/flows/graph", async (context) => {
600
+ const auth = requireCapability(context, "flows", "read");
601
+ const url = new URL(context.req.url);
602
+ requireKnownQuery(url, ["of", "folderId", "flowId", "limit"]);
603
+ const of = url.searchParams.get("of");
604
+ const input = RelationGraphInput.parse({
605
+ scope: of === "flow"
606
+ ? { of: "flow", flowId: url.searchParams.get("flowId") ?? "" }
607
+ : { of: "folder", folderId: url.searchParams.get("folderId") || null },
608
+ limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
609
+ });
610
+ return context.json(await deps.flows.relationGraph(asFlowActor(auth), input));
611
+ });
612
+ // A path variable is a door like any other, so it is parsed rather than trusted: `GetFlowInput`
613
+ // is the same schema the body-carrying routes and the MCP tools use for this identifier.
614
+ app.get("/flows/:flowId", async (context) => {
615
+ const auth = requireCapability(context, "flows", "read");
616
+ const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
617
+ return context.json(await deps.flows.get(asFlowActor(auth), input.flowId));
618
+ });
619
+ /**
620
+ * A single flow as the same bundle shape a node export makes: one entry plus the manifest
621
+ * (#136). `flows/read` like every other read of the graph.
622
+ *
623
+ * ⚠️ Deliberately no `flow_export` tool beside it, and the reason is NOT the one `node_export`
624
+ * carries (#436). There the memory argument decides — a subtree's zip would have to fit in Worker
625
+ * memory twice — and a flow bundle is one entry plus the manifest, so that argument does not
626
+ * reach here.
627
+ *
628
+ * What decides here is that the manifest of ONE entry answers nothing MCP does not already
629
+ * answer. `node_export` is worth a tool because its manifest is the PLAN of a subtree: every
630
+ * readable entry with its path, kind and media type, which is the map for fetching each one. For
631
+ * a single flow that map has one row, whose id the caller already holds — and the substance, the
632
+ * graph, is `flow_version_get` and `flow_graph`, with the record itself in `flow_get`. A tool
633
+ * whose whole answer is two other tools' answers costs bytes in every `tools/list` and makes the
634
+ * other names less reliable as a guide, which `packages/intel/CLAUDE.md` treats as the cost it is.
635
+ *
636
+ * ⚠️ This is not the case `.claude/rules/destructive.md` warns about — "a way only the surface
637
+ * has is a way an agent works around". Nothing about a flow is unreachable over MCP; only the zip
638
+ * CONTAINER is HTTP-only, exactly as it is for nodes. `mcp.e2e.ts` pins that this exception is
639
+ * named rather than merely absent.
640
+ */
641
+ app.get("/flows/:flowId/export", async (context) => {
642
+ const auth = requireCapability(context, "flows", "read");
643
+ const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
644
+ return zipResponse(await deps.bundle.exportFlow(asBundleActor(auth), input.flowId));
645
+ });
646
+ // What this flow calls, read out of its graph. The tree lists where a flow is filed; this lists
647
+ // what it runs, and a shared subflow answers both questions at once (ADR-0004 §3).
648
+ app.get("/flows/:flowId/calls", async (context) => {
649
+ const auth = requireCapability(context, "flows", "read");
650
+ const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
651
+ return context.json(await deps.flows.listCalls(asFlowActor(auth), input.flowId));
652
+ });
653
+ // Would this flow start, for whoever is asking, right now (#72). `flows/read` and nothing more:
654
+ // the answer somebody without `execute` needs most is that they are missing `execute`, and it
655
+ // names nothing they could not read out of the graph. A GET because it changes nothing — no run,
656
+ // no key, no audit event.
657
+ app.get("/flows/:flowId/validation", async (context) => {
658
+ const auth = requireCapability(context, "flows", "read");
659
+ const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
660
+ return context.json(await deps.flows.validate(asFlowActor(auth), input.flowId));
661
+ });
662
+ // What this flow needs: the documents and tools its graph names. `flows/read` and nothing more —
663
+ // it describes a flow the caller may already open, and names only what they may already see.
664
+ app.get("/flows/:flowId/requirements", async (context) => {
665
+ const auth = requireCapability(context, "flows", "read");
666
+ const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
667
+ return context.json(await deps.flows.listRequirements(asFlowActor(auth), input.flowId));
668
+ });
669
+ // The flow's own sharing (#530), segment for segment the node routes one screen up. `flows/share`
670
+ // was declared to Gate from the beginning and had no consumer until now; these four are it.
671
+ app.get("/flows/:flowId/grants", async (context) => {
672
+ const auth = requireCapability(context, "flows", "share");
673
+ return context.json(await deps.flows.listGrants(asFlowActor(auth), context.req.param("flowId")));
674
+ });
675
+ app.get("/flows/:flowId/effective-access", async (context) => {
676
+ const auth = requireCapability(context, "flows", "share");
677
+ return context.json(await deps.flows.listEffectiveAccess(asFlowActor(auth), context.req.param("flowId")));
678
+ });
679
+ app.post("/flows/:flowId/grants", async (context) => {
680
+ const auth = requireCapability(context, "flows", "share");
681
+ const input = ShareFlowInput.parse(await context.req.json().catch(() => null));
682
+ if (input.flowId !== context.req.param("flowId")) {
683
+ throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
684
+ }
685
+ return context.json(await deps.flows.share(asFlowActor(auth), input), 201);
686
+ });
687
+ app.post("/flows/:flowId/grants/:grantId/revoke", async (context) => {
688
+ const auth = requireCapability(context, "flows", "share");
689
+ const input = RevokeFlowGrantInput.parse(await context.req.json().catch(() => null));
690
+ if (input.flowId !== context.req.param("flowId") ||
691
+ input.grantId !== context.req.param("grantId")) {
692
+ throw new IntelError(400, "grant_id_mismatch", "Path and body grant IDs differ");
693
+ }
694
+ return context.json(await deps.flows.revokeGrant(asFlowActor(auth), input));
695
+ });
696
+ app.post("/flows", async (context) => {
697
+ const auth = requireCapability(context, "flows", "create");
698
+ const input = CreateFlowInput.parse(await context.req.json().catch(() => null));
699
+ return context.json(await deps.flows.create(asFlowActor(auth), input), 201);
700
+ });
701
+ app.patch("/flows/:flowId", async (context) => {
702
+ const auth = requireCapability(context, "flows", "write");
703
+ const input = UpdateFlowInput.parse(await context.req.json().catch(() => null));
704
+ if (input.flowId !== context.req.param("flowId")) {
705
+ throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
706
+ }
707
+ return context.json(await deps.flows.update(asFlowActor(auth), input));
708
+ });
709
+ // `flows/write`, the same capability renaming and moving need: archiving is organization, not a
710
+ // change to what the flow does.
711
+ app.post("/flows/:flowId/archive", async (context) => {
712
+ const auth = requireCapability(context, "flows", "write");
713
+ const input = ArchiveFlowInput.parse(await context.req.json().catch(() => null));
714
+ if (input.flowId !== context.req.param("flowId")) {
715
+ throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
716
+ }
717
+ return context.json(await deps.flows.archive(asFlowActor(auth), input));
718
+ });
719
+ // ⚠️ DELETE, and it means it (#457) — the flow's versions and the record of every run it had.
720
+ app.delete("/flows/:flowId", async (context) => {
721
+ const auth = requireCapability(context, "flows", "write");
722
+ const input = PurgeFlowInput.parse({ flowId: context.req.param("flowId") });
723
+ return context.json(await deps.flows.purge(asFlowActor(auth), input));
724
+ });
725
+ app.post("/flows/:flowId/versions", async (context) => {
726
+ const auth = requireCapability(context, "flows", "write");
727
+ const input = SaveFlowVersionInput.parse(await context.req.json().catch(() => null));
728
+ if (input.flowId !== context.req.param("flowId")) {
729
+ throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
730
+ }
731
+ return context.json(await deps.flows.save(asFlowActor(auth), input), 201);
732
+ });
733
+ // The GET beside `POST /flows/:flowId/versions`: the same collection, read instead of appended
734
+ // to (#144). `flows/read` like `GET /flows/:flowId` — the history describes a flow the caller
735
+ // may already open.
736
+ app.get("/flows/:flowId/versions", async (context) => {
737
+ const auth = requireCapability(context, "flows", "read");
738
+ const input = GetFlowInput.parse({ flowId: context.req.param("flowId") });
739
+ return context.json(await deps.flows.listVersions(asFlowActor(auth), input.flowId));
740
+ });
741
+ app.get("/flows/:flowId/versions/:versionId", async (context) => {
742
+ const auth = requireCapability(context, "flows", "read");
743
+ const input = GetFlowVersionInput.parse({
744
+ flowId: context.req.param("flowId"),
745
+ versionId: context.req.param("versionId"),
746
+ });
747
+ return context.json(await deps.flows.getVersion(asFlowActor(auth), input));
748
+ });
749
+ // What publishing would freeze, before it is published (ADR-0004 §5). A GET because it reads and
750
+ // changes nothing; the `publish` capability because it answers a question only a publisher has.
751
+ app.get("/flows/:flowId/versions/:versionId/publish-preview", async (context) => {
752
+ const auth = requireCapability(context, "flows", "publish");
753
+ const input = PreviewFlowPublishInput.parse({
754
+ flowId: context.req.param("flowId"),
755
+ versionId: context.req.param("versionId"),
756
+ });
757
+ return context.json(await deps.flows.previewPublish(asFlowActor(auth), input));
758
+ });
759
+ app.post("/flows/:flowId/publish", async (context) => {
760
+ const auth = requireCapability(context, "flows", "publish");
761
+ const input = PublishFlowInput.parse(await context.req.json().catch(() => null));
762
+ if (input.flowId !== context.req.param("flowId")) {
763
+ throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
764
+ }
765
+ return context.json(await deps.flows.publish(asFlowActor(auth), input));
766
+ });
767
+ // The reverse of publish, behind the same capability: whoever may publish may withdraw (#146).
768
+ app.post("/flows/:flowId/unpublish", async (context) => {
769
+ const auth = requireCapability(context, "flows", "publish");
770
+ const input = UnpublishFlowInput.parse(await context.req.json().catch(() => null));
771
+ if (input.flowId !== context.req.param("flowId")) {
772
+ throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
773
+ }
774
+ return context.json(await deps.flows.unpublish(asFlowActor(auth), input));
775
+ });
776
+ app.post("/flows/:flowId/runs", async (context) => {
777
+ const auth = requireCapability(context, "flows", "run");
778
+ const input = StartFlowRunInput.parse(await context.req.json().catch(() => null));
779
+ if (input.flowId !== context.req.param("flowId")) {
780
+ throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
781
+ }
782
+ return context.json(await deps.flows.start(asFlowActor(auth), input), 201);
783
+ });
784
+ // What this flow has done (#35). A GET beside `POST /flows/:flowId/runs`: the same collection,
785
+ // read instead of appended to. `flows/read` is the capability, and which of the runs actually
786
+ // appear is the resource rule the service applies per row.
787
+ app.get("/flows/:flowId/runs", async (context) => {
788
+ const auth = requireCapability(context, "flows", "read");
789
+ const url = new URL(context.req.url);
790
+ requireKnownQuery(url, ["failedOnly", "limit", "cursor"]);
791
+ const input = ListFlowRunsInput.parse({
792
+ flowId: context.req.param("flowId"),
793
+ // Present means "only the failed ones". A query string carries no booleans, so the parameter's
794
+ // presence is the flag rather than a string that would have to be spelled exactly right.
795
+ ...(url.searchParams.has("failedOnly") ? { failedOnly: true } : {}),
796
+ ...(url.searchParams.has("limit") ? { limit: Number(url.searchParams.get("limit")) } : {}),
797
+ ...(url.searchParams.has("cursor") ? { cursor: url.searchParams.get("cursor") } : {}),
798
+ });
799
+ return context.json(await deps.flows.listRuns(asFlowActor(auth), input));
800
+ });
801
+ /**
802
+ * The same list across every flow (#774) — "did anything run", which used to take one call per
803
+ * flow and therefore went unasked.
804
+ *
805
+ * ⚠️ It sits at the root beside the other three run routes rather than under a flow, and the
806
+ * reason is the one already written for them in `packages/intel/CLAUDE.md`: reading, cancelling and
807
+ * completing a run do not need the flow, and a run outlives it. Naming no flow is exactly that
808
+ * case. One tool answers both addresses — `flow_run_list` with `flowId: null` — which is the same
809
+ * many-to-one the attachment routes have.
810
+ */
811
+ app.get("/flow-runs", async (context) => {
812
+ const auth = requireCapability(context, "flows", "read");
813
+ const url = new URL(context.req.url);
814
+ requireKnownQuery(url, ["failedOnly", "limit", "cursor"]);
815
+ const input = ListFlowRunsInput.parse({
816
+ flowId: null,
817
+ ...(url.searchParams.has("failedOnly") ? { failedOnly: true } : {}),
818
+ ...(url.searchParams.has("limit") ? { limit: Number(url.searchParams.get("limit")) } : {}),
819
+ ...(url.searchParams.has("cursor") ? { cursor: url.searchParams.get("cursor") } : {}),
820
+ });
821
+ return context.json(await deps.flows.listRuns(asFlowActor(auth), input));
822
+ });
823
+ app.get("/flow-runs/:runId", async (context) => {
824
+ const auth = requireCapability(context, "flows", "read");
825
+ const input = GetFlowRunInput.parse({ runId: context.req.param("runId") });
826
+ return context.json(await deps.flows.getRun(asFlowActor(auth), input.runId));
827
+ });
828
+ // The drill-down behind the list: which step of this run ended it, and what it said.
829
+ app.get("/flow-runs/:runId/steps", async (context) => {
830
+ const auth = requireCapability(context, "flows", "read");
831
+ const input = GetFlowRunInput.parse({ runId: context.req.param("runId") });
832
+ return context.json(await deps.flows.listRunSteps(asFlowActor(auth), input.runId));
833
+ });
834
+ app.post("/flow-runs/:runId/steps/complete", async (context) => {
835
+ const auth = requireCapability(context, "flows", "run");
836
+ const input = CompleteFlowRunStepInput.parse(await context.req.json().catch(() => null));
837
+ if (input.runId !== context.req.param("runId")) {
838
+ throw new IntelError(400, "run_id_mismatch", "Path and body run IDs differ");
839
+ }
840
+ return context.json(await deps.flows.completeStep(asFlowActor(auth), input));
841
+ });
842
+ // `flows/run`, the same capability the start asked for: whoever may start a run may end it
843
+ // (#145). The resource-level `execute` is the service's question, asked against the flow.
844
+ app.post("/flow-runs/:runId/cancel", async (context) => {
845
+ const auth = requireCapability(context, "flows", "run");
846
+ const input = CancelFlowRunInput.parse(await context.req.json().catch(() => null));
847
+ if (input.runId !== context.req.param("runId")) {
848
+ throw new IntelError(400, "run_id_mismatch", "Path and body run IDs differ");
849
+ }
850
+ return context.json(await deps.flows.cancelRun(asFlowActor(auth), input));
851
+ });
852
+ app.get("/tools", async (context) => {
853
+ const auth = requireCapability(context, "tools", "read");
854
+ return context.json(await deps.tools.catalog(asToolActor(auth)));
855
+ });
856
+ // ⚠️ Registered before `/tools/test` and `/tools/execute` and behind `tools/read`, not a new
857
+ // capability: it answers strictly less than `/tools` does — the servers behind the same live
858
+ // list, with no schemas (D30).
859
+ app.get("/tools/servers", async (context) => {
860
+ const auth = requireCapability(context, "tools", "read");
861
+ return context.json(await deps.tools.servers(asToolActor(auth)));
862
+ });
863
+ app.post("/tools/test", async (context) => {
864
+ const auth = requireCapability(context, "tools", "test");
865
+ const input = TestToolInput.parse(await context.req.json().catch(() => null));
866
+ return context.json(await deps.tools.test(asToolActor(auth), input));
867
+ });
868
+ app.post("/tools/execute", async (context) => {
869
+ const auth = requireCapability(context, "tools", "execute");
870
+ const input = ExecuteToolInput.parse(await context.req.json().catch(() => null));
871
+ return context.json(await deps.tools.execute(asToolActor(auth), input));
872
+ });
873
+ return app;
874
+ }