@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.
Files changed (94) hide show
  1. package/README.md +65 -16
  2. package/package.json +2 -2
  3. package/packages/server/dist/app.js +48 -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 +158 -0
  11. package/packages/server/dist/claude-mcp.js +389 -0
  12. package/packages/server/dist/claude-plugins.d.ts +131 -0
  13. package/packages/server/dist/claude-plugins.js +394 -0
  14. package/packages/server/dist/claude-settings.d.ts +159 -0
  15. package/packages/server/dist/claude-settings.js +159 -0
  16. package/packages/server/dist/cli/args.d.ts +1 -9
  17. package/packages/server/dist/cli/args.js +46 -19
  18. package/packages/server/dist/cli/paddock.js +20 -39
  19. package/packages/server/dist/config.d.ts +166 -30
  20. package/packages/server/dist/config.js +106 -26
  21. package/packages/server/dist/herdctl-agent-config.d.ts +17 -1
  22. package/packages/server/dist/herdctl-agent-config.js +69 -7
  23. package/packages/server/dist/herdctl-agent-names.d.ts +11 -0
  24. package/packages/server/dist/herdctl-agent-names.js +31 -0
  25. package/packages/server/dist/herdctl.d.ts +75 -13
  26. package/packages/server/dist/herdctl.js +77 -18
  27. package/packages/server/dist/instance-config.js +36 -0
  28. package/packages/server/dist/mcp-servers.d.ts +187 -0
  29. package/packages/server/dist/mcp-servers.js +399 -0
  30. package/packages/server/dist/routes/chats.js +10 -4
  31. package/packages/server/dist/transcripts.d.ts +61 -30
  32. package/packages/server/dist/transcripts.js +141 -46
  33. package/packages/server/package.json +1 -1
  34. package/packages/web/dist/assets/{InstanceConfigPage-BgNXqAox.js → InstanceConfigPage-CCk2hVT9.js} +1 -1
  35. package/packages/web/dist/assets/{MarkdownRenderer-DfD_UtqU.js → MarkdownRenderer-B6gST_0U.js} +1 -1
  36. package/packages/web/dist/assets/{ProjectRedirect-1EXdHXmO.js → ProjectRedirect-BCmKT4mf.js} +1 -1
  37. package/packages/web/dist/assets/{ProjectView-DHb5vNUp.js → ProjectView-CCN98MWu.js} +3 -3
  38. package/packages/web/dist/assets/{ProjectsGrid-Br-EhLZE.js → ProjectsGrid-CYzuN_yD.js} +1 -1
  39. package/packages/web/dist/assets/{arc-C2Jekujs.js → arc-BxvtMCZQ.js} +1 -1
  40. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CflCA-em.js → architectureDiagram-3BPJPVTR-Bp6ffJ_u.js} +1 -1
  41. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Ds7GLNnC.js → blockDiagram-GPEHLZMM-BIY2X8nW.js} +1 -1
  42. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-D4WyGO6I.js → c4Diagram-AAUBKEIU-Br9stEJS.js} +1 -1
  43. package/packages/web/dist/assets/channel-CMdgNeFH.js +2 -0
  44. package/packages/web/dist/assets/{chunk-2J33WTMH-tQunO_M5.js → chunk-2J33WTMH-Djc7uqMV.js} +1 -1
  45. package/packages/web/dist/assets/{chunk-4BX2VUAB-DT5dOWd2.js → chunk-4BX2VUAB-yEVMLksS.js} +1 -1
  46. package/packages/web/dist/assets/{chunk-55IACEB6-CfTUqUEF.js → chunk-55IACEB6-tJaEdPZg.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-727SXJPM-sK5bOAA0.js → chunk-727SXJPM-bWEqh672.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-AQP2D5EJ-DcMgVe-P.js → chunk-AQP2D5EJ-B8U9fB-T.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-FMBD7UC4-In3h_-7v.js → chunk-FMBD7UC4-BeEITYig.js} +1 -1
  50. package/packages/web/dist/assets/{chunk-ND2GUHAM-DuaIeRaC.js → chunk-ND2GUHAM-L2rpLaoK.js} +1 -1
  51. package/packages/web/dist/assets/{chunk-QZHKN3VN-DeT5mR5T.js → chunk-QZHKN3VN-qqzPMZL_.js} +1 -1
  52. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +2 -0
  53. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +2 -0
  54. package/packages/web/dist/assets/{core-DgOgLvnM.js → core-CSaNbmLO.js} +1 -1
  55. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CyO-rznJ.js → cose-bilkent-S5V4N54A-BRF7Nech.js} +1 -1
  56. package/packages/web/dist/assets/{dagre-BM42HDAG-B_9Ihzgn.js → dagre-BM42HDAG-D10rJWOR.js} +1 -1
  57. package/packages/web/dist/assets/{diagram-2AECGRRQ-z5mvZ5YT.js → diagram-2AECGRRQ-DGjT5mAO.js} +1 -1
  58. package/packages/web/dist/assets/{diagram-5GNKFQAL-Bsz0cgfn.js → diagram-5GNKFQAL-DaH3bxHh.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-KO2AKTUF-B29k5f4q.js → diagram-KO2AKTUF-QrLW9Wn6.js} +1 -1
  60. package/packages/web/dist/assets/{diagram-LMA3HP47-CFrwsHoI.js → diagram-LMA3HP47-BX15wgYN.js} +1 -1
  61. package/packages/web/dist/assets/{diagram-OG6HWLK6-D9Sjc2HR.js → diagram-OG6HWLK6-DXQ_p63s.js} +1 -1
  62. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C3vbkD7M.js → erDiagram-TEJ5UH35-C_ioRruE.js} +1 -1
  63. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DkMIgntd.js → flowDiagram-I6XJVG4X-7d-xLXLM.js} +1 -1
  64. package/packages/web/dist/assets/{format-BFDqyFuw.js → format-BSDZezwD.js} +1 -1
  65. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-nvHanjpW.js → ganttDiagram-6RSMTGT7-DZYx5xcj.js} +1 -1
  66. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DbCNfOu7.js → gitGraphDiagram-PVQCEYII-dimo-osm.js} +1 -1
  67. package/packages/web/dist/assets/{index-DGCs1YIO.js → index-CUnFKMga.js} +2 -2
  68. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-aJjH9x3D.js → infoDiagram-5YYISTIA-BzApY25x.js} +1 -1
  69. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DoifwNnZ.js → ishikawaDiagram-YF4QCWOH-DtX47_9p.js} +1 -1
  70. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cz9kG9HN.js → journeyDiagram-JHISSGLW-C7fITKg1.js} +1 -1
  71. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BoXnfr7A.js → kanban-definition-UN3LZRKU-BOrWLf_W.js} +1 -1
  72. package/packages/web/dist/assets/{linear-BlTTO9yR.js → linear-DuyAaBu6.js} +1 -1
  73. package/packages/web/dist/assets/{mermaid.core-YAG8eSPQ.js → mermaid.core-BQlzuVvO.js} +4 -4
  74. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DtUBKfkr.js → mindmap-definition-RKZ34NQL-CmP748B5.js} +1 -1
  75. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-B3Rwpmy0.js → pieDiagram-4H26LBE5-CRd5Pdfq.js} +1 -1
  76. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B8eJtTyk.js → quadrantDiagram-W4KKPZXB-DpJm0Yw_.js} +1 -1
  77. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CpAluiY9.js → requirementDiagram-4Y6WPE33-B0o2PRMC.js} +1 -1
  78. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js → sankeyDiagram-5OEKKPKP-CBi3Ek3v.js} +1 -1
  79. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CRpqKSGm.js → sequenceDiagram-3UESZ5HK-C1stxfQg.js} +1 -1
  80. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-h3h8gXv_.js → stateDiagram-AJRCARHV-lCkiETWK.js} +1 -1
  81. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +2 -0
  82. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-PVr5V_pG.js → timeline-definition-PNZ67QCA-D3DkWJMC.js} +1 -1
  83. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-De-4NPAX.js → vennDiagram-CIIHVFJN-CO2-yC7K.js} +1 -1
  84. package/packages/web/dist/assets/{wardley-L42UT6IY-Dtv10D2d.js → wardley-L42UT6IY-Dfjp0kqX.js} +1 -1
  85. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-p2fJuQ6C.js → wardleyDiagram-YWT4CUSO-CIl0OPwS.js} +1 -1
  86. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DyYF-k92.js → xychartDiagram-2RQKCTM6-CuvlQuup.js} +1 -1
  87. package/packages/web/dist/index.html +1 -1
  88. package/packages/web/dist/sw.js +1 -1
  89. package/packages/server/dist/cli/claude-home-choice.d.ts +0 -44
  90. package/packages/server/dist/cli/claude-home-choice.js +0 -108
  91. package/packages/web/dist/assets/channel-B1K_gwAH.js +0 -2
  92. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +0 -2
  93. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +0 -2
  94. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BKRI9TnH.js +0 -2
