harden(explore): route forgejo + dispatch clients through the broker on WARD_BROKER_SOCK, dual mode (ward#329 unit C, on #331) #334

Closed
opened 2026-06-25 11:02:58 +00:00 by coilysiren · 2 comments
Owner

Unit C: route ward's forgejo + dispatch clients through the broker when WARD_BROKER_SOCK is set

Third build unit for ward#329 (root credential broker). Units A (cli-guard#167, pkg/broker

  • pkg/credseed) and B (ward#331, broker daemon + entrypoint lifecycle + ward-kdl-write in
    dev-base) have both landed on main. The broker now serves on a group-readable socket with
    WARD_BROKER_SOCK in the agent env, but nothing routes to it yet. This unit rewires the clients.

Still dual mode: FORGEJO_TOKEN stays in the agent env. The scrub is the cutover (Unit D)
and must come AFTER this lands, or explore breaks — see the correction on ward#329.

The two chokepoints to rewire

  1. cmd/ward/ops.go:109 forgejoTokenResolver (the ward ops forgejo <verb> path).
    When WARD_BROKER_SOCK is set, a write-tier verb must not resolve a local token and call
    the API directly. Instead forward the request to the broker via cli-guard's pkg/broker
    client; the broker runs ward-kdl-write ops forgejo <verb> as root and returns the result.
    When WARD_BROKER_SOCK is unset (normal non-explore containers), behavior is unchanged.

    • Decide cleanly what happens to read verbs and to out-of-tier verbs under the broker:
      reads can still go direct (no secret needed) or also route — pick the simpler, and reject
      delete/admin at the client with a clear "broker is write-tier only" message rather than a
      confusing API 403.
  2. cmd/ward/container.go:267 resolveForgejoToken (the dispatch / warded #N path).
    When WARD_BROKER_SOCK is set, a dispatch from inside a brokered explore box routes through
    the broker's dispatch #N request so the child env-file is seeded broker-side as root,
    not from a token the agent holds. When unset, the existing env→SSM path is unchanged.

Constraints

  • Dual mode is mandatory. Gate every change on WARD_BROKER_SOCK presence. Do not remove,
    unset, or stop reading FORGEJO_TOKEN in this unit — that is Unit D.
  • Prefer a single shared client helper over duplicating the broker round-trip at both
    chokepoints (the two resolvers are near-identical today; converge them as you rewire).
  • Keep the protocol versioned and the failure modes legible: a broker that is unreachable, a
    request rejected as out-of-tier, and a normal API error must each surface distinctly.

Done-condition

In a WARD_READONLY explore container: ward ops forgejo issue create/edit/comment succeeds
via the broker (verifiable: it works even though — anticipating Unit D — the agent's own
token would not), an out-of-tier delete is cleanly refused, and warded #N dispatches a
sibling whose token was seeded broker-side. In a normal (non-WARD_BROKER_SOCK) container,
nothing changes. Token still present in env (dual mode); the scrub is Unit D.

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

## Unit C: route ward's forgejo + dispatch clients through the broker when WARD_BROKER_SOCK is set Third build unit for **ward#329** (root credential broker). Units A (cli-guard#167, `pkg/broker` + `pkg/credseed`) and B (ward#331, broker daemon + entrypoint lifecycle + `ward-kdl-write` in dev-base) have both landed on `main`. The broker now **serves** on a group-readable socket with `WARD_BROKER_SOCK` in the agent env, but nothing routes to it yet. This unit rewires the clients. Still **dual mode**: `FORGEJO_TOKEN` stays in the agent env. The scrub is the cutover (Unit D) and must come AFTER this lands, or explore breaks — see the correction on ward#329. ### The two chokepoints to rewire 1. **`cmd/ward/ops.go:109` `forgejoTokenResolver`** (the `ward ops forgejo <verb>` path). When `WARD_BROKER_SOCK` is set, a write-tier verb must **not** resolve a local token and call the API directly. Instead forward the request to the broker via cli-guard's `pkg/broker` client; the broker runs `ward-kdl-write ops forgejo <verb>` as root and returns the result. When `WARD_BROKER_SOCK` is unset (normal non-explore containers), behavior is unchanged. - Decide cleanly what happens to **read** verbs and to **out-of-tier** verbs under the broker: reads can still go direct (no secret needed) or also route — pick the simpler, and reject delete/admin at the client with a clear "broker is write-tier only" message rather than a confusing API 403. 2. **`cmd/ward/container.go:267` `resolveForgejoToken`** (the dispatch / `warded #N` path). When `WARD_BROKER_SOCK` is set, a dispatch from inside a brokered explore box routes through the broker's `dispatch #N` request so the child env-file is seeded **broker-side as root**, not from a token the agent holds. When unset, the existing env→SSM path is unchanged. ### Constraints - **Dual mode is mandatory.** Gate every change on `WARD_BROKER_SOCK` presence. Do not remove, unset, or stop reading `FORGEJO_TOKEN` in this unit — that is Unit D. - Prefer a **single shared client helper** over duplicating the broker round-trip at both chokepoints (the two resolvers are near-identical today; converge them as you rewire). - Keep the protocol versioned and the failure modes legible: a broker that is unreachable, a request rejected as out-of-tier, and a normal API error must each surface distinctly. ### Done-condition In a `WARD_READONLY` explore container: `ward ops forgejo issue create/edit/comment` succeeds **via the broker** (verifiable: it works even though — anticipating Unit D — the agent's own token would not), an out-of-tier `delete` is cleanly refused, and `warded #N` dispatches a sibling whose token was seeded broker-side. In a normal (non-`WARD_BROKER_SOCK`) container, nothing changes. Token still present in env (dual mode); the scrub is Unit D. Filed from a read-only explore session via capture-and-dispatch. See ward#329 for the full design and ordered unit list (A=cli-guard#167 landed; B=ward#331 landed; C=this; D=cutover/scrub).
Author
Owner

🔒 Reserved by ward agent --driver claude — container ward-ward-issue-334-claude-2992a872 on host 482e6a3fcee4 is carrying this issue (reserved 2026-06-25T11:03:03Z). 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-334-claude-2992a872` on host `482e6a3fcee4` is carrying this issue (reserved 2026-06-25T11:03:03Z). 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: this one was more reading than writing. Units A/B left such a clean broker.Executor/Authorizer seam that the actual rewiring was small — most of the effort went into deciding where to intercept. The issue pointed at forgejoTokenResolver, but that only vends a token string; the real chokepoint for writes turned out to be the specverb Wrap, classifying each leaf by its verb.Spec.Name tail (issue create/edit/comment/close/reopen → broker, reads + --dry-run direct, everything else refused). That clicked once I found it.

The dispatch half (#2) fought back. --env-file is read agent-side by docker, so in dual mode there's no honest way to seed a child token the agent literally can't read without the broker performing the launch itself. I settled on the broker vending the seed over the dispatch #N response and falling back to env→SSM on any failure — faithful to "seeded broker-side" and never blocks a launch, but true agent-never-touches-it isolation genuinely belongs to Unit D's scrub, which will have to revisit how dispatch is served. Worth flagging now.

Confidence: high on #1 (unit-tested end-to-end over a live in-process socket); medium on #2 — only the in-process server exercises it, never a real warded #N from a brokered box (no docker in here to try it). Two rough edges worth a follow-up: stuffing a raw token into Result.Detail is a protocol smell that wants its own field, and TestAgentLaunchSilencesDockerNoiseWhenHeadless was already red on the base before I touched anything (shared-logPath count drift — unrelated, but someone should look).

Retrospective: this one was more *reading* than writing. Units A/B left such a clean `broker.Executor`/`Authorizer` seam that the actual rewiring was small — most of the effort went into deciding *where* to intercept. The issue pointed at `forgejoTokenResolver`, but that only vends a token string; the real chokepoint for writes turned out to be the specverb `Wrap`, classifying each leaf by its `verb.Spec.Name` tail (issue create/edit/comment/close/reopen → broker, reads + `--dry-run` direct, everything else refused). That clicked once I found it. The dispatch half (#2) fought back. `--env-file` is read agent-side by docker, so in dual mode there's no honest way to seed a child token the agent literally can't read *without the broker performing the launch itself*. I settled on the broker vending the seed over the `dispatch #N` response and falling back to env→SSM on any failure — faithful to "seeded broker-side" and never blocks a launch, but true agent-never-touches-it isolation genuinely belongs to Unit D's scrub, which will have to revisit how dispatch is served. Worth flagging now. Confidence: high on #1 (unit-tested end-to-end over a live in-process socket); medium on #2 — only the in-process server exercises it, never a real `warded #N` from a brokered box (no docker in here to try it). Two rough edges worth a follow-up: stuffing a raw token into `Result.Detail` is a protocol smell that wants its own field, and `TestAgentLaunchSilencesDockerNoiseWhenHeadless` was already red on the base before I touched anything (shared-logPath count drift — unrelated, but someone should look).
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#334
No description provided.