Merge CB-539 + CB-542: subscription profiles, with the env: bypass closed
CI / contract (push) Successful in 40s
CI / build (push) Successful in 50s

Verified by the lead in a clean worktree at 5afe8e1 rather than on the worker's
report: Tests run: 474, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS (main
was at 464).

The invariant this lands: there is no configuration in which a worker reaches an
Anthropic endpoint that no guard vetted. Closed at two layers — a fatal, profile-
naming refusal at config load, and a launcher-side strip so it holds for profiles
built in code that never passed validation.

The wiki half is a separate branch on the submodule's own remote; the pointer bump
follows as its own commit on main.
This commit was merged in pull request #14.
This commit is contained in:
2026-08-13 15:31:51 +02:00
5 changed files with 336 additions and 8 deletions
@@ -87,6 +87,9 @@ public final class Bridged {
// dangerous configuration cannot be reached by ignoring a log line. // dangerous configuration cannot be reached by ignoring a log line.
cfg.validateAuthExposure(); cfg.validateAuthExposure();
cfg.validateLeadScan(); cfg.validateLeadScan();
// CB-542: a subscription:true profile whose env: reseats ANTHROPIC_BASE_URL/AUTH_TOKEN would
// reach an unguarded endpoint (the launcher skips SubscriptionGuard for it). Refuse at load.
cfg.validateSubscriptionProfiles();
Path socket = cfg.herdrSocket() != null && !cfg.herdrSocket().isBlank() Path socket = cfg.herdrSocket() != null && !cfg.herdrSocket().isBlank()
? Path.of(cfg.herdrSocket()) ? Path.of(cfg.herdrSocket())
@@ -119,6 +119,17 @@ public record BridgedConfig(
* each adapter drives only its own kind. Normalised to lower-case; blank ⇒ the * each adapter drives only its own kind. Normalised to lower-case; blank ⇒ the
* default. It selects the adapter, not the transport — placement, tabs, cwd, and * default. It selects the adapter, not the transport — placement, tabs, cwd, and
* the readiness gate are kind-independent and stay in the shared base. * the readiness gate are kind-independent and stay in the shared base.
* @param subscription {@code true} to run this profile's workers on the operator's Claude
* subscription, on purpose (CB-539). When set, the launcher neither requires
* nor injects {@code ANTHROPIC_BASE_URL} / {@code ANTHROPIC_AUTH_TOKEN}, and the
* {@code SubscriptionGuard} base_url requirement is skipped <em>for this
* profile only</em>. Absent/{@code false} (the default) keeps today's hard
* refusal: a claude-code profile with no base_url may not spawn, because
* spawning one would bill the subscription. Mutually exclusive with
* {@code baseUrl} — setting both is a configuration error (the two state
* opposite intents). For the same reason, an {@code env:} entry naming
* {@code ANTHROPIC_BASE_URL} or {@code ANTHROPIC_AUTH_TOKEN} is refused at
* config load (CB-542): on the subscription path no guard would vet it.
*/ */
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public record Worker(String profile, String baseUrl, String model, public record Worker(String profile, String baseUrl, String model,
@@ -130,7 +141,8 @@ public record BridgedConfig(
String kind, String kind,
Map<String, String> env, Map<String, String> env,
Float weight, Float weight,
Integer maxLoad) { Integer maxLoad,
Boolean subscription) {
/** Peer kind spawned by {@link dev.ltms.bridged.worker.ClaudeCodeLauncher} (the default). */ /** Peer kind spawned by {@link dev.ltms.bridged.worker.ClaudeCodeLauncher} (the default). */
public static final String KIND_CLAUDE_CODE = "claude-code"; public static final String KIND_CLAUDE_CODE = "claude-code";
@@ -163,6 +175,7 @@ public record BridgedConfig(
env = (env == null) ? Map.of() : Map.copyOf(env); env = (env == null) ? Map.of() : Map.copyOf(env);
weight = (weight == null || weight <= 0.0f) ? 1.0f : weight; weight = (weight == null || weight <= 0.0f) ? 1.0f : weight;
maxLoad = (maxLoad == null || maxLoad <= 0) ? null : maxLoad; maxLoad = (maxLoad == null || maxLoad <= 0) ? null : maxLoad;
subscription = (subscription != null && subscription) ? Boolean.TRUE : Boolean.FALSE;
} }
/** /**
@@ -175,7 +188,7 @@ public record BridgedConfig(
String placement, String workspace, String tabLabel, String mcpUrl, String placement, String workspace, String tabLabel, String mcpUrl,
String cwd, List<String> parityOverlay) { String cwd, List<String> parityOverlay) {
this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel, this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel,
mcpUrl, cwd, parityOverlay, null, null, null, null, null, null); mcpUrl, cwd, parityOverlay, null, null, null, null, null, null, null);
} }
/** /**
@@ -187,7 +200,7 @@ public record BridgedConfig(
String placement, String workspace, String tabLabel, String mcpUrl, String placement, String workspace, String tabLabel, String mcpUrl,
String cwd, List<String> parityOverlay, String gitTokenEnv, String gitHostEnv) { String cwd, List<String> parityOverlay, String gitTokenEnv, String gitHostEnv) {
this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel, this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel,
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, null, null, null, null); mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, null, null, null, null, null);
} }
/** /**
@@ -200,13 +213,13 @@ public record BridgedConfig(
String cwd, List<String> parityOverlay, String gitTokenEnv, String gitHostEnv, String cwd, List<String> parityOverlay, String gitTokenEnv, String gitHostEnv,
String kind) { String kind) {
this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel, this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel,
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, kind, null, null, null); mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, kind, null, null, null, null);
} }
/** A copy with {@code profile} set — used to default a profile to its {@code workers} key. */ /** A copy with {@code profile} set — used to default a profile to its {@code workers} key. */
public Worker withProfile(String p) { public Worker withProfile(String p) {
return new Worker(p, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel, return new Worker(p, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel,
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, kind, env, weight, maxLoad); mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, kind, env, weight, maxLoad, subscription);
} }
/** True when this profile is served by the Claude Code adapter (the default kind). */ /** True when this profile is served by the Claude Code adapter (the default kind). */
@@ -219,11 +232,48 @@ public record BridgedConfig(
return KIND_OPENCODE.equals(kind); return KIND_OPENCODE.equals(kind);
} }
/**
* True when this profile runs on the operator's Claude subscription, on purpose (CB-539).
* Absent/{@code false} (the default) keeps the hard refusal: a claude-code profile with no
* base_url may not spawn, because doing so would bill the subscription.
*/
public boolean isSubscription() {
return Boolean.TRUE.equals(subscription);
}
/**
* Backward-compatible constructor without the CB-539 subscription flag — the worker stays on
* the off-subscription boundary (the default). Keeps pre-CB-539 call sites (and any YAML
* that omits the flag) compiling and behaving identically.
*/
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, Map<String, String> env, Float weight, Integer maxLoad) {
this(profile, baseUrl, model, configDir, tokenEnv, argv, placement, workspace, tabLabel,
mcpUrl, cwd, parityOverlay, gitTokenEnv, gitHostEnv, kind, env, weight, maxLoad, null);
}
/** True when this profile's workers are granted a forge token to open their own PR (CB-302). */ /** True when this profile's workers are granted a forge token to open their own PR (CB-302). */
public boolean hasGitToken() { public boolean hasGitToken() {
return gitTokenEnv != null && !gitTokenEnv.isBlank(); return gitTokenEnv != null && !gitTokenEnv.isBlank();
} }
/**
* True when this profile's {@code env:} block names a worker-side Anthropic binding variable.
* Those two keys are the adapter's, never the operator's: on a claude-code worker
* {@code ANTHROPIC_BASE_URL} is the endpoint the {@code SubscriptionGuard} vetted, and
* {@code ANTHROPIC_AUTH_TOKEN} is injected from {@code tokenEnv}. An {@code env:} entry for
* either is a bypass vector — it is what the subscription path would otherwise let survive
* unguarded — so it is rejected at config load (see
* {@link BridgedConfig#validateSubscriptionProfiles()}).
*/
public boolean envCarriesAnthropicBinding() {
return env != null
&& (env.containsKey("ANTHROPIC_BASE_URL") || env.containsKey("ANTHROPIC_AUTH_TOKEN"));
}
/** True when workers should land in their own tab in the worker space. */ /** True when workers should land in their own tab in the worker space. */
public boolean tabPlacement() { public boolean tabPlacement() {
return "tab".equals(placement); return "tab".equals(placement);
@@ -632,6 +682,44 @@ public record BridgedConfig(
+ "confused."); + "confused.");
} }
/**
* Reject a subscription profile whose {@code env:} block tries to reseat the Anthropic binding
* (CB-542).
*
* <p>Why this must be fatal rather than sanitised: {@code subscription: true} deliberately
* stops the launcher from writing {@code ANTHROPIC_BASE_URL}/{@code ANTHROPIC_AUTH_TOKEN} and
* skips the {@code SubscriptionGuard} for that profile. But the profile's {@code env:} map is
* layered into the worker environment separately, so an {@code ANTHROPIC_BASE_URL} sitting
* there would survive into the worker having passed no guard at all — {@code subscription: true}
* plus an {@code env:} repoint is a contradiction just like {@code subscription: true} plus a
* {@code baseUrl}. The launcher also hard-strips these two keys from the worker env as a
* belt-and-braces measure; this method is the loud, load-time refusal so the operator is told
* about the mistake instead of having it silently cleaned up.
*
* @throws IllegalStateException when any subscription profile's {@code env:} names
* {@code ANTHROPIC_BASE_URL} or {@code ANTHROPIC_AUTH_TOKEN},
* naming the profile and the offending key(s)
*/
public void validateSubscriptionProfiles() {
List<String> bad = new java.util.ArrayList<>();
workerProfiles().forEach((name, w) -> {
if (w.isSubscription() && w.envCarriesAnthropicBinding()) {
List<String> keys = w.env().keySet().stream()
.filter(k -> k.equals("ANTHROPIC_BASE_URL") || k.equals("ANTHROPIC_AUTH_TOKEN"))
.sorted()
.toList();
bad.add("worker profile '" + name + "' carries " + keys + " in env: — "
+ "subscription: true forbids ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN there, "
+ "because on the subscription no guard vets them (they would repoint the "
+ "worker past the SubscriptionGuard). Remove them from env: (or drop "
+ "subscription: true).");
}
});
if (!bad.isEmpty()) {
throw new IllegalStateException("refusing to start: " + String.join(" ", bad));
}
}
/** True for the loopback addresses and the unspecified-but-local forms we treat as same-host. */ /** True for the loopback addresses and the unspecified-but-local forms we treat as same-host. */
private static boolean isLoopbackBind(String host) { private static boolean isLoopbackBind(String host) {
if (host == null || host.isBlank()) { if (host == null || host.isBlank()) {
@@ -6,6 +6,8 @@ import dev.ltms.bridged.herdr.Agent;
import dev.ltms.bridged.herdr.AgentControl; import dev.ltms.bridged.herdr.AgentControl;
import dev.ltms.bridged.herdr.WorkspaceControl; import dev.ltms.bridged.herdr.WorkspaceControl;
import dev.ltms.bridged.peer.Capability; import dev.ltms.bridged.peer.Capability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
@@ -36,6 +38,8 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
/** Label prefix for this adapter's herdr agent names (drives naming + orphan reap). */ /** Label prefix for this adapter's herdr agent names (drives naming + orphan reap). */
private static final String NAME_PREFIX = "claude"; private static final String NAME_PREFIX = "claude";
private static final Logger log = LoggerFactory.getLogger(ClaudeCodeLauncher.class);
private final SubscriptionGuard guard; private final SubscriptionGuard guard;
/** /**
@@ -116,14 +120,43 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher {
*/ */
@Override @Override
protected Launch buildLaunch(BridgedConfig.Worker cfg) { protected Launch buildLaunch(BridgedConfig.Worker cfg) {
// CB-539: a profile may deliberately opt into the subscription (subscription: true) when no
// off-subscription endpoint exists for it — e.g. `sonnet` on `ccs`. That profile gets no
// ANTHROPIC_BASE_URL/AUTH_TOKEN (there is nothing to point them at) and the guard's base_url
// requirement is skipped FOR IT ONLY. Every other profile keeps the hard boundary below.
boolean onSubscription = cfg.isSubscription();
String baseUrl = cfg.baseUrl(); String baseUrl = cfg.baseUrl();
guard.assertWorker(baseUrl); // hard stop before we spawn anything
if (onSubscription) {
// NO SILENT CONTRADICTION: subscription:true + a baseUrl state opposite intents; refuse
// loudly rather than pick a winner.
if (baseUrl != null && !baseUrl.isBlank()) {
throw new IllegalStateException("profile '" + cfg.profile()
+ "' sets both subscription: true and a baseUrl ('" + baseUrl + "') — the two "
+ "are contradictory: a subscription profile must not point at an endpoint. "
+ "Drop baseUrl, or drop subscription: true.");
}
// Visible without anyone going looking for it: this worker bills the subscription.
log.warn("spawning profile '{}' on the Claude subscription (subscription: true) — this "
+ "worker WILL bill the operator's subscription", cfg.profile());
} else {
guard.assertWorker(baseUrl); // hard stop before we spawn anything
}
Map<String, String> workerEnv = baseEnv(cfg); Map<String, String> workerEnv = baseEnv(cfg);
workerEnv.put("ANTHROPIC_BASE_URL", baseUrl); if (onSubscription) {
// CB-542 belt-and-braces: on the subscription path no guard vets these two keys, and the
// profile's env: is layered in by baseEnv — so strip any that rode in there. Config load
// already rejects this (loudly, naming the profile); this makes the boundary hold even
// for a profile built in code that never passed through that validation.
workerEnv.remove("ANTHROPIC_BASE_URL");
workerEnv.remove("ANTHROPIC_AUTH_TOKEN");
} else {
workerEnv.put("ANTHROPIC_BASE_URL", baseUrl);
putIfPresent(workerEnv, "ANTHROPIC_AUTH_TOKEN", env.apply(cfg.tokenEnv()));
}
putIfPresent(workerEnv, "ANTHROPIC_MODEL", cfg.model()); putIfPresent(workerEnv, "ANTHROPIC_MODEL", cfg.model());
putIfPresent(workerEnv, "CLAUDE_CONFIG_DIR", cfg.configDir()); putIfPresent(workerEnv, "CLAUDE_CONFIG_DIR", cfg.configDir());
putIfPresent(workerEnv, "ANTHROPIC_AUTH_TOKEN", env.apply(cfg.tokenEnv()));
applyGitToken(workerEnv, cfg); applyGitToken(workerEnv, cfg);
return new Launch(workerEnv, argvWithModel(argvWithBridge(cfg), cfg)); return new Launch(workerEnv, argvWithModel(argvWithBridge(cfg), cfg));
@@ -640,4 +640,100 @@ class BridgedConfigTest {
assertEquals(1.0f, w.weight(), 0.0001f, "absent weight defaults to 1.0"); assertEquals(1.0f, w.weight(), 0.0001f, "absent weight defaults to 1.0");
assertNull(w.maxLoad(), "absent maxLoad defaults to unlimited (null)"); assertNull(w.maxLoad(), "absent maxLoad defaults to unlimited (null)");
} }
@Test
void subscriptionFlagBindsAndDefaultsFalse(@TempDir Path dir) throws Exception {
Path f = dir.resolve("subscription.yaml");
Files.writeString(f, """
workers:
sonnet:
subscription: true
argv: ["ccs", "sonnet"]
opted:
baseUrl: http://gx10.gw:8000
""");
BridgedConfig cfg = BridgedConfig.load(f);
assertTrue(cfg.workerProfiles().get("sonnet").isSubscription(),
"subscription: true binds as an explicit opt-in");
assertFalse(cfg.workerProfiles().get("opted").isSubscription(),
"a profile without the key stays off-subscription (the default)");
}
// ── CB-542: subscription:true must not smuggle an unguarded endpoint via env: ───────────────
@Test
void aSubscriptionProfileWithAnthropicBaseUrlInEnvIsRejected(@TempDir Path dir) throws Exception {
Path f = dir.resolve("baseUrl.yaml");
Files.writeString(f, """
workers:
sonnet:
subscription: true
argv: ["ccs", "sonnet"]
env:
ANTHROPIC_BASE_URL: http://anything-not-on-the-allowlist
""");
BridgedConfig cfg = BridgedConfig.load(f);
IllegalStateException e = assertThrows(IllegalStateException.class,
cfg::validateSubscriptionProfiles);
assertTrue(e.getMessage().contains("sonnet"), "the refusal names the offending profile");
assertTrue(e.getMessage().contains("ANTHROPIC_BASE_URL"),
"the refusal names the offending key: " + e.getMessage());
}
@Test
void aSubscriptionProfileWithAnthropicAuthTokenInEnvIsRejected(@TempDir Path dir) throws Exception {
Path f = dir.resolve("authToken.yaml");
Files.writeString(f, """
workers:
sonnet:
subscription: true
argv: ["ccs", "sonnet"]
env:
ANTHROPIC_AUTH_TOKEN: sk-ant-not-on-any-allowlist
""");
BridgedConfig cfg = BridgedConfig.load(f);
IllegalStateException e = assertThrows(IllegalStateException.class,
cfg::validateSubscriptionProfiles);
assertTrue(e.getMessage().contains("sonnet"), "the refusal names the offending profile");
assertTrue(e.getMessage().contains("ANTHROPIC_AUTH_TOKEN"),
"the refusal names the offending key: " + e.getMessage());
}
@Test
void aSubscriptionProfileWithACleanEnvPassesValidation(@TempDir Path dir) throws Exception {
Path f = dir.resolve("clean.yaml");
Files.writeString(f, """
workers:
sonnet:
subscription: true
argv: ["ccs", "sonnet"]
env:
JAVA_HOME: /opt/jdk
""");
BridgedConfig cfg = BridgedConfig.load(f);
assertDoesNotThrow(cfg::validateSubscriptionProfiles,
"a subscription profile may carry env: — just not the Anthropic binding keys");
}
@Test
void aNonSubscriptionProfileMayCarryAnthropicEnvKeys(@TempDir Path dir) throws Exception {
// The override is only dangerous on the subscription path, where no guard could vet it. A
// plain profile's env: is still overwritten by the launcher's guard-checked value (CB-511).
Path f = dir.resolve("nonsub.yaml");
Files.writeString(f, """
workers:
gx10:
baseUrl: http://gx10.gw:8000
env:
ANTHROPIC_BASE_URL: http://something
""");
BridgedConfig cfg = BridgedConfig.load(f);
assertDoesNotThrow(cfg::validateSubscriptionProfiles,
"only subscription:true profiles are checked — off-subscription ones keep the baseUrl guard");
}
} }
@@ -1,6 +1,7 @@
package dev.ltms.bridged.worker; package dev.ltms.bridged.worker;
import dev.ltms.bridged.config.BridgedConfig; import dev.ltms.bridged.config.BridgedConfig;
import dev.ltms.bridged.guard.GuardException;
import dev.ltms.bridged.guard.SubscriptionGuard; import dev.ltms.bridged.guard.SubscriptionGuard;
import dev.ltms.bridged.herdr.AgentControl; import dev.ltms.bridged.herdr.AgentControl;
import dev.ltms.bridged.herdr.FakeHerdr; import dev.ltms.bridged.herdr.FakeHerdr;
@@ -615,4 +616,111 @@ class ClaudeCodeLauncherTest {
assertFalse(spawnedArgs(herdr).contains("--model")); assertFalse(spawnedArgs(herdr).contains("--model"));
assertNull(startEnv(herdr).get("ANTHROPIC_MODEL")); assertNull(startEnv(herdr).get("ANTHROPIC_MODEL"));
} }
// --- CB-539: subscription-profile opt-in ----------------------------------------------------
/** A claude-code profile on the subscription: no baseUrl (by design), no off-sub endpoint. */
private static BridgedConfig.Worker subscriptionCfg(String profile, String baseUrl) {
return new BridgedConfig.Worker(
profile, baseUrl, "sonnet", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", profile), "tab", "bridged-workers", "w #{n}", null, null, null,
null, null, null, Map.of(), null, null, true);
}
@Test
void defaultRefusalIsPreservedForClaudeProfileWithNoBaseUrl() {
// Requirement 1: absent subscription:true ⇒ byte-identical refusal to today. A claude-code
// profile with no baseUrl and no subscription must still be refused (it would bill the sub).
FakeHerdr herdr = new FakeHerdr();
BridgedConfig.Worker cfg = new BridgedConfig.Worker(
"ltms-local", null, "coder", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", "ltms-local"), "tab", "bridged-workers", "w #{n}", null, null, null);
ClaudeCodeLauncher svc = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null);
GuardException ex = assertThrows(GuardException.class, () -> svc.spawn("ltms-local", null, null));
assertTrue(ex.getMessage().contains("no ANTHROPIC_BASE_URL"),
"the refusal names the missing baseUrl: " + ex.getMessage());
assertEquals(0, herdr.calls.stream().filter(c -> c.method().equals("agent.start")).count(),
"nothing was spawned before the refusal");
}
@Test
void subscriptionProfileSpawnsWithoutInjectedAnthropicVars() {
// Requirement on subscription:true: no baseUrl is required (or injected), and neither
// ANTHROPIC_BASE_URL nor ANTHROPIC_AUTH_TOKEN is injected even though the token env would
// resolve one if asked.
FakeHerdr herdr = new FakeHerdr();
BridgedConfig.Worker cfg = subscriptionCfg("sonnet", null);
new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(),
_ -> "would-be-token").spawn();
Map<String, String> env = startEnv(herdr);
assertNull(env.get("ANTHROPIC_BASE_URL"), "no baseUrl injected for a subscription profile");
assertNull(env.get("ANTHROPIC_AUTH_TOKEN"), "no auth token injected for a subscription profile");
assertEquals("sonnet", env.get("ANTHROPIC_MODEL"),
"the model alias is still injected; only the subscription-boundary vars are dropped");
}
@Test
void subscriptionPlusBaseUrlIsRefused() {
// Requirement 2: subscription:true + a baseUrl state opposite intents — refuse at spawn,
// naming the profile, rather than silently picking a winner.
FakeHerdr herdr = new FakeHerdr();
BridgedConfig.Worker cfg = subscriptionCfg("sonnet", "http://gx00.gw:8000");
ClaudeCodeLauncher svc = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), _ -> null);
IllegalStateException ex = assertThrows(IllegalStateException.class,
() -> svc.spawn("sonnet", null, null));
assertTrue(ex.getMessage().contains("sonnet"), "refusal names the profile: " + ex.getMessage());
assertTrue(ex.getMessage().contains("subscription"), "refusal explains the contradiction: " + ex.getMessage());
assertEquals(0, herdr.calls.stream().filter(c -> c.method().equals("agent.start")).count(),
"nothing was spawned before the contradiction was refused");
}
@Test
void nonSubscriptionProfilesAreStillAllowlistChecked() {
// Requirement 3: the guard keeps its teeth for every other profile — a base_url whose host is
// not on the allowlist is still refused, whether or not any subscription profile exists.
FakeHerdr herdr = new FakeHerdr();
BridgedConfig.Worker rogue = new BridgedConfig.Worker(
"rogue", "http://evil.example.com:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", "rogue"), "tab", "bridged-workers", "w #{n}", null, null, null);
ClaudeCodeLauncher svc = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(rogue.profile(), rogue), rogue.profile(), _ -> null);
GuardException ex = assertThrows(GuardException.class, () -> svc.spawn("rogue", null, null));
assertTrue(ex.getMessage().contains("not on the"), "refusal cites the allowlist: " + ex.getMessage());
assertEquals(0, herdr.calls.stream().filter(c -> c.method().equals("agent.start")).count(),
"nothing was spawned before the allowlist refusal");
}
@Test
void aSubscriptionProfileHasAnEnvSuppliedAnthropicBindingStripped() {
// CB-542: even a subscription profile whose env: carries ANTHROPIC_BASE_URL (or AUTH_TOKEN)
// must not hand them to the worker — on the subscription path no guard would vet them. Config
// load refuses this loudly; this launcher-side strip is the belt-and-braces that makes the
// invariant hold for a profile built in code that never passed through that validation.
FakeHerdr herdr = new FakeHerdr();
BridgedConfig.Worker cfg = new BridgedConfig.Worker(
"sonnet", null, "sonnet", null, "BRIDGED_WORKER_TOKEN",
List.of("ccs", "sonnet"), "tab", "bridged-workers", "w #{n}", null, null, null,
null, null, null,
Map.of("ANTHROPIC_BASE_URL", "http://evil.example.com",
"ANTHROPIC_AUTH_TOKEN", "sk-ant-bad", "JAVA_HOME", "/opt/jdk"),
null, null, true);
new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(),
_ -> "would-be-token").spawn();
Map<String, String> env = startEnv(herdr);
assertNull(env.get("ANTHROPIC_BASE_URL"),
"the unguarded endpoint must not survive into the worker");
assertNull(env.get("ANTHROPIC_AUTH_TOKEN"),
"the unguarded token must not survive into the worker");
assertEquals("/opt/jdk", env.get("JAVA_HOME"),
"only the Anthropic binding keys are stripped; the rest of env: still applies");
}
} }