harden(explore): broker daemon main + entrypoint lifecycle + ship ward-kdl-write to dev-base (ward#329 unit B, on cli-guard#167) #331

Closed
opened 2026-06-25 10:04:35 +00:00 by coilysiren · 2 comments
Owner

Unit B: ward broker daemon main + entrypoint lifecycle + ship ward-kdl-write to dev-base

Second build unit for ward#329 (root credential broker). Foundation cli-guard#167
(pkg/broker + pkg/credseed) has landed on cli-guard main. This unit wires that core
into the ward container.

Decided in ward#329: policy core in cli-guard, lifecycle glue in ward. This is the glue.

Do this in order

  1. Bump the cli-guard pin. go get cli-guard to the release that contains
    pkg/broker + pkg/credseed (cli-guard auto-releases a new minor on push to main, so
    the tag with #167 is the floor). Update go.mod/go.sum. Confirm the packages import.

  2. Broker daemon main (ward side). A small root daemon that constructs
    broker.Server (from cli-guard) with:

    • a ward-kdl-write executor — the concrete Executor that shells
      ward-kdl-write ops forgejo <verb> for create/edit/comment and ward agent headless ...
      for dispatch #N, as root, with the token. No delete, no admin (the tier binary is the
      enforcement boundary; the broker's write-tier authz is defense in depth on top).
    • the root-only token read from a root-readable source (see Unit D for the eventual
      /etc/ward/forgejo-token 0600 file; for now read the token the entrypoint already has).
  3. Ship ward-kdl-write into the dev-base image. Today only ward + ward-kdl install;
    the tier binaries build to bin/ via make build-ward-kdl-forgejo-tiers (Makefile:36-56)
    but are not shipped. The broker can't shell a binary that isn't present. Add ward-kdl-write
    (at least) to the image. Authoring the Dockerfile/release change and pushing to main is in
    scope; CI republishes :latest as a consequence of the landed commit.

  4. Entrypoint lifecycle (the ward#319 socat shape). Start the broker daemon as root,
    before the setpriv drop
    , create its socket, group-own it to the agent gid at mode 0660,
    and export WARD_BROKER_SOCK=unix://<path> to the agent env. Mirror in both
    cmd/ward/containerassets/entrypoint.sh and the Go port cmd/ward/container_bootstrap.go
    (the ANTHROPIC scrub / socat bridge already establish where pre-drop root work goes:
    entrypoint.sh:114/:752, Go :247/:343). Gate on WARD_READONLY initially.

Explicitly NOT in this unit

  • Do NOT scrub FORGEJO_TOKEN. This unit stands the broker up in dual mode: the token
    is still present in the agent env, the broker serves, but nothing routes to it yet. Removing
    the token is the cutover (Unit D) and must come after the client rewiring (Unit C), or
    explore breaks. See the "piece 1 is the cutover, not the first step" correction on ward#329.
  • Do NOT rewire the clients. ops.go:109 / container.go:267 routing to WARD_BROKER_SOCK
    is Unit C, filed next once this lands.

Done-condition

Broker daemon builds, ships, starts before the drop, and serves on a group-readable socket in a
WARD_READONLY explore container, with WARD_BROKER_SOCK set in the agent env — verified by a
smoke check (socket exists, agent gid can connect, a hand-sent write-tier request round-trips and
an out-of-tier/delete request is rejected). Token still present (dual mode); no client routed yet.

Filed from a read-only explore session via capture-and-dispatch. See ward#329 for the full
design and the ordered unit list (A=cli-guard#167 landed; B=this; C=client rewiring; D=cutover).

## Unit B: ward broker daemon main + entrypoint lifecycle + ship ward-kdl-write to dev-base Second build unit for **ward#329** (root credential broker). Foundation **cli-guard#167** (`pkg/broker` + `pkg/credseed`) has landed on cli-guard `main`. This unit wires that core into the ward container. Decided in ward#329: **policy core in cli-guard, lifecycle glue in ward.** This is the glue. ### Do this in order 1. **Bump the cli-guard pin.** `go get` cli-guard to the release that contains `pkg/broker` + `pkg/credseed` (cli-guard auto-releases a new minor on push to main, so the tag with #167 is the floor). Update `go.mod`/`go.sum`. Confirm the packages import. 2. **Broker daemon `main` (ward side).** A small root daemon that constructs `broker.Server` (from cli-guard) with: - a **ward-kdl-write executor** — the concrete `Executor` that shells `ward-kdl-write ops forgejo <verb>` for create/edit/comment and `ward agent headless ...` for `dispatch #N`, as root, with the token. No delete, no admin (the tier binary is the enforcement boundary; the broker's write-tier authz is defense in depth on top). - the **root-only token** read from a root-readable source (see Unit D for the eventual `/etc/ward/forgejo-token` 0600 file; for now read the token the entrypoint already has). 3. **Ship `ward-kdl-write` into the dev-base image.** Today only `ward` + `ward-kdl` install; the tier binaries build to `bin/` via `make build-ward-kdl-forgejo-tiers` (`Makefile:36-56`) but are not shipped. The broker can't shell a binary that isn't present. Add `ward-kdl-write` (at least) to the image. Authoring the Dockerfile/release change and pushing to main is in scope; CI republishes `:latest` as a consequence of the landed commit. 4. **Entrypoint lifecycle (the ward#319 socat shape).** Start the broker daemon **as root, before the `setpriv` drop**, create its socket, group-own it to the agent gid at mode 0660, and export `WARD_BROKER_SOCK=unix://<path>` to the agent env. Mirror in both `cmd/ward/containerassets/entrypoint.sh` and the Go port `cmd/ward/container_bootstrap.go` (the ANTHROPIC scrub / socat bridge already establish where pre-drop root work goes: `entrypoint.sh:114`/`:752`, Go `:247`/`:343`). Gate on `WARD_READONLY` initially. ### Explicitly NOT in this unit - **Do NOT scrub `FORGEJO_TOKEN`.** This unit stands the broker up in **dual mode**: the token is still present in the agent env, the broker serves, but nothing routes to it yet. Removing the token is the **cutover (Unit D)** and must come after the client rewiring (Unit C), or explore breaks. See the "piece 1 is the cutover, not the first step" correction on ward#329. - **Do NOT rewire the clients.** `ops.go:109` / `container.go:267` routing to `WARD_BROKER_SOCK` is **Unit C**, filed next once this lands. ### Done-condition Broker daemon builds, ships, starts before the drop, and serves on a group-readable socket in a `WARD_READONLY` explore container, with `WARD_BROKER_SOCK` set in the agent env — verified by a smoke check (socket exists, agent gid can connect, a hand-sent write-tier request round-trips and an out-of-tier/delete request is rejected). Token still present (dual mode); no client routed yet. Filed from a read-only explore session via capture-and-dispatch. See ward#329 for the full design and the ordered unit list (A=cli-guard#167 landed; B=this; C=client rewiring; D=cutover).
Author
Owner

🔒 Reserved by ward agent --driver claude — container ward-ward-issue-331-claude-61a6ac48 on host 482e6a3fcee4 is carrying this issue (reserved 2026-06-25T10:04:40Z). 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-331-claude-61a6ac48` on host `482e6a3fcee4` is carrying this issue (reserved 2026-06-25T10:04:40Z). 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

Candid retro: the core — daemon + authorizer + executor over cli-guard#167's policy core — was the satisfying part; the socket round-trip test went green first try. The fights were all at the edges. Biggest surprise: ward-kdl-write authenticates from SSM, which an explore container doesn't have, so I had to override the write tier's inherited auth to read FORGEJO_TOKEN from env — I only trusted that after reading cli-guard's inherit singleton rules and confirming it in the flattened embedded guardfile. 'Ship to dev-base' really meant shipping through ward's own release-asset + entrypoint channel (the image bakes in no ward binaries), and since the generator only builds host-arch, arm64 cross-builds from the generator's cache module — I couldn't exercise that release job from in here, so it's my lowest-confidence piece. Pre-commit's 2-line-comment and 4000-char-doc caps forced real tightening; kdlfmt and trufflehog are both absent from the dev-base image, so I committed with SKIP for those two only (not --no-verify). Confidence: high on the Go + lifecycle, medium on the release job. Two follow-ups worth filing: TestAgentLaunchSilencesDockerNoiseWhenHeadless already fails on a clean tree (unrelated, stale want-list, looks like ward#322 fallout) so I left it; and the generated write-tier .md still describes SSM auth because the doc renderer reads the inherit root, not the override.

Candid retro: the core — daemon + authorizer + executor over cli-guard#167's policy core — was the satisfying part; the socket round-trip test went green first try. The fights were all at the edges. Biggest surprise: ward-kdl-write authenticates from SSM, which an explore container doesn't have, so I had to override the write tier's inherited auth to read FORGEJO_TOKEN from env — I only trusted that after reading cli-guard's inherit singleton rules and confirming it in the flattened embedded guardfile. 'Ship to dev-base' really meant shipping through ward's own release-asset + entrypoint channel (the image bakes in no ward binaries), and since the generator only builds host-arch, arm64 cross-builds from the generator's cache module — I couldn't exercise that release job from in here, so it's my lowest-confidence piece. Pre-commit's 2-line-comment and 4000-char-doc caps forced real tightening; kdlfmt and trufflehog are both absent from the dev-base image, so I committed with SKIP for those two only (not --no-verify). Confidence: high on the Go + lifecycle, medium on the release job. Two follow-ups worth filing: TestAgentLaunchSilencesDockerNoiseWhenHeadless already fails on a clean tree (unrelated, stale want-list, looks like ward#322 fallout) so I left it; and the generated write-tier .md still describes SSM auth because the doc renderer reads the inherit root, not the override.
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#331
No description provided.