@@ -50,6 +50,8 @@ import { FleetManager, type DiscoveredSession, type ChatMessage, type SDKMessage
50
50
  import type { PaddockConfig } from "./config.js";
51
51
  import type { Project } from "./projects.js";
52
52
  import { type PaddockTrigger } from "./trigger-config.js";
53
+ import { type McpSources } from "./claude-mcp.js";
54
+ import { type HostPluginSource } from "./claude-plugins.js";
53
55
  import { type AdoptableSummary, type FilterReason } from "./adoptable.js";
54
56
  /**
55
57
  * One session an import did NOT bring in, with a reason a UI can show verbatim.
@@ -63,6 +65,20 @@ export type AdoptSkipped = AdoptSkippedSession | {
63
65
  reason: FilterReason;
64
66
  detail?: string;
65
67
  };
68
+ /**
69
+ * Outcome of deleting a chat (#689).
70
+ *
71
+ * Two fields rather than one boolean because "the transcript is gone" and "the
72
+ * chat is no longer listed here" stopped being the same thing: in a home paddock
73
+ * does not own, the file belongs to the user's terminal `claude` and is released
74
+ * rather than removed. See {@link HerdctlService.deleteSession}.
75
+ */
76
+ export interface ChatDeletion {
77
+ /** A transcript file was unlinked. False when none existed, or when retained. */
78
+ removed: boolean;
79
+ /** The transcript was deliberately left on disk because it is not paddock's. */
80
+ retained: boolean;
81
+ }
66
82
  /** Options passed through to a streamed trigger. */
67
83
  export interface ChatTurnOptions {
68
84
  prompt: string;
@@ -105,6 +121,8 @@ export { keeperAgentName, keeperSlugFromAgent, SWEEPER_PREFIX, sweeperAgentName,
105
121
  export declare function sliceTranscriptAtUuid(raw: string, cutUuid: string, endOnCompletedAssistant?: boolean): string | null;
106
122
  export declare class HerdctlService {
107
123
  private readonly cfg;
124
+ private readonly mcpSources;
125
+ private readonly hostPlugins;
108
126
  private fleet;
109
127
  private started;
110
128
  /**
@@ -164,13 +182,31 @@ export declare class HerdctlService {
164
182
  */
165
183
  private jobsIndexOrNull;
166
184
  private get jobsIndex();
167
- constructor(cfg: PaddockConfig);
168
185
  /**
169
- * The Claude home every transcript symlink is planted in, plus whether paddock
170
- * owns it (#620). One accessor, so no call site can quietly take a default and
171
- * land in a different home than the FleetManager was built with — the bug
172
- * `ensureSweeperHome` had, which put the sweeper's symlink in `~/.claude` while
173
- * every other agent's went to the configured home.
186
+ * The external MCP servers every keeper gets: the user's own under
187
+ * `claude.mcpServers: host` (#691 step 5, read once at boot by
188
+ * `loadHostMcpSource`) plus the ones this instance declares in its own
189
+ * `mcpServers:` block (step 6, resolved during config load). Both are merged
190
+ * into one value by `buildApp` and handed in here.
191
+ *
192
+ * A constructor argument rather than something this service reads for itself,
193
+ * for the same reason `ensureClaudeHome` runs in `buildApp`: the read has
194
+ * notices an operator needs to see in the boot log, and it must not happen at
195
+ * all under `mcpServers: own`. Defaults to empty so every existing caller —
196
+ * including the several tests that construct this with `{} as PaddockConfig` —
197
+ * keeps meaning "isolated".
198
+ *
199
+ * `hostPlugins` is the host's Claude Code plugins (#700), read at boot by
200
+ * `loadHostPlugins` under `claude.instructions: host`, and handed in for
201
+ * exactly the same reasons.
202
+ */
203
+ constructor(cfg: PaddockConfig, mcpSources?: McpSources, hostPlugins?: HostPluginSource);
204
+ /**
205
+ * The Claude home every transcript symlink is planted in, plus where those
206
+ * symlinks point (#620, #691). One accessor, so no call site can quietly take
207
+ * a default and land in a different home than the FleetManager was built with
208
+ * — the bug `ensureSweeperHome` had, which put the sweeper's symlink in
209
+ * `~/.claude` while every other agent's went to the configured home.
174
210
  */
175
211
  private get claudeHomeTarget();
176
212
  /**
@@ -421,13 +457,39 @@ export declare class HerdctlService {
421
457
  */
422
458
  removeProjectAgent(slug: string, hookNames?: string[], triggerNames?: string[]): Promise<void>;
423
459
  /**
424
- * Delete a single chat (session) by agent name + session id. The FleetManager
425
- * resolves the agent's working directory, removes the transcript JSONL, and
426
- * invalidates the discovery cache so the list reflects it immediately.
427
- * Validates the sessionId (rejects path traversal). Returns true if a
428
- * transcript file was removed, false if none existed.
429
- */
430
- deleteSession(agentName: string, sessionId: string): Promise<boolean>;
460
+ * Delete a single chat (session) by agent name + session id.
461
+ *
462
+ * Under `claude.transcripts: own`, this removes the transcript JSONL: the file
463
+ * lives in paddock's own tree (via the project's `.chats/` symlink), so it is
464
+ * ours to delete. The FleetManager resolves the agent's working directory,
465
+ * unlinks the file, validates the sessionId against traversal, and invalidates
466
+ * the discovery cache so the list reflects it immediately.
467
+ *
468
+ * Under `claude.transcripts: host`, it **releases the session instead** (#689).
469
+ * There is no copy to delete there: the symlink points OUT at
470
+ * `~/.claude/projects/<encoded-cwd>/`, so the transcript the agent reads and
471
+ * writes is the user's own — for an adopted chat, literally their terminal
472
+ * history. `rm`-ing it destroys history paddock never owned, which is the same
473
+ * class of mistake #682 was, pointed the other way: that one claimed where
474
+ * future sessions get written, this one deletes the past ones.
475
+ *
476
+ * `undoImport` already draws exactly this line ("A session adopted in place has
477
+ * no copy, and its transcript is the user's own — removing it would destroy
478
+ * history rather than restore it"); the invariant simply never reached here.
479
+ *
480
+ * The discriminator changed with #691 and the meaning did not. It used to be
481
+ * `ownsClaudeHome` — "is this home ours?" — which is now always true; the
482
+ * question that survives is "is this transcript ours?", and that is exactly
483
+ * what `transcripts` says. Deliberately NOT narrowed to "only adopted chats":
484
+ * under `host` a chat paddock started itself is in the user's tree too, and
485
+ * distinguishing them well enough to risk an `rm` is not worth the failure mode
486
+ * of getting it wrong. The cost is a transcript left behind, which the caller
487
+ * is told about; the alternative cost is somebody's history.
488
+ *
489
+ * The chat leaves paddock's list either way — which is what the user asked for
490
+ * — so `retained` is how a caller tells "gone" from "no longer shown here".
491
+ */
492
+ deleteSession(agentName: string, sessionId: string): Promise<ChatDeletion>;
431
493
  /**
432
494
  * Set (or clear) a chat's custom display name. Writes through the fleet's
433
495
  * shared SessionMetadataStore so a subsequent getAgentSessions reflects it
@@ -56,6 +56,8 @@ import { DEFAULT_MODEL } from "./models.js";
56
56
  import { ensureProjectChats, projectChatsDir } from "./transcripts.js";
57
57
  import { triggerRunsOnOwnAgent, isCuratorTrigger, } from "./trigger-config.js";
58
58
  import { buildAgentConfig, buildSweeperConfig, sweeperWorkingDir, buildTriggerConfig, ensureConfigFile as writeBootConfigFile, } from "./herdctl-agent-config.js";
59
+ import { EMPTY_MCP_SOURCES } from "./claude-mcp.js";
60
+ import { EMPTY_HOST_PLUGINS } from "./claude-plugins.js";
59
61
  import * as jobs from "./herdctl-jobs.js";
60
62
  import { JobsDirIndex } from "./herdctl-jobs-index.js";
61
63
  import { AdoptableIndex } from "./adoptable.js";
@@ -162,6 +164,8 @@ export function sliceTranscriptAtUuid(raw, cutUuid, endOnCompletedAssistant = fa
162
164
  }
163
165
  export class HerdctlService {
164
166
  cfg;
167
+ mcpSources;
168
+ hostPlugins;
165
169
  fleet = null;
166
170
  started = false;
167
171
  /**
@@ -223,18 +227,42 @@ export class HerdctlService {
223
227
  get jobsIndex() {
224
228
  return (this.jobsIndexOrNull ??= new JobsDirIndex(this.cfg.stateDir));
225
229
  }
226
- constructor(cfg) {
230
+ /**
231
+ * The external MCP servers every keeper gets: the user's own under
232
+ * `claude.mcpServers: host` (#691 step 5, read once at boot by
233
+ * `loadHostMcpSource`) plus the ones this instance declares in its own
234
+ * `mcpServers:` block (step 6, resolved during config load). Both are merged
235
+ * into one value by `buildApp` and handed in here.
236
+ *
237
+ * A constructor argument rather than something this service reads for itself,
238
+ * for the same reason `ensureClaudeHome` runs in `buildApp`: the read has
239
+ * notices an operator needs to see in the boot log, and it must not happen at
240
+ * all under `mcpServers: own`. Defaults to empty so every existing caller —
241
+ * including the several tests that construct this with `{} as PaddockConfig` —
242
+ * keeps meaning "isolated".
243
+ *
244
+ * `hostPlugins` is the host's Claude Code plugins (#700), read at boot by
245
+ * `loadHostPlugins` under `claude.instructions: host`, and handed in for
246
+ * exactly the same reasons.
247
+ */
248
+ constructor(cfg, mcpSources = EMPTY_MCP_SOURCES, hostPlugins = EMPTY_HOST_PLUGINS) {
227
249
  this.cfg = cfg;
250
+ this.mcpSources = mcpSources;
251
+ this.hostPlugins = hostPlugins;
228
252
  }
229
253
  /**
230
- * The Claude home every transcript symlink is planted in, plus whether paddock
231
- * owns it (#620). One accessor, so no call site can quietly take a default and
232
- * land in a different home than the FleetManager was built with — the bug
233
- * `ensureSweeperHome` had, which put the sweeper's symlink in `~/.claude` while
234
- * every other agent's went to the configured home.
254
+ * The Claude home every transcript symlink is planted in, plus where those
255
+ * symlinks point (#620, #691). One accessor, so no call site can quietly take
256
+ * a default and land in a different home than the FleetManager was built with
257
+ * — the bug `ensureSweeperHome` had, which put the sweeper's symlink in
258
+ * `~/.claude` while every other agent's went to the configured home.
235
259
  */
236
260
  get claudeHomeTarget() {
237
- return { path: this.cfg.claudeHome, owned: this.cfg.ownsClaudeHome };
261
+ return {
262
+ path: this.cfg.claudeHome,
263
+ transcripts: this.cfg.claude.transcripts,
264
+ userHome: this.cfg.legacyClaudeHome,
265
+ };
238
266
  }
239
267
  /**
240
268
  * The environment system prompt to append to a keeper turn (issue #635), or
@@ -309,10 +337,10 @@ export class HerdctlService {
309
337
  allowScheduleMutation: this.cfg.scheduleMutationEnabled,
310
338
  // Hand the engine the SAME Claude home paddock resolved (#588). Omitting
311
339
  // it makes the engine fall back to `os.homedir()/.claude` while paddock
312
- // honours `CLAUDE_HOME`, so discovery, adoption and paddock's `.chats/`
313
- // symlinks would resolve against different directories — sessions list
314
- // from one home and open empty from the other. Masked whenever the two
315
- // coincide, which is why it must be explicit.
340
+ // uses its own, so discovery, adoption and paddock's transcript symlinks
341
+ // would resolve against different directories — sessions list from one
342
+ // home and open empty from the other. Masked whenever the two coincide,
343
+ // which is why it must be explicit.
316
344
  claudeHomePath: this.cfg.claudeHome,
317
345
  });
318
346
  await this.fleet.initialize();
@@ -763,14 +791,45 @@ export class HerdctlService {
763
791
  }
764
792
  }
765
793
  /**
766
- * Delete a single chat (session) by agent name + session id. The FleetManager
767
- * resolves the agent's working directory, removes the transcript JSONL, and
768
- * invalidates the discovery cache so the list reflects it immediately.
769
- * Validates the sessionId (rejects path traversal). Returns true if a
770
- * transcript file was removed, false if none existed.
794
+ * Delete a single chat (session) by agent name + session id.
795
+ *
796
+ * Under `claude.transcripts: own`, this removes the transcript JSONL: the file
797
+ * lives in paddock's own tree (via the project's `.chats/` symlink), so it is
798
+ * ours to delete. The FleetManager resolves the agent's working directory,
799
+ * unlinks the file, validates the sessionId against traversal, and invalidates
800
+ * the discovery cache so the list reflects it immediately.
801
+ *
802
+ * Under `claude.transcripts: host`, it **releases the session instead** (#689).
803
+ * There is no copy to delete there: the symlink points OUT at
804
+ * `~/.claude/projects/<encoded-cwd>/`, so the transcript the agent reads and
805
+ * writes is the user's own — for an adopted chat, literally their terminal
806
+ * history. `rm`-ing it destroys history paddock never owned, which is the same
807
+ * class of mistake #682 was, pointed the other way: that one claimed where
808
+ * future sessions get written, this one deletes the past ones.
809
+ *
810
+ * `undoImport` already draws exactly this line ("A session adopted in place has
811
+ * no copy, and its transcript is the user's own — removing it would destroy
812
+ * history rather than restore it"); the invariant simply never reached here.
813
+ *
814
+ * The discriminator changed with #691 and the meaning did not. It used to be
815
+ * `ownsClaudeHome` — "is this home ours?" — which is now always true; the
816
+ * question that survives is "is this transcript ours?", and that is exactly
817
+ * what `transcripts` says. Deliberately NOT narrowed to "only adopted chats":
818
+ * under `host` a chat paddock started itself is in the user's tree too, and
819
+ * distinguishing them well enough to risk an `rm` is not worth the failure mode
820
+ * of getting it wrong. The cost is a transcript left behind, which the caller
821
+ * is told about; the alternative cost is somebody's history.
822
+ *
823
+ * The chat leaves paddock's list either way — which is what the user asked for
824
+ * — so `retained` is how a caller tells "gone" from "no longer shown here".
771
825
  */
772
826
  async deleteSession(agentName, sessionId) {
773
- return this.manager.deleteSession(agentName, sessionId);
827
+ if (this.cfg.claude.transcripts === "own") {
828
+ return { removed: await this.manager.deleteSession(agentName, sessionId), retained: false };
829
+ }
830
+ await this.manager.unadoptSession(agentName, sessionId).catch(() => undefined);
831
+ this.invalidateSessions(agentName);
832
+ return { removed: false, retained: true };
774
833
  }
775
834
  /**
776
835
  * Set (or clear) a chat's custom display name. Writes through the fleet's
@@ -1456,7 +1515,7 @@ export class HerdctlService {
1456
1515
  // existing unit-test seams (which reach `sweeperAgentConfig`/`ensureConfigFile`
1457
1516
  // via a cast) and internal callers keep working.
1458
1517
  keeperAgentConfig(project, modelOverride) {
1459
- return buildAgentConfig(this.cfg, project, modelOverride);
1518
+ return buildAgentConfig(this.cfg, project, modelOverride, this.mcpSources, this.hostPlugins);
1460
1519
  }
1461
1520
  sweeperAgentConfig(project) {
1462
1521
  return buildSweeperConfig(this.cfg, project);
@@ -36,6 +36,11 @@ import { DEFAULT_RECOVERY } from "./recovery-config.js";
36
36
  import { DEFAULT_ATTACHMENTS, sanitizeAllowedTypes } from "./attachments-config.js";
37
37
  import { DEFAULT_CURATION } from "./curation-config.js";
38
38
  import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
39
+ import { DEFAULT_TRANSCRIPTS_MODE } from "./transcripts.js";
40
+ import { DEFAULT_CREDENTIALS_MODE } from "./claude-credentials.js";
41
+ import { DEFAULT_INSTRUCTIONS_MODE } from "./claude-instructions.js";
42
+ import { DEFAULT_HOOKS_MODE } from "./claude-settings.js";
43
+ import { DEFAULT_MCP_SERVERS_MODE } from "./claude-mcp.js";
39
44
  /** Groups the Settings screen renders, in display order. */
40
45
  export const GROUPS = [
41
46
  { id: "curation", label: "Curation", description: "Per-file token budgets the post-turn sweeper keeps its curated files under." },
@@ -219,6 +224,37 @@ export const FIELDS = [
219
224
  { key: "stateDir", group: "advanced", label: "State dir", type: "string", envVars: ["PADDOCK_STATE_DIR"], default: null, editable: false },
220
225
  { key: "herdctlConfigPath", group: "advanced", label: "herdctl config path", type: "string", envVars: ["PADDOCK_HERDCTL_CONFIG"], default: null, editable: false },
221
226
  { key: "webDist", group: "advanced", label: "Web dist", type: "string", envVars: ["PADDOCK_WEB_DIST"], default: null, editable: false },
227
+ // What this instance shares with the host's Claude Code (#691). READ-ONLY on
228
+ // purpose: `host` means paddock writes to the user's real transcript files,
229
+ // and the symlinks that implement it are planted at agent-registration time —
230
+ // a toggle that silently does nothing until the next boot would be worse than
231
+ // no toggle. It is surfaced because "what is this instance sharing?" should be
232
+ // answerable without reading a YAML file.
233
+ { key: "claude.transcripts", group: "advanced", label: "Transcripts", help: "own = Paddock's own, in each project's .chats/; host = your ~/.claude transcripts, shared live.", type: "string", envVars: ["PADDOCK_CLAUDE_TRANSCRIPTS"], default: DEFAULT_TRANSCRIPTS_MODE, editable: false },
234
+ // Read-only for the same reason, plus one of its own: the secure-storage
235
+ // variable it sets is read by Claude Code when a turn's process starts, so a
236
+ // live toggle would apply to some turns and not others.
237
+ { key: "claude.credentials", group: "advanced", label: "Credentials", help: "host = this machine's Claude Code login (macOS Keychain, or your ~/.claude/.credentials.json); own = only a login of this instance's.", type: "string", envVars: ["PADDOCK_CLAUDE_CREDENTIALS"], default: DEFAULT_CREDENTIALS_MODE, editable: false },
238
+ // Read-only for the same reasons. `hooks` is the one worth finding here even
239
+ // though it cannot be changed here: "does this instance run the shell commands
240
+ // my ~/.claude/settings.json binds to tool use?" is a question with a security
241
+ // answer, and it should be readable without opening a YAML file.
242
+ { key: "claude.instructions", group: "advanced", label: "Instructions", help: "own = this instance's own only; host = your ~/.claude CLAUDE.md, agents/, commands/ and plugins/ as well.", type: "string", envVars: ["PADDOCK_CLAUDE_INSTRUCTIONS"], default: DEFAULT_INSTRUCTIONS_MODE, editable: false },
243
+ { key: "claude.hooks", group: "advanced", label: "Hooks", help: "own = your ~/.claude/settings.json hooks do NOT run here (its other keys still apply); host = they do.", type: "string", envVars: ["PADDOCK_CLAUDE_HOOKS"], default: DEFAULT_HOOKS_MODE, editable: false },
244
+ // The fifth lever, which step 5 shipped without surfacing here. Read-only like
245
+ // its four siblings, and worth the row for the same reason `hooks` is: an MCP
246
+ // server is a process this instance spawns, so "is this instance running my
247
+ // machine's MCP servers?" should be answerable without opening a YAML file.
248
+ //
249
+ // NOTE what is deliberately absent: the top-level `mcpServers:` block (#691
250
+ // step 6), which declares servers rather than borrowing them. Its values hold
251
+ // resolved credentials — an MCP server's `env` is where a stdio server's API
252
+ // token lives — and every field in this table is serialised verbatim into the
253
+ // GET response, so a row for it would publish tokens to any authenticated UI
254
+ // user. There is no redacting variant of `FieldSpec`, and inventing one to
255
+ // display a server list is not worth the leak surface: the boot log already
256
+ // names every declared server, secret-free, via `describeServer`.
257
+ { key: "claude.mcpServers", group: "advanced", label: "MCP servers", help: "own = only the servers Paddock provides itself; host = the ones declared in your ~/.claude.json as well.", type: "string", envVars: ["PADDOCK_CLAUDE_MCP_SERVERS"], default: DEFAULT_MCP_SERVERS_MODE, editable: false },
222
258
  // Auth: read-only in v1 (misconfig can lock everyone out — issue #385). Only
223
259
  // the mode is surfaced; JWT/JWKS internals stay out of the API.
224
260
  { key: "auth.mode", group: "advanced", label: "Auth mode", type: "string", envVars: ["PADDOCK_AUTH_MODE"], default: "none", editable: false, sensitive: true },
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The top-level `mcpServers:` block of `paddock.config.yaml` (#691 step 6):
3
+ * MCP servers a user declares **to paddock itself**.
4
+ *
5
+ * ## Why this is not another `claude:` key
6
+ *
7
+ * The five `claude:` levers all answer one question — *whose X does this instance
8
+ * use?* — and every one of them has a `host` side that borrows something the
9
+ * machine already has. `claude.mcpServers: host` therefore only helps a user who
10
+ * has already run `claude mcp add` on this box. Someone standing up paddock in a
11
+ * container, who wants Notion inside it, has nothing to borrow; the question they
12
+ * are asking is *"this instance should have this server"*, which is a declaration
13
+ * rather than an inheritance. So it is a sibling of `claude:`, not a member.
14
+ *
15
+ * ## Everything below the parse is shared with step 5
16
+ *
17
+ * `claude-mcp.ts` already built the hard half: the shape herdctl can carry, the
18
+ * per-working-directory merge, and — the trap — widening the keeper's
19
+ * `allowed_tools` by one `mcp__<name>__*` pattern per server, without which an
20
+ * attached server has every call auto-denied with no prompt and no log line. A
21
+ * declared server needs all of it identically, so this module produces
22
+ * {@link McpServerDefs} and hands them to {@link McpSources.declared}. The only
23
+ * thing that is genuinely new here is validation and secrets.
24
+ *
25
+ * ## Precedence
26
+ *
27
+ * `host` < declared < paddock's own. A name declared here beats the same name
28
+ * inherited from `~/.claude.json`, because this file is a statement about THIS
29
+ * instance and the other is ambient machine state. Paddock's own servers
30
+ * (`send_file`, the self-management tools, the browser server) still win, for the
31
+ * reason they already won over `host`: their configuration is box-specific and
32
+ * they are wired up by a different mechanism entirely. A collision with the
33
+ * browser server is warned about by name at boot rather than being silent; a
34
+ * collision with an INJECTED server is refused outright, because those are
35
+ * materialised as an HTTP bridge under the same `mcp__<name>__*` namespace and
36
+ * two servers claiming one namespace has no defined winner.
37
+ *
38
+ * ## Secrets, which is the real work here
39
+ *
40
+ * This is the first place in #691 where a user types a credential into paddock's
41
+ * own config file, and that file is git-tracked (and readable through the
42
+ * Settings screen's config path). So this module borrows the rule
43
+ * `management-config.ts` already sets for management tokens: **`env:VAR_NAME` is
44
+ * a reference, resolved from the environment at boot.** Here it is accepted
45
+ * anywhere a string is (`command`, each `args` entry, each `env` value, `url`),
46
+ * so there is one rule to learn rather than a list of blessed fields.
47
+ *
48
+ * Unlike a management token, an MCP `env` entry is not necessarily secret
49
+ * (`NOTION_VERSION: "2022-06-28"` is not), so an inline value is a WARNING and
50
+ * not an error — but the warning is emitted for any key that looks like a
51
+ * credential, and nothing in this module ever prints a value. `headers` follows
52
+ * the same rule and for a stronger reason: `Authorization` is the usual reason a
53
+ * header is declared at all. Every diagnostic names keys, variable names and
54
+ * server names only; a `url` is reported with its query string and userinfo
55
+ * stripped, because that is where an API key rides. {@link describeServer} is the
56
+ * single place a server is rendered for a human, so there is one function to
57
+ * audit rather than a scattering of template strings.
58
+ *
59
+ * The resolved values do land in the frozen `PaddockConfig` (as management tokens
60
+ * already do). They must never reach an API response: `instance-config.ts`
61
+ * publishes only the fields in its own `FIELDS` table, and this block is
62
+ * deliberately absent from it.
63
+ *
64
+ * One exposure the `env:` indirection does NOT close: under `driveMode: batch`
65
+ * the resolved value reaches a COMMAND LINE. See {@link argvExposure} — and note
66
+ * that carrying `headers` (#700) widens it, because an `Authorization` bearer is
67
+ * a likelier long-lived credential than an `env` entry.
68
+ *
69
+ * ## Validation is strict, and drops rather than degrades
70
+ *
71
+ * Step 5 passes a host server it cannot carry faithfully, with a warning, on the
72
+ * grounds that a user who declared a server elsewhere should get it plus a
73
+ * warning rather than nothing plus a warning. That reasoning inverts here,
74
+ * because the user typed this file: an unusable declaration is a mistake they can
75
+ * fix. So anything that cannot be carried is an ERROR and that server is not
76
+ * attached — including an unknown key, which is how a typo (`arg:` for `args:`)
77
+ * otherwise becomes a server that starts wrong.
78
+ *
79
+ * `headers:` and `type: sse` USED to be in that category, because herdctl's
80
+ * schema had no field for either. herdctl 5.32.0 (#446) carries both verbatim,
81
+ * so refusing them would now reject a declaration that works — they are accepted,
82
+ * and `headers` is a first-class secret-bearing field with the same `env:VAR`
83
+ * resolution and the same never-print rule as `env`.
84
+ *
85
+ * Never a boot failure, though: one bad server drops itself and the rest attach,
86
+ * matching `resolveManagementApiConfig`. A typo in a capability must not take an
87
+ * instance down.
88
+ */
89
+ import type { McpServerDefs, McpServerDef } from "./claude-mcp.js";
90
+ import type { DriveMode } from "./models.js";
91
+ /** On-disk shape of one entry of the `mcpServers:` block. Every field optional. */
92
+ export interface McpServerConfigFile {
93
+ /** Executable for a stdio server. Mutually exclusive with {@link url}. */
94
+ command?: string;
95
+ /** Arguments for {@link command}. */
96
+ args?: string[];
97
+ /** Environment for the server process. Values may be `env:VAR_NAME`. */
98
+ env?: Record<string, string>;
99
+ /** Endpoint for a remote server. Mutually exclusive with {@link command}. */
100
+ url?: string;
101
+ /** `stdio` for a {@link command}; `http` or `sse` for a {@link url}. */
102
+ type?: string;
103
+ /** Headers for a remote server. Values may be `env:VAR_NAME`. */
104
+ headers?: Record<string, string>;
105
+ }
106
+ /** On-disk shape of the whole block: a map of server name → declaration. */
107
+ export type McpServersConfigFile = Record<string, McpServerConfigFile | null | undefined>;
108
+ /** Outcome of resolving the block, with everything worth telling the operator. */
109
+ export interface DeclaredMcpResolution {
110
+ /** The servers that will be attached to every keeper. */
111
+ servers: McpServerDefs;
112
+ /** Declarations that could not be carried (logged at error level, server dropped). */
113
+ errors: string[];
114
+ /** Advice and fail-closed drops (logged at warn level). */
115
+ warnings: string[];
116
+ }
117
+ /**
118
+ * The prefix that makes a string an environment reference rather than a literal.
119
+ * Same spelling as `managementApi.clients.<id>.auth.ref`, deliberately: an
120
+ * operator should have to learn it once.
121
+ */
122
+ export declare const ENV_REF_PREFIX = "env:";
123
+ /**
124
+ * Server names paddock materialises itself, which a declaration may not take.
125
+ *
126
+ * Kept as literals rather than imported from `send-file-mcp.ts`/`self-mcp.ts` so
127
+ * config resolution stays free of the runtime modules — `mcp-servers-reserved`
128
+ * in the unit tests asserts the two spellings still agree.
129
+ */
130
+ export declare const RESERVED_MCP_SERVER_NAMES: readonly string[];
131
+ /**
132
+ * A URL with everything credential-shaped removed — no query string, no
133
+ * fragment, no `user:pass@`. Used for every log line that mentions a `url`,
134
+ * because a remote MCP endpoint's API key is conventionally a query parameter.
135
+ * An unparseable URL is reported as its scheme only rather than echoed.
136
+ */
137
+ export declare function redactUrl(raw: string): string;
138
+ /**
139
+ * A server rendered for a human, carrying no secret. THE chokepoint: every
140
+ * notice, warning and error about a declared server goes through here, so
141
+ * auditing "can a token reach a log line?" is auditing one function.
142
+ *
143
+ * Args and env values are counted, never printed — an API key is as likely to be
144
+ * `--token=…` in `args` as it is to be an `env` value. The command is reduced to
145
+ * its basename for the same reason, which the leak test in
146
+ * `test/unit/mcp-servers.test.ts` is what forced: a full path is more useful in a
147
+ * log, but the rule "nothing from the block is echoed" is only worth having if it
148
+ * has no exceptions to remember. The server's own name is printed alongside and
149
+ * is what actually identifies it.
150
+ */
151
+ export declare function describeServer(name: string, def: McpServerDef): string;
152
+ /**
153
+ * Resolve the whole `mcpServers:` block against `env`.
154
+ *
155
+ * Never throws. A declaration paddock cannot carry is reported and dropped so one
156
+ * typo cannot take an instance down — the same posture as
157
+ * `resolveManagementApiConfig`, and for the same reason: this is a capability,
158
+ * and losing a capability is better than losing the instance.
159
+ */
160
+ export declare function resolveDeclaredMcpServers(file: McpServersConfigFile | undefined, env: Record<string, string | undefined>): DeclaredMcpResolution;
161
+ /** A line for the boot log, at a level (mirrors `HostMcpNotice`). */
162
+ export interface DeclaredMcpNotice {
163
+ level: "info" | "warn";
164
+ message: string;
165
+ }
166
+ /**
167
+ * What to say at boot about the declared servers — the answer to "what does this
168
+ * instance actually have?", which should be readable off the log rather than
169
+ * inferred from a YAML file plus an environment.
170
+ *
171
+ * Takes the already-resolved servers, so it cannot see a raw declaration and
172
+ * therefore cannot leak one; every line it produces goes through
173
+ * {@link describeServer}.
174
+ */
175
+ export declare function declaredMcpNotices(opts: {
176
+ servers: McpServerDefs;
177
+ /** Names inherited from `~/.claude.json`, for the shadowing notice. */
178
+ hostNames?: readonly string[];
179
+ /** Whether paddock's own browser server is attached (it wins a name clash). */
180
+ browserMcp?: boolean;
181
+ /**
182
+ * The instance's drive mode, which decides whether a declared `env` value ends
183
+ * up on a command line. Omitted ⇒ say nothing (the several callers that only
184
+ * want the inventory line).
185
+ */
186
+ driveMode?: DriveMode;
187
+ }): DeclaredMcpNotice[];