Mount ~/.ward/agent-logs read-only into the director surface so directors can read run logs without a docker socket #525

Closed
opened 2026-07-02 08:52:27 +00:00 by coilysiren · 2 comments
Owner

Want

Mount the host agent-log drain read-only into the director read-only surface session, so a director can read a run's drained console.log / transcript.jsonl / meta.json without a docker socket. This is the safe answer to "directors can't read run logs": the surface deliberately has no docker socket (dispatch goes via the TCP broker), because the socket is host-root-equivalent - docker inspect <sibling> alone dumps Config.Env with every injected secret (FORGEJO_TOKEN, WARD_CLAUDE_CREDS_B64, AWS/Anthropic creds), and docker exec is full sibling takeover. A read-only bind of the already-drained artifacts gives log visibility with none of that surface.

Where

  • Host source: agentLogsDir() (cmd/ward/agent_log_drain.go:67 - $HOME/<AppDir>/agent-logs).
  • Mount seam: leastAccessMounts / mountOpts in cmd/ward/container_compute.go. Add a read-only opt-in exactly like AWSHome/containerAWSMount - a new mountOpts.AgentLogsDir that appends {Source: agentLogsDir(), Target: <containerAgentLogsMount, e.g. /opt/ward-agent-logs>, ReadOnly: true}. Define the target-path constant beside containerAWSMount.
  • Scope: set the opt-in only on the director-surface bring-up path (directorSurface / directorSurfaceArgv, cmd/ward/agent_director_heartbeat.go). Do not add it to the shared least-access default - engineers/advisors must not get it (a cross-run log read with far less justification). The surface session stays read-only, so the mount is :ro.

Residual to keep in mind (default: accept, per Kai)

The drained console.log and transcript.jsonl are raw - only the Slice-2 SigNoz envelope export is redacted (docs/agent-observability.md), not the on-disk artifacts. So a whole-dir read-only mount lets a director read every past run's raw stdout + session, which can contain secrets that leaked to stdout (a Read of a .env, a token in a traceback) and other runs' content. Kai accepted this: these live host-local unredacted already, same trust tier as ~/.ward/audit/, and read-only + no-socket removes the real escalation vector. A tighter option, if wanted later, is to mount only the meta.jsons (the secret-free allowlisted artifact) - note it, default to the full read-only dir mount Kai asked for.

Acceptance

  • A director read-only surface session sees agentLogsDir() at a fixed in-container path, read-only.
  • An engineer/advisor run does not get the mount.
  • Writing to the mount from the surface fails (:ro).
  • A short note in docs/agent-director.md (or agent-observability.md) that the surface reads the drain read-only, and that the drained artifacts are raw.

Refs

  • Sibling of ward#522 (same principle: scope/mediate the capability, never hand over the raw docker socket).
  • docs/agent-observability.md (the host-side drain this exposes), SECURITY.md:38 (socket-as-escape-vector, why not the socket).
## Want Mount the host agent-log drain read-only into the **director read-only surface session**, so a director can read a run's drained `console.log` / `transcript.jsonl` / `meta.json` without a docker socket. This is the safe answer to "directors can't read run logs": the surface deliberately has **no** docker socket (dispatch goes via the TCP broker), because the socket is host-root-equivalent - `docker inspect <sibling>` alone dumps `Config.Env` with every injected secret (`FORGEJO_TOKEN`, `WARD_CLAUDE_CREDS_B64`, AWS/Anthropic creds), and `docker exec` is full sibling takeover. A read-only bind of the already-drained artifacts gives log visibility with none of that surface. ## Where - Host source: `agentLogsDir()` (`cmd/ward/agent_log_drain.go:67` - `$HOME/<AppDir>/agent-logs`). - Mount seam: `leastAccessMounts` / `mountOpts` in `cmd/ward/container_compute.go`. Add a read-only opt-in exactly like `AWSHome`/`containerAWSMount` - a new `mountOpts.AgentLogsDir` that appends `{Source: agentLogsDir(), Target: <containerAgentLogsMount, e.g. /opt/ward-agent-logs>, ReadOnly: true}`. Define the target-path constant beside `containerAWSMount`. - Scope: set the opt-in **only** on the director-surface bring-up path (`directorSurface` / `directorSurfaceArgv`, `cmd/ward/agent_director_heartbeat.go`). Do **not** add it to the shared least-access default - engineers/advisors must not get it (a cross-run log read with far less justification). The surface session stays read-only, so the mount is `:ro`. ## Residual to keep in mind (default: accept, per Kai) The drained `console.log` and `transcript.jsonl` are **raw** - only the Slice-2 SigNoz *envelope* export is redacted (`docs/agent-observability.md`), not the on-disk artifacts. So a whole-dir read-only mount lets a director read every past run's raw stdout + session, which can contain secrets that leaked to stdout (a Read of a `.env`, a token in a traceback) and other runs' content. Kai accepted this: these live host-local unredacted already, same trust tier as `~/.ward/audit/`, and read-only + no-socket removes the real escalation vector. A tighter option, if wanted later, is to mount only the `meta.json`s (the secret-free allowlisted artifact) - note it, default to the full read-only dir mount Kai asked for. ## Acceptance - A director read-only surface session sees `agentLogsDir()` at a fixed in-container path, read-only. - An engineer/advisor run does **not** get the mount. - Writing to the mount from the surface fails (`:ro`). - A short note in `docs/agent-director.md` (or agent-observability.md) that the surface reads the drain read-only, and that the drained artifacts are raw. ## Refs - Sibling of ward#522 (same principle: scope/mediate the capability, never hand over the raw docker socket). - `docs/agent-observability.md` (the host-side drain this exposes), `SECURITY.md:38` (socket-as-escape-vector, why not the socket).
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-ward-525 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-07-02T08:52:33Z). 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 `engineer-claude-ward-525` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-07-02T08:52:33Z). 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

