Expand description
Local-container WorkspaceProvider (ticket
.kranz/tickets/local-container-workspace.md, design D-B implementation
#2 in docs/scoping/workspace-contract.md) — gives a mission an isolated
RUNNABLE environment: a per-mission compose project, dynamic host ports,
and contract health/readiness executed INSIDE the container network —
solving the host port clashes and Docker daemon contention that bare
worktrees cannot.
Relationship to crate::sandbox_container (M7 tier 3): this provider
REUSES its runtime detection (ContainerRuntime/crate::sandbox_container::detect) but the
concepts stay separate (“the APIs stay separate”): the sandbox is the
process blast radius for agent CLIs — its tier-3 --network none
semantics live in THAT layer — while the workspace is the runnable
environment (bootstrap/services/readiness/previews). The network model
here is the fs-tier bridge (the runtime’s default NAT): registry egress
works for bootstrap, and this provider never passes --network none.
Isolation unit: one compose project per mission,
kranz-ws-<sanitized-mission-id> — parallel missions get distinct
projects, hence distinct networks and no shared port namespace. Projects
are mission-owned (the name carries the mission id) and never shared.
docker compose (or the runtime’s compose subcommand) is required; a
runtime without one fails closed with the reason named, and a host with
no runtime at all fails closed at provision (run start, before spend).
Ports (the contract’s services[].port.policy):
dynamic→ published as host port 0 (OS-assigned); the assigned port is read back from the runtime once the service is up and recorded on the handle. Preview urlTemplates get{port}substitution ONLY with an actually-assigned dynamic port (never fabricated), and only when the contract declares exactly one dynamic service — a template never binds an arbitrary service’s port.fixed: N→ published asN:N; provision REFUSES before any container starts when N is already bound on the host, naming the service and the port — never a silent rebind.
Provision: render the compose file (from the contract’s services[] +
bootstrap/readiness + mounts[]) into the mission-owned runtime dir
(<mission-dir>/workspace/compose.json, gitignored — JSON is valid YAML
1.2, so compose -f parses it and the provider never hand-rolls YAML
escaping), compose up -d, then wait for each declared healthCheck by
polling it inside its service container. The PRIMARY CHECKOUT IS NEVER
MOUNTED OR WRITTEN: the mount set is exactly the mission execution root
(the integration worktree in worktree mode) plus the contract’s
mounts[], each at its identical path.
Readiness: the contract’s bootstrap[] then readiness[] run INSIDE the
container network via compose exec -T workspace sh -c … — the
workspace service holds the worktree mount — reporting through the same
gate phase shapes as the local-worktree provider
([crate::workspace_provider::report_gate_outcomes]), so block reasons
and decision lines are byte-identical to the host path. The golden-data
hooks (design D-D: clone/migrate/skewCheck at readiness, reset between
validation rounds) exec through the same path — a container workspace
never runs data hooks on the host.
Teardown: TeardownMode::Keep leaves the project running (documented:
previews stay live); Hibernate is compose stop (containers paused,
project kept); Destroy is compose down -v (project + volumes
removed), then the contract’s disk.prune hint runs on the host when
declared. The run loop drives Keep at non-terminal ends (a
blocked/paused mission keeps its project for resume) and the configured
workspace.teardownMode when a run reaches a terminal state (ticket
workspace-idle-hibernate).
v1 honesty notes: every container runs the shared default image
(crate::sandbox_container::DEFAULT_IMAGE) with its declared
start/healthCheck command — per-service images are a later additive
contract field (the schema’s deny_unknown_fields fails closed on an
image key today). A contract-less provision starts no containers and
needs no runtime (D-H: never imply a runnable environment that does not
exist).
Structs§
- Container
Workspace - The container provider’s handle state — everything
readinessandteardownneed across the seam’s three calls (the provider itself stays stateless). - Local
Container Provider - The local-container provider: per-mission compose project, dynamic ports, in-network contract readiness. See the module docs.