@edspencer/paddock 0.0.1 → 0.56.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 (294) hide show
  1. package/README.md +69 -0
  2. package/install-notice.mjs +29 -0
  3. package/package.json +58 -5
  4. package/packages/server/dist/adoptable.d.ts +127 -0
  5. package/packages/server/dist/adoptable.js +315 -0
  6. package/packages/server/dist/app.d.ts +67 -0
  7. package/packages/server/dist/app.js +325 -0
  8. package/packages/server/dist/archive.d.ts +42 -0
  9. package/packages/server/dist/archive.js +125 -0
  10. package/packages/server/dist/attachments-config.d.ts +114 -0
  11. package/packages/server/dist/attachments-config.js +165 -0
  12. package/packages/server/dist/attachments-hint.d.ts +53 -0
  13. package/packages/server/dist/attachments-hint.js +140 -0
  14. package/packages/server/dist/attachments.d.ts +33 -0
  15. package/packages/server/dist/attachments.js +117 -0
  16. package/packages/server/dist/auth.d.ts +84 -0
  17. package/packages/server/dist/auth.js +247 -0
  18. package/packages/server/dist/background.d.ts +36 -0
  19. package/packages/server/dist/background.js +137 -0
  20. package/packages/server/dist/bind-safety.d.ts +56 -0
  21. package/packages/server/dist/bind-safety.js +51 -0
  22. package/packages/server/dist/brand.d.ts +44 -0
  23. package/packages/server/dist/brand.js +88 -0
  24. package/packages/server/dist/chat-dto.d.ts +210 -0
  25. package/packages/server/dist/chat-dto.js +308 -0
  26. package/packages/server/dist/cli/import-chats.d.ts +21 -0
  27. package/packages/server/dist/cli/import-chats.js +197 -0
  28. package/packages/server/dist/cli/paddock.d.ts +2 -0
  29. package/packages/server/dist/cli/paddock.js +247 -0
  30. package/packages/server/dist/concurrency.d.ts +57 -0
  31. package/packages/server/dist/concurrency.js +71 -0
  32. package/packages/server/dist/config.d.ts +512 -0
  33. package/packages/server/dist/config.js +593 -0
  34. package/packages/server/dist/curation-config.d.ts +55 -0
  35. package/packages/server/dist/curation-config.js +64 -0
  36. package/packages/server/dist/event-bus.d.ts +87 -0
  37. package/packages/server/dist/event-bus.js +48 -0
  38. package/packages/server/dist/git.d.ts +128 -0
  39. package/packages/server/dist/git.js +446 -0
  40. package/packages/server/dist/github-auth.d.ts +44 -0
  41. package/packages/server/dist/github-auth.js +169 -0
  42. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  43. package/packages/server/dist/herdctl-agent-config.js +331 -0
  44. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  45. package/packages/server/dist/herdctl-agent-names.js +228 -0
  46. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  47. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  48. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  49. package/packages/server/dist/herdctl-jobs.js +225 -0
  50. package/packages/server/dist/herdctl.d.ts +572 -0
  51. package/packages/server/dist/herdctl.js +1218 -0
  52. package/packages/server/dist/hook-config.d.ts +217 -0
  53. package/packages/server/dist/hook-config.js +224 -0
  54. package/packages/server/dist/http-bytes.d.ts +24 -0
  55. package/packages/server/dist/http-bytes.js +52 -0
  56. package/packages/server/dist/index.d.ts +1 -0
  57. package/packages/server/dist/index.js +14 -0
  58. package/packages/server/dist/instance-config.d.ts +135 -0
  59. package/packages/server/dist/instance-config.js +345 -0
  60. package/packages/server/dist/localcommand.d.ts +23 -0
  61. package/packages/server/dist/localcommand.js +140 -0
  62. package/packages/server/dist/management-auth.d.ts +99 -0
  63. package/packages/server/dist/management-auth.js +165 -0
  64. package/packages/server/dist/management-config.d.ts +164 -0
  65. package/packages/server/dist/management-config.js +286 -0
  66. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  67. package/packages/server/dist/management-mcp-server.js +95 -0
  68. package/packages/server/dist/management-metadata.d.ts +67 -0
  69. package/packages/server/dist/management-metadata.js +56 -0
  70. package/packages/server/dist/management-ops.d.ts +110 -0
  71. package/packages/server/dist/management-ops.js +449 -0
  72. package/packages/server/dist/management-policy.d.ts +172 -0
  73. package/packages/server/dist/management-policy.js +259 -0
  74. package/packages/server/dist/message-provenance.d.ts +112 -0
  75. package/packages/server/dist/message-provenance.js +260 -0
  76. package/packages/server/dist/models.d.ts +164 -0
  77. package/packages/server/dist/models.js +208 -0
  78. package/packages/server/dist/openapi.d.ts +66 -0
  79. package/packages/server/dist/openapi.js +183 -0
  80. package/packages/server/dist/parent-detach.d.ts +30 -0
  81. package/packages/server/dist/parent-detach.js +109 -0
  82. package/packages/server/dist/preload.d.ts +43 -0
  83. package/packages/server/dist/preload.js +55 -0
  84. package/packages/server/dist/project-files.d.ts +76 -0
  85. package/packages/server/dist/project-files.js +191 -0
  86. package/packages/server/dist/project-mime.d.ts +35 -0
  87. package/packages/server/dist/project-mime.js +55 -0
  88. package/packages/server/dist/project-paths.d.ts +88 -0
  89. package/packages/server/dist/project-paths.js +126 -0
  90. package/packages/server/dist/project-types.d.ts +316 -0
  91. package/packages/server/dist/project-types.js +48 -0
  92. package/packages/server/dist/projects.d.ts +202 -0
  93. package/packages/server/dist/projects.js +894 -0
  94. package/packages/server/dist/queued-message.d.ts +35 -0
  95. package/packages/server/dist/queued-message.js +116 -0
  96. package/packages/server/dist/read-state.d.ts +46 -0
  97. package/packages/server/dist/read-state.js +139 -0
  98. package/packages/server/dist/recovery-config.d.ts +111 -0
  99. package/packages/server/dist/recovery-config.js +105 -0
  100. package/packages/server/dist/recovery.d.ts +262 -0
  101. package/packages/server/dist/recovery.js +490 -0
  102. package/packages/server/dist/resume-drain.d.ts +122 -0
  103. package/packages/server/dist/resume-drain.js +158 -0
  104. package/packages/server/dist/route-context.d.ts +168 -0
  105. package/packages/server/dist/route-context.js +39 -0
  106. package/packages/server/dist/route-errors.d.ts +8 -0
  107. package/packages/server/dist/route-errors.js +13 -0
  108. package/packages/server/dist/routes/chats.d.ts +12 -0
  109. package/packages/server/dist/routes/chats.js +1406 -0
  110. package/packages/server/dist/routes/git.d.ts +13 -0
  111. package/packages/server/dist/routes/git.js +236 -0
  112. package/packages/server/dist/routes/mcp.d.ts +83 -0
  113. package/packages/server/dist/routes/mcp.js +250 -0
  114. package/packages/server/dist/routes/meta.d.ts +15 -0
  115. package/packages/server/dist/routes/meta.js +447 -0
  116. package/packages/server/dist/routes/projects.d.ts +25 -0
  117. package/packages/server/dist/routes/projects.js +736 -0
  118. package/packages/server/dist/routes/triggers.d.ts +21 -0
  119. package/packages/server/dist/routes/triggers.js +324 -0
  120. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  121. package/packages/server/dist/routes/workspace-mount.js +32 -0
  122. package/packages/server/dist/routes.d.ts +26 -0
  123. package/packages/server/dist/routes.js +29 -0
  124. package/packages/server/dist/run-provenance.d.ts +109 -0
  125. package/packages/server/dist/run-provenance.js +206 -0
  126. package/packages/server/dist/runs.d.ts +92 -0
  127. package/packages/server/dist/runs.js +72 -0
  128. package/packages/server/dist/schedule-config.d.ts +68 -0
  129. package/packages/server/dist/schedule-config.js +75 -0
  130. package/packages/server/dist/schedule-session.d.ts +34 -0
  131. package/packages/server/dist/schedule-session.js +120 -0
  132. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  133. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  134. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  135. package/packages/server/dist/self-mcp-read.js +97 -0
  136. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  137. package/packages/server/dist/self-mcp-triggers.js +144 -0
  138. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  139. package/packages/server/dist/self-mcp-types.js +10 -0
  140. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  141. package/packages/server/dist/self-mcp-util.js +157 -0
  142. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  143. package/packages/server/dist/self-mcp-write.js +215 -0
  144. package/packages/server/dist/self-mcp.d.ts +95 -0
  145. package/packages/server/dist/self-mcp.js +448 -0
  146. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  147. package/packages/server/dist/send-file-mcp.js +281 -0
  148. package/packages/server/dist/session-hub.d.ts +186 -0
  149. package/packages/server/dist/session-hub.js +291 -0
  150. package/packages/server/dist/spawn-capability.d.ts +99 -0
  151. package/packages/server/dist/spawn-capability.js +106 -0
  152. package/packages/server/dist/star.d.ts +26 -0
  153. package/packages/server/dist/star.js +94 -0
  154. package/packages/server/dist/start.d.ts +1 -0
  155. package/packages/server/dist/start.js +21 -0
  156. package/packages/server/dist/subagents.d.ts +337 -0
  157. package/packages/server/dist/subagents.js +626 -0
  158. package/packages/server/dist/sweep.d.ts +122 -0
  159. package/packages/server/dist/sweep.js +696 -0
  160. package/packages/server/dist/tooldetails.d.ts +33 -0
  161. package/packages/server/dist/tooldetails.js +466 -0
  162. package/packages/server/dist/transcribe.d.ts +69 -0
  163. package/packages/server/dist/transcribe.js +119 -0
  164. package/packages/server/dist/transcripts.d.ts +42 -0
  165. package/packages/server/dist/transcripts.js +164 -0
  166. package/packages/server/dist/trigger-config.d.ts +278 -0
  167. package/packages/server/dist/trigger-config.js +417 -0
  168. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  169. package/packages/server/dist/trigger-runtime.js +82 -0
  170. package/packages/server/dist/trigger-session.d.ts +34 -0
  171. package/packages/server/dist/trigger-session.js +122 -0
  172. package/packages/server/dist/triggers.d.ts +83 -0
  173. package/packages/server/dist/triggers.js +124 -0
  174. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  175. package/packages/server/dist/trusted-proxy.js +157 -0
  176. package/packages/server/dist/turn-notice.d.ts +154 -0
  177. package/packages/server/dist/turn-notice.js +271 -0
  178. package/packages/server/dist/unread.d.ts +36 -0
  179. package/packages/server/dist/unread.js +123 -0
  180. package/packages/server/dist/usage.d.ts +66 -0
  181. package/packages/server/dist/usage.js +243 -0
  182. package/packages/server/dist/wake-injection.d.ts +156 -0
  183. package/packages/server/dist/wake-injection.js +136 -0
  184. package/packages/server/dist/whisper-local.d.ts +9 -0
  185. package/packages/server/dist/whisper-local.js +122 -0
  186. package/packages/server/dist/ws-context.d.ts +158 -0
  187. package/packages/server/dist/ws-context.js +1 -0
  188. package/packages/server/dist/ws-protocol.d.ts +423 -0
  189. package/packages/server/dist/ws-protocol.js +251 -0
  190. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  191. package/packages/server/dist/ws-self-mcp.js +28 -0
  192. package/packages/server/dist/ws-triggers.d.ts +29 -0
  193. package/packages/server/dist/ws-triggers.js +261 -0
  194. package/packages/server/dist/ws-turn.d.ts +86 -0
  195. package/packages/server/dist/ws-turn.js +751 -0
  196. package/packages/server/dist/ws.d.ts +62 -0
  197. package/packages/server/dist/ws.js +1024 -0
  198. package/packages/server/package.json +6 -0
  199. package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +2 -0
  200. package/packages/web/dist/assets/MarkdownRenderer-CJZFLj_8.js +30 -0
  201. package/packages/web/dist/assets/ProjectRedirect-BHwVbbqB.js +2 -0
  202. package/packages/web/dist/assets/ProjectView-BOvuAITg.js +13 -0
  203. package/packages/web/dist/assets/ProjectsGrid-BCojZOGY.js +2 -0
  204. package/packages/web/dist/assets/arc-BVyxvnkl.js +2 -0
  205. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-Cd9Puqg1.js +37 -0
  206. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  207. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-B_AQt_Rx.js +133 -0
  208. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  209. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-7e0TQtXB.js +11 -0
  210. package/packages/web/dist/assets/channel-C_1aISNn.js +2 -0
  211. package/packages/web/dist/assets/chunk-2J33WTMH-Dz4WZmUZ.js +2 -0
  212. package/packages/web/dist/assets/chunk-4BX2VUAB-DVWYldo6.js +2 -0
  213. package/packages/web/dist/assets/chunk-55IACEB6-BIMrizQd.js +2 -0
  214. package/packages/web/dist/assets/chunk-727SXJPM-CL2q9fF1.js +207 -0
  215. package/packages/web/dist/assets/chunk-AQP2D5EJ-BSq3HlQ9.js +232 -0
  216. package/packages/web/dist/assets/chunk-FMBD7UC4-Bgt64pLE.js +16 -0
  217. package/packages/web/dist/assets/chunk-ND2GUHAM-C8Pr2A38.js +2 -0
  218. package/packages/web/dist/assets/chunk-QZHKN3VN-BAOqh-MN.js +2 -0
  219. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +2 -0
  220. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +2 -0
  221. package/packages/web/dist/assets/core-lJeGACQg.js +4 -0
  222. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-BF4tameA.js +2 -0
  223. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  224. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  225. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  226. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  227. package/packages/web/dist/assets/dagre-BM42HDAG-DdsLM6ql.js +5 -0
  228. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  229. package/packages/web/dist/assets/diagram-2AECGRRQ-BQ78S_RM.js +44 -0
  230. package/packages/web/dist/assets/diagram-5GNKFQAL-CAKE6nHE.js +11 -0
  231. package/packages/web/dist/assets/diagram-KO2AKTUF-fEvHRXgr.js +4 -0
  232. package/packages/web/dist/assets/diagram-LMA3HP47-CtWsgeSm.js +25 -0
  233. package/packages/web/dist/assets/diagram-OG6HWLK6-Buog8k7_.js +25 -0
  234. package/packages/web/dist/assets/erDiagram-TEJ5UH35-CWvVVPZS.js +86 -0
  235. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-DsCGXtp4.js +163 -0
  236. package/packages/web/dist/assets/format-DamJQ9LO.js +2 -0
  237. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-CsdwIvvC.js +293 -0
  238. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-hZvrX1sD.js +107 -0
  239. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  240. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  241. package/packages/web/dist/assets/index-CGbZdrm8.js +71 -0
  242. package/packages/web/dist/assets/index-DJDQ210-.css +1 -0
  243. package/packages/web/dist/assets/infoDiagram-5YYISTIA-nE_Vtz_l.js +3 -0
  244. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  245. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  246. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js +71 -0
  247. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  248. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  249. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-mjU1fMc4.js +140 -0
  250. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  251. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-C_9xNaa3.js +90 -0
  252. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  253. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  254. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  255. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  256. package/packages/web/dist/assets/linear-BxKd91ho.js +2 -0
  257. package/packages/web/dist/assets/mermaid.core-C-CuhD8I.js +310 -0
  258. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bs6yfcZv.js +97 -0
  259. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  260. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  261. package/packages/web/dist/assets/pieDiagram-4H26LBE5-DNdqX7O5.js +31 -0
  262. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  263. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js +8 -0
  264. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  265. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-RmxMdm2q.js +85 -0
  266. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  267. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  268. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-OAAhWcfK.js +41 -0
  269. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  270. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-BWF7rCyN.js +163 -0
  271. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  272. package/packages/web/dist/assets/stateDiagram-AJRCARHV-M3b_zqqo.js +2 -0
  273. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +2 -0
  274. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  275. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-BpOCh63N.js +121 -0
  276. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  277. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-D_iPH45Q.js +35 -0
  278. package/packages/web/dist/assets/wardley-L42UT6IY-DVD9I2Om.js +174 -0
  279. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-Bn1IKwjn.js +79 -0
  280. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  281. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-DwtI0bkR.js +8 -0
  282. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  283. package/packages/web/dist/favicon.ico +0 -0
  284. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  285. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  286. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  287. package/packages/web/dist/icons/favicon-16.png +0 -0
  288. package/packages/web/dist/icons/favicon-32.png +0 -0
  289. package/packages/web/dist/icons/icon-192.png +0 -0
  290. package/packages/web/dist/icons/icon-512.png +0 -0
  291. package/packages/web/dist/icons/maskable-512.png +0 -0
  292. package/packages/web/dist/index.html +56 -0
  293. package/packages/web/dist/manifest.webmanifest +32 -0
  294. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Git routes: the fleet-level remote + GitHub device-flow auth, plus the