WARD-OUTCOME: done - the director read-only surface now binds ~/.ward/agent-logs read-only at /opt/ward-agent-logs; engineer/advisor runs don't get it.

Straightforward feature, and the mount seam was exactly where the issue said it'd be - mountOpts + leastAccessMounts already had the AWSHome/WardSource opt-in pattern to copy, so the code was ~15 lines. One design call worth naming: the issue framed this as "set the opt-in only on directorSurface", but the surface plan is built by the shared buildUpPlan, which had no notion of read-only. Rather than reach around the seam, I threaded a mountAgentLogs bool through it (surface passes readOnly, the four other callers pass false), so the mount stays owned by leastAccessMounts and the scoping stays honest.

The part that actually fought back was docs, not code. Every candidate doc (agent-surface.md, agent-observability.md, agent-director.md) is hand-tuned to sit at exactly 3997-3999 chars against a hard 4000-char pre-commit cap, so there was literally no room to add a note. The fleet-consistent answer was a new docs/agent-surface-log-read.md plus a one-line pointer, which then meant char-golfing ~40 chars back out of agent-surface.md to fit the See-also bullet. Also tripped the issue-ref linter (the new public doc needs full-URL ward#525 links, unlike the contributor-tuned docs which are exempt).

Confident in the result: build + full test suite green, pre-commit clean, two new tests cover the leastAccessMounts append and the buildUpPlan threading (mount present with the opt-in, absent without). Residual is the accepted one from the issue - the mount exposes raw drained artifacts - documented as such. If someone later wants the tighter meta.json-only variant, the seam is a one-field change.

WARD-OUTCOME: done - the director read-only surface now binds ~/.ward/agent-logs read-only at /opt/ward-agent-logs; engineer/advisor runs don't get it. Straightforward feature, and the mount seam was exactly where the issue said it'd be - `mountOpts` + `leastAccessMounts` already had the AWSHome/WardSource opt-in pattern to copy, so the code was ~15 lines. One design call worth naming: the issue framed this as "set the opt-in only on directorSurface", but the surface plan is built by the shared `buildUpPlan`, which had no notion of read-only. Rather than reach around the seam, I threaded a `mountAgentLogs bool` through it (surface passes `readOnly`, the four other callers pass `false`), so the mount stays owned by `leastAccessMounts` and the scoping stays honest. The part that actually fought back was docs, not code. Every candidate doc (agent-surface.md, agent-observability.md, agent-director.md) is hand-tuned to sit at exactly 3997-3999 chars against a hard 4000-char pre-commit cap, so there was literally no room to add a note. The fleet-consistent answer was a new docs/agent-surface-log-read.md plus a one-line pointer, which then meant char-golfing ~40 chars back out of agent-surface.md to fit the See-also bullet. Also tripped the issue-ref linter (the new public doc needs full-URL ward#525 links, unlike the contributor-tuned docs which are exempt). Confident in the result: build + full test suite green, pre-commit clean, two new tests cover the leastAccessMounts append and the buildUpPlan threading (mount present with the opt-in, absent without). Residual is the accepted one from the issue - the mount exposes raw drained artifacts - documented as such. If someone later wants the tighter meta.json-only variant, the seam is a one-field change.
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#525
No description provided.