---
name: "sndbxd"
status: "In progress"
url: "https://stitcom.be/projects/sndbxd"
author: "Stephen Titcombe"
---

# sndbxd

One small server for two jobs: throwaway sandboxes where AI agents can run code safely, and long-lived dev boxes I can SSH into from anywhere.

Status: In progress. Stack: Swift, Containerization, REST API.

- Host: Headless Mac, macOS 26
- Runtime: Apple Containerization
- Server: Swift, Hummingbird 2, GRDB
- Stage: Milestone 1

## Why build it

Coding agents work best when they can install packages, run tests and break things. I don’t want them doing that on my laptop. I also wanted a persistent development machine that survives a closed lid.

Both needs come down to the same thing: start a Linux environment fast, give it the right files, and control what it can reach. So both modes share one boot path and differ only in lifetime, storage and how you connect.

## Two modes, one boot path

| Property | Agent sandbox | Dev box |
| --- | --- | --- |
| Lifetime | Minutes to hours, ends at its TTL | Weeks or months |
| Connect | Exec API, called by the agent | SSH from any editor |
| Storage | Scratch disk, deleted with the sandbox | Persistent `/home` and `/workspace` |
| Network | Outbound traffic limited to an allowlist | Configured per template |

## How a request flows

1. **Clients**: Agents call exec; people use SSH
2. **REST API**: /v1, envelope, RFC 9457 errors
3. **Daemon**: Hummingbird 2 + GRDB state
4. **SandboxRuntime**: Protocol; a fake runtime for tests
5. **Linux VMs**: Containerization, copy-on-write clones

Environments start from templates: a base image, a list of install commands and some defaults. A template is built once with an open network, cached as a root filesystem, and cloned copy-on-write for every new sandbox. At runtime the network closes down to the allowlist.

I wrote my own daemon on the Containerization Swift package instead of wrapping the `container` CLI. That keeps state, quotas and errors in one place.

## Starting a sandbox

State changes go through one `PATCH` on the desired state. When the host is full, the API answers `422` with a problem document instead of queueing forever.

```http
PATCH /v1/sandboxes/sbx_7f2c
{ "desiredState": "running" }

202 Accepted
{
  "data": {
    "id": "sbx_7f2c",
    "template": "swift-6",
    "state": "starting",
    "expiresAt": "2026-11-03T04:10:00Z"
  }
}
```

## Where it stands

- **Now**: Walking skeleton: the API, state store and a fake runtime, end to end.
- **Next**: The real Containerization adapter and template builds.
- **Later**: Secrets injection, git auth and SSH access for dev boxes.

Inspired by the sandboxy example in Apple’s containerization repository.
