CB-604: an unknown kind: is silently accepted and routed to the wrong adapter #102

Closed
opened 2026-08-16 18:32:30 +02:00 by ltms · 1 comment
Owner

Found while writing the multi-profile routing Features entry (CB-595), not by looking for bugs.

What happens

A workers: profile's kind: selects its backend launcher. The value is lower-cased and then compared against the single string "opencode". It is never validated against the set of known kinds.

So kind: opencod is accepted silently and — because it does not equal "opencode" — routed into the claude-code adapter bucket. There is no config-load check anywhere that would catch it before a spawn is attempted.

It gets worse if argv: is also unset. The argv-defaulting logic special-cases only the exact string "claude-code", so the launch command falls back to List.of(kind) — literally the misspelled string. The daemon then tries to execute a program named opencod.

Why this matters more than a typo usually would

This is the shape of failure this repo keeps hitting: a configuration that is accepted, does nothing useful, and reports no error. The same shape as a defaulted dependency silently switching a feature off, which has happened nine times here.

The operator gets no signal at config load, no signal at startup, and a failure only at spawn time — where it looks like a launcher problem rather than a one-character config problem. bridged.yaml is gitignored, so nothing in CI will ever catch it either.

Note the contrast, which shows the right behaviour already exists nearby: CompositePeerLauncher's constructor does refuse two adapters claiming the same profile name, and fails loudly with worker profile '…' is claimed by two peer adapters. So the codebase already treats adapter-routing mistakes as fatal — just not this one.

Fix

Validate kind: at config load against the known set, and throw naming the bad value and the accepted ones. The daemon already fails fast at startup for a bad auth.mode/bind combination, so this is a matched, existing pattern rather than a new one.

Acceptance criteria

  1. An unknown kind: value fails at config load with a message naming the offending value and the accepted set.
  2. An absent or blank kind: still defaults to claude-code — that is the documented behaviour and must not change.
  3. Case-insensitivity is preserved: OpenCode must still work.
  4. A test covers the rejection, and a test covers the absent-value default.
  5. mvn -f bridged/pom.xml clean install green, run unpiped.

Evidence

bridged/src/main/java/dev/ltms/bridged/config/BridgedConfig.java:279-291,383-389 (the lower-case-and-compare, and the argv default), bridged/src/main/java/dev/ltms/bridged/member/CompositePeerLauncher.java:34-60,200-213 (the partition, and the duplicate-claim check that does fail loudly).

Found while writing the multi-profile routing Features entry (CB-595), not by looking for bugs. ## What happens A `workers:` profile's `kind:` selects its backend launcher. The value is lower-cased and then compared against the single string `"opencode"`. It is **never validated against the set of known kinds.** So `kind: opencod` is accepted silently and — because it does not equal `"opencode"` — routed into the **claude-code** adapter bucket. There is no config-load check anywhere that would catch it before a spawn is attempted. It gets worse if `argv:` is also unset. The argv-defaulting logic special-cases only the exact string `"claude-code"`, so the launch command falls back to `List.of(kind)` — literally the misspelled string. The daemon then tries to execute a program named `opencod`. ## Why this matters more than a typo usually would This is the shape of failure this repo keeps hitting: **a configuration that is accepted, does nothing useful, and reports no error.** The same shape as a defaulted dependency silently switching a feature off, which has happened nine times here. The operator gets no signal at config load, no signal at startup, and a failure only at spawn time — where it looks like a launcher problem rather than a one-character config problem. `bridged.yaml` is gitignored, so nothing in CI will ever catch it either. Note the contrast, which shows the right behaviour already exists nearby: `CompositePeerLauncher`'s constructor **does** refuse two adapters claiming the same profile name, and fails loudly with `worker profile '…' is claimed by two peer adapters`. So the codebase already treats adapter-routing mistakes as fatal — just not this one. ## Fix Validate `kind:` at config load against the known set, and throw naming the bad value and the accepted ones. The daemon already fails fast at startup for a bad `auth.mode`/bind combination, so this is a matched, existing pattern rather than a new one. ## Acceptance criteria 1. An unknown `kind:` value fails at config load with a message naming the offending value and the accepted set. 2. An absent or blank `kind:` still defaults to `claude-code` — that is the documented behaviour and must not change. 3. Case-insensitivity is preserved: `OpenCode` must still work. 4. A test covers the rejection, and a test covers the absent-value default. 5. `mvn -f bridged/pom.xml clean install` green, run unpiped. ## Evidence `bridged/src/main/java/dev/ltms/bridged/config/BridgedConfig.java:279-291,383-389` (the lower-case-and-compare, and the argv default), `bridged/src/main/java/dev/ltms/bridged/member/CompositePeerLauncher.java:34-60,200-213` (the partition, and the duplicate-claim check that does fail loudly).
ltms added this to the 1.1 — single-host close-out milestone 2026-08-16 18:32:30 +02:00
ltms closed this issue 2026-08-16 18:39:53 +02:00
Author
Owner

Merged into main as f5deaaf. I verified it myself rather than taking the report.

Build, unpiped: Tests run: 832, Failures: 0, Errors: 0, Skipped: 0 — BUILD SUCCESS, on a trial merge onto main in a scratch worktree.

Probed the real BridgedConfig.load with five values, not just the tests:

kind=opencod      -> REFUSED
kind=opencode     -> ACCEPTED, isOpenCode=true
kind=OpenCode     -> ACCEPTED, isOpenCode=true
kind=claude-code  -> ACCEPTED, isOpenCode=false
kind=<absent>     -> ACCEPTED, isOpenCode=false

Criteria 2 and 3 hold: the default and case-insensitivity both survive.

The message an operator now sees:

refusing to start: profile(s) [gemini=opencod] set an unrecognized kind — accepted values are claude-code, opencode (case-insensitive); an unrecognized kind would otherwise fall back to the claude-code adapter and try to launch a program named after the typo.

It names the profile, the bad value, the accepted set, and the consequence. That last clause is the part that saves the operator a debugging session.

Criterion 5 found two more fields with the same shape. Filed as #106, not folded in here. The auth.mode one is worse than this ticket: a typo of token silently behaves as loopback-trust, and a loopback bind hides it completely, so a daemon can run unauthenticated while the operator believes otherwise.

Closing.

Merged into `main` as `f5deaaf`. I verified it myself rather than taking the report. **Build**, unpiped: `Tests run: 832, Failures: 0, Errors: 0, Skipped: 0` — `BUILD SUCCESS`, on a trial merge onto `main` in a scratch worktree. **Probed the real `BridgedConfig.load`** with five values, not just the tests: ``` kind=opencod -> REFUSED kind=opencode -> ACCEPTED, isOpenCode=true kind=OpenCode -> ACCEPTED, isOpenCode=true kind=claude-code -> ACCEPTED, isOpenCode=false kind=<absent> -> ACCEPTED, isOpenCode=false ``` Criteria 2 and 3 hold: the default and case-insensitivity both survive. The message an operator now sees: > refusing to start: profile(s) [gemini=opencod] set an unrecognized kind — accepted values are claude-code, opencode (case-insensitive); an unrecognized kind would otherwise fall back to the claude-code adapter and try to launch a program named after the typo. It names the profile, the bad value, the accepted set, and the consequence. That last clause is the part that saves the operator a debugging session. **Criterion 5 found two more fields with the same shape.** Filed as #106, not folded in here. The `auth.mode` one is worse than this ticket: a typo of `token` silently behaves as `loopback-trust`, and a loopback bind hides it completely, so a daemon can run unauthenticated while the operator believes otherwise. Closing.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#102