# @parcha/pi-transport

**Experimental.** The API changes without notice between releases.

> **Removed:** the session home (`./home`), the box place (`./place`), and the move, restore and store checks of
> `./testing`. **Deprecated, to be removed:** the launcher (`./launch`, `Box.launcher` and `BoxLauncher`); the placer
> (from the root `jevPlacer`, `jevClassifier`, `environmentFacts`, `PlacementsDoc`, `writePlacement`,
> `isPlacementRecord`, `PlacementError` and the `Placement*` and `Placer` types); `./testing`'s `replayClassifier` and
> its `FakeLaunch`; and the `home`, `auto` and `purpose` config keys. To run an agent in a box, use `@parcha/agentrun-serve`:
> it uses this package's boxes, `./hands` and `./sync`, which stay.

A pi agent's tool calls go to Daytona and Modal sandboxes, and Jev places each one on the sandbox that fits it. [Jev](https://docs.typesafe.ai/) is TypeSafe's typed-decision model: it answers a question about a step with a choice and a probability, and writes no prose.

Tool calls can also run as serverless calls on an Archil disk (`archilExecBoxes`).

`boxEnv(box)` gives a [pi](https://github.com/earendil-works/pi) agent's `read`, `write`, `edit` and `bash` a box: it
is pi's `ExecutionEnv` over the box, for a pi-durable Harness. `transport(config, host)` does it for each conversation,
from a config that names the environments. The session, its store and its model calls stay with the host; only the
tool calls go to the box. Every provider is behind one `Box` interface:

| Where | Provider | Isolation |
| --- | --- | --- |
| This machine | `localBoxes`, from `@parcha/pi-transport/box/local` | none: for development and tests |
| Docker | `dockerBoxes`, from `@parcha/pi-transport/box/docker` | a container per box |
| Daytona | `daytonaBoxes`, from `@parcha/pi-transport/box/daytona` | a container per box on the host kernel; a `linux-vm` snapshot is a VM |
| Modal | `modalBoxes`, from `@parcha/pi-transport/box/modal` | a gVisor sandbox, or a VM (`isolation: "vm"`) |
| Archil exec | `archilExecBoxes`, from `@parcha/pi-transport/box/archil-exec` | a directory of the agent's work disk; each `exec` is one serverless call in a fresh VM container, and boxes on one disk share it |

Jev places a call from each environment's one-line `purpose`: the host asks `jevPlacer`, Jev reads the step and those
lines and says where it should run, and the host points its tools there.
The placer is deprecated and removed in 0.1.0-beta.31.

The reference below takes each part in turn: [a box](#a-box), [tools in a box](#hands-tools-run-in-a-box),
[where tools run](#where-tools-run-is-a-line-of-config),
[a box's archive](#a-boxs-archive-unpacked-on-this-machine-sync), [a run's files](#a-runs-files-between-this-machine-and-a-box-sync),
[Jev's placement](#where-a-session-runs-the-placer), [receipts](#what-a-box-cost-receipts),
[a program kept alive in a box](#launch-a-program-kept-alive-in-a-box) and [writing a provider](#writing-a-provider).

## A box

A `Box` is one running machine: a directory on this machine, a container, a sandbox, a VM. A `BoxProvider` creates
boxes, finds them again by id and lists them by label.

Install the package with its pi peers, pinned. The pi family is supported at 1.5.0 exactly.

```sh
npm install @parcha/pi-transport@latest @earendil-works/pi-durable@1.5.0 @earendil-works/pi-ai@1.5.0 @earendil-works/chord@1.5.0
```

The example below needs Docker; `localBoxes({ root: "./boxes" })` from `@parcha/pi-transport/box/local` runs it with
no Docker, in a directory on this machine.

```ts
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { finish } from "@parcha/pi-transport";
import { localBoxes } from "@parcha/pi-transport/box/local";

const provider = localBoxes({ root: "./boxes" });
const created = await provider.create({ maxLifetimeMs: 10 * 60_000 }, BACKGROUND_CONTEXT);
if (!created.ok) throw created.error;
const box = created.value;

const started = await box.exec(["uname", "-a"], undefined, BACKGROUND_CONTEXT);
if (!started.ok) throw started.error;
const done = await finish(started.value);
if (done.ok) console.log(done.value.exitCode, new TextDecoder().decode(done.value.stdout));
await box.destroy(BACKGROUND_CONTEXT);
```

That needs no key and no daemon: a box here is a directory under `./boxes`. The same calls on Docker, which runs a container per box and needs a Docker daemon:

```ts
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { finish } from "@parcha/pi-transport";
import { dockerBoxes } from "@parcha/pi-transport/box/docker";

const provider = dockerBoxes();
const created = await provider.create({ maxLifetimeMs: 10 * 60_000 }, BACKGROUND_CONTEXT);
if (!created.ok) throw created.error;
const box = created.value;

const started = await box.exec(["uname", "-a"], undefined, BACKGROUND_CONTEXT);
if (!started.ok) throw started.error;
const done = await finish(started.value);
if (done.ok) console.log(done.value.exitCode, new TextDecoder().decode(done.value.stdout));
await box.destroy(BACKGROUND_CONTEXT);
```

The examples are ES modules: save them as `.mjs` (or set `"type": "module"`); the `transport()` example below has TypeScript types, so save that one as `.mts` (Node runs it as it is). Install the peers, `@earendil-works/chord` and `@earendil-works/pi-durable`, beside `@parcha/pi-transport`. Node 22.19 or later.

Expected failures are returned as `Result` values with a `BoxError` and a code. Every box has a maximum lifetime
that its provider enforces with no call from the host, so a host that dies does not leave boxes behind. A program in
a box never receives the host process's environment. A provider refuses a spec field it cannot honour instead of
ignoring it. The table above lists the providers; the sections below say what each one does with a spec.

### Daytona

`daytonaBoxes({ apiKey, snapshot })` talks to the Daytona REST API with `fetch`; there is no SDK, and the key stays in
the host. Daytona sets a box's size with its snapshot, so a spec that names `cpus`, `memoryMb` or `gpu` is refused:
choose the size by naming a snapshot. A box that outlives `maxLifetimeMs` is ended by Daytona (`ttlMinutes`, measured
at about 65 to 70 s for a one-minute lifetime). Daytona's label index trails a create and answers each query shape on
its own schedule, so `list` always asks Daytona for the `pi-transport` label alone and filters the other labels itself,
and `create` returns only once that listing shows the box. A listing therefore pages through every box this package
made in the organization. The default workspace is `/tmp/workspace`; one the sandbox's user may not create (below `/run`) is
made with `sudo -n` and given to that user, and a snapshot without a passwordless sudo fails the create naming the path. Programs run through the toolbox with their input
and output spooled in `/tmp/pi-transport`, polled about every 150 ms. A default sandbox is a container on the host
kernel (not gVisor); the `vm` class is mapped from a `linux-vm` snapshot but was not verified live in this run (a VM
snapshot could not be created in the test organization), so treat it as untested.

### Modal

`modalBoxes(sdk, { appName })` takes the host's own Modal client, so this package depends on no Modal SDK. The
client is typed structurally (`ModalSdk`); the `ModalClient` of the `modal` package, version 0.11.0, fits it.

```ts
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { ModalClient } from "modal";
import { modalBoxes } from "@parcha/pi-transport/box/modal";

const provider = modalBoxes(new ModalClient(), { appName: "my-boxes" });
await provider.create({ maxLifetimeMs: 10 * 60_000, isolation: "vm" }, BACKGROUND_CONTEXT);
```

- `isolation: "vm"` starts a VM with its own kernel. `"gvisor"` or no value starts Modal's default sandbox. Any other
  value is refused with `unsupported`.
- `gpu: "T4"` works on the default sandbox only; a GPU with `isolation: "vm"` is refused with `invalid`. `facts().gpu` is the model
  served, read in the box with `nvidia-smi` (a bare `H100` may be served an H200; `H100!` pins the type): one name for identical
  cards, every model, in the order first seen and joined by ` + `, for a mixed box (`NVIDIA A100 + NVIDIA H100 80GB HBM3`). The request stays what the caller passed. A box that asked for a GPU and
  reports none fails the create and is ended. On attach the read runs again and the box is handed back whatever it says (see "Attach never fails a live sandbox"): a model named is the fact, none named reads `gpu: null` and the caller ends the box through the handle, and when every try errs the handle keeps the model measured at create (from the tag), or the attach is an error for a box whose tag has none.
- A `snapshot` and any `isolation` but `gvisor` or `vm` are refused with `unsupported`; a label key that is `pi-transport` or starts
  with `pi-transport.` is refused with `invalid`; every box carries `pi-transport=1` as its own tag. No other field of the spec is ignored.
- `network: "none"` blocks all traffic from the box. `network: { allow: ["api.example.com", "*.example.com", "10.0.0.0/8"] }`
  allows only those domains and IPv4 CIDRs. A malformed entry, an empty list, or `ports` on a box with `network: "none"` is
  refused with `invalid`. The default is open.
- `ports: [8080, { port: 5432, kind: "tcp" }]` exposes ports: a bare number is TLS (`https`), `tcp` is a raw socket.
  `facts().ports` names each port and its kind, and `await box.endpoint(8080, context)` returns the address (an `https` URL, or a
  `tcp` host and port), never Modal's id for the sandbox.
- `capabilities: ["fuse"]` starts a VM (`isolation: "vm"` is implied) and checks, as root in the box, that `/dev/fuse` is a character
  device that root can open (a VM's is `crw-------`, root-only, and that is enough: a FUSE mount there is root's, and nothing widens
  the device for other users). `facts().fuse` is `true` only after that check; a box that fails it is ended and the create fails,
  naming what is missing. It does not prove a given FUSE program mounts, only that the device is there for root. `fuse` with
  `isolation: "gvisor"` is refused with `unsupported`, and with a `gpu` with `invalid`.
- `facts().secretsVia` is `"fd"`: a launch's stdin and secrets (the launch frame) ride the stdin of the one exec that starts the
  launcher. A Modal exec's stdin is a stream with an explicit end (`close()` sends the EOF, also with no byte written), written in
  512 KiB chunks. `secretsVia: "fd"` means: no client-controlled place (argv, the `env` and `secrets` parameters, tags, facts,
  errors, echoed output) and no place in the box but the program's pipes holds the secret. The provider is trusted with the box,
  and whether it keeps an exec's stdin is undocumented, so no claim is made about that. A `close()` that fails on a program still
  running is returned by `wait()` (the program would otherwise wait for an EOF that never comes); on a program that already exited it
  is ignored. The input is closed once: a `close()` after the first (a launch closes it after the frame, and `finish` closes it again) returns
  the first and is no failure.
- **Attach never fails a live sandbox and never ends it.** A box found again by `attach` is always handed back, and the caller ends it
  through the handle if its facts do not fit. The facts are read in the box again (a failed read is tried three times, with a pause).
  A read that answers gives the facts as measured, so a box with no usable FUSE reads `fuse: false`. If every try errs, the handle
  keeps the hardware the create measured (the disk, whether FUSE works), which is kept in a tag set through Modal's API: nothing
  inside the box can write it. A security fact (the Yama setting, the network, the ports) is never taken from a tag on a read error: it is
  left out, unknown. A box made before the tag existed has nothing measured to fall back on: if every try errs, the attach is an error (the box keeps running; try
  again), never a guess given as measured. The workspace setup `attach` repeats can fail too, and then the attach is an error as well (never an ok handle
  on a box that may have no workspace); nothing is ended or created. `attach` returns `undefined` for a box that does not exist or has ended.
- `facts()` also reports what the box has, read in it: `diskGb` (the workspace's filesystem, whole GiB; left out when unreadable or over 1 PiB, which is what gVisor's filesystem claims), `ptraceScope`
  (`kernel.yama.ptrace_scope`; a box without the file reads 0; `null` when it could not be read) and `startedAt` (an ISO 8601 UTC
  time; for Modal, the moment it was asked to start).
- `maxLifetimeMs` is Modal's own timeout, rounded up to whole seconds and capped at 24 hours. Modal ends the box when
  it passes, whatever happens to the host.
- The live suite spends money and runs only when asked. It needs `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` in the
  environment and the SDK, which is not installed with this package: `npm install --no-save modal@0.11.0`, then
  `PI_TRANSPORT_LIVE_MODAL=1 npm run test:live -w @parcha/pi-transport` (`test:live` loads every live file, and each
  provider's own variable picks the ones that run). It names its Modal app and labels from `PI_TRANSPORT_LIVE_PREFIX`
  and `PI_TRANSPORT_LIVE_LABELS`, and `PI_TRANSPORT_LIVE_MODAL_KINDS` (`sandbox,vm,gpu`; default `sandbox,vm`) picks
  the kinds.

The new facts are optional, and an absent one never grants anything: `readFacts(facts)` reads them the way that fails closed. `fuse`
absent is `false`; `network` absent is unknown (a caller that needs `none` or an allowlist must refuse the box); `ports` absent is
unknown, not none (a caller that must be sure no port is exposed must refuse; `noPortExposed` is true only for a list that is
empty); `ptraceScope` absent is unknown (do not assume 1 or more); `diskGb` and `startedAt` absent are unknown (a receipt falls back to
the time the create call answered). Only Modal reports all of them and can restrict a network to an allowlist, expose ports or give
`fuse`. Docker takes `network: "none"` (`--network none`), reports `ports: []` and refuses the rest. Daytona takes `network: "none"`
(`networkBlockAll`) and reports `network: "none"` only once the sandbox's own read says Daytona blocked it; a sandbox it did not block
(an organization on tier 1 or 2, whose own network policy holds) is deleted and the create refused as `unsupported`. It takes an
allowlist of domains (a `*.` wildcard allowed) as Daytona's domain list (`domainAllowList`, which Daytona's egress proxy enforces,
re-signing TLS with its own CA) and reports `network: "allowlist"`, with the hosts in `allow`, only once the sandbox's own read
carries that list (comma-separated, or a list of hosts); a sandbox without it, or with a value that is neither, is deleted and
the create refused as `unsupported` the same way. A CIDR in the list is refused. Daytona also
reports `diskGb` and `startedAt`. Local reports `network: "open"` and `startedAt`, the Archil exec box `startedAt`, and those two
and Daytona refuse every other network, ports and capabilities as `unsupported`.

`localBoxes` gives a program a fixed `PATH` (`/usr/local/bin:/usr/bin:/bin`, or the `path` option), a private `HOME`
and `TMPDIR`, and the spec's `env`. A detached watchdog ends a box when its lifetime has passed. On Linux, `destroy`
and the watchdog end every program of the box, also one that went to the background or left its process group. Where
`/proc` is missing they end only the programs the calling process started, and the watchdog removes the directory only.
It refuses `image`, `snapshot`, `cpus`, `memoryMb`, `gpu`, `region` and any `isolation` but `none`.

`dockerBoxes` runs `sleep` for the lifetime in place of the image's own entrypoint, so Docker ends the box. It refuses
`snapshot`, `region` and any `isolation` but `container`. Docker cannot choose a GPU model: any `gpu` gives the
container every GPU of the host. `dockerBoxes({ user: "1000:1000" })` runs every program of its boxes as that uid:gid
(`--user`), so what a box writes in a bound directory stays the host user's. `dockerBoxes({ cpus: 2, memory: "4g",
pidsLimit: 1024 })` bounds every box: `--cpu-period 100000 --cpu-quota 200000`, `--memory 4g` with `--memory-swap 4g` (so
swap never adds to it) and `--pids-limit 1024`. CPUs, the option's or a spec's, are the CFS quota `--cpus` would set,
which Docker never checks against its own CPU count (a VM's, under Docker Desktop) and drops with a warning where it has no
CPU controller; `--cpus` fails in both. `cpus` is at least `0.01`, `memory` is bytes or Docker's size (`512m`, `4g`), and
`pidsLimit` is at most 4194304. A spec's own `cpus` or `memoryMb` takes the option's place, and `facts()` says what the box
got. With none of them a box has no limit.
It alone binds a spec's `mounts` (`[{ source: "/data/run", target: "/workspace/run", readOnly: true }]`, canonical paths used as given, at most 8) with `--mount type=bind`, never `-v`, and refuses a source this machine lacks with `not_found`; `provider.mounts` says whether a provider binds them, and every other one refuses them as `unsupported`.
`dockerBoxes({ volumes: [{ name: "v", target: "/run/x", readOnly: true }] })` mounts a named Docker volume on every box
(`--mount type=volume`, after the binds): it lives on the daemon's own disk, so it carries what a file-sharing layer to a
VM does not, such as a Unix socket another container made there. A name Docker would not take, or a target that is not an
absolute path in normal form, throws at once; a spec's bind on a volume's target is refused `invalid`.
`dockerBoxes({ dropCapabilities: true })` runs every box with `--cap-drop ALL` and `--security-opt no-new-privileges`, for a
host whose boxes' programs never change user: as root a box then cannot make a device node in a directory it binds. With
it, `attach` ends a box that was made without it (by an older version) instead of taking it, so the caller makes a new
one. A launcher that drops from root to another user (setpriv, runuser) needs Docker's default set, so it stays the
default. The daemon is the one the docker CLI reaches: when `DOCKER_HOST`, or the docker context, names another machine
(tcp://, ssh://) rather than a unix socket, `provider.mounts` is false and a spec with mounts is refused as `unsupported`,
since that daemon would bind its own paths, not this machine's. With both `DOCKER_HOST` and `DOCKER_CONTEXT` set, both
must be this machine, and every create checks again, so a `docker context use` after the provider was made refuses the
next spec with mounts.

### Archil exec

`archilExecBoxes({ client, disk })` makes a box of a directory of an agent's Archil WORK disk (never the disk of its
store), and runs each `exec` as one Archil serverless `exec`: a fresh container with the disk mounted, one request that
answers when the command ends. It takes the host's own SDK, so this package depends on none: `client` is `new Archil({
apiKey, region })` and `disk` is a disk of it, from the `disk` package (version 1.9.0 was measured; `npm install
--no-save disk@1.9.0`).

```ts
import * as archil from "disk";
import { archilExecBoxes } from "@parcha/pi-transport/box/archil-exec";

const client = new archil.Archil({ apiKey, region: "aws-us-east-1" });
const { disk } = await client.disks.create({ name: "agent-work" });
const provider = archilExecBoxes({ client, disk });
```

The box is the directory `boxes/<id>/` of the disk. Its labels, facts and end time are the file
`boxes.records/<id>.json`, outside every directory a command can hold, so `attach` and `list` find the box after a
restart and a box can be ended while a command runs in it. These are the limits, each one in the facts or refused with
a typed error:

- **One request per command.** `facts().liveProcesses` is `false`: no program keeps running between calls, and a
  command's output arrives after it ended. Use it for tools.
- **A 300 s limit.** `facts().maxExecMs` is 300 000. A `timeoutMs` above it is refused with `invalid`; a command with
  none still gets a deadline of 295 s of its own inside the container, so nothing runs past the point where the
  platform cuts a call and loses its later writes. A command ended by its deadline is a `timeout` error; an exit code
  of 124, which is what ends a command at its deadline, is read that way too.
- **No stdin.** `stdin.write` rejects with `unsupported`. Output is text: bytes that are not UTF-8 come back as U+FFFD,
  so move bytes with `upload` and `download`, which use the disk's object API and carry every byte value (8 MiB measured
  in about 0.3 s each way).
- **Aborting the context ends the wait at once.** The SDK has no signal to stop the command in the container, so the
  command ends at its own deadline (an abort of the raw HTTP request was measured to stop a command at the platform, but
  not through this SDK).
- **Through `boxEnv` (the file and bash tools).** An environment on such a box reads and writes files through `upload` and
  `download`, so a write keeps every byte and a binary read is exact. Each transfer runs under the operation deadline and is
  cancelled with its call, so one still queued behind a command is not run after a timeout or an abort. An append is the bytes
  uploaded to a file of their own and one command that appends it, so a command queued beside it keeps its change. A binary
  reader refuses a file over its limit before moving a byte. A path outside the box's workspace can be read as text only, and
  not written. A command's own deadline is the caller's `timeout`, and a caller that gives up never has its command run later.
  A command stopped by a timeout or an abort cannot be killed: nothing reaches a running call, so it ends at its own deadline
  and the next call to the box waits for it. Temporary and spill files go under the workspace. A watcher bills one call per
  interval.
- **One command at a time per box, across processes.** Every `exec` checks out the box's directory with a queue
  (`queueMs`, default 60 s), so a second process that attached the same box (a restore, another host) waits for the
  first instead of being refused (a plain write without a checkout loses writes: 3 of 5 parallel appends were refused),
  and this process also runs a box's execs in the order they were asked. Boxes do not wait for each other. A file
  written through `upload` takes no checkout: it waits for this process's own commands and retries while another process
  holds the directory, up to `queueMs`.
- **`destroy` ends the box for every client at once**, whatever still runs in it, by deleting its record; the directory
  is removed through its parent (`destroyWaitMs`, default 5 s) and a directory a running command held is removed by the
  next `list`, which sweeps every directory with no record.
- **A lifetime is checked, not enforced by a timer.** Archil has no timer that ends a directory: a box past its
  `maxLifetimeMs` is ended, and its directory removed, by the first call that looks at it (`attach`, `list`, `exec`,
  `status`, `upload`, `download`). A box nobody calls again stays on the disk until a `list` finds it.
- **The isolation is one VM per call, and the disk is shared.** Each call runs in its own fresh VM container, but boxes on one
  disk share it: a command can read and write every box's directory, and the records, on that disk. Give each agent its own
  work disk, never one disk to two agents.
- **A box has no env.** Archil's exec carries only the command, so a value could only ride in the command or in the box's
  record on the disk, where any command on that disk can read it. A spec with an env (any name, any value) is refused with
  `unsupported`, a per-call env of a `boxEnv` environment on such a box (any name, and `PATH`) is refused with a
  `spawn_error`, and the record and every call's command carry none. A program gets what it is given as arguments or as
  files. Each exec container's own environment holds the platform's `ARCHIL_CONTAINER_ID`, `ARCHIL_DISK_ID` and
  `ARCHIL_REGION` (measured; no key).
- Refused before any call: a GPU, a snapshot, a `workspace`, an isolation other than `vm`, more than 2 vCPU or 3938 MiB
  (`unsupported`); a label in the providers' namespace, an env of any kind, a lifetime that is not
  positive (`invalid`).
- A box billed per call gets its receipt with `calls` in place of seconds (see
  [receipts](#what-a-box-cost-receipts)).

The live suite spends money and runs only when asked. It needs the key in `ARCHIL_API_KEY` and the SDK:
`PI_TRANSPORT_LIVE_ARCHIL=1 npm run test:live -w @parcha/pi-transport` (`PI_TRANSPORT_LIVE_ARCHIL_SDK` names the SDK's
entry file when `disk` here is another version). It makes its own new disk in `aws-us-east-1`, deletes it at the end and
checks that none is left.

## Hands: tools run in a box

`boxEnv(box)` is pi's `ExecutionEnv` over a box. Hand it to a pi-durable Harness and the session's `read`, `write`,
`edit` and `bash` tools run in the box, while the session, its store and its model calls stay with the host.

```ts
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { boxEnv } from "@parcha/pi-transport/hands";

// `box` is a box made as in "A box" above, before its destroy.
const env = boxEnv(box);
const written = await env.writeFile("notes/a.txt", "hello", BACKGROUND_CONTEXT);
const ran = await env.exec("wc -c notes/a.txt", { timeout: 10 }, BACKGROUND_CONTEXT);
```

- Every file operation and every command is one `Box.exec` of a fixed script. Paths are arguments, never part of the
  script.
- Every file operation has a deadline, and so has the start of a command. A box that ended or stopped answering is
  a tool error on that call and on every later one, with the reason in the message. A command runs as long as its
  caller's timeout allows; with none it has no deadline, as in pi's local environment, unless `maxExecMs` sets one.
- A command runs as a process group of its own. A timeout or an abort kills the whole group with a second exec, also
  on a box whose provider cannot stop a program it started.
- The kill is confirmed, not assumed. The kill exec reads back that no live process of the group is left (a zombie does not
  count: a container whose first process does not reap would otherwise keep a killed group "alive" for ever) and is sent
  again, at 1, 2 and 4 seconds, when it fails, times out or finds the group alive. The whole confirmation has 10 seconds of
  wall time, a call in flight included, so a stop answers `aborted` or `timeout` after at most 10 seconds more than before,
  and only when a kill call failed. When the group could not be confirmed gone, the error is a `StoppedCommandError`: its
  `killUnconfirmed` is `true`, and the command stays tracked, so `cleanup()` ends it with the rest. `killUnconfirmed` is
  absent when the group was confirmed gone, which is the normal case.
- A host that dies leaves its commands running (ending a `docker exec` client does not end its program), and its
  `cleanup()` with it. `endLeftCommands(box, context)`, from `@parcha/pi-transport/hands`, kills every command
  group an environment wrote down in the box: the host that takes a box over calls it once, before its first command.
- The variables a caller asks for, `PATH` included, reach the command alone. The environment's own scripts keep the
  box's search path, so a `PATH` with no shell in it still runs a program named by its path.
- Results match pi's `NodeExecutionEnv` on the same files: the same values and the same error codes. Messages differ.
  The suite checks this with pi's own environment conformance cases and with random operation sequences run against
  both.
- The environment's id is `pi-transport:<provider>:<box id>`. `~` is the box's home directory.

What a box must have: a POSIX `sh`, and the GNU versions of `dd`, `find`, `mv`, `mktemp`, `realpath`, `sleep` and
`truncate` with `setsid` (coreutils, findutils and util-linux, as in Debian and Ubuntu images). An image built on
BusyBox does not have them. A string command runs in `bash` when the box has it, else in `sh`. The environment keeps its
commands' process groups in `.pi-transport` under the box's temporary directory (`TMPDIR`, else `/tmp`).

Where it differs from `NodeExecutionEnv`, on purpose:

- A command that is stopped is asked to end first and killed a second later. Node kills at once.
- With `inheritEnv: false` a command still has a default `PATH`.
- A rename across file systems succeeds, because `mv` copies.
- `listDir` returns a name that is not UTF-8 with the replacement character. Node fails the whole listing.
- A watcher polls, so its `mode` is `polling`.
- `openBinaryReader` and `openTextLineReader` read the file whole when they open it, in one round trip to the box, and
  serve every later read from that copy on the host. A file above `maxReaderBytes` (64 MiB by default) is refused with
  `not_supported` before a byte of it leaves the box; a command (`head`, `tail`, `sed`) reads such a file in parts.
  Node reads the ranges it is asked for. `readTextLines` with `maxLines` brings only those lines out of the box.

Not covered by hands: MCP servers, code mode and model calls stay with the Harness.

### Where tools run is a line of config

`transport(config, host)` is a Harness's `env`. It reads the `transport` block of an agent's config and runs each
conversation's tools in the environment the block's `hands` names.

```ts
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import type { TranscriptContext } from "@earendil-works/pi-ai";
import { createModels } from "@earendil-works/pi-ai/models";
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
import { createRegistry, Harness, MemoryStorage } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { parseTransportConfig } from "@parcha/pi-transport";
import { localBoxes } from "@parcha/pi-transport/box/local";
import { transport } from "@parcha/pi-transport/hands";

// The model is scripted, so this costs nothing: it runs `hostname` with pi's bash tool, then prints the result.
const faux = fauxProvider();
const models = createModels();
models.setProvider(faux.provider);
faux.setResponses([
	fauxAssistantMessage(fauxToolCall("bash", { command: "hostname" }), { stopReason: "toolUse" }),
	(transcript: TranscriptContext) => {
		const result = transcript.messages.at(-1);
		if (result?.role === "toolResult") {
			console.log(result.content.map((part) => (part.type === "text" ? part.text : "")).join("").trim());
		}
		return fauxAssistantMessage("Done.");
	},
]);
const registry = createRegistry();
registry.install(CodingTools);
const storage = new MemoryStorage();

const config = parseTransportConfig({
	environments: { sandbox: { provider: "local", maxLifetime: "6h" } },
	hands: "sandbox",
});
if (!config.ok) throw config.error;

// The transport is the Harness's `env`, so it is built first and reaches the Harness through this binding.
let harness: Harness;
const hands = transport(config.value, {
	// For Docker: `provider: "docker"` above, and `docker: dockerBoxes()` here.
	providers: { local: localBoxes({ root: join(mkdtempSync(join(tmpdir(), "pi-transport-")), "boxes") }) },
	commit: (change, context) => harness.commit(change, context),
});
harness = await Harness.open(storage, { models, registry, env: hands.env }, BACKGROUND_CONTEXT);

const model = { provider: "faux", modelId: faux.getModel().id };
const conversation = await harness.createConversation(
	{ ownership: { kind: "ownerless" }, agent: { model } },
	BACKGROUND_CONTEXT,
);
await (await conversation.submit({ type: "input", content: "run hostname" }, BACKGROUND_CONTEXT)).wait(BACKGROUND_CONTEXT);

// The session is over: end its boxes (`release()` leaves them running for a host that opens the session again).
await hands.destroy(BACKGROUND_CONTEXT);
await harness.close(BACKGROUND_CONTEXT);
```

- **A box is created at the first file operation or command, not before.** pi asks for a conversation's environment
  at every prompt it prepares as well as at every tool call, so `env` returns a lazy environment. Until its box
  exists its `cwd` is `.` and its id ends in `:unopened`. A run that calls no tool creates no box.
- **A host that opens the session again finds its boxes.** A box's id is written to a session document
  (`transport.boxes`) before a tool runs in it, and the next `transport()` on the same store asks the provider for
  that box. Concurrent first uses share one creation.
- **One box for the session, or one for each conversation.** `scope: "session"` is the default; with
  `scope: "conversation"` every conversation gets its own.
- **Roles.** `hands: { default: "sandbox", build: "gpu" }` names a role. `hands.assign(conversation, "build", context)`
  points a conversation's tools at it, or at any declared environment, from its next tool call. The assignment is a
  conversation document (`transport.hands`). A fork starts without one, on the default.
- **A box that is gone is one failed tool call.** A box that cannot be created, or that ended behind the session's
  back, is that call's error result with the provider's code in the message. The next call asks for a box again.
  A box the environment found lost answers `exec` with code `unknown` (pi's execution codes have no `gone`) and, as
  `error.cause`, the provider's `BoxError` with code `gone`; `onLost` fires once, however many calls find it out.
- **Two ways to let boxes go.** `release()` forgets them and leaves them running, for a host that will open the
  session again; a box nobody comes back to ends at its lifetime. `destroy()` ends every box of the session,
  including those only the session's documents still name.
- **Labels are the host's.** `labels` in the host go on every box the transport creates, for example a session
  id, so the host can list what it left behind. The transport adds none of its own: `pi-transport` and the keys
  under it belong to providers, which mark their own boxes.
- **`undefined` means the host's own environment.** `env` returns `undefined` when the config names no `hands`, or
  names a `local` environment and the host passed no `local` provider. A host puts its own environment behind it:
  `(await hands.env(target, context)) ?? new NodeExecutionEnv({ cwd })`.

`transport()` throws a `TransportError` at start for a config it cannot serve: an environment whose provider the
host did not pass, `hands: "auto"`, and `expose`.

`expose` is not available yet: no secret is passed into a box. When it is, a secret will travel with each command
over the exec's input and be set in the box by the environment's launcher. It will never be in `BoxSpec.env`, which
a provider may store and return, and never in a command line.

The repository's `examples/01-hands.ts` runs one scripted session whose tools go where a config file says. It is not in the
npm package; from a checkout, in `packages/pi-transport`:

```sh
node --experimental-strip-types examples/01-hands.ts examples/hands.local.json 'hostname; pwd'
```

## A box's archive, unpacked on this machine (`./sync`)

A box is not trusted, so neither is a byte of a tar archive it makes. The host never unpacks one with `tar`:
`@parcha/pi-transport/sync` reads it with its own reader as it arrives and writes it under one directory.

```ts
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { readStream } from "@parcha/pi-transport";
import { extractTar } from "@parcha/pi-transport/sync";

const packed = await box.exec(["tar", "-c", "-f", "-", "-C", "/work", "."], undefined, BACKGROUND_CONTEXT);
if (!packed.ok) throw packed.error;
const tar = packed.value;
// Both streams are read: a program whose error pipe fills stops writing its output.
const [extracted, errors] = await Promise.all([
	extractTar(tar.stdout, "/data/runs/r1/work", {
		onSkip: (skip) => console.warn(skip.reason, skip.path, skip.target ?? ""),
	}),
	readStream(tar.stderr),
]);
const exited = await tar.wait();
if (!exited.ok || exited.value !== 0) console.warn("tar failed:", new TextDecoder().decode(errors));
if (extracted.ok && extracted.value.stopped !== undefined) console.warn(extracted.value.stopped.message);
```

- **Regular files and directories only.** A symbolic or hard link, a device, a pipe or any other member is skipped,
  counted by reason (`receipt.skipped`, the first 100 by path in `receipt.skips`, each one to `onSkip`), and never
  created. The skip of a link carries `target`, what the link names, so a caller that keeps links keeps them as data.
- **Safe names, and only the ones asked for.** A name is refused when it is absolute, holds `.`, `..`, an empty segment
  or a NUL, or passes 255 bytes a segment or 4095 a path. `admit` decides by path and kind what may be written; a
  second member of a path is skipped, and a pax global header is not honoured.
- **Nothing outside the destination.** Every directory is opened from a held handle of the one above with
  `O_NOFOLLOW`, so a link already in the destination is never followed (`link_in_destination`), and a file never
  replaces a directory or the other way round (`occupied`). Each file is written under a temporary name and renamed
  into place; one cut off halfway is removed.
- **Modes capped.** A file gets at most 0755 (0644 without an execute bit), never setuid, setgid, group or world write;
  a directory at most 0755. `receipt.masked` counts the files written that lost a bit.
- **Limits.** The extraction stops cleanly past `maxBytes` of archive (header blocks, pax and long-name payloads and
  member data, each charged before it is read; default `MAX_PULL_BYTES`, 1 GiB) or `maxEntries` members, written or
  skipped (default `MAX_PULL_ENTRIES`, 100,000). One pax header or long name holds at most 64 KiB. A bad checksum, a
  lone zero block or an archive cut short stops it too (`malformed`). What was written before a stop stays, every file
  whole, and `receipt.stopped` says why.

`openExtractor` is the same reader fed a chunk at a time: `write` until it returns `false`, then `finish`, which waits
for a write still running. `keep: "newer"` keeps a host file newer than the member. `removeEntries` removes paths
under a directory by the same walk: a file or a link is unlinked (never what a link names), a directory only when
empty. A failure of the host's own file system is `transfer_failed`; everything the archive does wrong is in the
receipt.

## A run's files between this machine and a box (`./sync`)

A run whose box shares no disk with the host keeps its files on the host and moves them: in when the box starts, back
after each turn. `@parcha/pi-transport/sync` does both through any `Box`, with the transport of the movers above (one
`tar`, streamed or as files by `facts().bulk`, the same stall limit, deadline and clean-up).

```ts
import { pullAgentHome, pullDir, seedAgentHome, seedDir } from "@parcha/pi-transport/sync";

await seedAgentHome(box, { from: "/data/homes/a1", to: "/run/agentrun/home" }, ctx); // learning/ goes read-only
const seeded = await seedDir(box, { from: "/data/runs/r1/work", to: "/workspace" }, ctx);
let mark = seeded.ok ? seeded.value.mark : undefined; // the box's listing, with the SHA-256 of each file sent
const pulled = await pullDir(box, { from: "/workspace", to: "/data/runs/r1/work", since: mark, mirrorDeletes: true }, ctx);
if (pulled.ok) mark = pulled.value.mark; // kept for the next pull
```

- **Seed.** Regular files and directories go. A link on the host is never followed (it could carry a file from outside
  the directory into the box): it is skipped and counted with pipes, sockets and devices. `exclude` leaves paths out,
  `readOnly` sends them without a write bit. Modes are capped at 0755.
- **Pull.** Incremental by listing: the box lists the directory (kind, size, mode, time to the nanosecond, every path
  NUL-separated) and a path comes back when it is new or listed differently from the `mark` of the seed or the last
  pull. The box's clock is never compared with the host's: a listing sees a file restored with an old time, and
  deletions. Dotfiles come back like any file. `exclude` is applied to the listing, to deletions and to every member of
  the archive. `mirrorDeletes` removes what the box no longer has: files and links unlinked, a directory only when
  empty. `pullAgentHome` never brings back `learning/` and never removes it. The host reads at most `maxListingBytes`
  of the listing (default `MAX_LISTING_BYTES`, 64 MiB): past it the box's program is stopped and the pull is
  `too_large`. A copy that fails after something was written still returns the mark, with
  `stopped.reason: "copy_failed"` and the copy's error; a file asked for and not written keeps its old entry.
- **A home file another run wrote stays.** `overwrite: "if_unchanged"` (always on in `pullAgentHome`) lets a pulled file
  replace, or a mirrored deletion remove, the host's copy only while that copy is still the bytes the mark records: the
  SHA-256 of what was seeded into the box, or of what the last pull wrote. A host copy that changed since (another run's
  pull, a host edit) wins; the box's file is not fetched, and the skip is counted and named as `host_changed`. The
  mark keeps the hashes: `seedDir` returns the first one (what it sent; what the box already held comes on the first
  pull), each pull the next. Pulls into one directory run one at a time in a process, from the first check to the mark,
  so two runs' pulls into one home never both pass the check against the same bytes.
- **The archive is not trusted.** Every pulled member goes through `extractTar` (above), never `tar`. Two names of one
  inode in the box both come back as files: the box packs with `--hard-dereference`, and a hard link member is still
  refused.

The box needs `sh`, GNU `tar` and `find`, and `split`, `sha256sum`, `cut`, `wc` and `cat` when it moves files, as the
movers do. No name is ever put in a shell string: the box reads the list on its standard input.

## Where a session runs: the placer

> **Deprecated:** The placer, with the `auto` and `purpose` config keys, is removed in 0.1.0-beta.31. To run an agent in
> a box, use `@parcha/agentrun-serve`: it uses this package's boxes, `./hands` and `./sync`, which stay.

A config whose `home` is `auto` lets Jev place the session. Each environment then has a `purpose`, the author's one
line on what it is for, which is what Jev reads for it (required when auto places the session, never made up from the
facts); `auto.rest` is where the session goes back when nothing needs it elsewhere (default the first declared
environment) and `auto.threshold` how sure Jev must be (default 0.5).

`jevPlacer(models, auto.classifier, { rest, threshold })` asks Jev through pi-ai's `Models.classify` (provider
`typesafe`, model `jev-latest`, on `TYPESAFE_API_KEY`; or Jev through OpenRouter's System One, provider `openrouter`,
model `~typesafe/jev-latest`, on `OPENROUTER_API_KEY`): one call a decision, two questions. `need` is whether the
step needs something `rest` cannot do, wherever the session is now; `where` is a choice among the environments, each
by its purpose. Jev reads the step and each environment's purpose and facts (GPU, CPUs, memory, capabilities,
isolation, region; not the provider, which names how a box is made). The session
moves only for a need: `need` at least the threshold and `where` elsewhere. A session away from rest stays while its
next step needs that place, and goes back to rest at a turn's end when nothing does, by the config. Code checks only
structure: the judgment is Jev's.

- **Jev or the config, nothing else.** Every decision is a `PlacementRecord` whose `by` is `jev` or `config`. A
  decision Jev does not give (an error, an answer that is not one, the placer's own deadline, 15 s by default)
  keeps the session where it is and says why in `error`.
- **No stand-in.** `jevPlacer` refuses to build without the classifier model (`no_classifier`) or its provider's key
  (`no_key`).
- **On the key you have.** `jevClassifier(process.env)` picks the classifier as serve's decisions do: TypeSafe's own
  API when `TYPESAFE_API_KEY` is set, else Jev through OpenRouter when `OPENROUTER_API_KEY` is, else `undefined`. The
  host's `Models` holds pi-ai's `typesafeProvider()` and `openrouterProvider()`.
- **Cost.** A decision's usage counts tokens; `costUsd` is `null` when the classifier has no price (pi-ai's catalog
  gives TypeSafe's own API none, and prices Jev through OpenRouter): unknown, not 0.

`./testing` has `replayClassifier`, TypeSafe as a replay set keyed by the canonical question (an unknown question is
an error, never an answer), and `recordingClassifications` to record one. This package's set, 40 decisions recorded
against Jev, replays in its tests offline.

## What a box cost: receipts

`boxReceipts` names each box by a label (`box-1`, `box-2`, never the provider's id) and writes its receipt when it
ends: the provider, the kind of box, when it started and ended, and its cost at the rates the host passes. This
package holds no price table; a box the host gives no rate gets no receipt.

`boxReceipts({ prices, write })` keeps a ledger of the boxes its providers make: `ledger.wrap(provider)` is the same
provider, and each box it makes gets one receipt when it ends, destroyed through it, found ended by `status`, or found
gone by `attach` (a lifetime that ran out, or a sweep). The host writes each receipt as a live line, source
`transport`, kind `box.receipt`: `{ box, provider, kind, from, to, seconds, usd_per_second, cost_usd, source, priced }`;
a box its provider bills per call instead (`archil-exec`) carries `calls` and `usd_per_call` in place of `usd_per_second`,
which is then absent, never 0 (RECORDING.md draft 40).
`box` is a label, `box-1`, `box-2` as the boxes are made, never the provider's id. Hand the wrapped providers to
everything that makes boxes: a box place, and the transport's hands pools (`TransportHost.providers`).

A host that labels boxes of its own passes `label(made)`: it is called once when a provider has made a box, with that
box's own request and facts (`{ provider, kind, spec, facts }`, its id left out), so a host reads its label from the
request it made (`spec.labels`), never from the order it asked in: boxes asked for together, from providers that answer
at different speeds, are made in any order. After a restart, `taken` passes the labels the recording already holds (its
`box.receipt` lines), and the default labeler numbers on past them. A box the killed process made or was making is not
the new ledger's: no receipt is written for it, so its cost stays unknown.

The prices are the host's (`prices: [{ provider, kind, usdPerSecond, source, priced }]`, with `source` the page a price
was read from and `priced` the date it was read): this package holds no price table. A box's `kind` is found from its
facts (`gpu:<gpu>` for a box with a GPU, else its isolation), or by the host's `kindOf`. A box with no price writes no
receipt, so its cost stays unknown, never 0. `cost_usd` is an estimate at list price, `seconds` times the price per
second: not the provider's invoice, which bills by its own rounding, minimums and discounts. A price with `usdPerCall`
(and no `usdPerSecond`) is for a box billed per call: its receipt counts the `exec` calls the box started, one aborted or
given up on included, and `cost_usd` is `calls` times `usd_per_call` rounded to 9 decimals; `seconds` stays its lifetime,
for display only. A call refused before it is made is not counted. A box another process or host made is its maker's to receipt.

## Launch: a program kept alive in a box

> **Deprecated:** `@parcha/pi-transport/launch`, with `Box.launcher`, is removed in 0.1.0-beta.31. To run an agent in a
> box, use `@parcha/agentrun-serve`: it uses this package's boxes, `./hands` and `./sync`, which stay.

A box has no init system, and a host's `exec` ends when the host goes away. `launch` keeps one program running in a box
whatever happens to the host. It uploads a small `sh` supervisor and starts it in its own session, so the program
outlives the call, the `exec` and the host process. The launcher is `@parcha/pi-transport/launch`.

```ts
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { launch, launchState, launchStop } from "@parcha/pi-transport/launch";

// `box` is a box made as in "A box" above.

const started = await launch(box, "runner", { argv: ["node", "runner.js"], env: { APP_MODE: "home" } }, BACKGROUND_CONTEXT);
if (!started.ok) throw started.error; // a LaunchError: invalid, unsupported, refused, unsafe, box, timeout or failed
const handle = started.value.launch; // { v: 1, name: "runner" }: JSON, no secret
const state = await launchState(box, handle, BACKGROUND_CONTEXT); // read from a file in the box
await launchStop(box, handle, { graceMs: 5_000 }, BACKGROUND_CONTEXT);
```

`launchStatus(box, name, ctx)` and `stopLaunch(box, name, options, ctx)` do the same by name. `launchSpecError(name, spec)` is
the spec's own check, pure (no box, no I/O): the same `invalid` error `launch` would give, or undefined, for a host that asks
before it makes a box to launch in.

- `launch` twice with the same name starts nothing and returns the first one's state. A launch that has ended
  (`exited`, `gave_up`, `stopped`) starts again.
- The program restarts when it exits, unless the exit code is `0` or in `terminalExits`, or `restart` is `false`. More
  than `restartLimit.burst` restarts in `restartLimit.intervalMs` (default 5 in 60 s) ends in `gave_up` with the last
  exit code. Numbers must be finite and within bounds.
- The state is read from the launcher's own state file, never guessed from whether a process lives. It is `restarting`
  or `exited` while the launcher handles an exit, never `running`. It carries the launch's handle (`launch`), the
  `incarnation` now or last running (from 1), and `restarts` (the incarnation less one).
- `stopLaunch` ends the program and everything in its process group: TERM, then KILL after `graceMs`, then it reports
  `stopped`. `graceMs` is a number from 0 to 600000 and the call to the box is given that long on top of its usual time.
  The saved process id is signalled only while it still names the process the launch started; the id of a process that
  ended and was given to another job is left alone.
- `beforeStop` (`argv`, `timeoutMs` from 100 to 120000, default 30000) runs ONCE per launch, after the drain and
  before the final kill, as the launcher's own user, in a session of its own. One past its bound is killed (137). Its
  exit goes to `out.log`, and it never fails the stop.
- A stop is ONE stop's. The first one claims it, and the phase is `stopping` until it has killed what was left and
  written `stopped`; nothing else writes the state meanwhile.
  - A second stop at the same time never kills: it waits for the first one's end and returns its state. One whose own
    bound (its `graceMs`, the hook at its longest, and 3 s) ends first returns `stopping`.
  - A stop whose first one died (past its deadline: that stop's grace, its hook's bound and 3 s) is taken over by the next stop.
  - A `launch` of the name while it is stopped waits for the stop's end, then starts anew, never beside the hook. Past
    the stop's deadline it is `failed` ("stop it again").
  - What a stop returns, the second stop's included, is the state of the incarnation it stopped, as the launcher
    recorded it before it released the name: never that launch of the name, and never text the program or its hook
    printed. The record rests in the spool as `.stopped.<name>.<generation>` until a later reclaim of the name finds
    it older than any stop that could wait for it.
  - A stop is for the launch the call found: one whose script reaches the box only after that launch has ended and
    another of the name took the directory returns the launch it was for, from that record (or is `invalid`, as for a
    launch that does not exist, when there is none), and leaves the new one alone. This holds only when the host and the
    launch's `launcher.sh` are both of this version: a stop from a host that passes no generation, or for a launch an
    older `launcher.sh` runs, behaves as before, so a late stop can still act on a later launch of the name.
- Callers that meet are safe: several `launch` calls for one name start one program, a claim whose caller died before
  filling it is taken over after a minute, and a launch that ended is replaced by one caller only.
- The program's output goes to `out.log` in the launcher's directory, bounded to its tail (`logMaxBytes`), also after
  the program has exited. The launcher holds no pipe to the host.
- The launcher keeps its files in a spool, `.pi-transport-launch` in the box's `TMPDIR` (or `/tmp`), one directory per
  launch. The spool and each launch directory must be the launcher's uid's alone: a directory, never a symbolic link
  (read without following it), owned by that uid, with no group- or other-write bit. Every directory above the spool,
  up to `/`, must be root's or that uid's, never a symbolic link, with no group- or other-write bit unless it has the
  sticky bit (as `/tmp` does): else another user could rename the spool away and put another in its place. `TMPDIR` is
  read as the directory it names (`pwd -P`). Every call checks all of them before it uses them, a spool that already
  existed like a new one, and the launcher checks them again before it starts a process from them (a restart, its
  watchdog, the tools user's check) and before a stop's hook. A failed check is `unsafe`, naming the directory and why,
  and nothing runs from it. A local box makes its own directories (its `home`, `tmp` and `pids`) mode 700.
- The program's environment is PATH, HOME, LANG (`C.UTF-8`), `PI_TRANSPORT_INCARNATION` (its incarnation) and `env`.
  Nothing of the box runtime's own environment reaches it unless `inheritEnv` names it (default `false`): the
  incarnation unsets every other exported name, and a name the shell cannot unset refuses the incarnation (exit 126).
- `env` is plain settings kept in a file of the box. Never put a secret in it: a key-shaped name (a `_`-separated
  segment KEY, TOKEN, SECRET, PASSWORD, PASSWD, CREDENTIAL or CREDENTIALS) is refused by name, as are PATH, HOME, LANG
  and the names the launcher sets. So are the shell's, the loader's (`LD_*`) and every variable the C library itself strips
  in secure mode (glibc's `UNSECURE_ENVVARS`: `GCONV_PATH`, `GLIBC_TUNABLES`, `LOCPATH`, `NLSPATH` and the rest, and musl's
  `MUSL_LOCPATH`; `TMPDIR` excepted), in `env` and `inheritEnv`: `env` is exported before a root launcher drops to `user`, so they would act on
  that switch. The launcher clears them at every exec that starts its shell, and reads only `/proc`.
- A box may launch natively (`Box.launcher`, a test's fake box): the three calls then use it, read on every call. A
  provider leaves it undefined.
- `stdin` (bytes, at most 64 KiB; disk's mount token) is written to EVERY incarnation's stdin and then closed. `secrets`
  (names to values, at most 64 KiB as JSON) reach every incarnation on one inherited fd, whose NUMBER is in
  `PI_TRANSPORT_SECRETS_FD` (3): one JSON object, then EOF. Both travel on ONE path, the launch frame below, and rest
  nowhere: never in a file of the box, an environment, an argv, the state or `out.log`. Only a box whose facts say
  `secretsVia: "fd"` (a real stdin path; `local` and `docker`) takes them; any other refuses `unsupported` before
  anything runs.
- The launch frame: the exec that starts the launcher gets, on its stdin, the line `PI-TRANSPORT-LAUNCH/1`, then the
  secrets as JSON and the stdin, each base64 (standard alphabet, padded, unwrapped) on a line of its own (empty when
  absent), then EOF. The launcher keeps the two parts in unexported shell variables and decodes them, for each
  incarnation, straight into pipes; it refuses where `printf` is not a shell builtin or `base64` is missing. A malformed
  frame is `invalid`, and nothing starts.
- The in-box watchdog never brings back the supervisor of a launch that has `stdin` or `secrets`: only that supervisor
  held them. The launch then reads `lost`, and only a `launch` that passes them again starts it again.
- `user` (a name or a numeric uid) is who the program runs as, resolved in the box (`/etc/passwd`, then `getent`). A
  root launcher finds how it drops to it by doing it once at the start: a `setpriv` that takes `--reuid` (util-linux;
  BusyBox's does not), else `runuser`, else the launch is `unsupported` (Alpine's base image). A launch with `stdin` or
  `secrets` runs its program as root only with a `toolsUser` (below); a launcher that is not root runs the program only
  as itself (another user is `unsupported`). An unknown user is `invalid`. The state reports it (`user: { name, uid }`).
- A launch with `stdin` or `secrets` whose program does not run as root needs the box's ptrace scope
  (`kernel.yama.ptrace_scope`) at 1 or more, or a process of the same user could read them from the program's memory. It
  is measured in the box before the frame is read: a missing file counts as 0, a 0 on a box with its own kernel (`vm`)
  is raised to 1 and read back, and any other 0 is `refused`.
- `toolsUser` (a name or a numeric uid) goes with a program run as root (no `user` on a root launcher, `"root"` or uid
  0). It is the user that program runs everything it does not trust as: the user disk's `--run-as` names. A launch with
  `stdin` or `secrets` runs its program as root only with one, and then needs no ptrace scope: a process of the tools
  user cannot attach to a root one without `CAP_SYS_PTRACE`. The launcher checks it in the box, before it reads the frame,
  in at most 60 s (the start's call to the box, a revive's too, is given that and its usual 30 s on top):
  - it exists, is not root and has not the root group (else `invalid`);
  - where the box has `sudo`, `sudo -l -U`, run with no environment but PATH, answers exactly its sentence for a user who
    may run nothing (else `refused`, with sudo's first line);
  - on every mount without `nosuid`, it can run no setuid or setgid root file and no file with capabilities. The mounts
    are chosen by filesystem type from the mount table, never by mount point: the kernel's own pseudo filesystems (`proc`,
    `sysfs`, `cgroup`, `cgroup2`, `devpts`, `mqueue`, `securityfs`, `debugfs`, `tracefs`, `bpf`, `pstore`, `configfs`,
    `fusectl`, `binfmt_misc`, `efivarfs`) are skipped, and every other type, an unknown one, `tmpfs` and `devtmpfs`
    included, is walked. Each file is found as root and tested as the tools user, so a directory it can pass through but
    not list hides nothing. Else the launch is `refused`, naming up to five; a box without `getcap` is `unsupported`. A
    walk ignores an entry that ends mid-walk only where `find` takes `-ignore_readdir_race` (the control runs it); on a
    `find` without it (busybox's) that stays `unsupported`, and every other `find` error is `unsupported` on every `find`.
  - the check fails closed: `find` and `getcap` must each report a probe it plants (empty, root's, in the launch's own
    root-only directory, removed before anything runs as the tools user), the tools user's test must pass on its own
    shell, and a walk that fails on a mount, or finds no mount to walk, is `unsupported`, as is a getcap line that
    names no file (a line is read to its file whatever its form: several capability groups, `path = caps`, a path
    with spaces). The image needs `getcap` and
    `setcap` (Debian's `libcap2-bin`).
  The program gets `PI_TRANSPORT_TOOLS_USER` and `PI_TRANSPORT_TOOLS_UID`; the state reports `toolsUser: { name, uid }`.
- What `toolsUser` means is the HOST'S guarantee: the program runs everything it does not trust as that user, with no
  capabilities, no supplementary groups and none of its own descriptors (fd 3 and its stdin included), as disk's own
  spawn does. The launcher cannot see the program's children, and it does not measure, so the host and its image keep:
  - no root-run code the tools user can write (the program's files, a PATH directory, a cron or service unit);
  - no root service the tools user can reach on a socket;
  - no later change by root: a sudo rule, a setuid file, a capability, or a mount without `nosuid`.
  The residual: root in the box can still trace and read the program, so the image is trusted. A program run as root
  shares root with every root process in the box (the launcher, any other root launch, a root daemon), so its secrets
  are only as safe as the least trusted of them.
- If both launcher processes die, the state reads `lost`: the program may still run, unsupervised. The next `launch`
  of the name adopts it and starts a supervisor again.
- On Daytona a stopped and started sandbox keeps its disk and loses its processes. The launcher then reads `lost`, the
  program is gone, and nothing restarts by itself: the host calls `launch` again, and the old state and output are
  still there.

The box needs `sh`, `setsid`, `sed`, `cut`, `awk`, `wc`, `tail`, `date`, `sleep`, `id` and `ls`, and for a launch with
`stdin` or `secrets` a builtin `printf` and `base64`. It is tested on Debian (dash) and Alpine (BusyBox).

`probeAcknowledgedWrites(box)` in `@parcha/pi-transport/testing` checks that a write a program was told was durable
survives that program's death: a writer appends ten records, each followed by an `fsync`, then it is killed and a second
process counts them. A runtime is fit to hold a session's disk only when every acknowledged record is kept.

## Testing a host against a fake provider

`fakeBoxes(options)` (`@parcha/pi-transport/testing`) is a `BoxProvider` that does what a test scripts and writes down every call it
was asked, so a test of a host (anything built on a `Box`) asserts what the host asked and how it behaves when a provider misbehaves,
with no machine, key or network. It follows the `Box` contract (typed errors as values, stable ids, labels, facts), so a test written
against it holds on a real provider. It runs no real program: a program in a fake box is a function a test registers with
`provider.program(match, run)`, and a box's files are in memory (`box.files`).

```ts
import { fakeBoxes, fakeClock, modalLike } from "@parcha/pi-transport/testing";

const provider = fakeBoxes(modalLike({ clock: fakeClock() }));
provider.program("inst", async ({ stdin }) => { await stdin.readAll(); return 0; });
provider.script.on({ op: "create" }, { fail: "unknown", leakBox: true }); // fails, but the box exists and `list` finds it
```

- `provider.calls` is every call in the order the provider saw it (`seq`, `op`, `box`, exact `argv`, env names never values, the stdin
  bytes written, file paths and sizes, the spec, the typed result, and the label of the script rule that decided it). A failed
  `create` is in it. `provider.leaks(marker)` says where a value appears in anything recorded (argv, stdin, an upload's bytes or
  path, env, a label, the spec): the check that a secret travelled only on the channel it was meant for.
- `provider.script.on(match, outcome, { times, label })` decides a call: a typed `fail`, a `delayMs`, a `hang` that ends on abort or
  timeout, a lost end of an empty stdin (`lose: "stdin-eof"`, the `docker exec -i` fault), a dropped or truncated output stream, a
  scripted `exit`, a box that goes `gone` in the middle of an exec, a `refuse`. A match names the op, a box, the nth call, a command
  or any predicate over the call. `fakeClock()` moves time only when a test says (`advance`): delays, `timeoutMs` and a box's
  `maxLifetimeMs` all run on it.
- Deprecated, removed in 0.1.0-beta.31 with the launcher: `FakeLaunch` and the launcher half of the fake that this
  bullet describes. Each fake box fills `Box.launcher`, so the launcher's own `launch`, `launchState` and `launchStop` (`@parcha/pi-transport/launch`)
  run over it with no exec of a launcher program. A launch has a scriptable state (`phase`, `incarnation`, `restarts`, `lastExit`),
  restarts under the spec's policy (`restart`, `restartDelayMs`, `restartLimit`, `terminalExits`), the stdin replayed to every
  incarnation, `secrets` on the program's secrets channel, and a stop that drains, runs the `beforeStop` program, then kills. The
  stdin and secrets are recorded on the `launch` call only, so `leaks(token)` finds them there and nowhere else; each start is an
  `incarnation` call, never an `exec`. Errors are the launcher's `LaunchError`. A rule can fail a restart
  (`{ op: "incarnation", when: (c) => c.incarnation === 2 }`), make the state unreadable (`{ op: "launchState" }` with a `fail`), or
  end the program with a chosen code (`{ op: "launch" }` with an `exit`); `fake.launches[i].exit(code)` does it later.
- `modalLike()` (a VM with FUSE and a ptrace scope a launcher can raise) and `daytonaLike()` (a container with no FUSE and a scope that
  cannot be raised) are presets; `fuse`, `startedAt` and `ptraceScope` are extra facts until the box contract carries them.

The fake does not run a shell, a real process or a mount: those are the real providers' and their live suites'.

## Writing a provider

Fill `BoxProvider` and `Box`, then run the conformance suite against real boxes:

```ts
import { describe, expect, it } from "vitest";
import { registerBoxConformance } from "@parcha/pi-transport/testing";

registerBoxConformance({ describe, expect, it }, "my boxes", (use) => use(myBoxes()), {
	// One box is left alone for its lifetime. `ended` must ask the vendor's own API, never the provider under test.
	lifetime: { maxLifetimeMs: 60_000, graceMs: 120_000, ended: (box) => myVendorSaysGone(box.id) },
});
```

A box whose `exec` carries input and streams output can take `uploadByExec` and `downloadByExec` as its file calls.

A provider whose boxes keep no program running between calls and take no input (`facts().liveProcesses` is false) passes
`withoutLiveProcesses: "<why>"`: the suite then leaves out the cases that need a live program or stdin, and one case in
their place requires the facts to say so and stdin to be refused with `unsupported`; the reason is in its name.
A provider whose boxes have no env passes `withoutEnv: "<why>"`: the suite creates its boxes with no env, leaves out the case that
reads the spec's env, and adds one that requires a spec with an env to be refused with `unsupported`; the reason is in its name.
