fix(roster): a supplied roster path has to name servers #685

Merged
coilyco-ops merged 1 commit from fix/a-roster-path-must-name-servers-claude into main 2026-08-13 18:49:46 +00:00
Member

closes #684

before:  LoadMCPRoster(deploy/mcp-roster.yml) -> servers=0  err=<nil>
after:   ... -> "MCP roster ... names no servers: check the file is the roster
                 itself rather than the ConfigMap that carries it"

Verified against deploy's actual file, not a fixture.

Why it was silent

LoadMCPRoster uses a plain yaml.Unmarshal with no KnownFields, so a ConfigMap's apiVersion, kind, metadata and data are ignored, mcpServers is absent, and the result is an empty roster.

An empty roster is a legitimate state, which is exactly what hid this. A wrong path and a deliberate no-tool deployment produced the same observable service. AGENTS.md already describes the symptom without naming the cause:

without one the roster is empty and a tool case fails for a reason that is not the agent's

Why refusing is right rather than lenient

Supplying a path and resolving it to nothing is a mistake every time. The genuine no-tool case supplies no path at all — which is already how the code reads it, since MCPRosterPath empty means no roster. So this refuses something that was never a valid way to say "no tools".

The message names the ConfigMap specifically, because that is the file someone will have pointed at and the two are not distinguishable by reading either one.

The contrast worth keeping

LoadAccessPolicy(ConfigMap)  -> error          (KnownFields(true))
LoadMCPRoster(ConfigMap)     -> 0 servers, nil  (before this)

Same wrong input, opposite behaviours, in one package. The loud one cost a delivered-and-useless gate; the quiet one costs a deployment its tools with nothing in a log.

Tests cover the ConfigMap shape, four ways of naming no servers, and that a real roster still loads with its server named — so the guard cannot have traded a silent failure for a loud one on correct input.

ward exec gate green.

closes #684 ``` before: LoadMCPRoster(deploy/mcp-roster.yml) -> servers=0 err=<nil> after: ... -> "MCP roster ... names no servers: check the file is the roster itself rather than the ConfigMap that carries it" ``` Verified against deploy's **actual file**, not a fixture. ## Why it was silent `LoadMCPRoster` uses a plain `yaml.Unmarshal` with no `KnownFields`, so a ConfigMap's `apiVersion`, `kind`, `metadata` and `data` are ignored, `mcpServers` is absent, and the result is an empty roster. **An empty roster is a legitimate state**, which is exactly what hid this. A wrong path and a deliberate no-tool deployment produced the same observable service. `AGENTS.md` already describes the symptom without naming the cause: > without one the roster is empty and a tool case fails for a reason that is not the agent's ## Why refusing is right rather than lenient **Supplying a path and resolving it to nothing is a mistake every time.** The genuine no-tool case supplies no path at all — which is already how the code reads it, since `MCPRosterPath` empty means no roster. So this refuses something that was never a valid way to say "no tools". The message names the ConfigMap specifically, because that is the file someone will have pointed at and **the two are not distinguishable by reading either one**. ## The contrast worth keeping ``` LoadAccessPolicy(ConfigMap) -> error (KnownFields(true)) LoadMCPRoster(ConfigMap) -> 0 servers, nil (before this) ``` Same wrong input, opposite behaviours, in one package. The loud one cost a delivered-and-useless gate; the quiet one costs a deployment its tools with nothing in a log. Tests cover the ConfigMap shape, four ways of naming no servers, and that a real roster still loads with its server named — so the guard cannot have traded a silent failure for a loud one on correct input. `ward exec gate` green.
fix(roster): a supplied roster path has to name servers
All checks were successful
ci / image-build (pull_request) Successful in 23s
ci / test (pull_request) Successful in 38s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
e5386af37a
LoadMCPRoster parsed deploy's mcp-roster.yml, which is a ConfigMap, and
returned zero servers with a nil error. There is no KnownFields here, so the
wrapper keys are ignored, mcpServers is absent, and the result is an empty
roster nobody asked for.

An empty roster is a legitimate state, which is what made this silent. A wrong
path and a deliberate no-tool deployment produced the same observable service.
AGENTS.md already describes the symptom without naming this cause: a tool case
fails for a reason that is not the agent's.

Supplying a path and resolving it to nothing is a mistake every time. The
genuine no-tool case supplies no path, which is already how the code reads it,
so refusing here costs nothing real.

The message names the ConfigMap specifically, because that is the file someone
will have pointed at and the difference is not visible by reading either one.

Verified against deploy's actual file, which now fails rather than loading as
an empty roster. The access policy loader fails loudly on the same input and
this one failed silently. Two loaders, one wrong input, opposite behaviours.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!685
No description provided.