@edspencer/paddock 0.0.1 → 0.57.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 (296) 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 +538 -0
  33. package/packages/server/dist/config.js +625 -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/environment-prompt.d.ts +52 -0
  37. package/packages/server/dist/environment-prompt.js +61 -0
  38. package/packages/server/dist/event-bus.d.ts +87 -0
  39. package/packages/server/dist/event-bus.js +48 -0
  40. package/packages/server/dist/git.d.ts +128 -0
  41. package/packages/server/dist/git.js +446 -0
  42. package/packages/server/dist/github-auth.d.ts +44 -0
  43. package/packages/server/dist/github-auth.js +169 -0
  44. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  45. package/packages/server/dist/herdctl-agent-config.js +331 -0
  46. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  47. package/packages/server/dist/herdctl-agent-names.js +228 -0
  48. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  49. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  50. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  51. package/packages/server/dist/herdctl-jobs.js +225 -0
  52. package/packages/server/dist/herdctl.d.ts +623 -0
  53. package/packages/server/dist/herdctl.js +1279 -0
  54. package/packages/server/dist/hook-config.d.ts +217 -0
  55. package/packages/server/dist/hook-config.js +224 -0
  56. package/packages/server/dist/http-bytes.d.ts +24 -0
  57. package/packages/server/dist/http-bytes.js +52 -0
  58. package/packages/server/dist/index.d.ts +1 -0
  59. package/packages/server/dist/index.js +14 -0
  60. package/packages/server/dist/instance-config.d.ts +143 -0
  61. package/packages/server/dist/instance-config.js +385 -0
  62. package/packages/server/dist/localcommand.d.ts +23 -0
  63. package/packages/server/dist/localcommand.js +140 -0
  64. package/packages/server/dist/management-auth.d.ts +99 -0
  65. package/packages/server/dist/management-auth.js +165 -0
  66. package/packages/server/dist/management-config.d.ts +164 -0
  67. package/packages/server/dist/management-config.js +286 -0
  68. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  69. package/packages/server/dist/management-mcp-server.js +95 -0
  70. package/packages/server/dist/management-metadata.d.ts +67 -0
  71. package/packages/server/dist/management-metadata.js +56 -0
  72. package/packages/server/dist/management-ops.d.ts +110 -0
  73. package/packages/server/dist/management-ops.js +449 -0
  74. package/packages/server/dist/management-policy.d.ts +172 -0
  75. package/packages/server/dist/management-policy.js +259 -0
  76. package/packages/server/dist/message-provenance.d.ts +112 -0
  77. package/packages/server/dist/message-provenance.js +260 -0
  78. package/packages/server/dist/models.d.ts +164 -0
  79. package/packages/server/dist/models.js +208 -0
  80. package/packages/server/dist/openapi.d.ts +66 -0
  81. package/packages/server/dist/openapi.js +183 -0
  82. package/packages/server/dist/parent-detach.d.ts +30 -0
  83. package/packages/server/dist/parent-detach.js +109 -0
  84. package/packages/server/dist/preload.d.ts +43 -0
  85. package/packages/server/dist/preload.js +55 -0
  86. package/packages/server/dist/project-files.d.ts +76 -0
  87. package/packages/server/dist/project-files.js +191 -0
  88. package/packages/server/dist/project-mime.d.ts +35 -0
  89. package/packages/server/dist/project-mime.js +55 -0
  90. package/packages/server/dist/project-paths.d.ts +88 -0
  91. package/packages/server/dist/project-paths.js +126 -0
  92. package/packages/server/dist/project-types.d.ts +316 -0
  93. package/packages/server/dist/project-types.js +48 -0
  94. package/packages/server/dist/projects.d.ts +202 -0
  95. package/packages/server/dist/projects.js +894 -0
  96. package/packages/server/dist/queued-message.d.ts +35 -0
  97. package/packages/server/dist/queued-message.js +116 -0
  98. package/packages/server/dist/read-state.d.ts +46 -0
  99. package/packages/server/dist/read-state.js +139 -0
  100. package/packages/server/dist/recovery-config.d.ts +111 -0
  101. package/packages/server/dist/recovery-config.js +105 -0
  102. package/packages/server/dist/recovery.d.ts +262 -0
  103. package/packages/server/dist/recovery.js +490 -0
  104. package/packages/server/dist/resume-drain.d.ts +122 -0
  105. package/packages/server/dist/resume-drain.js +158 -0
  106. package/packages/server/dist/route-context.d.ts +168 -0
  107. package/packages/server/dist/route-context.js +39 -0
  108. package/packages/server/dist/route-errors.d.ts +8 -0
  109. package/packages/server/dist/route-errors.js +13 -0
  110. package/packages/server/dist/routes/chats.d.ts +12 -0
  111. package/packages/server/dist/routes/chats.js +1406 -0
  112. package/packages/server/dist/routes/git.d.ts +13 -0
  113. package/packages/server/dist/routes/git.js +236 -0
  114. package/packages/server/dist/routes/mcp.d.ts +83 -0
  115. package/packages/server/dist/routes/mcp.js +250 -0
  116. package/packages/server/dist/routes/meta.d.ts +15 -0
  117. package/packages/server/dist/routes/meta.js +447 -0
  118. package/packages/server/dist/routes/projects.d.ts +25 -0
  119. package/packages/server/dist/routes/projects.js +736 -0
  120. package/packages/server/dist/routes/triggers.d.ts +21 -0
  121. package/packages/server/dist/routes/triggers.js +324 -0
  122. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  123. package/packages/server/dist/routes/workspace-mount.js +32 -0
  124. package/packages/server/dist/routes.d.ts +26 -0
  125. package/packages/server/dist/routes.js +29 -0
  126. package/packages/server/dist/run-provenance.d.ts +109 -0
  127. package/packages/server/dist/run-provenance.js +206 -0
  128. package/packages/server/dist/runs.d.ts +92 -0
  129. package/packages/server/dist/runs.js +72 -0
  130. package/packages/server/dist/schedule-config.d.ts +68 -0
  131. package/packages/server/dist/schedule-config.js +75 -0
  132. package/packages/server/dist/schedule-session.d.ts +34 -0
  133. package/packages/server/dist/schedule-session.js +120 -0
  134. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  135. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  136. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  137. package/packages/server/dist/self-mcp-read.js +97 -0
  138. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  139. package/packages/server/dist/self-mcp-triggers.js +144 -0
  140. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  141. package/packages/server/dist/self-mcp-types.js +10 -0
  142. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  143. package/packages/server/dist/self-mcp-util.js +157 -0
  144. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  145. package/packages/server/dist/self-mcp-write.js +215 -0
  146. package/packages/server/dist/self-mcp.d.ts +95 -0
  147. package/packages/server/dist/self-mcp.js +448 -0
  148. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  149. package/packages/server/dist/send-file-mcp.js +281 -0
  150. package/packages/server/dist/session-hub.d.ts +186 -0
  151. package/packages/server/dist/session-hub.js +291 -0
  152. package/packages/server/dist/spawn-capability.d.ts +99 -0
  153. package/packages/server/dist/spawn-capability.js +106 -0
  154. package/packages/server/dist/star.d.ts +26 -0
  155. package/packages/server/dist/star.js +94 -0
  156. package/packages/server/dist/start.d.ts +1 -0
  157. package/packages/server/dist/start.js +21 -0
  158. package/packages/server/dist/subagents.d.ts +337 -0
  159. package/packages/server/dist/subagents.js +626 -0
  160. package/packages/server/dist/sweep.d.ts +122 -0
  161. package/packages/server/dist/sweep.js +696 -0
  162. package/packages/server/dist/tooldetails.d.ts +33 -0
  163. package/packages/server/dist/tooldetails.js +466 -0
  164. package/packages/server/dist/transcribe.d.ts +69 -0
  165. package/packages/server/dist/transcribe.js +119 -0
  166. package/packages/server/dist/transcripts.d.ts +42 -0
  167. package/packages/server/dist/transcripts.js +164 -0
  168. package/packages/server/dist/trigger-config.d.ts +278 -0
  169. package/packages/server/dist/trigger-config.js +417 -0
  170. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  171. package/packages/server/dist/trigger-runtime.js +82 -0
  172. package/packages/server/dist/trigger-session.d.ts +34 -0
  173. package/packages/server/dist/trigger-session.js +122 -0
  174. package/packages/server/dist/triggers.d.ts +83 -0
  175. package/packages/server/dist/triggers.js +124 -0
  176. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  177. package/packages/server/dist/trusted-proxy.js +157 -0
  178. package/packages/server/dist/turn-notice.d.ts +154 -0
  179. package/packages/server/dist/turn-notice.js +271 -0
  180. package/packages/server/dist/unread.d.ts +36 -0
  181. package/packages/server/dist/unread.js +123 -0
  182. package/packages/server/dist/usage.d.ts +66 -0
  183. package/packages/server/dist/usage.js +243 -0
  184. package/packages/server/dist/wake-injection.d.ts +156 -0
  185. package/packages/server/dist/wake-injection.js +136 -0
  186. package/packages/server/dist/whisper-local.d.ts +9 -0
  187. package/packages/server/dist/whisper-local.js +122 -0
  188. package/packages/server/dist/ws-context.d.ts +158 -0
  189. package/packages/server/dist/ws-context.js +1 -0
  190. package/packages/server/dist/ws-protocol.d.ts +423 -0
  191. package/packages/server/dist/ws-protocol.js +251 -0
  192. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  193. package/packages/server/dist/ws-self-mcp.js +28 -0
  194. package/packages/server/dist/ws-triggers.d.ts +29 -0
  195. package/packages/server/dist/ws-triggers.js +261 -0
  196. package/packages/server/dist/ws-turn.d.ts +86 -0
  197. package/packages/server/dist/ws-turn.js +751 -0
  198. package/packages/server/dist/ws.d.ts +62 -0
  199. package/packages/server/dist/ws.js +1024 -0
  200. package/packages/server/package.json +6 -0
  201. package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
  202. package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
  203. package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
  204. package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
  205. package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
  206. package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
  207. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
  208. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  209. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
  210. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  211. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
  212. package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
  213. package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
  214. package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
  215. package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
  216. package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
  217. package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
  218. package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
  219. package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
  220. package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
  221. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
  222. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
  223. package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
  224. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.js +2 -0
  225. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  226. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  227. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  228. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  229. package/packages/web/dist/assets/dagre-BM42HDAG-C6Uuq-dq.js +5 -0
  230. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  231. package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
  232. package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
  233. package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
  234. package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
  235. package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
  236. package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
  237. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
  238. package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
  239. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
  240. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.js +107 -0
  241. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  242. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  243. package/packages/web/dist/assets/index-B-YuzkF6.js +71 -0
  244. package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
  245. package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.js +3 -0
  246. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  247. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  248. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js +71 -0
  249. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  250. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  251. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-Bc9fuI2i.js +140 -0
  252. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  253. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.js +90 -0
  254. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  255. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  256. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  257. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  258. package/packages/web/dist/assets/linear-BzGHkF9K.js +2 -0
  259. package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
  260. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.js +97 -0
  261. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  262. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  263. package/packages/web/dist/assets/pieDiagram-4H26LBE5-CDmilo8S.js +31 -0
  264. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  265. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
  266. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  267. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.js +85 -0
  268. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  269. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  270. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-BkUK73W0.js +41 -0
  271. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  272. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
  273. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  274. package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
  275. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
  276. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  277. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
  278. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  279. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
  280. package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
  281. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
  282. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  283. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.js +8 -0
  284. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  285. package/packages/web/dist/favicon.ico +0 -0
  286. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  287. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  288. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  289. package/packages/web/dist/icons/favicon-16.png +0 -0
  290. package/packages/web/dist/icons/favicon-32.png +0 -0
  291. package/packages/web/dist/icons/icon-192.png +0 -0
  292. package/packages/web/dist/icons/icon-512.png +0 -0
  293. package/packages/web/dist/icons/maskable-512.png +0 -0
  294. package/packages/web/dist/index.html +56 -0
  295. package/packages/web/dist/manifest.webmanifest +32 -0
  296. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Event-hook config helpers (Epic G, ticket G1 — hook foundation).
