ward-kdl tiers: generalize the per-tier build beyond forgejo + prove additive read-base tiering on signoz (migration foundation) #338

Closed
opened 2026-06-25 17:19:34 +00:00 by coilysiren · 3 comments
Owner

Goal

Lay the foundation for the full ward-kdl read/write/admin tier migration (epic: see back-link comment): generalize the tier build past forgejo, and prove the additive read-base build-up pattern on a second area, signoz. This is the schema-setting run every per-area fan-out copies, so it sets the layout and the build loop. It does not fan out to other areas and does not touch the monolith or ward ops (that boundary is ward#278's Phase 1 / Phase 2 split - hold it here too).

The posture (do not deviate)

read is the least-privileged base. Build UP. Each higher tier inherits the one below and adds grants. A forgotten grant stays denied (fail-closed). Never author the full surface at admin and subtract downward - one missed subtraction hands a read child an admin verb.

  • read = the get/list/search/view surface (the semantic reads).
  • write = inherit read + create/edit (authoring).
  • admin = inherit write + delete (the destructive superset).

Curated never denials live where their verb family does and inherit upward. Where a tier must escalate past a broad base denial by name, that needs the cli-guard override capability (separate issue) - signoz does not need it (see below), which is exactly why signoz is the right first proof.

Why signoz is the second area

cmd/ward-kdl/ward-kdl.signoz.guardfile.kdl is CRU-only: every delete is denied (never delete dashboard, never delete rule) and the enable/disable toggle is unsurfaced (ward#241). So its tiering is pure additive build-up, no override needed:

  • read: get pipeline, get/list dashboard, get/list rule.
  • write: inherit read + create query-range, create pipeline, create/edit dashboard, create/edit rule. The two never delete denials sit here (or read) and inherit up.
  • admin: inherit write. signoz has no allowed delete, so admin == write for this area. That is correct and intended, and it proves the chain composes even when the top tier adds nothing.

Instructive edge case: create query-range is a POST that reads telemetry (nothing is created). Tier placement follows semantics, not HTTP method - it is a read, but it is mechanically a create leaf. Decide and document where it lands (recommend: keep it in the tier its create verb naturally compiles to, i.e. write, and note the semantic mismatch in a comment - or, if cli-guard can alias it to a read verb, prefer that). This choice is itself part of the schema other areas will reference, so write down the reasoning.

Multi-area-per-tier-binary (the real structural proof)

The driver "discovers every ward-kdl.*.guardfile.kdl beside it that shares the wrap <bin> name and merges them into one binary." So a wrap ward-kdl-read signoz guardfile dropped into cmd/ward-kdl/ward-kdl-read/ alongside the forgejo one merges into a single ward-kdl-read binary carrying both areas. That is the whole point of the migration - one tier binary, every area - and this run proves it on forgejo+signoz.

Steps

  1. Author the signoz tier guardfiles under the existing per-tier subdirs:
    • cmd/ward-kdl/ward-kdl-read/ward-kdl.signoz.read.guardfile.kdl (the singleton-bearing root for signoz: spec/base-url/auth; read leaves).
    • cmd/ward-kdl/ward-kdl-write/ward-kdl.signoz.write.guardfile.kdl (inherit the read sibling, add create/edit, carry the never delete denials).
    • cmd/ward-kdl/ward-kdl-admin/ward-kdl.signoz.admin.guardfile.kdl (inherit write; adds nothing for signoz - keep it as the explicit tier anchor).
    • Re-declare restrict per tier if signoz needs an owner/scope gate (it has none today - confirm).
  2. Generalize the Makefile build-ward-kdl-forgejo-tiers target into a tier build that locks/builds/gens every guardfile in each tier subdir, not a hardcoded forgejo triplet. Rename to build-ward-kdl-tiers (keep a forgejo alias if anything references it). The target should pick up new areas by file discovery, mirroring how build-ward-kdl already discovers base guardfiles - adding an area is dropping three guardfiles, no Makefile edit.
  3. Re-lock/build/gen. Each tier binary now carries forgejo + signoz. Commit the regenerated per-tier *.lock.json, specverb.lock, main.go, and the docs/ward-kdl/ward-kdl.signoz.{read,write,admin}.guardfile.md reference docs.

Acceptance

  • make build-ward-kdl-tiers builds all three binaries, each carrying both forgejo and signoz verbs (grep --help / generated docs for a signoz leaf in ward-kdl-read and a forgejo leaf in the same binary).
  • signoz tiering matches the map above: read = get/list, write = +create/edit + the query-range read-POST, admin == write (no delete). never delete dashboard/never delete rule present and effective at write and admin.
  • forgejo tiers unchanged in effective surface (this run only generalizes the build + adds signoz; the forgejo curated port is ward#278, not here).
  • The build target is area-discovering, not forgejo-hardcoded - adding a future area is three guardfiles, no target edit.
  • Monolith ward-kdl.forgejo.guardfile.kdl, sync-ops-assets, cmd/ward/opsassets/*, and ward ops forgejo behaviour untouched (Phase 1 boundary, ward#278).
  • Existing tests pass; make build, make test, make vet, make lint green before push.

Out of scope (Kai-owned / later)

  • Context selection wiring (full ward-kdl-admin native binary, -read install in CI docker images, -write warded broker already exists) - epic Phase 2.
  • Retiring the forgejo monolith or rewiring ward ops forgejo to a tier - ward#278 Phase 2.
  • The cli-guard override keyword - separate issue; not needed for signoz.
  • Fanning out to other areas - that is the post-foundation fan-out the epic tracks.

References

ward#240 (the chain), ward#278 (forgejo curated port, the sibling schema), ward#246 (build+install the tier binaries), ward#329 (the broker that shells ward-kdl-write), ward#241 (signoz CRU-only curation), cli-guard#159/#160 (wildcard grants, inherit).

## Goal Lay the **foundation** for the full ward-kdl read/write/admin tier migration (epic: see back-link comment): generalize the tier build past forgejo, and prove the additive read-base build-up pattern on a **second area, signoz**. This is the schema-setting run every per-area fan-out copies, so it sets the layout and the build loop. It does **not** fan out to other areas and does **not** touch the monolith or `ward ops` (that boundary is ward#278's Phase 1 / Phase 2 split - hold it here too). ## The posture (do not deviate) **read is the least-privileged base. Build UP.** Each higher tier `inherit`s the one below and **adds** grants. A forgotten grant stays denied (fail-closed). Never author the full surface at admin and subtract downward - one missed subtraction hands a read child an admin verb. - **read** = the get/list/search/view surface (the semantic reads). - **write** = inherit read + create/edit (authoring). - **admin** = inherit write + delete (the destructive superset). Curated `never` denials live where their verb family does and inherit upward. Where a tier must escalate past a broad base denial by name, that needs the cli-guard `override` capability (separate issue) - **signoz does not need it** (see below), which is exactly why signoz is the right first proof. ## Why signoz is the second area `cmd/ward-kdl/ward-kdl.signoz.guardfile.kdl` is **CRU-only**: every delete is denied (`never delete dashboard`, `never delete rule`) and the enable/disable toggle is unsurfaced (ward#241). So its tiering is **pure additive build-up, no override needed**: - **read**: `get pipeline`, `get`/`list dashboard`, `get`/`list rule`. - **write**: inherit read + `create query-range`, `create pipeline`, `create`/`edit dashboard`, `create`/`edit rule`. The two `never delete` denials sit here (or read) and inherit up. - **admin**: inherit write. signoz has **no allowed delete**, so admin == write for this area. That is correct and intended, and it proves the chain composes even when the top tier adds nothing. **Instructive edge case:** `create query-range` is a POST that *reads* telemetry (nothing is created). Tier placement follows **semantics, not HTTP method** - it is a read, but it is mechanically a `create` leaf. Decide and document where it lands (recommend: keep it in the tier its `create` verb naturally compiles to, i.e. write, and note the semantic mismatch in a comment - or, if cli-guard can alias it to a read verb, prefer that). This choice is itself part of the schema other areas will reference, so write down the reasoning. ## Multi-area-per-tier-binary (the real structural proof) The driver "discovers every `ward-kdl.*.guardfile.kdl` beside it that shares the `wrap <bin>` name and merges them into one binary." So a `wrap ward-kdl-read` signoz guardfile dropped into `cmd/ward-kdl/ward-kdl-read/` alongside the forgejo one **merges into a single `ward-kdl-read` binary carrying both areas**. That is the whole point of the migration - one tier binary, every area - and this run proves it on forgejo+signoz. ## Steps 1. Author the signoz tier guardfiles under the existing per-tier subdirs: - `cmd/ward-kdl/ward-kdl-read/ward-kdl.signoz.read.guardfile.kdl` (the singleton-bearing root for signoz: `spec`/`base-url`/`auth`; read leaves). - `cmd/ward-kdl/ward-kdl-write/ward-kdl.signoz.write.guardfile.kdl` (`inherit` the read sibling, add create/edit, carry the `never delete` denials). - `cmd/ward-kdl/ward-kdl-admin/ward-kdl.signoz.admin.guardfile.kdl` (`inherit` write; adds nothing for signoz - keep it as the explicit tier anchor). - Re-declare `restrict` per tier if signoz needs an owner/scope gate (it has none today - confirm). 2. Generalize the Makefile `build-ward-kdl-forgejo-tiers` target into a tier build that **locks/builds/gens every guardfile in each tier subdir**, not a hardcoded forgejo triplet. Rename to `build-ward-kdl-tiers` (keep a forgejo alias if anything references it). The target should pick up new areas by file discovery, mirroring how `build-ward-kdl` already discovers base guardfiles - adding an area is dropping three guardfiles, no Makefile edit. 3. Re-lock/build/gen. Each tier binary now carries forgejo + signoz. Commit the regenerated per-tier `*.lock.json`, `specverb.lock`, `main.go`, and the `docs/ward-kdl/ward-kdl.signoz.{read,write,admin}.guardfile.md` reference docs. ## Acceptance - `make build-ward-kdl-tiers` builds all three binaries, each carrying **both** forgejo and signoz verbs (grep `--help` / generated docs for a signoz leaf in `ward-kdl-read` and a forgejo leaf in the same binary). - signoz tiering matches the map above: read = get/list, write = +create/edit + the query-range read-POST, admin == write (no delete). `never delete dashboard`/`never delete rule` present and effective at write and admin. - forgejo tiers unchanged in effective surface (this run only generalizes the build + adds signoz; the forgejo curated port is ward#278, not here). - The build target is area-discovering, not forgejo-hardcoded - adding a future area is three guardfiles, no target edit. - Monolith `ward-kdl.forgejo.guardfile.kdl`, `sync-ops-assets`, `cmd/ward/opsassets/*`, and `ward ops forgejo` behaviour untouched (Phase 1 boundary, ward#278). - Existing tests pass; `make build`, `make test`, `make vet`, `make lint` green before push. ## Out of scope (Kai-owned / later) - Context selection wiring (full `ward-kdl-admin` native binary, `-read` install in CI docker images, `-write` warded broker already exists) - epic Phase 2. - Retiring the forgejo monolith or rewiring `ward ops forgejo` to a tier - ward#278 Phase 2. - The cli-guard `override` keyword - separate issue; not needed for signoz. - Fanning out to other areas - that is the post-foundation fan-out the epic tracks. ## References ward#240 (the chain), ward#278 (forgejo curated port, the sibling schema), ward#246 (build+install the tier binaries), ward#329 (the broker that shells `ward-kdl-write`), ward#241 (signoz CRU-only curation), cli-guard#159/#160 (wildcard grants, `inherit`).
Author
Owner

Foundation run for the full tier-migration epic ward#339. Engine sibling cli-guard#169 (the override keyword) is not needed for this signoz run - signoz is CRU-only - but is the prerequisite for the curated-escalation half (forgejo ward#278).

Foundation run for the full tier-migration epic ward#339. Engine sibling cli-guard#169 (the override keyword) is not needed for this signoz run - signoz is CRU-only - but is the prerequisite for the curated-escalation half (forgejo ward#278).
Author
Owner

🔒 Reserved by ward agent --driver claude — container ward-ward-issue-338-claude-78767463 on host 4f2062d4fed1 is carrying this issue (reserved 2026-06-25T17:21:06Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `ward-ward-issue-338-claude-78767463` on host `4f2062d4fed1` is carrying this issue (reserved 2026-06-25T17:21:06Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

Retrospective from the container that carried this.

The mechanism docs paid off — once I understood that the driver merges every guardfile in a tier subdir sharing the wrap name, the surface work was mostly transcription from the monolith with the verbs split read/write/admin. The structural proof landed cleanly: each tier binary really does carry forgejo + signoz, and admin==write for a CRU-only area is satisfying to see compile.

The thing that fought back was the vendored spec. forgejo's spec is remote, so its tiers need no local file — but signoz's hand-authored spec has to sit in each tier dir, because the inheriting write/admin members resolve spec relative to their own dir and go:embed can't reach a sibling. So 'adding an area is three guardfiles' isn't literally true for a vendored-spec area; the build target has to vendor the spec per tier. I keyed a copy-before-lock off the read guardfile's spec line and gitignored the copies, keeping the base file the single source of truth — but that nuance is worth knowing before the fan-out.

Two surprises: re-locking exposed stale committed forgejo write/admin docs (still said ssm though the guardfiles moved to env FORGEJO_TOKEN in #331) — regenerating fixed them, surface untouched. And specverb.lock netted to zero because signoz's SSM auth re-introduced exactly the aws-sdk deps the env-auth forgejo write tier had dropped. query-range I placed in write (the verb it compiles to) with the read-not-write semantics flagged inline, per the issue's recommendation.

Confidence is high — verified every tier surface via --help, full pre-commit suite green (I had to pull kdlfmt + trufflehog binaries onto PATH since they weren't in the container, rather than skip them). Two follow-ups worth filing: (1) an unrelated pre-existing test, TestAgentLaunchSilencesDockerNoiseWhenHeadless, already fails on main (a merge left its expected pull-count stale) — not touched here; (2) when the broker needs signoz-write, it'll want an env auth override like forgejo's #331, since the inherited SSM auth won't resolve in-container.

Retrospective from the container that carried this. The mechanism docs paid off — once I understood that the driver merges every guardfile in a tier subdir sharing the wrap name, the surface work was mostly transcription from the monolith with the verbs split read/write/admin. The structural proof landed cleanly: each tier binary really does carry forgejo + signoz, and admin==write for a CRU-only area is satisfying to see compile. The thing that fought back was the vendored spec. forgejo's spec is remote, so its tiers need no local file — but signoz's hand-authored spec has to sit *in each tier dir*, because the inheriting write/admin members resolve `spec` relative to their own dir and go:embed can't reach a sibling. So 'adding an area is three guardfiles' isn't literally true for a vendored-spec area; the build target has to vendor the spec per tier. I keyed a copy-before-lock off the read guardfile's spec line and gitignored the copies, keeping the base file the single source of truth — but that nuance is worth knowing before the fan-out. Two surprises: re-locking exposed stale committed forgejo write/admin docs (still said `ssm` though the guardfiles moved to `env FORGEJO_TOKEN` in #331) — regenerating fixed them, surface untouched. And specverb.lock netted to zero because signoz's SSM auth re-introduced exactly the aws-sdk deps the env-auth forgejo write tier had dropped. query-range I placed in write (the verb it compiles to) with the read-not-write semantics flagged inline, per the issue's recommendation. Confidence is high — verified every tier surface via --help, full pre-commit suite green (I had to pull kdlfmt + trufflehog binaries onto PATH since they weren't in the container, rather than skip them). Two follow-ups worth filing: (1) an unrelated pre-existing test, TestAgentLaunchSilencesDockerNoiseWhenHeadless, already fails on main (a merge left its expected pull-count stale) — not touched here; (2) when the broker needs signoz-write, it'll want an env auth override like forgejo's #331, since the inherited SSM auth won't resolve in-container.
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-flight-deck/ward#338
No description provided.