design(explore): explore can't dispatch headless work - revoke is too broad, no dispatch channel #315

Closed
opened 2026-06-25 06:55:47 +00:00 by coilyco-ops · 8 comments
Member

The gap

Dispatching headless agents is the point of ward agent explore: you read and
reason in a safe, no-direct-push session, and the natural product of that reasoning is
commissioned work - file the issue, dispatch the headless fix. An explore session that
can't dispatch is a notepad, not an agent.

But an explore container has every outbound channel removed, so the "finish the task"
finish-line (file + dispatch) is structurally unreachable from inside it:

  1. No push credential - revoke_push_credential nukes the whole FORGEJO_TOKEN and
    the git helper, not just push-to-this-clone. Killing the push to the explore clone is
    correct. Killing the token that would file an issue or seed a dispatch throws out the
    capability that justifies the surface.
  2. No docker socket - no /var/run/docker.sock, so the container can't spawn a sibling
    warded #N headless run.
  3. No writable host mount - /tmp is container-local, host maps are read-only, so the
    box can't even hand a launcher file back to the operator. Only conversation text leaves.

Why this is the bug under the bug

"Read-only" was only ever meant to mean this clone can't push to its own remote -
nothing leaks out via this box's git tree. It was never meant to mean explore can't
commission agents that do their own isolated feature -> merge -> push in their own
container with their own credential and lifecycle. Those are fully compatible: a
dispatched headless run is sealed independently of the explore box.

The current build conflates the two and revokes too broadly.

What a fix looks like

  • Scope the revoke to push-to-this-clone while preserving a dispatch-only
    capability: a token that can file issues + launch headless carries but cannot push the
    explore branch.
  • Provide an actual dispatch path into the container (docker socket or a wired relay)
    so warded #N works from inside explore.
  • Rewrite docs/agent-explore.md: line 12 sells explore as "a hard guarantee that
    nothing leaves the box," which anchors the wrong mental model. If dispatch is the
    point, reframe as "nothing leaves this clone; commissioned work still ships."

