@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.
- package/README.md +35 -14
- package/package.json +1 -1
- package/packages/server/dist/app.js +35 -8
- package/packages/server/dist/claude-credentials.d.ts +116 -0
- package/packages/server/dist/claude-credentials.js +103 -0
- package/packages/server/dist/claude-home.d.ts +102 -30
- package/packages/server/dist/claude-home.js +437 -97
- package/packages/server/dist/claude-instructions.d.ts +121 -0
- package/packages/server/dist/claude-instructions.js +114 -0
- package/packages/server/dist/claude-mcp.d.ts +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 -9
- package/packages/server/dist/cli/args.js +36 -18
- package/packages/server/dist/cli/paddock.js +20 -39
- package/packages/server/dist/config.d.ts +166 -30
- package/packages/server/dist/config.js +106 -26
- package/packages/server/dist/herdctl-agent-config.d.ts +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/transcripts.d.ts +61 -30
- package/packages/server/dist/transcripts.js +141 -46
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-BgNXqAox.js → InstanceConfigPage-Ca905oey.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DfD_UtqU.js → MarkdownRenderer-ChlC1ezH.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-1EXdHXmO.js → ProjectRedirect-CslqbhUb.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-DHb5vNUp.js → ProjectView-DkNdilGo.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-Br-EhLZE.js → ProjectsGrid-BrlvLc-Y.js} +1 -1
- package/packages/web/dist/assets/{arc-C2Jekujs.js → arc-CGqCI5nQ.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CflCA-em.js → architectureDiagram-3BPJPVTR-Cp4KTxE2.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Ds7GLNnC.js → blockDiagram-GPEHLZMM-yFxaTv04.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-D4WyGO6I.js → c4Diagram-AAUBKEIU-59lhWjRt.js} +1 -1
- package/packages/web/dist/assets/channel-CZs6Z1i5.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-tQunO_M5.js → chunk-2J33WTMH-BrpXGnJg.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DT5dOWd2.js → chunk-4BX2VUAB-UxBDb5pi.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CfTUqUEF.js → chunk-55IACEB6-D4ZJyUHW.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-sK5bOAA0.js → chunk-727SXJPM-DWdPj_8E.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-DcMgVe-P.js → chunk-AQP2D5EJ-BIpNbflY.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-In3h_-7v.js → chunk-FMBD7UC4-f6iJDfdI.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-DuaIeRaC.js → chunk-ND2GUHAM-Bz3TKIJk.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-DeT5mR5T.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-DgOgLvnM.js → core-TyFI6p0n.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CyO-rznJ.js → cose-bilkent-S5V4N54A-CIkdi0XF.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-B_9Ihzgn.js → dagre-BM42HDAG-BnLJCfNs.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-z5mvZ5YT.js → diagram-2AECGRRQ-D0OqfDB9.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Bsz0cgfn.js → diagram-5GNKFQAL-DQ2spGrK.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-B29k5f4q.js → diagram-KO2AKTUF-RDIVQ_S3.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CFrwsHoI.js → diagram-LMA3HP47-DKoTfh2C.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-D9Sjc2HR.js → diagram-OG6HWLK6-6k0OuSAL.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C3vbkD7M.js → erDiagram-TEJ5UH35-B-oQaYAC.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DkMIgntd.js → flowDiagram-I6XJVG4X-B7qpYe9C.js} +1 -1
- package/packages/web/dist/assets/{format-BFDqyFuw.js → format-PHdEBNOi.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-nvHanjpW.js → ganttDiagram-6RSMTGT7-SfBsqMmx.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DbCNfOu7.js → gitGraphDiagram-PVQCEYII-CuM4Rl_i.js} +1 -1
- package/packages/web/dist/assets/{index-DGCs1YIO.js → index-D_1ogxg1.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-aJjH9x3D.js → infoDiagram-5YYISTIA-Dq-32Oj4.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DoifwNnZ.js → ishikawaDiagram-YF4QCWOH-BLFHCj_o.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cz9kG9HN.js → journeyDiagram-JHISSGLW-CHP-Yu63.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BoXnfr7A.js → kanban-definition-UN3LZRKU-DRuehSqn.js} +1 -1
- package/packages/web/dist/assets/{linear-BlTTO9yR.js → linear-Bwz2vnH7.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-YAG8eSPQ.js → mermaid.core-CeMMnCip.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DtUBKfkr.js → mindmap-definition-RKZ34NQL-CEdqeoRW.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-B3Rwpmy0.js → pieDiagram-4H26LBE5-BdDXBx87.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B8eJtTyk.js → quadrantDiagram-W4KKPZXB-Bovd1Zhx.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CpAluiY9.js → requirementDiagram-4Y6WPE33-DiyrKnJ2.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js → sankeyDiagram-5OEKKPKP-BuX7TRVw.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CRpqKSGm.js → sequenceDiagram-3UESZ5HK-BjetZWJo.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-h3h8gXv_.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-PVr5V_pG.js → timeline-definition-PNZ67QCA-BpQ8g8Bm.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-De-4NPAX.js → vennDiagram-CIIHVFJN-Cj99BWER.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-Dtv10D2d.js → wardley-L42UT6IY-S7AGTOWw.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-p2fJuQ6C.js → wardleyDiagram-YWT4CUSO-LNXkUA7S.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DyYF-k92.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/server/dist/cli/claude-home-choice.d.ts +0 -44
- package/packages/server/dist/cli/claude-home-choice.js +0 -108
- package/packages/web/dist/assets/channel-B1K_gwAH.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BKRI9TnH.js +0 -2
package/README.md
CHANGED
|
@@ -26,11 +26,34 @@ claude setup-token # Claude Max/Pro
|
|
|
26
26
|
export ANTHROPIC_API_KEY=sk-ant-… # or API billing
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
If you already use Claude Code on this machine, there is nothing to do
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
**If you already use Claude Code on this machine, there is nothing to do.** Paddock
|
|
30
|
+
keeps a Claude home of its own under the data dir, but not a login of its own: it uses
|
|
31
|
+
the one you already have — the macOS Keychain entry on a Mac, your
|
|
32
|
+
`~/.claude/.credentials.json` elsewhere (symlinked in, never copied). Reading a login
|
|
33
|
+
writes nothing, and nothing else is shared with it.
|
|
34
|
+
|
|
35
|
+
To give Paddock its own instead, set `claude: { credentials: own }` in
|
|
36
|
+
`<data-dir>/paddock.config.yaml`; a login is then a token in the environment as above,
|
|
37
|
+
or a one-off `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`. Either way,
|
|
38
|
+
Paddock says at startup if it can find no credentials at all.
|
|
39
|
+
|
|
40
|
+
## What else Paddock does and does not take from your `~/.claude`
|
|
41
|
+
|
|
42
|
+
Apart from that login: **nothing, by default.** Your `CLAUDE.md`, `agents/`,
|
|
43
|
+
`commands/` and `plugins/` are not loaded, and the hooks your `settings.json` binds to
|
|
44
|
+
tool use do not run. Both are one key in `<data-dir>/paddock.config.yaml`:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
claude:
|
|
48
|
+
instructions: host # own | host, default own — CLAUDE.md, agents, commands, plugins
|
|
49
|
+
hooks: host # own | host, default own — shell commands settings.json binds
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`instructions: own` is worth knowing about if you have curated a `~/.claude/CLAUDE.md`:
|
|
53
|
+
your Paddock agents will not see it until you set `host`. Each project's own `CLAUDE.md`
|
|
54
|
+
always applies. `hooks` is off by default because inheriting someone's shell commands is
|
|
55
|
+
not a thing to discover after the fact; the rest of your `settings.json` — permissions,
|
|
56
|
+
model, statusline — applies either way.
|
|
34
57
|
|
|
35
58
|
## Open your own project
|
|
36
59
|
|
|
@@ -49,13 +72,13 @@ The flag is the consent, so here is everything it does:
|
|
|
49
72
|
- creates **`.paddock/`** for the workspace's state and **`.chats/`** for transcripts
|
|
50
73
|
- appends those two entries to **`.gitignore`**
|
|
51
74
|
|
|
52
|
-
**Nothing is written into your `~/.claude
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
**Nothing is written into your `~/.claude`** — no file, no symlink. Paddock uses a
|
|
76
|
+
Claude home of its own under `.paddock/` and relocates transcripts into `.chats/`.
|
|
77
|
+
Sessions found in `~/.claude` are *offered* for import: nothing is moved, copied or
|
|
78
|
+
linked until you confirm, and your terminal `claude` keeps working exactly as before.
|
|
79
|
+
To share one set of transcripts between Paddock and your terminal instead, set
|
|
80
|
+
`claude: { transcripts: host }` in `<data-dir>/paddock.config.yaml`. (Your *login* is
|
|
81
|
+
already shared, which is why there is nothing to log into — see Credentials above.)
|
|
59
82
|
|
|
60
83
|
To undo it: `rm -rf .paddock .chats` and drop the two `.gitignore` lines.
|
|
61
84
|
|
|
@@ -70,8 +93,6 @@ Without `--here`, Paddock never touches the directory you ran it from.
|
|
|
70
93
|
--here Open the CURRENT directory as the workspace
|
|
71
94
|
-o, --open Open the app in your browser once it is listening
|
|
72
95
|
--verbose Show the server's own logs (quiet by default)
|
|
73
|
-
--isolated-claude-home
|
|
74
|
-
Use Paddock's own Claude home instead of ~/.claude
|
|
75
96
|
-v, --version Print the version
|
|
76
97
|
-h, --help Show help
|
|
77
98
|
```
|
package/package.json
CHANGED
|
@@ -20,6 +20,8 @@ import { promises as fs } from "node:fs";
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { loadPaddockConfig } from "./config.js";
|
|
22
22
|
import { ensureClaudeHome, countLegacyTranscriptLinks, findPlantedChatsLinks, } from "./claude-home.js";
|
|
23
|
+
import { loadHostMcpSource } from "./claude-mcp.js";
|
|
24
|
+
import { declaredMcpNotices } from "./mcp-servers.js";
|
|
23
25
|
import { installHerdctlLogBridge } from "./agent-errors.js";
|
|
24
26
|
import { ProjectStore, ROOT_KEY } from "./projects.js";
|
|
25
27
|
import { AttachmentStore } from "./attachments.js";
|
|
@@ -108,7 +110,32 @@ export async function buildApp(opts = {}) {
|
|
|
108
110
|
// --- project layer + herdctl ------------------------------------------
|
|
109
111
|
const projects = new ProjectStore(cfg.projectsRoot);
|
|
110
112
|
await projects.init();
|
|
111
|
-
|
|
113
|
+
// The user's own MCP servers, under `claude.mcpServers: host` (#691 step 5).
|
|
114
|
+
// Read BEFORE the service is constructed because every keeper's agent config
|
|
115
|
+
// carries them, and read exactly once — a `.claude.json` that grows a server
|
|
116
|
+
// mid-run is picked up at the next restart, which is what the notice says.
|
|
117
|
+
// Under `own` (the default) the file is not opened at all.
|
|
118
|
+
const hostMcp = await loadHostMcpSource(cfg);
|
|
119
|
+
for (const notice of hostMcp.notices)
|
|
120
|
+
app.log[notice.level](notice.message);
|
|
121
|
+
// …and the servers this instance declares for ITSELF, from the top-level
|
|
122
|
+
// `mcpServers:` block (#691 step 6). Resolved during config load (it needs
|
|
123
|
+
// `process.env` for the `env:VAR` references), logged here because that is
|
|
124
|
+
// where the logger is. Nothing below carries a value out of the block: the
|
|
125
|
+
// diagnostics name keys and variable names only, and every server mentioned in
|
|
126
|
+
// a notice is rendered by `describeServer`.
|
|
127
|
+
for (const message of cfg.mcpServersDiagnostics.errors)
|
|
128
|
+
app.log.error(message);
|
|
129
|
+
for (const message of cfg.mcpServersDiagnostics.warnings)
|
|
130
|
+
app.log.warn(message);
|
|
131
|
+
for (const notice of declaredMcpNotices({
|
|
132
|
+
servers: cfg.mcpServers,
|
|
133
|
+
hostNames: Object.keys(hostMcp.source.user),
|
|
134
|
+
browserMcp: cfg.browserMcp,
|
|
135
|
+
})) {
|
|
136
|
+
app.log[notice.level](notice.message);
|
|
137
|
+
}
|
|
138
|
+
const herdctl = new HerdctlService(cfg, { ...hostMcp.source, declared: cfg.mcpServers });
|
|
112
139
|
const git = new GitService(cfg.projectsRoot, cfg.gitAuthor);
|
|
113
140
|
const githubAuth = new GithubAuth(path.join(cfg.dataDir, "github-auth.json"), cfg.githubClientId);
|
|
114
141
|
const archive = new ArchiveStore(cfg.dataDir);
|
|
@@ -163,13 +190,13 @@ export async function buildApp(opts = {}) {
|
|
|
163
190
|
for (const notice of (await ensureClaudeHome(cfg)).notices) {
|
|
164
191
|
app.log[notice.level](notice.message);
|
|
165
192
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
// Unconditional since #691 — paddock always owns its home, so there is no
|
|
194
|
+
// longer a layout in which these links are the live ones.
|
|
195
|
+
const stale = await countLegacyTranscriptLinks(cfg.legacyClaudeHome, cfg.dataDir);
|
|
196
|
+
if (stale > 0) {
|
|
197
|
+
app.log.info(`${stale} transcript symlink(s) from a previous layout still point into ${cfg.dataDir} ` +
|
|
198
|
+
`from ${path.join(cfg.legacyClaudeHome, "projects")}. Nothing reads them any more; ` +
|
|
199
|
+
`paddock leaves them alone because it does not write to ~/.claude. Safe to delete.`);
|
|
173
200
|
}
|
|
174
201
|
// #682: links an affected build planted at a path the user had not used yet.
|
|
175
202
|
// Unlike the residue above these are still LIVE — they redirect the user's own
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `claude.credentials` lever (#691): whose Claude Code LOGIN this instance
|
|
3
|
+
* uses.
|
|
4
|
+
*
|
|
5
|
+
* Paddock always owns its Claude home (`<dataDir>/claude-home`, #691), and that
|
|
6
|
+
* is what made a macOS login invisible (#683). Claude Code files its secure
|
|
7
|
+
* storage entry under a service name derived from the config dir, so the moment
|
|
8
|
+
* paddock points `CLAUDE_CONFIG_DIR` at its own home, `claude /login`'s Keychain
|
|
9
|
+
* entry is under a name paddock's agents never look up. On a Mac with no token in
|
|
10
|
+
* the environment that is an instance which boots cleanly and fails every single
|
|
11
|
+
* turn with "Not logged in".
|
|
12
|
+
*
|
|
13
|
+
* ## The mechanism
|
|
14
|
+
*
|
|
15
|
+
* Read out of the SDK bundle (`@anthropic-ai/claude-agent-sdk/sdk.mjs`), which
|
|
16
|
+
* derives the service name like this:
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* var _z = "-credentials";
|
|
20
|
+
* function yz(e = "") {
|
|
21
|
+
* let t = process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR,
|
|
22
|
+
* r = t !== undefined ? !t : !process.env.CLAUDE_CONFIG_DIR,
|
|
23
|
+
* n = t !== undefined ? t.normalize("NFC") : Yt(),
|
|
24
|
+
* o = r ? "" : `-${sha256(n).digest("hex").substring(0, 8)}`;
|
|
25
|
+
* return `Claude Code${OAUTH_FILE_SUFFIX}${e}${o}`;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* `OAUTH_FILE_SUFFIX` is `""` in production and the only caller passes `_z`, so a
|
|
30
|
+
* plain `claude /login` files under exactly **`Claude Code-credentials`** — which
|
|
31
|
+
* is the name `probeMacosKeychainLogin` looks for, now confirmed rather than
|
|
32
|
+
* guessed.
|
|
33
|
+
*
|
|
34
|
+
* The load-bearing line is `r`. **`CLAUDE_SECURESTORAGE_CONFIG_DIR`, when
|
|
35
|
+
* DEFINED, overrides `CLAUDE_CONFIG_DIR` for secure storage only.** Define it as
|
|
36
|
+
* the EMPTY STRING and `!t` is true, so the hash suffix is dropped and the
|
|
37
|
+
* plain-name entry — the user's real login — is the one read, while
|
|
38
|
+
* `CLAUDE_CONFIG_DIR` still points at paddock's own home. That decoupling is the
|
|
39
|
+
* whole lever: one variable, no home moved, nothing else shared.
|
|
40
|
+
*
|
|
41
|
+
* The empty string is a value, not a mistake. Anywhere it looks like a bug to
|
|
42
|
+
* "clean up", it is the signal.
|
|
43
|
+
*
|
|
44
|
+
* ## Why this defaults to `host`, against the pattern
|
|
45
|
+
*
|
|
46
|
+
* Every other key in the `claude:` block defaults to `own`, and this one does
|
|
47
|
+
* not. **Isolation is about writes.** Reading a Keychain entry (or a symlinked
|
|
48
|
+
* `~/.claude/.credentials.json`) puts no file of the user's at risk: nothing is
|
|
49
|
+
* created, moved or deleted, and the user's own `claude` is unaffected. What
|
|
50
|
+
* defaulting to `own` WOULD do is recreate #683 — a first run that boots fine,
|
|
51
|
+
* says everything is ready, and fails every turn — which is the worst first
|
|
52
|
+
* impression paddock can make and the one it just made.
|
|
53
|
+
*
|
|
54
|
+
* So the guarantee `own` everywhere buys ("nothing outside the data dir is
|
|
55
|
+
* written") survives this default intact, and the deliberate exception is stated
|
|
56
|
+
* here rather than left to be inferred from a table.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Whose Claude Code login this instance uses — the `claude.credentials` key.
|
|
60
|
+
*
|
|
61
|
+
* `own` = only what is inside paddock's own Claude home: a token in the
|
|
62
|
+
* environment, or a `.credentials.json` an operator put there / logged in with.
|
|
63
|
+
* `host` = this machine's Claude Code login as well — the macOS Keychain entry on
|
|
64
|
+
* darwin, and the user's `~/.claude/.credentials.json` (bridged by symlink) on
|
|
65
|
+
* everything else.
|
|
66
|
+
*/
|
|
67
|
+
export type CredentialsMode = "own" | "host";
|
|
68
|
+
/** Sharing a login writes nothing. See the module doc for why this is not `own`. */
|
|
69
|
+
export declare const DEFAULT_CREDENTIALS_MODE: CredentialsMode;
|
|
70
|
+
/** Type guard, so an unknown config value falls back instead of failing a boot. */
|
|
71
|
+
export declare function isKnownCredentialsMode(value: string): value is CredentialsMode;
|
|
72
|
+
/**
|
|
73
|
+
* The variable Claude Code resolves its SECURE-STORAGE scope from, independently
|
|
74
|
+
* of `CLAUDE_CONFIG_DIR`. Not exported by the SDK; read from its bundle.
|
|
75
|
+
*/
|
|
76
|
+
export declare const SECURE_STORAGE_DIR_VAR = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
77
|
+
/**
|
|
78
|
+
* The value that means "use the unsuffixed service name" — i.e. the entry a
|
|
79
|
+
* plain `claude /login` wrote. Empty on purpose; see the module doc.
|
|
80
|
+
*/
|
|
81
|
+
export declare const HOST_SECURE_STORAGE_DIR = "";
|
|
82
|
+
/** What {@link applyCredentialsMode} did, for the boot notice. */
|
|
83
|
+
export type CredentialsDecision =
|
|
84
|
+
/** `host`: the variable was set to `""`, so the plain-name login is used. */
|
|
85
|
+
{
|
|
86
|
+
action: "shared";
|
|
87
|
+
}
|
|
88
|
+
/** `own`: the variable is unset, so the login is scoped to paddock's home. */
|
|
89
|
+
| {
|
|
90
|
+
action: "isolated";
|
|
91
|
+
}
|
|
92
|
+
/** An operator set the variable to something of their own; left alone. */
|
|
93
|
+
| {
|
|
94
|
+
action: "deferred";
|
|
95
|
+
value: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Put this instance's credentials mode into the environment Claude Code runs in.
|
|
99
|
+
*
|
|
100
|
+
* **Mutates `env`, and by default that is `process.env` — deliberately.** There
|
|
101
|
+
* is no per-invocation seam to use instead: herdctl builds the SDK's `options.env`
|
|
102
|
+
* itself (`withClaudeConfigDir`, which spreads `process.env`) and the CLI runtime
|
|
103
|
+
* lets execa merge over the inherited environment, and neither takes an env
|
|
104
|
+
* contribution from paddock. Unlike the Claude home — which herdctl warns must be
|
|
105
|
+
* per-invocation because one process hosts many agents with potentially different
|
|
106
|
+
* homes — the credentials mode is a property of the INSTANCE: every agent in it
|
|
107
|
+
* has the same answer, so a process-wide value cannot leak one agent's setting
|
|
108
|
+
* into another's. Called once, at boot, before the fleet starts.
|
|
109
|
+
*
|
|
110
|
+
* An operator who set the variable to a non-empty value themselves is honoured
|
|
111
|
+
* rather than clobbered, the same courtesy `CLAUDE_CONFIG_DIR` gets (herdctl#423):
|
|
112
|
+
* they have named a third secure-storage scope that neither mode can express. An
|
|
113
|
+
* EMPTY value is not treated that way — it is exactly what `host` writes, so it is
|
|
114
|
+
* ours by construction and `own` clears it.
|
|
115
|
+
*/
|
|
116
|
+
export declare function applyCredentialsMode(mode: CredentialsMode, env?: NodeJS.ProcessEnv): CredentialsDecision;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `claude.credentials` lever (#691): whose Claude Code LOGIN this instance
|
|
3
|
+
* uses.
|
|
4
|
+
*
|
|
5
|
+
* Paddock always owns its Claude home (`<dataDir>/claude-home`, #691), and that
|
|
6
|
+
* is what made a macOS login invisible (#683). Claude Code files its secure
|
|
7
|
+
* storage entry under a service name derived from the config dir, so the moment
|
|
8
|
+
* paddock points `CLAUDE_CONFIG_DIR` at its own home, `claude /login`'s Keychain
|
|
9
|
+
* entry is under a name paddock's agents never look up. On a Mac with no token in
|
|
10
|
+
* the environment that is an instance which boots cleanly and fails every single
|
|
11
|
+
* turn with "Not logged in".
|
|
12
|
+
*
|
|
13
|
+
* ## The mechanism
|
|
14
|
+
*
|
|
15
|
+
* Read out of the SDK bundle (`@anthropic-ai/claude-agent-sdk/sdk.mjs`), which
|
|
16
|
+
* derives the service name like this:
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* var _z = "-credentials";
|
|
20
|
+
* function yz(e = "") {
|
|
21
|
+
* let t = process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR,
|
|
22
|
+
* r = t !== undefined ? !t : !process.env.CLAUDE_CONFIG_DIR,
|
|
23
|
+
* n = t !== undefined ? t.normalize("NFC") : Yt(),
|
|
24
|
+
* o = r ? "" : `-${sha256(n).digest("hex").substring(0, 8)}`;
|
|
25
|
+
* return `Claude Code${OAUTH_FILE_SUFFIX}${e}${o}`;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* `OAUTH_FILE_SUFFIX` is `""` in production and the only caller passes `_z`, so a
|
|
30
|
+
* plain `claude /login` files under exactly **`Claude Code-credentials`** — which
|
|
31
|
+
* is the name `probeMacosKeychainLogin` looks for, now confirmed rather than
|
|
32
|
+
* guessed.
|
|
33
|
+
*
|
|
34
|
+
* The load-bearing line is `r`. **`CLAUDE_SECURESTORAGE_CONFIG_DIR`, when
|
|
35
|
+
* DEFINED, overrides `CLAUDE_CONFIG_DIR` for secure storage only.** Define it as
|
|
36
|
+
* the EMPTY STRING and `!t` is true, so the hash suffix is dropped and the
|
|
37
|
+
* plain-name entry — the user's real login — is the one read, while
|
|
38
|
+
* `CLAUDE_CONFIG_DIR` still points at paddock's own home. That decoupling is the
|
|
39
|
+
* whole lever: one variable, no home moved, nothing else shared.
|
|
40
|
+
*
|
|
41
|
+
* The empty string is a value, not a mistake. Anywhere it looks like a bug to
|
|
42
|
+
* "clean up", it is the signal.
|
|
43
|
+
*
|
|
44
|
+
* ## Why this defaults to `host`, against the pattern
|
|
45
|
+
*
|
|
46
|
+
* Every other key in the `claude:` block defaults to `own`, and this one does
|
|
47
|
+
* not. **Isolation is about writes.** Reading a Keychain entry (or a symlinked
|
|
48
|
+
* `~/.claude/.credentials.json`) puts no file of the user's at risk: nothing is
|
|
49
|
+
* created, moved or deleted, and the user's own `claude` is unaffected. What
|
|
50
|
+
* defaulting to `own` WOULD do is recreate #683 — a first run that boots fine,
|
|
51
|
+
* says everything is ready, and fails every turn — which is the worst first
|
|
52
|
+
* impression paddock can make and the one it just made.
|
|
53
|
+
*
|
|
54
|
+
* So the guarantee `own` everywhere buys ("nothing outside the data dir is
|
|
55
|
+
* written") survives this default intact, and the deliberate exception is stated
|
|
56
|
+
* here rather than left to be inferred from a table.
|
|
57
|
+
*/
|
|
58
|
+
/** Sharing a login writes nothing. See the module doc for why this is not `own`. */
|
|
59
|
+
export const DEFAULT_CREDENTIALS_MODE = "host";
|
|
60
|
+
/** Type guard, so an unknown config value falls back instead of failing a boot. */
|
|
61
|
+
export function isKnownCredentialsMode(value) {
|
|
62
|
+
return value === "own" || value === "host";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The variable Claude Code resolves its SECURE-STORAGE scope from, independently
|
|
66
|
+
* of `CLAUDE_CONFIG_DIR`. Not exported by the SDK; read from its bundle.
|
|
67
|
+
*/
|
|
68
|
+
export const SECURE_STORAGE_DIR_VAR = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
|
|
69
|
+
/**
|
|
70
|
+
* The value that means "use the unsuffixed service name" — i.e. the entry a
|
|
71
|
+
* plain `claude /login` wrote. Empty on purpose; see the module doc.
|
|
72
|
+
*/
|
|
73
|
+
export const HOST_SECURE_STORAGE_DIR = "";
|
|
74
|
+
/**
|
|
75
|
+
* Put this instance's credentials mode into the environment Claude Code runs in.
|
|
76
|
+
*
|
|
77
|
+
* **Mutates `env`, and by default that is `process.env` — deliberately.** There
|
|
78
|
+
* is no per-invocation seam to use instead: herdctl builds the SDK's `options.env`
|
|
79
|
+
* itself (`withClaudeConfigDir`, which spreads `process.env`) and the CLI runtime
|
|
80
|
+
* lets execa merge over the inherited environment, and neither takes an env
|
|
81
|
+
* contribution from paddock. Unlike the Claude home — which herdctl warns must be
|
|
82
|
+
* per-invocation because one process hosts many agents with potentially different
|
|
83
|
+
* homes — the credentials mode is a property of the INSTANCE: every agent in it
|
|
84
|
+
* has the same answer, so a process-wide value cannot leak one agent's setting
|
|
85
|
+
* into another's. Called once, at boot, before the fleet starts.
|
|
86
|
+
*
|
|
87
|
+
* An operator who set the variable to a non-empty value themselves is honoured
|
|
88
|
+
* rather than clobbered, the same courtesy `CLAUDE_CONFIG_DIR` gets (herdctl#423):
|
|
89
|
+
* they have named a third secure-storage scope that neither mode can express. An
|
|
90
|
+
* EMPTY value is not treated that way — it is exactly what `host` writes, so it is
|
|
91
|
+
* ours by construction and `own` clears it.
|
|
92
|
+
*/
|
|
93
|
+
export function applyCredentialsMode(mode, env = process.env) {
|
|
94
|
+
const operator = env[SECURE_STORAGE_DIR_VAR];
|
|
95
|
+
if (operator !== undefined && operator !== "")
|
|
96
|
+
return { action: "deferred", value: operator };
|
|
97
|
+
if (mode === "host") {
|
|
98
|
+
env[SECURE_STORAGE_DIR_VAR] = HOST_SECURE_STORAGE_DIR;
|
|
99
|
+
return { action: "shared" };
|
|
100
|
+
}
|
|
101
|
+
delete env[SECURE_STORAGE_DIR_VAR];
|
|
102
|
+
return { action: "isolated" };
|
|
103
|
+
}
|
|
@@ -1,26 +1,54 @@
|
|
|
1
1
|
import type { PaddockConfig } from "./config.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
* The user's file-based Claude Code login, bridged in only under
|
|
4
|
+
* `claude.credentials: host` (#691).
|
|
5
|
+
*
|
|
6
|
+
* This is what the lever means on Linux and in the Docker image, where `claude
|
|
7
|
+
* login` writes a file rather than using a keychain: on darwin `credentials: host`
|
|
8
|
+
* is the `CLAUDE_SECURESTORAGE_CONFIG_DIR` empty-string trick, here it is this
|
|
9
|
+
* symlink, and one config key has to mean the same thing on both.
|
|
10
|
+
*
|
|
11
|
+
* Symlinked rather than copied — a copy would duplicate a live secret onto disk
|
|
12
|
+
* AND diverge the moment Claude Code refreshed the token.
|
|
13
|
+
*
|
|
14
|
+
* It was bridged unconditionally before the lever existed, which is why
|
|
15
|
+
* `credentials` defaulting to `host` keeps every existing install working
|
|
16
|
+
* unchanged; `own` is the new behaviour, and it has to also REMOVE a link a
|
|
17
|
+
* previous boot planted, or "isolated" would be a claim that only holds for
|
|
18
|
+
* instances that were never anything else.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CREDENTIAL_ENTRY = ".credentials.json";
|
|
21
|
+
/**
|
|
22
|
+
* Entries of the user's Claude home that paddock can bridge into its own — the
|
|
23
|
+
* complete list, for callers that want to reason about the surface rather than
|
|
24
|
+
* about one lever.
|
|
25
|
+
*
|
|
26
|
+
* There is no longer an "always bridged" set: since #691 step 4, every entry here
|
|
27
|
+
* belongs to exactly one key of the `claude:` block, and the mapping is the whole
|
|
28
|
+
* point of the block.
|
|
29
|
+
*
|
|
30
|
+
* | Entry | Lever |
|
|
31
|
+
* |---|---|
|
|
32
|
+
* | `CLAUDE.md`, `agents/`, `commands/`, `plugins/` | `claude.instructions` |
|
|
33
|
+
* | `settings.json` (its `hooks` key) | `claude.hooks` |
|
|
34
|
+
* | `.credentials.json` | `claude.credentials` |
|
|
35
|
+
*
|
|
36
|
+
* `settings.json` appears under `hooks` because that is the half of it a lever
|
|
37
|
+
* governs; its OTHER keys — permissions, model, statusline, `enabledPlugins` —
|
|
38
|
+
* are inherited in every mode, which is why `hooks: own` writes a filtered file
|
|
39
|
+
* rather than declining to bridge one. See `claude-settings.ts`.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately NOT bridged in any mode: `projects/` (that's the whole point), and
|
|
42
|
+
* everything that is per-instance runtime state — `todos/`, `shell-snapshots/`,
|
|
43
|
+
* `statsig/`, `sessions/`. Those are exactly what should be per-instance.
|
|
44
|
+
*
|
|
45
|
+
* The fifth lever, `claude.mcpServers`, has NO row here on purpose: MCP servers
|
|
46
|
+
* are declared in `~/.claude.json`, which is a sibling of the home rather than an
|
|
47
|
+
* entry inside it, so it is not bridgeable at all — paddock reads it and passes
|
|
48
|
+
* the servers programmatically (`claude-mcp.ts`). That asymmetry is the reason
|
|
49
|
+
* MCP inheritance broke separately from everything in this table.
|
|
22
50
|
*/
|
|
23
|
-
export declare const
|
|
51
|
+
export declare const BRIDGEABLE_ENTRIES: readonly string[];
|
|
24
52
|
/**
|
|
25
53
|
* What a Keychain probe concluded.
|
|
26
54
|
*
|
|
@@ -42,15 +70,24 @@ export type KeychainProbe = () => Promise<KeychainFinding>;
|
|
|
42
70
|
* ## The asymmetry, which is deliberate
|
|
43
71
|
*
|
|
44
72
|
* A positive result is trustworthy: an entry under that service name is a login.
|
|
45
|
-
* A negative result is NOT —
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
73
|
+
* A negative result is NOT — `security` can fail for a dozen reasons that have
|
|
74
|
+
* nothing to do with the user, and the service name, though now read straight out
|
|
75
|
+
* of the SDK bundle rather than guessed, is still Claude Code's internal detail
|
|
76
|
+
* and can change. So a miss returns `unknown`, and the caller falls back to the
|
|
77
|
+
* existing, correct, generic warning. Being wrong about the name therefore costs
|
|
78
|
+
* nothing beyond the improvement not happening.
|
|
50
79
|
*
|
|
51
80
|
* Never prompts and never reads a secret: `find-generic-password` WITHOUT `-w`
|
|
52
81
|
* reports existence only (attributes, which are discarded), so nothing sensitive
|
|
53
82
|
* reaches a log, a transcript, or this process's memory.
|
|
83
|
+
*
|
|
84
|
+
* One known imprecision, left in on purpose: Claude Code's own lookup also passes
|
|
85
|
+
* `-a` (`process.env.USER`, falling back to the OS username), and this does not.
|
|
86
|
+
* So an entry filed under a DIFFERENT account still reads as `found` here while
|
|
87
|
+
* Claude Code would miss it — a service running under a different `USER` than the
|
|
88
|
+
* terminal that logged in. Matching that exactly would trade a rare false
|
|
89
|
+
* positive for a rarer false negative, and a false negative is the one that costs
|
|
90
|
+
* something: it downgrades a specific instruction back to the generic warning.
|
|
54
91
|
*/
|
|
55
92
|
export declare function probeMacosKeychainLogin(platform?: string): Promise<KeychainFinding>;
|
|
56
93
|
/** A message for the operator, at the level it should be logged at. */
|
|
@@ -61,21 +98,48 @@ export interface ClaudeHomeNotice {
|
|
|
61
98
|
export interface ClaudeHomeReport {
|
|
62
99
|
/** Entries symlinked in from the user's home this boot. */
|
|
63
100
|
bridged: string[];
|
|
101
|
+
/**
|
|
102
|
+
* Entries whose bridge symlink was REMOVED this boot because the lever that
|
|
103
|
+
* governs them now says `own` (#691).
|
|
104
|
+
*
|
|
105
|
+
* The bridge is non-clobbering, so without this an instance that was ever
|
|
106
|
+
* `host` would keep reading the user's file forever after being reconfigured —
|
|
107
|
+
* "isolated" would be a claim that only holds for instances that were never
|
|
108
|
+
* anything else.
|
|
109
|
+
*/
|
|
110
|
+
withdrawn: string[];
|
|
111
|
+
/**
|
|
112
|
+
* Entries paddock WROTE into its own home this boot rather than symlinking,
|
|
113
|
+
* because a lever needs part of a file and not the whole of it. Only
|
|
114
|
+
* `settings.json` today — see `claude-settings.ts`.
|
|
115
|
+
*/
|
|
116
|
+
generated: string[];
|
|
64
117
|
/** Messages for `app.ts` to log — config resolution has no logger of its own. */
|
|
65
118
|
notices: ClaudeHomeNotice[];
|
|
66
119
|
}
|
|
67
120
|
/**
|
|
68
|
-
* Create paddock's Claude home and
|
|
121
|
+
* Create paddock's Claude home and put the user-level config into it that this
|
|
122
|
+
* instance's `claude:` levers say it should have (#691).
|
|
69
123
|
*
|
|
70
|
-
* Idempotent and non-clobbering:
|
|
71
|
-
*
|
|
72
|
-
*
|
|
124
|
+
* Idempotent and non-clobbering: a real file the user put in paddock's home is
|
|
125
|
+
* left completely alone, so this is safe to run on every start. What it DOES
|
|
126
|
+
* maintain is what paddock itself planted — bridge symlinks, withdrawn when a
|
|
127
|
+
* lever flips to `own`, and the generated `settings.json`, regenerated from the
|
|
128
|
+
* user's current one. Ownership of a generated file is decided by hash
|
|
129
|
+
* ({@link classifyOwnEntry}), never by "it is at a name we use".
|
|
73
130
|
*
|
|
74
131
|
* Never throws. A home that cannot be created is not a reason to refuse to boot
|
|
75
132
|
* — Claude Code falls back to its own default and chat still works, which is the
|
|
76
133
|
* same failure mode `ensureProjectChats` has always had.
|
|
134
|
+
*
|
|
135
|
+
* **Mutates `env`** (`process.env` by default) to carry `claude.credentials` into
|
|
136
|
+
* the environment Claude Code runs in — see {@link applyCredentialsMode} for why
|
|
137
|
+
* that is the only seam available and why it is safe process-wide. Done here
|
|
138
|
+
* because this is already the one call that runs before the fleet starts, and
|
|
139
|
+
* because the credential warning below has to describe the environment it just
|
|
140
|
+
* set rather than the one it found.
|
|
77
141
|
*/
|
|
78
|
-
export declare function ensureClaudeHome(cfg: Pick<PaddockConfig, "claudeHome" | "legacyClaudeHome" | "
|
|
142
|
+
export declare function ensureClaudeHome(cfg: Pick<PaddockConfig, "claudeHome" | "legacyClaudeHome" | "claude">, env?: NodeJS.ProcessEnv, probeKeychain?: KeychainProbe): Promise<ClaudeHomeReport>;
|
|
79
143
|
/** One of the user's transcript folders, made readable through paddock's home. */
|
|
80
144
|
export interface LegacyMirror {
|
|
81
145
|
/** Folder name inside `<claudeHome>/projects/` — `encodePathForCli(engineCwd)`. */
|
|
@@ -139,6 +203,14 @@ export declare function legacySourcePath(legacyHome: string, folderName: string)
|
|
|
139
203
|
* mirroring them would re-offer paddock's own chats as "adoptable". That also
|
|
140
204
|
* makes the leftovers from before this change inert rather than confusing.
|
|
141
205
|
*
|
|
206
|
+
* A folder paddock's own home ALREADY links to directly is skipped for the same
|
|
207
|
+
* reason. That is the `claude.transcripts: host` case (#691): there the folder in
|
|
208
|
+
* the user's home is a real directory that paddock itself writes through, so
|
|
209
|
+
* mirroring it would offer every project's own live chats back as importable —
|
|
210
|
+
* the #658 mistake with the arrow reversed. (Adoption de-duplicates sources by
|
|
211
|
+
* resolved real path, so this was noise rather than breakage; it is skipped here
|
|
212
|
+
* because a link paddock plants and then mirrors is confusing to find on disk.)
|
|
213
|
+
*
|
|
142
214
|
* Idempotent and never throws. Returns every mirror it maintains, not only the
|
|
143
215
|
* ones planted this call, so a caller can rely on the mapping after any call.
|
|
144
216
|
*/
|