@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,449 @@
1
+ import { keeperAgentName } from "./herdctl.js";
2
+ import { isKnownModel, isKnownDriveMode } from "./models.js";
3
+ import { ROOT_KEY } from "./projects.js";
4
+ import { childOf } from "./run-provenance.js";
5
+ import { resolveMaxSpawnDepth } from "./spawn-capability.js";
6
+ import { isCuratorTrigger, mergeTriggerUpdate } from "./trigger-config.js";
7
+ import { assertOperation, assertProject, isOperationAllowed, isProjectAllowed, isInternalPrincipal, TRIGGER_OPERATIONS, WRITE_OPERATIONS, } from "./management-policy.js";
8
+ /**
9
+ * Frame an agent-initiated FORK kickoff (issue #214 Phase 2). A fork inherits the
10
+ * parent's transcript as context — and when the parent is the *live* chat doing
11
+ * the forking, that snapshot is taken mid-turn, so the child would otherwise
12
+ * inherit the parent's "I am still mid-task" identity and reject the seeded
13
+ * instruction (observed in QA). This preamble tells the child the history above
14
+ * is inherited background and that its job now is the given directive — which is
15
+ * exactly the fan-out contract ("fork this chat N times, one work-item each").
16
+ */
17
+ export function forkKickoffPrompt(directive) {
18
+ return ("[Paddock fan-out] You are a NEW chat forked from the conversation above. " +
19
+ "That history is INHERITED CONTEXT — you are NOT in the middle of the prior " +
20
+ "turn, and its final exchange may be truncated at the fork point; do not try " +
21
+ "to continue it. Use it as background, then carry out this instruction as your " +
22
+ "task now:\n\n" +
23
+ directive);
24
+ }
25
+ /**
26
+ * Project a persisted {@link TriggerDto} (+ optional herdctl runtime state for a
27
+ * schedule trigger) onto the flat {@link SelfMcpTrigger} shape the unified trigger
28
+ * tools return (Epic T / T3).
29
+ */
30
+ function toSelfMcpTrigger(dto, info) {
31
+ const when = dto.trigger;
32
+ const run = dto.run;
33
+ const isSchedule = when.type === "schedule";
34
+ return {
35
+ name: dto.name,
36
+ agentName: dto.agentName,
37
+ type: when.type,
38
+ cron: when.type === "schedule" ? when.cron ?? null : null,
39
+ interval: when.type === "schedule" ? when.interval ?? null : null,
40
+ event: when.type === "event" ? when.on : null,
41
+ path: when.type === "webhook" ? when.path : null,
42
+ prompt: run.prompt ?? null,
43
+ promptFile: run.promptFile ?? null,
44
+ session: run.session,
45
+ tools: run.tools ?? [],
46
+ maxSpawnDepth: run.maxSpawnDepth ?? null,
47
+ permissionMode: run.permissionMode ?? null,
48
+ model: run.model ?? null,
49
+ maxTurns: run.maxTurns ?? null,
50
+ enabled: dto.enabled === true,
51
+ status: isSchedule ? info?.status ?? (dto.enabled === false ? "disabled" : "idle") : null,
52
+ lastRunAt: isSchedule ? info?.lastRunAt ?? null : null,
53
+ nextRunAt: isSchedule ? info?.nextRunAt ?? null : null,
54
+ lastError: isSchedule ? info?.lastError ?? null : null,
55
+ };
56
+ }
57
+ /**
58
+ * Build the management operations bound to one caller's context.
59
+ *
60
+ * `parentProvenance` is the provenance of the chat these ops run IN — so any
61
+ * child they spawn is `childOf(parentProvenance)` (origin `spawned`, depth+1).
62
+ * The human path passes a human root (depth 0 → children depth 1); the spawned
63
+ * path passes the current turn's own `{ origin, depth }`, so the `maxSpawnDepth`
64
+ * bound descends and the tree can't run away.
65
+ */
66
+ export function buildManagementOps(ctx, params) {
67
+ const { currentProjectSlug, currentSessionId, parentProvenance, includeWrite, includeTriggers, includeProjects, spawnDepthCap, } = params;
68
+ const { deps, hub, startAgentTurn, composePreloadedPrompt, fireTrigger } = ctx;
69
+ /**
70
+ * Every workspace an unfiltered enumeration should cover (#560): the ROOT plus
71
+ * its child projects.
72
+ *
73
+ * `ProjectStore.list()` deliberately returns children only — the root is
74
+ * reachable, never enumerated (pinned by `root-workspace.test.ts`) — so the
75
+ * root is fetched separately and prepended here, the same way
76
+ * `GET /api/projects` folds it into its response. Its read is tolerated rather
77
+ * than fatal: a root record that won't parse should not 500 the whole listing.
78
+ */
79
+ const allWorkspaces = async () => {
80
+ const [root, children] = await Promise.all([
81
+ deps.projects.get(ROOT_KEY).catch(() => null),
82
+ deps.projects.list(),
83
+ ]);
84
+ return root ? [root, ...children] : children;
85
+ };
86
+ const read = {
87
+ listProjects: async () => {
88
+ // The root rides along as an ordinary member here so the policy wrapper's
89
+ // `isProjectAllowed` filter covers it for free (a scoped client whose
90
+ // `projects` list doesn't match `""` never sees it). The MCP handler is what
91
+ // splits it back out into its own `root` field — it is not a project.
92
+ const projects = await allWorkspaces();
93
+ return projects.map((p) => ({
94
+ slug: p.slug,
95
+ name: p.name,
96
+ area: p.group && p.group.length > 0 ? p.group : undefined,
97
+ status: p.status,
98
+ }));
99
+ },
100
+ listChats: async (projectSlug) => {
101
+ // ABSENT vs EMPTY (#560). `""` is the root workspace's key: an explicit
102
+ // address that must resolve to the root, not degrade into "every project".
103
+ const targets = projectSlug !== undefined
104
+ ? [await deps.projects.get(projectSlug)]
105
+ : await allWorkspaces();
106
+ const chats = [];
107
+ for (const p of targets) {
108
+ const sessions = await deps.herdctl.listSessions(p);
109
+ // Hoisted: every session in this project is keyed by the SAME keeper agent.
110
+ const agent = keeperAgentName(p.slug);
111
+ for (const s of sessions) {
112
+ chats.push({
113
+ project: p.slug,
114
+ sessionId: s.sessionId,
115
+ name: s.customName ?? s.autoName ?? s.sessionId.slice(0, 8),
116
+ updatedAt: s.mtime,
117
+ running: hub.isRunning(s.sessionId),
118
+ // #489: the archived flag the web UI has always had (`chat-dto.ts`) but
119
+ // the MCP surface never reported — so the two disagreed about what "the
120
+ // chat list" is. Cost is nil: ArchiveStore lazy-loads once and caches,
121
+ // so this is a `Set.has()` per chat, not a read per chat.
122
+ archived: await deps.archive.isArchived(agent, s.sessionId),
123
+ });
124
+ }
125
+ }
126
+ return chats;
127
+ },
128
+ readChat: async (projectSlug, chatSessionId) => {
129
+ const project = await deps.projects.get(projectSlug);
130
+ const messages = await deps.herdctl.sessionMessages(keeperAgentName(project.slug), chatSessionId);
131
+ return messages.map((m) => ({
132
+ role: m.role,
133
+ text: m.content,
134
+ timestamp: m.timestamp,
135
+ }));
136
+ },
137
+ };
138
+ if (!includeWrite)
139
+ return { read };
140
+ const driveModeFor = (p) => p.driveMode && isKnownDriveMode(p.driveMode) ? p.driveMode : deps.cfg.driveMode;
141
+ // The child runs in its TARGET project, so its spawn bound comes from THAT
142
+ // project's override (else the instance default), not the parent's (#262) —
143
+ // then narrowed by any caller-scoped cap (never widened).
144
+ const maxSpawnDepthFor = (p) => {
145
+ const resolved = resolveMaxSpawnDepth(p.maxSpawnDepth, deps.cfg.maxSpawnDepth);
146
+ return spawnDepthCap === undefined ? resolved : Math.min(resolved, spawnDepthCap);
147
+ };
148
+ const spawnedChild = childOf(parentProvenance);
149
+ // #290: the SENDER of any message these ops inject is THIS chat. Resolve its
150
+ // display name at injection time (best effort) so the recipient's history can
151
+ // say "↩ sent by <name>" and deep-link back.
152
+ const senderForCurrentChat = async () => {
153
+ const sid = currentSessionId();
154
+ if (!sid)
155
+ return { kind: "agent" };
156
+ let name;
157
+ try {
158
+ const cur = await deps.projects.get(currentProjectSlug);
159
+ const sessions = await deps.herdctl.listSessions(cur);
160
+ const found = sessions.find((s) => s.sessionId === sid);
161
+ name = found?.customName ?? found?.autoName ?? undefined;
162
+ }
163
+ catch {
164
+ /* name is best-effort — the link still resolves without it */
165
+ }
166
+ return { kind: "chat", project: currentProjectSlug, sessionId: sid, name };
167
+ };
168
+ /**
169
+ * THIS chat, as the parent edge to record on anything it creates (#509).
170
+ * Deliberately id-only and name-free: unlike the message sender above, a
171
+ * lineage edge is resolved against the live chat list at render time, so
172
+ * caching a display name here would just go stale.
173
+ *
174
+ * Null when there is no calling chat — the external `/mcp` transport binds
175
+ * `currentSessionId: () => null`, and a schedule/hook fire has no parent.
176
+ */
177
+ const parentRefForCurrentChat = () => {
178
+ const sid = currentSessionId();
179
+ return sid ? { project: currentProjectSlug, sessionId: sid } : undefined;
180
+ };
181
+ const write = {
182
+ currentProjectSlug,
183
+ currentSessionId,
184
+ createChat: async (projectSlug, kickoff, o) => {
185
+ const p = await deps.projects.get(projectSlug);
186
+ const composed = o?.preloadContext
187
+ ? await composePreloadedPrompt(projectSlug, kickoff)
188
+ : kickoff;
189
+ const overrideModel = o?.model && isKnownModel(o.model) ? o.model : undefined;
190
+ if (overrideModel)
191
+ await deps.herdctl.ensureAgentModel(p, overrideModel);
192
+ const newId = await startAgentTurn({
193
+ projectSlug,
194
+ agentName: keeperAgentName(projectSlug),
195
+ workingDir: p.workingDir,
196
+ resume: null,
197
+ prompt: composed,
198
+ driveMode: driveModeFor(p),
199
+ fallbackModel: overrideModel ?? p.model,
200
+ origin: spawnedChild.origin,
201
+ depth: spawnedChild.depth,
202
+ parent: parentRefForCurrentChat(),
203
+ maxSpawnDepth: maxSpawnDepthFor(p),
204
+ sender: await senderForCurrentChat(),
205
+ });
206
+ if (o?.name) {
207
+ await deps.herdctl
208
+ .renameSession(keeperAgentName(projectSlug), newId, o.name)
209
+ .catch(() => undefined);
210
+ }
211
+ return { sessionId: newId };
212
+ },
213
+ forkChat: async ({ projectSlug, sourceSessionId, prompt: kickoff, name, model }) => {
214
+ const p = await deps.projects.get(projectSlug);
215
+ if (!(await deps.herdctl.sessionExists(p, sourceSessionId))) {
216
+ throw new Error(`chat not found: ${sourceSessionId} in project ${projectSlug}`);
217
+ }
218
+ const newId = await deps.herdctl.forkSession(p, sourceSessionId, name);
219
+ // Stamp the forked CHILD's provenance here (not via startAgentTurn, which
220
+ // only stamps a brand-new `resume:null` chat): a fork with no kickoff never
221
+ // calls startAgentTurn, so this covers both cases.
222
+ //
223
+ // A fork's parent is the chat it was forked FROM, not the chat that called
224
+ // the tool — a fork belongs under its source in the tree. This is the only
225
+ // record of that edge: forkSession rewrites the source id out of the child's
226
+ // transcript, and a kickoff-less fork injects no message to infer it from.
227
+ await deps.runProvenance
228
+ ?.stamp(newId, childOf(parentProvenance, { project: projectSlug, sessionId: sourceSessionId }))
229
+ .catch(() => undefined);
230
+ if (kickoff && kickoff.trim().length > 0) {
231
+ const overrideModel = model && isKnownModel(model) ? model : undefined;
232
+ if (overrideModel)
233
+ await deps.herdctl.ensureAgentModel(p, overrideModel);
234
+ await startAgentTurn({
235
+ projectSlug,
236
+ agentName: keeperAgentName(projectSlug),
237
+ workingDir: p.workingDir,
238
+ resume: newId,
239
+ prompt: forkKickoffPrompt(kickoff),
240
+ driveMode: driveModeFor(p),
241
+ fallbackModel: overrideModel ?? p.model,
242
+ origin: spawnedChild.origin,
243
+ depth: spawnedChild.depth,
244
+ maxSpawnDepth: maxSpawnDepthFor(p),
245
+ sender: await senderForCurrentChat(),
246
+ });
247
+ }
248
+ return { sessionId: newId };
249
+ },
250
+ sendMessage: async (projectSlug, targetSessionId, kickoff) => {
251
+ const p = await deps.projects.get(projectSlug);
252
+ if (!(await deps.herdctl.sessionExists(p, targetSessionId))) {
253
+ throw new Error(`chat not found: ${targetSessionId} in project ${projectSlug}`);
254
+ }
255
+ await startAgentTurn({
256
+ projectSlug,
257
+ agentName: keeperAgentName(projectSlug),
258
+ workingDir: p.workingDir,
259
+ resume: targetSessionId,
260
+ prompt: kickoff,
261
+ driveMode: driveModeFor(p),
262
+ fallbackModel: p.model,
263
+ origin: spawnedChild.origin,
264
+ depth: spawnedChild.depth,
265
+ maxSpawnDepth: maxSpawnDepthFor(p),
266
+ sender: await senderForCurrentChat(),
267
+ });
268
+ },
269
+ setArchived: async (projectSlug, targetSessionId, archived) => {
270
+ await deps.projects.get(projectSlug);
271
+ const changed = await deps.archive.setArchived(keeperAgentName(projectSlug), targetSessionId, archived);
272
+ // Epic G / G1: after the archive COMMITS, emit the `onArchive` lifecycle
273
+ // event (only on a real transition INTO archived) so the dispatcher fires
274
+ // the project's enabled onArchive hooks.
275
+ if (changed && archived) {
276
+ deps.events?.emit("onArchive", { slug: projectSlug, sessionId: targetSessionId });
277
+ }
278
+ },
279
+ // Project provisioning (#467). Behind its OWN gate, separate from the other
280
+ // write tools, because this is the one that creates INSTANCE-level state. The
281
+ // body is deliberately the SAME two calls, in the same order, that
282
+ // `POST /api/projects` makes — store create, then keeper registration — so the
283
+ // REST and MCP paths can't drift: all validation, the #187 repo clone and its
284
+ // rollback live in `ProjectStore.create`.
285
+ projectsMcpEnabled: includeProjects,
286
+ createProject: async (input) => {
287
+ // The handler already validated `status` against PROJECT_STATUSES (the same
288
+ // list ProjectStatus is derived from), so narrowing it here is sound — it's
289
+ // only `string` on the MCP input type to keep that module free of the
290
+ // project layer's types.
291
+ const project = await deps.projects.create({
292
+ ...input,
293
+ status: input.status,
294
+ });
295
+ // Mirror the route's non-fatal treatment: the project IS created even if
296
+ // registering its keeper/sweeper agents fails. Unlike the route (which only
297
+ // logs) we report it, since the agent's very next step is usually a
298
+ // `create_chat` into this project, which needs a live keeper.
299
+ let agentRegistered = true;
300
+ try {
301
+ await deps.herdctl.ensureProjectAgent(project);
302
+ }
303
+ catch {
304
+ agentRegistered = false;
305
+ }
306
+ return {
307
+ slug: project.slug,
308
+ name: project.name,
309
+ dir: project.dir,
310
+ workingDir: project.workingDir,
311
+ repoBacked: project.repoBacked === true,
312
+ ...(project.repo ? { repo: project.repo } : {}),
313
+ agentRegistered,
314
+ };
315
+ },
316
+ triggersMcpEnabled: includeTriggers,
317
+ listTriggers: async (projectSlug) => {
318
+ if (!deps.triggers)
319
+ return [];
320
+ const dtos = await deps.triggers.list(projectSlug);
321
+ const p = await deps.projects.get(projectSlug).catch(() => null);
322
+ const runtime = p ? await deps.herdctl.listAgentSchedules(p).catch(() => []) : [];
323
+ const byName = new Map(runtime.map((s) => [s.name, s]));
324
+ return dtos.map((dto) => toSelfMcpTrigger(dto, byName.get(dto.name)));
325
+ },
326
+ setTrigger: async (projectSlug, name, incoming) => {
327
+ if (!deps.triggers)
328
+ throw new Error("trigger management is unavailable");
329
+ const existing = await deps.triggers.get(projectSlug, name).catch(() => null);
330
+ const record = mergeTriggerUpdate(existing, incoming);
331
+ const dto = await deps.triggers.set(projectSlug, name, record);
332
+ const p = await deps.projects.get(projectSlug).catch(() => null);
333
+ const runtime = p ? await deps.herdctl.listAgentSchedules(p).catch(() => []) : [];
334
+ const info = runtime.find((s) => s.name === name);
335
+ return toSelfMcpTrigger(dto, info);
336
+ },
337
+ removeTrigger: async (projectSlug, name) => {
338
+ if (!deps.triggers)
339
+ return false;
340
+ return deps.triggers.remove(projectSlug, name);
341
+ },
342
+ runTrigger: async (projectSlug, name) => {
343
+ if (!deps.triggers)
344
+ return null;
345
+ // Reject the post-turn curator up front with a clear message (the generic
346
+ // fire path can't run it — it has no scoped agent; see fireTrigger).
347
+ const p = await deps.projects.get(projectSlug).catch(() => null);
348
+ const rec = p?.triggers?.[name];
349
+ if (rec && isCuratorTrigger(rec)) {
350
+ throw new Error("the post-turn curator trigger runs automatically after each turn and can't be run on demand");
351
+ }
352
+ return fireTrigger(projectSlug, name);
353
+ },
354
+ };
355
+ return { read, write };
356
+ }
357
+ /**
358
+ * Wrap built ops so every call is checked against `principal`.
359
+ *
360
+ * Returns the ops UNCHANGED for the internal principal — the keeper path must be
361
+ * byte-for-byte identical to its pre-#312 behaviour, and skipping the wrapper
362
+ * entirely is the most honest way to guarantee that.
363
+ *
364
+ * The write bag is dropped wholesale when the principal is granted no write
365
+ * operation, so a read-only client's write tools are ABSENT rather than
366
+ * present-and-refusing — matching how the existing capability gates behave.
367
+ */
368
+ export function enforceManagementPolicy(ops, principal) {
369
+ if (isInternalPrincipal(principal))
370
+ return ops;
371
+ const { scope } = principal;
372
+ const read = {
373
+ listProjects: async () => {
374
+ assertOperation(principal, "list_projects");
375
+ // Enumeration filters rather than denies — see the module header.
376
+ return (await ops.read.listProjects()).filter((p) => isProjectAllowed(scope, p.slug));
377
+ },
378
+ listChats: async (projectSlug) => {
379
+ assertOperation(principal, "list_chats");
380
+ if (projectSlug !== undefined)
381
+ assertProject(principal, "list_chats", projectSlug);
382
+ return (await ops.read.listChats(projectSlug)).filter((c) => isProjectAllowed(scope, c.project));
383
+ },
384
+ readChat: async (projectSlug, sessionId) => {
385
+ assertOperation(principal, "read_chat");
386
+ assertProject(principal, "read_chat", projectSlug);
387
+ return ops.read.readChat(projectSlug, sessionId);
388
+ },
389
+ };
390
+ const grantsAnyWrite = WRITE_OPERATIONS.some((op) => isOperationAllowed(scope, op));
391
+ const grantsAnyTrigger = TRIGGER_OPERATIONS.some((op) => isOperationAllowed(scope, op));
392
+ if (!ops.write || (!grantsAnyWrite && !grantsAnyTrigger))
393
+ return { read };
394
+ const w = ops.write;
395
+ // `async` so a denial surfaces as a REJECTED PROMISE, never a synchronous
396
+ // throw: these callbacks are typed as returning promises, and a caller that
397
+ // only attaches `.catch()` (rather than awaiting inside a try) would otherwise
398
+ // see the error escape as an unhandled exception.
399
+ const guard = async (operation, projectSlug, run) => {
400
+ assertOperation(principal, operation);
401
+ assertProject(principal, operation, projectSlug);
402
+ return run();
403
+ };
404
+ const write = {
405
+ currentProjectSlug: w.currentProjectSlug,
406
+ currentSessionId: w.currentSessionId,
407
+ // Instance-level, so there is no project to scope-check — only the verb.
408
+ projectsMcpEnabled: w.projectsMcpEnabled && isOperationAllowed(scope, "create_project"),
409
+ createProject: (input) => {
410
+ assertOperation(principal, "create_project");
411
+ return w.createProject(input);
412
+ },
413
+ createChat: (projectSlug, prompt, o) => guard("create_chat", projectSlug, () => w.createChat(projectSlug, prompt, o)),
414
+ // NOTE: `fork_chat_batch` is a convenience wrapper that calls this same
415
+ // callback N times, so it is gated on `fork_chat` here AND on
416
+ // `fork_chat_batch` at tool-assembly time (see managementToolFilter).
417
+ forkChat: (args) => guard("fork_chat", args.projectSlug, () => w.forkChat(args)),
418
+ sendMessage: (projectSlug, sessionId, prompt) => guard("send_message", projectSlug, () => w.sendMessage(projectSlug, sessionId, prompt)),
419
+ setArchived: (projectSlug, sessionId, archived) => guard(archived ? "archive_chat" : "unarchive_chat", projectSlug, () => w.setArchived(projectSlug, sessionId, archived)),
420
+ // Only offer the trigger tools when the principal is granted at least one.
421
+ triggersMcpEnabled: w.triggersMcpEnabled && grantsAnyTrigger,
422
+ listTriggers: (projectSlug) => guard("list_triggers", projectSlug, () => w.listTriggers(projectSlug)),
423
+ setTrigger: (projectSlug, name, trigger) => guard("set_trigger", projectSlug, () => w.setTrigger(projectSlug, name, trigger)),
424
+ removeTrigger: (projectSlug, name) => guard("remove_trigger", projectSlug, () => w.removeTrigger(projectSlug, name)),
425
+ runTrigger: (projectSlug, name) => guard("run_trigger", projectSlug, () => w.runTrigger(projectSlug, name)),
426
+ };
427
+ return { read, write };
428
+ }
429
+ /**
430
+ * A predicate selecting which MCP tools a principal should even SEE.
431
+ *
432
+ * Tool-list filtering is presentation, not enforcement — the ops wrapper above
433
+ * is what actually refuses a call. But hiding out-of-scope tools means a client
434
+ * never offers its model a verb it cannot use, which avoids a whole class of
435
+ * confusing "the tool exists but always errors" behaviour.
436
+ */
437
+ export function managementToolFilter(principal) {
438
+ if (isInternalPrincipal(principal))
439
+ return () => true;
440
+ return (toolName) => {
441
+ if (!isOperationAllowed(principal.scope, toolName))
442
+ return false;
443
+ // The batch fan-out executes through `fork_chat`, so offering it without
444
+ // that grant would produce a tool that is present but denied on every call.
445
+ if (toolName === "fork_chat_batch")
446
+ return isOperationAllowed(principal.scope, "fork_chat");
447
+ return true;
448
+ };
449
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Management API policy primitives (issue #312 M1).
3
+ *
4
+ * The `ManagementPrincipal` (WHO is calling) + `ManagementScope` (WHAT they may
5
+ * reach) and the pure predicates that decide a single call. This module is
6
+ * deliberately transport-free and dependency-free: it knows nothing about MCP,
7
+ * HTTP, Fastify or the ops callbacks. Everything here is a pure function over
8
+ * plain data, so the security-critical decisions are unit-testable without a
9
+ * fleet, a socket, or a server.
10
+ *
11
+ * ── Why policy lives HERE and not in the MCP transport ──────────────────────
12
+ * The ticket's central structural decision: enforcement happens at the OPS
13
+ * layer, not in whichever transport happens to be calling. Paddock will grow a
14
+ * REST surface for the same turn-spawning operations (#465); if policy lived in
15
+ * the MCP handler, REST would have to reimplement it and the two would drift.
16
+ * So the transports authenticate (producing a principal) and the ops layer
17
+ * enforces (consuming one) — see management-ops.ts.
18
+ *
19
+ * ── The blast-radius framing that drives the defaults ───────────────────────
20
+ * `create_chat` / `send_message` / `fork_chat*` / `run_trigger` START KEEPER
21
+ * TURNS, and a keeper runs with Bash + Write. So **any write scope is
22
+ * effectively remote code execution on the host.** Read-only is therefore the
23
+ * default for a configured client, writes are opt-in per client, and the
24
+ * grammar below is deny-biased at every ambiguity: deny beats allow, an unknown
25
+ * operation is denied, and an empty allow-list grants nothing.
26
+ */
27
+ /** How a principal proved its identity. Transports set this; policy ignores it. */
28
+ export type ManagementCredentialKind = "internal" | "token" | "oauth";
29
+ /**
30
+ * What a principal may reach. Both dimensions are independently constrained:
31
+ * WHICH operations (`allow`/`deny`) and WHICH projects (`projects`/
32
+ * `denyProjects`). A call must satisfy both.
33
+ */
34
+ export interface ManagementScope {
35
+ /**
36
+ * Project slugs this principal may touch. `["*"]` = every project. Matching
37
+ * is exact-or-`*` (see {@link matchesPattern}); an empty list reaches nothing.
38
+ */
39
+ projects: string[];
40
+ /**
41
+ * Operation names this principal may invoke. Supports a trailing `*` wildcard
42
+ * (`"list_*"`) and the bare `"*"`. Empty grants nothing.
43
+ */
44
+ allow: string[];
45
+ /** Operation names explicitly refused. Deny ALWAYS beats allow. */
46
+ deny: string[];
47
+ /** Project slugs explicitly refused. Deny ALWAYS beats allow. */
48
+ denyProjects?: string[];
49
+ /**
50
+ * Recursion bound applied to turns this principal spawns. Absent ⇒ the
51
+ * instance/project default resolves as it does for an internal caller.
52
+ */
53
+ maxSpawnDepth?: number;
54
+ }
55
+ /** The authenticated caller of a management operation. */
56
+ export interface ManagementPrincipal {
57
+ /**
58
+ * Stable per-client identity — the `managementApi.clients` key for an external
59
+ * caller, or `"internal"` for the in-process keeper path. This is what gets
60
+ * LOGGED and stamped as provenance; the credential itself never is.
61
+ */
62
+ clientId: string;
63
+ kind: ManagementCredentialKind;
64
+ scope: ManagementScope;
65
+ }
66
+ /** Read-only operations: no state change, no turn started. */
67
+ export declare const READ_OPERATIONS: readonly ["list_projects", "list_chats", "read_chat"];
68
+ /** Chat write operations. The first four START TURNS (see the RCE note above). */
69
+ export declare const WRITE_OPERATIONS: readonly ["create_project", "create_chat", "fork_chat", "send_message", "fork_chat_batch", "archive_chat", "unarchive_chat"];
70
+ /** Unified trigger operations (Epic T / T3). `list_triggers` is the read one. */
71
+ export declare const TRIGGER_OPERATIONS: readonly ["list_triggers", "set_trigger", "remove_trigger", "run_trigger"];
72
+ /** Every operation the management surface knows about. */
73
+ export declare const ALL_OPERATIONS: readonly string[];
74
+ /**
75
+ * Operations that cause a keeper turn to run — directly (`create_chat`,
76
+ * `send_message`, `fork_chat*`, `run_trigger`) or by arming something that will
77
+ * (`set_trigger`). Granting ANY of these to a principal is granting code
78
+ * execution on the host; kept as an explicit set so that fact stays greppable
79
+ * and so docs/UI can warn on it.
80
+ */
81
+ /**
82
+ * Operations that grant code execution on the host, and so define the risk class
83
+ * a write scope really is. A superset of {@link TURN_SPAWNING_OPERATIONS}:
84
+ * `create_project` starts no turn, but it clones a CALLER-SUPPLIED git URL and
85
+ * creates instance-level state, which belongs in the same warning.
86
+ */
87
+ export declare const HIGH_RISK_OPERATIONS: readonly string[];
88
+ export declare const TURN_SPAWNING_OPERATIONS: readonly string[];
89
+ /**
90
+ * The DEFAULT scope for a configured client that specifies none — read-only
91
+ * across all projects. Matches the shape written on issue #312 verbatim
92
+ * (`allow: ["list_*", "read_chat"]`), which covers the three read tools plus
93
+ * `list_triggers` while excluding every mutating verb.
94
+ */
95
+ export declare const DEFAULT_READ_ONLY_SCOPE: ManagementScope;
96
+ /** Unrestricted scope — used ONLY for the in-process keeper principal. */
97
+ export declare const FULL_SCOPE: ManagementScope;
98
+ /**
99
+ * The implicit full-trust principal for the in-process keeper path.
100
+ *
101
+ * The internal caller is bounded by DEPTH (`maxSpawnDepth`, shipped in #278),
102
+ * not by auth+scope — it has no credential because it isn't a distinct identity.
103
+ * Handing it a full scope keeps a single enforcement path (every op checks a
104
+ * principal) while guaranteeing the keeper behaviour is byte-for-byte unchanged:
105
+ * every check below short-circuits to allow.
106
+ */
107
+ export declare const INTERNAL_PRINCIPAL: ManagementPrincipal;
108
+ /** True for the implicit in-process principal (bounded by depth, not scope). */
109
+ export declare function isInternalPrincipal(principal: ManagementPrincipal): boolean;
110
+ /**
111
+ * Match one pattern against one value. Supports exactly two forms — a bare `"*"`
112
+ * (everything) and a trailing-`*` prefix (`"list_*"`) — plus exact equality.
113
+ *
114
+ * Deliberately NOT a general glob: a security predicate should be trivially
115
+ * auditable by the operator writing it, and the ticket calls for starting with
116
+ * explicit lists and adding globs only as needed. Anything fancier (`?`, `[]`,
117
+ * embedded `*`) is treated as a literal, so a typo'd pattern fails CLOSED
118
+ * (matches nothing) rather than accidentally widening a grant.
119
+ */
120
+ export declare function matchesPattern(pattern: string, value: string): boolean;
121
+ /**
122
+ * Whether `scope` permits invoking `operation`. Deny beats allow; an operation
123
+ * outside {@link ALL_OPERATIONS} is refused regardless of the allow-list, so a
124
+ * typo in config (or a tool added later that policy hasn't been taught about)
125
+ * can never be reached through a stale `"*"` grant.
126
+ */
127
+ export declare function isOperationAllowed(scope: ManagementScope, operation: string): boolean;
128
+ /** Whether `scope` permits touching `projectSlug`. Deny beats allow. */
129
+ export declare function isProjectAllowed(scope: ManagementScope, projectSlug: string): boolean;
130
+ /** The concrete operations `scope` grants, in catalogue order. */
131
+ export declare function allowedOperations(scope: ManagementScope): string[];
132
+ /** True when `scope` grants at least one operation that starts a keeper turn. */
133
+ export declare function grantsTurnSpawning(scope: ManagementScope): boolean;
134
+ export declare const OAUTH_SCOPE_READ = "paddock:read";
135
+ export declare const OAUTH_SCOPE_WRITE = "paddock:write";
136
+ /** Every OAuth scope this resource server understands (for discovery). */
137
+ export declare const OAUTH_SCOPES_SUPPORTED: readonly string[];
138
+ /**
139
+ * The coarse OAuth scope an operation needs. Anything that mutates state or
140
+ * starts a turn is `write`; everything else is `read`.
141
+ */
142
+ export declare function requiredOauthScope(operation: string): string;
143
+ /** The coarse OAuth scopes a fine-grained scope amounts to. */
144
+ export declare function oauthScopesFor(scope: ManagementScope): string[];
145
+ /** True when `scope` grants any operation that amounts to code execution. */
146
+ export declare function grantsCodeExecution(scope: ManagementScope): boolean;
147
+ /**
148
+ * Thrown when policy refuses a call. Carries the structured facts a transport
149
+ * needs to render its own error (MCP `isError` text, or an HTTP 403) and that
150
+ * the server log wants — `clientId` + `operation`, never the credential.
151
+ */
152
+ export declare class ManagementDeniedError extends Error {
153
+ readonly code: "operation_denied" | "project_denied";
154
+ readonly clientId: string;
155
+ readonly operation: string;
156
+ readonly projectSlug?: string;
157
+ constructor(args: {
158
+ code: "operation_denied" | "project_denied";
159
+ clientId: string;
160
+ operation: string;
161
+ projectSlug?: string;
162
+ });
163
+ }
164
+ /** Throw unless `principal` may invoke `operation`. */
165
+ export declare function assertOperation(principal: ManagementPrincipal, operation: string): void;
166
+ /**
167
+ * Throw unless `principal` may reach `projectSlug` via `operation`. Callers that
168
+ * ENUMERATE (list_projects/list_chats) must filter with {@link isProjectAllowed}
169
+ * instead — a scoped client listing its two projects should see those two, not a
170
+ * denial. This assert is for calls that name a target explicitly.
171
+ */
172
+ export declare function assertProject(principal: ManagementPrincipal, operation: string, projectSlug: string): void;