# Stock-SBX adapter

`marsh-sbx` is the only boundary to the installed Docker Sandboxes runtime. It
runs an absolute, startup-resolved stock `sbx` binary and uses its public
`create`, `ls`, `inspect`, `mount`, `umount`, `cp`, `exec`, `stop`, and `rm`
operations. It does not start another SBX service, ship a patched binary, or
run a registered command directly in a VM. Host `sbx` is trusted.

## Version and installation

Startup requires stock SBX v0.47.0 or newer,
checks the exact reported version and commit, and probes the local Kit v3
create surface before creating product state. Older inventory fixtures (from
v0.46.0, for example) remain as parser and model coverage, not supported
releases. The version check does
not qualify every later version or prove that a build has the EROFS
writable-layer discard fix; acceptance checks container cleanup directly.

Homebrew owns installation of `sbx`. marsh resolves `MARSH_SBX`, otherwise
`sbx` on `PATH`, and canonicalizes it. A running daemon keeps the executable it
resolved at startup, and its handshake includes a digest of that executable, so
a client restarts an idle daemon after an SBX upgrade. MCP registrations keep
their stored path; rerun them after an upgrade.

## Ownership map and names

Every VM marsh creates has a random name: `marsh-k-` plus 8 base36 characters
for Kit VMs, `marsh-s-` for shell VMs. `vm_ownership.rs` keeps a persisted map
from `(purpose, key)` to name and observed UUID in the host control directory.

- A missing entry gets a fresh random name, persisted as an intent before
  `sbx create`. After create the UUID is recorded.
- The next inventory adopts an intent whose name is present and drops one,
  loaded at startup, whose name is absent.
- A VM is usable only if its name is present with the recorded UUID. A name
  with a different UUID, or not in the map, is foreign and refused.
- Stop and remove target only a recorded UUID that is currently present.
  Removal drops the entry, so the next use gets a new name.

Warm requests use a cached `sbx ls --json` view, refreshed on cold start, after
a lifecycle change, or after a failure. `docs/model/Ownership.tla` specifies
this protocol.

## Kit VMs and fresh containers

A registry entry names a native Kit v3 source directory or an immutable Kit OCI
reference; mutable tags are rejected. The Kit's OCI config and descriptor own
entrypoint, command, environment, user, workdir, capabilities, network policy,
and services. The adapter does not translate or add to them. Local source is
an authoring path; the release run uses published digests.

Each Kit VM runs one retained `sbx exec -i ... marsh-worker --serve`
transport, which also keeps stock SBX from stopping an idle VM. The worker
reports its generation at startup and answers liveness pings. Per attempt the
daemon admits the sources, takes a reference on the retained mounts, sends
bounded attempt-keyed frames (start, input, EOF, signal, resize, cancel), relays
output, and records the terminal report. The worker creates one container from
the exact Kit image with the host user's nonroot UID/GID, natural cwd and home,
exact grants, and resource ceilings; reports Docker's container ID; waits;
deletes the container; and verifies its absence.

Jobs keep the image's static environment plus validated `HOME`, `USER`,
`LOGNAME`, and `MARSH_SELECTED_HOME`. When stock SBX provides them, the adapter
also passes only the fixed proxy endpoint, bounded credential-mode sentinels, a
read-only CA bundle, and the sandbox-local MCP Gateway URL and sentinel name as
a pair. Gateway tools loaded into a Kit VM are visible to every job in it. Jobs
receive no proxy-held raw credentials, worker environment, or Docker/SBX socket.
Their daemon requests go through the scoped job capability socket
(`docs/design/processes.md`). An agent's explicit login can persist state in
the shared guest home; the [security model](https://runmar.sh/docs/security.md) distinguishes that
from SBX's proxy-held credentials.

`--load` prepares the VM, worker, and job image before returning.

## Shell VM and relay

The shell VM uses the packaged shell template, built on the DHI Debian ARM64
base. The adapter installs
the Linux `marsh` and relay binaries, creates the Mac user's UID/GID, and
mounts the launch directory at its identical path and the selected home backing
(`MARSH_HOME/home`, or an ephemeral slot) at `/Users/<user>`. The real Mac home
is never mounted. Attachment uses `sbx exec -it` or `sbx exec -i`. The relay
runs as the shell user with a session-scoped socket and token in a 0700
runtime directory; the token never appears in argv, the home, or a job
environment.

## Mounts

Sources are opened with `O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC` at admission and
their identity (volume UUID and persistent file ID on macOS, via
`marsh-host-identity`) is checked before and after `sbx mount`. Natural host
paths are only mount targets, never authority. A mount is made on first use
and retained while the VM is warm; concurrent users share it by reference. An
idle mount is replaced only when its source identity changed, and mounts are
released when the VM is retired. Stock SBX accepts only pathname mount
sources, so the reopen inside `sbx mount` is an upstream limitation accepted
under the trusted-host assumption.

## Failure behavior

Unknown state is never treated as absence or success. The adapter does not
create a second daemon or VM pool, replay a job, or fall back to direct VM
execution. Memory, PID, output, writable-layer, and wall limits produce typed
outcomes. Transport loss during active work, uncertain container cleanup, or a
missing create-boundary event within the bounded wait produces a nonzero
`cleanup_uncertain` receipt and quarantines the VM. A quarantined Kit VM takes
no new jobs until `marsh workers reset KIT` retires it; the next use creates a
fresh VM under a new random name. `marsh reset` and `marsh stop` clean up
the whole scope once no shells or jobs are active. Do not delete the journal or
ownership map as a recovery shortcut.
