@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.
- 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 +538 -0
- package/packages/server/dist/config.js +625 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/environment-prompt.d.ts +52 -0
- package/packages/server/dist/environment-prompt.js +61 -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 +623 -0
- package/packages/server/dist/herdctl.js +1279 -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 +143 -0
- package/packages/server/dist/instance-config.js +385 -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-Bypd_quL.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
- package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
- package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
- package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.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-C6Uuq-dq.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
- package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.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-B-YuzkF6.js +71 -0
- package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.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-Cs0D32Ry.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-Bc9fuI2i.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.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-BzGHkF9K.js +2 -0
- package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.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-CDmilo8S.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.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-BkUK73W0.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.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,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git routes: the fleet-level remote + GitHub device-flow auth, plus the
|
|
3
|
+
* per-project working-tree status / diff / commit surface (#258). Push is a
|
|
4
|
+
* separate explicit action from a project commit.
|
|
5
|
+
*/
|
|
6
|
+
import type { FastifyInstance } from "fastify";
|
|
7
|
+
import type { RouteCtx } from "../route-context.js";
|
|
8
|
+
export declare function registerGitRoutes(app: FastifyInstance, ctx: RouteCtx): void;
|
|
9
|
+
/**
|
|
10
|
+
* The workspace-scoped git surface: registered once per workspace mount (root
|
|
11
|
+
* and per project) — see `workspace-mount.ts`. Paths are relative to the mount.
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerGitWorkspaceRoutes(app: FastifyInstance, ctx: RouteCtx): void;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { sendProjectError } from "../route-errors.js";
|
|
2
|
+
export function registerGitRoutes(app, ctx) {
|
|
3
|
+
const { git, githubAuth } = ctx;
|
|
4
|
+
// --- git (backing store): fleet-level remote + connection state --------
|
|
5
|
+
app.get("/api/git", {
|
|
6
|
+
schema: {
|
|
7
|
+
tags: ["Git"],
|
|
8
|
+
summary: "Get fleet-level git remote and GitHub connection state",
|
|
9
|
+
description: "Returns the configured backing-store remote details merged with a `github` object describing the current GitHub device-flow connection status.",
|
|
10
|
+
response: {
|
|
11
|
+
200: {
|
|
12
|
+
description: "Object containing the remote configuration fields plus a nested `github` object with connection state.",
|
|
13
|
+
type: "object",
|
|
14
|
+
additionalProperties: true,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}, async () => {
|
|
19
|
+
const [remote, github] = await Promise.all([git.remote(), githubAuth.status()]);
|
|
20
|
+
return { ...remote, github };
|
|
21
|
+
});
|
|
22
|
+
// Push the working tree to origin (the NAS bare repo / configured remote).
|
|
23
|
+
app.post("/api/git/push", {
|
|
24
|
+
schema: {
|
|
25
|
+
tags: ["Git"],
|
|
26
|
+
summary: "Push the working tree to origin",
|
|
27
|
+
description: "Pushes the projects working tree to the configured remote (the NAS bare repo / origin). Returns an object describing the push result.",
|
|
28
|
+
response: {
|
|
29
|
+
200: {
|
|
30
|
+
description: "Object describing the outcome of the push operation.",
|
|
31
|
+
type: "object",
|
|
32
|
+
additionalProperties: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}, async () => {
|
|
37
|
+
return git.push();
|
|
38
|
+
});
|
|
39
|
+
// GitHub device-flow auth: begin → poll → disconnect.
|
|
40
|
+
app.post("/api/git/github/connect", {
|
|
41
|
+
schema: {
|
|
42
|
+
tags: ["Git"],
|
|
43
|
+
summary: "Begin GitHub device-flow authentication",
|
|
44
|
+
description: "Starts the GitHub device-flow authorization and returns the device/user codes and verification URL. Responds with 400 (`{ error }`) if the flow cannot be started.",
|
|
45
|
+
response: {
|
|
46
|
+
200: {
|
|
47
|
+
description: "Object with the GitHub device-flow details (e.g. device code, user code, verification URI).",
|
|
48
|
+
type: "object",
|
|
49
|
+
additionalProperties: true,
|
|
50
|
+
},
|
|
51
|
+
400: {
|
|
52
|
+
description: "Error object `{ error }` when the device flow cannot be started.",
|
|
53
|
+
type: "object",
|
|
54
|
+
additionalProperties: true,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}, async (_req, reply) => {
|
|
59
|
+
try {
|
|
60
|
+
return await githubAuth.startDeviceFlow();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return reply.code(400).send({ error: err.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
app.post("/api/git/github/poll", {
|
|
67
|
+
schema: {
|
|
68
|
+
tags: ["Git"],
|
|
69
|
+
summary: "Poll GitHub device-flow authentication",
|
|
70
|
+
description: "Polls GitHub for completion of a previously-started device flow using the supplied `deviceCode`. Returns the current auth status; responds with 400 (`{ error }`) if `deviceCode` is missing.",
|
|
71
|
+
body: {
|
|
72
|
+
// Documentation-only (no validation/coercion): accept any/empty body.
|
|
73
|
+
type: ["object", "null"],
|
|
74
|
+
additionalProperties: true,
|
|
75
|
+
properties: {
|
|
76
|
+
deviceCode: { description: "The device code returned by the connect step." },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
response: {
|
|
80
|
+
200: {
|
|
81
|
+
description: "Object describing the current device-flow poll result / auth status.",
|
|
82
|
+
type: "object",
|
|
83
|
+
additionalProperties: true,
|
|
84
|
+
},
|
|
85
|
+
400: {
|
|
86
|
+
description: "Error object `{ error }` when `deviceCode` is missing.",
|
|
87
|
+
type: "object",
|
|
88
|
+
additionalProperties: true,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}, async (req, reply) => {
|
|
93
|
+
const code = req.body?.deviceCode;
|
|
94
|
+
if (!code)
|
|
95
|
+
return reply.code(400).send({ error: "deviceCode required" });
|
|
96
|
+
return githubAuth.pollDeviceFlow(code);
|
|
97
|
+
});
|
|
98
|
+
app.post("/api/git/github/disconnect", {
|
|
99
|
+
schema: {
|
|
100
|
+
tags: ["Git"],
|
|
101
|
+
summary: "Disconnect the GitHub connection",
|
|
102
|
+
description: "Clears the stored GitHub credentials / connection. Returns `{ ok: true }` on success.",
|
|
103
|
+
response: {
|
|
104
|
+
200: {
|
|
105
|
+
description: "Object `{ ok: true }` confirming the disconnect.",
|
|
106
|
+
type: "object",
|
|
107
|
+
additionalProperties: true,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}, async () => {
|
|
112
|
+
await githubAuth.disconnect();
|
|
113
|
+
return { ok: true };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The workspace-scoped git surface: registered once per workspace mount (root
|
|
118
|
+
* and per project) — see `workspace-mount.ts`. Paths are relative to the mount.
|
|
119
|
+
*/
|
|
120
|
+
export function registerGitWorkspaceRoutes(app, ctx) {
|
|
121
|
+
const { git, projects } = ctx;
|
|
122
|
+
// --- git (backing-store capability, phase 1: read surface) -------------
|
|
123
|
+
// Uncommitted changes confined to this project's subtree. Returns
|
|
124
|
+
// `{ repo: false }` when the projects dir isn't a git working tree, so the
|
|
125
|
+
// UI hides the git affordance entirely. Never throws on git errors.
|
|
126
|
+
app.get("/git/status", {
|
|
127
|
+
schema: {
|
|
128
|
+
tags: ["Git"],
|
|
129
|
+
summary: "Get a project's git working-tree status",
|
|
130
|
+
description: "Returns the uncommitted changes confined to this project's subtree. Returns `{ repo: false }` when the projects dir isn't a git working tree. Never throws on git errors.",
|
|
131
|
+
params: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
slug: { type: "string", description: "The project slug." },
|
|
135
|
+
},
|
|
136
|
+
required: ["slug"],
|
|
137
|
+
},
|
|
138
|
+
response: {
|
|
139
|
+
200: {
|
|
140
|
+
description: "Object describing the project's working-tree status, or `{ repo: false }` when not a git working tree.",
|
|
141
|
+
type: "object",
|
|
142
|
+
additionalProperties: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}, async (req, reply) => {
|
|
147
|
+
try {
|
|
148
|
+
const project = await projects.get(req.params.slug);
|
|
149
|
+
return await git.projectStatus(project.dir);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return sendProjectError(reply, err);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// Unified diff for the project's tracked changes (working tree vs HEAD), or a
|
|
156
|
+
// single file via ?file=. Untracked files are reported by /git/status instead.
|
|
157
|
+
app.get("/git/diff", {
|
|
158
|
+
schema: {
|
|
159
|
+
tags: ["Git"],
|
|
160
|
+
summary: "Get a project's unified diff",
|
|
161
|
+
description: "Returns the raw unified diff (text/plain) for the project's tracked changes (working tree vs HEAD), or for a single file via `?file=`. Untracked files are reported by /git/status instead.",
|
|
162
|
+
params: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
slug: { type: "string", description: "The project slug." },
|
|
166
|
+
},
|
|
167
|
+
required: ["slug"],
|
|
168
|
+
},
|
|
169
|
+
querystring: {
|
|
170
|
+
type: "object",
|
|
171
|
+
additionalProperties: true,
|
|
172
|
+
properties: {
|
|
173
|
+
file: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Optional project-relative path to diff a single file.",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
produces: ["text/plain"],
|
|
180
|
+
},
|
|
181
|
+
}, async (req, reply) => {
|
|
182
|
+
try {
|
|
183
|
+
const project = await projects.get(req.params.slug);
|
|
184
|
+
const diff = await git.projectDiff(project.dir, req.query.file);
|
|
185
|
+
reply.header("content-type", "text/plain; charset=utf-8");
|
|
186
|
+
return diff;
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
return sendProjectError(reply, err);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Commit this project's pending changes (phase 2). `committed: false` when
|
|
193
|
+
// there was nothing to commit. Push is a separate explicit action (/api/git/push).
|
|
194
|
+
app.post("/git/commit", {
|
|
195
|
+
schema: {
|
|
196
|
+
tags: ["Git"],
|
|
197
|
+
summary: "Commit a project's pending changes",
|
|
198
|
+
description: "Commits the project's pending changes. Optional `files` (project-relative) commits only those changes; omitted commits the whole subtree. Returns an object where `committed: false` indicates there was nothing to commit. Push is a separate explicit action (/api/git/push).",
|
|
199
|
+
params: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
slug: { type: "string", description: "The project slug." },
|
|
203
|
+
},
|
|
204
|
+
required: ["slug"],
|
|
205
|
+
},
|
|
206
|
+
body: {
|
|
207
|
+
// Documentation-only (no validation/coercion): accept any/empty body.
|
|
208
|
+
type: ["object", "null"],
|
|
209
|
+
additionalProperties: true,
|
|
210
|
+
properties: {
|
|
211
|
+
message: { description: "Optional commit message; defaults to `Update <project name>`." },
|
|
212
|
+
files: { description: "Optional project-relative paths (array) to commit only those changes." },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
response: {
|
|
216
|
+
200: {
|
|
217
|
+
description: "Object describing the commit result; `committed: false` when there was nothing to commit.",
|
|
218
|
+
type: "object",
|
|
219
|
+
additionalProperties: true,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}, async (req, reply) => {
|
|
224
|
+
try {
|
|
225
|
+
const project = await projects.get(req.params.slug);
|
|
226
|
+
const message = req.body?.message?.trim() || `Update ${project.name}`;
|
|
227
|
+
// Optional `files` (project-relative) commits only those changes (#258);
|
|
228
|
+
// omitted ⇒ commit the whole subtree (legacy behavior).
|
|
229
|
+
const files = Array.isArray(req.body?.files) ? req.body?.files : undefined;
|
|
230
|
+
return await git.commitProject(project.dir, message, files);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
return sendProjectError(reply, err);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/mcp` Management API endpoint (issue #312).
|
|
3
|
+
*
|
|
4
|
+
* M1 shipped the gate (authentication, principal resolution, fail-closed
|
|
5
|
+
* behaviour); M2 mounts the streamable-HTTP MCP transport behind it and the RFC
|
|
6
|
+
* 9728 discovery document beside it.
|
|
7
|
+
*
|
|
8
|
+
* ── The response matrix ────────────────────────────────────────────────────
|
|
9
|
+
* 404 — no `managementApi.clients` (or no `publicUrl`). The endpoint DOES
|
|
10
|
+
* NOT EXIST until an operator deliberately turns it on. This is the
|
|
11
|
+
* backstop for the deploy-ordering hazard: exempting `/mcp` at an edge
|
|
12
|
+
* proxy removes the only other gate in front of it.
|
|
13
|
+
* 401 — credential missing or bad. Always `401` + `WWW-Authenticate`, NEVER
|
|
14
|
+
* a 302 to a login page: an MCP client cannot follow a redirect, and
|
|
15
|
+
* OAuth discovery reads this exact challenge.
|
|
16
|
+
* 403 — authenticated but out of scope (`insufficient_scope`), or plaintext
|
|
17
|
+
* from a non-loopback client. `X-Forwarded-Proto: https` lifts that
|
|
18
|
+
* refusal only when the immediate peer is a trusted proxy (#474) —
|
|
19
|
+
* a client cannot vouch for its own transport.
|
|
20
|
+
* 405 — GET/DELETE. In stateless mode the transport answers a GET with an SSE
|
|
21
|
+
* stream that never emits anything, so a client would hang forever on a
|
|
22
|
+
* socket with no headers.
|
|
23
|
+
*
|
|
24
|
+
* Authentication runs in `onRequest` — BEFORE body parsing — so a malformed or
|
|
25
|
+
* oversized body can never preempt the auth decision.
|
|
26
|
+
*/
|
|
27
|
+
import type { FastifyInstance, FastifyRequest } from "fastify";
|
|
28
|
+
import type { RouteCtx } from "../route-context.js";
|
|
29
|
+
import type { ManagementPrincipal } from "../management-policy.js";
|
|
30
|
+
import type { ManagementApiConfig } from "../management-config.js";
|
|
31
|
+
/** The URL path the management surface is mounted at. */
|
|
32
|
+
export declare const MCP_PATH = "/mcp";
|
|
33
|
+
declare module "fastify" {
|
|
34
|
+
interface FastifyRequest {
|
|
35
|
+
/**
|
|
36
|
+
* The authenticated Management API caller, set by the `/mcp` guard. Absent
|
|
37
|
+
* on every other route — this is NOT the browser user (`req.user`), which is
|
|
38
|
+
* a separate, independent identity (see management-auth.ts).
|
|
39
|
+
*/
|
|
40
|
+
managementPrincipal?: ManagementPrincipal;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** How a request earned (or failed to earn) its "secure" verdict. */
|
|
44
|
+
export interface TransportSecurity {
|
|
45
|
+
secure: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* `tls` — real TLS at this process; `forwarded` — a TRUSTED proxy said https;
|
|
48
|
+
* `loopback` — the client is on this host; `spoofable` — a forwarded https
|
|
49
|
+
* claim we refused because the peer is not a trusted proxy; `plaintext` —
|
|
50
|
+
* no claim at all.
|
|
51
|
+
*/
|
|
52
|
+
via: "tls" | "forwarded" | "loopback" | "spoofable" | "plaintext";
|
|
53
|
+
/** The immediate peer's socket address, for the log line and the 403 body. */
|
|
54
|
+
peer: string;
|
|
55
|
+
/**
|
|
56
|
+
* True when the verdict rests on a forwarded header believed under the
|
|
57
|
+
* DEFAULT trust list rather than an operator-named proxy — worth telling the
|
|
58
|
+
* operator once, since only an explicit list makes this a control (#474).
|
|
59
|
+
*/
|
|
60
|
+
assumedProxy: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether the request reached us over a secure channel.
|
|
64
|
+
*
|
|
65
|
+
* Three ways to be secure: real TLS at this process, a TLS-terminating proxy
|
|
66
|
+
* that set `X-Forwarded-Proto: https`, or a loopback client (nothing left the
|
|
67
|
+
* host, so there is no wire to sniff). Everything else is plaintext over a real
|
|
68
|
+
* network, where a bearer token would be readable in transit — refused,
|
|
69
|
+
* mirroring the spirit of the bind-safety guard (#435).
|
|
70
|
+
*
|
|
71
|
+
* The forwarded case is the one #474 tightened: the header is believed only
|
|
72
|
+
* when the IMMEDIATE PEER — the socket address, which no client can set — is a
|
|
73
|
+
* trusted proxy. See trusted-proxy.ts for the trust list and its default.
|
|
74
|
+
*
|
|
75
|
+
* Note what this deliberately does NOT use: Fastify's `trustProxy`. In the
|
|
76
|
+
* version pinned here (4.28) enabling it makes `req.protocol` return
|
|
77
|
+
* `x-forwarded-proto` whenever the header is PRESENT, without consulting the
|
|
78
|
+
* trust function at all — the trust check applies to `req.ip`/`req.ips` only.
|
|
79
|
+
* Building this guard on `req.protocol` would therefore reproduce the exact bug
|
|
80
|
+
* it is meant to close, so the peer check is done here, explicitly.
|
|
81
|
+
*/
|
|
82
|
+
export declare function evaluateTransportSecurity(req: FastifyRequest, cfg: ManagementApiConfig): TransportSecurity;
|
|
83
|
+
export declare function registerMcpRoutes(app: FastifyInstance, ctx: RouteCtx): void;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
|
+
import { authenticateManagementRequest, challengeHeader, isManagementApiEnabled, resourceMetadataUrl, } from "../management-auth.js";
|
|
4
|
+
import { buildProtectedResourceMetadata, PROTECTED_RESOURCE_METADATA_PREFIX, MCP_RESOURCE_PATH, } from "../management-metadata.js";
|
|
5
|
+
import { buildManagementOps, enforceManagementPolicy } from "../management-ops.js";
|
|
6
|
+
import { selfMcpServerDefForOps } from "../ws-self-mcp.js";
|
|
7
|
+
import { buildManagementMcpServer } from "../management-mcp-server.js";
|
|
8
|
+
import { HUMAN_ROOT } from "../run-provenance.js";
|
|
9
|
+
import { defaultTrustedProxies, isLoopbackAddress, isTrustedProxy } from "../trusted-proxy.js";
|
|
10
|
+
/** The URL path the management surface is mounted at. */
|
|
11
|
+
export const MCP_PATH = MCP_RESOURCE_PATH;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the request reached us over a secure channel.
|
|
14
|
+
*
|
|
15
|
+
* Three ways to be secure: real TLS at this process, a TLS-terminating proxy
|
|
16
|
+
* that set `X-Forwarded-Proto: https`, or a loopback client (nothing left the
|
|
17
|
+
* host, so there is no wire to sniff). Everything else is plaintext over a real
|
|
18
|
+
* network, where a bearer token would be readable in transit — refused,
|
|
19
|
+
* mirroring the spirit of the bind-safety guard (#435).
|
|
20
|
+
*
|
|
21
|
+
* The forwarded case is the one #474 tightened: the header is believed only
|
|
22
|
+
* when the IMMEDIATE PEER — the socket address, which no client can set — is a
|
|
23
|
+
* trusted proxy. See trusted-proxy.ts for the trust list and its default.
|
|
24
|
+
*
|
|
25
|
+
* Note what this deliberately does NOT use: Fastify's `trustProxy`. In the
|
|
26
|
+
* version pinned here (4.28) enabling it makes `req.protocol` return
|
|
27
|
+
* `x-forwarded-proto` whenever the header is PRESENT, without consulting the
|
|
28
|
+
* trust function at all — the trust check applies to `req.ip`/`req.ips` only.
|
|
29
|
+
* Building this guard on `req.protocol` would therefore reproduce the exact bug
|
|
30
|
+
* it is meant to close, so the peer check is done here, explicitly.
|
|
31
|
+
*/
|
|
32
|
+
export function evaluateTransportSecurity(req, cfg) {
|
|
33
|
+
const peer = req.socket.remoteAddress ?? "";
|
|
34
|
+
const base = { peer, assumedProxy: false };
|
|
35
|
+
// `req.socket.encrypted` rather than `req.protocol`: with trustProxy off they
|
|
36
|
+
// agree, and this cannot be influenced by a header even if that changes.
|
|
37
|
+
if (req.socket.encrypted) {
|
|
38
|
+
return { ...base, secure: true, via: "tls" };
|
|
39
|
+
}
|
|
40
|
+
const fwd = req.headers["x-forwarded-proto"];
|
|
41
|
+
const proto = (Array.isArray(fwd) ? fwd[0] : fwd)?.split(",")[0]?.trim().toLowerCase();
|
|
42
|
+
const trusted = cfg.trustedProxies ?? defaultTrustedProxies();
|
|
43
|
+
if (proto === "https") {
|
|
44
|
+
if (isTrustedProxy(peer, trusted)) {
|
|
45
|
+
return {
|
|
46
|
+
...base,
|
|
47
|
+
secure: true,
|
|
48
|
+
via: "forwarded",
|
|
49
|
+
assumedProxy: !trusted.explicit,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// A loopback client that spoofs the header is still secure — by being on
|
|
53
|
+
// loopback, not by asking. Fall through so it gets the honest reason.
|
|
54
|
+
if (!isLoopbackAddress(peer))
|
|
55
|
+
return { ...base, secure: false, via: "spoofable" };
|
|
56
|
+
}
|
|
57
|
+
if (isLoopbackAddress(peer))
|
|
58
|
+
return { ...base, secure: true, via: "loopback" };
|
|
59
|
+
return { ...base, secure: false, via: "plaintext" };
|
|
60
|
+
}
|
|
61
|
+
export function registerMcpRoutes(app, ctx) {
|
|
62
|
+
const { cfg, managementOpsContext } = ctx;
|
|
63
|
+
// ── Discovery (unauthenticated by design) ────────────────────────────────
|
|
64
|
+
// A client fetches this BEFORE it holds any credential, so gating it would
|
|
65
|
+
// make discovery impossible. The document names the authorization server and
|
|
66
|
+
// supported scopes — never a secret.
|
|
67
|
+
//
|
|
68
|
+
// Served at BOTH the path-inserted URL (what clients actually request for a
|
|
69
|
+
// resource with a path) and the bare root (harmless, and other clients may
|
|
70
|
+
// use it). Still 404s when the surface is off, so a disabled instance doesn't
|
|
71
|
+
// advertise a management API.
|
|
72
|
+
const metadataHandler = async (_req, reply) => {
|
|
73
|
+
if (!isManagementApiEnabled(cfg.managementApi)) {
|
|
74
|
+
return reply.code(404).send({ error: "not found" });
|
|
75
|
+
}
|
|
76
|
+
const doc = buildProtectedResourceMetadata(cfg.managementApi);
|
|
77
|
+
if (!doc)
|
|
78
|
+
return reply.code(404).send({ error: "not found" });
|
|
79
|
+
return reply
|
|
80
|
+
.header("access-control-allow-origin", "*")
|
|
81
|
+
.header("cache-control", "public, max-age=300")
|
|
82
|
+
.type("application/json; charset=utf-8")
|
|
83
|
+
.send(doc);
|
|
84
|
+
};
|
|
85
|
+
for (const url of [
|
|
86
|
+
`${PROTECTED_RESOURCE_METADATA_PREFIX}${MCP_RESOURCE_PATH}`,
|
|
87
|
+
PROTECTED_RESOURCE_METADATA_PREFIX,
|
|
88
|
+
]) {
|
|
89
|
+
app.get(url, { schema: { hide: true } }, metadataHandler);
|
|
90
|
+
}
|
|
91
|
+
// ── The gate ─────────────────────────────────────────────────────────────
|
|
92
|
+
// Peers we have already warned about (see the assumedProxy branch below).
|
|
93
|
+
// Per-app, and capped, so a hostile spread of source addresses can't grow it.
|
|
94
|
+
const warnedPeers = new Set();
|
|
95
|
+
const guard = async (req, reply) => {
|
|
96
|
+
if (!isManagementApiEnabled(cfg.managementApi)) {
|
|
97
|
+
return reply.code(404).send({ error: "not found" });
|
|
98
|
+
}
|
|
99
|
+
const transport = evaluateTransportSecurity(req, cfg.managementApi);
|
|
100
|
+
if (!transport.secure) {
|
|
101
|
+
req.log.warn({ url: req.url, peer: transport.peer, via: transport.via }, transport.via === "spoofable"
|
|
102
|
+
? "management API: refusing a plaintext request whose X-Forwarded-Proto: https came from a peer that is not a trusted proxy (#474)"
|
|
103
|
+
: "management API: refusing a plaintext non-loopback request (a bearer token would be readable in transit)");
|
|
104
|
+
return reply.code(403).send({
|
|
105
|
+
error: "https required",
|
|
106
|
+
code: "insecure_transport",
|
|
107
|
+
message: transport.via === "spoofable"
|
|
108
|
+
? `The management API refuses plaintext requests from non-loopback clients. ` +
|
|
109
|
+
`X-Forwarded-Proto: https was ignored because the peer (${transport.peer}) is not a ` +
|
|
110
|
+
`trusted proxy — a client cannot vouch for its own transport. If that address IS ` +
|
|
111
|
+
`your TLS-terminating proxy, list it in managementApi.trustedProxies (or ` +
|
|
112
|
+
`PADDOCK_MANAGEMENT_TRUSTED_PROXIES); otherwise terminate TLS in front of Paddock, ` +
|
|
113
|
+
`or connect over loopback.`
|
|
114
|
+
: `The management API refuses plaintext requests from non-loopback clients ` +
|
|
115
|
+
`(peer: ${transport.peer || "unknown"}). Terminate TLS in front of Paddock (and ` +
|
|
116
|
+
`forward X-Forwarded-Proto from a trusted proxy), or connect over loopback.`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Believed a forwarded scheme from a peer that only matched the DEFAULT
|
|
120
|
+
// trust list. It is probably the operator's proxy — but nothing proves it,
|
|
121
|
+
// and if it is not, the token in this request just crossed a network in
|
|
122
|
+
// clear. Say so once per peer rather than on every request.
|
|
123
|
+
if (transport.assumedProxy && !warnedPeers.has(transport.peer)) {
|
|
124
|
+
if (warnedPeers.size < 32)
|
|
125
|
+
warnedPeers.add(transport.peer);
|
|
126
|
+
req.log.warn({ peer: transport.peer }, `management API: believing X-Forwarded-Proto: https from ${transport.peer}, which matched ` +
|
|
127
|
+
`the DEFAULT trusted-proxy list (loopback + private address space) rather than a proxy ` +
|
|
128
|
+
`you named. If that peer is not your TLS terminator, bearer tokens are reaching this ` +
|
|
129
|
+
`instance in plaintext. Set managementApi.trustedProxies to make this a control (#474).`);
|
|
130
|
+
}
|
|
131
|
+
const result = authenticateManagementRequest(req.headers.authorization, cfg.managementApi);
|
|
132
|
+
if (!result.ok) {
|
|
133
|
+
// Log the failure and the caller, never the presented credential.
|
|
134
|
+
req.log.warn({ url: req.url, failure: result.failure, ip: req.socket.remoteAddress }, "management API: authentication failed");
|
|
135
|
+
return reply
|
|
136
|
+
.code(401)
|
|
137
|
+
.header("WWW-Authenticate", challengeHeader(result.failure, resourceMetadataUrl(cfg.managementApi)))
|
|
138
|
+
.send({ error: "unauthorized", code: "auth_required" });
|
|
139
|
+
}
|
|
140
|
+
req.managementPrincipal = result.principal;
|
|
141
|
+
};
|
|
142
|
+
// ── GET / DELETE → 405 ───────────────────────────────────────────────────
|
|
143
|
+
// Stateless transport: a GET would open an SSE stream that never emits, so the
|
|
144
|
+
// client waits forever on a header-less socket. Refuse explicitly instead.
|
|
145
|
+
app.route({
|
|
146
|
+
method: ["GET", "DELETE"],
|
|
147
|
+
url: MCP_PATH,
|
|
148
|
+
schema: { hide: true },
|
|
149
|
+
onRequest: guard,
|
|
150
|
+
handler: async (_req, reply) => reply
|
|
151
|
+
.code(405)
|
|
152
|
+
.header("allow", "POST")
|
|
153
|
+
.send({
|
|
154
|
+
jsonrpc: "2.0",
|
|
155
|
+
error: {
|
|
156
|
+
code: -32000,
|
|
157
|
+
message: "Method not allowed. Use POST for the MCP endpoint.",
|
|
158
|
+
},
|
|
159
|
+
id: null,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
// ── POST → the MCP transport ─────────────────────────────────────────────
|
|
163
|
+
app.route({
|
|
164
|
+
method: ["POST"],
|
|
165
|
+
url: MCP_PATH,
|
|
166
|
+
// Kept out of the OpenAPI document (#466): this is a JSON-RPC MCP endpoint,
|
|
167
|
+
// not a REST resource, and a body schema here would become an active
|
|
168
|
+
// request validator over MCP payloads.
|
|
169
|
+
schema: { hide: true },
|
|
170
|
+
onRequest: guard,
|
|
171
|
+
handler: async (req, reply) => {
|
|
172
|
+
const principal = req.managementPrincipal;
|
|
173
|
+
if (!principal)
|
|
174
|
+
return reply.code(401).send({ error: "unauthorized" });
|
|
175
|
+
if (!managementOpsContext) {
|
|
176
|
+
// Wiring error rather than a client error — the surface is configured
|
|
177
|
+
// but the server didn't hand the route its ops context.
|
|
178
|
+
req.log.error("management API: ops context unavailable; /mcp cannot serve requests");
|
|
179
|
+
return reply.code(503).send({ error: "unavailable", code: "ops_unavailable" });
|
|
180
|
+
}
|
|
181
|
+
// Ops are built PER REQUEST, bound to this principal, then policed. An
|
|
182
|
+
// external caller has no "current chat", so it addresses everything
|
|
183
|
+
// explicitly and its spawns are rooted like a human's.
|
|
184
|
+
const ops = buildManagementOps(managementOpsContext, {
|
|
185
|
+
currentProjectSlug: "",
|
|
186
|
+
currentSessionId: () => null,
|
|
187
|
+
parentProvenance: HUMAN_ROOT,
|
|
188
|
+
// All three capability gates are opened here and NARROWED by scope
|
|
189
|
+
// immediately below. That is the right split for an external caller:
|
|
190
|
+
// these per-project/per-instance gates exist to bound what a KEEPER may
|
|
191
|
+
// reach, whereas an external client is bounded by its credential. So a
|
|
192
|
+
// client gets `create_project` only by naming it in `allow` — the
|
|
193
|
+
// read-only default excludes it, as it does every other mutation.
|
|
194
|
+
includeWrite: true,
|
|
195
|
+
includeTriggers: true,
|
|
196
|
+
includeProjects: true,
|
|
197
|
+
spawnDepthCap: principal.scope.maxSpawnDepth,
|
|
198
|
+
});
|
|
199
|
+
const policed = enforceManagementPolicy(ops, principal);
|
|
200
|
+
const def = selfMcpServerDefForOps(policed, principal);
|
|
201
|
+
const server = buildManagementMcpServer(def, principal);
|
|
202
|
+
// Stateless: a fresh server+transport per request. No session store, no
|
|
203
|
+
// cross-request state, restart-safe — and a principal's tool visibility
|
|
204
|
+
// can never leak into another caller's session.
|
|
205
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
206
|
+
sessionIdGenerator: undefined,
|
|
207
|
+
});
|
|
208
|
+
await server.connect(transport);
|
|
209
|
+
const headers = new Headers();
|
|
210
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
211
|
+
if (v === undefined)
|
|
212
|
+
continue;
|
|
213
|
+
for (const one of Array.isArray(v) ? v : [v])
|
|
214
|
+
headers.append(k, String(one));
|
|
215
|
+
}
|
|
216
|
+
const base = cfg.managementApi.publicUrl ?? "http://localhost";
|
|
217
|
+
const response = await transport.handleRequest(new Request(new URL(req.url, base), { method: "POST", headers }), {
|
|
218
|
+
// MANDATORY: Fastify has already drained the socket, so without this
|
|
219
|
+
// the transport calls .json() on an empty body and reports
|
|
220
|
+
// "Parse error: Invalid JSON" for perfectly valid input.
|
|
221
|
+
parsedBody: req.body,
|
|
222
|
+
});
|
|
223
|
+
// Flush response headers BEFORE the body. Node buffers writeHead() until
|
|
224
|
+
// the first body byte, so a slow tool call (every turn-spawning op is one)
|
|
225
|
+
// would stall the response headers for its whole duration — measured at
|
|
226
|
+
// 3011ms vs 28ms — which reads as a hang to a proxy with a header timeout.
|
|
227
|
+
// `forEach` rather than `entries()`: the server tsconfig's lib doesn't
|
|
228
|
+
// include the DOM iterable declarations, so `Headers` has no iterator here.
|
|
229
|
+
const outHeaders = {};
|
|
230
|
+
response.headers.forEach((value, key) => {
|
|
231
|
+
outHeaders[key] = value;
|
|
232
|
+
});
|
|
233
|
+
reply.raw.writeHead(response.status, outHeaders);
|
|
234
|
+
reply.raw.flushHeaders();
|
|
235
|
+
if (!response.body) {
|
|
236
|
+
reply.raw.end();
|
|
237
|
+
void server.close();
|
|
238
|
+
return reply.hijack();
|
|
239
|
+
}
|
|
240
|
+
const body = Readable.fromWeb(response.body);
|
|
241
|
+
body.pipe(reply.raw);
|
|
242
|
+
reply.raw.on("close", () => {
|
|
243
|
+
body.destroy();
|
|
244
|
+
void server.close();
|
|
245
|
+
});
|
|
246
|
+
// Hijack tells Fastify we own the socket — the SDK has already written it.
|
|
247
|
+
return reply.hijack();
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance/identity/metadata routes + the attachment byte surface: voice
|
|
3
|
+
* transcription, `/api/me`, health, `/api/models`, `/api/fleet`,
|
|
4
|
+
* instance-wide settings (issue #385), the inbound composer
|
|
5
|
+
* upload (#328), and the range-serving `/api/chat-files/:id` endpoint (#112/#126).
|
|
6
|
+
*/
|
|
7
|
+
import type { FastifyInstance } from "fastify";
|
|
8
|
+
import { type RouteCtx } from "../route-context.js";
|
|
9
|
+
export declare function registerMetaRoutes(app: FastifyInstance, ctx: RouteCtx): void;
|
|
10
|
+
/**
|
|
11
|
+
* The workspace-scoped slice of this file: registered once per workspace mount
|
|
12
|
+
* (root and per project) — see `workspace-mount.ts`. Paths are relative to the
|
|
13
|
+
* mount.
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerMetaWorkspaceRoutes(app: FastifyInstance, ctx: RouteCtx): void;
|