@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.
Files changed (75) hide show
  1. package/README.md +32 -4
  2. package/package.json +2 -2
  3. package/packages/server/dist/app.js +14 -1
  4. package/packages/server/dist/claude-mcp.d.ts +16 -4
  5. package/packages/server/dist/claude-mcp.js +83 -61
  6. package/packages/server/dist/claude-plugins.d.ts +131 -0
  7. package/packages/server/dist/claude-plugins.js +394 -0
  8. package/packages/server/dist/cli/args.d.ts +1 -1
  9. package/packages/server/dist/cli/args.js +9 -0
  10. package/packages/server/dist/herdctl-agent-config.d.ts +7 -1
  11. package/packages/server/dist/herdctl-agent-config.js +20 -4
  12. package/packages/server/dist/herdctl.d.ts +7 -1
  13. package/packages/server/dist/herdctl.js +9 -2
  14. package/packages/server/dist/mcp-servers.d.ts +31 -12
  15. package/packages/server/dist/mcp-servers.js +127 -33
  16. package/packages/server/package.json +1 -1
  17. package/packages/web/dist/assets/{InstanceConfigPage-Ca905oey.js → InstanceConfigPage-CCk2hVT9.js} +1 -1
  18. package/packages/web/dist/assets/{MarkdownRenderer-ChlC1ezH.js → MarkdownRenderer-B6gST_0U.js} +1 -1
  19. package/packages/web/dist/assets/{ProjectRedirect-CslqbhUb.js → ProjectRedirect-BCmKT4mf.js} +1 -1
  20. package/packages/web/dist/assets/{ProjectView-DkNdilGo.js → ProjectView-CCN98MWu.js} +3 -3
  21. package/packages/web/dist/assets/{ProjectsGrid-BrlvLc-Y.js → ProjectsGrid-CYzuN_yD.js} +1 -1
  22. package/packages/web/dist/assets/{arc-CGqCI5nQ.js → arc-BxvtMCZQ.js} +1 -1
  23. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Cp4KTxE2.js → architectureDiagram-3BPJPVTR-Bp6ffJ_u.js} +1 -1
  24. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-yFxaTv04.js → blockDiagram-GPEHLZMM-BIY2X8nW.js} +1 -1
  25. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-59lhWjRt.js → c4Diagram-AAUBKEIU-Br9stEJS.js} +1 -1
  26. package/packages/web/dist/assets/channel-CMdgNeFH.js +2 -0
  27. package/packages/web/dist/assets/{chunk-2J33WTMH-BrpXGnJg.js → chunk-2J33WTMH-Djc7uqMV.js} +1 -1
  28. package/packages/web/dist/assets/{chunk-4BX2VUAB-UxBDb5pi.js → chunk-4BX2VUAB-yEVMLksS.js} +1 -1
  29. package/packages/web/dist/assets/{chunk-55IACEB6-D4ZJyUHW.js → chunk-55IACEB6-tJaEdPZg.js} +1 -1
  30. package/packages/web/dist/assets/{chunk-727SXJPM-DWdPj_8E.js → chunk-727SXJPM-bWEqh672.js} +1 -1
  31. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BIpNbflY.js → chunk-AQP2D5EJ-B8U9fB-T.js} +1 -1
  32. package/packages/web/dist/assets/{chunk-FMBD7UC4-f6iJDfdI.js → chunk-FMBD7UC4-BeEITYig.js} +1 -1
  33. package/packages/web/dist/assets/{chunk-ND2GUHAM-Bz3TKIJk.js → chunk-ND2GUHAM-L2rpLaoK.js} +1 -1
  34. package/packages/web/dist/assets/{chunk-QZHKN3VN-buQmkafK.js → chunk-QZHKN3VN-qqzPMZL_.js} +1 -1
  35. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +2 -0
  36. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +2 -0
  37. package/packages/web/dist/assets/{core-TyFI6p0n.js → core-CSaNbmLO.js} +1 -1
  38. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CIkdi0XF.js → cose-bilkent-S5V4N54A-BRF7Nech.js} +1 -1
  39. package/packages/web/dist/assets/{dagre-BM42HDAG-BnLJCfNs.js → dagre-BM42HDAG-D10rJWOR.js} +1 -1
  40. package/packages/web/dist/assets/{diagram-2AECGRRQ-D0OqfDB9.js → diagram-2AECGRRQ-DGjT5mAO.js} +1 -1
  41. package/packages/web/dist/assets/{diagram-5GNKFQAL-DQ2spGrK.js → diagram-5GNKFQAL-DaH3bxHh.js} +1 -1
  42. package/packages/web/dist/assets/{diagram-KO2AKTUF-RDIVQ_S3.js → diagram-KO2AKTUF-QrLW9Wn6.js} +1 -1
  43. package/packages/web/dist/assets/{diagram-LMA3HP47-DKoTfh2C.js → diagram-LMA3HP47-BX15wgYN.js} +1 -1
  44. package/packages/web/dist/assets/{diagram-OG6HWLK6-6k0OuSAL.js → diagram-OG6HWLK6-DXQ_p63s.js} +1 -1
  45. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B-oQaYAC.js → erDiagram-TEJ5UH35-C_ioRruE.js} +1 -1
  46. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-B7qpYe9C.js → flowDiagram-I6XJVG4X-7d-xLXLM.js} +1 -1
  47. package/packages/web/dist/assets/{format-PHdEBNOi.js → format-BSDZezwD.js} +1 -1
  48. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-SfBsqMmx.js → ganttDiagram-6RSMTGT7-DZYx5xcj.js} +1 -1
  49. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CuM4Rl_i.js → gitGraphDiagram-PVQCEYII-dimo-osm.js} +1 -1
  50. package/packages/web/dist/assets/{index-D_1ogxg1.js → index-CUnFKMga.js} +2 -2
  51. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-Dq-32Oj4.js → infoDiagram-5YYISTIA-BzApY25x.js} +1 -1
  52. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BLFHCj_o.js → ishikawaDiagram-YF4QCWOH-DtX47_9p.js} +1 -1
  53. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CHP-Yu63.js → journeyDiagram-JHISSGLW-C7fITKg1.js} +1 -1
  54. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DRuehSqn.js → kanban-definition-UN3LZRKU-BOrWLf_W.js} +1 -1
  55. package/packages/web/dist/assets/{linear-Bwz2vnH7.js → linear-DuyAaBu6.js} +1 -1
  56. package/packages/web/dist/assets/{mermaid.core-CeMMnCip.js → mermaid.core-BQlzuVvO.js} +4 -4
  57. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CEdqeoRW.js → mindmap-definition-RKZ34NQL-CmP748B5.js} +1 -1
  58. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BdDXBx87.js → pieDiagram-4H26LBE5-CRd5Pdfq.js} +1 -1
  59. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bovd1Zhx.js → quadrantDiagram-W4KKPZXB-DpJm0Yw_.js} +1 -1
  60. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-DiyrKnJ2.js → requirementDiagram-4Y6WPE33-B0o2PRMC.js} +1 -1
  61. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BuX7TRVw.js → sankeyDiagram-5OEKKPKP-CBi3Ek3v.js} +1 -1
  62. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-BjetZWJo.js → sequenceDiagram-3UESZ5HK-C1stxfQg.js} +1 -1
  63. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-4AUBV6dF.js → stateDiagram-AJRCARHV-lCkiETWK.js} +1 -1
  64. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +2 -0
  65. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BpQ8g8Bm.js → timeline-definition-PNZ67QCA-D3DkWJMC.js} +1 -1
  66. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Cj99BWER.js → vennDiagram-CIIHVFJN-CO2-yC7K.js} +1 -1
  67. package/packages/web/dist/assets/{wardley-L42UT6IY-S7AGTOWw.js → wardley-L42UT6IY-Dfjp0kqX.js} +1 -1
  68. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-LNXkUA7S.js → wardleyDiagram-YWT4CUSO-CIl0OPwS.js} +1 -1
  69. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DGdTh4Xk.js → xychartDiagram-2RQKCTM6-CuvlQuup.js} +1 -1
  70. package/packages/web/dist/index.html +1 -1
  71. package/packages/web/dist/sw.js +1 -1
  72. package/packages/web/dist/assets/channel-CZs6Z1i5.js +0 -2
  73. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +0 -2
  74. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +0 -2
  75. 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, and the hooks your `settings.json` binds to