3
+ * per-project working-tree status / diff / commit surface (#258). Push is a
4
+ * separate explicit action from a project commit.
5
+ */
6
+ import type { FastifyInstance } from "fastify";
7
+ import type { RouteCtx } from "../route-context.js";
8
+ export declare function registerGitRoutes(app: FastifyInstance, ctx: RouteCtx): void;
9
+ /**
10
+ * The workspace-scoped git surface: registered once per workspace mount (root
11
+ * and per project) — see `workspace-mount.ts`. Paths are relative to the mount.
12
+ */
13
+ export declare function registerGitWorkspaceRoutes(app: FastifyInstance, ctx: RouteCtx): void;
@@ -0,0 +1,236 @@
1
+ import { sendProjectError } from "../route-errors.js";
2
+ export function registerGitRoutes(app, ctx) {
3
+ const { git, githubAuth } = ctx;
4
+ // --- git (backing store): fleet-level remote + connection state --------
5
+ app.get("/api/git", {
6
+ schema: {
7
+ tags: ["Git"],
8
+ summary: "Get fleet-level git remote and GitHub connection state",
9
+ description: "Returns the configured backing-store remote details merged with a `github` object describing the current GitHub device-flow connection status.",
10
+ response: {
11
+ 200: {
12
+ description: "Object containing the remote configuration fields plus a nested `github` object with connection state.",
13
+ type: "object",
14
+ additionalProperties: true,
15
+ },
16
+ },
17
+ },
18
+ }, async () => {
19
+ const [remote, github] = await Promise.all([git.remote(), githubAuth.status()]);
20
+ return { ...remote, github };
21
+ });
22
+ // Push the working tree to origin (the NAS bare repo / configured remote).
23
+ app.post("/api/git/push", {
24
+ schema: {
25
+ tags: ["Git"],
26
+ summary: "Push the working tree to origin",
27
+ description: "Pushes the projects working tree to the configured remote (the NAS bare repo / origin). Returns an object describing the push result.",
28
+ response: {
29
+ 200: {
30
+ description: "Object describing the outcome of the push operation.",
31
+ type: "object",
32
+ additionalProperties: true,
33
+ },
34
+ },
35
+ },
36
+ }, async () => {
37
+ return git.push();
38
+ });
39
+ // GitHub device-flow auth: begin → poll → disconnect.
40
+ app.post("/api/git/github/connect", {
41
+ schema: {
42
+ tags: ["Git"],
43
+ summary: "Begin GitHub device-flow authentication",
44
+ description: "Starts the GitHub device-flow authorization and returns the device/user codes and verification URL. Responds with 400 (`{ error }`) if the flow cannot be started.",
45
+ response: {
46
+ 200: {
47
+ description: "Object with the GitHub device-flow details (e.g. device code, user code, verification URI).",
48
+ type: "object",
49
+ additionalProperties: true,
50
+ },
51
+ 400: {
52
+ description: "Error object `{ error }` when the device flow cannot be started.",
53
+ type: "object",
54
+ additionalProperties: true,
55
+ },
56
+ },
57
+ },
58
+ }, async (_req, reply) => {
59
+ try {
60
+ return await githubAuth.startDeviceFlow();
61
+ }
62
+ catch (err) {
63
+ return reply.code(400).send({ error: err.message });
64
+ }
65
+ });
66
+ app.post("/api/git/github/poll", {
67
+ schema: {
68
+ tags: ["Git"],
69
+ summary: "Poll GitHub device-flow authentication",
70
+ description: "Polls GitHub for completion of a previously-started device flow using the supplied `deviceCode`. Returns the current auth status; responds with 400 (`{ error }`) if `deviceCode` is missing.",
71
+ body: {
72
+ // Documentation-only (no validation/coercion): accept any/empty body.
73
+ type: ["object", "null"],
74
+ additionalProperties: true,
75
+ properties: {
76
+ deviceCode: { description: "The device code returned by the connect step." },
77
+ },
78
+ },
79
+ response: {
80
+ 200: {
81
+ description: "Object describing the current device-flow poll result / auth status.",
82
+ type: "object",
83
+ additionalProperties: true,
84
+ },
85
+ 400: {
86
+ description: "Error object `{ error }` when `deviceCode` is missing.",
87
+ type: "object",
88
+ additionalProperties: true,
89
+ },
90
+ },
91
+ },
92
+ }, async (req, reply) => {
93
+ const code = req.body?.deviceCode;
94
+ if (!code)
95
+ return reply.code(400).send({ error: "deviceCode required" });
96
+ return githubAuth.pollDeviceFlow(code);
97
+ });
98
+ app.post("/api/git/github/disconnect", {
99
+ schema: {
100
+ tags: ["Git"],
101
+ summary: "Disconnect the GitHub connection",
102
+ description: "Clears the stored GitHub credentials / connection. Returns `{ ok: true }` on success.",
103
+ response: {
104
+ 200: {
105
+ description: "Object `{ ok: true }` confirming the disconnect.",
106
+ type: "object",
107
+ additionalProperties: true,
108
+ },
109
+ },
110
+ },
111
+ }, async () => {
112
+ await githubAuth.disconnect();
113
+ return { ok: true };
114
+ });
115
+ }
116
+ /**
117
+ * The workspace-scoped git surface: registered once per workspace mount (root
118
+ * and per project) — see `workspace-mount.ts`. Paths are relative to the mount.
119
+ */
120
+ export function registerGitWorkspaceRoutes(app, ctx) {
121
+ const { git, projects } = ctx;
122
+ // --- git (backing-store capability, phase 1: read surface) -------------
123
+ // Uncommitted changes confined to this project's subtree. Returns
124
+ // `{ repo: false }` when the projects dir isn't a git working tree, so the
125
+ // UI hides the git affordance entirely. Never throws on git errors.
126
+ app.get("/git/status", {
127
+ schema: {
128
+ tags: ["Git"],
129
+ summary: "Get a project's git working-tree status",
130
+ description: "Returns the uncommitted changes confined to this project's subtree. Returns `{ repo: false }` when the projects dir isn't a git working tree. Never throws on git errors.",
131
+ params: {
132
+ type: "object",
133
+ properties: {
134
+ slug: { type: "string", description: "The project slug." },
135
+ },
136
+ required: ["slug"],
137
+ },
138
+ response: {
139
+ 200: {
140
+ description: "Object describing the project's working-tree status, or `{ repo: false }` when not a git working tree.",
141
+ type: "object",
142
+ additionalProperties: true,
143
+ },
144
+ },
145
+ },
146
+ }, async (req, reply) => {
147
+ try {
148
+ const project = await projects.get(req.params.slug);
149
+ return await git.projectStatus(project.dir);
150
+ }
151
+ catch (err) {
152
+ return sendProjectError(reply, err);
153
+ }
154
+ });
155
+ // Unified diff for the project's tracked changes (working tree vs HEAD), or a
156
+ // single file via ?file=. Untracked files are reported by /git/status instead.
157
+ app.get("/git/diff", {
158
+ schema: {
159
+ tags: ["Git"],
160
+ summary: "Get a project's unified diff",
161
+ description: "Returns the raw unified diff (text/plain) for the project's tracked changes (working tree vs HEAD), or for a single file via `?file=`. Untracked files are reported by /git/status instead.",
162
+ params: {
163
+ type: "object",
164
+ properties: {
165
+ slug: { type: "string", description: "The project slug." },
166
+ },
167
+ required: ["slug"],
168
+ },
169
+ querystring: {
170
+ type: "object",
171
+ additionalProperties: true,
172
+ properties: {
173
+ file: {
174
+ type: "string",
175
+ description: "Optional project-relative path to diff a single file.",
176
+ },
177
+ },
178
+ },
179
+ produces: ["text/plain"],
180
+ },
181
+ }, async (req, reply) => {
182
+ try {
183
+ const project = await projects.get(req.params.slug);
184
+ const diff = await git.projectDiff(project.dir, req.query.file);
185
+ reply.header("content-type", "text/plain; charset=utf-8");
186
+ return diff;
187
+ }
188
+ catch (err) {
189
+ return sendProjectError(reply, err);
190
+ }
191
+ });
192
+ // Commit this project's pending changes (phase 2). `committed: false` when
193
+ // there was nothing to commit. Push is a separate explicit action (/api/git/push).
194
+ app.post("/git/commit", {
195
+ schema: {
196
+ tags: ["Git"],
197
+ summary: "Commit a project's pending changes",
198
+ description: "Commits the project's pending changes. Optional `files` (project-relative) commits only those changes; omitted commits the whole subtree. Returns an object where `committed: false` indicates there was nothing to commit. Push is a separate explicit action (/api/git/push).",
199
+ params: {
200
+ type: "object",
201
+ properties: {
202
+ slug: { type: "string", description: "The project slug." },
203
+ },
204
+ required: ["slug"],
205
+ },
206
+ body: {
207
+ // Documentation-only (no validation/coercion): accept any/empty body.
208
+ type: ["object", "null"],
209
+ additionalProperties: true,
210
+ properties: {
211
+ message: { description: "Optional commit message; defaults to `Update <project name>`." },
212
+ files: { description: "Optional project-relative paths (array) to commit only those changes." },
213
+ },
214
+ },
215
+ response: {
216
+ 200: {
217
+ description: "Object describing the commit result; `committed: false` when there was nothing to commit.",
218
+ type: "object",
219
+ additionalProperties: true,
220
+ },
221
+ },
222
+ },
223
+ }, async (req, reply) => {
224
+ try {
225
+ const project = await projects.get(req.params.slug);
226
+ const message = req.body?.message?.trim() || `Update ${project.name}`;
227
+ // Optional `files` (project-relative) commits only those changes (#258);
228
+ // omitted ⇒ commit the whole subtree (legacy behavior).
229
+ const files = Array.isArray(req.body?.files) ? req.body?.files : undefined;
230
+ return await git.commitProject(project.dir, message, files);
231
+ }
232
+ catch (err) {
233
+ return sendProjectError(reply, err);
234
+ }
235
+ });
236
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The `/mcp` Management API endpoint (issue #312).
3
+ *
4
+ * M1 shipped the gate (authentication, principal resolution, fail-closed
5
+ * behaviour); M2 mounts the streamable-HTTP MCP transport behind it and the RFC
6
+ * 9728 discovery document beside it.
7
+ *
8
+ * ── The response matrix ────────────────────────────────────────────────────
9
+ * 404 — no `managementApi.clients` (or no `publicUrl`). The endpoint DOES
10
+ * NOT EXIST until an operator deliberately turns it on. This is the
11
+ * backstop for the deploy-ordering hazard: exempting `/mcp` at an edge
12
+ * proxy removes the only other gate in front of it.
13
+ * 401 — credential missing or bad. Always `401` + `WWW-Authenticate`, NEVER
14
+ * a 302 to a login page: an MCP client cannot follow a redirect, and
15
+ * OAuth discovery reads this exact challenge.
16
+ * 403 — authenticated but out of scope (`insufficient_scope`), or plaintext
17
+ * from a non-loopback client. `X-Forwarded-Proto: https` lifts that
18
+ * refusal only when the immediate peer is a trusted proxy (#474) —
19
+ * a client cannot vouch for its own transport.
20
+ * 405 — GET/DELETE. In stateless mode the transport answers a GET with an SSE
21
+ * stream that never emits anything, so a client would hang forever on a
22
+ * socket with no headers.
23
+ *
24
+ * Authentication runs in `onRequest` — BEFORE body parsing — so a malformed or
25
+ * oversized body can never preempt the auth decision.
26
+ */
27
+ import type { FastifyInstance, FastifyRequest } from "fastify";
28
+ import type { RouteCtx } from "../route-context.js";
29
+ import type { ManagementPrincipal } from "../management-policy.js";
30
+ import type { ManagementApiConfig } from "../management-config.js";
31
+ /** The URL path the management surface is mounted at. */
32
+ export declare const MCP_PATH = "/mcp";
33
+ declare module "fastify" {
34
+ interface FastifyRequest {
35
+ /**
36
+ * The authenticated Management API caller, set by the `/mcp` guard. Absent
37
+ * on every other route — this is NOT the browser user (`req.user`), which is
38
+ * a separate, independent identity (see management-auth.ts).
39
+ */
40
+ managementPrincipal?: ManagementPrincipal;
41
+ }
42
+ }
43
+ /** How a request earned (or failed to earn) its "secure" verdict. */
44
+ export interface TransportSecurity {
45
+ secure: boolean;
46
+ /**
47
+ * `tls` — real TLS at this process; `forwarded` — a TRUSTED proxy said https;
48
+ * `loopback` — the client is on this host; `spoofable` — a forwarded https
49
+ * claim we refused because the peer is not a trusted proxy; `plaintext` —
50
+ * no claim at all.
51
+ */
52
+ via: "tls" | "forwarded" | "loopback" | "spoofable" | "plaintext";
53
+ /** The immediate peer's socket address, for the log line and the 403 body. */
54
+ peer: string;
55
+ /**
56
+ * True when the verdict rests on a forwarded header believed under the
57
+ * DEFAULT trust list rather than an operator-named proxy — worth telling the
58
+ * operator once, since only an explicit list makes this a control (#474).
59
+ */
60
+ assumedProxy: boolean;
61
+ }
62
+ /**
63
+ * Whether the request reached us over a secure channel.
64
+ *
65
+ * Three ways to be secure: real TLS at this process, a TLS-terminating proxy
66
+ * that set `X-Forwarded-Proto: https`, or a loopback client (nothing left the
67
+ * host, so there is no wire to sniff). Everything else is plaintext over a real
68
+ * network, where a bearer token would be readable in transit — refused,
69
+ * mirroring the spirit of the bind-safety guard (#435).
70
+ *
71
+ * The forwarded case is the one #474 tightened: the header is believed only
72
+ * when the IMMEDIATE PEER — the socket address, which no client can set — is a
73
+ * trusted proxy. See trusted-proxy.ts for the trust list and its default.
74
+ *
75
+ * Note what this deliberately does NOT use: Fastify's `trustProxy`. In the
76
+ * version pinned here (4.28) enabling it makes `req.protocol` return
77
+ * `x-forwarded-proto` whenever the header is PRESENT, without consulting the
78
+ * trust function at all — the trust check applies to `req.ip`/`req.ips` only.
79
+ * Building this guard on `req.protocol` would therefore reproduce the exact bug
80
+ * it is meant to close, so the peer check is done here, explicitly.
81
+ */
82
+ export declare function evaluateTransportSecurity(req: FastifyRequest, cfg: ManagementApiConfig): TransportSecurity;
83
+ export declare function registerMcpRoutes(app: FastifyInstance, ctx: RouteCtx): void;
@@ -0,0 +1,250 @@
1
+ import { Readable } from "node:stream";
2
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
+ import { authenticateManagementRequest, challengeHeader, isManagementApiEnabled, resourceMetadataUrl, } from "../management-auth.js";
4
+ import { buildProtectedResourceMetadata, PROTECTED_RESOURCE_METADATA_PREFIX, MCP_RESOURCE_PATH, } from "../management-metadata.js";
5
+ import { buildManagementOps, enforceManagementPolicy } from "../management-ops.js";
6
+ import { selfMcpServerDefForOps } from "../ws-self-mcp.js";
7
+ import { buildManagementMcpServer } from "../management-mcp-server.js";
8
+ import { HUMAN_ROOT } from "../run-provenance.js";
9
+ import { defaultTrustedProxies, isLoopbackAddress, isTrustedProxy } from "../trusted-proxy.js";
10
+ /** The URL path the management surface is mounted at. */
11
+ export const MCP_PATH = MCP_RESOURCE_PATH;
12
+ /**
13
+ * Whether the request reached us over a secure channel.
14
+ *
15
+ * Three ways to be secure: real TLS at this process, a TLS-terminating proxy
16
+ * that set `X-Forwarded-Proto: https`, or a loopback client (nothing left the
17
+ * host, so there is no wire to sniff). Everything else is plaintext over a real
18
+ * network, where a bearer token would be readable in transit — refused,
19
+ * mirroring the spirit of the bind-safety guard (#435).
20
+ *
21
+ * The forwarded case is the one #474 tightened: the header is believed only
22
+ * when the IMMEDIATE PEER — the socket address, which no client can set — is a
23
+ * trusted proxy. See trusted-proxy.ts for the trust list and its default.
24
+ *
25
+ * Note what this deliberately does NOT use: Fastify's `trustProxy`. In the
26
+ * version pinned here (4.28) enabling it makes `req.protocol` return
27
+ * `x-forwarded-proto` whenever the header is PRESENT, without consulting the
28
+ * trust function at all — the trust check applies to `req.ip`/`req.ips` only.
29
+ * Building this guard on `req.protocol` would therefore reproduce the exact bug
30
+ * it is meant to close, so the peer check is done here, explicitly.
31
+ */
32
+ export function evaluateTransportSecurity(req, cfg) {
33
+ const peer = req.socket.remoteAddress ?? "";
34
+ const base = { peer, assumedProxy: false };
35
+ // `req.socket.encrypted` rather than `req.protocol`: with trustProxy off they
36
+ // agree, and this cannot be influenced by a header even if that changes.
37
+ if (req.socket.encrypted) {
38
+ return { ...base, secure: true, via: "tls" };
39
+ }
40
+ const fwd = req.headers["x-forwarded-proto"];
41
+ const proto = (Array.isArray(fwd) ? fwd[0] : fwd)?.split(",")[0]?.trim().toLowerCase();
42
+ const trusted = cfg.trustedProxies ?? defaultTrustedProxies();
43
+ if (proto === "https") {
44
+ if (isTrustedProxy(peer, trusted)) {
45
+ return {
46
+ ...base,
47
+ secure: true,
48
+ via: "forwarded",
49
+ assumedProxy: !trusted.explicit,
50
+ };
51
+ }
52
+ // A loopback client that spoofs the header is still secure — by being on
53
+ // loopback, not by asking. Fall through so it gets the honest reason.
54
+ if (!isLoopbackAddress(peer))
55
+ return { ...base, secure: false, via: "spoofable" };
56
+ }
57
+ if (isLoopbackAddress(peer))
58
+ return { ...base, secure: true, via: "loopback" };
59
+ return { ...base, secure: false, via: "plaintext" };
60
+ }
61
+ export function registerMcpRoutes(app, ctx) {
62
+ const { cfg, managementOpsContext } = ctx;
63
+ // ── Discovery (unauthenticated by design) ────────────────────────────────
64
+ // A client fetches this BEFORE it holds any credential, so gating it would
65
+ // make discovery impossible. The document names the authorization server and
66
+ // supported scopes — never a secret.
67
+ //
68
+ // Served at BOTH the path-inserted URL (what clients actually request for a
69
+ // resource with a path) and the bare root (harmless, and other clients may
70
+ // use it). Still 404s when the surface is off, so a disabled instance doesn't
71
+ // advertise a management API.
72
+ const metadataHandler = async (_req, reply) => {
73
+ if (!isManagementApiEnabled(cfg.managementApi)) {
74
+ return reply.code(404).send({ error: "not found" });
75
+ }
76
+ const doc = buildProtectedResourceMetadata(cfg.managementApi);
77
+ if (!doc)
78
+ return reply.code(404).send({ error: "not found" });
79
+ return reply
80
+ .header("access-control-allow-origin", "*")
81
+ .header("cache-control", "public, max-age=300")
82
+ .type("application/json; charset=utf-8")
83
+ .send(doc);
84
+ };
85
+ for (const url of [
86
+ `${PROTECTED_RESOURCE_METADATA_PREFIX}${MCP_RESOURCE_PATH}`,
87
+ PROTECTED_RESOURCE_METADATA_PREFIX,
88
+ ]) {
89
+ app.get(url, { schema: { hide: true } }, metadataHandler);
90
+ }
91
+ // ── The gate ─────────────────────────────────────────────────────────────
92
+ // Peers we have already warned about (see the assumedProxy branch below).
93
+ // Per-app, and capped, so a hostile spread of source addresses can't grow it.
94
+ const warnedPeers = new Set();
95
+ const guard = async (req, reply) => {
96
+ if (!isManagementApiEnabled(cfg.managementApi)) {
97
+ return reply.code(404).send({ error: "not found" });
98
+ }
99
+ const transport = evaluateTransportSecurity(req, cfg.managementApi);
100
+ if (!transport.secure) {
101
+ req.log.warn({ url: req.url, peer: transport.peer, via: transport.via }, transport.via === "spoofable"
102
+ ? "management API: refusing a plaintext request whose X-Forwarded-Proto: https came from a peer that is not a trusted proxy (#474)"
103
+ : "management API: refusing a plaintext non-loopback request (a bearer token would be readable in transit)");
104
+ return reply.code(403).send({
105
+ error: "https required",
106
+ code: "insecure_transport",
107
+ message: transport.via === "spoofable"
108
+ ? `The management API refuses plaintext requests from non-loopback clients. ` +
109
+ `X-Forwarded-Proto: https was ignored because the peer (${transport.peer}) is not a ` +
110
+ `trusted proxy — a client cannot vouch for its own transport. If that address IS ` +
111
+ `your TLS-terminating proxy, list it in managementApi.trustedProxies (or ` +
112
+ `PADDOCK_MANAGEMENT_TRUSTED_PROXIES); otherwise terminate TLS in front of Paddock, ` +
113
+ `or connect over loopback.`
114
+ : `The management API refuses plaintext requests from non-loopback clients ` +
115
+ `(peer: ${transport.peer || "unknown"}). Terminate TLS in front of Paddock (and ` +
116
+ `forward X-Forwarded-Proto from a trusted proxy), or connect over loopback.`,
117
+ });
118
+ }
119
+ // Believed a forwarded scheme from a peer that only matched the DEFAULT
120
+ // trust list. It is probably the operator's proxy — but nothing proves it,
121
+ // and if it is not, the token in this request just crossed a network in
122
+ // clear. Say so once per peer rather than on every request.
123
+ if (transport.assumedProxy && !warnedPeers.has(transport.peer)) {
124
+ if (warnedPeers.size < 32)
125
+ warnedPeers.add(transport.peer);
126
+ req.log.warn({ peer: transport.peer }, `management API: believing X-Forwarded-Proto: https from ${transport.peer}, which matched ` +
127
+ `the DEFAULT trusted-proxy list (loopback + private address space) rather than a proxy ` +
128
+ `you named. If that peer is not your TLS terminator, bearer tokens are reaching this ` +
129
+ `instance in plaintext. Set managementApi.trustedProxies to make this a control (#474).`);
130
+ }
131
+ const result = authenticateManagementRequest(req.headers.authorization, cfg.managementApi);
132
+ if (!result.ok) {
133
+ // Log the failure and the caller, never the presented credential.
134
+ req.log.warn({ url: req.url, failure: result.failure, ip: req.socket.remoteAddress }, "management API: authentication failed");
135
+ return reply
136
+ .code(401)
137
+ .header("WWW-Authenticate", challengeHeader(result.failure, resourceMetadataUrl(cfg.managementApi)))
138
+ .send({ error: "unauthorized", code: "auth_required" });
139
+ }
140
+ req.managementPrincipal = result.principal;
141
+ };
142
+ // ── GET / DELETE → 405 ───────────────────────────────────────────────────
143
+ // Stateless transport: a GET would open an SSE stream that never emits, so the
144
+ // client waits forever on a header-less socket. Refuse explicitly instead.
145
+ app.route({
146
+ method: ["GET", "DELETE"],
147
+ url: MCP_PATH,
148
+ schema: { hide: true },
149
+ onRequest: guard,
150
+ handler: async (_req, reply) => reply
151
+ .code(405)
152
+ .header("allow", "POST")
153
+ .send({
154
+ jsonrpc: "2.0",
155
+ error: {
156
+ code: -32000,
157
+ message: "Method not allowed. Use POST for the MCP endpoint.",
158
+ },
159
+ id: null,
160
+ }),
161
+ });
162
+ // ── POST → the MCP transport ─────────────────────────────────────────────
163
+ app.route({
164
+ method: ["POST"],
165
+ url: MCP_PATH,
166
+ // Kept out of the OpenAPI document (#466): this is a JSON-RPC MCP endpoint,
167
+ // not a REST resource, and a body schema here would become an active
168
+ // request validator over MCP payloads.
169
+ schema: { hide: true },
170
+ onRequest: guard,
171
+ handler: async (req, reply) => {
172
+ const principal = req.managementPrincipal;
173
+ if (!principal)
174
+ return reply.code(401).send({ error: "unauthorized" });
175
+ if (!managementOpsContext) {
176
+ // Wiring error rather than a client error — the surface is configured
177
+ // but the server didn't hand the route its ops context.
178
+ req.log.error("management API: ops context unavailable; /mcp cannot serve requests");
179
+ return reply.code(503).send({ error: "unavailable", code: "ops_unavailable" });
180
+ }
181
+ // Ops are built PER REQUEST, bound to this principal, then policed. An
182
+ // external caller has no "current chat", so it addresses everything
183
+ // explicitly and its spawns are rooted like a human's.
184
+ const ops = buildManagementOps(managementOpsContext, {
185
+ currentProjectSlug: "",
186
+ currentSessionId: () => null,
187
+ parentProvenance: HUMAN_ROOT,
188
+ // All three capability gates are opened here and NARROWED by scope
189
+ // immediately below. That is the right split for an external caller:
190
+ // these per-project/per-instance gates exist to bound what a KEEPER may
191
+ // reach, whereas an external client is bounded by its credential. So a
192
+ // client gets `create_project` only by naming it in `allow` — the
193
+ // read-only default excludes it, as it does every other mutation.
194
+ includeWrite: true,
195
+ includeTriggers: true,
196
+ includeProjects: true,
197
+ spawnDepthCap: principal.scope.maxSpawnDepth,
198
+ });
199
+ const policed = enforceManagementPolicy(ops, principal);
200
+ const def = selfMcpServerDefForOps(policed, principal);
201
+ const server = buildManagementMcpServer(def, principal);
202
+ // Stateless: a fresh server+transport per request. No session store, no
203
+ // cross-request state, restart-safe — and a principal's tool visibility
204
+ // can never leak into another caller's session.
205
+ const transport = new WebStandardStreamableHTTPServerTransport({
206
+ sessionIdGenerator: undefined,
207
+ });
208
+ await server.connect(transport);
209
+ const headers = new Headers();
210
+ for (const [k, v] of Object.entries(req.headers)) {
211
+ if (v === undefined)
212
+ continue;
213
+ for (const one of Array.isArray(v) ? v : [v])
214
+ headers.append(k, String(one));
215
+ }
216
+ const base = cfg.managementApi.publicUrl ?? "http://localhost";
217
+ const response = await transport.handleRequest(new Request(new URL(req.url, base), { method: "POST", headers }), {
218
+ // MANDATORY: Fastify has already drained the socket, so without this
219
+ // the transport calls .json() on an empty body and reports
220
+ // "Parse error: Invalid JSON" for perfectly valid input.
221
+ parsedBody: req.body,
222
+ });
223
+ // Flush response headers BEFORE the body. Node buffers writeHead() until
224
+ // the first body byte, so a slow tool call (every turn-spawning op is one)
225
+ // would stall the response headers for its whole duration — measured at
226
+ // 3011ms vs 28ms — which reads as a hang to a proxy with a header timeout.
227
+ // `forEach` rather than `entries()`: the server tsconfig's lib doesn't
228
+ // include the DOM iterable declarations, so `Headers` has no iterator here.
229
+ const outHeaders = {};
230
+ response.headers.forEach((value, key) => {
231
+ outHeaders[key] = value;
232
+ });
233
+ reply.raw.writeHead(response.status, outHeaders);
234
+ reply.raw.flushHeaders();
235
+ if (!response.body) {
236
+ reply.raw.end();
237
+ void server.close();
238
+ return reply.hijack();
239
+ }
240
+ const body = Readable.fromWeb(response.body);
241
+ body.pipe(reply.raw);
242
+ reply.raw.on("close", () => {
243
+ body.destroy();
244
+ void server.close();
245
+ });
246
+ // Hijack tells Fastify we own the socket — the SDK has already written it.
247
+ return reply.hijack();
248
+ },
249
+ });
250
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Instance/identity/metadata routes + the attachment byte surface: voice
3
+ * transcription, `/api/me`, health, `/api/models`, `/api/fleet`,
4
+ * instance-wide settings (issue #385), the inbound composer
5
+ * upload (#328), and the range-serving `/api/chat-files/:id` endpoint (#112/#126).
6
+ */
7
+ import type { FastifyInstance } from "fastify";
8
+ import { type RouteCtx } from "../route-context.js";
9
+ export declare function registerMetaRoutes(app: FastifyInstance, ctx: RouteCtx): void;
10
+ /**
11
+ * The workspace-scoped slice of this file: registered once per workspace mount
12
+ * (root and per project) — see `workspace-mount.ts`. Paths are relative to the
13
+ * mount.
14
+ */
15
+ export declare function registerMetaWorkspaceRoutes(app: FastifyInstance, ctx: RouteCtx): void;