Decision needed (why this isn't auto-fixed)

Wiring a forgejo token + docker socket into a read-only box is security-sensitive and
needs a deliberate trust-boundary design (dispatch-only credential scoping, socket
exposure). Filing for Kai's call rather than headless-dispatching it blind.

Found in a warded explore session diagnosing the claude-onboarding seed gap (sibling
issue); the dispatch wall is why that diagnosis had to be carried out by hand.

## The gap Dispatching headless agents is the **point** of `ward agent explore`: you read and reason in a safe, no-direct-push session, and the natural product of that reasoning is commissioned work - file the issue, dispatch the headless fix. An explore session that can't dispatch is a notepad, not an agent. But an explore container has every outbound channel removed, so the "finish the task" finish-line (file + dispatch) is structurally unreachable from inside it: 1. **No push credential** - `revoke_push_credential` nukes the whole `FORGEJO_TOKEN` and the git helper, not just push-to-this-clone. Killing the push to the explore clone is correct. Killing the token that would file an issue or seed a dispatch throws out the capability that justifies the surface. 2. **No docker socket** - no `/var/run/docker.sock`, so the container can't spawn a sibling `warded #N` headless run. 3. **No writable host mount** - `/tmp` is container-local, host maps are read-only, so the box can't even hand a launcher file back to the operator. Only conversation text leaves. ## Why this is the bug under the bug "Read-only" was only ever meant to mean **this clone can't push to its own remote** - nothing leaks out via this box's git tree. It was never meant to mean explore can't commission agents that do their own isolated feature -> merge -> push in their own container with their own credential and lifecycle. Those are fully compatible: a dispatched headless run is sealed independently of the explore box. The current build conflates the two and revokes too broadly. ## What a fix looks like - Scope the revoke to **push-to-this-clone** while preserving a **dispatch-only** capability: a token that can file issues + launch headless carries but cannot push the explore branch. - Provide an actual dispatch path into the container (docker socket or a wired relay) so `warded #N` works from inside explore. - Rewrite `docs/agent-explore.md`: line 12 sells explore as "a hard guarantee that nothing leaves the box," which anchors the wrong mental model. If dispatch is the point, reframe as "nothing leaves *this clone*; commissioned work still ships." ## Decision needed (why this isn't auto-fixed) Wiring a forgejo token + docker socket into a read-only box is security-sensitive and needs a deliberate trust-boundary design (dispatch-only credential scoping, socket exposure). Filing for Kai's call rather than headless-dispatching it blind. Found in a `warded explore` session diagnosing the claude-onboarding seed gap (sibling issue); the dispatch wall is why that diagnosis had to be carried out by hand.
Author
Member

🛫 ward pre-flight: NO-GO

ward agent headless --driver claude ran a pre-flight feasibility read on this issue before detaching a fire-and-forget run, and the agent judged it NO-GO - it should not be carried unattended until a human weighs in.

security-sensitive trust-boundary design (dispatch-only token scoping + docker socket/relay exposure) that the issue itself explicitly reserves for Kai's decision, not an implementable spec.

No container was launched. Review the issue (clarify the scope, resolve the unknown, or split it), then re-dispatch - ward agent headless --driver claude <ref> --no-preflight skips this gate once you've decided it's good to go.

full pre-flight read

This issue is explicitly a design/decision filing, not an implementation ticket: its own "Decision needed" section says wiring a forgejo token plus docker socket into a read-only box is security-sensitive, requires a deliberate trust-boundary design (dispatch-only credential scoping, socket exposure), and was filed "for Kai's call rather than headless-dispatching it blind." The core unknown is the trust boundary itself - how to scope a dispatch-only credential that can file/launch but not push, and whether to expose a docker socket vs. a wired relay - which is precisely the costly, hard-to-undo security fork that an unattended agent should not pick on its own. The repo is correct (this is ward's explore surface), so it's not a misfile, but committing a credential-scoping design to main without review is exactly the irreversible-blast-radius wall.

NO-GO: security-sensitive trust-boundary design (dispatch-only token scoping + docker socket/relay exposure) that the issue itself explicitly reserves for Kai's decision, not an implementable spec.


Posted automatically by ward agent headless --driver claude pre-flight (ward#147, ward#149).

— Claude (she/her), via ward agent

### 🛫 ward pre-flight: NO-GO `ward agent headless --driver claude` ran a pre-flight feasibility read on this issue before detaching a fire-and-forget run, and the agent judged it **NO-GO** - it should not be carried unattended until a human weighs in. > security-sensitive trust-boundary design (dispatch-only token scoping + docker socket/relay exposure) that the issue itself explicitly reserves for Kai's decision, not an implementable spec. No container was launched. Review the issue (clarify the scope, resolve the unknown, or split it), then re-dispatch - `ward agent headless --driver claude <ref> --no-preflight` skips this gate once you've decided it's good to go. <details><summary>full pre-flight read</summary> This issue is explicitly a design/decision filing, not an implementation ticket: its own "Decision needed" section says wiring a forgejo token plus docker socket into a read-only box is security-sensitive, requires a deliberate trust-boundary design (dispatch-only credential scoping, socket exposure), and was filed "for Kai's call rather than headless-dispatching it blind." The core unknown is the trust boundary itself - how to scope a dispatch-only credential that can file/launch but not push, and whether to expose a docker socket vs. a wired relay - which is precisely the costly, hard-to-undo security fork that an unattended agent should not pick on its own. The repo is correct (this is ward's explore surface), so it's not a misfile, but committing a credential-scoping design to main without review is exactly the irreversible-blast-radius wall. NO-GO: security-sensitive trust-boundary design (dispatch-only token scoping + docker socket/relay exposure) that the issue itself explicitly reserves for Kai's decision, not an implementable spec. </details> --- Posted automatically by `ward agent headless --driver claude` pre-flight (ward#147, ward#149). <!-- ward-preflight-nogo --> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Owner

for now we can "just" mount forgejo + docker socket - I'll circle back to something better later

for now we can "just" mount forgejo + docker socket - I'll circle back to something better later
Author
Member

🛫 ward pre-flight: NO-GO

ward agent headless --driver claude ran a pre-flight feasibility read on this issue before detaching a fire-and-forget run, and the agent judged it NO-GO - it should not be carried unattended until a human weighs in.

security-sensitive sandbox/credential boundary loosening that should have a human review the diff before it lands on main, and the dispatch-still-no-push invariant is hard to verify unattended.

No container was launched. Review the issue (clarify the scope, resolve the unknown, or split it), then re-dispatch - ward agent headless --driver claude <ref> --no-preflight skips this gate once you've decided it's good to go.

full pre-flight read

The decision blocker is resolved - Kai's comment explicitly picks the approach ("just mount forgejo + docker socket - I'll circle back later"), so this is no longer an open design fork, and the repo is plainly ward. The main risk is that this change deliberately loosens a security sandbox: it has to scope the revoke down to push-to-this-clone while preserving a dispatch-only token AND expose the docker socket, and getting the credential scoping subtly wrong (leaving real push capability, or over-exposing the socket) silently defeats the guarantee the box exists to provide. That correctness is also hard to verify unattended - confirming "dispatch works but the explore branch genuinely still can't push" needs a working docker-in-docker dispatch path I can't reliably exercise in an ephemeral container. A loosening of a trust boundary landing on main fire-and-forget, with no human eyeing the diff, is exactly the wall worth a human even when the direction is chosen.

NO-GO: security-sensitive sandbox/credential boundary loosening that should have a human review the diff before it lands on main, and the dispatch-still-no-push invariant is hard to verify unattended.


Posted automatically by ward agent headless --driver claude pre-flight (ward#147, ward#149).

— Claude (she/her), via ward agent

### 🛫 ward pre-flight: NO-GO `ward agent headless --driver claude` ran a pre-flight feasibility read on this issue before detaching a fire-and-forget run, and the agent judged it **NO-GO** - it should not be carried unattended until a human weighs in. > security-sensitive sandbox/credential boundary loosening that should have a human review the diff before it lands on main, and the dispatch-still-no-push invariant is hard to verify unattended. No container was launched. Review the issue (clarify the scope, resolve the unknown, or split it), then re-dispatch - `ward agent headless --driver claude <ref> --no-preflight` skips this gate once you've decided it's good to go. <details><summary>full pre-flight read</summary> The decision blocker is resolved - Kai's comment explicitly picks the approach ("just mount forgejo + docker socket - I'll circle back later"), so this is no longer an open design fork, and the repo is plainly ward. The main risk is that this change deliberately loosens a security sandbox: it has to scope the revoke down to push-to-this-clone while preserving a dispatch-only token AND expose the docker socket, and getting the credential scoping subtly wrong (leaving real push capability, or over-exposing the socket) silently defeats the guarantee the box exists to provide. That correctness is also hard to verify unattended - confirming "dispatch works but the explore branch genuinely still can't push" needs a working docker-in-docker dispatch path I can't reliably exercise in an ephemeral container. A loosening of a trust boundary landing on main fire-and-forget, with no human eyeing the diff, is exactly the wall worth a human even when the direction is chosen. NO-GO: security-sensitive sandbox/credential boundary loosening that should have a human review the diff before it lands on main, and the dispatch-still-no-push invariant is hard to verify unattended. </details> --- Posted automatically by `ward agent headless --driver claude` pre-flight (ward#147, ward#149). <!-- ward-preflight-nogo --> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Member

Decision: host-side relay (not token-in-box, not socket-in-box)

Picked the host-side relay model. Rejected the alternatives:

  • Dispatch-only scoped token in the box - Forgejo tokens don't scope to "file issues + launch, but no push to this one clone"; it would need a separate bot identity and still leaves a live credential inside a box whose whole value proposition is "no credential inside the box."
  • Docker socket in the box - socket access is root-equivalent on the host; largest blast radius, defeats the read-only guarantee.

Why the relay fits cleanly

The explore box keeps zero Forgejo token, zero git helper, zero docker socket - the hard read-only guarantee on the clone is untouched. What the box does still have is plain network egress (the revoke_push_credential step strips the git credential, not the network). The relay rides that egress:

explore box ──POST dispatch-request envelope──> otel-a2a-relay
                                                      │ verifies (handwritten URLs refused)
host watcher  ◀──────────────────────────────────────┘
   │ runs `warded #N` OUTSIDE the box, with real creds, in its own sealed container
   ▼
commissioned headless run: own clone, own credential, own lifecycle

A dispatched run was always sealed independently of the explore box, so this is fully compatible with read-only: nothing leaves this clone; commissioned work still ships.

Build order (cross-repo, security-sensitive - a deliberate build, not a blind dispatch)

  1. otel-a2a-relay - add a dispatch-request envelope type + endpoint: { target_issue_ref, dispatch_cmd: "warded #N", optional new-issue payload }, verified the same way the relay already refuses handwritten URLs.
  2. Host watcher - a small daemon where warded + creds live; polls/subscribes the relay, verifies the envelope, runs warded #N (or files the issue then dispatches). Rollout via ansible, not baked into ward.
  3. ward / explore-box client - when WARD_READONLY=1, a warded dispatch #N (or the existing finish-the-task path) emits an envelope to the relay instead of attempting docker/push. Small client wiring; the box never gains a credential.

Doc fix (from this issue)

Rewrite docs/agent-explore.md line ~12: "a hard guarantee that nothing leaves the box" anchors the wrong model. Reframe as "nothing leaves this clone; commissioned work still ships via the relay."

Decision recorded from a warded explore papercut sweep. Sibling papercuts dispatched the same night: ward#299 (read-only pre-push message), ward#236 (front-load context), ward#291 (push-landing verify).

## Decision: host-side relay (not token-in-box, not socket-in-box) Picked the **host-side relay** model. Rejected the alternatives: - **Dispatch-only scoped token in the box** - Forgejo tokens don't scope to "file issues + launch, but no push to this one clone"; it would need a separate bot identity and still leaves a live credential inside a box whose whole value proposition is "no credential inside the box." - **Docker socket in the box** - socket access is root-equivalent on the host; largest blast radius, defeats the read-only guarantee. ## Why the relay fits cleanly The explore box keeps **zero** Forgejo token, **zero** git helper, **zero** docker socket - the hard read-only guarantee on the clone is untouched. What the box *does* still have is plain **network egress** (the `revoke_push_credential` step strips the git credential, not the network). The relay rides that egress: ``` explore box ──POST dispatch-request envelope──> otel-a2a-relay │ verifies (handwritten URLs refused) host watcher ◀──────────────────────────────────────┘ │ runs `warded #N` OUTSIDE the box, with real creds, in its own sealed container ▼ commissioned headless run: own clone, own credential, own lifecycle ``` A dispatched run was always sealed independently of the explore box, so this is fully compatible with read-only: nothing leaves *this clone*; commissioned work still ships. ## Build order (cross-repo, security-sensitive - a deliberate build, not a blind dispatch) 1. **otel-a2a-relay** - add a `dispatch-request` envelope type + endpoint: `{ target_issue_ref, dispatch_cmd: "warded #N", optional new-issue payload }`, verified the same way the relay already refuses handwritten URLs. 2. **Host watcher** - a small daemon where `warded` + creds live; polls/subscribes the relay, verifies the envelope, runs `warded #N` (or files the issue then dispatches). Rollout via ansible, not baked into ward. 3. **ward / explore-box client** - when `WARD_READONLY=1`, a `warded dispatch #N` (or the existing finish-the-task path) emits an envelope to the relay instead of attempting docker/push. Small client wiring; the box never gains a credential. ## Doc fix (from this issue) Rewrite `docs/agent-explore.md` line ~12: "a hard guarantee that nothing leaves the box" anchors the wrong model. Reframe as **"nothing leaves this clone; commissioned work still ships via the relay."** Decision recorded from a `warded explore` papercut sweep. Sibling papercuts dispatched the same night: ward#299 (read-only pre-push message), ward#236 (front-load context), ward#291 (push-landing verify).
Author
Member

Dispatch channel is now wired across both repos: (1) revoke scoped to push-to-this-clone with FORGEJO_TOKEN kept for dispatch + the docker socket mounted (on main); (2) socket grant via the socket's owning group, plus a root socat bridge for root:root sockets (ward#319, on main); (3) writeTokenEnvFile prefers an env token so in-container warded #N resolves creds without host SSM; (4) docker CLI + socat in the dev-base image (agentic-os 207f5b7). Two follow-ups remain for fully-functional dispatch: a dispatch-only credential (the kept token is the same bot token, so the push boundary is policy-enforced not credential-enforced), and host-path translation for the sibling's mounts (the explore container's tmp/cwd paths don't exist on the host daemon a sibling docker run targets).

Dispatch channel is now wired across both repos: (1) revoke scoped to push-to-this-clone with FORGEJO_TOKEN kept for dispatch + the docker socket mounted (on main); (2) socket grant via the socket's owning group, plus a root `socat` bridge for `root:root` sockets (ward#319, on main); (3) `writeTokenEnvFile` prefers an env token so in-container `warded #N` resolves creds without host SSM; (4) docker CLI + socat in the dev-base image (agentic-os `207f5b7`). Two follow-ups remain for fully-functional dispatch: a **dispatch-only credential** (the kept token is the same bot token, so the push boundary is policy-enforced not credential-enforced), and **host-path translation** for the sibling's mounts (the explore container's tmp/cwd paths don't exist on the host daemon a sibling `docker run` targets).
Owner

dispatch-only credential

^ no dont do this

host-path translation

^ yes do this

> dispatch-only credential ^ no dont do this > host-path translation ^ yes do this
Author
Member

DECISION - broker-mediated dispatch blessed: explore files the issue via the ward-kdl-write broker surface (#329) and labels it headless; the host-side director/backlog loop (#324/#493) picks it up. No direct dispatch channel, no token in the container. Flipped headless. Recorded by Claude Code (Fable) during the 2026-07-01 ward launch triage session with Kai.

DECISION - broker-mediated dispatch blessed: explore files the issue via the ward-kdl-write broker surface (#329) and labels it headless; the host-side director/backlog loop (#324/#493) picks it up. No direct dispatch channel, no token in the container. Flipped headless. Recorded by Claude Code (Fable) during the 2026-07-01 ward launch triage session with Kai.
Owner

there really isnt a slot for "explore" anymore

there really isnt a slot for "explore" anymore
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#315
No description provided.