Container Runtime

Container Runtime

Container parity with VM agentic-dev landed in `2026.5.0` under the #181 epic (issues #182–#186). The dashboard, REST surface, and AIWG bridge treat containers as first-class workloads alongside QEMU/KVM VMs — same lifecycle vocabulary, same loadout selector, same mission dispatch flow.

This document is the reference for operators picking a runtime and for integrators wiring container instances into the AIWG bridge. The Rust source of truth is `management/src/docker_runtime.rs`; the HTTP surface that wraps it is `management/src/http/containers.rs`.


Public API

`docker_runtime` is the single chokepoint for Docker shell-outs. Every container lifecycle operation funnels through these functions:

SymbolPurpose
`DockerMonitorConfig`Poll cadence + orphan-age threshold; loaded from env (`DOCKER_MONITOR_ENABLED`, `DOCKER_POLL_INTERVAL_SECS`, `DOCKER_ORPHANED_AGE_SECS`).
`ContainerInfo` / `ContainerStatus`Normalized `docker ps` row — `Running`, `Stopped`, or `Other(raw)`. `finished_at` populated for stopped containers.
`SpawnOpts``env: Vec<(String,String)>`, `labels: Vec<(key, value)>`, `mounts: Vec<(host, container)>`, `network: Option<String>`, `cmd: Vec<String>`, and optional unique `control_uid`.
`list_containers()``docker ps -a --filter label=agentic-sandbox=true`. Managed containers only — we never surface containers we did not spawn.
`spawn_container(name, image, opts)`Runs a platform-aware `docker run -d --label agentic-sandbox=true --name {name} …`. Linux adds the host-gateway mapping; Docker Desktop uses its native host DNS. Returns the container ID.
`start_container(name)` / `stop_container(name, timeout)`Idempotent lifecycle verbs over the same label-filtered set.
`remove_container(id)``docker rm -f` on a single ID.
`get_container_by_name(name)`Convenience lookup over `list_containers()`.
`spawn_docker_monitor(config, metrics, instance_registry, agent_registry)`Background task: polls every `poll_interval_secs`, emits `container.*` lifecycle events, revokes executor readiness when a container stops or disappears, and sweeps orphans older than `orphaned_age_secs`.

On Linux, the runtime adds `--add-host host.docker.internal:host-gateway`; without it the in-container agent's default `MANAGEMENT_SERVER=host.docker.internal:8120` does not resolve. On macOS, Docker Desktop provides `host.docker.internal` natively, so the runtime does not add the Linux-only mapping. Docker Desktop also rejects `network: host` at validation time because it cannot preserve Linux host-network semantics. Bind-mount source and destination paths must be absolute, and source paths must exist before Docker is invoked; Docker Desktop file-sharing denials return an actionable Settings path instead of raw daemon output.


Runtime selection: VM vs container

Both runtimes register against the same `OutputAggregator`, speak the same gRPC contract from `agent-rs`, and surface in the same dashboard sidebar. They differ where the substrate differs.

DimensionVM (QEMU/KVM)Container (Docker)
IsolationFull hardware virtualization. Kernel boundary between host and workload.Process namespace. Shared kernel.
Startup time30–90 s cold (cloud-init runs once); 5–15 s warm.1–3 s typical for `agentic/agent:dev`-derived images.
Resource overhead~512 MB RAM floor per VM (kernel + systemd + journald). Dedicated virtual disk.~50 MB RAM floor. Layered filesystem; no per-instance kernel.
NetworkLibvirt-managed bridge (`192.168.122.0/24` default). Per-VM IP. `agentshare` profile gets `--network none` for isolation.Docker bridge by default. Linux may use `--network host` and injects `host.docker.internal:host-gateway`; Docker Desktop uses native `host.docker.internal` and rejects host mode.
PersistenceDisk image survives `virsh destroy`; only `provision-vm.sh --destroy` wipes it.Container filesystem is ephemeral unless mounts are bound. Use `mounts: [(host_path, /workdir)]` for persistence.
AIWG framework installBaked into the cloud-init seed by `provision-vm.sh` via loadout.Baked into the image at build time; `claude` / `codex` / `opencode` images rebase onto `agentic/agent:dev`.
Operator escape hatch`virsh console`; direct `ssh agent@<ip>` only for dev/break-glass because it bypasses gateway policy/audit.`docker exec -it <name> bash`.
Crash recovery`crash_loop.rs` detector triggers `provision-vm.sh` rebuild. See `crash-loop.md`.Monitor sweeps stopped containers older than `orphaned_age_secs` (default 1 h). No auto-rebuild — operator decides.

When to pick a VM

  • The workload runs untrusted code, downloads arbitrary binaries, or

