---
name: krillbox
description: Spawn, connect to, and operate on-demand SSH-gated Linux microVMs ("boxes") on krillbox. Boxes boot the moment you connect and suspend when idle. Use when you need a real, isolated VM to run commands, build, or persist a per-tenant workspace.
homepage: https://krillbox.gra.ke2.cloud.ovh.net
---

# krillbox -- agent skill

krillbox gives you ephemeral, isolated Linux **boxes** -- real microVMs (Cloud
Hypervisor + KVM), not shared-kernel containers. You SSH to a name; the gateway
boots or wakes the box and proxies you to its `sshd`. Idle boxes are snapshotted
and suspended, then resumed in well under a second on the next connection. The
gateway host is `krillbox.gra.ke2.cloud.ovh.net`.

## Mental model

- **Box** -- one microVM, addressed by name. Spawned on first connect, woken on
  later connects, suspended when idle. State on disk survives suspend and host
  migration.
- **Tenant** -- your identity (derived from your SSH key / API token). Your boxes
  and your `/wrk` workspace are private to it.
- **Golden image** -- the template a box is cloned from (instant ZFS copy-on-write).
- **No explicit lifecycle needed** -- connecting is create-or-wake; walking away is
  suspend. You rarely manage state by hand.

## Quickstart

The SSH **user** is the endpoint (`box@` = spawn-or-wake, `cli@` = run a CLI verb),
not the box name. The box name comes from a wildcard ssh-config `%h`, or you name it
explicitly via `ssh cli@<host> box up <name>`.

```bash
# Spawn-or-wake and get a shell (your default box).
ssh box@krillbox.gra.ke2.cloud.ovh.net

# Recommended ~/.ssh/config so `ssh mybox.box` just works (name passed as %h):
#   Host *.box
#       User vmuser
#       ProxyCommand ssh -p 22 box@krillbox.gra.ke2.cloud.ovh.net %h
# then: ssh mybox.box

# One-off command / detached job / file copy:
ssh box@krillbox.gra.ke2.cloud.ovh.net uname -a
ssh box@krillbox.gra.ke2.cloud.ovh.net run ./build.sh        # detached job
scp ./file box@krillbox.gra.ke2.cloud.ovh.net:/root/

# Inline image / flavor (box[:image[:flavor]] — 1st colon image, 2nd flavor):
ssh box:debian-docker@krillbox.gra.ke2.cloud.ovh.net      # pick an image
ssh box::big@krillbox.gra.ke2.cloud.ovh.net               # bigger flavor (empty image → double colon)
ssh box:debian-docker:big@krillbox.gra.ke2.cloud.ovh.net  # both
```

## Connecting -- two modes per transport

Both the SSH and HTTP front doors offer the same choice: let the **gateway
terminate** (it sees plaintext, renders menus, proxies you in with its key), or a
**pass-through tunnel** (gateway shuttles ciphertext only; your client talks to the
box directly). Pick by how much you trust the gateway.

### SSH

| | Gateway mode (default) | End-to-end jump |
|---|---|---|
| Command | `ssh box@krillbox.gra.ke2.cloud.ovh.net` | `ssh -J krillbox.gra.ke2.cloud.ovh.net root@box` |
| Terminates | Gateway -> proxies to box `sshd` | Client <-> box (raw relay) |
| Gateway sees | Plaintext | Ciphertext only |
| Auth to box | Gateway proxy key | Your own key (injected at launch) |
| Default class | Standard | Confidential by default |

```bash
ssh box@krillbox.gra.ke2.cloud.ovh.net                       # gateway terminates & proxies
ssh -J krillbox.gra.ke2.cloud.ovh.net root@mybox             # end-to-end; gateway is a dumb jump
ssh -J krillbox.gra.ke2.cloud.ovh.net root@mybox+1h:debian-default   # pin lifetime + image
```

`scp`/`sftp`, agent-forwarding, and `-L`/`-R` port-forwards work through both modes.
On `-J` the gateway never holds a key to your box and cannot read the session; an
unpinned `-J` target is provisioned **confidential**.

### HTTP

| | Gateway mode | CONNECT proxy |
|---|---|---|
| Endpoint | `/ws/<box>` (WebSocket PTY), `/exec` | HTTP `CONNECT` tunnel (proxy port) |
| Terminates | Gateway renders a terminal | Client <-> box (box-direct TLS) |
| Gateway sees | Plaintext PTY | Ciphertext only |

```bash
wss://krillbox.gra.ke2.cloud.ovh.net/ws/mybox                            # browser/agent PTY
curl -x https://krillbox.gra.ke2.cloud.ovh.net:8081 https://mybox.internal/   # box-direct tunnel
```

Confidential boxes are **never** gateway-terminated -- they're reachable only over an
end-to-end tunnel (`-J` / CONNECT).

## In-band CLI

Run gateway CLI verbs without opening a box shell via the `cli@` user, or over HTTP:

```bash
ssh cli@krillbox.gra.ke2.cloud.ovh.net box list
ssh cli@krillbox.gra.ke2.cloud.ovh.net box suspend mybox
ssh cli@krillbox.gra.ke2.cloud.ovh.net box resume mybox
# HTTP equivalent (authenticated tenant):
curl -H "Authorization: Bearer $TOKEN" -d 'box list' https://krillbox.gra.ke2.cloud.ovh.net/cli
```

## Run a command over HTTP -- `POST /exec/{box}`

Run one command in a box (spawned/woken on demand) and get its **exit code,
split stdout/stderr, and stdin** -- the programmatic counterpart to SSH. Auth is
`Authorization: Bearer <apikey>`.

