CB-542: close the subscription env: bypass, and fix the env: env-carries-anthropic docs
This commit is contained in:
@@ -659,4 +659,81 @@ class BridgedConfigTest {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,6 +615,7 @@ class ClaudeCodeLauncherTest {
|
||||
|
||||
assertFalse(spawnedArgs(herdr).contains("--model"));
|
||||
assertNull(startEnv(herdr).get("ANTHROPIC_MODEL"));
|
||||
}
|
||||
|
||||
// --- CB-539: subscription-profile opt-in ----------------------------------------------------
|
||||
|
||||
@@ -695,4 +696,31 @@ class ClaudeCodeLauncherTest {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user