diff --git a/11-Features.md b/11-Features.md index a059c12..e2d2268 100644 --- a/11-Features.md +++ b/11-Features.md @@ -1724,9 +1724,9 @@ special-cases the exact string `"claude-code"`. Nothing caught it before the spa **Gotcha.** The composite constructor separately refuses two adapters claiming the same profile name ("worker profile '…' is claimed by two peer adapters"), so that failure mode has always been loud. -Checking for the same shape elsewhere found three more unvalidated fields — `auth.mode`, the -per-profile `placement:`, and the top-level `placement:` policy, which is validated but only lazily -at first spawn. That is CB-606; until it lands, a typo in those three is still accepted silently. + +Checking for the same shape elsewhere found three more fields, all fixed the same way by **CB-606** +(2026-08-16) — see *Every config value is checked against its valid set* below. --- @@ -1772,6 +1772,47 @@ the sourcing gap, and the unit file gives the operator no prompt to look for it. --- +## Every config value is checked against its valid set + +**What.** A config value the daemon does not recognize now **refuses to start**, naming the field, the +value, the accepted set, and what would otherwise have happened. Four fields carried this defect and +all four are covered: a profile's `kind:` (CB-604), `auth.mode`, a profile's `placement:`, and the +top-level `placement:` policy (all CB-606). + +``` +refusing to start: auth.mode=toekn is not recognized — accepted values are loopback-trust, +token (case-insensitive); an unrecognized mode would otherwise silently fall back to +loopback-trust, which authenticates nobody. +``` + +**On.** Always on; nothing to configure. Every valid value still works case-insensitively, and every +absent value keeps its old default — `kind:` → `claude-code`, `auth.mode` → `loopback-trust`, a +profile's `placement:` → `tab`, the policy → `fixed`. + +**Why.** All four fields had the same shape: the value was lower-cased in a compact constructor, then +compared against exactly **one** string. Anything else fell through to the other branch silently. So +a typo was accepted, did nothing the operator asked for, and reported no error. + +`auth.mode` is why this is a release blocker rather than tidying. A typo of `token` behaved as +`loopback-trust`, and `validateAuthExposure()` only fires on a **non-loopback** bind — so on a +loopback bind, the common case, the mistake was invisible end to end. The daemon started cleanly and +authenticated nobody, while the operator's config said `token` and the operator believed it. + +**Gotcha — the general lesson, not the instances.** CB-604 fixed `kind:` alone. The brief for it also +asked the worker to *report* any other field with the same shape and explicitly not to fix them. That +one question found the other three. **When you fix a defect of shape, ask what else has that shape** +— the instance you noticed is rarely the worst one. Here the field nobody had filed a ticket about +was the field that turned authentication off. + +Two implementation notes worth keeping. The top-level policy was already validated, by +`PlacementPolicies.fromName` — but **lazily, at first spawn**, through the supplier in +`CompositePeerLauncher` that re-reads live config. So a bad name still started a daemon that looked +healthy and failed much later. It is now called eagerly at load as well, which also covers +`ConfigRef.reload()`. And the check calls `fromName` rather than copying its list, so there is still +one source of truth for what a valid policy name is. + +--- + ## Backfill status This page was started after the fact, so it is **not yet complete**. Entries above are written from