# Runtimes (/docs/scruff/runtimes)



```sh
scruff runtime up    my-lane --backend tart
scruff runtime enter my-lane --backend tart
scruff runtime down  my-lane --backend tart
```

A runtime backend hands **one lane** to an isolation environment, a VM or a
container, and takes it back out again.

**Nothing here is automatic.** Creating and reaping a lane never touch a
backend, there is no default one, and `--backend <id>` is required every single
time. A lane pays a VM's boot and disk only when somebody asks for it by name.
`enter` replaces scruff with the session; the backend keeps running until a
separate `down`.

## tart: the one that ships built in [#tart-the-one-that-ships-built-in]

```sh
export SCRUFF_TART_BASE=ghcr.io/cirruslabs/macos-tahoe-base:latest   # or your own image
scruff runtime up    my-lane --backend tart    # clone it, boot it headless, wait for a shell
scruff runtime enter my-lane --backend tart    # ssh in as $SCRUFF_TART_USER (default: admin)
scruff runtime down  my-lane --backend tart    # stop and delete the clone
```

A lane's guest is named `scruff-<lane>`, and the lane's worktree is **shared**
into it at `/Volumes/My Shared Files/work`. Shared, not copied, so there is one
copy of the work and no sync step to get wrong.

The guest boots `--no-graphics`, which does not mean "no UI". It runs a full
WindowServer and draws the real interface; it just draws it to nothing.

<Callout type="info" title="This is how an agent sees a change without taking your screen">
  `screencapture -x` over ssh into that guest returns **real pixels**, and
  `osascript` sends real keystrokes. So an agent that has to look at a menu bar, a
  window animation or an installer can boot a disposable macOS and drive it as
  hard as it likes, while your pointer, your focus and your desktop stay yours.
  Prefer it over asking to drive the machine you are on.
</Callout>

Three environment variables and no config file. `SCRUFF_TART_BASE` is required
and has no default on purpose: the images are tens of gigabytes and which one
you want is a real choice, so scruff refuses (**exit 2**) rather than pulling
one you did not ask for. `SCRUFF_TART_USER` is the guest account, `admin` by
default, which is what every cirruslabs base image ships. `SCRUFF_TART_SSH_WAIT`
is how many seconds a booted guest gets to start answering ssh, `180` by
default.

**`up` returns when a shell answers, not when the guest has an address.** `tart
ip` answers the moment the guest takes a DHCP lease, most of a minute before sshd
accepts anything, so scruff waits for the shell and prints the address only once
something has answered on it. If nothing does inside the wait, `up` exits **1**
naming the boot log and leaves the guest running for you to look at; `down`
removes it.

The guest's console goes to that boot log, `$SCRUFF_STATE/runtime/scruff-<lane>.log`,
never to your terminal. So `up` is safe to read through a pipe or a `$( )`: it
returns while the guest is still running, and nothing the guest prints later
lands in your shell.

`enter`, and the readiness probe before it, keep the guest out of your
`known_hosts`. vmnet hands the same address to the next clone, which is a
different host with a different key, and one saved entry would turn every later
lane on that address into a host-key refusal. Drive the guest yourself with the
same two options:

```sh
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null admin@$(tart ip scruff-my-lane)
```

Needs [`tart`](https://tart.run) on `PATH`. Without it the verb **degrades**
with the install command rather than failing.

It is built in only because its `setup` is four commands and two waits, which one
argv slot cannot hold. Nothing else about it is privileged: your own `tart.toml`
still wins wholesale, and

```sh
scruff runtime eject tart
```

prints the built-in as an adapter file to start from.

## Every other backend is a file you write [#every-other-backend-is-a-file-you-write]

```toml
# ~/.config/scruff/adapters/runtime/apple-container.toml
kind     = "runtime"
id       = "apple-container"
setup    = ["container", "run", "-d", "--name", "scruff-{{.Name}}", "-v", "{{.Path}}:/work", "IMAGE"]
enter    = ["container", "exec", "-it", "scruff-{{.Name}}", "bash"]
teardown = ["container", "rm", "-f", "scruff-{{.Name}}"]
```

Each value is an **argv slice, not a command line**. It is executed directly,
with no shell in the way, so a branch name with a space in it is one argument
rather than a quoting bug. The `{{…}}` variables are
[the set every adapter shares](/docs/scruff/config#template-variables).

## Five failures worth telling apart [#five-failures-worth-telling-apart]

|                                                                                      |                                                                                                           |
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| no such adapter file                                                                 | **2**, refused, naming the path it looked for. `tart` is the one id that falls back to a built-in instead |
| `--backend tart` with no `SCRUFF_TART_BASE`                                          | **2**, refused, naming the image to pull                                                                  |
| `--backend tart` took an address but no shell answered inside `SCRUFF_TART_SSH_WAIT` | **1**, naming the boot log. The guest is left running to be looked at; `down` removes it                  |
| the backend's own binary is not on `PATH`                                            | **3**, degraded. Install it and the same command works                                                    |
| the backend ran and exited non-zero                                                  | **1**. It attempted the thing and failed at it: a VM that already exists, a full disk, a bad image        |
