Restore brokered Forgejo access for read-only directors after the Go-bootstrap migration #1521

Closed
opened 2026-07-23 05:24:10 +00:00 by coilyco-ops · 4 comments
Member

Live symptom

A current read-only director surface is running Ward v0.789.0 with:

WARD_READONLY=1
FORGEJO_TOKEN=<present>
WARD_BROKER_SOCK=<unset>
/run/ward/broker.sock=<absent>

When the bot PAT rotated during the session, direct ward ops forgejo ... calls began failing with Forgejo 401 access token does not exist. Resolving the current /forgejo/coilyco-ops/api-token value from SSM and injecting it into the same command made it succeed immediately. The failure is therefore the frozen credential path in the surface, not Forgejo availability or repository authorization.

Current main (v0.792.0) has the same lifecycle gap.

Regression trace

The intended boundary did exist:

  • 7a53fe0 (ward#329 Unit B) added start_broker to cmd/ward/containerassets/entrypoint.sh: start ward container broker as root, wait for its group-readable socket, isolate its log, and export WARD_BROKER_SOCK before dropping to the agent.
  • 2dac155 (ward#334 Unit C) made the Forgejo write client route through that socket when it is present.

Then 613c9bb (ward#649, refactor: delegate container config to bootstrap) deleted the shell implementation while shrinking the entrypoint by 1,096 lines. The equivalent lifecycle was not ported to cmd/ward/container_bootstrap.go:

  • current source still ships cmd/ward/broker.go, broker_exec.go, and broker_client.go;
  • no production bootstrap call starts ward container broker or exports WARD_BROKER_SOCK;
  • revokePushCredential explicitly keeps FORGEJO_TOKEN in the dropped agent environment;
  • the generated director doctrine likewise says the full bot token is present.

That leaves the broker daemon/client effectively dead on director startup and silently reverts the closed ward#329 boundary. Ward#608's final raw-token cutover is also closed, but current source does not implement it.

Impact

  1. The dropped read-only agent holds the full bot credential, so the promised credential-enforced no-push boundary is absent.
  2. Forgejo reads and writes use a container-lifetime token snapshot and strand a long-lived director after rotation.
  3. Broker authorization and write-tier routing are bypassed because their activation variable is never set.
  4. Docs describe a brokered director while the generated runtime doctrine and actual environment expose the raw-token fallback.

Do

Restore the read-only director's brokered Forgejo path in the Go bootstrap, preserving the previously decided ward#329/ward#334 boundary rather than adding another client path:

  • start the credential broker during the root bootstrap phase;
  • group-own and permission its socket for the dropped agent, keep daemon output off the shared TUI, and export WARD_BROKER_SOCK into the agent process;
  • route the Forgejo operations a director is instructed to use through an authorized broker path;
  • remove the full raw FORGEJO_TOKEN from the dropped read-only agent once broker reads/writes/dispatch no longer depend on it;
  • make credential rotation behavior explicit and recoverable. A director must either acquire the current broker credential without handling the secret or be automatically recycled with a clear reason; a raw Forgejo 401 from a silently frozen env snapshot is not an acceptable surface.

Do not weaken the read-only clone's push guard and do not expose token values in argv, logs, audit rows, issue text, or agent-visible files.

Regression coverage

Add a bootstrap-level test that would have failed on 613c9bb:

  • WARD_READONLY=1 starts the broker and hands a reachable socket to the dropped agent;
  • the agent environment does not contain the raw bot PAT;
  • an allowed issue read/write succeeds through the broker and an out-of-tier mutation is refused;
  • a simulated credential change from token A to token B has the documented recovery behavior without asking the agent to fetch or print B;
  • non-read-only engineer/reaper credential behavior remains unchanged;
  • generated doctrine and broker docs describe the same actual boundary.

References

ward#329, ward#334, ward#608, ward#649; regression commit 613c9bb.

## Live symptom A current read-only director surface is running Ward `v0.789.0` with: ```text WARD_READONLY=1 FORGEJO_TOKEN=<present> WARD_BROKER_SOCK=<unset> /run/ward/broker.sock=<absent> ``` When the bot PAT rotated during the session, direct `ward ops forgejo ...` calls began failing with Forgejo `401 access token does not exist`. Resolving the current `/forgejo/coilyco-ops/api-token` value from SSM and injecting it into the same command made it succeed immediately. The failure is therefore the frozen credential path in the surface, not Forgejo availability or repository authorization. Current `main` (`v0.792.0`) has the same lifecycle gap. ## Regression trace The intended boundary did exist: - `7a53fe0` (ward#329 Unit B) added `start_broker` to `cmd/ward/containerassets/entrypoint.sh`: start `ward container broker` as root, wait for its group-readable socket, isolate its log, and export `WARD_BROKER_SOCK` before dropping to the agent. - `2dac155` (ward#334 Unit C) made the Forgejo write client route through that socket when it is present. Then `613c9bb` (ward#649, `refactor: delegate container config to bootstrap`) deleted the shell implementation while shrinking the entrypoint by 1,096 lines. The equivalent lifecycle was not ported to `cmd/ward/container_bootstrap.go`: - current source still ships `cmd/ward/broker.go`, `broker_exec.go`, and `broker_client.go`; - no production bootstrap call starts `ward container broker` or exports `WARD_BROKER_SOCK`; - `revokePushCredential` explicitly keeps `FORGEJO_TOKEN` in the dropped agent environment; - the generated director doctrine likewise says the full bot token is present. That leaves the broker daemon/client effectively dead on director startup and silently reverts the closed ward#329 boundary. Ward#608's final raw-token cutover is also closed, but current source does not implement it. ## Impact 1. The dropped read-only agent holds the full bot credential, so the promised credential-enforced no-push boundary is absent. 2. Forgejo reads and writes use a container-lifetime token snapshot and strand a long-lived director after rotation. 3. Broker authorization and write-tier routing are bypassed because their activation variable is never set. 4. Docs describe a brokered director while the generated runtime doctrine and actual environment expose the raw-token fallback. ## Do Restore the read-only director's brokered Forgejo path in the Go bootstrap, preserving the previously decided ward#329/ward#334 boundary rather than adding another client path: - start the credential broker during the root bootstrap phase; - group-own and permission its socket for the dropped agent, keep daemon output off the shared TUI, and export `WARD_BROKER_SOCK` into the agent process; - route the Forgejo operations a director is instructed to use through an authorized broker path; - remove the full raw `FORGEJO_TOKEN` from the dropped read-only agent once broker reads/writes/dispatch no longer depend on it; - make credential rotation behavior explicit and recoverable. A director must either acquire the current broker credential without handling the secret or be automatically recycled with a clear reason; a raw Forgejo 401 from a silently frozen env snapshot is not an acceptable surface. Do not weaken the read-only clone's push guard and do not expose token values in argv, logs, audit rows, issue text, or agent-visible files. ## Regression coverage Add a bootstrap-level test that would have failed on `613c9bb`: - `WARD_READONLY=1` starts the broker and hands a reachable socket to the dropped agent; - the agent environment does not contain the raw bot PAT; - an allowed issue read/write succeeds through the broker and an out-of-tier mutation is refused; - a simulated credential change from token A to token B has the documented recovery behavior without asking the agent to fetch or print B; - non-read-only engineer/reaper credential behavior remains unchanged; - generated doctrine and broker docs describe the same actual boundary. ## References ward#329, ward#334, ward#608, ward#649; regression commit `613c9bb`.
Author
Member

WARDED_WORKFLOW: blocked 🛑

details

manual close/update snapshot at 2026-07-23T05:38:42Z is newer than the latest ward acknowledgement at 2026-07-23T05:24:29Z

This action is blocked until the feedback is visibly acknowledged.

— Codex, via ward agent

WARDED_WORKFLOW: blocked 🛑 <details><summary>details</summary> manual close/update snapshot at 2026-07-23T05:38:42Z is newer than the latest ward acknowledgement at 2026-07-23T05:24:29Z This action is blocked until the feedback is visibly acknowledged. </details> <!-- ward-agent-signature --> — Codex, via `ward agent`
Author
Member

WARDED_WORKFLOW: blocked 🛑

details

human comment by @coilyco-ops at 2026-07-23T05:39:54Z is newer than the latest ward acknowledgement at 2026-07-23T05:39:44Z

This action is blocked until the feedback is visibly acknowledged.

— Codex, via ward agent

WARDED_WORKFLOW: blocked 🛑 <details><summary>details</summary> human comment by @coilyco-ops at 2026-07-23T05:39:54Z is newer than the latest ward acknowledgement at 2026-07-23T05:39:44Z This action is blocked until the feedback is visibly acknowledged. </details> <!-- ward-agent-signature --> — Codex, via `ward agent`
Author
Member

WARDED_WORKFLOW: #1523

details

Review gate: intentionally skipped because the temporary Ward default is pending brokered QA.

workflow: pull-request-and-merge; review summary: intentionally skipped (temporary Ward default pending brokered QA)

Felt: contained lifecycle restoration. Confidence: high. Surprise: merge required explicit acknowledgement of its own PR snapshot. Follow-up: exercise a live director against the next rotated bot credential.

WARDED_WORKFLOW: https://forgejo.coilysiren.me/coilyco-flight-deck/ward/pulls/1523 <details><summary>details</summary> Review gate: intentionally skipped because the temporary Ward default is pending brokered QA. workflow: pull-request-and-merge; review summary: intentionally skipped (temporary Ward default pending brokered QA) Felt: contained lifecycle restoration. Confidence: high. Surprise: merge required explicit acknowledgement of its own PR snapshot. Follow-up: exercise a live director against the next rotated bot credential. </details> <!-- ward-agent-signature -->
Author
Member

WARDED_WORKFLOW: reservation-released

release details

Run finished with WARDED_WORKFLOW: https://forgejo.coilysiren.me/coilyco-flight-deck/ward/pulls/1523.

ward container reap released container engineer-codex-ward-1521 (--harness codex): the terminal outcome supersedes the reservation, so a later redispatch no longer needs --override-reservation.

Outcome summary: #1523

— Codex, via ward agent

<!-- ward-agent-reservation-released --> WARDED_WORKFLOW: reservation-released <details><summary>release details</summary> Run finished with `WARDED_WORKFLOW: https://forgejo.coilysiren.me/coilyco-flight-deck/ward/pulls/1523`. `ward container reap` released container `engineer-codex-ward-1521` (`--harness codex`): the terminal outcome supersedes the reservation, so a later redispatch no longer needs `--override-reservation`. **Outcome summary:** https://forgejo.coilysiren.me/coilyco-flight-deck/ward/pulls/1523 </details> <!-- ward-agent-signature --> — Codex, via `ward agent`
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#1521
No description provided.