@edspencer/paddock 0.61.1 → 0.62.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 (92) hide show
  1. package/README.md +35 -14
  2. package/package.json +1 -1
  3. package/packages/server/dist/app.js +35 -8
  4. package/packages/server/dist/claude-credentials.d.ts +116 -0
  5. package/packages/server/dist/claude-credentials.js +103 -0
  6. package/packages/server/dist/claude-home.d.ts +102 -30
  7. package/packages/server/dist/claude-home.js +437 -97
  8. package/packages/server/dist/claude-instructions.d.ts +121 -0
  9. package/packages/server/dist/claude-instructions.js +114 -0
  10. package/packages/server/dist/claude-mcp.d.ts +146 -0
  11. package/packages/server/dist/claude-mcp.js +367 -0
  12. package/packages/server/dist/claude-settings.d.ts +159 -0
  13. package/packages/server/dist/claude-settings.js +159 -0
  14. package/packages/server/dist/cli/args.d.ts +1 -9
  15. package/packages/server/dist/cli/args.js +36 -18
  16. package/packages/server/dist/cli/paddock.js +20 -39
  17. package/packages/server/dist/config.d.ts +166 -30
  18. package/packages/server/dist/config.js +106 -26
  19. package/packages/server/dist/herdctl-agent-config.d.ts +11 -1
  20. package/packages/server/dist/herdctl-agent-config.js +53 -7
  21. package/packages/server/dist/herdctl-agent-names.d.ts +11 -0
  22. package/packages/server/dist/herdctl-agent-names.js +31 -0
  23. package/packages/server/dist/herdctl.d.ts +69 -13
  24. package/packages/server/dist/herdctl.js +70 -18
  25. package/packages/server/dist/instance-config.js +36 -0
  26. package/packages/server/dist/mcp-servers.d.ts +168 -0
  27. package/packages/server/dist/mcp-servers.js +305 -0
  28. package/packages/server/dist/routes/chats.js +10 -4
  29. package/packages/server/dist/transcripts.d.ts +61 -30
  30. package/packages/server/dist/transcripts.js +141 -46
  31. package/packages/server/package.json +1 -1
  32. package/packages/web/dist/assets/{InstanceConfigPage-BgNXqAox.js → InstanceConfigPage-Ca905oey.js} +1 -1
  33. package/packages/web/dist/assets/{MarkdownRenderer-DfD_UtqU.js → MarkdownRenderer-ChlC1ezH.js} +1 -1
  34. package/packages/web/dist/assets/{ProjectRedirect-1EXdHXmO.js → ProjectRedirect-CslqbhUb.js} +1 -1
  35. package/packages/web/dist/assets/{ProjectView-DHb5vNUp.js → ProjectView-DkNdilGo.js} +3 -3
  36. package/packages/web/dist/assets/{ProjectsGrid-Br-EhLZE.js → ProjectsGrid-BrlvLc-Y.js} +1 -1
  37. package/packages/web/dist/assets/{arc-C2Jekujs.js → arc-CGqCI5nQ.js} +1 -1
  38. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CflCA-em.js → architectureDiagram-3BPJPVTR-Cp4KTxE2.js} +1 -1
  39. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Ds7GLNnC.js → blockDiagram-GPEHLZMM-yFxaTv04.js} +1 -1
  40. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-D4WyGO6I.js → c4Diagram-AAUBKEIU-59lhWjRt.js} +1 -1
  41. package/packages/web/dist/assets/channel-CZs6Z1i5.js +2 -0
  42. package/packages/web/dist/assets/{chunk-2J33WTMH-tQunO_M5.js → chunk-2J33WTMH-BrpXGnJg.js} +1 -1
  43. package/packages/web/dist/assets/{chunk-4BX2VUAB-DT5dOWd2.js → chunk-4BX2VUAB-UxBDb5pi.js} +1 -1
  44. package/packages/web/dist/assets/{chunk-55IACEB6-CfTUqUEF.js → chunk-55IACEB6-D4ZJyUHW.js} +1 -1
  45. package/packages/web/dist/assets/{chunk-727SXJPM-sK5bOAA0.js → chunk-727SXJPM-DWdPj_8E.js} +1 -1
  46. package/packages/web/dist/assets/{chunk-AQP2D5EJ-DcMgVe-P.js → chunk-AQP2D5EJ-BIpNbflY.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-FMBD7UC4-In3h_-7v.js → chunk-FMBD7UC4-f6iJDfdI.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-ND2GUHAM-DuaIeRaC.js → chunk-ND2GUHAM-Bz3TKIJk.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-QZHKN3VN-DeT5mR5T.js → chunk-QZHKN3VN-buQmkafK.js} +1 -1
  50. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +2 -0
  51. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +2 -0
  52. package/packages/web/dist/assets/{core-DgOgLvnM.js → core-TyFI6p0n.js} +1 -1
  53. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CyO-rznJ.js → cose-bilkent-S5V4N54A-CIkdi0XF.js} +1 -1
  54. package/packages/web/dist/assets/{dagre-BM42HDAG-B_9Ihzgn.js → dagre-BM42HDAG-BnLJCfNs.js} +1 -1
  55. package/packages/web/dist/assets/{diagram-2AECGRRQ-z5mvZ5YT.js → diagram-2AECGRRQ-D0OqfDB9.js} +1 -1
  56. package/packages/web/dist/assets/{diagram-5GNKFQAL-Bsz0cgfn.js → diagram-5GNKFQAL-DQ2spGrK.js} +1 -1
  57. package/packages/web/dist/assets/{diagram-KO2AKTUF-B29k5f4q.js → diagram-KO2AKTUF-RDIVQ_S3.js} +1 -1
  58. package/packages/web/dist/assets/{diagram-LMA3HP47-CFrwsHoI.js → diagram-LMA3HP47-DKoTfh2C.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-OG6HWLK6-D9Sjc2HR.js → diagram-OG6HWLK6-6k0OuSAL.js} +1 -1
  60. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C3vbkD7M.js → erDiagram-TEJ5UH35-B-oQaYAC.js} +1 -1
  61. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DkMIgntd.js → flowDiagram-I6XJVG4X-B7qpYe9C.js} +1 -1
  62. package/packages/web/dist/assets/{format-BFDqyFuw.js → format-PHdEBNOi.js} +1 -1
  63. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-nvHanjpW.js → ganttDiagram-6RSMTGT7-SfBsqMmx.js} +1 -1
  64. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DbCNfOu7.js → gitGraphDiagram-PVQCEYII-CuM4Rl_i.js} +1 -1
  65. package/packages/web/dist/assets/{index-DGCs1YIO.js → index-D_1ogxg1.js} +2 -2
  66. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-aJjH9x3D.js → infoDiagram-5YYISTIA-Dq-32Oj4.js} +1 -1
  67. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DoifwNnZ.js → ishikawaDiagram-YF4QCWOH-BLFHCj_o.js} +1 -1
  68. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cz9kG9HN.js → journeyDiagram-JHISSGLW-CHP-Yu63.js} +1 -1
  69. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BoXnfr7A.js → kanban-definition-UN3LZRKU-DRuehSqn.js} +1 -1
  70. package/packages/web/dist/assets/{linear-BlTTO9yR.js → linear-Bwz2vnH7.js} +1 -1
  71. package/packages/web/dist/assets/{mermaid.core-YAG8eSPQ.js → mermaid.core-CeMMnCip.js} +4 -4
  72. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DtUBKfkr.js → mindmap-definition-RKZ34NQL-CEdqeoRW.js} +1 -1
  73. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-B3Rwpmy0.js → pieDiagram-4H26LBE5-BdDXBx87.js} +1 -1
  74. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B8eJtTyk.js → quadrantDiagram-W4KKPZXB-Bovd1Zhx.js} +1 -1
  75. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CpAluiY9.js → requirementDiagram-4Y6WPE33-DiyrKnJ2.js} +1 -1
  76. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js → sankeyDiagram-5OEKKPKP-BuX7TRVw.js} +1 -1
  77. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CRpqKSGm.js → sequenceDiagram-3UESZ5HK-BjetZWJo.js} +1 -1
  78. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-h3h8gXv_.js → stateDiagram-AJRCARHV-4AUBV6dF.js} +1 -1
  79. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Cq84MOtb.js +2 -0
  80. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-PVr5V_pG.js → timeline-definition-PNZ67QCA-BpQ8g8Bm.js} +1 -1
  81. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-De-4NPAX.js → vennDiagram-CIIHVFJN-Cj99BWER.js} +1 -1
  82. package/packages/web/dist/assets/{wardley-L42UT6IY-Dtv10D2d.js → wardley-L42UT6IY-S7AGTOWw.js} +1 -1
  83. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-p2fJuQ6C.js → wardleyDiagram-YWT4CUSO-LNXkUA7S.js} +1 -1
  84. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DyYF-k92.js → xychartDiagram-2RQKCTM6-DGdTh4Xk.js} +1 -1
  85. package/packages/web/dist/index.html +1 -1
  86. package/packages/web/dist/sw.js +1 -1
  87. package/packages/server/dist/cli/claude-home-choice.d.ts +0 -44
  88. package/packages/server/dist/cli/claude-home-choice.js +0 -108
  89. package/packages/web/dist/assets/channel-B1K_gwAH.js +0 -2
  90. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +0 -2
  91. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +0 -2
  92. 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
