From 723fe494e92e2c34078e45b993ea0e9fba1d8d51 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Thu, 3 Sep 2026 09:50:49 +0700 Subject: [PATCH 1/2] CB-612: suppress unneeded token warnings --- .../src/main/java/dev/ltms/fleet/Fleetd.java | 8 ++- .../dev/ltms/fleet/config/FleetConfig.java | 10 +++- .../ltms/fleet/RequiredSecretEnvVarsTest.java | 59 +++++++++++++++++-- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java b/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java index fa9541f..a650085 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java +++ b/fleetd/src/main/java/dev/ltms/fleet/Fleetd.java @@ -795,7 +795,7 @@ public final class Fleetd { /** * CB-594: which env vars the loaded config actually needs, and why — every non-{@code - * subscription} profile's {@code tokenEnv} (a subscription profile never reads one, see + * subscription} profile's explicitly configured {@code tokenEnv} (a subscription profile never reads one, see * {@link FleetConfig.Profile#isSubscription()}), plus every profile's {@code gitTokenEnv} * where set (opt-in), plus a configured {@code broker.uriEnv} (CB-151). Derived from the * config, not hard-coded, so a new profile is covered for free. A var required by more than one @@ -812,7 +812,9 @@ public final class Fleetd { static Map> requiredSecretEnvVars(FleetConfig cfg) { Map> requiredBy = new LinkedHashMap<>(); cfg.profiles().forEach((name, profile) -> { - if (!profile.isSubscription()) { + // All kinds are checked when tokenEnv is explicit. OpenCode may use provider credentials, + // but an explicit tokenEnv still declares a required host secret for its configured provider. + if (!profile.isSubscription() && profile.hasTokenEnv()) { requiredBy.computeIfAbsent(profile.tokenEnv(), _ -> new ArrayList<>()) .add("profile '" + name + "' tokenEnv"); } @@ -838,7 +840,7 @@ public final class Fleetd { *

A missing entry only warns — it must never refuse to start. A daemon that boots and says * what is wrong is strictly more useful than one that will not boot at all. */ - private static void reportRequiredSecrets(FleetConfig cfg) { + static void reportRequiredSecrets(FleetConfig cfg) { Map> requiredBy = requiredSecretEnvVars(cfg); if (requiredBy.isEmpty()) { log.info("startup secrets: no profile references a token env var — nothing to check"); diff --git a/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java b/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java index 27ed389..04a98a3 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java +++ b/fleetd/src/main/java/dev/ltms/fleet/config/FleetConfig.java @@ -240,7 +240,8 @@ public record FleetConfig( * @param configDir {@code CLAUDE_CONFIG_DIR} so the worker inherits the profile's * skills/MCP/hooks (may be {@code null}) * @param tokenEnv name of the host env var holding the worker's auth token; its value - * is injected as {@code ANTHROPIC_AUTH_TOKEN} (never stored in config) + * is injected as {@code ANTHROPIC_AUTH_TOKEN} (never stored in config). + * {@code null}/blank means this profile needs no token. * @param argv launch command; defaults to {@code ["claude"]} * @param placement where a worker lands: {@code "tab"} (default — its own tab in the * worker space) or {@code "pane"} (legacy — split the focused tab) @@ -389,7 +390,7 @@ public record FleetConfig( ? (KIND_CLAUDE_CODE.equals(k) ? List.of("claude") : List.of(k)) : List.copyOf(argv); kind = k; - tokenEnv = (tokenEnv == null || tokenEnv.isBlank()) ? "FLEETD_WORKER_TOKEN" : tokenEnv; + tokenEnv = (tokenEnv == null || tokenEnv.isBlank()) ? null : tokenEnv; placement = (placement == null || placement.isBlank()) ? "tab" : placement.toLowerCase(); workspace = (workspace == null || workspace.isBlank()) ? "fleet" : workspace; // CB-557: no per-profile default any more. A label is generated from the member's ROLE @@ -604,6 +605,11 @@ public record FleetConfig( return gitTokenEnv != null && !gitTokenEnv.isBlank(); } + /** True when this profile explicitly names a host token environment variable. */ + public boolean hasTokenEnv() { + return tokenEnv != null && !tokenEnv.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 diff --git a/fleetd/src/test/java/dev/ltms/fleet/RequiredSecretEnvVarsTest.java b/fleetd/src/test/java/dev/ltms/fleet/RequiredSecretEnvVarsTest.java index d8dbfde..e6980c5 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/RequiredSecretEnvVarsTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/RequiredSecretEnvVarsTest.java @@ -1,8 +1,12 @@ package dev.ltms.fleet; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import dev.ltms.fleet.config.FleetConfig; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.slf4j.LoggerFactory; import java.nio.file.Files; import java.nio.file.Path; @@ -26,19 +30,66 @@ class RequiredSecretEnvVarsTest { return FleetConfig.load(f); } + private static ListAppender attach() { + Logger logger = (Logger) LoggerFactory.getLogger(Fleetd.class); + ListAppender appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + return appender; + } + + private static void detach(ListAppender appender) { + ((Logger) LoggerFactory.getLogger(Fleetd.class)).detachAppender(appender); + } + @Test - void collectsATokenEnvPerNonSubscriptionProfile(@TempDir Path dir) throws Exception { + void explicitlyConfiguredUnsetTokenEnvWarnsWithCurrentWording(@TempDir Path dir) throws Exception { FleetConfig cfg = load(dir, """ profiles: local: baseUrl: http://gx00.gw:8000 - tokenEnv: AI_GATEWAY_TOKEN + tokenEnv: CB115_MISSING_TOKEN_ENV_819367 """); Map> required = Fleetd.requiredSecretEnvVars(cfg); - assertTrue(required.containsKey("AI_GATEWAY_TOKEN")); - assertEquals(List.of("profile 'local' tokenEnv"), required.get("AI_GATEWAY_TOKEN")); + assertTrue(required.containsKey("CB115_MISSING_TOKEN_ENV_819367")); + assertEquals(List.of("profile 'local' tokenEnv"), required.get("CB115_MISSING_TOKEN_ENV_819367")); + + ListAppender appender = attach(); + try { + Fleetd.reportRequiredSecrets(cfg); + } finally { + detach(appender); + } + + assertTrue(appender.list.stream().anyMatch(e -> + e.getLevel() == ch.qos.logback.classic.Level.WARN + && e.getFormattedMessage().contains("startup secret CB115_MISSING_TOKEN_ENV_819367: MISSING") + && e.getFormattedMessage().contains("profile 'local' tokenEnv")), + "an explicitly configured but unset tokenEnv must retain the startup warning"); + } + + @Test + void profileWithoutTokenEnvDoesNotRequireTheOldDefault(@TempDir Path dir) throws Exception { + FleetConfig cfg = load(dir, """ + profiles: + local: + baseUrl: http://gx00.gw:8000 + """); + + assertTrue(Fleetd.requiredSecretEnvVars(cfg).isEmpty(), + "an absent tokenEnv means this profile needs no token, not FLEETD_WORKER_TOKEN"); + + ListAppender appender = attach(); + try { + Fleetd.reportRequiredSecrets(cfg); + } finally { + detach(appender); + } + + assertFalse(appender.list.stream().anyMatch(e -> e.getLevel() == ch.qos.logback.classic.Level.WARN), + "a profile without tokenEnv must not produce a startup secret warning"); } @Test -- 2.52.0 From adb7a6788089bd9e10e689c88c7b2b5585b1c4fd Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Thu, 3 Sep 2026 09:59:21 +0700 Subject: [PATCH 2/2] CB-612: support tokenless Claude Code profiles --- .../ltms/fleet/member/ClaudeCodeLauncher.java | 2 +- .../fleet/member/ClaudeCodeLauncherTest.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java b/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java index fa1b391..f66fe33 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java +++ b/fleetd/src/main/java/dev/ltms/fleet/member/ClaudeCodeLauncher.java @@ -258,7 +258,7 @@ public final class ClaudeCodeLauncher extends HerdrPeerLauncher { workerEnv.remove("ANTHROPIC_AUTH_TOKEN"); } else { workerEnv.put("ANTHROPIC_BASE_URL", baseUrl); - putIfPresent(workerEnv, "ANTHROPIC_AUTH_TOKEN", env.apply(cfg.tokenEnv())); + putIfPresent(workerEnv, "ANTHROPIC_AUTH_TOKEN", resolveEnv(cfg.tokenEnv())); } putIfPresent(workerEnv, "ANTHROPIC_MODEL", cfg.model()); putIfPresent(workerEnv, "CLAUDE_CONFIG_DIR", cfg.configDir()); diff --git a/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java b/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java index f6127ee..20d87db 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/member/ClaudeCodeLauncherTest.java @@ -583,6 +583,24 @@ class ClaudeCodeLauncherTest { assertNull(env.get("GITEA_HOST"), "no forge host without a granted token"); } + @Test + void noTokenEnvSpawnsWithoutInjectingAnAuthToken() { + FakeHerdr herdr = new FakeHerdr(); + FleetConfig.Profile cfg = new FleetConfig.Profile( + "local-direct", "http://gx00.gw:8000", "coder", null, null, + List.of("claude"), "tab", "fleetd-workers", "w #{n}", null, null, null); + ClaudeCodeLauncher launcher = new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr), + new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(), System::getenv); + + assertDoesNotThrow(() -> { + launcher.spawn(); + }, + "a profile that needs no token must still spawn against its configured endpoint"); + Map env = startEnv(herdr); + assertFalse(env.containsKey("ANTHROPIC_AUTH_TOKEN"), + "an unconfigured tokenEnv must not inject an auth-token key"); + } + // --- CB-117 orphan reap: the pure predicate -------------------------------- @Test -- 2.52.0