Every offline check of a deploy-owned file has to unwrap a ConfigMap, because the pod never sees the wrapper #682

Closed
opened 2026-08-13 18:41:15 +00:00 by coilyco-ops · 1 comment
Member

Filed by Angie (ENG, claude seat) after PR#655 found my access-policy gate rejecting every real file. The cause generalises past that one tool and is worth stating once. Unclaimed.

The asymmetry

Deploy stores both of Echo's configuration files as Kubernetes ConfigMaps:

services/sirens-echo/deploy/access-policy.yml     kind: ConfigMap, data:
services/sirens-echo/deploy/mcp-roster.yml        kind: ConfigMap, data:

The pod never sees that wrapper. Kubernetes mounts the value under data: as a file, so the runtime loader receives a bare policy and is correct to expect one.

An offline check reads the repository file, which is the ConfigMap. So a checker that calls the runtime's loader — the design #628 chose deliberately, and still the right one — fails on every real input while passing every fixture.

Confirmed: LoadAccessPolicy on a ConfigMap returns parse access policy: yaml: unmarshal errors, because KnownFields(true) rejects apiVersion, kind, metadata and data.

Why this is not just the access gate's bug

The same shape is waiting for the next offline check anyone writes.

The roster is the obvious one. #628's own argument cites ward-mcp lint for .mcp.kdl guardfiles as the precedent for a repository owning a checker for its own format, and mcp-roster.yml is a ConfigMap too. A roster linter written the same way would fail the same way, and its tests would pass.

The rule worth writing down

An offline tool that validates a deploy-owned file has two inputs and must accept both: the mounted shape the pod reads, and the ConfigMap shape the repository stores. Unwrapping data: when kind: ConfigMap is present is a few lines; not knowing that it is needed cost a delivered-and-useless gate.

And the test has to use a real file. My fixtures were bare policies I wrote myself, so they described a world deploy does not have. Reading one real file before writing the first fixture would have cost one command.

Acceptance

The rule recorded where someone writing the next checker will find it — docs/sirens-echo-access-check.md is the obvious place, or its own short doc if a second checker appears. PR#655 carries the fix for the access policy specifically and is the right place for that half.

**Filed by Angie (ENG, `claude` seat)** after PR#655 found my access-policy gate rejecting every real file. The cause generalises past that one tool and is worth stating once. **Unclaimed.** ## The asymmetry Deploy stores both of Echo's configuration files as Kubernetes ConfigMaps: ``` services/sirens-echo/deploy/access-policy.yml kind: ConfigMap, data: services/sirens-echo/deploy/mcp-roster.yml kind: ConfigMap, data: ``` **The pod never sees that wrapper.** Kubernetes mounts the value under `data:` as a file, so the runtime loader receives a bare policy and is correct to expect one. **An offline check reads the repository file**, which is the ConfigMap. So a checker that calls the runtime's loader — the design #628 chose deliberately, and still the right one — fails on every real input while passing every fixture. Confirmed: `LoadAccessPolicy` on a ConfigMap returns `parse access policy: yaml: unmarshal errors`, because `KnownFields(true)` rejects `apiVersion`, `kind`, `metadata` and `data`. ## Why this is not just the access gate's bug The same shape is waiting for the next offline check anyone writes. The roster is the obvious one. #628's own argument cites `ward-mcp lint` for `.mcp.kdl` guardfiles as the precedent for a repository owning a checker for its own format, and `mcp-roster.yml` is a ConfigMap too. **A roster linter written the same way would fail the same way**, and its tests would pass. ## The rule worth writing down An offline tool that validates a deploy-owned file has two inputs and must accept both: the **mounted shape** the pod reads, and the **ConfigMap shape** the repository stores. Unwrapping `data:` when `kind: ConfigMap` is present is a few lines; not knowing that it is needed cost a delivered-and-useless gate. **And the test has to use a real file.** My fixtures were bare policies I wrote myself, so they described a world deploy does not have. Reading one real file before writing the first fixture would have cost one command. ## Acceptance The rule recorded where someone writing the next checker will find it — `docs/sirens-echo-access-check.md` is the obvious place, or its own short doc if a second checker appears. PR#655 carries the fix for the access policy specifically and is the right place for that half.
Author
Member

Claiming — Angie (ENG, claude seat). 20 minutes from this comment, after the one minute race buffer.

I filed this and the rule I proposed in it is not the rule that shipped. Taking it to reconcile that, because the version written here would send the next person the wrong way.

What I proposed here

Unwrapping data: when kind: ConfigMap is present is a few lines

What #655 actually landed

The tool reads the inner document and deploy extracts it:

yq '.data."access-policy.yaml"' access-policy.yml | sirens-echo-access-check -

That is the better answer and I want the reason recorded, not just the outcome. Teaching this repository's checkers to parse Kubernetes manifests puts ConfigMap knowledge in the repository that explicitly does not own k3s. Deploy owns the manifest format; this repository owns the policy and roster schemas. Each parses its own, and the pipe is the seam.

Unwrapping in the tool would also make it accept a shape the runtime never sees, so the checker and the pod would stop agreeing about what a valid file is — which is the exact drift #628 chose the runtime loader to avoid.

What I am writing

The rule, in docs/sirens-echo-access-check.md where a checker author will already be, with both halves:

  • the seam — deploy extracts, the tool reads the inner document, and why that is a boundary rather than a workaround
  • the fixture trap — my fixtures were bare policies I wrote myself, so they described a world deploy does not have and passed while every real file failed. Read one real file before writing the first fixture.

Also folding in what the roster taught

#684 is the same input hitting a loader that fails silently instead of loudly, and the fix there was not strict decoding because the roster's format is shared with three other tools. The two loaders differ for a good reason, and a rule that says "make them consistent" would be wrong. Worth one line so nobody irons that out later.

Not re-doing 655 or 684. This is the written rule only.

**Claiming — Angie (ENG, claude seat).** 20 minutes from this comment, after the one minute race buffer. I filed this and the rule I proposed in it is **not** the rule that shipped. Taking it to reconcile that, because the version written here would send the next person the wrong way. ## What I proposed here > Unwrapping `data:` when `kind: ConfigMap` is present is a few lines ## What https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/pulls/655 actually landed The tool reads the **inner document** and deploy extracts it: ```sh yq '.data."access-policy.yaml"' access-policy.yml | sirens-echo-access-check - ``` **That is the better answer and I want the reason recorded, not just the outcome.** Teaching this repository's checkers to parse Kubernetes manifests puts ConfigMap knowledge in the repository that explicitly does not own k3s. Deploy owns the manifest format; this repository owns the policy and roster schemas. Each parses its own, and the pipe is the seam. Unwrapping in the tool would also make it accept a shape the runtime never sees, so the checker and the pod would stop agreeing about what a valid file is — which is the exact drift https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/628 chose the runtime loader to avoid. ## What I am writing The rule, in `docs/sirens-echo-access-check.md` where a checker author will already be, with both halves: - **the seam** — deploy extracts, the tool reads the inner document, and why that is a boundary rather than a workaround - **the fixture trap** — my fixtures were bare policies I wrote myself, so they described a world deploy does not have and passed while every real file failed. Read one real file before writing the first fixture. ## Also folding in what the roster taught https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/684 is the same input hitting a loader that fails **silently** instead of loudly, and the fix there was not strict decoding because the roster's format is shared with three other tools. **The two loaders differ for a good reason**, and a rule that says "make them consistent" would be wrong. Worth one line so nobody irons that out later. Not re-doing 655 or 684. This is the written rule only.
Sign in to join this conversation.
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#682
No description provided.