**Request body** is "framed JSON + raw stdin": a JSON header line, then a newline,
then arbitrary bytes piped to the command's stdin until EOF. Use
`Content-Type: application/x-runtime-exec`.

```text
{"cmd":"jq -r .name","args":[],"env":{"FOO":"bar"},"image":"","flavor":"","stay_alive":300}
<optional stdin bytes...>
```

Header fields: `cmd` (required), `args[]`, `env{}`, `image`, `flavor`, `backend`,
`stay_alive` (seconds, default 300). `args` are shell-quoted and appended to `cmd`;
`env` becomes a `KEY='val'` prefix. (Convenience: a `text/plain` body is taken
verbatim as the command, with no stdin.)

**Response** -- streaming SSE is the **default**; `?stream=false` returns a buffered
JSON envelope.

```bash
# Buffered envelope: exit code + base64 stdout/stderr + duration
printf '{"cmd":"echo hi; echo oops 1>&2; exit 7"}\n' | curl -sN \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/x-runtime-exec" \
  --data-binary @- "https://krillbox.gra.ke2.cloud.ovh.net/exec/mybox?stream=false"
# -> {"exit_code":7,"signal":"","stdout":"aGkK","stderr":"b29wcwo=","duration_ms":3}

# Streaming (default): SSE frames, base64 payloads, final exit event
printf '{"cmd":"cat"}\nstdin-bytes\n' | curl -sN \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/x-runtime-exec" \
  --data-binary @- "https://krillbox.gra.ke2.cloud.ovh.net/exec/mybox"
# -> event: stdout / data: <base64> ... event: exit / data: {"code":0,"signal":"@"}
```

- **Buffered** (`?stream=false`): `200 application/json`
  `{exit_code, signal, stdout(b64), stderr(b64), duration_ms}`. `exit_code` is `-1`
  when the process was killed by a signal (then `signal` carries the name, e.g. `TERM`).
- **Streaming** (default): `200 text/event-stream` -- `event: stdout` / `event: stderr`
  frames whose `data:` is a base64 chunk, a periodic `: keepalive` comment, and a
  final `event: exit` with `{"code","signal"}`. Errors after the stream starts arrive
  as `event: error {"message"}`.
- Pre-stream failures (bad header, no such box, dial failure) return a JSON error
  `{"error":"..."}` with a `4xx/5xx` status.

## Spawning & cloning

```bash
ssh mybox.box                                          # default flavor + image (via config %h)
ssh cli@krillbox.gra.ke2.cloud.ovh.net box up dev --image debian-default --flavor big
ssh cli@krillbox.gra.ke2.cloud.ovh.net box up dev                     # then, inside the box: krillbox-guest auto-suspend off
ssh cli@krillbox.gra.ke2.cloud.ovh.net box up copy --from src         # clone another box (instant CoW)
```

Boxes are crash-consistent thin clones of a golden snapshot; snapshot / rollback /
send-receive all work with stock ZFS underneath.

```bash
ssh cli@krillbox.gra.ke2.cloud.ovh.net box snapshot mybox [name]   # ZFS snapshot of the disk
ssh cli@krillbox.gra.ke2.cloud.ovh.net box snapshot list            # your snapshots
ssh cli@krillbox.gra.ke2.cloud.ovh.net box snapshot rm mybox@snap   # delete one
```

## Networking & DNS

Each tenant gets an isolated virtual network. Your boxes share it, reach each other
by name, and have stable IPs for the box's lifetime (including across host
migration). Cluster DNS resolves box names within the tenant; egress to the
internet works out of the box. Cross-box and cross-host traffic is routed
automatically -- no per-box network config.

## Workspace (`/wrk`)

Every tenant has one persistent workspace mounted at `/wrk` inside any box --
S3-backed, shared across all your boxes, surviving box deletion. Inspect it from
inside a box:

```bash
krillbox-guest info             # box id, name, tenant
krillbox-guest workspace info   # endpoint, mount, bytes used, object count
```

There's also an HTTP file API for the workspace (list / read / write / mkdir / rm),
authenticated by api key -> tenant. Use it to move files in/out without a shell.

## Auto-suspend

A box suspends after an idle timeout when **all** of: no gateway session, no
in-VM interactive user (`who`), and load below threshold -- sustained for the
timeout. Inbound connections keep it up; a box's own outbound traffic does not.
Resume on next connect is sub-second (memory restored from snapshot).

- Detached jobs with no interactive session are **not** activity -- a `nohup`'d
  build with nobody connected will suspend. Control it **from inside the box**
  (metadata API), e.g. `krillbox-guest keep-alive 2h` (hold up for a long job),
  `krillbox-guest auto-suspend off` (disable for this box), or
  `krillbox-guest idle --timeout=30m --load=1.0` (tune the policy). There is no
  launch-time opt-out via `ssh cli@`.

## Flavors & images

```bash
ssh cli@krillbox.gra.ke2.cloud.ovh.net flavors          # tiny / small / default / big / huge (CPU+RAM)
ssh cli@krillbox.gra.ke2.cloud.ovh.net images           # base images + descriptions
```

Pick at spawn with `box up <name> --flavor <f> --image <i>` (or inline
`box[:image[:flavor]]@host` — first colon is the image, second is the flavor, so use
`box::big@host` for the big flavor and `box:debian-docker@host` to pick an image).
In-guest init is either the minimal
PID-1 `krillbox-init` (default, ~0.5s boot) or full systemd, chosen at image-bake
time.

## Reference

- Human docs: https://krillbox.gra.ke2.cloud.ovh.net/docs/
- API (OpenAPI + Swagger): https://krillbox.gra.ke2.cloud.ovh.net/swagger - https://krillbox.gra.ke2.cloud.ovh.net/openapi.json