- * Defaults to `<dataDir>/claude-home` (#620); see {@link resolveClaudeHome}
100
- * for the full precedence.
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
- * Does paddock OWN {@link claudeHome} — i.e. is it something other than the
127
- * user's `~/.claude`?
202
+ * What this instance shares with the host's Claude Code (#691) — currently
203
+ * just `transcripts`. See {@link ClaudeConfig}.
128
204
  *
129
- * The single gate on every mutating transcript operation (#620). Paddock's
130
- * self-healing relocation is allowed to migrate a stray real transcript
131
- * directory into `.chats/` inside a home it owns, because that directory is
132
- * its own doing. Inside the user's home it is somebody else's data — a
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
- ownsClaudeHome: boolean;
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 override; beneath `CLAUDE_HOME`/`CLAUDE_CONFIG_DIR` (#620). */
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 the Claude home paddock runs its agents against.
684
+ * Resolve where paddock's OWN Claude home lives.
565
685
  *
566
686
  * Precedence, highest first:
567
687
  *
568
- * 1. **`CLAUDE_HOME`** — paddock's own long-standing override. Setting it to
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
- * 3. A `claudeHome:` key in the instance config file.
578
- * 4. **`<dataDir>/claude-home`** — the default since #620.
694
+ * 2. A `claudeHome:` key in the instance config file.
695
+ * 3. **`<dataDir>/claude-home`** — the default.
579
696
  *
580
- * On the default, and why it changed: paddock already nests herdctl's whole
581
- * state dir, the projects tree and the generated `herdctl.yaml` under its data
582
- * dir. Transcripts were the one exception, reached by planting symlinks in the
583
- * user's home — not a design choice but a constraint, because until
584
- * herdctl#423 nothing set `CLAUDE_CONFIG_DIR` and the SDK wrote to `~/.claude`
585
- * no matter what paddock configured. With that gone, owning the home makes the
586
- * data dir relocatable as a unit, leaves `~/.claude` alone, and (because there
587
- * is no longer a `.claude` path component) unblocks agent memory writes, which
588
- * the harness refuses for any path containing `.claude`.
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
- // Paddock owns any home that is not the user's own `~/.claude` (#620) —
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 the Claude home paddock runs its agents against.
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. **`CLAUDE_HOME`** — paddock's own long-standing override. Setting it to
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
- * 3. A `claudeHome:` key in the instance config file.
642
- * 4. **`<dataDir>/claude-home`** — the default since #620.
683
+ * 2. A `claudeHome:` key in the instance config file.
684
+ * 3. **`<dataDir>/claude-home`** — the default.
643
685
  *
644
- * On the default, and why it changed: paddock already nests herdctl's whole
645
- * state dir, the projects tree and the generated `herdctl.yaml` under its data
646
- * dir. Transcripts were the one exception, reached by planting symlinks in the
647
- * user's home — not a design choice but a constraint, because until
648
- * herdctl#423 nothing set `CLAUDE_CONFIG_DIR` and the SDK wrote to `~/.claude`
649
- * no matter what paddock configured. With that gone, owning the home makes the
650
- * data dir relocatable as a unit, leaves `~/.claude` alone, and (because there
651
- * is no longer a `.claude` path component) unblocks agent memory writes, which
652
- * the harness refuses for any path containing `.claude`.
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("CLAUDE_HOME") ?? envOpt("CLAUDE_CONFIG_DIR");
665
- if (env !== undefined)
666
- return abs(env);
667
- if (fromFile !== undefined && fromFile !== "")
668
- return abs(fromFile);
669
- return path.join(dataDir, DEFAULT_CLAUDE_HOME_DIRNAME);
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,6 @@
1
1
  import type { PaddockConfig } from "./config.js";
2
2
  import type { Project } from "./projects.js";
3
+ import { type McpSources } from "./claude-mcp.js";
3
4
  import { type PaddockTrigger } from "./trigger-config.js";
4
5
  /**
5
6
  * A project's keeper agent config. Inherits the fleet `defaults` (runtime,
@@ -24,8 +25,17 @@ import { type PaddockTrigger } from "./trigger-config.js";
24
25
  * not `<dataDir>`: `projectsRoot` IS the instance's backing repo, so the file is
25
26
  * version-controlled. Since #516 the ROOT project reaches it the same way —
26
27
  * its cwd IS `projectsRoot`, so the walk-up needs no special-casing.
28
+ *
29
+ * `mcpSources` is the external MCP servers this instance attaches — the user's
30
+ * own under `claude.mcpServers: host` (#691 step 5) plus the ones the instance
31
+ * declares in its own `mcpServers:` block (step 6). Both are resolved once at
32
+ * boot and passed in rather than re-read here, so this stays pure and so a
33
+ * `.claude.json` is never opened under `mcpServers: own`. It defaults to empty,
34
+ * which is both the isolated mode and what every existing caller means. The
35
+ * KEEPER is the only agent that gets them: the sweeper is tool-less and each
36
+ * trigger declares its own narrow allowlist, so neither could call one anyway.
27
37
  */
28
- export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string): Record<string, unknown> & {
38
+ export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string, mcpSources?: McpSources): Record<string, unknown> & {
29
39
  name: string;
30
40
  };
31
41
  /**
@@ -13,9 +13,10 @@
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";
16
17
  import { DEFAULT_MODEL, SWEEPER_DEFAULT_MODEL, DEFAULT_PERMISSION_MODE, DEFAULT_MAX_TURNS, } from "./models.js";
17
18
  import { triggerToAgentToolConfig, triggersToHerdctlSchedules, curatorTriggerOf, } from "./trigger-config.js";
18
- import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers, BROWSER_MCP_TOOL, DENIED_TOOLS, KEEPER_MAX_CONCURRENT, KEEPER_SESSION_TIMEOUT, } from "./herdctl-agent-names.js";
19
+ import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers, FLEET_ALLOWED_TOOLS, DENIED_TOOLS, KEEPER_MAX_CONCURRENT, KEEPER_SESSION_TIMEOUT, } from "./herdctl-agent-names.js";
19
20
  /**
20
21
  * A project's keeper agent config. Inherits the fleet `defaults` (runtime,
21
22
  * max_turns, permission_mode, allowed/denied tools) via addAgent's deep-merge;
@@ -39,8 +40,17 @@ import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers,
39
40
  * not `<dataDir>`: `projectsRoot` IS the instance's backing repo, so the file is
40
41
  * version-controlled. Since #516 the ROOT project reaches it the same way —
41
42
  * its cwd IS `projectsRoot`, so the walk-up needs no special-casing.
43
+ *
44
+ * `mcpSources` is the external MCP servers this instance attaches — the user's
45
+ * own under `claude.mcpServers: host` (#691 step 5) plus the ones the instance
46
+ * declares in its own `mcpServers:` block (step 6). Both are resolved once at
47
+ * boot and passed in rather than re-read here, so this stays pure and so a
48
+ * `.claude.json` is never opened under `mcpServers: own`. It defaults to empty,
49
+ * which is both the isolated mode and what every existing caller means. The
50
+ * KEEPER is the only agent that gets them: the sweeper is tool-less and each
51
+ * trigger declares its own narrow allowlist, so neither could call one anyway.
42
52
  */
43
- export function buildAgentConfig(cfg, project, modelOverride) {
53
+ export function buildAgentConfig(cfg, project, modelOverride, mcpSources = EMPTY_MCP_SOURCES) {
44
54
  const config = {
45
55
  name: keeperAgentName(project.slug),
46
56
  description: project.summary || `Claude Code agent for project ${project.name}.`,
@@ -101,11 +111,42 @@ export function buildAgentConfig(cfg, project, modelOverride) {
101
111
  const schedules = triggersToHerdctlSchedules(project.triggers);
102
112
  if (schedules)
103
113
  config.schedules = schedules;
104
- // Browser MCP (headless Chromium) when enabled for this box; `mcp__playwright__*`
105
- // is already on the inherited defaults.allowed_tools.
114
+ // MCP servers, from three sources, all landing on the SAME `mcp_servers` key:
115
+ //
116
+ // - the browser MCP (headless Chromium) when enabled for this box —
117
+ // `mcp__playwright__*` is already on the inherited defaults.allowed_tools;
118
+ // - the user's own servers under `claude.mcpServers: host` (#691 step 5);
119
+ // - the ones this instance declares in its own `mcpServers:` block (#691 step
120
+ // 6), which win over the inherited ones (`mcpServersFor` merges them last).
121
+ //
122
+ // Neither of the latter two is on the default allowlist, because their names are
123
+ // not knowable at config-file-write time — hence the widening below.
124
+ //
125
+ // Paddock's own server still wins a name collision, against BOTH: its flags are
126
+ // box-specific (the Ansible-installed chromium engine, --no-sandbox for an
127
+ // unprivileged LXC) and someone else's `playwright` would almost certainly not
128
+ // start here. A declared collision is warned about by name at boot
129
+ // (`declaredMcpNotices`) so it is not silent.
106
130
  const browser = browserMcpServers(cfg.browserMcp);
107
- if (browser)
108
- config.mcp_servers = browser;
131
+ const external = mcpServersFor(mcpSources, project.workingDir);
132
+ const servers = { ...external, ...(browser ?? {}) };
133
+ if (Object.keys(servers).length > 0)
134
+ config.mcp_servers = servers;
135
+ // Widen the allowlist by exactly those servers' tool patterns. WITHOUT this
136
+ // both levers are a no-op with no error: both runtimes auto-deny any tool
137
+ // missing from an explicit `allowed_tools`, so an attached-but-unlisted server
138
+ // connects and then has every call refused. herdctl does this automatically for
139
+ // INJECTED servers only (`cli-runtime.js`), never for config `mcp_servers` —
140
+ // which is why `mcp__playwright__*` is hard-coded into the defaults.
141
+ //
142
+ // Patterns already on the defaults are filtered out, so nothing is restated
143
+ // for an instance with no external servers (or one whose only server paddock's
144
+ // own overrode above) — that config stays byte-identical to before this lever.
145
+ const extra = Object.keys(external)
146
+ .map(mcpToolPattern)
147
+ .filter((pattern) => !FLEET_ALLOWED_TOOLS.includes(pattern));
148
+ if (extra.length > 0)
149
+ config.allowed_tools = [...FLEET_ALLOWED_TOOLS, ...extra];
109
150
  return config;
110
151
  }
111
152
  /**
@@ -330,7 +371,12 @@ export async function ensureConfigFile(cfg) {
330
371
  // several of these surface as deferred tools reached through it. These
331
372
  // only actually DO anything in session drive-mode (the reaper re-fires
332
373
  // them); in batch mode they're inert (documented in the box CLAUDE.md).
333
- allowed_tools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "Task", "TodoWrite", "Skill", "NotebookEdit", "ToolSearch", "ScheduleWakeup", "Monitor", "CronCreate", "CronList", "CronDelete", BROWSER_MCP_TOOL],
374
+ // The list itself lives in `herdctl-agent-names.ts` as FLEET_ALLOWED_TOOLS,
375
+ // because a keeper with host MCP servers attached (#691 step 5) has to
376
+ // restate it plus their `mcp__<server>__*` patterns — herdctl's
377
+ // `mergeAgentConfig` REPLACES arrays rather than merging them, so an agent
378
+ // that widens the allowlist inherits nothing from here.
379
+ allowed_tools: [...FLEET_ALLOWED_TOOLS],
334
380
  // Best-effort denylist (#179): narrow, honest catastrophic-wipe patterns
335
381
  // that do NOT block legitimate absolute-path cleanup. See DENIED_TOOLS.
336
382
  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)