fleetd #134: make tool-surface neutralization visible to the daemon and worker #245

Closed
agent wants to merge 0 commits from worker/cb134-148-overlay-visible-c9b986-10 into main
Member

Scope

fleetd #134, the real one this time — GitWorktrees.isolateToolSurface (fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java:450), not overlayParity (that was #148 point 3, already merged in #242). This PR covers points 1 and 2 of the ticket; point 3 (documentation) is drafted below for the lead to apply, per the brief.

Point 1 — the daemon must say it

isolateToolSurface (and the per-file neutralize helper it calls) logged nothing before. Now it collects which of the three configured files (.mcp.json, opencode.json, .autoenv) were actually neutralized vs. absent-and-skipped, and logs one info summary per provisioning — same shape as overlayParity's fix in #148 point 3: name the denominator, name what was neutralized, name why anything was not.

Exact new log lines

  • All three present: tool-surface isolation: neutralized 3 of 3 configs: .mcp.json, opencode.json, .autoenv — the worktree copy is a stub, not the repo's file; edit the real file in the primary checkout instead
  • Only .mcp.json present (the common case — it's always created regardless of presence): tool-surface isolation: neutralized 1 of 3 configs: .mcp.json (opencode.json absent, .autoenv absent) — the worktree copy is a stub, not the repo's file; edit the real file in the primary checkout instead

Point 2 — the worker must be able to discover it from inside its own worktree

Mechanism chosen: worktree-scoped git config, not a working-tree file.

isolateToolSurface now also writes, once per provisioning (only when at least one file was neutralized — in practice always, since .mcp.json is unconditional):

git config --worktree --add fleet.neutralizedConfig <file>     # once per neutralized file
git config --worktree fleet.neutralizedConfigNote "the worktree copy of each fleet.neutralizedConfig path is a stub, not the repo's committed file; edit the real file from the primary checkout instead"

A worker discovers this from inside its own worktree with:

git config --worktree --get-all fleet.neutralizedConfig
git config --worktree --get fleet.neutralizedConfigNote

Why this and not a worktree file: worktree-scoped config lives in .git/worktrees/<nonce>/config.worktree, never in the working tree, so it can never appear in git status — satisfying the brief's constraint without needing a gitignore entry or any other working-tree trick. It also isn't new to this codebase: configureEnvironmentCredentialHelper and configureHttpsUrlRewriteForSshOrigin already use the exact same mechanism (and the same extensions.worktreeConfig enablement) for the credential helper and the SSH→HTTPS rewrite, both already running earlier in the same add() call. I re-enable extensions.worktreeConfig defensively inside isolateToolSurface itself too, so the method doesn't silently depend on call order in add() for correctness.

A gitignored path was the other option on the table; I didn't pick it because a gitignored file can still be read as a false "this IS the file" by a naive cat/Read, still needs a name collision check against whatever the repo itself might commit, and — unlike git config — has no natural multi-value/key-value shape for "which files, and why." git config --worktree gives both for free and reuses an established pattern.

git status --porcelain proof

New test aProvisionedWorktreeHasCleanGitStatusDespiteNeutralizedConfigRecordkeeping runs the full porcelain status (not a single-file check) on a freshly provisioned worktree with all three hostile configs present and asserts it is empty. I also mutation-tested this directly (see table below, mutation F): making recordNeutralizedConfigForWorker write an actual file into the worktree turned this test red, along with the pre-existing anUntrackedOnlyWorktreeCountsAsDirty test — confirming the assertion is not vacuous.

Credential boundary — unchanged (criterion 4/6)

Stub content and --skip-worktree marking logic are untouched; only logging/config-recording is new. Verified by mutation G below: removing the --skip-worktree call kills 5 existing tests, including allThreeConfigsAreNeutralizedWhenPresent and theNeutralizedConfigIsNotAPendingLocalModification.

Tests added (GitWorktreesTest.java, 4 new, all drive the real GitWorktrees#add path per criterion 5)

  1. isolateToolSurfaceLogsAllThreeConfigsNeutralized — exact summary line, all three present.
  2. isolateToolSurfaceLogsAbsentConfigsWithReason — exact summary line, two absent.
  3. aWorkerCanDiscoverNeutralizedConfigsFromWorktreeScopedGitConfig — reads back fleet.neutralizedConfig/fleet.neutralizedConfigNote via git config --worktree, asserts the full file set and that the note states both "stub" and "primary checkout".
  4. aProvisionedWorktreeHasCleanGitStatusDespiteNeutralizedConfigRecordkeeping — criterion 3, full git status --porcelain.

Build

cd fleetd/ && mvn clean install — full output read, not piped.

Tests run: 1172, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

(Baseline on main after #242 was 1168; +4 new tests here, 0 failures.)

Mutation testing (production lines changed)

Each mutation applied to GitWorktrees.java, ran mvn test -Dtest=GitWorktreesTest, checked grep -cE 'COMPILATION ERROR|cannot find symbol' was 0 before trusting a red result, then reverted.

# Mutation Compile errors Result
A denominator WORKTREE_HOSTILE_CONFIGS.size() → neutralized.size() in the summary log call 0 RED — isolateToolSurfaceLogsAbsentConfigsWithReason
B swapped neutralized/skipped order in the combined detail string 0 RED — isolateToolSurfaceLogsAbsentConfigsWithReason
C dropped the " absent" reason suffix 0 RED — isolateToolSurfaceLogsAbsentConfigsWithReason
D disabled recordNeutralizedConfigForWorker entirely (if (true) return;) 0 RED — aWorkerCanDiscoverNeutralizedConfigsFromWorktreeScopedGitConfig
E dropped "primary checkout" from the note text 0 RED — aWorkerCanDiscoverNeutralizedConfigsFromWorktreeScopedGitConfig
F leaked an extra NEUTRALIZED-CONFIGS.txt file into the worktree alongside the config record 0 RED (2) — the new clean-status test AND the pre-existing anUntrackedOnlyWorktreeCountsAsDirty
G removed the --skip-worktree marking in neutralize 0 RED (5) — allThreeConfigsAreNeutralizedWhenPresent, theNeutralizedConfigIsNotAPendingLocalModification, aTrackedOpencodeConfigIsNeutralizedAndHidden, anUntrackedOnlyWorktreeCountsAsDirty, the new clean-status test

All mutations reverted; the final mvn clean install above ran on the clean, reverted tree.

Draft CLAUDE.md text (point 3 — not applied by me; CLAUDE.md is out of my scope)

For the "## Project addendum — claude-bridge" section, e.g. near the existing "Never commit .mcp.json..." bullet:

- **A provisioned worktree neutralizes `.mcp.json`, `opencode.json`, and `.autoenv`** — the repo's
  committed copies would otherwise mount the primary's IDE/forge servers or block on an autoenv
  prompt (fleetd #134). The worktree's copy of each is a stub, not the repo's real file; the daemon
  logs a per-spawn summary, but that log is not visible to the worker. From inside its own worktree
  a worker (or the lead debugging one) can check which files were neutralized with
  `git config --worktree --get-all fleet.neutralizedConfig` (and
  `git config --worktree --get fleet.neutralizedConfigNote` for the consequence) — never trust the
  content of these files as the repo's real configuration.

Scope boundaries respected

Did not touch wiki/, .mcp.json, CLAUDE.md, or FleetConfig.java. Only GitWorktrees.java and GitWorktreesTest.java changed. Staged explicitly, never git add -A. Rebased onto origin/main (which already carries #242) before starting this unit.

Caveat for review

None outside stated scope.

## Scope fleetd #134, the real one this time — `GitWorktrees.isolateToolSurface` (fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java:450), not `overlayParity` (that was #148 point 3, already merged in #242). This PR covers points 1 and 2 of the ticket; point 3 (documentation) is drafted below for the lead to apply, per the brief. ## Point 1 — the daemon must say it `isolateToolSurface` (and the per-file `neutralize` helper it calls) logged nothing before. Now it collects which of the three configured files (`.mcp.json`, `opencode.json`, `.autoenv`) were actually neutralized vs. absent-and-skipped, and logs one `info` summary per provisioning — same shape as `overlayParity`'s fix in #148 point 3: name the denominator, name what was neutralized, name why anything was not. ### Exact new log lines - All three present: `tool-surface isolation: neutralized 3 of 3 configs: .mcp.json, opencode.json, .autoenv — the worktree copy is a stub, not the repo's file; edit the real file in the primary checkout instead` - Only `.mcp.json` present (the common case — it's always created regardless of presence): `tool-surface isolation: neutralized 1 of 3 configs: .mcp.json (opencode.json absent, .autoenv absent) — the worktree copy is a stub, not the repo's file; edit the real file in the primary checkout instead` ## Point 2 — the worker must be able to discover it from inside its own worktree **Mechanism chosen: worktree-scoped git config, not a working-tree file.** `isolateToolSurface` now also writes, once per provisioning (only when at least one file was neutralized — in practice always, since `.mcp.json` is unconditional): ``` git config --worktree --add fleet.neutralizedConfig <file> # once per neutralized file git config --worktree fleet.neutralizedConfigNote "the worktree copy of each fleet.neutralizedConfig path is a stub, not the repo's committed file; edit the real file from the primary checkout instead" ``` A worker discovers this from inside its own worktree with: ``` git config --worktree --get-all fleet.neutralizedConfig git config --worktree --get fleet.neutralizedConfigNote ``` **Why this and not a worktree file:** worktree-scoped config lives in `.git/worktrees/<nonce>/config.worktree`, never in the working tree, so it can never appear in `git status` — satisfying the brief's constraint without needing a gitignore entry or any other working-tree trick. It also isn't new to this codebase: `configureEnvironmentCredentialHelper` and `configureHttpsUrlRewriteForSshOrigin` already use the exact same mechanism (and the same `extensions.worktreeConfig` enablement) for the credential helper and the SSH→HTTPS rewrite, both already running earlier in the same `add()` call. I re-enable `extensions.worktreeConfig` defensively inside `isolateToolSurface` itself too, so the method doesn't silently depend on call order in `add()` for correctness. A gitignored path was the other option on the table; I didn't pick it because a gitignored file can still be read as a false "this IS the file" by a naive `cat`/`Read`, still needs a name collision check against whatever the repo itself might commit, and — unlike git config — has no natural multi-value/key-value shape for "which files, and why." `git config --worktree` gives both for free and reuses an established pattern. ## `git status --porcelain` proof New test `aProvisionedWorktreeHasCleanGitStatusDespiteNeutralizedConfigRecordkeeping` runs the full porcelain status (not a single-file check) on a freshly provisioned worktree with all three hostile configs present and asserts it is empty. I also mutation-tested this directly (see table below, mutation F): making `recordNeutralizedConfigForWorker` write an actual file into the worktree turned this test red, along with the pre-existing `anUntrackedOnlyWorktreeCountsAsDirty` test — confirming the assertion is not vacuous. ## Credential boundary — unchanged (criterion 4/6) Stub content and `--skip-worktree` marking logic are untouched; only logging/config-recording is new. Verified by mutation G below: removing the `--skip-worktree` call kills 5 existing tests, including `allThreeConfigsAreNeutralizedWhenPresent` and `theNeutralizedConfigIsNotAPendingLocalModification`. ## Tests added (GitWorktreesTest.java, 4 new, all drive the real `GitWorktrees#add` path per criterion 5) 1. `isolateToolSurfaceLogsAllThreeConfigsNeutralized` — exact summary line, all three present. 2. `isolateToolSurfaceLogsAbsentConfigsWithReason` — exact summary line, two absent. 3. `aWorkerCanDiscoverNeutralizedConfigsFromWorktreeScopedGitConfig` — reads back `fleet.neutralizedConfig`/`fleet.neutralizedConfigNote` via `git config --worktree`, asserts the full file set and that the note states both "stub" and "primary checkout". 4. `aProvisionedWorktreeHasCleanGitStatusDespiteNeutralizedConfigRecordkeeping` — criterion 3, full `git status --porcelain`. ## Build `cd fleetd/ && mvn clean install` — full output read, not piped. ``` Tests run: 1172, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` (Baseline on main after #242 was 1168; +4 new tests here, 0 failures.) ## Mutation testing (production lines changed) Each mutation applied to `GitWorktrees.java`, ran `mvn test -Dtest=GitWorktreesTest`, checked `grep -cE 'COMPILATION ERROR|cannot find symbol'` was 0 before trusting a red result, then reverted. | # | Mutation | Compile errors | Result | |---|---|---|---| | A | denominator `WORKTREE_HOSTILE_CONFIGS.size()` → `neutralized.size()` in the summary log call | 0 | RED — `isolateToolSurfaceLogsAbsentConfigsWithReason` | | B | swapped `neutralized`/`skipped` order in the combined detail string | 0 | RED — `isolateToolSurfaceLogsAbsentConfigsWithReason` | | C | dropped the `" absent"` reason suffix | 0 | RED — `isolateToolSurfaceLogsAbsentConfigsWithReason` | | D | disabled `recordNeutralizedConfigForWorker` entirely (`if (true) return;`) | 0 | RED — `aWorkerCanDiscoverNeutralizedConfigsFromWorktreeScopedGitConfig` | | E | dropped "primary checkout" from the note text | 0 | RED — `aWorkerCanDiscoverNeutralizedConfigsFromWorktreeScopedGitConfig` | | F | leaked an extra `NEUTRALIZED-CONFIGS.txt` file into the worktree alongside the config record | 0 | RED (2) — the new clean-status test AND the pre-existing `anUntrackedOnlyWorktreeCountsAsDirty` | | G | removed the `--skip-worktree` marking in `neutralize` | 0 | RED (5) — `allThreeConfigsAreNeutralizedWhenPresent`, `theNeutralizedConfigIsNotAPendingLocalModification`, `aTrackedOpencodeConfigIsNeutralizedAndHidden`, `anUntrackedOnlyWorktreeCountsAsDirty`, the new clean-status test | All mutations reverted; the final `mvn clean install` above ran on the clean, reverted tree. ## Draft `CLAUDE.md` text (point 3 — not applied by me; `CLAUDE.md` is out of my scope) For the "## Project addendum — claude-bridge" section, e.g. near the existing "Never commit `.mcp.json`..." bullet: ```markdown - **A provisioned worktree neutralizes `.mcp.json`, `opencode.json`, and `.autoenv`** — the repo's committed copies would otherwise mount the primary's IDE/forge servers or block on an autoenv prompt (fleetd #134). The worktree's copy of each is a stub, not the repo's real file; the daemon logs a per-spawn summary, but that log is not visible to the worker. From inside its own worktree a worker (or the lead debugging one) can check which files were neutralized with `git config --worktree --get-all fleet.neutralizedConfig` (and `git config --worktree --get fleet.neutralizedConfigNote` for the consequence) — never trust the content of these files as the repo's real configuration. ``` ## Scope boundaries respected Did not touch `wiki/`, `.mcp.json`, `CLAUDE.md`, or `FleetConfig.java`. Only `GitWorktrees.java` and `GitWorktreesTest.java` changed. Staged explicitly, never `git add -A`. Rebased onto `origin/main` (which already carries #242) before starting this unit. ## Caveat for review None outside stated scope.
agent added 1 commit 2026-09-03 06:25:46 +02:00
fleetd #134: make tool-surface neutralization visible to the daemon and the worker
CI / contract (pull_request) Successful in 53s
CI / build (pull_request) Successful in 1m35s
d654ccb818
isolateToolSurface replaced .mcp.json/opencode.json/.autoenv with neutral stubs and
marked them --skip-worktree, but said nothing anywhere. A real worker read a 3-byte
{} stub for opencode.json, where the repo's real file is 30+ lines, and truthfully
(but wrongly) reported a mount key did not exist.

Two readers, two fixes:
- the daemon operator gets one info log per provisioning, naming the denominator,
  what was neutralized, and why anything was not (same shape as overlayParity's
  fix in #148 point 3).
- the worker gets the same fact recorded in worktree-scoped git config
  (fleet.neutralizedConfig / fleet.neutralizedConfigNote), discoverable with
  `git config --worktree --get-all fleet.neutralizedConfig` from inside its own
  worktree, without asking the lead. Not a working-tree file: this repo already
  uses worktree-scoped config for the credential helper and the SSH->HTTPS
  rewrite, and it lives under .git/worktrees/<nonce>/ so it can never appear in
  `git status` for the worker to trip on or commit.

The neutralization itself (stub content, --skip-worktree marking) is unchanged.
ltms closed this pull request 2026-09-03 06:32:41 +02:00
Some checks are pending
CI / contract (pull_request) Successful in 53s
CI / build (pull_request) Successful in 1m35s

Pull request closed

Sign in to join this conversation.