diff --git a/fleetd/fleetd.example.yaml b/fleetd/fleetd.example.yaml index f33f622..79a0d0a 100644 --- a/fleetd/fleetd.example.yaml +++ b/fleetd/fleetd.example.yaml @@ -172,7 +172,11 @@ herdrSocket: ~/.config/herdr/herdr.sock # skills/MCP/hooks. Omit to leave the worker on the host default. # parityOverlay → repo-relative paths copied primary→worktree so a worker in a provisioned # worktree sees the same local config (CB-301-ext). Omit for the default set: -# [.env, .envrc]. (.claude/settings.local.json is NOT in the default — it +# [.env] only (CB-148). .envrc is left out of the default on purpose: it is +# executable shell that direnv runs on every cd, so copying it carries +# behaviour into the worker, not just values, unlike .env. An operator who +# wants it copied can still write parityOverlay: [.env, .envrc] explicitly. +# (.claude/settings.local.json is NOT in the default — it # pre-approves IDE/tool grants a member must not hold ambiently; CB-525/CB-634.) # # Do NOT add .mcp.json (CB-525). A worker's tools are whatever its launcher @@ -309,7 +313,7 @@ profiles: # errorPattern: "503 Service Unavailable" # opt-in: classify a backend outage (fleetd #201/#227) — see the key doc above # configDir: /Users/me/.ccs/instances/gx10 # CLAUDE_CONFIG_DIR — inherit that profile's skills/MCP # cwd: /Users/me/src/myrepo # pin the working dir; omit to inherit the primary's - # parityOverlay: [".env", ".envrc"] # the default; never add .mcp.json or .claude/settings.local.json — see above + # parityOverlay: [".env"] # the default; add ".envrc" explicitly if you want it copied too (CB-148) — never add .mcp.json or .claude/settings.local.json — see above # ideMcpUrl: http://127.0.0.1:29170/index-mcp/streamable-http # opt-in (CB-634): IDE code intelligence, pinned to the worktree # ideProjectDir: fleetd # CB-634: module dir the IDE opens + the overlay pins (this repo's pom is in fleetd/) # ideOpenCommand: env DISPLAY=:10.0 idea {dir} # CB-634 auto-open: opens {dir} in the IDE at spawn; omit to open by hand diff --git a/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java b/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java index 3fbe5ff..81300ac 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java +++ b/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java @@ -259,7 +259,13 @@ public record FleetConfig( * @param cwd fixed working directory for this profile's workers (CB-112 "told otherwise"); * {@code null}/blank → inherit the primary's cwd, else the daemon's * @param parityOverlay repo-relative paths copied primary→worktree for config parity; null/empty - * defaults to a sensible set of local config files. + * defaults to {@code [.env]} only (CB-148 point 2). {@code .env} is data — a + * copy of it can only carry values. {@code .envrc} is executable shell that + * {@code direnv} evaluates on every {@code cd}, so copying it moves behaviour + * into the worker, not just values, and that is a different risk from copying + * data. It is deliberately left out of the default for that reason. The knob + * still supports it: an operator who wants it copied writes + * {@code parityOverlay: [.env, .envrc]} explicitly and owns that choice. *

Every overlay path must be gitignored or tracked-and-skipped. * CB-576 made {@code release()} preserve a worktree that {@code git status * --porcelain} reports as dirty, and it deliberately counts untracked files — @@ -269,11 +275,10 @@ public record FleetConfig( * worktrees then accumulate with no error anywhere. *

Checked on 2026-08-15 (CB-581): inert as configured. Tracked overlay * files carry {@code --skip-worktree} so {@code --porcelain} cannot see them, - * {@code fleetd.yaml} is gitignored, and the default pair {@code .env} / - * {@code .envrc} does not exist in this repo. Note the default applies to - * every profile, so creating either file at the repo root is enough - * to make it live. Add a new overlay path to {@code .gitignore} in the same - * change that adds it here. + * {@code fleetd.yaml} is gitignored, and {@code .env} does not exist in this + * repo. Note the default applies to every profile, so creating + * {@code .env} at the repo root is enough to make it live. Add a new overlay + * path to {@code .gitignore} in the same change that adds it here. *

CB-578 stage C raised the stakes: a preserved dirty worktree is now also * committed to {@code refs/wip/} via {@code git add -A}. The overlay * carries the primary's own environment files into the worktree, so a @@ -421,7 +426,7 @@ public record FleetConfig( // servers do not exist there to be enabled. This is defence in depth, not a live fix: the // worker stays isolated only because this separate mechanism already removes the servers. parityOverlay = (parityOverlay == null || parityOverlay.isEmpty()) - ? List.of(".env", ".envrc") + ? List.of(".env") : List.copyOf(parityOverlay); // gitTokenEnv stays null when unset (opt-in). gitHostEnv defaults so operators enabling // checkpoints need only set gitTokenEnv; it is injected only alongside a resolved token. diff --git a/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java b/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java index 52de820..2ba37ec 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/config/FleetConfigTest.java @@ -1863,7 +1863,7 @@ class FleetConfigTest { } @Test - void parityOverlayDefaultsToEnvFilesOnly(@TempDir Path dir) throws Exception { + void parityOverlayDefaultsToDotEnvOnly(@TempDir Path dir) throws Exception { Path f = dir.resolve("no-overlay.yaml"); Files.writeString(f, """ bind: @@ -1874,9 +1874,11 @@ class FleetConfigTest { """); FleetConfig cfg = FleetConfig.load(f); - assertEquals(List.of(".env", ".envrc"), + assertEquals(List.of(".env"), cfg.profiles().get("gx10").parityOverlay(), - "the default parity overlay is the env files; settings.local.json is no longer copied by default"); + "the default parity overlay is .env only (CB-148): .envrc is executable shell that " + + "direnv runs on every cd, so it is no longer copied by default; settings.local.json " + + "is also not copied by default"); } @Test @@ -1897,6 +1899,25 @@ class FleetConfigTest { "an operator's explicit list survives verbatim — the default only changes when unset"); } + @Test + void parityOverlayExplicitEnvrcOptInStillWorks(@TempDir Path dir) throws Exception { + Path f = dir.resolve("explicit-envrc-overlay.yaml"); + Files.writeString(f, """ + bind: + port: 8080 + profiles: + gx10: + baseUrl: http://gx10.gw:8000 + parityOverlay: [".env", ".envrc"] + """); + + FleetConfig cfg = FleetConfig.load(f); + assertEquals(List.of(".env", ".envrc"), + cfg.profiles().get("gx10").parityOverlay(), + "an operator can still opt into copying .envrc explicitly (CB-148); it is only " + + "dropped from the unset default, not removed as a capability"); + } + // ── CB-542: subscription:true must not smuggle an unguarded endpoint via env: ─────────────── @Test diff --git a/fleetd/src/test/java/dev/ltms/fleet/session/WorktreeSessionManagerTest.java b/fleetd/src/test/java/dev/ltms/fleet/session/WorktreeSessionManagerTest.java index aeee299..4d5504d 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/session/WorktreeSessionManagerTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/session/WorktreeSessionManagerTest.java @@ -140,7 +140,7 @@ class WorktreeSessionManagerTest { void worktreeAcquireRunsParityOverlayWithProfileDefaults() { FakeHerdr herdr = new FakeHerdr(); FakeWorktrees worktrees = new FakeWorktrees().withRepoRoot("/repo").withPrefix("/wt") - .track(".envrc") + .track(".env") .exists(".claude/settings.local.json"); SessionManager sessions = new SessionManager(workerService(herdr), worktrees); @@ -151,14 +151,15 @@ class WorktreeSessionManagerTest { FakeWorktrees.OverlayCall overlay = worktrees.lastOverlay(); assertNotNull(overlay); assertEquals("/repo", overlay.repoRoot()); - assertEquals(List.of(".env", ".envrc"), - overlay.requested(), "default parity overlay is used when unset"); + assertEquals(List.of(".env"), + overlay.requested(), "default parity overlay is used when unset (CB-148: .env only, " + + ".envrc is no longer defaulted because it is executable shell direnv runs on cd)"); assertFalse(overlay.requested().contains(".mcp.json"), "CB-525: replicating the primary's MCP config gives a worker the primary's IDE " + "servers, which navigate its edits out of its own worktree"); - assertEquals(List.of(".envrc"), overlay.copied(), + assertEquals(List.of(".env"), overlay.copied(), "existing paths are copied; missing paths are skipped"); - assertEquals(List.of(".envrc"), overlay.skipWorktree(), + assertEquals(List.of(".env"), overlay.skipWorktree(), "tracked copied paths are --skip-worktree'd"); }