# Contributing

> Project layout, design constraints (Docker-only core, sqlite truth, docker CLI shell-out), issue report fields, and extension boundaries for integrators.

- Repository: tastyeffectco/sandboxes
- GitHub: https://github.com/tastyeffectco/sandboxes
- Human docs: https://grok-wiki.com/public/docs/tastyeffectco-sandboxes-f551c1a2e9a0
- Complete Markdown: https://grok-wiki.com/public/docs/tastyeffectco-sandboxes-f551c1a2e9a0/llms-full.txt

## Source Files

- `CONTRIBUTING.md`
- `ARCHITECTURE.md`
- `control-plane/README.md`
- `image/README.md`
- `LICENSE`

---

---
title: "Contributing"
description: "Project layout, design constraints (Docker-only core, sqlite truth, docker CLI shell-out), issue report fields, and extension boundaries for integrators."
---

sandboxed is maintained as a small, self-hostable stack: a Go control plane (`sandboxd`) in `control-plane/`, a per-sandbox base image in `image/`, Traefik edge config in `traefik/`, and compose/install scripts at the repository root. Changes should preserve the headline constraint—**runs fully on Docker with one command**—and the three non-negotiables below before adding features or host dependencies.

## Project layout

The repository splits operational surfaces by responsibility. Sandboxes are **not** compose services; `sandboxd` launches them at runtime as sibling containers on `${SANDBOXED_NETWORK}`.

:::files
sandboxed/
├── docker-compose.yml     # traefik + sandboxd stack
├── install.sh             # idempotent installer
├── uninstall.sh           # stack + managed-container cleanup
├── .env.example           # all configuration keys and defaults
├── traefik/               # static + dynamic (wake, forward-auth)
├── image/                 # sandboxed-base image, skel, HOME_LAYOUT contract
└── control-plane/         # sandboxd + runtimed (Go 1.22+)
    ├── cmd/sandboxd/      # daemon entrypoint, env wiring, background workers
    ├── cmd/runtimed/      # in-sandbox supervisor (baked into base image)
    ├── internal/          # docker, store, reconcile, reaper, wake, api, …
    └── migrations/        # numbered SQLite schema files
:::

| Path | Owns |
|------|------|
| `docker-compose.yml` | Traefik edge, `sandboxd` build, host socket + data-dir mounts |
| `control-plane/` | Lifecycle API, SQLite store, reconciler, reapers, wake path |
| `image/` | Base image Dockerfile, `/opt/sandbox-skel`, registry proxy config |
| `traefik/` | Entrypoints, catch-all wake router, optional TLS / forward-auth |
| `SANDBOXED_DATA_DIR` (default `/var/lib/sandboxed`) | `workspaces/<id>/`, `state/sandboxd.db`, logs |

<Note>
`image/scripts/dev/` are **debugging utilities only**. They do not update SQLite; containers created outside `sandboxd` are orphans (logged, not adopted). See [Control plane development](/control-plane-development) for the supported dev loop.
</Note>

## Design constraints

Contributions that fight these constraints are unlikely to merge unless they are optional, default-off, and clearly scoped.

### Docker-only core

New **host** dependencies beyond Docker Engine + Compose are a hard sell. The product promise is a single `./install.sh` (or `docker compose up -d --build`) with no Kubernetes, separate database server, or message queue. Optional features that need extra host tooling should ship behind env flags and stay off by default.

### SQLite is the source of truth

All durable sandbox lifecycle state lives in SQLite (WAL) under `SANDBOXED_DATA_DIR/state/sandboxd.db`. The boot-time reconciler in `internal/reconcile` **converges Docker to SQLite, never the other way**. Do not add state that exists only in Docker labels or container metadata without a matching row.

```mermaid
stateDiagram-v2
    [*] --> creating
    creating --> running
    creating --> error
    running --> stopped
    stopped --> running : docker start / wake
    running --> error
    error --> [*]
    stopped --> [*] : destroy / purge
```

| Rule | Implication |
|------|-------------|
| Every managed container `s-{ulid}` should have one `sandbox` row | Create/delete paths must update the store |
| Reconciler on boot | Rows drive `docker start`/`stop` and status repair; Docker drift is corrected toward the DB |
| Orphans (container/mount without row) | **Logged only** in v1—no auto-adoption; hand-debugged `docker run` is not destroyed |
| Migrations | Numbered SQL under `control-plane/migrations/`; applied by the migration runner at startup |

