Skip to content

Decide instance discovery and the dashboard's Docker access #226

Closed

Description

@lbacik

Question

How does the dashboard find agent containers, and how does it get the Docker access it needs to run agentctl safely? Telemetry is read via docker exec ... agentctl telemetry --json, so even the read-only v1 needs exec, not just container listing.

  • Discovery: container labels and naming, filtering to agent containers only, handling containers that start, stop or are replaced while the dashboard runs.
  • Exec invocation: docker exec as user agent with the absolute agentctl path, timeouts, and the TARGET_REPO check the operator doc requires before mutating commands (v2), or also before reads.
  • Blast radius: Docker socket access with exec is effectively root on the host. Options: raw socket, docker-socket-proxy (which can allow exec but not restrict which command runs), a host-side helper limited to agentctl status|telemetry, or running the dashboard as a host process. Which one for v1, and does v2 control change the answer?
  • v1 vs v2: how the dashboard guarantees it only issues read-only commands in v1, and what changes when control (stop, stop --after, resume, handoff, next issue) arrives.

Part of map #222.

Activity

  1. changed the title [-]Decide instance discovery and the control path from the dashboard[/-] [+]Decide instance discovery and the dashboard's Docker access[/+] on Oct 14, 2026
  2. self-assigned this
    on Oct 14, 2026
  3. lbacik commented on Oct 14, 2026

    @lbacik
    OwnerAuthor

    Resolution

    Decided in a grilling session; the operator asked to take the recommended answer on every question.

    Discovery

    • Agent-side change: the image carries LABEL io.github.lbacik.simple-coding-agent.role="agent" (in the Dockerfile, not in compose), so every container built from it is discoverable without operator configuration.
    • The dashboard lists docker ps -a --filter label=io.github.lbacik.simple-coding-agent.role=agent and drops containers labelled com.docker.compose.oneoff=True (docker compose run containers have no live agent and must never be addressed).
    • Rediscovery happens on every poll cycle; no docker events stream in v1. Running containers are polled; exited ones are shown as "not running" without any exec; removed containers disappear.
    • Managed instance identity = container name (stable across docker compose up recreation). The container ID is tracked only to detect replacement; the target repository shown in the UI comes from the snapshot.

    Exec invocation

    • The dashboard shells out to the docker CLI via an argv list (no shell), not the Docker Engine SDK: it honours Docker contexts/DOCKER_HOST (the remote-context fog) and is exactly what an operator would type.
    • Argv is built from constants: docker exec --user agent <container-id> /usr/local/bin/agentctl telemetry --json. No -t/-i. It targets the container ID resolved in the same cycle, never the name, so a recreated container is never hit by a stale decision.
    • Dashboard-side timeout 5 s (configurable); at most one in-flight exec per instance (the control server is serial). On failure the instance is shown as unreachable with the reason, and the last good snapshot stays visible with its age.

    TARGET_REPO check

    • Reads need no separate check: the snapshot identifies its own target repository (requirement passed to "Define the telemetry snapshot contract").
    • v2 mutations must be bound to the container ID and target repository the operator saw; the recommended mechanism is an agent-side guard (agentctl rejects a command whose expected target repository does not match). Mechanism is left in the v2 control fog.

    Blast radius

    • v1 runs as a host process under the operator's own account, using the operator's existing Docker CLI access, with the web UI bound to 127.0.0.1 only. It grants no privilege the operator does not already have.
    • Rejected for v1: mounting the raw socket into a dashboard container (root-equivalent for a web-facing process); docker-socket-proxy (cannot restrict which command exec runs, and allowing POST /containers/{id}/exec also opens container creation); a separate host-side helper (an extra component that reduces nothing while the dashboard already runs as the operator). Revisit when the dashboard must be reachable remotely.
    • Polling is timer-driven: HTTP handlers only read the in-memory cache, so in v1 no request path reaches an argv.

    Read-only guarantee (v1) and v2

    • One gateway module is the only code that builds Docker argv. In v1 it exposes exactly list_instances() and telemetry(container_id); there is no generic exec. A test pins every argv the gateway can produce.
    • v2 keeps the same deployment and adds one named gateway method per control command (stop, stop --after, resume, handoff, next issue), with the dashboard generating --request-id for safe retries; recovery and errors reset are never added. Request-triggered exec in v2 additionally needs CSRF/Origin protection (part of the v2 control fog).

    Agent-side changes surfaced

    1. Image label io.github.lbacik.simple-coding-agent.role="agent".
    2. Snapshot carries the target repository (-> telemetry snapshot contract).
    3. agentctl telemetry finishes well inside 5 s without a TTY and reports failures machine-readably (-> agentctl telemetry command).
    4. (v2) Expected-target-repository guard on mutating commands (fog).

    Assets

    • Glossary: Dashboard and Managed instance added to CONTEXT.md on branch docs/fleet-dashboard-glossary (commit 457b102).
    • No ADR in this repo: the deployment choice lives in the dashboard project and is cheap to revisit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

wayfinder:grillingDecision requiring a live human discussion

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions