harden(explore): dispatch-only Forgejo token instead of reusing the bot push token (ward#315) #318

Closed
opened 2026-06-25 08:05:26 +00:00 by coilyco-ops · 2 comments
Member

Deferred from the ward#315 explore dispatch landing.

ward agent explore keeps the coilyco-ops FORGEJO_TOKEN so the read-only session can file issues and dispatch a sibling warded #N. That token is the bot's full push credential. The push boundary is therefore policy-enforced, not credential-enforced:

  • the entrypoint drops this clone's git push wiring (revoke_push_credential), and
  • the composed read-only block tells the agent not to hand-build an authenticated push URL.

A misbehaving or jailbroken session could still reconstruct a push URL from the live token. The proper guard is a dispatch-only credential that can file issues + launch headless but cannot push to this clone, making the boundary credential-enforced.

Constraint: Forgejo tokens don't natively scope to "file + launch, no push", so this likely needs a separate bot identity or a narrowly-scoped token, not just a flag. Tracked separately so #315 can land the working (policy-enforced) path now.

Deferred from the ward#315 explore dispatch landing. `ward agent explore` keeps the coilyco-ops `FORGEJO_TOKEN` so the read-only session can file issues and dispatch a sibling `warded #N`. That token is the bot's **full push** credential. The push boundary is therefore **policy-enforced**, not credential-enforced: - the entrypoint drops this clone's git push wiring (`revoke_push_credential`), and - the composed read-only block tells the agent not to hand-build an authenticated push URL. A misbehaving or jailbroken session could still reconstruct a push URL from the live token. The proper guard is a **dispatch-only credential** that can file issues + launch headless but cannot push to this clone, making the boundary credential-enforced. Constraint: Forgejo tokens don't natively scope to "file + launch, no push", so this likely needs a separate bot identity or a narrowly-scoped token, not just a flag. Tracked separately so #315 can land the working (policy-enforced) path now.
Author
Member

Decision (Kai, 2026-06-25): go with a root broker, not a scoped Forgejo token. The token doesn't need to be scopeable because the agent never holds one.

Design: write the full bot token root-only (/etc/ward/forgejo-token, 0600 root:root) and scrub FORGEJO_TOKEN from the agent's env at the setpriv drop (the way ANTHROPIC_* already is). A root broker daemon on /run/ward-broker.sock (group-readable by the agent, same shape as the ward#319 socat bridge) is the only credentialed actor and exposes exactly two ops: file-issue and dispatch #N. For each it shells the capability binary as root - ward-kdl-dispatch ops forgejo issue create ... (no push leaf, structurally) and ward agent headless ... (which reseeds the sibling's env-file with the full token so the sibling still pushes in its own container).

Because the agent holds no credential, bare git push, curl to the API, and ward ops forgejo all just fail with no creds - the boundary is credential-enforced by absence, no deny-list needed. Closes the leak paths enumerated under ward#315.

Build order: (1) the ward-kdl-dispatch capability tier binary (low-risk, self-contained, the surface the broker runs); (2) the broker daemon + token confinement + env scrub + client rewiring (high-risk live-dispatch path, overlaps ward#267/#268 cli-guard extraction). Starting (1).

Decision (Kai, 2026-06-25): go with a **root broker**, not a scoped Forgejo token. The token doesn't need to be scopeable because the agent never holds one. Design: write the full bot token root-only (`/etc/ward/forgejo-token`, 0600 root:root) and scrub `FORGEJO_TOKEN` from the agent's env at the `setpriv` drop (the way `ANTHROPIC_*` already is). A root broker daemon on `/run/ward-broker.sock` (group-readable by the agent, same shape as the ward#319 socat bridge) is the only credentialed actor and exposes exactly two ops: file-issue and dispatch #N. For each it shells the capability binary as root - `ward-kdl-dispatch ops forgejo issue create ...` (no push leaf, structurally) and `ward agent headless ...` (which reseeds the sibling's env-file with the full token so the sibling still pushes in its own container). Because the agent holds no credential, bare `git push`, `curl` to the API, and `ward ops forgejo` all just fail with no creds - the boundary is credential-enforced by absence, no deny-list needed. Closes the leak paths enumerated under ward#315. Build order: (1) the `ward-kdl-dispatch` capability tier binary (low-risk, self-contained, the surface the broker runs); (2) the broker daemon + token confinement + env scrub + client rewiring (high-risk live-dispatch path, overlaps ward#267/#268 cli-guard extraction). Starting (1).
Author
Member

Superseded by ward#329. The design evolved past 'dispatch-only token': explore gets the existing ward-kdl-write tier (real explorers need write access wider than issue-authoring), and the boundary is enforced by a root broker holding the full token (agent holds no token, so push is impossible by absence) rather than by scoping a Forgejo token. Closing in favor of ward#329.

Superseded by ward#329. The design evolved past 'dispatch-only token': explore gets the existing **ward-kdl-write** tier (real explorers need write access wider than issue-authoring), and the boundary is enforced by a **root broker** holding the full token (agent holds no token, so push is impossible by absence) rather than by scoping a Forgejo token. Closing in favor of ward#329.
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#318
No description provided.