Workspace files persist on disk (`workspaces/<id>/` bind mounts), separate from SQLite but referenced by row paths in the schema.

### Docker CLI shell-out

`internal/docker` is a thin typed wrapper over the **`docker` CLI** (`os/exec`), not the Docker SDK. Policy (hardened `RunSpec`, networks, labels) lives in callers; the package only encodes invocation. Reach for the SDK only with a measured bottleneck and strong review justification.

The `sandboxd` container image bundles `docker-ce-cli` and talks to the host daemon via the mounted `/var/run/docker.sock` (see `control-plane/Dockerfile`).

<Warning>
The control plane is **root-equivalent on the host** through the Docker socket. Treat the host as a trust boundary; do not co-locate unrelated secrets. See [Production deployment](/production-deployment) for hardening when exposing the API beyond loopback.
</Warning>

### Other v1 trade-offs (conscious, not bugs)

| Area | v1 choice | Extension / hardening path |
|------|-----------|----------------------------|
| Workspace storage | Plain directory per sandbox | Disk quotas at fs/volume layer; multi-host sharding is out of core scope |
| Snapshots / templates | API present; **experimental** on directory storage | Contribute a directory-tar snapshot backend, or use plain workspace copies |
| Egress | Default-allow, no logging | Host firewall / proxy; not a control-plane default |
| `memory.high` cgroup throttle | Opt-in `SANDBOXED_SET_MEMORY_HIGH` | Needs host cgroup access from the control-plane container |
| External identity columns | Opaque passthrough (`external_*`); equality checks only | `sandboxd` does not interpret upstream IDs beyond storage |

Full rationale: [Overview](/overview) and the architecture doc’s “Design choices & current limitations” table.

## Development loop

<Steps>
<Step title="Control plane (Go 1.22+)">
From `control-plane/`:

```bash
go build ./...
go test ./...
go vet ./...
```

`sandboxd` requires **CGO** (`github.com/mattn/go-sqlite3`). Container builds set `CGO_ENABLED=1`. `runtimed` is CGO-free and is compiled into the sandbox base image, not the host.
</Step>
<Step title="Full stack">
Run `./install.sh` or `docker compose up -d --build`, then exercise the API on `SANDBOXED_API_BIND` (default `http://127.0.0.1:9090`). Verify:

```bash
curl -s http://127.0.0.1:9090/healthz
curl -s http://127.0.0.1:9090/readyz
```

The base image build is slow the first time; later runs cache it.
</Step>
<Step title="Base image / sandbox contract">
Image changes live under `image/` (Dockerfile, `skel/`, `etc/`). Layout and survival rules are defined in `image/HOME_LAYOUT.md`—update that contract before changing seeding or mount paths in control-plane code.
</Step>
</Steps>

Match surrounding style and comment density. Include tests for behavior changes where practical (`control-plane` has package-level `_test.go` coverage in `internal/api`, `internal/auth`, `internal/traefik`, `cmd/runtimed`, etc.).

## Reporting issues

Useful bug reports speed reproduction on a Docker-only host. Include:

| Field | What to attach |
|-------|----------------|
| Docker version | Output of `docker version` |
| OS / kernel | Host distribution and version |
| Control-plane logs | `docker compose logs sandboxd` (and Traefik if routing-related) |
| Triggering request | Exact HTTP method, path, and JSON body (redact secrets) |
| Sandbox id | ULID from create response, if applicable |
| `.env` deltas | Non-default `HTTP_PORT`, `PREVIEW_DOMAIN`, auth, data dir |

For preview or wake failures, also note the preview URL pattern and whether the sandbox was stopped (idle) or never started. See [Troubleshooting](/troubleshooting) for common `readyz`, port-80, ULID, and userns-remap failures.

## Extension boundaries for integrators

Integrate **through the HTTP API**, not by managing `docker run` lifecycle in parallel with `sandboxd`.

### Supported integration surfaces

