[Phase 2] Tier deploy + UX: ward shells the context's tier binary, ambient tier, --tier downshift (capstone of ward#339) #342

Closed
opened 2026-06-25 18:25:28 +00:00 by coilysiren · 1 comment
Owner

Phase-2 deploy + UX for the tier migration (epic ward#339). Decisions settled with Kai 2026-06-25. This is the capstone - it ships nothing until the tier contents exist (cli-guard#169 -> ward#278 -> the per-area fan-out), because there is no tier binary to shell until then. Filed now so the design is not lost; do not dispatch until the gate below clears.

The UX principle: the tier is ambient, never typed

A human or agent always runs ward ops <area> <verb>. The context decides the privilege, not the command. This is already true in a warded container (the broker routes to write); the migration generalizes it to native (admin) and CI (read). ward-kdl-{read,write,admin} stay plumbing nobody types by name.

Decisions

1. ward shells the context's tier binary (privilege by absence)

ward is a thin front; ward ops <area> execs whichever ward-kdl-{read,write,admin} binary is present in this context - admin natively, write via the broker in containers, read in CI. Privilege is defined by which binary is installed, so a dangerous verb is physically absent where it should not exist (the CI box has no admin binary, so delete is not gated - it cannot be reached). This is the run-time mirror of the compile-time never/override engine (cli-guard#169): absence, not a flag. It generalizes the broker (ward#329), which already shells ward-kdl-write - the container half is built; native and CI are the new halves.

2. Context signal: broker socket + explicit profile

  • broker socket present -> write (as today, ward#329). The credential stays root-held; the agent shells nothing privileged directly.
  • otherwise an explicit WARD_TIER profile - the dev-base CI image sets read, a native install leaves the default admin. Explicit and auditable: you can always see why a context is the tier it is. No silent environment inference (a misdetection would silently change privilege).

3. Native downshift: ward ops --tier read|write

Native defaults to admin, but --tier read|write runs one call at a lower tier deliberately - to dry-run a dangerous op at the privilege an agent will actually have, or rehearse what CI sees. It only ever lowers privilege (never raises), and it works by shelling the named lower binary, so it is the same absence guarantee, not a flag that re-enables verbs.

Distribution plan

  • Publish all three tier binaries as release assets, per-arch (linux amd64 + arm64), extending the existing publish-kdl-write job in .forgejo/workflows/release.yml into publish-kdl-{read,write,admin}. Same generator-renders-amd64, cross-build-arm64 shape (docs/homebrew-build.md, docs/ward-kdl.md). One release stamps ward + all three tiers at the same version - lockstep, since each tier embeds its own inherit-flattened guardfile and ward embeds its own opsassets.
  • native admin: arrives with the native install (brew tap formula, alongside ward), or as the admin release asset. Reconcile the vehicle with ward#246 (which scoped "tap ships three" before write took the release-asset route).
  • container write: downloaded by the entrypoint, shelled by the broker - exists (ward#329), grows from forgejo-only to all-area as the fan-out lands.
  • CI read: baked into the dev-base image (the CI surface is the image), with WARD_TIER=read set there.

The native ward ops transition (coordinate with ward#278 Phase 2)

Today native ward ops <area> runs the embedded curated monolith in-process (sync-ops-assets -> cmd/ward/opsassets, the specverb path). Decision 1 changes native ops to shell ward-kdl-admin instead. That is exactly ward#278's deferred Phase-2 question ("what does ward ops forgejo become - embed the admin tier? a default? split?"), now answered: shell the admin tier binary, tier chosen by context. When this lands, the in-process opsassets embedding can retire (or stay as an admin fallback). This is the one piece that touches the existing native surface, so it is the most review-sensitive.

Build units (sequence after the gate)

  1. publish-kdl-{read,admin} release-asset jobs, symmetric with publish-kdl-write (parallel-safe; can land early, ships a partial surface that grows like write already does).
  2. WARD_TIER profile resolution in ward + the broker-socket precedence; dev-base image sets read.
  3. ward ops shells the resolved tier binary; --tier read|write native downshift.
  4. Native ops -> admin binary, retire/relegate the in-process opsassets embedding (ward#278 Phase 2, review-sensitive).
  5. Native install vehicle for the admin binary (reconcile ward#246).

Gate (why this waits)

Nothing here ships until there is an all-area tier binary to shell:

  • cli-guard#169 (override engine) - in flight.
  • release + REF bump of cli-guard into ward (the cross-module dance, ward#326).
  • ward#278 (forgejo curated port) + the per-area fan-out - fills read/write/admin with real content across every area.

Unit 1 (publish read/admin assets) is the only piece that is parallel-safe before the fan-out, exactly as write publishes a forgejo-only asset today.

References

ward#339 (epic), ward#329 (the broker this generalizes), ward#278 (the native-surface Phase-2 question this answers), ward#246 (tap-ships-three, vehicle to reconcile), ward#326 (the cross-module release dance), cli-guard#169 (the compile-time absence engine this mirrors at run time), docs/broker.md, docs/homebrew-build.md, docs/ward-kdl.md.

Phase-2 deploy + UX for the tier migration (epic ward#339). Decisions settled with Kai 2026-06-25. This is the **capstone** - it ships nothing until the tier contents exist (cli-guard#169 -> ward#278 -> the per-area fan-out), because there is no tier binary to shell until then. Filed now so the design is not lost; **do not dispatch until the gate below clears.** ## The UX principle: the tier is ambient, never typed A human or agent always runs `ward ops <area> <verb>`. The **context** decides the privilege, not the command. This is already true in a warded container (the broker routes to write); the migration generalizes it to native (admin) and CI (read). `ward-kdl-{read,write,admin}` stay plumbing nobody types by name. ## Decisions ### 1. ward shells the context's tier binary (privilege by absence) `ward` is a thin front; `ward ops <area>` execs whichever `ward-kdl-{read,write,admin}` binary is present in this context - admin natively, write via the broker in containers, read in CI. Privilege is defined by **which binary is installed**, so a dangerous verb is *physically absent* where it should not exist (the CI box has no admin binary, so `delete` is not gated - it cannot be reached). This is the run-time mirror of the compile-time never/override engine (cli-guard#169): absence, not a flag. It generalizes the broker (ward#329), which already shells `ward-kdl-write` - the container half is built; native and CI are the new halves. ### 2. Context signal: broker socket + explicit profile - **broker socket present** -> write (as today, ward#329). The credential stays root-held; the agent shells nothing privileged directly. - **otherwise an explicit `WARD_TIER` profile** - the dev-base CI image sets `read`, a native install leaves the default `admin`. Explicit and auditable: you can always see why a context is the tier it is. **No silent environment inference** (a misdetection would silently change privilege). ### 3. Native downshift: `ward ops --tier read|write` Native defaults to admin, but `--tier read|write` runs one call at a lower tier deliberately - to dry-run a dangerous op at the privilege an agent will actually have, or rehearse what CI sees. It **only ever lowers** privilege (never raises), and it works by shelling the named lower binary, so it is the same absence guarantee, not a flag that re-enables verbs. ## Distribution plan - **Publish all three tier binaries as release assets, per-arch** (linux amd64 + arm64), extending the existing `publish-kdl-write` job in `.forgejo/workflows/release.yml` into `publish-kdl-{read,write,admin}`. Same generator-renders-amd64, cross-build-arm64 shape (docs/homebrew-build.md, docs/ward-kdl.md). One release stamps `ward` + all three tiers at the **same version** - lockstep, since each tier embeds its own inherit-flattened guardfile and ward embeds its own opsassets. - **native admin**: arrives with the native install (brew tap formula, alongside `ward`), or as the admin release asset. Reconcile the vehicle with ward#246 (which scoped "tap ships three" before write took the release-asset route). - **container write**: downloaded by the entrypoint, shelled by the broker - **exists** (ward#329), grows from forgejo-only to all-area as the fan-out lands. - **CI read**: baked into the dev-base image (the CI surface is the image), with `WARD_TIER=read` set there. ## The native `ward ops` transition (coordinate with ward#278 Phase 2) Today native `ward ops <area>` runs the embedded curated monolith **in-process** (sync-ops-assets -> cmd/ward/opsassets, the specverb path). Decision 1 changes native `ops` to **shell `ward-kdl-admin`** instead. That is exactly ward#278's deferred Phase-2 question ("what does `ward ops forgejo` become - embed the admin tier? a default? split?"), now answered: **shell the admin tier binary, tier chosen by context.** When this lands, the in-process opsassets embedding can retire (or stay as an admin fallback). This is the one piece that touches the existing native surface, so it is the most review-sensitive. ## Build units (sequence after the gate) 1. `publish-kdl-{read,admin}` release-asset jobs, symmetric with `publish-kdl-write` (parallel-safe; can land early, ships a partial surface that grows like write already does). 2. `WARD_TIER` profile resolution in ward + the broker-socket precedence; dev-base image sets `read`. 3. `ward ops` shells the resolved tier binary; `--tier read|write` native downshift. 4. Native `ops` -> admin binary, retire/relegate the in-process opsassets embedding (ward#278 Phase 2, review-sensitive). 5. Native install vehicle for the admin binary (reconcile ward#246). ## Gate (why this waits) Nothing here ships until there is an all-area tier binary to shell: - **cli-guard#169** (override engine) - in flight. - **release + REF bump** of cli-guard into ward (the cross-module dance, ward#326). - **ward#278** (forgejo curated port) + the **per-area fan-out** - fills read/write/admin with real content across every area. Unit 1 (publish read/admin assets) is the only piece that is parallel-safe before the fan-out, exactly as write publishes a forgejo-only asset today. ## References ward#339 (epic), ward#329 (the broker this generalizes), ward#278 (the native-surface Phase-2 question this answers), ward#246 (tap-ships-three, vehicle to reconcile), ward#326 (the cross-module release dance), cli-guard#169 (the compile-time absence engine this mirrors at run time), docs/broker.md, docs/homebrew-build.md, docs/ward-kdl.md.
Member

Stays consult: design is settled but the body's own gate holds (do not dispatch until the #278 fan-out lands). Flip to headless when the gate clears - this is blocked-on-dependency, not undecided. Recorded by Claude Code (Fable) during the 2026-07-01 ward launch triage session with Kai.

Stays consult: design is settled but the body's own gate holds (do not dispatch until the #278 fan-out lands). Flip to headless when the gate clears - this is blocked-on-dependency, not undecided. Recorded by Claude Code (Fable) during the 2026-07-01 ward launch triage session with Kai.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#342
No description provided.