Expand description
The compose shell-out engine (04-01, RIG-01): runner seam, version check, LOCKED arg builders, and the LDJSON/array parsers — serde models out, no printing (the TUI rides the actions layer in Phase 6).
§The runner seam
ComposeRunner is the ONLY way any rig code spawns a process:
actions and discovery take &dyn ComposeRunner, so every decision
path is unit-testable against a scripted fake (no docker needed).
The production DockerCompose shells out via
tokio::process::Command (the workspace process feature — the one
Phase 4 dependency change). run prefixes the compose subcommand;
run_docker spawns the PLAIN docker CLI (volume ls / port
attribution — no -p prefix, those are not compose project ops);
run_streaming pipes stdout and forwards lines as they arrive (the
logs -f follow shape, 04-02).
§LOCKED invocation shapes (research §Compose invocation shapes)
Every builder is a pure function whose exact output vector is
unit-pinned — -p <resolved-name> EXPLICIT on every project op
(Pitfall 8: no implicit directory-name projects), --project-directory
always on resolve (Pitfall 8: .env loading is cwd-sensitive),
--remove-orphans on up AND down (Pitfall 4), --wait-timeout
explicit on up (Pitfall 3: healthchecks + image pulls).
§The two output conventions (research Pitfall 1)
ps/volume ls/docker ps emit ONE OBJECT PER LINE (LDJSON —
parsed with a StreamDeserializer); config --format json emits a
SINGLE doc (an object on current compose; an older array shape is
tolerated by unwrapping the first element). BOTH conventions are
fixture-pinned so the divergence can never regress into a naive
from_str::<Vec<T>>.
Structs§
- Compose
Output - One completed
docker …invocation: captured output + exit code. A spawn FAILURE (no docker binary) iscode: 127with the spawn error instderr— one error path, no io::Error leakage. - Docker
Compose - Production
ComposeRunner: shells out to the realdockerbinary viatokio::process::Command. - Docker
PsEntry - One
docker ps --filter publish=row, reduced to attribution: the container’s (first) name and its compose project label, when it has one.docker psJSON differs fromcompose psJSON (Namesplural,Labelssometimes a"k=v,k=v"string instead of a map) — both shapes are tolerated and fixture-pinned. - Port
Mapping - One published-port mapping from a resolved compose config
(container
target→ hostpublished). - Publisher
- One
docker compose pspublisher row (live-captured field names). - Service
Status - One
docker compose psservice row (live-captured field names;Health/Publishersoptional — services without healthchecks or ports omit them). - Volume
Entry - One
docker volume lsrow (only the name is consumed by status).
Traits§
- Compose
Runner - The process seam for everything rig-related. Actions NEVER spawn processes directly — they script this trait (the GatewayApi precedent), which is what makes the whole family testable without docker.
Functions§
- check_
output - Map a completed invocation: exit 0 → the stdout str; nonzero →
CoreError::Rigcarrying the stderr tail (last [STDERR_TAIL_LINES] lines — research Pitfall 3). - compose_
version - Verify
docker composeanswers with major version ≥ 2 (the v1docker-composePython binary never answers todocker compose— absence IS the install-hint case). Returns the version string (e.g."5.1.2"). - config_
args - The resolve step (research Pattern 1):
docker compose -f <file> --project-directory <dir> config --format json.--project-directoryis ALWAYS explicit —.env(and thusCOMPOSE_PROJECT_NAME) loading is cwd-sensitive (Pitfall 8). - docker_
ps_ publish_ args docker ps --filter publish=<port> --format json— host-port occupancy with attribution (research Pattern 3); also a PLAIN-docker shape viaComposeRunner::run_docker.- down_
args down: stop + remove containers/networks;--remove-orphansalways (Pitfall 4);-v(named+anonymous volume deletion) only for the reset teardown half (04-02).- logs_
args logs(human-form passthrough by design — the streaming exception when--follow; wired by 04-02’srig_logsviarunone-shot /run_streamingfollow). Invocation shape LOCKED from day one.- parse_
config - Parse
docker compose config --format jsonoutput into aRigPlanskeleton:.nameis THE identity truth (honors the rig’s own.envCOMPOSE_PROJECT_NAME), services are the service map’s keys,port_mappingsthe collected target→published pairs, volumes the volume map’s keys. Tolerant where compose is shape-shifty (publishedarrives as string OR number; the doc may be a bare object — current compose — or a single-element array — older builds); loud where identity is at stake (no.name→ error, never an implicit directory-name project). - parse_
docker_ ps_ ldjson - Parse
docker ps --format jsonLDJSON intoDockerPsEntryrows (name + compose-project attribution only). - parse_
ps_ ldjson - Parse
docker compose ps --format jsonLDJSON intoServiceStatusrows. - parse_
volume_ ls_ ldjson - Parse
docker volume ls --format jsonLDJSON (research Pitfall 1). - ps_args
psas LDJSON (research Pitfall 1): one object per service.- reset_
preview - The reset preview (04-02, RIG-01): the named-volume names
rig reset’sdown -vhalf will remove, reported in the result data so agents see WHAT reset took before/as it acts. Label-filtered at the docker layer (volume_ls_args) and name-filtered here — only<project>_-prefixedvolumes are reset’s to take (defense in depth; research Pitfall 4 shape:Name+Labels). - up_args
up(research LOCKED shape): explicit-p <name>(never an implicit directory-name project), detached +--waitwith an EXPLICIT timeout (Pitfall 3:--waitblocks on healthchecks and image pulls),--remove-orphans.- volume_
ls_ args docker volume ls(04-01 status / 04-02 reset preview): PLAIN docker CLI — nocomposesubcommand, no-pprefix (volumes are labeled, not project-scoped, at this layer). Invoked viaComposeRunner::run_docker, which spawnsdocker, NOTdocker compose, for exactly this shape.