44
- tool use do not run. Both are one key in `<data-dir>/paddock.config.yaml`:
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=… ghcr.io/edspencer/paddock:latest
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.62.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.31.0",
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
- const herdctl = new HerdctlService(cfg, { ...hostMcp.source, declared: cfg.mcpServers });
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 narrower agent-config shape
33
- * here is what makes the lossiness above visible at the type level rather than
34
- * at runtime.
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
- /** `dropped` = not passed at all; `degraded` = passed, but not faithfully. */
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 cannot be carried, honestly
75
- *
76
- * herdctl's `McpServerSchema` is `{command?, args?, env?, url?}` and it is a
77
- * plain `z.object`, so anything else is silently STRIPPED at `addAgent`. Two
78
- * consequences that matter and that #691 does not mention:
79
- *
80
- * - **`headers` is dropped.** A remote MCP server authenticated with a bearer
81
- * header therefore arrives unauthenticated.
82
- * - **`type` is dropped**, and `transformMcpServer` maps any `url` to
83
- * `type: "http"`. An `sse` server is silently downgraded to HTTP.
84
- *
85
- * And those two strippings are worse than they look, because of where MCP OAuth
86
- * tokens turn out to live. #691 listed that as an open question; it is now
87
- * answered, from the bundled CLI binary rather than by inference:
88
- *
89
- * - Tokens are stored under a top-level **`mcpOAuth`** key in the SAME credential
90
- * store as `claudeAiOauth` — `<securestorage-dir>/.credentials.json` on Linux,
91
- * the one `Claude Code-credentials` keychain item on darwin. There is no
92
- * MCP-specific service name and no `mcp-oauth/` directory. The store is
93
- * resolved by `CLAUDE_SECURESTORAGE_CONFIG_DIR ?? CLAUDE_CONFIG_DIR ?? ~/.claude`,
94
- * which is exactly the variable `claude.credentials` drives. **So
95
- * `credentials: host` DOES carry MCP OAuth tokens** — they are not separable
96
- * from the Anthropic login, in either direction.
97
- * - But the per-server key is `` `${serverName}|${sha256({type,url,headers}).slice(0,16)}` ``.
98
- * A dropped `headers`, or an `sse` coerced to `http`, changes that hash and the
99
- * stored token is simply not found. So the combination that works today is
100
- * `credentials: host` + `mcpServers: host` for an OAuth server declared with no
101
- * headers and no `type` disagreement; a header-authenticated or `sse` server
102
- * arrives looking unauthenticated even though its token is right there.
103
- *
104
- * Neither is paddock's to fix here (both need a herdctl schema change). What
105
- * paddock CAN do is refuse to be silent about it: {@link parseHostMcpConfig}
106
- * records every key it had to drop and every `sse` it had to coerce, and the
107
- * boot notice names the server. The server is still passed — this is a
108
- * capability lever, not a security one, and a user who declared a server should
109
- * get it plus a warning rather than nothing plus a warning. The one case that IS
110
- * dropped is a server with neither `command` nor `url`, which cannot be started
111
- * at all.
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 {};