Skip to main content

Module workspace_container

Module workspace_container 

Source
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 as N: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§

ContainerWorkspace
The container provider’s handle state — everything readiness and teardown need across the seam’s three calls (the provider itself stays stateless).
LocalContainerProvider
The local-container provider: per-mission compose project, dynamic ports, in-network contract readiness. See the module docs.