@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,286 @@
1
+ /**
2
+ * `managementApi.clients` config resolution (issue #312 M1).
3
+ *
4
+ * Turns the optional `managementApi` block of `paddock.config.yaml` into the
5
+ * resolved list of external clients the token authenticator will accept, each
6
+ * carrying its {@link ManagementScope}. Pure over an injected environment map so
7
+ * the whole validation matrix is unit-testable without touching `process.env`.
8
+ *
9
+ * ── Why token material is REFERENCED, never inlined ─────────────────────────
10
+ * `paddock.config.yaml` is git-tracked (and editable from the instance Settings
11
+ * screen, #385). A secret written there leaks into history the moment it is
12
+ * committed, and no later edit removes it. So a client's credential is given as
13
+ * an env REFERENCE (`ref: env:PADDOCK_MCP_TOKEN_MY_LAPTOP`) and an inline value
14
+ * is a hard config error — not a warning — because accepting one silently would
15
+ * teach operators the unsafe habit.
16
+ *
17
+ * ── Failure posture ─────────────────────────────────────────────────────────
18
+ * Two different kinds of problem, handled differently on purpose:
19
+ * - **Malformed config** (inline secret, unknown auth type, bad scope shape)
20
+ * is an ERROR. The operator wrote something meaningless; surface it.
21
+ * - **An unresolvable reference** (env var unset/blank/too short) DROPS THAT
22
+ * CLIENT with a loud warning, leaving other clients working. Dropping is the
23
+ * fail-closed direction: the credential simply doesn't exist, so nothing can
24
+ * authenticate as that client. If every client drops, `/mcp` reverts to its
25
+ * unconfigured 404 — the endpoint ceases to exist rather than opening up.
26
+ */
27
+ import { DEFAULT_READ_ONLY_SCOPE, grantsCodeExecution, } from "./management-policy.js";
28
+ import { parseTrustedProxies } from "./trusted-proxy.js";
29
+ /**
30
+ * Recommended token prefix. A prefixed token additionally carries the INSTANCE
31
+ * it was minted for (`pdk_<instanceId>_<secret>`), which is what makes a
32
+ * credential for instance A meaningless at instance B — see
33
+ * {@link tokenInstanceId}. Also gives secret scanners something to match on.
34
+ */
35
+ export const TOKEN_PREFIX = "pdk_";
36
+ /**
37
+ * Minimum accepted token length. Not a strength guarantee — just a floor that
38
+ * rejects obvious placeholders ("changeme", "test") before they can ever
39
+ * authenticate a turn-spawning client.
40
+ */
41
+ export const MIN_TOKEN_LENGTH = 24;
42
+ /**
43
+ * Validate the canonical public URL. Returns the normalised origin (no trailing
44
+ * slash) or an error string.
45
+ *
46
+ * Rejects plaintext for anything but loopback: `/mcp` carries bearer tokens, so
47
+ * advertising an `http://` origin to the world would publish a URL on which the
48
+ * credential is readable in transit. This is the config-time half of the epic's
49
+ * "refuse to serve `/mcp` over plaintext" rule (routes/mcp.ts enforces the
50
+ * per-request half).
51
+ */
52
+ export function validatePublicUrl(raw) {
53
+ let parsed;
54
+ try {
55
+ parsed = new URL(raw.trim());
56
+ }
57
+ catch {
58
+ return { error: `must be an absolute URL (got "${raw}")` };
59
+ }
60
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
61
+ return { error: `must be an http(s) URL (got "${parsed.protocol}")` };
62
+ }
63
+ const host = parsed.hostname;
64
+ const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1";
65
+ if (parsed.protocol === "http:" && !isLoopback) {
66
+ return {
67
+ error: `must be https for a non-loopback host (got "${raw}") — the management API carries ` +
68
+ `bearer tokens, so a plaintext public URL would expose them in transit`,
69
+ };
70
+ }
71
+ if (parsed.search || parsed.hash)
72
+ return { error: "must not carry a query string or fragment" };
73
+ // Normalise to an origin + optional path, dropping any trailing slash so the
74
+ // RFC 9728 byte-comparison has one canonical form to work from.
75
+ const path = parsed.pathname.replace(/\/+$/, "");
76
+ return { url: `${parsed.origin}${path}` };
77
+ }
78
+ /** Normalize a YAML list field that may be a real array or a delimited string. */
79
+ function toList(raw) {
80
+ if (raw === undefined)
81
+ return undefined;
82
+ if (Array.isArray(raw)) {
83
+ return raw.map((v) => String(v).trim()).filter((v) => v.length > 0);
84
+ }
85
+ const s = String(raw).trim();
86
+ if (s.length === 0)
87
+ return [];
88
+ return s
89
+ .split(/[\n,]/)
90
+ .map((v) => v.trim())
91
+ .filter((v) => v.length > 0);
92
+ }
93
+ function toOptionalInt(raw) {
94
+ if (raw === undefined || raw === null || raw === "")
95
+ return undefined;
96
+ const n = typeof raw === "number" ? raw : Number.parseInt(String(raw).trim(), 10);
97
+ if (!Number.isFinite(n) || n < 0)
98
+ return undefined;
99
+ return Math.floor(n);
100
+ }
101
+ /**
102
+ * The instance id embedded in a prefixed token (`pdk_<instanceId>_<secret>`), or
103
+ * undefined for a token that carries none.
104
+ *
105
+ * This is the audience-binding hook the ticket asks for on day one: the
106
+ * authenticator refuses a prefixed token whose embedded instance doesn't match
107
+ * this instance, so copying a credential to a second Paddock does not make it
108
+ * work there — even though the secret bytes are identical.
109
+ */
110
+ export function tokenInstanceId(token) {
111
+ if (!token.startsWith(TOKEN_PREFIX))
112
+ return undefined;
113
+ const rest = token.slice(TOKEN_PREFIX.length);
114
+ const sep = rest.indexOf("_");
115
+ if (sep <= 0)
116
+ return undefined;
117
+ return rest.slice(0, sep);
118
+ }
119
+ /**
120
+ * Resolve one client's scope, defaulting to READ-ONLY when the operator gave
121
+ * none. The default is the security posture of the whole feature: a client
122
+ * added without thinking about scope gets the risk class that cannot execute
123
+ * code (see the RCE framing in management-policy.ts).
124
+ */
125
+ export function resolveClientScope(file) {
126
+ if (!file)
127
+ return { ...DEFAULT_READ_ONLY_SCOPE };
128
+ const projects = toList(file.projects) ?? [...DEFAULT_READ_ONLY_SCOPE.projects];
129
+ const allow = toList(file.allow) ?? [...DEFAULT_READ_ONLY_SCOPE.allow];
130
+ const deny = toList(file.deny) ?? [];
131
+ const denyProjects = toList(file.denyProjects);
132
+ const maxSpawnDepth = toOptionalInt(file.maxSpawnDepth);
133
+ return {
134
+ projects,
135
+ allow,
136
+ deny,
137
+ ...(denyProjects && denyProjects.length > 0 ? { denyProjects } : {}),
138
+ ...(maxSpawnDepth !== undefined ? { maxSpawnDepth } : {}),
139
+ };
140
+ }
141
+ /**
142
+ * Resolve the `managementApi` block against `env`.
143
+ *
144
+ * Never throws: a bad client is reported and dropped so one typo can't take the
145
+ * whole instance down (the browser UI and keepers are unaffected by management-
146
+ * API config). The caller logs `errors`/`warnings` at boot.
147
+ */
148
+ export function resolveManagementApiConfig(file, env) {
149
+ const errors = [];
150
+ const warnings = [];
151
+ const clients = [];
152
+ const instanceId = file?.instanceId?.trim() || undefined;
153
+ const entries = Object.entries(file?.clients ?? {});
154
+ // Which peers may tell us "this arrived over https" (#474). Env wins over the
155
+ // file, matching every other setting's precedence; saying nothing yields the
156
+ // compatibility default rather than a strict list, so an existing deployment
157
+ // behind a TLS-terminating sidecar keeps working across the upgrade.
158
+ const trustedProxiesRaw = env.PADDOCK_MANAGEMENT_TRUSTED_PROXIES ?? file?.trustedProxies;
159
+ const parsedProxies = parseTrustedProxies(trustedProxiesRaw);
160
+ errors.push(...parsedProxies.errors);
161
+ warnings.push(...parsedProxies.warnings);
162
+ const trustedProxies = parsedProxies.trusted;
163
+ // Resolve the canonical public URL up front: it gates the whole surface, so a
164
+ // bad or missing value must fail the feature closed rather than half-enable it.
165
+ let publicUrl;
166
+ const rawPublicUrl = file?.publicUrl?.trim();
167
+ if (rawPublicUrl) {
168
+ const checked = validatePublicUrl(rawPublicUrl);
169
+ if ("error" in checked) {
170
+ errors.push(`managementApi.publicUrl: ${checked.error} — the management API is disabled`);
171
+ return {
172
+ config: {
173
+ clients: [],
174
+ ...(instanceId ? { instanceId } : {}),
175
+ trustedProxies,
176
+ },
177
+ errors,
178
+ warnings,
179
+ };
180
+ }
181
+ publicUrl = checked.url;
182
+ }
183
+ else if (entries.length > 0) {
184
+ // Required whenever clients exist. Enforced in M1 (before any client can be
185
+ // configured) so that turning on M2's discovery is a no-op for the operator
186
+ // rather than a breaking config change.
187
+ errors.push("managementApi.publicUrl: required when managementApi.clients is set (the canonical " +
188
+ "public origin clients reach this instance at, e.g. https://paddock.example.com) — " +
189
+ "the management API is disabled");
190
+ return {
191
+ config: {
192
+ clients: [],
193
+ ...(instanceId ? { instanceId } : {}),
194
+ trustedProxies,
195
+ },
196
+ errors,
197
+ warnings,
198
+ };
199
+ }
200
+ for (const [rawId, raw] of entries) {
201
+ const clientId = rawId.trim();
202
+ if (clientId.length === 0) {
203
+ errors.push("managementApi.clients: a client key is empty — skipped");
204
+ continue;
205
+ }
206
+ const where = `managementApi.clients.${clientId}`;
207
+ if (!raw || typeof raw !== "object") {
208
+ errors.push(`${where}: not a mapping — skipped`);
209
+ continue;
210
+ }
211
+ const auth = raw.auth ?? {};
212
+ // Hard error: a secret written INTO the git-tracked config file.
213
+ if (auth.token !== undefined || auth.secret !== undefined) {
214
+ errors.push(`${where}.auth: inline token material is not allowed (the config file is ` +
215
+ `git-tracked). Use \`ref: env:VAR_NAME\` and set the value in the environment — skipped`);
216
+ continue;
217
+ }
218
+ const type = (auth.type ?? "token").trim();
219
+ if (type !== "token") {
220
+ errors.push(`${where}.auth.type: unsupported type "${type}" (M1 supports "token") — skipped`);
221
+ continue;
222
+ }
223
+ const ref = auth.ref?.trim();
224
+ if (!ref) {
225
+ errors.push(`${where}.auth.ref: required (e.g. \`env:PADDOCK_MCP_TOKEN_${clientId
226
+ .toUpperCase()
227
+ .replace(/[^A-Z0-9]/g, "_")}\`) — skipped`);
228
+ continue;
229
+ }
230
+ if (!ref.startsWith("env:")) {
231
+ errors.push(`${where}.auth.ref: must be an \`env:VAR_NAME\` reference (got "${ref}") — skipped`);
232
+ continue;
233
+ }
234
+ const varName = ref.slice("env:".length).trim();
235
+ if (varName.length === 0) {
236
+ errors.push(`${where}.auth.ref: names no environment variable — skipped`);
237
+ continue;
238
+ }
239
+ const token = env[varName]?.trim();
240
+ if (!token) {
241
+ // Fail-closed drop, not a boot failure: the credential doesn't exist, so
242
+ // nothing can authenticate as this client.
243
+ warnings.push(`${where}: environment variable ${varName} is unset or empty — client disabled`);
244
+ continue;
245
+ }
246
+ if (token.length < MIN_TOKEN_LENGTH) {
247
+ warnings.push(`${where}: token from ${varName} is shorter than ${MIN_TOKEN_LENGTH} characters — client disabled`);
248
+ continue;
249
+ }
250
+ // Instance binding advice (enforcement happens in the authenticator).
251
+ const embedded = tokenInstanceId(token);
252
+ if (embedded === undefined) {
253
+ warnings.push(`${where}: token from ${varName} has no \`${TOKEN_PREFIX}<instanceId>_\` prefix, so it is ` +
254
+ `NOT bound to this instance — the same value would authenticate at any Paddock that ` +
255
+ `configures it. Prefer a prefixed token.`);
256
+ }
257
+ else if (instanceId && embedded !== instanceId) {
258
+ warnings.push(`${where}: token from ${varName} is bound to instance "${embedded}" but this instance is ` +
259
+ `"${instanceId}" — it will be REJECTED at authentication. Check which instance minted it.`);
260
+ }
261
+ else if (!instanceId) {
262
+ warnings.push(`${where}: token from ${varName} is bound to instance "${embedded}" but ` +
263
+ `managementApi.instanceId is not set, so the binding cannot be enforced.`);
264
+ }
265
+ const scope = resolveClientScope(raw.scope);
266
+ if (grantsCodeExecution(scope)) {
267
+ // Deliberately loud: this grant is code execution on the host.
268
+ warnings.push(`${where}: scope grants code-execution operations — this client can run code on this host ` +
269
+ `(a spawned keeper has a shell; create_project clones a caller-supplied URL). Ensure its ` +
270
+ `token is treated as a production secret.`);
271
+ }
272
+ clients.push({ clientId, token, scope });
273
+ }
274
+ const authorizationServers = toList(file?.authorizationServers) ?? [];
275
+ return {
276
+ config: {
277
+ clients,
278
+ ...(instanceId ? { instanceId } : {}),
279
+ ...(publicUrl ? { publicUrl } : {}),
280
+ ...(authorizationServers.length > 0 ? { authorizationServers } : {}),
281
+ trustedProxies,
282
+ },
283
+ errors,
284
+ warnings,
285
+ };
286
+ }
@@ -0,0 +1,20 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import type { InjectedMcpServerDef } from "@herdctl/core";
3
+ import { type ManagementPrincipal } from "./management-policy.js";
4
+ /** Server identity advertised in the MCP `initialize` handshake. */
5
+ export declare const MANAGEMENT_MCP_SERVER_NAME = "paddock";
6
+ /**
7
+ * Package version, advertised to the client so it can report which Paddock it
8
+ * is talking to. Resolved at runtime (dist/management-mcp-server.js →
9
+ * ../package.json), the same way app.ts resolves it for the OpenAPI document,
10
+ * so it tracks releases without a build step.
11
+ */
12
+ export declare const MANAGEMENT_MCP_SERVER_VERSION: string;
13
+ /**
14
+ * Build an MCP server exposing `def`'s tools to `principal`.
15
+ *
16
+ * One server instance per request (the transport is stateless), so nothing here
17
+ * is shared across callers and a principal's tool visibility can never leak into
18
+ * another's session.
19
+ */
20
+ export declare function buildManagementMcpServer(def: InjectedMcpServerDef, principal: ManagementPrincipal): Server;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The MCP server for external Management API callers (issue #312 M2).
3
+ *
4
+ * Adapts the SAME tool definitions the in-process keeper gets — an
5
+ * {@link InjectedMcpServerDef} produced by `selfMcpServerDefForOps` — onto the
6
+ * official MCP SDK, so an external client sees exactly the toolset a keeper
7
+ * sees, minus whatever its scope hides. Nothing is redefined here: if a tool is
8
+ * added to the self-MCP, it appears over `/mcp` with no further work, which is
9
+ * the "upgrade the server, every client's toolset upgrades" property the epic
10
+ * calls out.
11
+ *
12
+ * ── Why the LOW-LEVEL `Server`, not `McpServer` ────────────────────────────
13
+ * The high-level `McpServer.registerTool` wants Zod schemas and converts them to
14
+ * JSON Schema for the wire. Our tools already carry hand-written JSON Schema
15
+ * (shared with herdctl's injected-MCP bridge), so going through Zod would mean
16
+ * translating JSON Schema → Zod → JSON Schema and hoping the round trip is
17
+ * faithful. Implementing `tools/list` + `tools/call` directly passes our schemas
18
+ * through verbatim and keeps ONE source of truth for the tool surface.
19
+ */
20
+ import { createRequire } from "node:module";
21
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
+ import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
+ import { ManagementDeniedError } from "./management-policy.js";
24
+ /** Server identity advertised in the MCP `initialize` handshake. */
25
+ export const MANAGEMENT_MCP_SERVER_NAME = "paddock";
26
+ /**
27
+ * Package version, advertised to the client so it can report which Paddock it
28
+ * is talking to. Resolved at runtime (dist/management-mcp-server.js →
29
+ * ../package.json), the same way app.ts resolves it for the OpenAPI document,
30
+ * so it tracks releases without a build step.
31
+ */
32
+ export const MANAGEMENT_MCP_SERVER_VERSION = (() => {
33
+ try {
34
+ const require = createRequire(import.meta.url);
35
+ return require("../package.json").version ?? "0.0.0";
36
+ }
37
+ catch {
38
+ return "0.0.0";
39
+ }
40
+ })();
41
+ /**
42
+ * Build an MCP server exposing `def`'s tools to `principal`.
43
+ *
44
+ * One server instance per request (the transport is stateless), so nothing here
45
+ * is shared across callers and a principal's tool visibility can never leak into
46
+ * another's session.
47
+ */
48
+ export function buildManagementMcpServer(def, principal) {
49
+ const server = new Server({ name: MANAGEMENT_MCP_SERVER_NAME, version: MANAGEMENT_MCP_SERVER_VERSION }, { capabilities: { tools: {} } });
50
+ const byName = new Map(def.tools.map((t) => [t.name, t]));
51
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
52
+ tools: def.tools.map((t) => ({
53
+ name: t.name,
54
+ description: t.description,
55
+ // Passed through verbatim — see the module header.
56
+ inputSchema: t.inputSchema,
57
+ })),
58
+ }));
59
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
60
+ const tool = byName.get(request.params.name);
61
+ // A tool hidden by scope is simply absent from the map, so an external caller
62
+ // guessing a name it wasn't offered gets the same answer as for a typo.
63
+ if (!tool) {
64
+ return {
65
+ content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
66
+ isError: true,
67
+ };
68
+ }
69
+ try {
70
+ const result = await tool.handler((request.params.arguments ?? {}));
71
+ return result;
72
+ }
73
+ catch (error) {
74
+ // A policy denial is a normal, expected outcome for a scoped caller — it
75
+ // must reach the model as a readable tool error rather than blowing up the
76
+ // JSON-RPC layer. (The HTTP 403 + `insufficient_scope` challenge is the
77
+ // right shape for a REQUEST-level refusal; a denial DURING a tool call is
78
+ // already past that point, so it is reported in-band.)
79
+ if (error instanceof ManagementDeniedError) {
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text",
84
+ text: `${error.message} (client "${principal.clientId}"; operation "${error.operation}")`,
85
+ },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
92
+ }
93
+ });
94
+ return server;
95
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * RFC 9728 protected-resource metadata (issue #312 M2).
3
+ *
4
+ * The document an MCP client fetches to learn HOW to authenticate against
5
+ * `/mcp`. Paddock publishes this itself: it is the resource server's job, and
6
+ * IdPs do not emit it.
7
+ *
8
+ * ── Two things here are easy to get wrong ──────────────────────────────────
9
+ *
10
+ * 1. **`resource` must byte-match the URL the client used** (RFC 9728 §3.3) —
11
+ * a mismatch means the client MUST discard the document. So it is built from
12
+ * the operator-configured `publicUrl`, never from `req.headers.host`: behind
13
+ * a TLS-terminating proxy the scheme would be wrong, and the header is
14
+ * attacker-controlled anyway.
15
+ *
16
+ * 2. **The URL is path-INSERTED, not path-appended.** For a resource at
17
+ * `https://host/mcp` the metadata lives at
18
+ * `https://host/.well-known/oauth-protected-resource/mcp` — the well-known
19
+ * segment goes between the host and the resource path. A verified trace of a
20
+ * real Claude Code session showed it requests ONLY that form and never the
21
+ * bare root, so serving the root alone leaves discovery silently broken. We
22
+ * serve both and rely on the path-inserted one.
23
+ */
24
+ import type { ManagementApiConfig } from "./management-config.js";
25
+ /** The well-known prefix, per RFC 9728. */
26
+ export declare const PROTECTED_RESOURCE_METADATA_PREFIX = "/.well-known/oauth-protected-resource";
27
+ /** The MCP resource path this instance serves. */
28
+ export declare const MCP_RESOURCE_PATH = "/mcp";
29
+ /** The RFC 9728 document. `authorization_servers` is optional in the RFC but MANDATORY per MCP. */
30
+ export interface ProtectedResourceMetadata {
31
+ resource: string;
32
+ authorization_servers?: string[];
33
+ scopes_supported: string[];
34
+ bearer_methods_supported: string[];
35
+ resource_name: string;
36
+ resource_documentation?: string;
37
+ }
38
+ /** The canonical resource identifier — what a client must byte-match. */
39
+ export declare function resourceIdentifier(cfg: ManagementApiConfig): string | undefined;
40
+ /**
41
+ * The absolute, path-inserted metadata URL (what clients actually request), or
42
+ * undefined when no document will be served — pointing a client at a URL that
43
+ * 404s is worse than omitting the pointer, since `resource_metadata` is a
44
+ * promise that discovery will work.
45
+ */
46
+ export declare function metadataUrl(cfg: ManagementApiConfig): string | undefined;
47
+ /**
48
+ * Build the metadata document, or undefined when it cannot be built COMPLETELY.
49
+ *
50
+ * Requires an authorization server. RFC 9728 makes `authorization_servers`
51
+ * optional, but the MCP spec makes it mandatory ("MUST include the
52
+ * `authorization_servers` field containing at least one authorization server"),
53
+ * and a token-only deployment has none — so rather than publish a document the
54
+ * governing spec calls invalid, we publish nothing.
55
+ *
56
+ * This is not theoretical. A discovery trace of a real Claude Code session
57
+ * against an earlier build that served the document WITHOUT
58
+ * `authorization_servers` showed the client fetch it, find no authorization
59
+ * server, and then go hunting: `/.well-known/oauth-authorization-server`, then
60
+ * `/.well-known/openid-configuration`, both 404. Publishing nothing produces the
61
+ * same end state (an OAuth-only client cannot connect without OAuth configured)
62
+ * without the misleading intermediate step.
63
+ *
64
+ * Nothing is lost for the supported path: a client holding a static bearer token
65
+ * never performs discovery at all.
66
+ */
67
+ export declare function buildProtectedResourceMetadata(cfg: ManagementApiConfig): ProtectedResourceMetadata | undefined;
@@ -0,0 +1,56 @@
1
+ import { OAUTH_SCOPES_SUPPORTED } from "./management-policy.js";
2
+ /** The well-known prefix, per RFC 9728. */
3
+ export const PROTECTED_RESOURCE_METADATA_PREFIX = "/.well-known/oauth-protected-resource";
4
+ /** The MCP resource path this instance serves. */
5
+ export const MCP_RESOURCE_PATH = "/mcp";
6
+ /** The canonical resource identifier — what a client must byte-match. */
7
+ export function resourceIdentifier(cfg) {
8
+ return cfg.publicUrl ? `${cfg.publicUrl}${MCP_RESOURCE_PATH}` : undefined;
9
+ }
10
+ /**
11
+ * The absolute, path-inserted metadata URL (what clients actually request), or
12
+ * undefined when no document will be served — pointing a client at a URL that
13
+ * 404s is worse than omitting the pointer, since `resource_metadata` is a
14
+ * promise that discovery will work.
15
+ */
16
+ export function metadataUrl(cfg) {
17
+ if (!cfg.publicUrl)
18
+ return undefined;
19
+ if (!cfg.authorizationServers || cfg.authorizationServers.length === 0)
20
+ return undefined;
21
+ return `${cfg.publicUrl}${PROTECTED_RESOURCE_METADATA_PREFIX}${MCP_RESOURCE_PATH}`;
22
+ }
23
+ /**
24
+ * Build the metadata document, or undefined when it cannot be built COMPLETELY.
25
+ *
26
+ * Requires an authorization server. RFC 9728 makes `authorization_servers`
27
+ * optional, but the MCP spec makes it mandatory ("MUST include the
28
+ * `authorization_servers` field containing at least one authorization server"),
29
+ * and a token-only deployment has none — so rather than publish a document the
30
+ * governing spec calls invalid, we publish nothing.
31
+ *
32
+ * This is not theoretical. A discovery trace of a real Claude Code session
33
+ * against an earlier build that served the document WITHOUT
34
+ * `authorization_servers` showed the client fetch it, find no authorization
35
+ * server, and then go hunting: `/.well-known/oauth-authorization-server`, then
36
+ * `/.well-known/openid-configuration`, both 404. Publishing nothing produces the
37
+ * same end state (an OAuth-only client cannot connect without OAuth configured)
38
+ * without the misleading intermediate step.
39
+ *
40
+ * Nothing is lost for the supported path: a client holding a static bearer token
41
+ * never performs discovery at all.
42
+ */
43
+ export function buildProtectedResourceMetadata(cfg) {
44
+ const resource = resourceIdentifier(cfg);
45
+ if (!resource)
46
+ return undefined;
47
+ if (!cfg.authorizationServers || cfg.authorizationServers.length === 0)
48
+ return undefined;
49
+ return {
50
+ resource,
51
+ authorization_servers: [...cfg.authorizationServers],
52
+ scopes_supported: [...OAUTH_SCOPES_SUPPORTED],
53
+ bearer_methods_supported: ["header"],
54
+ resource_name: "Paddock Management API",
55
+ };
56
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The transport-agnostic Management API ops layer (issue #312 M1).
3
+ *
4
+ * This is the single place every management operation is implemented, and the
5
+ * single place policy is enforced. Two exports carry that:
6
+ *
7
+ * - {@link buildManagementOps} — constructs the operation callbacks (the
8
+ * `SelfMcpContext` read bag + `SelfMcpWriteContext` write bag) over the
9
+ * shared {@link ChatHandlerContext}. Extracted from `ws-self-mcp.ts`, which
10
+ * previously built the ops AND assembled the MCP server def in one function.
11
+ * Splitting them is what lets a NON-MCP caller (HTTP now, REST parity in
12
+ * #465) reach the same operations.
13
+ * - {@link enforceManagementPolicy} — wraps a built ops bag with a
14
+ * {@link ManagementPrincipal}, so every call is scope-checked centrally.
15
+ *
16
+ * ── Why enforcement is HERE and not in the transport ────────────────────────
17
+ * The load-bearing decision on #312. If the MCP handler enforced scope, the REST
18
+ * endpoints of #465 would have to reimplement it, and the two would drift — a
19
+ * drift whose failure mode is an unchecked write path, i.e. code execution. By
20
+ * enforcing on the ops themselves, any transport that obtains a principal
21
+ * inherits the identical checks for free, and a new transport cannot forget.
22
+ *
23
+ * ── The in-process keeper path is unchanged ─────────────────────────────────
24
+ * A keeper is bounded by DEPTH (`maxSpawnDepth`, #278), not by auth+scope: it is
25
+ * not a distinct identity. It therefore runs under {@link INTERNAL_PRINCIPAL},
26
+ * whose full scope makes every check below short-circuit to allow. This module
27
+ * is a pure refactor for keepers — no behaviour change.
28
+ *
29
+ * ── Enumerate vs. address ───────────────────────────────────────────────────
30
+ * A scoped principal listing projects/chats gets a FILTERED view rather than a
31
+ * denial: "show me what I can see" is a reasonable request, and erroring would
32
+ * make a narrowly-scoped client useless. Operations that NAME a target are
33
+ * asserted instead, so an out-of-scope slug is refused loudly.
34
+ *
35
+ * ── The root workspace is an ADDRESS, not an absence (#560) ──────────────────
36
+ * A workspace key is a path relative to `projectsRoot`, so the ROOT's key is the
37
+ * empty string. Every read here therefore branches on `!== undefined`, never on
38
+ * truthiness — `if (!projectSlug)` silently retargets a root request at "all
39
+ * projects", which is exactly the bug #560 reported.
40
+ */
41
+ import type { ChatHandlerContext } from "./ws-context.js";
42
+ import type { SelfMcpContext, SelfMcpWriteContext } from "./self-mcp.js";
43
+ import { type RunProvenance } from "./run-provenance.js";
44
+ import { type ManagementPrincipal } from "./management-policy.js";
45
+ /**
46
+ * Frame an agent-initiated FORK kickoff (issue #214 Phase 2). A fork inherits the
47
+ * parent's transcript as context — and when the parent is the *live* chat doing
48
+ * the forking, that snapshot is taken mid-turn, so the child would otherwise
49
+ * inherit the parent's "I am still mid-task" identity and reject the seeded
50
+ * instruction (observed in QA). This preamble tells the child the history above
51
+ * is inherited background and that its job now is the given directive — which is
52
+ * exactly the fan-out contract ("fork this chat N times, one work-item each").
53
+ */
54
+ export declare function forkKickoffPrompt(directive: string): string;
55
+ /** The built ops. `write` is absent when the caller isn't granted write access. */
56
+ export interface ManagementOps {
57
+ read: SelfMcpContext;
58
+ write?: SelfMcpWriteContext;
59
+ }
60
+ export interface ManagementOpsParams {
61
+ currentProjectSlug: string;
62
+ currentSessionId: () => string | null;
63
+ parentProvenance: RunProvenance;
64
+ includeWrite: boolean;
65
+ includeTriggers: boolean;
66
+ /**
67
+ * Whether to expose `create_project` (#467). Its OWN gate, separate from the
68
+ * other write tools, because it creates INSTANCE-level state and clones
69
+ * caller-supplied git URLs.
70
+ */
71
+ includeProjects: boolean;
72
+ /**
73
+ * Optional ceiling on the spawn depth granted to turns these ops start
74
+ * (a scoped external principal's `scope.maxSpawnDepth`). Applied as a MINIMUM
75
+ * against the target project's resolved bound, so a scope can only ever
76
+ * NARROW what the project would allow, never widen it.
77
+ */
78
+ spawnDepthCap?: number;
79
+ }
80
+ /**
81
+ * Build the management operations bound to one caller's context.
82
+ *
83
+ * `parentProvenance` is the provenance of the chat these ops run IN — so any
84
+ * child they spawn is `childOf(parentProvenance)` (origin `spawned`, depth+1).
85
+ * The human path passes a human root (depth 0 → children depth 1); the spawned
86
+ * path passes the current turn's own `{ origin, depth }`, so the `maxSpawnDepth`
87
+ * bound descends and the tree can't run away.
88
+ */
89
+ export declare function buildManagementOps(ctx: ChatHandlerContext, params: ManagementOpsParams): ManagementOps;
90
+ /**
91
+ * Wrap built ops so every call is checked against `principal`.
92
+ *
93
+ * Returns the ops UNCHANGED for the internal principal — the keeper path must be
94
+ * byte-for-byte identical to its pre-#312 behaviour, and skipping the wrapper
95
+ * entirely is the most honest way to guarantee that.
96
+ *
97
+ * The write bag is dropped wholesale when the principal is granted no write
98
+ * operation, so a read-only client's write tools are ABSENT rather than
99
+ * present-and-refusing — matching how the existing capability gates behave.
100
+ */
101
+ export declare function enforceManagementPolicy(ops: ManagementOps, principal: ManagementPrincipal): ManagementOps;
102
+ /**
103
+ * A predicate selecting which MCP tools a principal should even SEE.
104
+ *
105
+ * Tool-list filtering is presentation, not enforcement — the ops wrapper above
106
+ * is what actually refuses a call. But hiding out-of-scope tools means a client
107
+ * never offers its model a verb it cannot use, which avoids a whole class of
108
+ * confusing "the tool exists but always errors" behaviour.
109
+ */
110
+ export declare function managementToolFilter(principal: ManagementPrincipal): (toolName: string) => boolean;