| Surface | Use for |
|---------|---------|
| `POST /v1/sandboxes/{id}/tasks` | Headless coding agents (`runtimed`); SSE on `.../tasks/{taskId}/events` |
| `POST /sandbox`, `GET /sandboxes`, `GET /sandbox/{id}` | Create, list, inspect (legacy paths; v1 equivalents exist) |
| `PUT` / `GET /v1/sandboxes/{id}/files` | Workspace file CRUD without exec |
| `POST /sandbox/{id}/exec` | Non-interactive commands (no TTY/stdin) |
| `POST /sandbox/{id}/keepalive`, `POST /v1/sandboxes/{id}/stop` | Idle / manual stop semantics |
| `DELETE /sandbox/{id}` vs `POST /sandbox/{id}/purge` | Destroy container (keep workspace) vs destroy + delete workspace |
| Preview URLs | `http://s-{id}-{port}.preview.{PREVIEW_DOMAIN}` (see [Preview URL reference](/preview-url-reference)) |
| `GET /llm.txt` | **Public** machine-readable API contract (no bearer token); served from `SANDBOXD_LLM_TXT_PATH` (default `/etc/sandboxed/llm.txt`); 404 if unset |

<Info>
`GET /llm.txt` is exempt from service-token auth so agents and third-party tools can fetch the contract without credentials. Mount or deploy the file at `SANDBOXD_LLM_TXT_PATH`; edits are read per request (no redeploy required). When auth is enabled, all other external routes need `Authorization: Bearer <secret>` unless loopback-exempt—see [API authentication](/api-authentication).
</Info>

### Do not rely on (v1)

- **Hand-started containers** (`docker run --name s-...`) without a SQLite row—they remain orphans; the reconciler will not adopt them.
- **`image/scripts/dev/`** for production lifecycle—they bypass the store.
- **State only in Docker** (labels, inspect output) without a store migration and reconciler path.
- **Interactive exec** (TTY/stdin)—the exec API is non-interactive by design.
- **Snapshots/templates** as production guarantees—they are experimental on directory-backed storage until a contributed backend lands.

### Safe extension patterns

- **Env injection at create** — pass provider keys in `POST /sandbox` body `env` so tasks and exec see them.
- **External identity** — store upstream user/app ids in `external_*` columns; `sandboxd` treats them as opaque passthrough for claim/purge hooks.
- **Private previews** — `visibility=private` + forward-auth (Traefik → `/forward-auth`); see [Private previews](/private-previews).
- **Custom base image** — set `SANDBOXD_IMAGE` / `SANDBOXED_IMAGE`; keep `HOME_LAYOUT.md` contract if you change skel paths.
- **Hardening at the edge** — TLS, wildcard DNS, API tokens, egress rules on the host—without forking core lifecycle logic.

Provider-neutral agent integration: any coding CLI that runs inside the sandbox (OpenCode, Claude Code, etc.) is invoked via tasks or exec; no hosted model vendor is required in core.

## Contribution guidelines

- **Keep the core lean** — prefer config toggles and host-level hardening over growing `sandboxd` for every tenant policy.
- **Preserve SQLite ↔ Docker direction** — new lifecycle features need store columns, migrations, and reconciler awareness.
- **Prefer CLI shell-out** in `internal/docker` unless profiling proves otherwise.
- **Document contract changes** in `image/HOME_LAYOUT.md` when touching workspace layout or seeding.
- **License** — contributions are accepted under the [MIT License](https://github.com/tastyeffectco/sandboxes/blob/main/LICENSE) (Copyright (c) 2026 sandboxed contributors).

Pull requests that tighten isolation, snapshot backends, or auth defaults are especially welcome once they respect the constraints above.

## Related pages

<CardGroup>
<Card title="Control plane development" href="/control-plane-development">
Go build/test/vet, CGO sqlite note, package map, and compose rebuild loop.
</Card>
<Card title="v1 API reference" href="/v1-api-reference">
Public `/v1/sandboxes` routes, error envelope, tasks, and files API.
</Card>
<Card title="Legacy API reference" href="/legacy-api-reference">
`/sandbox*` routes, healthz/readyz, metrics, and `GET /llm.txt`.
</Card>
<Card title="Architecture (overview)" href="/overview">
sandboxd, Traefik, runtimed, and create → task → preview path.
</Card>
</CardGroup>
