@edspencer/paddock 0.61.1 → 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 +65 -16
- package/package.json +2 -2
- package/packages/server/dist/app.js +48 -8
- package/packages/server/dist/claude-credentials.d.ts +116 -0
- package/packages/server/dist/claude-credentials.js +103 -0
- package/packages/server/dist/claude-home.d.ts +102 -30
- package/packages/server/dist/claude-home.js +437 -97
- package/packages/server/dist/claude-instructions.d.ts +121 -0
- package/packages/server/dist/claude-instructions.js +114 -0
- package/packages/server/dist/claude-mcp.d.ts +158 -0
- package/packages/server/dist/claude-mcp.js +389 -0
- package/packages/server/dist/claude-plugins.d.ts +131 -0
- package/packages/server/dist/claude-plugins.js +394 -0
- package/packages/server/dist/claude-settings.d.ts +159 -0
- package/packages/server/dist/claude-settings.js +159 -0
- package/packages/server/dist/cli/args.d.ts +1 -9
- package/packages/server/dist/cli/args.js +46 -19
- package/packages/server/dist/cli/paddock.js +20 -39
- package/packages/server/dist/config.d.ts +166 -30
- package/packages/server/dist/config.js +106 -26
- package/packages/server/dist/herdctl-agent-config.d.ts +17 -1
- package/packages/server/dist/herdctl-agent-config.js +69 -7
- package/packages/server/dist/herdctl-agent-names.d.ts +11 -0
- package/packages/server/dist/herdctl-agent-names.js +31 -0
- package/packages/server/dist/herdctl.d.ts +75 -13
- package/packages/server/dist/herdctl.js +77 -18
- package/packages/server/dist/instance-config.js +36 -0
- package/packages/server/dist/mcp-servers.d.ts +187 -0
- package/packages/server/dist/mcp-servers.js +399 -0
- package/packages/server/dist/routes/chats.js +10 -4
- package/packages/server/dist/transcripts.d.ts +61 -30
- package/packages/server/dist/transcripts.js +141 -46
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-BgNXqAox.js → InstanceConfigPage-CCk2hVT9.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DfD_UtqU.js → MarkdownRenderer-B6gST_0U.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-1EXdHXmO.js → ProjectRedirect-BCmKT4mf.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-DHb5vNUp.js → ProjectView-CCN98MWu.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-Br-EhLZE.js → ProjectsGrid-CYzuN_yD.js} +1 -1
- package/packages/web/dist/assets/{arc-C2Jekujs.js → arc-BxvtMCZQ.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CflCA-em.js → architectureDiagram-3BPJPVTR-Bp6ffJ_u.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Ds7GLNnC.js → blockDiagram-GPEHLZMM-BIY2X8nW.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-D4WyGO6I.js → c4Diagram-AAUBKEIU-Br9stEJS.js} +1 -1
- package/packages/web/dist/assets/channel-CMdgNeFH.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-tQunO_M5.js → chunk-2J33WTMH-Djc7uqMV.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DT5dOWd2.js → chunk-4BX2VUAB-yEVMLksS.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CfTUqUEF.js → chunk-55IACEB6-tJaEdPZg.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-sK5bOAA0.js → chunk-727SXJPM-bWEqh672.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-DcMgVe-P.js → chunk-AQP2D5EJ-B8U9fB-T.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-In3h_-7v.js → chunk-FMBD7UC4-BeEITYig.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-DuaIeRaC.js → chunk-ND2GUHAM-L2rpLaoK.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-DeT5mR5T.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-DgOgLvnM.js → core-CSaNbmLO.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CyO-rznJ.js → cose-bilkent-S5V4N54A-BRF7Nech.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-B_9Ihzgn.js → dagre-BM42HDAG-D10rJWOR.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-z5mvZ5YT.js → diagram-2AECGRRQ-DGjT5mAO.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Bsz0cgfn.js → diagram-5GNKFQAL-DaH3bxHh.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-B29k5f4q.js → diagram-KO2AKTUF-QrLW9Wn6.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CFrwsHoI.js → diagram-LMA3HP47-BX15wgYN.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-D9Sjc2HR.js → diagram-OG6HWLK6-DXQ_p63s.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C3vbkD7M.js → erDiagram-TEJ5UH35-C_ioRruE.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DkMIgntd.js → flowDiagram-I6XJVG4X-7d-xLXLM.js} +1 -1
- package/packages/web/dist/assets/{format-BFDqyFuw.js → format-BSDZezwD.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-nvHanjpW.js → ganttDiagram-6RSMTGT7-DZYx5xcj.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DbCNfOu7.js → gitGraphDiagram-PVQCEYII-dimo-osm.js} +1 -1
- package/packages/web/dist/assets/{index-DGCs1YIO.js → index-CUnFKMga.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-aJjH9x3D.js → infoDiagram-5YYISTIA-BzApY25x.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DoifwNnZ.js → ishikawaDiagram-YF4QCWOH-DtX47_9p.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cz9kG9HN.js → journeyDiagram-JHISSGLW-C7fITKg1.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BoXnfr7A.js → kanban-definition-UN3LZRKU-BOrWLf_W.js} +1 -1
- package/packages/web/dist/assets/{linear-BlTTO9yR.js → linear-DuyAaBu6.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-YAG8eSPQ.js → mermaid.core-BQlzuVvO.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DtUBKfkr.js → mindmap-definition-RKZ34NQL-CmP748B5.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-B3Rwpmy0.js → pieDiagram-4H26LBE5-CRd5Pdfq.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B8eJtTyk.js → quadrantDiagram-W4KKPZXB-DpJm0Yw_.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CpAluiY9.js → requirementDiagram-4Y6WPE33-B0o2PRMC.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js → sankeyDiagram-5OEKKPKP-CBi3Ek3v.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CRpqKSGm.js → sequenceDiagram-3UESZ5HK-C1stxfQg.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-h3h8gXv_.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-PVr5V_pG.js → timeline-definition-PNZ67QCA-D3DkWJMC.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-De-4NPAX.js → vennDiagram-CIIHVFJN-CO2-yC7K.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-Dtv10D2d.js → wardley-L42UT6IY-Dfjp0kqX.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-p2fJuQ6C.js → wardleyDiagram-YWT4CUSO-CIl0OPwS.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DyYF-k92.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/server/dist/cli/claude-home-choice.d.ts +0 -44
- package/packages/server/dist/cli/claude-home-choice.js +0 -108
- package/packages/web/dist/assets/channel-B1K_gwAH.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BKRI9TnH.js +0 -2
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type DriveMode } from "./models.js";
|
|
2
|
+
import { type TranscriptsMode } from "./transcripts.js";
|
|
3
|
+
import { type CredentialsMode } from "./claude-credentials.js";
|
|
4
|
+
import { type InstructionsMode } from "./claude-instructions.js";
|
|
5
|
+
import { type HooksMode } from "./claude-settings.js";
|
|
6
|
+
import { type McpServersMode, type McpServerDefs } from "./claude-mcp.js";
|
|
7
|
+
import { type McpServersConfigFile } from "./mcp-servers.js";
|
|
2
8
|
import { type RecoveryConfig } from "./recovery-config.js";
|
|
3
9
|
import { type CurationConfig } from "./curation-config.js";
|
|
4
10
|
import { type ManagementApiConfig, type ManagementApiConfigFile } from "./management-config.js";
|
|
@@ -62,6 +68,75 @@ export interface BrandConfig {
|
|
|
62
68
|
/** Accent color as a hex string driving the primary buttons + logo chip. */
|
|
63
69
|
accent: string;
|
|
64
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* What this instance shares with the host machine's Claude Code (#691).
|
|
73
|
+
*
|
|
74
|
+
* One key per concern, one vocabulary: `own` is paddock's, isolated inside the
|
|
75
|
+
* data dir; `host` is this machine's Claude Code. `own` everywhere is the
|
|
76
|
+
* guarantee an operator should be able to read straight off the config file —
|
|
77
|
+
* **nothing outside the data dir is read or written**.
|
|
78
|
+
*
|
|
79
|
+
* All five levers of #691's sequence now exist. Each is a key here rather than
|
|
80
|
+
* another meaning welded onto the Claude home, which is how the three incidents
|
|
81
|
+
* (#682, #683, #689) that motivated this all happened. What remains of #691 is
|
|
82
|
+
* step 6 — a place for a user to declare their OWN MCP servers, which is a
|
|
83
|
+
* different question from whose existing ones this instance borrows.
|
|
84
|
+
*/
|
|
85
|
+
export interface ClaudeConfig {
|
|
86
|
+
/**
|
|
87
|
+
* Whose session transcripts this instance uses. `own` (default) keeps them in
|
|
88
|
+
* each project's `.chats/`; `host` shares the user's real
|
|
89
|
+
* `~/.claude/projects/<encoded-cwd>/` folder live, in both directions.
|
|
90
|
+
*
|
|
91
|
+
* Driven by `PADDOCK_CLAUDE_TRANSCRIPTS`, then the `claude.transcripts:` key.
|
|
92
|
+
* See `transcripts.ts` for the mechanism and why it is not a repointed
|
|
93
|
+
* `CLAUDE_CONFIG_DIR`.
|
|
94
|
+
*/
|
|
95
|
+
transcripts: TranscriptsMode;
|
|
96
|
+
/**
|
|
97
|
+
* Whose Claude Code login this instance uses. `host` (the default, and the ONE
|
|
98
|
+
* key that does not default to `own`) also reads this machine's own login — the
|
|
99
|
+
* macOS Keychain entry on darwin, the user's `~/.claude/.credentials.json`
|
|
100
|
+
* elsewhere; `own` uses only what is inside paddock's own Claude home.
|
|
101
|
+
*
|
|
102
|
+
* Driven by `PADDOCK_CLAUDE_CREDENTIALS`, then the `claude.credentials:` key.
|
|
103
|
+
* See `claude-credentials.ts` for the mechanism, and for why sharing a login is
|
|
104
|
+
* the safe default when everything else here isolates.
|
|
105
|
+
*/
|
|
106
|
+
credentials: CredentialsMode;
|
|
107
|
+
/**
|
|
108
|
+
* Whose user-level instructions this instance loads — the user's `~/.claude`
|
|
109
|
+
* `CLAUDE.md`, `agents/`, `commands/` and `plugins/`. `own` (default) loads
|
|
110
|
+
* none of them; `host` symlinks them in, which is what every version before
|
|
111
|
+
* this one did unconditionally.
|
|
112
|
+
*
|
|
113
|
+
* Driven by `PADDOCK_CLAUDE_INSTRUCTIONS`, then the `claude.instructions:` key.
|
|
114
|
+
* See `claude-instructions.ts` — including the case AGAINST this default,
|
|
115
|
+
* which is real and is kept there rather than deleted.
|
|
116
|
+
*/
|
|
117
|
+
instructions: InstructionsMode;
|
|
118
|
+
/**
|
|
119
|
+
* Whether this instance runs the host machine's Claude Code hooks — shell
|
|
120
|
+
* commands `~/.claude/settings.json` binds to tool use and session lifecycle.
|
|
121
|
+
* `own` (default) writes paddock's own `settings.json` carrying every other key
|
|
122
|
+
* of the user's with `hooks` dropped; `host` symlinks theirs in whole.
|
|
123
|
+
*
|
|
124
|
+
* Driven by `PADDOCK_CLAUDE_HOOKS`, then the `claude.hooks:` key. The one lever
|
|
125
|
+
* here that governs code execution rather than data — see `claude-settings.ts`.
|
|
126
|
+
*/
|
|
127
|
+
hooks: HooksMode;
|
|
128
|
+
/**
|
|
129
|
+
* Whose MCP servers this instance's keepers get. `own` (default) attaches only
|
|
130
|
+
* the servers paddock itself provides; `host` also attaches the ones declared
|
|
131
|
+
* in the user's `~/.claude.json` — the top-level `mcpServers` plus any scoped
|
|
132
|
+
* to a project's own working directory.
|
|
133
|
+
*
|
|
134
|
+
* Driven by `PADDOCK_CLAUDE_MCP_SERVERS`, then the `claude.mcpServers:` key.
|
|
135
|
+
* The one lever whose source is NOT inside `~/.claude` — see `claude-mcp.ts`
|
|
136
|
+
* for why that made it break silently and separately from the other four.
|
|
137
|
+
*/
|
|
138
|
+
mcpServers: McpServersMode;
|
|
139
|
+
}
|
|
65
140
|
/** OpenAPI / Swagger-UI reference surface config (see PaddockConfig.openapi). */
|
|
66
141
|
export interface OpenApiConfig {
|
|
67
142
|
/** Mount the `/open-api` UI + `/open-api.json` spec. Opt-in — defaults false. */
|
|
@@ -96,8 +171,9 @@ export interface PaddockConfig {
|
|
|
96
171
|
/**
|
|
97
172
|
* Absolute path to the Claude home THIS INSTANCE owns — the directory whose
|
|
98
173
|
* `projects/<encoded-cwd>/` folders hold Claude Code's session transcripts.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
174
|
+
* `<dataDir>/claude-home` unless an operator relocates it; paddock ALWAYS owns
|
|
175
|
+
* it (#691) and refuses to start if it resolves to the user's own `~/.claude`.
|
|
176
|
+
* See {@link resolveClaudeHome}.
|
|
101
177
|
*
|
|
102
178
|
* Resolved ONCE here (#588) rather than re-read from `process.env` at each call
|
|
103
179
|
* site, because it must be the SAME value everywhere: paddock's transcript
|
|
@@ -123,19 +199,15 @@ export interface PaddockConfig {
|
|
|
123
199
|
*/
|
|
124
200
|
legacyClaudeHome: string;
|
|
125
201
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
202
|
+
* What this instance shares with the host's Claude Code (#691) — currently
|
|
203
|
+
* just `transcripts`. See {@link ClaudeConfig}.
|
|
128
204
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* `claude` CLI session run in a directory paddock happens to also manage — and
|
|
134
|
-
* moving it is the destructive behaviour this flag exists to prevent.
|
|
135
|
-
*
|
|
136
|
-
* Derived, not configured: `resolve(claudeHome) !== resolve(legacyClaudeHome)`.
|
|
205
|
+
* Replaces the derived `ownsClaudeHome` flag, which was the single lever every
|
|
206
|
+
* distinct concern hung off. Paddock now always owns its home, so "may we
|
|
207
|
+
* write here?" has one answer and the question that actually varies — whose
|
|
208
|
+
* transcripts these are — is asked directly.
|
|
137
209
|
*/
|
|
138
|
-
|
|
210
|
+
claude: ClaudeConfig;
|
|
139
211
|
/** Provider-agnostic user-authentication config (see AUTH.md). */
|
|
140
212
|
auth: AuthConfig;
|
|
141
213
|
/** Voice-dictation (Whisper) capability (per-instance; default off). */
|
|
@@ -288,6 +360,31 @@ export interface PaddockConfig {
|
|
|
288
360
|
errors: string[];
|
|
289
361
|
warnings: string[];
|
|
290
362
|
};
|
|
363
|
+
/**
|
|
364
|
+
* MCP servers this instance declares for itself (#691 step 6), resolved from
|
|
365
|
+
* the top-level `mcpServers:` block with every `env:VAR_NAME` reference read
|
|
366
|
+
* from the environment. Every project's keeper is attached all of them, and
|
|
367
|
+
* they win a name collision against anything inherited via
|
|
368
|
+
* `claude.mcpServers: host`.
|
|
369
|
+
*
|
|
370
|
+
* **Holds resolved secret material** — an MCP server's `env` is where a stdio
|
|
371
|
+
* server's API token lives — exactly as {@link managementApi}'s client tokens
|
|
372
|
+
* do. It must never be serialised into an API response; `instance-config.ts`
|
|
373
|
+
* publishes only the fields in its own `FIELDS` table, and this is
|
|
374
|
+
* deliberately not one of them.
|
|
375
|
+
*/
|
|
376
|
+
mcpServers: McpServerDefs;
|
|
377
|
+
/**
|
|
378
|
+
* Operator-facing problems found while resolving {@link mcpServers}: a
|
|
379
|
+
* declaration paddock cannot carry (`errors`, server dropped) or one whose
|
|
380
|
+
* `env:` reference did not resolve / whose secret is inlined (`warnings`).
|
|
381
|
+
* Carried, not logged, for the same reason as
|
|
382
|
+
* {@link managementApiDiagnostics}. Never contains a value from the block.
|
|
383
|
+
*/
|
|
384
|
+
mcpServersDiagnostics: {
|
|
385
|
+
errors: string[];
|
|
386
|
+
warnings: string[];
|
|
387
|
+
};
|
|
291
388
|
/**
|
|
292
389
|
* Instance default for the hook-management MCP (Epic G / G5, GG-4) — the
|
|
293
390
|
* `mcp__paddock_manage__{list,set,remove}_hook` tools that let a project agent
|
|
@@ -385,8 +482,31 @@ export interface PaddockConfigFile {
|
|
|
385
482
|
stateDir?: string;
|
|
386
483
|
herdctlConfigPath?: string;
|
|
387
484
|
webDist?: string;
|
|
388
|
-
/** Claude home
|
|
485
|
+
/** Where paddock's OWN Claude home lives; beneath `CLAUDE_CONFIG_DIR` (#691). */
|
|
389
486
|
claudeHome?: string;
|
|
487
|
+
/**
|
|
488
|
+
* What this instance shares with the host's Claude Code (#691). Each key takes
|
|
489
|
+
* `own` or `host`; a matching `PADDOCK_CLAUDE_*` env var still overrides it
|
|
490
|
+
* (file < env). Absent ⇒ fully isolated.
|
|
491
|
+
*/
|
|
492
|
+
claude?: {
|
|
493
|
+
transcripts?: string;
|
|
494
|
+
credentials?: string;
|
|
495
|
+
instructions?: string;
|
|
496
|
+
hooks?: string;
|
|
497
|
+
mcpServers?: string;
|
|
498
|
+
};
|
|
499
|
+
/**
|
|
500
|
+
* MCP servers this instance declares for ITSELF (#691 step 6) — a sibling of
|
|
501
|
+
* `claude:`, not a member of it, because it answers a different question:
|
|
502
|
+
* `claude.mcpServers` borrows whatever this machine already has, while this
|
|
503
|
+
* says what paddock should have regardless.
|
|
504
|
+
*
|
|
505
|
+
* File-only, like `managementApi`: a map of servers does not express as a
|
|
506
|
+
* scalar env var. Token material belongs in the environment and is referenced
|
|
507
|
+
* as `env:VAR_NAME` from any string here — see `mcp-servers.ts`.
|
|
508
|
+
*/
|
|
509
|
+
mcpServers?: McpServersConfigFile;
|
|
390
510
|
auth?: {
|
|
391
511
|
mode?: string;
|
|
392
512
|
userHeader?: string;
|
|
@@ -561,35 +681,43 @@ export declare function loadPaddockConfig(): PaddockConfig;
|
|
|
561
681
|
*/
|
|
562
682
|
export declare function userClaudeHome(): string;
|
|
563
683
|
/**
|
|
564
|
-
* Resolve
|
|
684
|
+
* Resolve where paddock's OWN Claude home lives.
|
|
565
685
|
*
|
|
566
686
|
* Precedence, highest first:
|
|
567
687
|
*
|
|
568
|
-
* 1. **`
|
|
569
|
-
* `$HOME/.claude` restores the pre-#620 behaviour exactly, which is the
|
|
570
|
-
* documented escape hatch if the relocation causes trouble.
|
|
571
|
-
* 2. **`CLAUDE_CONFIG_DIR`** — the variable *Claude Code itself* resolves its
|
|
688
|
+
* 1. **`CLAUDE_CONFIG_DIR`** — the variable *Claude Code itself* resolves its
|
|
572
689
|
* home from. Honouring it is load-bearing, not a courtesy: herdctl
|
|
573
690
|
* deliberately refuses to clobber an operator-set `CLAUDE_CONFIG_DIR`
|
|
574
691
|
* (herdctl#423), so if paddock picked a different home the SDK would write
|
|
575
692
|
* transcripts to one tree while herdctl read from another — the exact
|
|
576
693
|
* split-brain #588 fixed. Deferring to it makes that state unreachable.
|
|
577
|
-
*
|
|
578
|
-
*
|
|
694
|
+
* 2. A `claudeHome:` key in the instance config file.
|
|
695
|
+
* 3. **`<dataDir>/claude-home`** — the default.
|
|
579
696
|
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
697
|
+
* ## Paddock always OWNS this home, and the one value it refuses
|
|
698
|
+
*
|
|
699
|
+
* `CLAUDE_HOME` used to sit above all of this, and setting it to `~/.claude` was
|
|
700
|
+
* the documented escape hatch back to the pre-#620 layout. It is gone (#691),
|
|
701
|
+
* because pointing the home at the user's own directory is what welded five
|
|
702
|
+
* unrelated concerns to one lever: it decided whose transcripts were at risk,
|
|
703
|
+
* which keychain entry was visible, where agent memory landed, and whether
|
|
704
|
+
* delete meant delete. Those are now separate keys (`claude:`), so the home no
|
|
705
|
+
* longer needs to move for any of them.
|
|
706
|
+
*
|
|
707
|
+
* Which leaves exactly one value that must not be allowed through, from any
|
|
708
|
+
* source: the user's own `~/.claude`. It re-welds the lever, and it re-breaks
|
|
709
|
+
* agent memory (#690) — the memory dir is `<claudeHome>/projects/<enc>/memory`,
|
|
710
|
+
* and the agent harness refuses to write to any path with a `.claude` component.
|
|
711
|
+
* So it REFUSES TO START rather than silently degrading, which is the same
|
|
712
|
+
* fail-closed shape as the bind-safety guard. `CLAUDE_CONFIG_DIR` cannot simply
|
|
713
|
+
* be ignored instead: it is Claude Code's own variable, and disagreeing with an
|
|
714
|
+
* operator-set value is the herdctl#423 split-brain again.
|
|
589
715
|
*
|
|
590
716
|
* NOT canonical()-ed: this exact string is what herdctl hands Claude Code as
|
|
591
717
|
* `CLAUDE_CONFIG_DIR`, and canonicalising would silently diverge from the
|
|
592
718
|
* literal path the two of them encode into `<home>/projects/<encoded-cwd>`.
|
|
719
|
+
* The refusal check DOES resolve both sides, so a symlinked or trailing-slash
|
|
720
|
+
* spelling of `~/.claude` cannot slip past it.
|
|
593
721
|
*
|
|
594
722
|
* This is the RESOLVER, not the accessor. It is read once by
|
|
595
723
|
* {@link loadPaddockConfig} into {@link PaddockConfig.claudeHome}; runtime code
|
|
@@ -597,5 +725,13 @@ export declare function userClaudeHome(): string;
|
|
|
597
725
|
* engine can never end up resolving two different homes (#588).
|
|
598
726
|
*/
|
|
599
727
|
export declare function resolveClaudeHome(dataDir: string, fromFile?: string): string;
|
|
728
|
+
/**
|
|
729
|
+
* Why `home` is unusable as paddock's own Claude home, or `undefined` if it is
|
|
730
|
+
* fine. Pure, so the whole decision is testable without touching `$HOME`.
|
|
731
|
+
*
|
|
732
|
+
* `fromEnv` only changes which fix is named first — the refusal itself is the
|
|
733
|
+
* same whichever layer supplied the value.
|
|
734
|
+
*/
|
|
735
|
+
export declare function claudeHomeRefusal(home: string, userHome: string, fromEnv: boolean): string | undefined;
|
|
600
736
|
/** Directory name of paddock's own Claude home inside the data dir (#620). */
|
|
601
737
|
export declare const DEFAULT_CLAUDE_HOME_DIRNAME = "claude-home";
|
|
@@ -24,6 +24,12 @@ import { fileURLToPath } from "node:url";
|
|
|
24
24
|
import YAML from "yaml";
|
|
25
25
|
import { DEFAULT_DRIVE_MODE, isKnownDriveMode, isKnownModel, } from "./models.js";
|
|
26
26
|
import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capability.js";
|
|
27
|
+
import { DEFAULT_TRANSCRIPTS_MODE, isKnownTranscriptsMode, } from "./transcripts.js";
|
|
28
|
+
import { DEFAULT_CREDENTIALS_MODE, isKnownCredentialsMode, } from "./claude-credentials.js";
|
|
29
|
+
import { DEFAULT_INSTRUCTIONS_MODE, isKnownInstructionsMode, } from "./claude-instructions.js";
|
|
30
|
+
import { DEFAULT_HOOKS_MODE, isKnownHooksMode } from "./claude-settings.js";
|
|
31
|
+
import { DEFAULT_MCP_SERVERS_MODE, isKnownMcpServersMode, } from "./claude-mcp.js";
|
|
32
|
+
import { resolveDeclaredMcpServers } from "./mcp-servers.js";
|
|
27
33
|
import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
|
|
28
34
|
import { DEFAULT_RECOVERY } from "./recovery-config.js";
|
|
29
35
|
import { DEFAULT_CURATION } from "./curation-config.js";
|
|
@@ -286,10 +292,7 @@ export function loadPaddockConfig() {
|
|
|
286
292
|
webDist: abs(envOr("PADDOCK_WEB_DIST", fileOr(file.webDist, defaultWebDist))),
|
|
287
293
|
claudeHome: resolvedClaudeHome,
|
|
288
294
|
legacyClaudeHome,
|
|
289
|
-
|
|
290
|
-
// including one an operator pointed it at explicitly. The user's home is the
|
|
291
|
-
// only one whose contents belong to somebody else.
|
|
292
|
-
ownsClaudeHome: path.resolve(resolvedClaudeHome) !== path.resolve(legacyClaudeHome),
|
|
295
|
+
claude: loadClaudeConfig(file.claude),
|
|
293
296
|
auth: loadAuthConfig(file.auth),
|
|
294
297
|
transcription: loadTranscriptionConfig(file.transcription),
|
|
295
298
|
brand: loadBrandConfig(file.brand),
|
|
@@ -312,6 +315,13 @@ export function loadPaddockConfig() {
|
|
|
312
315
|
const { config, errors, warnings } = resolveManagementApiConfig(file.managementApi, process.env);
|
|
313
316
|
return { managementApi: config, managementApiDiagnostics: { errors, warnings } };
|
|
314
317
|
})(),
|
|
318
|
+
...(() => {
|
|
319
|
+
// #691 step 6: file-only, and resolved against the real environment because
|
|
320
|
+
// any string in the block may be an `env:VAR_NAME` reference. Diagnostics
|
|
321
|
+
// ride along for app.ts to log — they never carry a resolved value.
|
|
322
|
+
const { servers, errors, warnings } = resolveDeclaredMcpServers(file.mcpServers, process.env);
|
|
323
|
+
return { mcpServers: servers, mcpServersDiagnostics: { errors, warnings } };
|
|
324
|
+
})(),
|
|
315
325
|
hooksMcpEnabled: loadHooksMcpEnabled(file.hooksMcpEnabled),
|
|
316
326
|
recovery: loadRecoveryConfig(file.recovery),
|
|
317
327
|
attachments: loadAttachmentsConfig(file.attachments),
|
|
@@ -625,35 +635,78 @@ export function userClaudeHome() {
|
|
|
625
635
|
return path.join(os.homedir(), ".claude");
|
|
626
636
|
}
|
|
627
637
|
/**
|
|
628
|
-
* Resolve
|
|
638
|
+
* Resolve what this instance shares with the host's Claude Code (#691).
|
|
639
|
+
*
|
|
640
|
+
* Precedence is the file's usual env > file > default. An unrecognised value
|
|
641
|
+
* falls back to the default rather than failing the boot — the same rule every
|
|
642
|
+
* other enum here follows (`driveMode`, whisper `mode`). For `transcripts` that
|
|
643
|
+
* is also the safe direction: a typo isolates rather than silently sharing the
|
|
644
|
+
* user's files. For `credentials` the default is `host` and a typo therefore
|
|
645
|
+
* falls back to sharing — deliberately, because the failure it avoids (#683: an
|
|
646
|
+
* instance that boots clean and fails every turn) is the worse one, and reading a
|
|
647
|
+
* login risks no file of the user's. See `claude-credentials.ts`.
|
|
648
|
+
*
|
|
649
|
+
* `hooks` is the one where falling back on a typo is load-bearing in the other
|
|
650
|
+
* direction: `PADDOCK_CLAUDE_HOOKS=hots` isolates rather than executing the
|
|
651
|
+
* host's shell commands, which is the only acceptable way for a security lever
|
|
652
|
+
* to misread its input. `mcpServers` follows the same direction for a weaker
|
|
653
|
+
* reason — an MCP server is a process paddock spawns, so a typo that attaches
|
|
654
|
+
* nothing is better than one that attaches the user's whole set unasked.
|
|
655
|
+
*/
|
|
656
|
+
function loadClaudeConfig(file = {}) {
|
|
657
|
+
const transcripts = (envOpt("PADDOCK_CLAUDE_TRANSCRIPTS") ?? fileOpt(file.transcripts))?.toLowerCase();
|
|
658
|
+
const credentials = (envOpt("PADDOCK_CLAUDE_CREDENTIALS") ?? fileOpt(file.credentials))?.toLowerCase();
|
|
659
|
+
const instructions = (envOpt("PADDOCK_CLAUDE_INSTRUCTIONS") ?? fileOpt(file.instructions))?.toLowerCase();
|
|
660
|
+
const hooks = (envOpt("PADDOCK_CLAUDE_HOOKS") ?? fileOpt(file.hooks))?.toLowerCase();
|
|
661
|
+
const mcpServers = (envOpt("PADDOCK_CLAUDE_MCP_SERVERS") ?? fileOpt(file.mcpServers))?.toLowerCase();
|
|
662
|
+
return {
|
|
663
|
+
transcripts: transcripts && isKnownTranscriptsMode(transcripts) ? transcripts : DEFAULT_TRANSCRIPTS_MODE,
|
|
664
|
+
credentials: credentials && isKnownCredentialsMode(credentials) ? credentials : DEFAULT_CREDENTIALS_MODE,
|
|
665
|
+
instructions: instructions && isKnownInstructionsMode(instructions)
|
|
666
|
+
? instructions
|
|
667
|
+
: DEFAULT_INSTRUCTIONS_MODE,
|
|
668
|
+
hooks: hooks && isKnownHooksMode(hooks) ? hooks : DEFAULT_HOOKS_MODE,
|
|
669
|
+
mcpServers: mcpServers && isKnownMcpServersMode(mcpServers) ? mcpServers : DEFAULT_MCP_SERVERS_MODE,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Resolve where paddock's OWN Claude home lives.
|
|
629
674
|
*
|
|
630
675
|
* Precedence, highest first:
|
|
631
676
|
*
|
|
632
|
-
* 1. **`
|
|
633
|
-
* `$HOME/.claude` restores the pre-#620 behaviour exactly, which is the
|
|
634
|
-
* documented escape hatch if the relocation causes trouble.
|
|
635
|
-
* 2. **`CLAUDE_CONFIG_DIR`** — the variable *Claude Code itself* resolves its
|
|
677
|
+
* 1. **`CLAUDE_CONFIG_DIR`** — the variable *Claude Code itself* resolves its
|
|
636
678
|
* home from. Honouring it is load-bearing, not a courtesy: herdctl
|
|
637
679
|
* deliberately refuses to clobber an operator-set `CLAUDE_CONFIG_DIR`
|
|
638
680
|
* (herdctl#423), so if paddock picked a different home the SDK would write
|
|
639
681
|
* transcripts to one tree while herdctl read from another — the exact
|
|
640
682
|
* split-brain #588 fixed. Deferring to it makes that state unreachable.
|
|
641
|
-
*
|
|
642
|
-
*
|
|
683
|
+
* 2. A `claudeHome:` key in the instance config file.
|
|
684
|
+
* 3. **`<dataDir>/claude-home`** — the default.
|
|
643
685
|
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
686
|
+
* ## Paddock always OWNS this home, and the one value it refuses
|
|
687
|
+
*
|
|
688
|
+
* `CLAUDE_HOME` used to sit above all of this, and setting it to `~/.claude` was
|
|
689
|
+
* the documented escape hatch back to the pre-#620 layout. It is gone (#691),
|
|
690
|
+
* because pointing the home at the user's own directory is what welded five
|
|
691
|
+
* unrelated concerns to one lever: it decided whose transcripts were at risk,
|
|
692
|
+
* which keychain entry was visible, where agent memory landed, and whether
|
|
693
|
+
* delete meant delete. Those are now separate keys (`claude:`), so the home no
|
|
694
|
+
* longer needs to move for any of them.
|
|
695
|
+
*
|
|
696
|
+
* Which leaves exactly one value that must not be allowed through, from any
|
|
697
|
+
* source: the user's own `~/.claude`. It re-welds the lever, and it re-breaks
|
|
698
|
+
* agent memory (#690) — the memory dir is `<claudeHome>/projects/<enc>/memory`,
|
|
699
|
+
* and the agent harness refuses to write to any path with a `.claude` component.
|
|
700
|
+
* So it REFUSES TO START rather than silently degrading, which is the same
|
|
701
|
+
* fail-closed shape as the bind-safety guard. `CLAUDE_CONFIG_DIR` cannot simply
|
|
702
|
+
* be ignored instead: it is Claude Code's own variable, and disagreeing with an
|
|
703
|
+
* operator-set value is the herdctl#423 split-brain again.
|
|
653
704
|
*
|
|
654
705
|
* NOT canonical()-ed: this exact string is what herdctl hands Claude Code as
|
|
655
706
|
* `CLAUDE_CONFIG_DIR`, and canonicalising would silently diverge from the
|
|
656
707
|
* literal path the two of them encode into `<home>/projects/<encoded-cwd>`.
|
|
708
|
+
* The refusal check DOES resolve both sides, so a symlinked or trailing-slash
|
|
709
|
+
* spelling of `~/.claude` cannot slip past it.
|
|
657
710
|
*
|
|
658
711
|
* This is the RESOLVER, not the accessor. It is read once by
|
|
659
712
|
* {@link loadPaddockConfig} into {@link PaddockConfig.claudeHome}; runtime code
|
|
@@ -661,12 +714,39 @@ export function userClaudeHome() {
|
|
|
661
714
|
* engine can never end up resolving two different homes (#588).
|
|
662
715
|
*/
|
|
663
716
|
export function resolveClaudeHome(dataDir, fromFile) {
|
|
664
|
-
const env = envOpt("
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
717
|
+
const env = envOpt("CLAUDE_CONFIG_DIR");
|
|
718
|
+
const home = env !== undefined
|
|
719
|
+
? abs(env)
|
|
720
|
+
: fromFile !== undefined && fromFile !== ""
|
|
721
|
+
? abs(fromFile)
|
|
722
|
+
: path.join(dataDir, DEFAULT_CLAUDE_HOME_DIRNAME);
|
|
723
|
+
const refusal = claudeHomeRefusal(home, userClaudeHome(), env !== undefined);
|
|
724
|
+
if (refusal !== undefined)
|
|
725
|
+
throw new Error(refusal);
|
|
726
|
+
return home;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Why `home` is unusable as paddock's own Claude home, or `undefined` if it is
|
|
730
|
+
* fine. Pure, so the whole decision is testable without touching `$HOME`.
|
|
731
|
+
*
|
|
732
|
+
* `fromEnv` only changes which fix is named first — the refusal itself is the
|
|
733
|
+
* same whichever layer supplied the value.
|
|
734
|
+
*/
|
|
735
|
+
export function claudeHomeRefusal(home, userHome, fromEnv) {
|
|
736
|
+
if (path.resolve(home) !== path.resolve(userHome))
|
|
737
|
+
return undefined;
|
|
738
|
+
return (`refusing to start: the Claude home resolves to your own ${userHome}, which paddock ` +
|
|
739
|
+
`must not run as its own home. It re-couples everything paddock keeps separate — ` +
|
|
740
|
+
`whose transcripts a delete removes, which login is visible, where agent memory is ` +
|
|
741
|
+
`written (agents cannot write under a \`.claude\` path at all, #690). ` +
|
|
742
|
+
(fromEnv
|
|
743
|
+
? `CLAUDE_CONFIG_DIR is what points it there: unset it, or point it at a directory ` +
|
|
744
|
+
`of paddock's own. `
|
|
745
|
+
: `The \`claudeHome:\` key in paddock.config.yaml points it there: remove it, or name a ` +
|
|
746
|
+
`directory of paddock's own. `) +
|
|
747
|
+
`To SHARE your Claude Code transcripts, set \`claude: { transcripts: host }\` (or ` +
|
|
748
|
+
`PADDOCK_CLAUDE_TRANSCRIPTS=host) — that shares the files themselves and keeps ` +
|
|
749
|
+
`paddock's home where it belongs.`);
|
|
670
750
|
}
|
|
671
751
|
/** Directory name of paddock's own Claude home inside the data dir (#620). */
|
|
672
752
|
export const DEFAULT_CLAUDE_HOME_DIRNAME = "claude-home";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PaddockConfig } from "./config.js";
|
|
2
2
|
import type { Project } from "./projects.js";
|
|
3
|
+
import { type McpSources } from "./claude-mcp.js";
|
|
4
|
+
import { type HostPluginSource } from "./claude-plugins.js";
|
|
3
5
|
import { type PaddockTrigger } from "./trigger-config.js";
|
|
4
6
|
/**
|
|
5
7
|
* A project's keeper agent config. Inherits the fleet `defaults` (runtime,
|
|
@@ -24,8 +26,22 @@ import { type PaddockTrigger } from "./trigger-config.js";
|
|
|
24
26
|
* not `<dataDir>`: `projectsRoot` IS the instance's backing repo, so the file is
|
|
25
27
|
* version-controlled. Since #516 the ROOT project reaches it the same way —
|
|
26
28
|
* its cwd IS `projectsRoot`, so the walk-up needs no special-casing.
|
|
29
|
+
*
|
|
30
|
+
* `mcpSources` is the external MCP servers this instance attaches — the user's
|
|
31
|
+
* own under `claude.mcpServers: host` (#691 step 5) plus the ones the instance
|
|
32
|
+
* declares in its own `mcpServers:` block (step 6). Both are resolved once at
|
|
33
|
+
* boot and passed in rather than re-read here, so this stays pure and so a
|
|
34
|
+
* `.claude.json` is never opened under `mcpServers: own`. It defaults to empty,
|
|
35
|
+
* which is both the isolated mode and what every existing caller means. The
|
|
36
|
+
* KEEPER is the only agent that gets them: the sweeper is tool-less and each
|
|
37
|
+
* trigger declares its own narrow allowlist, so neither could call one anyway.
|
|
38
|
+
*
|
|
39
|
+
* `hostPlugins` is the same story for the host's Claude Code plugins (#700),
|
|
40
|
+
* resolved once at boot by `claude-plugins.ts` and gated by `claude.instructions`
|
|
41
|
+
* rather than `claude.mcpServers` — see that module for why. Keeper-only for the
|
|
42
|
+
* same reasons, and empty unless the lever is on.
|
|
27
43
|
*/
|
|
28
|
-
export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string): Record<string, unknown> & {
|
|
44
|
+
export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string, mcpSources?: McpSources, hostPlugins?: HostPluginSource): Record<string, unknown> & {
|
|
29
45
|
name: string;
|
|
30
46
|
};
|
|
31
47
|
/**
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
import { promises as fs } from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import YAML from "yaml";
|
|
16
|
+
import { EMPTY_MCP_SOURCES, mcpServersFor, mcpToolPattern, } from "./claude-mcp.js";
|
|
17
|
+
import { EMPTY_HOST_PLUGINS } from "./claude-plugins.js";
|
|
16
18
|
import { DEFAULT_MODEL, SWEEPER_DEFAULT_MODEL, DEFAULT_PERMISSION_MODE, DEFAULT_MAX_TURNS, } from "./models.js";
|
|
17
19
|
import { triggerToAgentToolConfig, triggersToHerdctlSchedules, curatorTriggerOf, } from "./trigger-config.js";
|
|
18
|
-
import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers,
|
|
20
|
+
import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers, FLEET_ALLOWED_TOOLS, DENIED_TOOLS, KEEPER_MAX_CONCURRENT, KEEPER_SESSION_TIMEOUT, } from "./herdctl-agent-names.js";
|
|
19
21
|
/**
|
|
20
22
|
* A project's keeper agent config. Inherits the fleet `defaults` (runtime,
|
|
21
23
|
* max_turns, permission_mode, allowed/denied tools) via addAgent's deep-merge;
|
|
@@ -39,8 +41,22 @@ import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers,
|
|
|
39
41
|
* not `<dataDir>`: `projectsRoot` IS the instance's backing repo, so the file is
|
|
40
42
|
* version-controlled. Since #516 the ROOT project reaches it the same way —
|
|
41
43
|
* its cwd IS `projectsRoot`, so the walk-up needs no special-casing.
|
|
44
|
+
*
|
|
45
|
+
* `mcpSources` is the external MCP servers this instance attaches — the user's
|
|
46
|
+
* own under `claude.mcpServers: host` (#691 step 5) plus the ones the instance
|
|
47
|
+
* declares in its own `mcpServers:` block (step 6). Both are resolved once at
|
|
48
|
+
* boot and passed in rather than re-read here, so this stays pure and so a
|
|
49
|
+
* `.claude.json` is never opened under `mcpServers: own`. It defaults to empty,
|
|
50
|
+
* which is both the isolated mode and what every existing caller means. The
|
|
51
|
+
* KEEPER is the only agent that gets them: the sweeper is tool-less and each
|
|
52
|
+
* trigger declares its own narrow allowlist, so neither could call one anyway.
|
|
53
|
+
*
|
|
54
|
+
* `hostPlugins` is the same story for the host's Claude Code plugins (#700),
|
|
55
|
+
* resolved once at boot by `claude-plugins.ts` and gated by `claude.instructions`
|
|
56
|
+
* rather than `claude.mcpServers` — see that module for why. Keeper-only for the
|
|
57
|
+
* same reasons, and empty unless the lever is on.
|
|
42
58
|
*/
|
|
43
|
-
export function buildAgentConfig(cfg, project, modelOverride) {
|
|
59
|
+
export function buildAgentConfig(cfg, project, modelOverride, mcpSources = EMPTY_MCP_SOURCES, hostPlugins = EMPTY_HOST_PLUGINS) {
|
|
44
60
|
const config = {
|
|
45
61
|
name: keeperAgentName(project.slug),
|
|
46
62
|
description: project.summary || `Claude Code agent for project ${project.name}.`,
|
|
@@ -101,11 +117,52 @@ export function buildAgentConfig(cfg, project, modelOverride) {
|
|
|
101
117
|
const schedules = triggersToHerdctlSchedules(project.triggers);
|
|
102
118
|
if (schedules)
|
|
103
119
|
config.schedules = schedules;
|
|
104
|
-
//
|
|
105
|
-
//
|
|
120
|
+
// MCP servers, from three sources, all landing on the SAME `mcp_servers` key:
|
|
121
|
+
//
|
|
122
|
+
// - the browser MCP (headless Chromium) when enabled for this box —
|
|
123
|
+
// `mcp__playwright__*` is already on the inherited defaults.allowed_tools;
|
|
124
|
+
// - the user's own servers under `claude.mcpServers: host` (#691 step 5);
|
|
125
|
+
// - the ones this instance declares in its own `mcpServers:` block (#691 step
|
|
126
|
+
// 6), which win over the inherited ones (`mcpServersFor` merges them last).
|
|
127
|
+
//
|
|
128
|
+
// Neither of the latter two is on the default allowlist, because their names are
|
|
129
|
+
// not knowable at config-file-write time — hence the widening below.
|
|
130
|
+
//
|
|
131
|
+
// Paddock's own server still wins a name collision, against BOTH: its flags are
|
|
132
|
+
// box-specific (the Ansible-installed chromium engine, --no-sandbox for an
|
|
133
|
+
// unprivileged LXC) and someone else's `playwright` would almost certainly not
|
|
134
|
+
// start here. A declared collision is warned about by name at boot
|
|
135
|
+
// (`declaredMcpNotices`) so it is not silent.
|
|
106
136
|
const browser = browserMcpServers(cfg.browserMcp);
|
|
107
|
-
|
|
108
|
-
|
|
137
|
+
const external = mcpServersFor(mcpSources, project.workingDir);
|
|
138
|
+
const servers = { ...external, ...(browser ?? {}) };
|
|
139
|
+
if (Object.keys(servers).length > 0)
|
|
140
|
+
config.mcp_servers = servers;
|
|
141
|
+
// Widen the allowlist by exactly those servers' tool patterns. WITHOUT this
|
|
142
|
+
// both levers are a no-op with no error: both runtimes auto-deny any tool
|
|
143
|
+
// missing from an explicit `allowed_tools`, so an attached-but-unlisted server
|
|
144
|
+
// connects and then has every call refused. herdctl does this automatically for
|
|
145
|
+
// INJECTED servers only (`cli-runtime.js`), never for config `mcp_servers` —
|
|
146
|
+
// which is why `mcp__playwright__*` is hard-coded into the defaults.
|
|
147
|
+
//
|
|
148
|
+
// Patterns already on the defaults are filtered out, so nothing is restated
|
|
149
|
+
// for an instance with no external servers (or one whose only server paddock's
|
|
150
|
+
// own overrode above) — that config stays byte-identical to before this lever.
|
|
151
|
+
//
|
|
152
|
+
// The host's Claude Code PLUGINS ride on the same two mechanisms (#700, and the
|
|
153
|
+
// `plugins` passthrough herdctl 5.32.0 added for it). They are a separate key
|
|
154
|
+
// rather than more `mcp_servers` because a plugin is a directory the engine
|
|
155
|
+
// loads, not a server paddock can describe — but their servers land in the same
|
|
156
|
+
// tool namespace and need the same allowlist widening, under names paddock has
|
|
157
|
+
// to derive rather than read (`claude-plugins.ts`).
|
|
158
|
+
if (hostPlugins.plugins.length > 0)
|
|
159
|
+
config.plugins = hostPlugins.plugins;
|
|
160
|
+
const extra = [
|
|
161
|
+
...Object.keys(external).map(mcpToolPattern),
|
|
162
|
+
...hostPlugins.toolPatterns,
|
|
163
|
+
].filter((pattern) => !FLEET_ALLOWED_TOOLS.includes(pattern));
|
|
164
|
+
if (extra.length > 0)
|
|
165
|
+
config.allowed_tools = [...FLEET_ALLOWED_TOOLS, ...extra];
|
|
109
166
|
return config;
|
|
110
167
|
}
|
|
111
168
|
/**
|
|
@@ -330,7 +387,12 @@ export async function ensureConfigFile(cfg) {
|
|
|
330
387
|
// several of these surface as deferred tools reached through it. These
|
|
331
388
|
// only actually DO anything in session drive-mode (the reaper re-fires
|
|
332
389
|
// them); in batch mode they're inert (documented in the box CLAUDE.md).
|
|
333
|
-
|
|
390
|
+
// The list itself lives in `herdctl-agent-names.ts` as FLEET_ALLOWED_TOOLS,
|
|
391
|
+
// because a keeper with host MCP servers attached (#691 step 5) has to
|
|
392
|
+
// restate it plus their `mcp__<server>__*` patterns — herdctl's
|
|
393
|
+
// `mergeAgentConfig` REPLACES arrays rather than merging them, so an agent
|
|
394
|
+
// that widens the allowlist inherits nothing from here.
|
|
395
|
+
allowed_tools: [...FLEET_ALLOWED_TOOLS],
|
|
334
396
|
// Best-effort denylist (#179): narrow, honest catastrophic-wipe patterns
|
|
335
397
|
// that do NOT block legitimate absolute-path cleanup. See DENIED_TOOLS.
|
|
336
398
|
denied_tools: [...DENIED_TOOLS],
|
|
@@ -100,6 +100,17 @@ export declare function visibleProjectAgentNames(project: Project): string[];
|
|
|
100
100
|
* no-op.
|
|
101
101
|
*/
|
|
102
102
|
export declare const BROWSER_MCP_TOOL = "mcp__playwright__*";
|
|
103
|
+
/**
|
|
104
|
+
* The fleet-wide `defaults.allowed_tools` a keeper inherits.
|
|
105
|
+
*
|
|
106
|
+
* Extracted from `ensureConfigFile` — where the reasoning for each entry still
|
|
107
|
+
* lives — because `claude.mcpServers: host` (#691 step 5) has to hand a keeper
|
|
108
|
+
* this list PLUS one `mcp__<server>__*` pattern per attached host server. Arrays
|
|
109
|
+
* are replaced rather than merged by herdctl's `mergeAgentConfig`, so widening
|
|
110
|
+
* the allowlist per-agent means restating the defaults, and restating them means
|
|
111
|
+
* they can only be written down once.
|
|
112
|
+
*/
|
|
113
|
+
export declare const FLEET_ALLOWED_TOOLS: readonly string[];
|
|
103
114
|
/**
|
|
104
115
|
* The keeper's default `denied_tools` — a **best-effort, defence-in-depth**
|
|
105
116
|
* denylist, NOT a sandbox. Real isolation (per-agent filesystem confinement)
|
|
@@ -116,6 +116,37 @@ export function visibleProjectAgentNames(project) {
|
|
|
116
116
|
* no-op.
|
|
117
117
|
*/
|
|
118
118
|
export const BROWSER_MCP_TOOL = "mcp__playwright__*";
|
|
119
|
+
/**
|
|
120
|
+
* The fleet-wide `defaults.allowed_tools` a keeper inherits.
|
|
121
|
+
*
|
|
122
|
+
* Extracted from `ensureConfigFile` — where the reasoning for each entry still
|
|
123
|
+
* lives — because `claude.mcpServers: host` (#691 step 5) has to hand a keeper
|
|
124
|
+
* this list PLUS one `mcp__<server>__*` pattern per attached host server. Arrays
|
|
125
|
+
* are replaced rather than merged by herdctl's `mergeAgentConfig`, so widening
|
|
126
|
+
* the allowlist per-agent means restating the defaults, and restating them means
|
|
127
|
+
* they can only be written down once.
|
|
128
|
+
*/
|
|
129
|
+
export const FLEET_ALLOWED_TOOLS = [
|
|
130
|
+
"Read",
|
|
131
|
+
"Edit",
|
|
132
|
+
"Write",
|
|
133
|
+
"Bash",
|
|
134
|
+
"Glob",
|
|
135
|
+
"Grep",
|
|
136
|
+
"WebFetch",
|
|
137
|
+
"WebSearch",
|
|
138
|
+
"Task",
|
|
139
|
+
"TodoWrite",
|
|
140
|
+
"Skill",
|
|
141
|
+
"NotebookEdit",
|
|
142
|
+
"ToolSearch",
|
|
143
|
+
"ScheduleWakeup",
|
|
144
|
+
"Monitor",
|
|
145
|
+
"CronCreate",
|
|
146
|
+
"CronList",
|
|
147
|
+
"CronDelete",
|
|
148
|
+
BROWSER_MCP_TOOL,
|
|
149
|
+
];
|
|
119
150
|
/**
|
|
120
151
|
* The keeper's default `denied_tools` — a **best-effort, defence-in-depth**
|
|
121
152
|
* denylist, NOT a sandbox. Real isolation (per-agent filesystem confinement)
|