3
+ *
4
+ * A hook is an **event-triggered agent turn** (design doc §5, GG-1..GG-4). Each
5
+ * hook is registered as its OWN herdctl agent `hook-<slug>-<name>` — exactly how
6
+ * keeper/sweeper agents are registered via `fleet.addAgent` — whose tool
7
+ * config (`allowed_tools`/`denied_tools`/`permission_mode`/`model`/`max_turns`) IS
8
+ * its capability set. There is NO hook "kind"/profile and NO "curator" concept: a
9
+ * hook with no tools is simply a hook granted no tools; a hook that must write files
10
+ * is granted `Write` and does the I/O itself.
11
+ *
12
+ * This module is the small, pure surface around that model — the exact shape of the
13
+ * shipped `schedule-config.ts` so the two features stay symmetric:
14
+ *
15
+ * - {@link sanitizeHooks} — validate/normalise a hand-edited `project.yaml` `hooks`
16
+ * map, DROPPING malformed entries so one bad edit can't brick the project's agent
17
+ * registration (an invalid agent config would throw in `addAgent`).
18
+ * - {@link hookToAgentToolConfig} — project a hook's {@link HookCapabilities} onto
19
+ * the exact herdctl agent tool-config fields, so the hook agent enforces the
20
+ * capability by construction (the capability banner in G6 is therefore truthful).
21
+ *
22
+ * Keeping this off `projects.ts`/`herdctl.ts` makes each piece unit-testable in
23
+ * isolation and gives G3/G4/G5 (visibility, Hooks tab, hook MCP) ONE frozen shape to
24
+ * build against.
25
+ */
26
+ /**
27
+ * The lifecycle events a hook can trigger on. v1 wires **`onArchive`** (fired after
28
+ * a chat-archive commits — Ed's motivating cleanup example). The enum is the
29
+ * extension point for the cheap after-commit siblings the design lists
30
+ * (onChatCreate, onFork, onUnarchive, onProjectCreate); `onProjectDelete` needs
31
+ * before/blocking semantics and is deliberately deferred.
32
+ */
33
+ export type HookEvent = "onArchive";
34
+ /** The events v1 knows about — the guard {@link sanitizeHook} validates against. */
35
+ export declare const HOOK_EVENTS: ReadonlySet<HookEvent>;
36
+ /** Claude Code permission mode a hook agent's turns run under. */
37
+ export type HookPermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan";
38
+ /**
39
+ * A hook's capability set (GG-1) — projected verbatim onto the hook's OWN herdctl
40
+ * agent's tool config, so the registered agent enforces exactly these tools. An
41
+ * absent/empty {@link HookCapabilities.allowedTools} is a **tool-less** hook (it can
42
+ * only think + return text); granting `Bash` lets it spin down servers / delete
43
+ * clones itself. This is intentionally the whole capability model — there is no
44
+ * higher-level profile.
45
+ */
46
+ export interface HookCapabilities {
47
+ /**
48
+ * The tools the hook agent may use (herdctl `allowed_tools`). Omit or `[]` for a
49
+ * tool-less hook. The CLI runtime auto-denies any tool NOT on this list, so this
50
+ * is the effective grant.
51
+ */
52
+ allowedTools?: string[];
53
+ /** Tools explicitly denied even if otherwise allowed (herdctl `denied_tools`). */
54
+ deniedTools?: string[];
55
+ /** The permission mode the hook agent's turns run under (herdctl `permission_mode`). */
56
+ permissionMode?: HookPermissionMode;
57
+ /** Model override for the hook agent; defaults to the keeper default when absent. */
58
+ model?: string;
59
+ /** Max agent turns — bounds a runaway hook. Defaults to {@link HOOK_DEFAULT_MAX_TURNS}. */
60
+ maxTurns?: number;
61
+ }
62
+ /**
63
+ * A hook declaration — persisted per project (`project.yaml` `hooks` map, keyed by
64
+ * name) with prompt bodies in `.paddock/hooks/*.md` (git-tracked + keeper-editable),
65
+ * mirroring the shipped `.paddock/schedules/*.md` pattern.
66
+ *
67
+ * **New hooks default `enabled: false`** (GG-3) — a safe-create default so nothing
68
+ * fires the instant a hook is written; enabling is just editing it to `true`.
69
+ */
70
+ export interface PaddockHook {
71
+ /** The lifecycle event this hook fires on. */
72
+ event: HookEvent;
73
+ /** The capability set granted to the hook's agent (GG-1). Absent = tool-less. */
74
+ capabilities?: HookCapabilities;
75
+ /**
76
+ * The inline prompt the hook turn runs. A hook may instead point at a
77
+ * {@link promptFile}, which Paddock reads fresh at fire time. When both are set the
78
+ * file wins.
79
+ */
80
+ prompt?: string;
81
+ /**
82
+ * PADDOCK-ONLY convenience: a git-tracked, keeper-editable prompt file under the
83
+ * project's `.paddock/hooks/` dir (e.g. `"cleanup.md"`), relative to that dir. Read
84
+ * at fire time and used as the hook's prompt. Traversal outside `.paddock/hooks/`
85
+ * and non-`.md` names are rejected.
86
+ */
87
+ promptFile?: string;
88
+ /**
89
+ * Whether the hook is armed. **Defaults `false`** for a newly-created hook (GG-3);
90
+ * a disabled hook is registered as an agent but never fired by the dispatcher.
91
+ */
92
+ enabled?: boolean;
93
+ }
94
+ /**
95
+ * A hook's CRUD/DTO shape — a {@link PaddockHook} plus its map key `name` and the
96
+ * herdctl agent it registers as. Retained as a shared building block the unified
97
+ * trigger DTO (trigger-config.ts) is projected from.
98
+ */
99
+ export interface HookDto extends PaddockHook {
100
+ /** The hook's name — the `project.yaml` map key + the `<name>` in its agent name. */
101
+ name: string;
102
+ /** The herdctl agent this hook is registered as (`hook-<slug>-<name>`). */
103
+ agentName: string;
104
+ }
105
+ /**
106
+ * The compact, web-facing capability descriptor for a hook chat (Epic G / G3,
107
+ * GG-6). Rides on the chat DTO whenever a chat's provenance origin is `hook`, so
108
+ * the floating capability banner can state — TRUTHFULLY FROM CONFIG — what the hook
109
+ * agent is: its trigger {@link event}, its herdctl agent name, whether it's armed,
110
+ * and the exact tool grant herdctl enforces on its turns, resolving the same
111
+ * {@link HookCapabilities} defaults {@link hookToAgentToolConfig} projects onto the
112
+ * registered agent, so the descriptor and the enforced capability can never disagree.
113
+ */
114
+ export interface ChatHookInfo {
115
+ /** The hook's name (`project.yaml` map key + the `<name>` in its agent name). */
116
+ name: string;
117
+ /** The lifecycle event that fires this hook. */
118
+ event: HookEvent;
119
+ /** The herdctl agent enforcing the capability (`hook-<slug>-<name>`). */
120
+ agentName: string;
121
+ /** Whether the hook is armed (a disabled hook's past chats are still shown). */
122
+ enabled: boolean;
123
+ /** The exact tool grant (herdctl `allowed_tools`); `[]` = a tool-less hook. */
124
+ allowedTools: string[];
125
+ /** Tools explicitly denied even if otherwise allowed, when the hook sets any. */
126
+ deniedTools?: string[];
127
+ /** The permission mode the hook's turns run under, when the hook sets one. */
128
+ permissionMode?: HookPermissionMode;
129
+ /** The hook agent's model override, when set (else the keeper default applies). */
130
+ model?: string;
131
+ /** The hook's max agent turns (its runaway bound). */
132
+ maxTurns: number;
133
+ }
134
+ /** Default max agent turns for a hook when its capabilities don't set one. */
135
+ export declare const HOOK_DEFAULT_MAX_TURNS = 30;
136
+ /**
137
+ * Resolve whether a project's turns get the hook-management MCP (Epic G / G5,
138
+ * GG-4): a per-project override wins; otherwise inherit the instance default.
139
+ * Mirrors {@link import("./spawn-capability.js").resolveMaxSpawnDepth} — a boolean
140
+ * override is carried on disk only when set, so an absent value transparently
141
+ * inherits the instance (env / YAML) default. A non-boolean override is ignored
142
+ * (defensive: a hand-edited `project.yaml` can't wedge dispatch).
143
+ */
144
+ export declare function resolveHooksMcpEnabled(override: boolean | undefined, instanceDefault: boolean): boolean;
145
+ /**
146
+ * Merge a partial hook update over the existing hook so an edit that OMITS a field
147
+ * preserves it (reused by the unified trigger merge). A full-REPLACE persist would
148
+ * otherwise let a caller that changes only the prompt silently wipe
149
+ * the hook's capability grant (and vice versa). `incoming` carries ONLY the fields
150
+ * the caller supplied (the MCP handler builds it that way), so a shallow overlay is
151
+ * exactly "patch the provided fields": a supplied `capabilities` replaces the whole
152
+ * set (the caller gave a new one — intended), while an omitted one is inherited. A
153
+ * brand-new hook (`existing` null) starts from `{}` and defaults `enabled: false`
154
+ * (GG-3, safe-create). Returns an untrusted record for {@link sanitizeHook} to
155
+ * validate (so it stays `Record<string, unknown>`, never a typed `PaddockHook`).
156
+ *
157
+ * `prompt` and `promptFile` are MUTUALLY EXCLUSIVE (the file wins at fire time), so
158
+ * supplying one clears the inherited counterpart — otherwise switching a file-backed
159
+ * hook to an inline prompt would leave the stale `promptFile` winning and silently
160
+ * discard the edit. Supplying neither (a capability/enabled-only edit) leaves the
161
+ * existing prompt source untouched.
162
+ */
163
+ export declare function mergeHookUpdate(existing: PaddockHook | null | undefined, incoming: Record<string, unknown>): Record<string, unknown>;
164
+ /**
165
+ * One tool a hook may be granted, for the capability picker (G4). `name` is the
166
+ * literal `allowed_tools` pattern the hook agent is registered with; `group`
167
+ * clusters them in the UI; `description` says precisely what granting it lets the
168
+ * hook do. This is the same set of tools the keeper defaults grant (herdctl.ts
169
+ * `defaults.allowed_tools`), so the picker never offers a tool the CLI runtime
170
+ * would auto-deny — the capability the UI shows is the capability enforced.
171
+ */
172
+ export interface GrantableTool {
173
+ name: string;
174
+ group: "read" | "write" | "web" | "orchestration" | "browser";
175
+ description: string;
176
+ }
177
+ /**
178
+ * The tools a hook can be granted, mirroring the keeper's default allowlist
179
+ * (herdctl `defaults.allowed_tools`) so the picker is truthful. Grouped + described
180
+ * for the G4 capability picker. `ToolSearch` is included because several of the
181
+ * autonomy tools surface as deferred tools reached through it; the browser MCP is a
182
+ * no-op unless the box enables Playwright (documented on {@link BROWSER_MCP_TOOL}).
183
+ * The `mcp__playwright__*` pattern is offered verbatim so a hook that lists it gets
184
+ * the browser tools auto-allowed exactly as the keeper does.
185
+ */
186
+ export declare const GRANTABLE_TOOLS: readonly GrantableTool[];
187
+ /** A hook name we're willing to key on (also a safe herdctl agent-name segment). */
188
+ export declare function isValidHookName(name: string): boolean;
189
+ /**
190
+ * Validate + normalise one untrusted capability record. Never returns null (a hook
191
+ * with no valid capabilities is simply tool-less); unknown fields are dropped.
192
+ */
193
+ export declare function sanitizeCapabilities(raw: unknown): HookCapabilities | undefined;
194
+ /**
195
+ * Validate + normalise one untrusted hook record into a {@link PaddockHook}, or
196
+ * `null` if it's malformed (unknown/absent event). A record with neither `prompt`
197
+ * nor `promptFile` is still valid — it runs an empty prompt (the caller's business),
198
+ * we don't drop it — but an unknown `event` is dropped so a typo can't silently
199
+ * arm nothing OR (worse) take down the whole project's agent registration.
200
+ */
201
+ export declare function sanitizeHook(raw: unknown): PaddockHook | null;
202
+ /**
203
+ * Sanitise a whole `hooks` map, dropping malformed entries and entries with an
204
+ * unsafe name. Returns `undefined` when nothing survives (so it stays absent on
205
+ * disk / off the project record), exactly like {@link sanitizeHooks}' schedule twin.
206
+ */
207
+ export declare function sanitizeHooks(raw: unknown): Record<string, PaddockHook> | undefined;
208
+ /**
209
+ * Project a hook's {@link HookCapabilities} onto the exact herdctl agent tool-config
210
+ * fields (snake_case), so the registered `hook-<slug>-<name>` agent enforces the
211
+ * capability BY CONSTRUCTION. A tool-less hook yields `allowed_tools: []` (the CLI
212
+ * runtime then denies every tool). Always sets `allowed_tools` + `max_turns` so a
213
+ * hook agent never silently inherits the keeper's broad default toolset; the other
214
+ * fields are set only when the capability specifies them (else the fleet defaults
215
+ * apply). This is the ONE place capability→config translation lives.
216
+ */
217
+ export declare function hookToAgentToolConfig(caps: HookCapabilities | undefined): Record<string, unknown>;
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Event-hook config helpers (Epic G, ticket G1 — hook foundation).
3
+ *
4
+ * A hook is an **event-triggered agent turn** (design doc §5, GG-1..GG-4). Each
5
+ * hook is registered as its OWN herdctl agent `hook-<slug>-<name>` — exactly how
6
+ * keeper/sweeper agents are registered via `fleet.addAgent` — whose tool
7
+ * config (`allowed_tools`/`denied_tools`/`permission_mode`/`model`/`max_turns`) IS
8
+ * its capability set. There is NO hook "kind"/profile and NO "curator" concept: a
9
+ * hook with no tools is simply a hook granted no tools; a hook that must write files
10
+ * is granted `Write` and does the I/O itself.
11
+ *
12
+ * This module is the small, pure surface around that model — the exact shape of the
13
+ * shipped `schedule-config.ts` so the two features stay symmetric:
14
+ *
15
+ * - {@link sanitizeHooks} — validate/normalise a hand-edited `project.yaml` `hooks`
16
+ * map, DROPPING malformed entries so one bad edit can't brick the project's agent
17
+ * registration (an invalid agent config would throw in `addAgent`).
18
+ * - {@link hookToAgentToolConfig} — project a hook's {@link HookCapabilities} onto
19
+ * the exact herdctl agent tool-config fields, so the hook agent enforces the
20
+ * capability by construction (the capability banner in G6 is therefore truthful).
21
+ *
22
+ * Keeping this off `projects.ts`/`herdctl.ts` makes each piece unit-testable in
23
+ * isolation and gives G3/G4/G5 (visibility, Hooks tab, hook MCP) ONE frozen shape to
24
+ * build against.
25
+ */
26
+ /** The events v1 knows about — the guard {@link sanitizeHook} validates against. */
27
+ export const HOOK_EVENTS = new Set(["onArchive"]);
28
+ const PERMISSION_MODES = new Set([
29
+ "default",
30
+ "acceptEdits",
31
+ "bypassPermissions",
32
+ "plan",
33
+ ]);
34
+ /** Default max agent turns for a hook when its capabilities don't set one. */
35
+ export const HOOK_DEFAULT_MAX_TURNS = 30;
36
+ /**
37
+ * Resolve whether a project's turns get the hook-management MCP (Epic G / G5,
38
+ * GG-4): a per-project override wins; otherwise inherit the instance default.
39
+ * Mirrors {@link import("./spawn-capability.js").resolveMaxSpawnDepth} — a boolean
40
+ * override is carried on disk only when set, so an absent value transparently
41
+ * inherits the instance (env / YAML) default. A non-boolean override is ignored
42
+ * (defensive: a hand-edited `project.yaml` can't wedge dispatch).
43
+ */
44
+ export function resolveHooksMcpEnabled(override, instanceDefault) {
45
+ return typeof override === "boolean" ? override : instanceDefault === true;
46
+ }
47
+ /**
48
+ * Merge a partial hook update over the existing hook so an edit that OMITS a field
49
+ * preserves it (reused by the unified trigger merge). A full-REPLACE persist would
50
+ * otherwise let a caller that changes only the prompt silently wipe
51
+ * the hook's capability grant (and vice versa). `incoming` carries ONLY the fields
52
+ * the caller supplied (the MCP handler builds it that way), so a shallow overlay is
53
+ * exactly "patch the provided fields": a supplied `capabilities` replaces the whole
54
+ * set (the caller gave a new one — intended), while an omitted one is inherited. A
55
+ * brand-new hook (`existing` null) starts from `{}` and defaults `enabled: false`
56
+ * (GG-3, safe-create). Returns an untrusted record for {@link sanitizeHook} to
57
+ * validate (so it stays `Record<string, unknown>`, never a typed `PaddockHook`).
58
+ *
59
+ * `prompt` and `promptFile` are MUTUALLY EXCLUSIVE (the file wins at fire time), so
60
+ * supplying one clears the inherited counterpart — otherwise switching a file-backed
61
+ * hook to an inline prompt would leave the stale `promptFile` winning and silently
62
+ * discard the edit. Supplying neither (a capability/enabled-only edit) leaves the
63
+ * existing prompt source untouched.
64
+ */
65
+ export function mergeHookUpdate(existing, incoming) {
66
+ const base = existing
67
+ ? {
68
+ event: existing.event,
69
+ ...(existing.capabilities ? { capabilities: existing.capabilities } : {}),
70
+ ...(existing.prompt !== undefined ? { prompt: existing.prompt } : {}),
71
+ ...(existing.promptFile !== undefined ? { promptFile: existing.promptFile } : {}),
72
+ enabled: existing.enabled === true,
73
+ }
74
+ : {};
75
+ const record = { ...base, ...incoming };
76
+ // Switching prompt source: one supplied side clears the inherited other, so the
77
+ // record never ends up with both (which would let the stale file win at fire time).
78
+ if (incoming.prompt !== undefined && incoming.promptFile === undefined)
79
+ delete record.promptFile;
80
+ if (incoming.promptFile !== undefined && incoming.prompt === undefined)
81
+ delete record.prompt;
82
+ if (record.enabled === undefined)
83
+ record.enabled = false;
84
+ return record;
85
+ }
86
+ /**
87
+ * The tools a hook can be granted, mirroring the keeper's default allowlist
88
+ * (herdctl `defaults.allowed_tools`) so the picker is truthful. Grouped + described
89
+ * for the G4 capability picker. `ToolSearch` is included because several of the
90
+ * autonomy tools surface as deferred tools reached through it; the browser MCP is a
91
+ * no-op unless the box enables Playwright (documented on {@link BROWSER_MCP_TOOL}).
92
+ * The `mcp__playwright__*` pattern is offered verbatim so a hook that lists it gets
93
+ * the browser tools auto-allowed exactly as the keeper does.
94
+ */
95
+ export const GRANTABLE_TOOLS = [
96
+ { name: "Read", group: "read", description: "Read a file from the project working dir." },
97
+ { name: "Glob", group: "read", description: "Find files by glob pattern." },
98
+ { name: "Grep", group: "read", description: "Search file contents (ripgrep)." },
99
+ { name: "Edit", group: "write", description: "Edit an existing file in place." },
100
+ { name: "Write", group: "write", description: "Create or overwrite a file — needed to author OVERVIEW/CHANGELOG etc." },
101
+ { name: "NotebookEdit", group: "write", description: "Edit Jupyter notebook cells." },
102
+ { name: "Bash", group: "write", description: "Run shell commands — spin down pm servers, delete clones, git, etc. The broadest grant." },
103
+ { name: "WebFetch", group: "web", description: "Fetch and read a URL." },
104
+ { name: "WebSearch", group: "web", description: "Search the web." },
105
+ { name: "Task", group: "orchestration", description: "Spawn a sub-agent to do a scoped task." },
106
+ { name: "TodoWrite", group: "orchestration", description: "Track a multi-step plan as a checklist." },
107
+ { name: "Skill", group: "orchestration", description: "Invoke a packaged skill (code-review, deep-research, …)." },
108
+ { name: "ToolSearch", group: "orchestration", description: "Load deferred tool schemas on demand." },
109
+ { name: "ScheduleWakeup", group: "orchestration", description: "Schedule a follow-up wake (session drive-mode only)." },
110
+ { name: "Monitor", group: "orchestration", description: "Watch a condition/background task and be re-invoked." },
111
+ { name: "CronCreate", group: "orchestration", description: "Create a cron schedule (session drive-mode only)." },
112
+ { name: "CronList", group: "orchestration", description: "List cron schedules." },
113
+ { name: "CronDelete", group: "orchestration", description: "Delete a cron schedule." },
114
+ { name: "mcp__playwright__*", group: "browser", description: "Drive a headless browser (navigate / click / snapshot). No-op unless the box enables Playwright." },
115
+ ];
116
+ /** A hook name we're willing to key on (also a safe herdctl agent-name segment). */
117
+ export function isValidHookName(name) {
118
+ return typeof name === "string" && /^[A-Za-z0-9._-]+$/.test(name) && name.length <= 64;
119
+ }
120
+ /** Normalise an untrusted string[] of tool patterns, dropping non-strings/blanks. */
121
+ function sanitizeToolList(raw) {
122
+ if (!Array.isArray(raw))
123
+ return undefined;
124
+ const out = [];
125
+ for (const v of raw) {
126
+ if (typeof v === "string" && v.trim() !== "")
127
+ out.push(v.trim());
128
+ }
129
+ return out.length > 0 ? out : undefined;
130
+ }
131
+ /**
132
+ * Validate + normalise one untrusted capability record. Never returns null (a hook
133
+ * with no valid capabilities is simply tool-less); unknown fields are dropped.
134
+ */
135
+ export function sanitizeCapabilities(raw) {
136
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
137
+ return undefined;
138
+ const o = raw;
139
+ const out = {};
140
+ const allowed = sanitizeToolList(o.allowedTools);
141
+ if (allowed)
142
+ out.allowedTools = allowed;
143
+ const denied = sanitizeToolList(o.deniedTools);
144
+ if (denied)
145
+ out.deniedTools = denied;
146
+ if (typeof o.permissionMode === "string" && PERMISSION_MODES.has(o.permissionMode)) {
147
+ out.permissionMode = o.permissionMode;
148
+ }
149
+ if (typeof o.model === "string" && o.model.trim() !== "")
150
+ out.model = o.model.trim();
151
+ if (typeof o.maxTurns === "number" && Number.isFinite(o.maxTurns) && o.maxTurns > 0) {
152
+ out.maxTurns = Math.floor(o.maxTurns);
153
+ }
154
+ return Object.keys(out).length > 0 ? out : undefined;
155
+ }
156
+ /**
157
+ * Validate + normalise one untrusted hook record into a {@link PaddockHook}, or
158
+ * `null` if it's malformed (unknown/absent event). A record with neither `prompt`
159
+ * nor `promptFile` is still valid — it runs an empty prompt (the caller's business),
160
+ * we don't drop it — but an unknown `event` is dropped so a typo can't silently
161
+ * arm nothing OR (worse) take down the whole project's agent registration.
162
+ */
163
+ export function sanitizeHook(raw) {
164
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
165
+ return null;
166
+ const o = raw;
167
+ if (typeof o.event !== "string" || !HOOK_EVENTS.has(o.event))
168
+ return null;
169
+ const out = { event: o.event };
170
+ const caps = sanitizeCapabilities(o.capabilities);
171
+ if (caps)
172
+ out.capabilities = caps;
173
+ if (typeof o.prompt === "string")
174
+ out.prompt = o.prompt;
175
+ if (typeof o.promptFile === "string" && o.promptFile.trim() !== "") {
176
+ out.promptFile = o.promptFile.trim();
177
+ }
178
+ if (typeof o.enabled === "boolean")
179
+ out.enabled = o.enabled;
180
+ return out;
181
+ }
182
+ /**
183
+ * Sanitise a whole `hooks` map, dropping malformed entries and entries with an
184
+ * unsafe name. Returns `undefined` when nothing survives (so it stays absent on
185
+ * disk / off the project record), exactly like {@link sanitizeHooks}' schedule twin.
186
+ */
187
+ export function sanitizeHooks(raw) {
188
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
189
+ return undefined;
190
+ const out = {};
191
+ for (const [name, val] of Object.entries(raw)) {
192
+ if (!isValidHookName(name))
193
+ continue;
194
+ const h = sanitizeHook(val);
195
+ if (h)
196
+ out[name] = h;
197
+ }
198
+ return Object.keys(out).length > 0 ? out : undefined;
199
+ }
200
+ /**
201
+ * Project a hook's {@link HookCapabilities} onto the exact herdctl agent tool-config
202
+ * fields (snake_case), so the registered `hook-<slug>-<name>` agent enforces the
203
+ * capability BY CONSTRUCTION. A tool-less hook yields `allowed_tools: []` (the CLI
204
+ * runtime then denies every tool). Always sets `allowed_tools` + `max_turns` so a
205
+ * hook agent never silently inherits the keeper's broad default toolset; the other
206
+ * fields are set only when the capability specifies them (else the fleet defaults
207
+ * apply). This is the ONE place capability→config translation lives.
208
+ */
209
+ export function hookToAgentToolConfig(caps) {
210
+ const out = {
211
+ // A hook's grant is exactly its allowedTools — default to NONE (tool-less), never
212
+ // the keeper's broad inherited allowlist. An empty list = the CLI runtime denies
213
+ // all tools, which is the correct "no capability" semantics.
214
+ allowed_tools: caps?.allowedTools ?? [],
215
+ max_turns: caps?.maxTurns ?? HOOK_DEFAULT_MAX_TURNS,
216
+ };
217
+ if (caps?.deniedTools)
218
+ out.denied_tools = caps.deniedTools;
219
+ if (caps?.permissionMode)
220
+ out.permission_mode = caps.permissionMode;
221
+ if (caps?.model)
222
+ out.model = caps.model;
223
+ return out;
224
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Byte-serving HTTP helpers for the raw-file / attachment endpoints (issue #126):
3
+ * the Content-Security-Policy chosen per MIME type and the `Range:` header parser
4
+ * that makes an inline `<video>` play. Pure — no Fastify or route-deps ties.
5
+ */
6
+ /**
7
+ * The Content-Security-Policy to serve a chat attachment with, chosen from its
8
+ * MIME type (issue #126). A media/PDF subresource gets a bare `default-src
9
+ * 'none'` (the `sandbox` token does nothing useful for it and we don't want it
10
+ * anywhere near `<video>` playback); everything else keeps the locked-down
11
+ * `sandbox; default-src 'none'` that guards a directly-opened image/HTML/SVG.
12
+ */
13
+ export declare function cspFor(mime: string): string;
14
+ /**
15
+ * Parse an HTTP `Range` header against a known total size (issue #126). Supports
16
+ * the single-range forms `bytes=start-`, `bytes=start-end`, and the suffix
17
+ * `bytes=-N` (last N bytes). Returns the resolved `{ start, end }` (inclusive),
18
+ * `"unsatisfiable"` for a malformed/out-of-bounds range (→ 416), or `null` when
19
+ * there is no range to honor (→ serve the full body, 200).
20
+ */
21
+ export declare function parseRangeHeader(header: string | undefined, total: number): {
22
+ start: number;
23
+ end: number;
24
+ } | "unsatisfiable" | null;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Byte-serving HTTP helpers for the raw-file / attachment endpoints (issue #126):
3
+ * the Content-Security-Policy chosen per MIME type and the `Range:` header parser
4
+ * that makes an inline `<video>` play. Pure — no Fastify or route-deps ties.
5
+ */
6
+ /**
7
+ * The Content-Security-Policy to serve a chat attachment with, chosen from its
8
+ * MIME type (issue #126). A media/PDF subresource gets a bare `default-src
9
+ * 'none'` (the `sandbox` token does nothing useful for it and we don't want it
10
+ * anywhere near `<video>` playback); everything else keeps the locked-down
11
+ * `sandbox; default-src 'none'` that guards a directly-opened image/HTML/SVG.
12
+ */
13
+ export function cspFor(mime) {
14
+ const base = mime.split(";")[0].trim().toLowerCase();
15
+ if (base.startsWith("video/") || base === "application/pdf")
16
+ return "default-src 'none'";
17
+ return "sandbox; default-src 'none'";
18
+ }
19
+ /**
20
+ * Parse an HTTP `Range` header against a known total size (issue #126). Supports
21
+ * the single-range forms `bytes=start-`, `bytes=start-end`, and the suffix
22
+ * `bytes=-N` (last N bytes). Returns the resolved `{ start, end }` (inclusive),
23
+ * `"unsatisfiable"` for a malformed/out-of-bounds range (→ 416), or `null` when
24
+ * there is no range to honor (→ serve the full body, 200).
25
+ */
26
+ export function parseRangeHeader(header, total) {
27
+ if (!header)
28
+ return null;
29
+ const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
30
+ if (!m)
31
+ return null; // multi-range or a form we don't handle → fall back to 200
32
+ const [, startStr, endStr] = m;
33
+ if (startStr === "" && endStr === "")
34
+ return null;
35
+ let start;
36
+ let end;
37
+ if (startStr === "") {
38
+ // Suffix form `bytes=-N`: the last N bytes.
39
+ const n = Number(endStr);
40
+ if (n === 0)
41
+ return "unsatisfiable";
42
+ start = Math.max(0, total - n);
43
+ end = total - 1;
44
+ }
45
+ else {
46
+ start = Number(startStr);
47
+ end = endStr === "" ? total - 1 : Math.min(Number(endStr), total - 1);
48
+ }
49
+ if (start > end || start >= total)
50
+ return "unsatisfiable";
51
+ return { start, end };
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Paddock server bootstrap.
3
+ *
4
+ * Serves the REST API + WebSocket chat transport, and (in production) the
5
+ * built web SPA from packages/web/dist. The app itself is assembled by
6
+ * `buildApp()` (src/app.ts); the process lifecycle lives in `start()`
7
+ * (src/start.ts), which the `paddock` CLI shares.
8
+ */
9
+ import { start } from "./start.js";
10
+ start().catch((err) => {
11
+ // eslint-disable-next-line no-console
12
+ console.error("fatal:", err);
13
+ process.exit(1);
14
+ });