needs to exercise kernel features the container runtime forbids (raw sockets, ptrace of arbitrary PIDs, loading kernel modules).

  • The workload needs to survive container daemon restarts independent

of host reboot.

  • The mission persists for hours and the storage cost of a virtual

disk is acceptable.

  • The mission needs the `agentshare --network none` isolation tier

(forensics / red-team profiles).

When to pick a container

  • The workload is a short-lived agent task (minutes to ~1 h).
  • Fast iteration: rebuild image once, spawn dozens of fresh instances.
  • The toolchain in `agentic/agent:dev` is sufficient (Python via uv,

Node via fnm, Go, Rust via rustup, ripgrep/fd/bat/jq/delta/xh, cmake/ninja/meson, aider pinned to Python 3.12, `gh` + `gh copilot`).

  • The provider image (claude / codex / opencode) is one of the rebased

variants that already speak the agent protocol.


Image catalog

Container images are layered: a shared dev toolchain at the bottom, provider-specific images on top.

ImagePurposeBuilt from
`agentic/agent:dev`Shared dev toolchain layer. Mirrors the `agentic-dev` VM profile's `apt`/`uv`/`fnm`/`rustup` package set. /etc/profile.d snippet stabilizes PATH across login shells.Debian base + AIWG bootstrap. See `CHANGELOG.md` 2026.5.0 entry for #182.
`agentic/claude:latest`Claude Code CLI on top of `agentic/agent:dev`.Rebased onto shared base for parity (#183).
`agentic/codex:latest`OpenAI Codex CLI on top of `agentic/agent:dev`.Rebased onto shared base (#184).
`agentic/opencode:latest`OpenCode CLI on top of `agentic/agent:dev`.Rebased onto shared base (#185).
`agentic/automation-control:latest`Blueprint for orchestrator-driven TUI control sessions. Includes Codex, Aider, shared dev tools, and `agentic-provider-inventory` without bundling credentials.Extends `agentic/codex:latest` (#346).

The CI smoke matrix (#186) builds each image and asserts:

  • `python --version`, `node --version`, `go version`, `cargo --version`

all resolve.

  • `rg --version`, `fd --version`, `bat --version`, `jq --version`,

`xh --version`, `grpcurl --version` all resolve.

  • The agent binary inside the image dials the management server and

registers within the smoke window.

Automation-control blueprint

Use `agentic/automation-control:latest` when an external orchestrator needs a general-purpose sandbox session it can observe, search, and drive through the PTY control plane. The image intentionally does not embed secrets or auto-launch provider login flows from global env. Start with the credential-free probe, then use the inventory and readiness helpers before starting a managed provider TUI:

agentic-provider-inventory
agentic-provider-readiness codex
agentic-codex-automation
agentic-claude-automation

`agentic-codex-automation` prefers `OPENAI_API_KEY_FILE` or `AGENTIC_CREDENTIAL_DIR/openai_api_key`, then sets `OPENAI_API_KEY` only in the final provider process. `agentic-claude-automation` does the same for `ANTHROPIC_API_KEY_FILE` or `AGENTIC_CREDENTIAL_DIR/anthropic_api_key`. Both wrappers support `AGENTIC_PROVIDER_HOME` for isolated provider home/config/cache directories.

`agentic-provider-readiness` emits structured tab-separated readiness rows: provider, CLI presence/version, auth state, and error class. It does not print credential values.

Then launch provider TUIs only after the orchestrator has satisfied its credential and Controller-input policy gates. The target model for automated provider launch is ADR-028: startup profiles reference credential ids, the credential broker issues session-scoped leases, and provider launchers consume leased files from a per-session credential directory. Managed Docker refuses startup profiles containing raw credential refs. Use the credential proxy for supported protocols or QEMU when a provider requires local raw material; image-baked credentials and `docker run -e` provider tokens remain outside the managed security boundary.


REST surface

Container lifecycle lives at `/api/v1/containers/`, mirroring the shape of `/api/v1/vms/`. The full list is in `management/src/http/containers.rs`; the relevant endpoints are:

  • `GET /api/v1/containers` — list managed containers.
  • `POST /api/v1/containers` — create + spawn. With no explicit transport,

management mounts its UDS, registers a unique per-instance control UID, and starts workload children as capability-free UID/GID `10001:10001`. No bootstrap token or mTLS key is issued. An explicitly supplied transport is a compatibility path and does not receive the split-identity claim.

  • `GET /api/v1/containers/{name}` — single-container detail.
  • `POST /api/v1/containers/{name}/start` — start a stopped container.
  • `POST /api/v1/containers/{name}/stop` — graceful stop with timeout.
  • `DELETE /api/v1/containers/{name}` — `docker rm -f`.

The image catalog endpoint (`GET /api/v1/container-images` → `management/src/http/container_images.rs:56`) returns the curated provider image set the dashboard offers in the Create dialog.

PTY exec inside a container is not part of this surface — that lives behind the `pty-ws/v1` binding (see `pty-rendering.md`) which attaches to whatever the container entrypoint produces via the existing in-container agent path.


AIWG bridge integration

When `AIWG_SERVE_ENDPOINT` is set, the management server registers itself as an A2A executor (see `aiwg-executor.md`). Mission dispatch lands at `POST /api/v1/sessions/:id/dispatch` and routes to either a VM or a container depending on the session's recorded runtime.

  • The bridge does not know or care which runtime backs an

instance. It addresses by `(instance_id, session_id)`.

  • The dispatch handler resolves the instance to its runtime, then

invokes the matching session-attach path.

  • Container instances participate in the same `mission.*` event

vocabulary the executor contract emits — `mission.dispatched`, `mission.completed`, `mission.failed`. The events stream over the same `/ws/executors/{id}` channel.

The 2026.5.0 `server_hello` capability banner (#190) advertises both runtimes; AIWG's `replayCapable` gate flips on for either.


Loadout interaction

Today's published loadout profiles (`images/qemu/profiles/`) are VM-only — `agentic-dev.yaml`, `agentic-dev-cloud-init.yaml`. The loadout schema described in `LOADOUTS.md` is reused for container instances by setting an explicit runtime on the manifest. Container "loadouts" today are effectively the choice of image (for example `agentic/claude:latest`, `agentic/codex:latest`, `agentic/opencode:latest`, or `agentic/automation-control:latest`) plus mounts and env; the formal `runtime:` field unifies that with the VM profile syntax.

When provisioning a container from the dashboard:

1. Pick Container in the Runtime dropdown of the Unified Instances Create dialog (#178). 2. Pick an image from the curated `agentic/*:latest` provider list. 3. Optionally add bind mounts (host path → `/workdir`-style container path) for persistence. v2 admin Docker provision accepts `mounts` as `host_path:container_path` strings. 4. The dashboard issues `POST /api/v1/containers`; management injects the instance-bound UDS transport and split control/workload identity.

For v2 admin Docker provision, `agentshare: true` creates a per-instance host tree under `AGENTSHARE_ROOT` or `/srv/agentshare`:

{AGENTSHARE_ROOT}/instances/{instance_id}/
├── workspace/
├── inbox/
├── outbox/
└── comms/

The Docker runtime bind-mounts that tree into the container at the canonical work paths clients expect:

Host subdirectoryContainer paths
`workspace/``/workspace`, `/root/workspace`
`inbox/``/mnt/inbox`, `/root/inbox`, `/inbox`
`outbox/``/mnt/outbox`, `/root/outbox`, `/outbox`
`comms/``/mnt/comms`, `/root/comms`, `/comms`

If a caller supplies an explicit mount for one of those container paths, that mount wins and the default for that path is skipped. Docker AgentCards advertise `adapter-command/v1` only when a workspace mount is available, so orchestrators can treat the extension as a live capability contract rather than an unconditional server feature.

Admin v2 inventory exposes container bootstrap readiness separately from runtime metadata. Running containers without a Ready agent report `operation_status: "bootstrap_pending"` with `agent_registered: false` and `agent_ready: false`; stopped containers that exited before registration report `operation_status: "not_ready"` and include `container_finished_at` when Docker provides it. Cockpit and other bridge consumers should poll these fields before creating sessions instead of assuming an AgentCard URL implies a usable agent.


Operational notes

  • Orphan cleanup is enabled by default. Set

`DOCKER_MONITOR_ENABLED=false` to disable the background monitor and sweep. The monitor is scoped by the `agentic-sandbox=true` Docker label, not a name prefix, and removes stopped managed containers after `DOCKER_ORPHANED_AGE_SECS` (default 3600). Do not apply that label to operator-owned containers that management must not lifecycle-manage.

  • Docker process state is negative readiness evidence only. A stopped or

disappeared container immediately revokes executor readiness. A running container does not become dispatchable until its agent completes authenticated registration.

  • Stop ≠ delete. The dashboard's Stop button calls

`POST /api/v1/containers/{name}/stop` and leaves the container in Stopped state so the operator can restart or inspect it. Force-off goes through the same path with timeout 0.

  • Container metrics flow into the same `Metrics` aggregator as VM

metrics (`management/src/telemetry/metrics.rs`). See `telemetry.md` for the label scheme.

  • Lifecycle events emit through the same SSE stream as VM events

(`/api/v1/events?follow=true`). See `transport-audit.md`.


See also

container `runtime:` field per #178).

`pty-ws/v1` binding (works against containers via in-container agent).

(container parity is operator-driven for now).

— observability for either runtime.

  • `CHANGELOG.md` — 2026.5.0 entry for the #181 epic.