CB-511: give workers a toolchain — propagate the daemon PATH, add profile env:
CI / build (push) Successful in 1m17s
CI / build (push) Successful in 1m17s
Workers could not run `mvn` or `java`. Every delegated task that asked for a build came back "mvn is not on PATH", and the worker was right. Root cause: HerdrPeerLauncher seeded the worker environment with an EMPTY map, so bridged passed only the vars it explicitly set (OPENCODE_CONFIG, GITEA_TOKEN, ANTHROPIC_*) and never PATH. herdr merges that map into its own process env, so a worker inherited whatever PATH the herdr SERVER was started with. On this host that server (pid 79870, PPID 1) had been up since Jul 4 with a PATH containing neither the JDK nor Maven. Confirmed on a live worker: its PATH was byte-identical to herdr's, and the only var bridged had contributed was OPENCODE_CONFIG. The failure was invisible and non-deterministic: the fleet's capabilities depended on how a long-lived daemon happened to be launched weeks earlier. There are three herdr processes on this box with three different PATHs; the one owning the socket is the one without a toolchain. bridged itself HAD Maven on PATH the whole time — it just never passed it on. It also quietly contradicted the project's own principle that "a worker is a full peer of the primary", and the implementer skill's instruction to build, commit and open a PR. Every delegation so far has depended on the primary running the build gate. Fix: baseEnv(cfg) seeds each worker with the daemon's own PATH, then applies the profile's new optional env: map. Adapter-specific vars are layered on top and therefore win — that ordering is load-bearing, not incidental: it stops an env: entry from overwriting ANTHROPIC_BASE_URL and slipping past SubscriptionGuard, which is checked against the profile's baseUrl alone. Pinned by a test. Because the default is now the daemon's PATH, both supervision units set PATH explicitly — launchd and systemd do not source a login shell, so under CB-504 the daemon (and every worker) would otherwise get a bare /usr/bin:/bin and this bug would silently return in production. 324 tests (was 321): daemon-PATH propagation, profile env: passthrough including an explicit PATH override, and the guard-bypass ordering. Verified live: daemon restarted, worker spawned, and asked to run the tools — "Apache Maven 3.9.16", "java version 25.0.2". Previously both were absent.
This commit is contained in:
@@ -417,4 +417,56 @@ class ClaudeCodeLauncherTest {
|
||||
assertEquals(0, paneCloseCount(herdr, handle.id()),
|
||||
"no orphan pane close from the gate path");
|
||||
}
|
||||
|
||||
// --- CB-511: worker environment seeding -----------------------------------------------------
|
||||
|
||||
@Test
|
||||
void workerInheritsTheDaemonPath() {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
BridgedConfig.Worker cfg = new BridgedConfig.Worker(
|
||||
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
|
||||
List.of("claude"), "tab", "bridged-workers", "w #{n}", null, null, null);
|
||||
new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
|
||||
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(),
|
||||
k -> "PATH".equals(k) ? "/opt/tools/bin:/usr/bin" : null).spawn();
|
||||
|
||||
assertEquals("/opt/tools/bin:/usr/bin", startEnv(herdr).get("PATH"),
|
||||
"a worker with no PATH cannot run the build it is asked to run");
|
||||
}
|
||||
|
||||
@Test
|
||||
void profileEnvIsInjectedIntoTheWorker() {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
BridgedConfig.Worker cfg = new BridgedConfig.Worker(
|
||||
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
|
||||
List.of("claude"), "tab", "bridged-workers", "w #{n}", null, null, null, null, null,
|
||||
null, Map.of("JAVA_HOME", "/opt/jdk", "PATH", "/profile/bin"));
|
||||
new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
|
||||
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(),
|
||||
k -> "PATH".equals(k) ? "/daemon/bin" : null).spawn();
|
||||
|
||||
Map<String, String> env = startEnv(herdr);
|
||||
assertEquals("/opt/jdk", env.get("JAVA_HOME"), "profile env: is passed through");
|
||||
assertEquals("/profile/bin", env.get("PATH"), "an explicit profile PATH overrides the daemon's");
|
||||
}
|
||||
|
||||
/**
|
||||
* The security-relevant ordering. {@code SubscriptionGuard} is checked against the profile's
|
||||
* {@code baseUrl} only, so if a profile's {@code env:} could overwrite ANTHROPIC_BASE_URL a
|
||||
* worker could be pointed at an unguarded host while the guard passed on a benign one.
|
||||
*/
|
||||
@Test
|
||||
void profileEnvCannotOverrideGuardCheckedAnthropicVars() {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
BridgedConfig.Worker cfg = new BridgedConfig.Worker(
|
||||
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
|
||||
List.of("claude"), "tab", "bridged-workers", "w #{n}", null, null, null, null, null,
|
||||
null, Map.of("ANTHROPIC_BASE_URL", "http://evil.example.com"));
|
||||
new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
|
||||
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(),
|
||||
_ -> null).spawn();
|
||||
|
||||
assertEquals("http://gx00.gw:8000", startEnv(herdr).get("ANTHROPIC_BASE_URL"),
|
||||
"the guard-checked baseUrl must win over any env: entry, or the boundary is bypassable");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user