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:
@@ -107,7 +107,8 @@ public record BridgedConfig(
|
||||
String cwd,
|
||||
List<String> parityOverlay,
|
||||
String gitTokenEnv, String gitHostEnv,
|
||||
String kind) {
|
||||
String kind,
|
||||
Map<String, String> env) {
|
||||
|
||||
/** Peer kind spawned by {@link dev.ltms.bridged.worker.ClaudeCodeLauncher} (the default). */
|
||||
public static final String KIND_CLAUDE_CODE = "claude-code";
|
||||
@@ -133,6 +134,7 @@ public record BridgedConfig(
|
||||
// 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.
|
||||
gitHostEnv = (gitHostEnv == null || gitHostEnv.isBlank()) ? "GITEA_HOST" : gitHostEnv;
|
||||
env = (env == null) ? Map.of() : Map.copyOf(env);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,13 +159,26 @@ public record BridgedConfig(
|
||||
String placement, String workspace, String tabLabel, String mcpUrl,
|
||||
String cwd, List<String> parityOverlay, String gitTokenEnv, String gitHostEnv) {
|
||||
this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel,
|
||||
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, null);
|
||||
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible constructor without the CB-511 {@code env:} passthrough — the worker
|
||||
* gets the daemon's PATH and nothing else. Keeps pre-CB-511 call sites working.
|
||||
*/
|
||||
public Worker(String profile, String baseUrl, String model,
|
||||
String configDir, String tokenEnv, List<String> argv,
|
||||
String placement, String workspace, String tabLabel, String mcpUrl,
|
||||
String cwd, List<String> parityOverlay, String gitTokenEnv, String gitHostEnv,
|
||||
String kind) {
|
||||
this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel,
|
||||
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, kind, null);
|
||||
}
|
||||
|
||||
/** A copy with {@code profile} set — used to default a profile to its {@code workers} key. */
|
||||
public Worker withProfile(String p) {
|
||||
return new Worker(p, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel,
|
||||
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, kind);
|
||||
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, kind, env);
|
||||
}
|
||||
|
||||
/** True when this profile is served by the Claude Code adapter (the default kind). */
|
||||
|
||||
@@ -119,7 +119,7 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
|
||||
String baseUrl = cfg.baseUrl();
|
||||
guard.assertWorker(baseUrl); // hard stop before we spawn anything
|
||||
|
||||
Map<String, String> workerEnv = newEnv();
|
||||
Map<String, String> workerEnv = baseEnv(cfg);
|
||||
workerEnv.put("ANTHROPIC_BASE_URL", baseUrl);
|
||||
putIfPresent(workerEnv, "ANTHROPIC_MODEL", cfg.model());
|
||||
putIfPresent(workerEnv, "CLAUDE_CONFIG_DIR", cfg.configDir());
|
||||
|
||||
@@ -496,8 +496,33 @@ public abstract class HerdrPeerLauncher implements PeerLauncher {
|
||||
}
|
||||
|
||||
/** A fresh mutable env map — the conventional starting point for {@link #buildLaunch}. */
|
||||
protected static Map<String, String> newEnv() {
|
||||
return new LinkedHashMap<>();
|
||||
/**
|
||||
* Seed a worker's environment (CB-511): the daemon's own {@code PATH}, then the profile's
|
||||
* {@code env:} entries.
|
||||
*
|
||||
* <p>Why this exists: bridged passes herdr an explicit env map, and herdr merges it into
|
||||
* <em>its own</em> process environment. So before this, a worker inherited whatever PATH the
|
||||
* herdr server happened to be started with — on this host, one from weeks earlier with no JDK
|
||||
* and no Maven, which left workers unable to run the build they were being asked to run. The
|
||||
* worker's toolchain must follow from configuration, not from how a long-lived daemon was
|
||||
* launched.
|
||||
*
|
||||
* <p>Adapter-specific variables are layered on top of this by {@code buildLaunch} and therefore
|
||||
* win. That ordering is deliberate and load-bearing: it stops a profile's {@code env:} from
|
||||
* overriding {@code ANTHROPIC_BASE_URL} and slipping past {@link
|
||||
* dev.ltms.bridged.guard.SubscriptionGuard}, which is checked against the profile's
|
||||
* {@code baseUrl} and nothing else.
|
||||
*/
|
||||
protected Map<String, String> baseEnv(BridgedConfig.Worker cfg) {
|
||||
Map<String, String> workerEnv = new LinkedHashMap<>();
|
||||
String path = env.apply("PATH");
|
||||
if (path != null && !path.isBlank()) {
|
||||
workerEnv.put("PATH", path);
|
||||
}
|
||||
if (cfg != null && cfg.env() != null) {
|
||||
workerEnv.putAll(cfg.env());
|
||||
}
|
||||
return workerEnv;
|
||||
}
|
||||
|
||||
/** Defensive copy of {@code argv} plus room to append launch flags. */
|
||||
|
||||
@@ -138,7 +138,7 @@ public final class OpenCodeLauncher extends HerdrPeerLauncher {
|
||||
*/
|
||||
@Override
|
||||
protected Launch buildLaunch(BridgedConfig.Worker cfg) {
|
||||
Map<String, String> workerEnv = newEnv();
|
||||
Map<String, String> workerEnv = baseEnv(cfg);
|
||||
// A config file is needed for the bridge MCP mount, for a pinned endpoint (CB-508), or both.
|
||||
if (cfg.hasMcp() || hasCustomProvider(cfg)) {
|
||||
workerEnv.put("OPENCODE_CONFIG", writeConfig(cfg).toString());
|
||||
|
||||
Reference in New Issue
Block a user