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