@edspencer/paddock 0.62.0 → 0.63.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 +32 -4
- package/package.json +2 -2
- package/packages/server/dist/app.js +14 -1
- package/packages/server/dist/claude-mcp.d.ts +16 -4
- package/packages/server/dist/claude-mcp.js +83 -61
- package/packages/server/dist/claude-plugins.d.ts +131 -0
- package/packages/server/dist/claude-plugins.js +394 -0
- package/packages/server/dist/cli/args.d.ts +1 -1
- package/packages/server/dist/cli/args.js +9 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +7 -1
- package/packages/server/dist/herdctl-agent-config.js +20 -4
- package/packages/server/dist/herdctl.d.ts +7 -1
- package/packages/server/dist/herdctl.js +9 -2
- package/packages/server/dist/mcp-servers.d.ts +31 -12
- package/packages/server/dist/mcp-servers.js +127 -33
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-Ca905oey.js → InstanceConfigPage-CCk2hVT9.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-ChlC1ezH.js → MarkdownRenderer-B6gST_0U.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-CslqbhUb.js → ProjectRedirect-BCmKT4mf.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-DkNdilGo.js → ProjectView-CCN98MWu.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-BrlvLc-Y.js → ProjectsGrid-CYzuN_yD.js} +1 -1
- package/packages/web/dist/assets/{arc-CGqCI5nQ.js → arc-BxvtMCZQ.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Cp4KTxE2.js → architectureDiagram-3BPJPVTR-Bp6ffJ_u.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-yFxaTv04.js → blockDiagram-GPEHLZMM-BIY2X8nW.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-59lhWjRt.js → c4Diagram-AAUBKEIU-Br9stEJS.js} +1 -1
- package/packages/web/dist/assets/channel-CMdgNeFH.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-BrpXGnJg.js → chunk-2J33WTMH-Djc7uqMV.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-UxBDb5pi.js → chunk-4BX2VUAB-yEVMLksS.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-D4ZJyUHW.js → chunk-55IACEB6-tJaEdPZg.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DWdPj_8E.js → chunk-727SXJPM-bWEqh672.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BIpNbflY.js → chunk-AQP2D5EJ-B8U9fB-T.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-f6iJDfdI.js → chunk-FMBD7UC4-BeEITYig.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-Bz3TKIJk.js → chunk-ND2GUHAM-L2rpLaoK.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-buQmkafK.js → chunk-QZHKN3VN-qqzPMZL_.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +2 -0
- package/packages/web/dist/assets/{core-TyFI6p0n.js → core-CSaNbmLO.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CIkdi0XF.js → cose-bilkent-S5V4N54A-BRF7Nech.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-BnLJCfNs.js → dagre-BM42HDAG-D10rJWOR.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-D0OqfDB9.js → diagram-2AECGRRQ-DGjT5mAO.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-DQ2spGrK.js → diagram-5GNKFQAL-DaH3bxHh.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-RDIVQ_S3.js → diagram-KO2AKTUF-QrLW9Wn6.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DKoTfh2C.js → diagram-LMA3HP47-BX15wgYN.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-6k0OuSAL.js → diagram-OG6HWLK6-DXQ_p63s.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B-oQaYAC.js → erDiagram-TEJ5UH35-C_ioRruE.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-B7qpYe9C.js → flowDiagram-I6XJVG4X-7d-xLXLM.js} +1 -1
- package/packages/web/dist/assets/{format-PHdEBNOi.js → format-BSDZezwD.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-SfBsqMmx.js → ganttDiagram-6RSMTGT7-DZYx5xcj.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CuM4Rl_i.js → gitGraphDiagram-PVQCEYII-dimo-osm.js} +1 -1
- package/packages/web/dist/assets/{index-D_1ogxg1.js → index-CUnFKMga.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-Dq-32Oj4.js → infoDiagram-5YYISTIA-BzApY25x.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BLFHCj_o.js → ishikawaDiagram-YF4QCWOH-DtX47_9p.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CHP-Yu63.js → journeyDiagram-JHISSGLW-C7fITKg1.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DRuehSqn.js → kanban-definition-UN3LZRKU-BOrWLf_W.js} +1 -1
- package/packages/web/dist/assets/{linear-Bwz2vnH7.js → linear-DuyAaBu6.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-CeMMnCip.js → mermaid.core-BQlzuVvO.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CEdqeoRW.js → mindmap-definition-RKZ34NQL-CmP748B5.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BdDXBx87.js → pieDiagram-4H26LBE5-CRd5Pdfq.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bovd1Zhx.js → quadrantDiagram-W4KKPZXB-DpJm0Yw_.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-DiyrKnJ2.js → requirementDiagram-4Y6WPE33-B0o2PRMC.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BuX7TRVw.js → sankeyDiagram-5OEKKPKP-CBi3Ek3v.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-BjetZWJo.js → sequenceDiagram-3UESZ5HK-C1stxfQg.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-4AUBV6dF.js → stateDiagram-AJRCARHV-lCkiETWK.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BpQ8g8Bm.js → timeline-definition-PNZ67QCA-D3DkWJMC.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Cj99BWER.js → vennDiagram-CIIHVFJN-CO2-yC7K.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-S7AGTOWw.js → wardley-L42UT6IY-Dfjp0kqX.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-LNXkUA7S.js → wardleyDiagram-YWT4CUSO-CIl0OPwS.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DGdTh4Xk.js → xychartDiagram-2RQKCTM6-CuvlQuup.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/channel-CZs6Z1i5.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Cq84MOtb.js +0 -2
package/README.md
CHANGED
|
@@ -40,13 +40,17 @@ Paddock says at startup if it can find no credentials at all.
|
|
|
40
40
|
## What else Paddock does and does not take from your `~/.claude`
|
|
41
41
|
|
|
42
42
|
Apart from that login: **nothing, by default.** Your `CLAUDE.md`, `agents/`,
|
|
43
|
-
`commands/` and `plugins/` are not loaded,
|
|
44
|
-
|
|
43
|
+
`commands/` and `plugins/` are not loaded, the hooks your `settings.json` binds to tool
|
|
44
|
+
use do not run, your transcripts are not touched, and your `~/.claude.json` MCP servers
|
|
45
|
+
are not attached. Each is one key in `<data-dir>/paddock.config.yaml`:
|
|
45
46
|
|
|
46
47
|
```yaml
|
|
47
48
|
claude:
|
|
49
|
+
transcripts: host # own | host, default own — whose session transcripts
|
|
50
|
+
credentials: host # own | host, default host — the one shared by default
|
|
48
51
|
instructions: host # own | host, default own — CLAUDE.md, agents, commands, plugins
|
|
49
52
|
hooks: host # own | host, default own — shell commands settings.json binds
|
|
53
|
+
mcpServers: host # own | host, default own — the servers in your ~/.claude.json
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
`instructions: own` is worth knowing about if you have curated a `~/.claude/CLAUDE.md`:
|
|
@@ -55,6 +59,22 @@ always applies. `hooks` is off by default because inheriting someone's shell com
|
|
|
55
59
|
not a thing to discover after the fact; the rest of your `settings.json` — permissions,
|
|
56
60
|
model, statusline — applies either way.
|
|
57
61
|
|
|
62
|
+
To give this instance an MCP server your machine does not have — the case `host`
|
|
63
|
+
cannot serve — declare it in a **sibling** `mcpServers:` block of the same file, using
|
|
64
|
+
`env:VAR_NAME` anywhere a string goes so the token stays out of the git-tracked file
|
|
65
|
+
(keep `driveMode` on its default `session` for a server holding a credential — `batch`
|
|
66
|
+
passes the definition to `claude` as a command-line argument, where any local user can
|
|
67
|
+
read it):
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
mcpServers:
|
|
71
|
+
notion:
|
|
72
|
+
command: npx
|
|
73
|
+
args: ["-y", "@notionhq/notion-mcp-server"]
|
|
74
|
+
env:
|
|
75
|
+
NOTION_TOKEN: env:NOTION_TOKEN
|
|
76
|
+
```
|
|
77
|
+
|
|
58
78
|
## Open your own project
|
|
59
79
|
|
|
60
80
|
```sh
|
|
@@ -118,10 +138,18 @@ A multi-arch Docker image is published alongside this package, and is the better
|
|
|
118
138
|
fit for a server deployment:
|
|
119
139
|
|
|
120
140
|
```sh
|
|
121
|
-
docker run -d -p 4000:4000 -v /srv/paddock-data:/data \
|
|
122
|
-
-e CLAUDE_CODE_OAUTH_TOKEN=…
|
|
141
|
+
docker run -d -p 127.0.0.1:4000:4000 -v /srv/paddock-data:/data \
|
|
142
|
+
-e CLAUDE_CODE_OAUTH_TOKEN=… \
|
|
143
|
+
-e PADDOCK_DANGEROUSLY_ALLOW_OPEN=1 \
|
|
144
|
+
ghcr.io/edspencer/paddock:latest
|
|
123
145
|
```
|
|
124
146
|
|
|
147
|
+
`PADDOCK_DANGEROUSLY_ALLOW_OPEN=1` is required: the image binds `0.0.0.0` and the
|
|
148
|
+
default auth mode is `none`, and Paddock refuses to bind a routable interface
|
|
149
|
+
unauthenticated unless told to. Publishing on `127.0.0.1:` is what makes that
|
|
150
|
+
safe — Paddock runs code and spends Claude tokens, so put an auth mode or a
|
|
151
|
+
reverse proxy in front of it before exposing the port to a network.
|
|
152
|
+
|
|
125
153
|
## Links
|
|
126
154
|
|
|
127
155
|
- [Documentation](https://github.com/edspencer/paddock#readme)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edspencer/paddock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@fastify/swagger-ui": "4.2.0",
|
|
38
38
|
"@fastify/websocket": "10.0.1",
|
|
39
39
|
"@herdctl/chat": "0.8.0",
|
|
40
|
-
"@herdctl/core": "5.
|
|
40
|
+
"@herdctl/core": "5.32.0",
|
|
41
41
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
42
42
|
"fastify": "4.28.1",
|
|
43
43
|
"jose": "6.2.3",
|
|
@@ -21,6 +21,7 @@ import path from "node:path";
|
|
|
21
21
|
import { loadPaddockConfig } from "./config.js";
|
|
22
22
|
import { ensureClaudeHome, countLegacyTranscriptLinks, findPlantedChatsLinks, } from "./claude-home.js";
|
|
23
23
|
import { loadHostMcpSource } from "./claude-mcp.js";
|
|
24
|
+
import { loadHostPlugins } from "./claude-plugins.js";
|
|
24
25
|
import { declaredMcpNotices } from "./mcp-servers.js";
|
|
25
26
|
import { installHerdctlLogBridge } from "./agent-errors.js";
|
|
26
27
|
import { ProjectStore, ROOT_KEY } from "./projects.js";
|
|
@@ -132,10 +133,22 @@ export async function buildApp(opts = {}) {
|
|
|
132
133
|
servers: cfg.mcpServers,
|
|
133
134
|
hostNames: Object.keys(hostMcp.source.user),
|
|
134
135
|
browserMcp: cfg.browserMcp,
|
|
136
|
+
// …and where a declared credential ends up once the runtime has it: on the
|
|
137
|
+
// `claude` command line under `batch`, in-process under `session`. Paddock
|
|
138
|
+
// cannot close that from here, so it names it at boot instead.
|
|
139
|
+
driveMode: cfg.driveMode,
|
|
135
140
|
})) {
|
|
136
141
|
app.log[notice.level](notice.message);
|
|
137
142
|
}
|
|
138
|
-
|
|
143
|
+
// …and the host's Claude Code PLUGINS (#700), which no amount of reading
|
|
144
|
+
// `.claude.json` can find: a plugin declares its own MCP servers internally.
|
|
145
|
+
// Gated by `claude.instructions` (which is what bridges `plugins/`), with
|
|
146
|
+
// `claude.mcpServers` deciding only whether the plugins' servers come too —
|
|
147
|
+
// see `claude-plugins.ts` for why that is not the single lever #700 assumes.
|
|
148
|
+
const hostPlugins = await loadHostPlugins(cfg);
|
|
149
|
+
for (const notice of hostPlugins.notices)
|
|
150
|
+
app.log[notice.level](notice.message);
|
|
151
|
+
const herdctl = new HerdctlService(cfg, { ...hostMcp.source, declared: cfg.mcpServers }, hostPlugins.source);
|
|
139
152
|
const git = new GitService(cfg.projectsRoot, cfg.gitAuthor);
|
|
140
153
|
const githubAuth = new GithubAuth(path.join(cfg.dataDir, "github-auth.json"), cfg.githubClientId);
|
|
141
154
|
const archive = new ArchiveStore(cfg.dataDir);
|
|
@@ -29,15 +29,20 @@ export declare function hostMcpConfigPath(legacyClaudeHome: string): string;
|
|
|
29
29
|
* One MCP server, in the only shape herdctl's `McpServerSchema` can carry.
|
|
30
30
|
*
|
|
31
31
|
* Deliberately NOT `SDKMcpServerConfig`: that is what herdctl produces on the
|
|
32
|
-
* far side of `transformMcpServer`, and pinning the
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* far side of `transformMcpServer`, and pinning the agent-config shape here is
|
|
33
|
+
* what keeps any remaining lossiness visible at the type level rather than at
|
|
34
|
+
* runtime. As of herdctl 5.32.0 the two differ only in what the SDK has that
|
|
35
|
+
* herdctl still has no field for (`tools`).
|
|
35
36
|
*/
|
|
36
37
|
export interface McpServerDef {
|
|
38
|
+
/** Explicit transport. Wins over the bare-`url` ⇒ `http` inference (5.32.0). */
|
|
39
|
+
type?: "stdio" | "sse" | "http";
|
|
37
40
|
command?: string;
|
|
38
41
|
args?: string[];
|
|
39
42
|
env?: Record<string, string>;
|
|
40
43
|
url?: string;
|
|
44
|
+
/** Carried verbatim since 5.32.0 — and part of the OAuth token's key. */
|
|
45
|
+
headers?: Record<string, string>;
|
|
41
46
|
}
|
|
42
47
|
/** A set of MCP servers, keyed by the name their tools are namespaced under. */
|
|
43
48
|
export type McpServerDefs = Record<string, McpServerDef>;
|
|
@@ -45,7 +50,14 @@ export type McpServerDefs = Record<string, McpServerDef>;
|
|
|
45
50
|
export interface HostMcpCaveat {
|
|
46
51
|
/** The server's name in `.claude.json`. */
|
|
47
52
|
name: string;
|
|
48
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* `dropped` = not passed at all; `degraded` = passed, but not faithfully.
|
|
55
|
+
*
|
|
56
|
+
* Nothing produces `degraded` since herdctl 5.32.0 carries `headers` and
|
|
57
|
+
* `type` verbatim (#700) — the variant and its notice branch are kept because
|
|
58
|
+
* the class of defect ("the engine has no field for this") is the one #699 was
|
|
59
|
+
* filed about and is one schema narrowing away from coming back.
|
|
60
|
+
*/
|
|
49
61
|
kind: "dropped" | "degraded";
|
|
50
62
|
/** Human-readable, and specific enough to act on. */
|
|
51
63
|
reason: string;
|
|
@@ -71,44 +71,73 @@
|
|
|
71
71
|
* only when there are some, so an instance with no host servers stays
|
|
72
72
|
* byte-identical to before.
|
|
73
73
|
*
|
|
74
|
-
* ## What
|
|
75
|
-
*
|
|
76
|
-
* herdctl
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
74
|
+
* ## What could not be carried, and now can (herdctl 5.32.0)
|
|
75
|
+
*
|
|
76
|
+
* Until `@herdctl/core@5.32.0`, `McpServerSchema` was `{command?, args?, env?,
|
|
77
|
+
* url?}`, so `headers` and `type` were silently STRIPPED at `addAgent` and
|
|
78
|
+
* `transformMcpServer` rewrote every `url` to `type: "http"`. A
|
|
79
|
+
* header-authenticated server arrived unauthenticated and an `sse` server was
|
|
80
|
+
* downgraded to HTTP, so #699 shipped a boot warning naming every server that
|
|
81
|
+
* lost a field.
|
|
82
|
+
*
|
|
83
|
+
* herdctl#446 (in 5.32.0) widened the schema to mirror the SDK's own
|
|
84
|
+
* `McpServerConfig` — `type`, `headers`, `timeout` and `alwaysLoad` — and an
|
|
85
|
+
* explicit `type` now wins over the bare-`url` inference. Verified against the
|
|
86
|
+
* installed 5.32.0 rather than assumed: `addAgent` → `getAgents()` →
|
|
87
|
+
* `toSDKOptions()` returns `{type: "sse", url, headers}` unchanged for a server
|
|
88
|
+
* declared that way, and still infers `type: "http"` for a bare `url`. So the
|
|
89
|
+
* two `degraded` caveats are gone and {@link McpServerDef} carries both fields.
|
|
90
|
+
*
|
|
91
|
+
* Why that mattered more than it looked, and why it is worth not regressing:
|
|
92
|
+
* MCP OAuth tokens are stored under a top-level **`mcpOAuth`** key in the SAME
|
|
93
|
+
* credential store as `claudeAiOauth` — `<securestorage-dir>/.credentials.json`
|
|
94
|
+
* on Linux, the one `Claude Code-credentials` keychain item on darwin. There is
|
|
95
|
+
* no MCP-specific service name and no `mcp-oauth/` directory. The store is
|
|
96
|
+
* resolved by `CLAUDE_SECURESTORAGE_CONFIG_DIR ?? CLAUDE_CONFIG_DIR ?? ~/.claude`,
|
|
97
|
+
* which is exactly the variable `claude.credentials` drives — **so
|
|
98
|
+
* `credentials: host` DOES carry MCP OAuth tokens**, in either direction. But
|
|
99
|
+
* the per-server key is `` `${serverName}|${sha256({type,url,headers}).slice(0,16)}` ``,
|
|
100
|
+
* so a dropped header or a coerced `type` changed the hash and the stored token
|
|
101
|
+
* was simply not found. Carrying both fields verbatim is what makes
|
|
102
|
+
* `credentials: host` + `mcpServers: host` work for an OAuth server at all.
|
|
103
|
+
*
|
|
104
|
+
* `z.object` still strips what it has no field for (`tools`, and anything Claude
|
|
105
|
+
* Code grows next), so this narrowing is still a narrowing — it is just no longer
|
|
106
|
+
* one that breaks authentication. The one case that is still DROPPED is a server
|
|
107
|
+
* with neither `command` nor `url`, which cannot be started at all.
|
|
108
|
+
*
|
|
109
|
+
* ## The cost of carrying `headers`, which is real and is upstream
|
|
110
|
+
*
|
|
111
|
+
* herdctl's **CLI runtime** serialises the whole `mcp_servers` record into a
|
|
112
|
+
* single `--mcp-config '{"mcpServers":…}'` argv element, so everything in it is
|
|
113
|
+
* readable from `/proc/<pid>/cmdline` by any process of the same user. That was
|
|
114
|
+
* already true of an `env` value; it is now true of an `Authorization` header as
|
|
115
|
+
* well, which is the more likely place a bearer token lives. The SDK runtime
|
|
116
|
+
* passes the record in-process and is unaffected.
|
|
117
|
+
*
|
|
118
|
+
* Paddock's chats default to the SDK runtime (`driveMode: session`), but the
|
|
119
|
+
* sweeper and triggers are always one-shot CLI runs and a `driveMode: batch`
|
|
120
|
+
* project's turns are too — and neither the sweeper nor a trigger is given these
|
|
121
|
+
* servers, so the exposure is `driveMode: batch` only. Not paddock's to fix (the
|
|
122
|
+
* argv shape is herdctl's) and not a reason to go back to dropping the header:
|
|
123
|
+
* a stripped header is an authentication failure for everyone, while this is a
|
|
124
|
+
* same-user disclosure on one non-default drive mode. Worth knowing before
|
|
125
|
+
* putting a long-lived token in a `headers` block on a shared box.
|
|
126
|
+
*
|
|
127
|
+
* #702 found and measured this for a DECLARED server's `env` (it reads the token
|
|
128
|
+
* back out of a real spawned process's argv), and `mcp-servers.ts`'s
|
|
129
|
+
* `argvExposure` is the boot warning — now widened to `headers` for the same
|
|
130
|
+
* reason. There is no equivalent warning for a HOST server, deliberately: this
|
|
131
|
+
* module does not look at a value it did not resolve, and a user who ran
|
|
132
|
+
* `claude mcp add` on the box is already in the argv-exposure position with their
|
|
133
|
+
* own terminal.
|
|
134
|
+
*
|
|
135
|
+
* ## Plugins are the other half of this lever
|
|
136
|
+
*
|
|
137
|
+
* A Claude Code **plugin** can provide MCP servers too, and they are invisible to
|
|
138
|
+
* everything above: they are declared inside the plugin, not in `.claude.json`
|
|
139
|
+
* (#700). `claude-plugins.ts` is that half — it enumerates the host's installed
|
|
140
|
+
* plugin directories for `agent.plugins`, the second thing 5.32.0 added.
|
|
112
141
|
*
|
|
113
142
|
* ## Scope: keepers only
|
|
114
143
|
*
|
|
@@ -195,6 +224,22 @@ function narrowServer(name, raw, caveats) {
|
|
|
195
224
|
}
|
|
196
225
|
if (typeof raw.url === "string" && raw.url !== "")
|
|
197
226
|
server.url = raw.url;
|
|
227
|
+
// Both carried verbatim since herdctl 5.32.0 (#700). Before that they were
|
|
228
|
+
// stripped at `addAgent` and this function pushed a `degraded` caveat naming
|
|
229
|
+
// the server; the fields reaching `toSDKOptions()` intact is what retired that
|
|
230
|
+
// warning. A `type` we do not recognise is left off rather than passed on —
|
|
231
|
+
// herdctl's enum would strip it anyway, and the bare-`url` inference is the
|
|
232
|
+
// better fallback than nothing.
|
|
233
|
+
if (raw.type === "stdio" || raw.type === "sse" || raw.type === "http")
|
|
234
|
+
server.type = raw.type;
|
|
235
|
+
if (isRecord(raw.headers)) {
|
|
236
|
+
const headers = {};
|
|
237
|
+
for (const [k, v] of Object.entries(raw.headers))
|
|
238
|
+
if (typeof v === "string")
|
|
239
|
+
headers[k] = v;
|
|
240
|
+
if (Object.keys(headers).length > 0)
|
|
241
|
+
server.headers = headers;
|
|
242
|
+
}
|
|
198
243
|
if (server.command === undefined && server.url === undefined) {
|
|
199
244
|
caveats.push({
|
|
200
245
|
name,
|
|
@@ -203,29 +248,6 @@ function narrowServer(name, raw, caveats) {
|
|
|
203
248
|
});
|
|
204
249
|
return undefined;
|
|
205
250
|
}
|
|
206
|
-
// The lossy bits. `type` and `headers` are real keys of Claude Code's own MCP
|
|
207
|
-
// config that herdctl's schema has no field for and therefore strips; saying so
|
|
208
|
-
// is the difference between "my Notion server does not work" and "my Notion
|
|
209
|
-
// server's bearer header was dropped, here is the issue to file".
|
|
210
|
-
if (isRecord(raw.headers) && Object.keys(raw.headers).length > 0) {
|
|
211
|
-
caveats.push({
|
|
212
|
-
name,
|
|
213
|
-
kind: "degraded",
|
|
214
|
-
reason: `its \`headers\` (${Object.keys(raw.headers).join(", ")}) cannot be carried — ` +
|
|
215
|
-
`herdctl's MCP schema has no field for them, so the server is passed WITHOUT them, ` +
|
|
216
|
-
`any auth they held is gone, and its stored OAuth token (keyed on a hash that ` +
|
|
217
|
-
`includes the headers) will not be found either`,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
if (raw.type === "sse") {
|
|
221
|
-
caveats.push({
|
|
222
|
-
name,
|
|
223
|
-
kind: "degraded",
|
|
224
|
-
reason: 'it is an `sse` server, and herdctl maps every `url` to `type: "http"` — it will ' +
|
|
225
|
-
"be connected to as HTTP, and any OAuth token stored against the `sse` shape " +
|
|
226
|
-
"will not be found",
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
251
|
return server;
|
|
230
252
|
}
|
|
231
253
|
function serversOf(raw, caveats) {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { McpServersMode } from "./claude-mcp.js";
|
|
2
|
+
import type { InstructionsMode } from "./claude-instructions.js";
|
|
3
|
+
/**
|
|
4
|
+
* One plugin to load, in herdctl's `PluginSchema` object form.
|
|
5
|
+
*
|
|
6
|
+
* The schema also accepts a bare path string and normalises it to this, but
|
|
7
|
+
* paddock always needs the object because {@link HostPlugin.skipMcpDiscovery} is
|
|
8
|
+
* how the two levers are kept apart.
|
|
9
|
+
*/
|
|
10
|
+
export interface HostPlugin {
|
|
11
|
+
type: "local";
|
|
12
|
+
path: string;
|
|
13
|
+
skipMcpDiscovery?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Something about a host plugin worth telling the operator at boot. */
|
|
16
|
+
export interface HostPluginCaveat {
|
|
17
|
+
/** The plugin's id (`<name>@<marketplace>`) or its directory name. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** `dropped` = not passed at all; `degraded` = passed, but incompletely. */
|
|
20
|
+
kind: "dropped" | "degraded";
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
/** What {@link enumerateHostPlugins} found. */
|
|
24
|
+
export interface HostPluginSource {
|
|
25
|
+
/** Ready for `agent.plugins`, in registry order. */
|
|
26
|
+
plugins: HostPlugin[];
|
|
27
|
+
/**
|
|
28
|
+
* `mcp__<server>__*` patterns for every plugin-provided server paddock could
|
|
29
|
+
* name statically. Empty whenever MCP discovery is off.
|
|
30
|
+
*/
|
|
31
|
+
toolPatterns: string[];
|
|
32
|
+
caveats: HostPluginCaveat[];
|
|
33
|
+
}
|
|
34
|
+
/** Nothing inherited: `instructions: own`, and where tests start. */
|
|
35
|
+
export declare const EMPTY_HOST_PLUGINS: HostPluginSource;
|
|
36
|
+
/** The plugin root, `join(CLAUDE_CONFIG_DIR, "plugins")` in the CLI's terms. */
|
|
37
|
+
export declare function hostPluginRoot(legacyClaudeHome: string): string;
|
|
38
|
+
/** The CLI's own registry of what is installed, under {@link hostPluginRoot}. */
|
|
39
|
+
export declare const INSTALLED_PLUGINS_FILE = "installed_plugins.json";
|
|
40
|
+
/** A plugin's manifest, relative to its directory. */
|
|
41
|
+
export declare const PLUGIN_MANIFEST: string;
|
|
42
|
+
/** A plugin's out-of-manifest MCP declaration, relative to its directory. */
|
|
43
|
+
export declare const PLUGIN_MCP_FILE = ".mcp.json";
|
|
44
|
+
/**
|
|
45
|
+
* The allowlist pattern for one server of one plugin.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately not `mcpToolPattern` from `claude-mcp.ts`: the name a plugin's
|
|
48
|
+
* server is registered under is not the name it is declared under, and that
|
|
49
|
+
* derivation is the whole content of this function.
|
|
50
|
+
*/
|
|
51
|
+
export declare function pluginMcpToolPattern(pluginName: string, serverName: string): string;
|
|
52
|
+
/** One plugin's name and the MCP servers it declares, as far as they are static. */
|
|
53
|
+
export interface PluginServerNames {
|
|
54
|
+
/** `manifest.name`, else the directory name — the CLI's own fallback order. */
|
|
55
|
+
pluginName: string;
|
|
56
|
+
/** Server names as declared, BEFORE the `plugin:<plugin>:` prefixing. */
|
|
57
|
+
servers: string[];
|
|
58
|
+
/**
|
|
59
|
+
* A `mcpServers` the manifest points at rather than spells out, and that could
|
|
60
|
+
* not be resolved to a file inside the plugin directory. Non-empty means the
|
|
61
|
+
* allowlist for this plugin is incomplete.
|
|
62
|
+
*/
|
|
63
|
+
unresolved: string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read one plugin directory's declared MCP server names.
|
|
67
|
+
*
|
|
68
|
+
* Mirrors the CLI's own discovery order — `<dir>/.mcp.json` first, then whatever
|
|
69
|
+
* `manifest.mcpServers` adds — with the one deliberate gap described in the
|
|
70
|
+
* module doc: a manifest that points at an MCPB source (or at any path outside
|
|
71
|
+
* its own directory) is recorded in {@link PluginServerNames.unresolved} rather
|
|
72
|
+
* than fetched. Paddock does not download anything to build an allowlist.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readPluginServerNames(dir: string): Promise<PluginServerNames>;
|
|
75
|
+
/** One installed plugin: its id, and where it landed on disk. */
|
|
76
|
+
interface InstalledPlugin {
|
|
77
|
+
id: string;
|
|
78
|
+
installPath: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parse `installed_plugins.json`, v1 or v2. Pure, so the whole selection is
|
|
82
|
+
* testable from a string.
|
|
83
|
+
*
|
|
84
|
+
* v2 keys each `<name>@<marketplace>` id to an ARRAY of installations (one per
|
|
85
|
+
* scope), each carrying an explicit `installPath`. v1 keyed it to a single record
|
|
86
|
+
* with no path, which the CLI derives as
|
|
87
|
+
* `<root>/cache/<marketplace>/<name>/<version>` with each segment sanitised
|
|
88
|
+
* `[^a-zA-Z0-9\-_] → -` (and the version segment additionally allowing `.`).
|
|
89
|
+
*
|
|
90
|
+
* Fails soft in every direction: this is a file paddock does not own and must
|
|
91
|
+
* never turn into a boot failure.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseInstalledPlugins(raw: string, root: string): InstalledPlugin[];
|
|
94
|
+
/** A line for the boot log, at a level (mirrors `HostMcpNotice`). */
|
|
95
|
+
export interface HostPluginNotice {
|
|
96
|
+
level: "info" | "warn";
|
|
97
|
+
message: string;
|
|
98
|
+
}
|
|
99
|
+
/** What {@link loadHostPlugins} found, ready for the agent config and the log. */
|
|
100
|
+
export interface HostPluginReport {
|
|
101
|
+
source: HostPluginSource;
|
|
102
|
+
notices: HostPluginNotice[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Enumerate the host's installed plugin directories. Pure apart from reading the
|
|
106
|
+
* plugin tree, and takes the two files' contents rather than paths so the whole
|
|
107
|
+
* selection is testable without a home directory.
|
|
108
|
+
*/
|
|
109
|
+
export declare function enumerateHostPlugins(opts: {
|
|
110
|
+
root: string;
|
|
111
|
+
installed: string | undefined;
|
|
112
|
+
settings: unknown;
|
|
113
|
+
/** Whether the plugins' own MCP servers come too. */
|
|
114
|
+
mcp: boolean;
|
|
115
|
+
}): Promise<HostPluginSource>;
|
|
116
|
+
/**
|
|
117
|
+
* Read the host's plugin registry — once, at boot — and say what came of it.
|
|
118
|
+
*
|
|
119
|
+
* Under `instructions: own` nothing is opened at all, for the same reason
|
|
120
|
+
* `loadHostMcpSource` does not open `.claude.json` under `mcpServers: own`: "own
|
|
121
|
+
* everywhere means nothing outside the data dir is read" is a guarantee that has
|
|
122
|
+
* to be true in the place someone would check it.
|
|
123
|
+
*/
|
|
124
|
+
export declare function loadHostPlugins(cfg: {
|
|
125
|
+
legacyClaudeHome: string;
|
|
126
|
+
claude: {
|
|
127
|
+
instructions: InstructionsMode;
|
|
128
|
+
mcpServers: McpServersMode;
|
|
129
|
+
};
|
|
130
|
+
}): Promise<HostPluginReport>;
|
|
131
|
+
export {};
|