@edspencer/paddock 0.61.0 → 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.
- package/README.md +36 -4
- package/package.json +1 -1
- package/packages/server/dist/agent-errors.d.ts +41 -0
- package/packages/server/dist/agent-errors.js +127 -0
- package/packages/server/dist/app.js +57 -9
- 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 +164 -25
- package/packages/server/dist/claude-home.js +528 -77
- 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 +146 -0
- package/packages/server/dist/claude-mcp.js +367 -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 -1
- package/packages/server/dist/cli/args.js +39 -5
- package/packages/server/dist/cli/paddock.js +30 -6
- 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 +11 -1
- package/packages/server/dist/herdctl-agent-config.js +53 -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 +69 -13
- package/packages/server/dist/herdctl.js +70 -18
- package/packages/server/dist/instance-config.js +36 -0
- package/packages/server/dist/mcp-servers.d.ts +168 -0
- package/packages/server/dist/mcp-servers.js +305 -0
- package/packages/server/dist/routes/chats.js +10 -4
- package/packages/server/dist/sweep.js +13 -1
- package/packages/server/dist/transcripts.d.ts +62 -24
- package/packages/server/dist/transcripts.js +141 -28
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-CzMqTdUU.js → InstanceConfigPage-Ca905oey.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-YSEZHWDe.js → MarkdownRenderer-ChlC1ezH.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DLWxZbqK.js → ProjectRedirect-CslqbhUb.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BFX8ff2n.js → ProjectView-DkNdilGo.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-DLOeY_ww.js → ProjectsGrid-BrlvLc-Y.js} +1 -1
- package/packages/web/dist/assets/{arc-BwC1ik-b.js → arc-CGqCI5nQ.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CsVvXQWw.js → architectureDiagram-3BPJPVTR-Cp4KTxE2.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DXQk2bIa.js → blockDiagram-GPEHLZMM-yFxaTv04.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DXiA26It.js → c4Diagram-AAUBKEIU-59lhWjRt.js} +1 -1
- package/packages/web/dist/assets/channel-CZs6Z1i5.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-Dt5nC02A.js → chunk-2J33WTMH-BrpXGnJg.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DcjnWvl6.js → chunk-4BX2VUAB-UxBDb5pi.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CRa3LkU4.js → chunk-55IACEB6-D4ZJyUHW.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-H3V-11qu.js → chunk-727SXJPM-DWdPj_8E.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-DPCrENYV.js → chunk-AQP2D5EJ-BIpNbflY.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-C4UO0ZTc.js → chunk-FMBD7UC4-f6iJDfdI.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-BtI37CnI.js → chunk-ND2GUHAM-Bz3TKIJk.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-C5bTaK5j.js → chunk-QZHKN3VN-buQmkafK.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +2 -0
- package/packages/web/dist/assets/{core-DB3cpm5N.js → core-TyFI6p0n.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-q-cjTZO4.js → cose-bilkent-S5V4N54A-CIkdi0XF.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DYEV5HFo.js → dagre-BM42HDAG-BnLJCfNs.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-D16XCw78.js → diagram-2AECGRRQ-D0OqfDB9.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-CeztryYE.js → diagram-5GNKFQAL-DQ2spGrK.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-D5SLIMDS.js → diagram-KO2AKTUF-RDIVQ_S3.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-Bw-sVPxM.js → diagram-LMA3HP47-DKoTfh2C.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-DsRMITgx.js → diagram-OG6HWLK6-6k0OuSAL.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-T8Qqc_ku.js → erDiagram-TEJ5UH35-B-oQaYAC.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-Cyx47UWU.js → flowDiagram-I6XJVG4X-B7qpYe9C.js} +1 -1
- package/packages/web/dist/assets/{format-D2I0duZa.js → format-PHdEBNOi.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DxYJmG9o.js → ganttDiagram-6RSMTGT7-SfBsqMmx.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-COK5YOTD.js → gitGraphDiagram-PVQCEYII-CuM4Rl_i.js} +1 -1
- package/packages/web/dist/assets/{index-wBXQMkGI.js → index-D_1ogxg1.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BamRN_Nn.js → infoDiagram-5YYISTIA-Dq-32Oj4.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DuOHdpEH.js → ishikawaDiagram-YF4QCWOH-BLFHCj_o.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Bri7GKTW.js → journeyDiagram-JHISSGLW-CHP-Yu63.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BdkgUnzI.js → kanban-definition-UN3LZRKU-DRuehSqn.js} +1 -1
- package/packages/web/dist/assets/{linear-DOcNPCpB.js → linear-Bwz2vnH7.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-D9SbVBpN.js → mermaid.core-CeMMnCip.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CVmLm7zn.js → mindmap-definition-RKZ34NQL-CEdqeoRW.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-D0N10eVU.js → pieDiagram-4H26LBE5-BdDXBx87.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B5gnYdTa.js → quadrantDiagram-W4KKPZXB-Bovd1Zhx.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CwtxezNZ.js → requirementDiagram-4Y6WPE33-DiyrKnJ2.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-D79O3cux.js → sankeyDiagram-5OEKKPKP-BuX7TRVw.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-xhKcI6tI.js → sequenceDiagram-3UESZ5HK-BjetZWJo.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DBYMAjcX.js → stateDiagram-AJRCARHV-4AUBV6dF.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Cq84MOtb.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-e-MYF4lm.js → timeline-definition-PNZ67QCA-BpQ8g8Bm.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-B8UsvRDM.js → vennDiagram-CIIHVFJN-Cj99BWER.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-1ucACjTl.js → wardley-L42UT6IY-S7AGTOWw.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DJufLd_u.js → wardleyDiagram-YWT4CUSO-LNXkUA7S.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-qqaZNlu9.js → xychartDiagram-2RQKCTM6-DGdTh4Xk.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/channel-DDgTXrGP.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU1kd_Vu.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU1kd_Vu.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CZtr9Jvq.js +0 -2
|
@@ -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,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,
|
|
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
|
-
//
|
|
105
|
-
//
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -50,6 +50,7 @@ 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";
|
|
53
54
|
import { type AdoptableSummary, type FilterReason } from "./adoptable.js";
|
|
54
55
|
/**
|
|
55
56
|
* One session an import did NOT bring in, with a reason a UI can show verbatim.
|
|
@@ -63,6 +64,20 @@ export type AdoptSkipped = AdoptSkippedSession | {
|
|
|
63
64
|
reason: FilterReason;
|
|
64
65
|
detail?: string;
|
|
65
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Outcome of deleting a chat (#689).
|
|
69
|
+
*
|
|
70
|
+
* Two fields rather than one boolean because "the transcript is gone" and "the
|
|
71
|
+
* chat is no longer listed here" stopped being the same thing: in a home paddock
|
|
72
|
+
* does not own, the file belongs to the user's terminal `claude` and is released
|
|
73
|
+
* rather than removed. See {@link HerdctlService.deleteSession}.
|
|
74
|
+
*/
|
|
75
|
+
export interface ChatDeletion {
|
|
76
|
+
/** A transcript file was unlinked. False when none existed, or when retained. */
|
|
77
|
+
removed: boolean;
|
|
78
|
+
/** The transcript was deliberately left on disk because it is not paddock's. */
|
|
79
|
+
retained: boolean;
|
|
80
|
+
}
|
|
66
81
|
/** Options passed through to a streamed trigger. */
|
|
67
82
|
export interface ChatTurnOptions {
|
|
68
83
|
prompt: string;
|
|
@@ -105,6 +120,7 @@ export { keeperAgentName, keeperSlugFromAgent, SWEEPER_PREFIX, sweeperAgentName,
|
|
|
105
120
|
export declare function sliceTranscriptAtUuid(raw: string, cutUuid: string, endOnCompletedAssistant?: boolean): string | null;
|
|
106
121
|
export declare class HerdctlService {
|
|
107
122
|
private readonly cfg;
|
|
123
|
+
private readonly mcpSources;
|
|
108
124
|
private fleet;
|
|
109
125
|
private started;
|
|
110
126
|
/**
|
|
@@ -164,13 +180,27 @@ export declare class HerdctlService {
|
|
|
164
180
|
*/
|
|
165
181
|
private jobsIndexOrNull;
|
|
166
182
|
private get jobsIndex();
|
|
167
|
-
constructor(cfg: PaddockConfig);
|
|
168
183
|
/**
|
|
169
|
-
* The
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* `
|
|
173
|
-
*
|
|
184
|
+
* The external MCP servers every keeper gets: the user's own under
|
|
185
|
+
* `claude.mcpServers: host` (#691 step 5, read once at boot by
|
|
186
|
+
* `loadHostMcpSource`) plus the ones this instance declares in its own
|
|
187
|
+
* `mcpServers:` block (step 6, resolved during config load). Both are merged
|
|
188
|
+
* into one value by `buildApp` and handed in here.
|
|
189
|
+
*
|
|
190
|
+
* A constructor argument rather than something this service reads for itself,
|
|
191
|
+
* for the same reason `ensureClaudeHome` runs in `buildApp`: the read has
|
|
192
|
+
* notices an operator needs to see in the boot log, and it must not happen at
|
|
193
|
+
* all under `mcpServers: own`. Defaults to empty so every existing caller —
|
|
194
|
+
* including the several tests that construct this with `{} as PaddockConfig` —
|
|
195
|
+
* keeps meaning "isolated".
|
|
196
|
+
*/
|
|
197
|
+
constructor(cfg: PaddockConfig, mcpSources?: McpSources);
|
|
198
|
+
/**
|
|
199
|
+
* The Claude home every transcript symlink is planted in, plus where those
|
|
200
|
+
* symlinks point (#620, #691). One accessor, so no call site can quietly take
|
|
201
|
+
* a default and land in a different home than the FleetManager was built with
|
|
202
|
+
* — the bug `ensureSweeperHome` had, which put the sweeper's symlink in
|
|
203
|
+
* `~/.claude` while every other agent's went to the configured home.
|
|
174
204
|
*/
|
|
175
205
|
private get claudeHomeTarget();
|
|
176
206
|
/**
|
|
@@ -421,13 +451,39 @@ export declare class HerdctlService {
|
|
|
421
451
|
*/
|
|
422
452
|
removeProjectAgent(slug: string, hookNames?: string[], triggerNames?: string[]): Promise<void>;
|
|
423
453
|
/**
|
|
424
|
-
* Delete a single chat (session) by agent name + session id.
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
|
|
430
|
-
|
|
454
|
+
* Delete a single chat (session) by agent name + session id.
|
|
455
|
+
*
|
|
456
|
+
* Under `claude.transcripts: own`, this removes the transcript JSONL: the file
|
|
457
|
+
* lives in paddock's own tree (via the project's `.chats/` symlink), so it is
|
|
458
|
+
* ours to delete. The FleetManager resolves the agent's working directory,
|
|
459
|
+
* unlinks the file, validates the sessionId against traversal, and invalidates
|
|
460
|
+
* the discovery cache so the list reflects it immediately.
|
|
461
|
+
*
|
|
462
|
+
* Under `claude.transcripts: host`, it **releases the session instead** (#689).
|
|
463
|
+
* There is no copy to delete there: the symlink points OUT at
|
|
464
|
+
* `~/.claude/projects/<encoded-cwd>/`, so the transcript the agent reads and
|
|
465
|
+
* writes is the user's own — for an adopted chat, literally their terminal
|
|
466
|
+
* history. `rm`-ing it destroys history paddock never owned, which is the same
|
|
467
|
+
* class of mistake #682 was, pointed the other way: that one claimed where
|
|
468
|
+
* future sessions get written, this one deletes the past ones.
|
|
469
|
+
*
|
|
470
|
+
* `undoImport` already draws exactly this line ("A session adopted in place has
|
|
471
|
+
* no copy, and its transcript is the user's own — removing it would destroy
|
|
472
|
+
* history rather than restore it"); the invariant simply never reached here.
|
|
473
|
+
*
|
|
474
|
+
* The discriminator changed with #691 and the meaning did not. It used to be
|
|
475
|
+
* `ownsClaudeHome` — "is this home ours?" — which is now always true; the
|
|
476
|
+
* question that survives is "is this transcript ours?", and that is exactly
|
|
477
|
+
* what `transcripts` says. Deliberately NOT narrowed to "only adopted chats":
|
|
478
|
+
* under `host` a chat paddock started itself is in the user's tree too, and
|
|
479
|
+
* distinguishing them well enough to risk an `rm` is not worth the failure mode
|
|
480
|
+
* of getting it wrong. The cost is a transcript left behind, which the caller
|
|
481
|
+
* is told about; the alternative cost is somebody's history.
|
|
482
|
+
*
|
|
483
|
+
* The chat leaves paddock's list either way — which is what the user asked for
|
|
484
|
+
* — so `retained` is how a caller tells "gone" from "no longer shown here".
|
|
485
|
+
*/
|
|
486
|
+
deleteSession(agentName: string, sessionId: string): Promise<ChatDeletion>;
|
|
431
487
|
/**
|
|
432
488
|
* Set (or clear) a chat's custom display name. Writes through the fleet's
|
|
433
489
|
* shared SessionMetadataStore so a subsequent getAgentSessions reflects it
|
|
@@ -56,6 +56,7 @@ 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";
|
|
59
60
|
import * as jobs from "./herdctl-jobs.js";
|
|
60
61
|
import { JobsDirIndex } from "./herdctl-jobs-index.js";
|
|
61
62
|
import { AdoptableIndex } from "./adoptable.js";
|
|
@@ -162,6 +163,7 @@ export function sliceTranscriptAtUuid(raw, cutUuid, endOnCompletedAssistant = fa
|
|
|
162
163
|
}
|
|
163
164
|
export class HerdctlService {
|
|
164
165
|
cfg;
|
|
166
|
+
mcpSources;
|
|
165
167
|
fleet = null;
|
|
166
168
|
started = false;
|
|
167
169
|
/**
|
|
@@ -223,18 +225,37 @@ export class HerdctlService {
|
|
|
223
225
|
get jobsIndex() {
|
|
224
226
|
return (this.jobsIndexOrNull ??= new JobsDirIndex(this.cfg.stateDir));
|
|
225
227
|
}
|
|
226
|
-
|
|
228
|
+
/**
|
|
229
|
+
* The external MCP servers every keeper gets: the user's own under
|
|
230
|
+
* `claude.mcpServers: host` (#691 step 5, read once at boot by
|
|
231
|
+
* `loadHostMcpSource`) plus the ones this instance declares in its own
|
|
232
|
+
* `mcpServers:` block (step 6, resolved during config load). Both are merged
|
|
233
|
+
* into one value by `buildApp` and handed in here.
|
|
234
|
+
*
|
|
235
|
+
* A constructor argument rather than something this service reads for itself,
|
|
236
|
+
* for the same reason `ensureClaudeHome` runs in `buildApp`: the read has
|
|
237
|
+
* notices an operator needs to see in the boot log, and it must not happen at
|
|
238
|
+
* all under `mcpServers: own`. Defaults to empty so every existing caller —
|
|
239
|
+
* including the several tests that construct this with `{} as PaddockConfig` —
|
|
240
|
+
* keeps meaning "isolated".
|
|
241
|
+
*/
|
|
242
|
+
constructor(cfg, mcpSources = EMPTY_MCP_SOURCES) {
|
|
227
243
|
this.cfg = cfg;
|
|
244
|
+
this.mcpSources = mcpSources;
|
|
228
245
|
}
|
|
229
246
|
/**
|
|
230
|
-
* The Claude home every transcript symlink is planted in, plus
|
|
231
|
-
*
|
|
232
|
-
* land in a different home than the FleetManager was built with
|
|
233
|
-
* `ensureSweeperHome` had, which put the sweeper's symlink in
|
|
234
|
-
* every other agent's went to the configured home.
|
|
247
|
+
* The Claude home every transcript symlink is planted in, plus where those
|
|
248
|
+
* symlinks point (#620, #691). One accessor, so no call site can quietly take
|
|
249
|
+
* a default and land in a different home than the FleetManager was built with
|
|
250
|
+
* — the bug `ensureSweeperHome` had, which put the sweeper's symlink in
|
|
251
|
+
* `~/.claude` while every other agent's went to the configured home.
|
|
235
252
|
*/
|
|
236
253
|
get claudeHomeTarget() {
|
|
237
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
path: this.cfg.claudeHome,
|
|
256
|
+
transcripts: this.cfg.claude.transcripts,
|
|
257
|
+
userHome: this.cfg.legacyClaudeHome,
|
|
258
|
+
};
|
|
238
259
|
}
|
|
239
260
|
/**
|
|
240
261
|
* The environment system prompt to append to a keeper turn (issue #635), or
|
|
@@ -309,10 +330,10 @@ export class HerdctlService {
|
|
|
309
330
|
allowScheduleMutation: this.cfg.scheduleMutationEnabled,
|
|
310
331
|
// Hand the engine the SAME Claude home paddock resolved (#588). Omitting
|
|
311
332
|
// it makes the engine fall back to `os.homedir()/.claude` while paddock
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
333
|
+
// uses its own, so discovery, adoption and paddock's transcript symlinks
|
|
334
|
+
// would resolve against different directories — sessions list from one
|
|
335
|
+
// home and open empty from the other. Masked whenever the two coincide,
|
|
336
|
+
// which is why it must be explicit.
|
|
316
337
|
claudeHomePath: this.cfg.claudeHome,
|
|
317
338
|
});
|
|
318
339
|
await this.fleet.initialize();
|
|
@@ -763,14 +784,45 @@ export class HerdctlService {
|
|
|
763
784
|
}
|
|
764
785
|
}
|
|
765
786
|
/**
|
|
766
|
-
* Delete a single chat (session) by agent name + session id.
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
787
|
+
* Delete a single chat (session) by agent name + session id.
|
|
788
|
+
*
|
|
789
|
+
* Under `claude.transcripts: own`, this removes the transcript JSONL: the file
|
|
790
|
+
* lives in paddock's own tree (via the project's `.chats/` symlink), so it is
|
|
791
|
+
* ours to delete. The FleetManager resolves the agent's working directory,
|
|
792
|
+
* unlinks the file, validates the sessionId against traversal, and invalidates
|
|
793
|
+
* the discovery cache so the list reflects it immediately.
|
|
794
|
+
*
|
|
795
|
+
* Under `claude.transcripts: host`, it **releases the session instead** (#689).
|
|
796
|
+
* There is no copy to delete there: the symlink points OUT at
|
|
797
|
+
* `~/.claude/projects/<encoded-cwd>/`, so the transcript the agent reads and
|
|
798
|
+
* writes is the user's own — for an adopted chat, literally their terminal
|
|
799
|
+
* history. `rm`-ing it destroys history paddock never owned, which is the same
|
|
800
|
+
* class of mistake #682 was, pointed the other way: that one claimed where
|
|
801
|
+
* future sessions get written, this one deletes the past ones.
|
|
802
|
+
*
|
|
803
|
+
* `undoImport` already draws exactly this line ("A session adopted in place has
|
|
804
|
+
* no copy, and its transcript is the user's own — removing it would destroy
|
|
805
|
+
* history rather than restore it"); the invariant simply never reached here.
|
|
806
|
+
*
|
|
807
|
+
* The discriminator changed with #691 and the meaning did not. It used to be
|
|
808
|
+
* `ownsClaudeHome` — "is this home ours?" — which is now always true; the
|
|
809
|
+
* question that survives is "is this transcript ours?", and that is exactly
|
|
810
|
+
* what `transcripts` says. Deliberately NOT narrowed to "only adopted chats":
|
|
811
|
+
* under `host` a chat paddock started itself is in the user's tree too, and
|
|
812
|
+
* distinguishing them well enough to risk an `rm` is not worth the failure mode
|
|
813
|
+
* of getting it wrong. The cost is a transcript left behind, which the caller
|
|
814
|
+
* is told about; the alternative cost is somebody's history.
|
|
815
|
+
*
|
|
816
|
+
* The chat leaves paddock's list either way — which is what the user asked for
|
|
817
|
+
* — so `retained` is how a caller tells "gone" from "no longer shown here".
|
|
771
818
|
*/
|
|
772
819
|
async deleteSession(agentName, sessionId) {
|
|
773
|
-
|
|
820
|
+
if (this.cfg.claude.transcripts === "own") {
|
|
821
|
+
return { removed: await this.manager.deleteSession(agentName, sessionId), retained: false };
|
|
822
|
+
}
|
|
823
|
+
await this.manager.unadoptSession(agentName, sessionId).catch(() => undefined);
|
|
824
|
+
this.invalidateSessions(agentName);
|
|
825
|
+
return { removed: false, retained: true };
|
|
774
826
|
}
|
|
775
827
|
/**
|
|
776
828
|
* Set (or clear) a chat's custom display name. Writes through the fleet's
|
|
@@ -1456,7 +1508,7 @@ export class HerdctlService {
|
|
|
1456
1508
|
// existing unit-test seams (which reach `sweeperAgentConfig`/`ensureConfigFile`
|
|
1457
1509
|
// via a cast) and internal callers keep working.
|
|
1458
1510
|
keeperAgentConfig(project, modelOverride) {
|
|
1459
|
-
return buildAgentConfig(this.cfg, project, modelOverride);
|
|
1511
|
+
return buildAgentConfig(this.cfg, project, modelOverride, this.mcpSources);
|
|
1460
1512
|
}
|
|
1461
1513
|
sweeperAgentConfig(project) {
|
|
1462
1514
|
return buildSweeperConfig(this.cfg, project);
|