Skip to content

Commit e98cbb0

Browse files
authored
feat: add projectSetupLayer config and sandbox build command (#110)
## Summary - Add `projectSetupLayer` config option: a bash script baked into a cached image layer between base and agent overlay, so projects can cache expensive system-level setup (apt packages, language runtimes, etc.) - Add `ox sandbox build` command for explicitly building sandbox layers with `--no-cache` support - Add `--project` flag to `ox sandbox hash` for inspecting setup layer hashes ## Image Stack ``` base → projectSetupLayer (optional) → agent overlay → session ``` The setup script runs as root (Docker: `docker exec --user root`, Cloud: `sudo`), and its content is hashed with the base image hash for cache invalidation. When either the base image or the script changes, the layer rebuilds automatically. ## New Commands ```bash # Build all layers through agent ox sandbox build --agent claude # Build just through project setup layer ox sandbox build --project # Force rebuild everything ox sandbox build --agent claude --no-cache # Inspect hashes ox sandbox hash --project ox sandbox hash --project --cloud ``` ## Key Changes - **Config**: New `projectSetupLayer` string field in `OxConfig` - **Docker**: `ensureProjectSetupLayer()` builds via run/exec/commit pattern, streams output to terminal - **Cloud**: `ensureProjectSetupCloudSnapshot()` builds via volume/sandbox/snapshot pattern - **Resource cleanup**: Classifies `oxl-`/`oxlb-` prefixed cloud resources and `ox-sandbox:md5-*-l-*` Docker images - **CLI**: `sandbox build` with `--agent`, `--project`, `--cloud`, `--no-cache` flags; `sandbox hash --project` - **Force rebuild**: `force` parameter threaded through all ensure functions and `SandboxProvider.ensureImage()` ## Files Changed | File | Changes | |------|---------| | `src/services/config.ts` | Add `projectSetupLayer` field | | `src/services/docker.ts` | Hash/tag computation, `ensureProjectSetupLayer`, `force` param, GHCR pull skip for setup layers | | `src/services/sandbox/cloudSnapshot.ts` | Cloud snapshot build, `force` param, setup hash in agent slugs | | `src/services/sandbox/cloudProvider.ts` | Chain setup layer into `ensureImage()` | | `src/services/sandbox/resources.ts` | Classify setup layer resources in cleanup | | `src/services/sandbox/sandboxExec.ts` | Add `stream` option for real-time output | | `src/services/sandbox/types.ts` | Add `force` to `SandboxProvider.ensureImage()` | | `src/services/sandbox/dockerProvider.ts` | Thread `force` through | | `src/commands/sandbox.ts` | `build` subcommand, `--project` flag on `hash`, flag validation | | Tests | New tests for hash, slug, and resource classification |
1 parent cdabdcd commit e98cbb0

26 files changed

+1920
-155
lines changed

docs/configuration.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ The wizard walks through sandbox provider, agent, model, and authentication setu
4242
| `cloudRegion` | `string` | `ord` | Cloud sandbox region: `ord` (Chicago) or `ams` (Amsterdam) |
4343
| `sandboxBaseImage` | `string` | -- | Override Docker image for sandbox containers |
4444
| `buildSandboxFromDockerfile` | `boolean\|string` | `false` | Build sandbox image from Dockerfile. `true` uses the built-in Dockerfile; a string value specifies a path to a custom Dockerfile. Takes precedence over `sandboxBaseImage`. |
45+
| `projectSetupLayer` | `string` | -- | Bash script to run on top of the base sandbox image and cache as a reusable layer. Use for system-level dependencies like apt packages, language runtimes, Docker, or browser tooling. Runs before the agent overlay and without your project repo mounted. |
4546
| `overlayMounts` | `string[]` | -- | Paths to isolate with Docker volume mounts in [mount mode](sandbox-providers.md#mount-mode). E.g., `["node_modules"]` |
47+
| `privileged` | `boolean` | `false` | Run Docker sandbox containers in privileged mode (`--privileged`). Required for Docker-in-Docker. Only applies to the Docker sandbox provider. |
48+
| `rootInitScript` | `string` | -- | Shell command to run as **root** inside the sandbox before `initScript`. Useful for installing system packages. E.g., `"apt-get update && apt-get install -y build-essential"` |
4649
| `initScript` | `string` | -- | Shell command to run inside the sandbox before starting the agent. Runs in all modes. E.g., `"npm install"` |
4750

4851
### Port Forwarding
@@ -141,6 +144,18 @@ API_KEY=your-key-here
141144

142145
These variables are injected into the sandbox container at startup and are available to both the init script and the agent.
143146

147+
## Build-Time vs Run-Time Setup
148+
149+
Ox supports three different setup hooks for sandbox customization:
150+
151+
- `projectSetupLayer` -- build-time, cached image layer; best for system packages and heavyweight tooling that should be reused across sessions
152+
- `rootInitScript` -- run-time, runs as root on every session start, resume, and shell creation
153+
- `initScript` -- run-time, runs as the sandbox user on every session start, resume, and shell creation
154+
155+
Use `projectSetupLayer` for things like `apt-get install`, browser dependencies, Docker tooling, or language runtimes that do not depend on your repository contents. Because it runs before your repo is mounted and is cached as an image/snapshot layer, it avoids repeating expensive setup work on every session.
156+
157+
Use `rootInitScript` or `initScript` for per-session setup that depends on the working directory, environment variables, checked-out code, or other state that is only available at container startup.
158+
144159
## Example Config
145160

146161
```yaml
@@ -153,6 +168,11 @@ tigerServiceId: null
153168
themeName: tokyonight
154169
overlayMounts:
155170
- node_modules
171+
projectSetupLayer: |
172+
apt-get update
173+
apt-get install -y ffmpeg chromium
174+
privileged: true
175+
rootInitScript: "apt-get update && apt-get install -y build-essential"
156176
initScript: "npm install"
157177
appPort: 3000
158178
additionalPorts:

docs/sandbox-providers.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,32 @@ Agents run in local Docker containers built from purpose-built images that inclu
99
### How It Works
1010

1111
1. **Base image** -- Ox uses a pre-built image from GHCR (`ghcr.io/timescale/ox`) based on Ubuntu 24.04 with git, ripgrep, curl, tmux, GitHub CLI, and other essentials
12-
2. **Agent overlay** -- On top of the base, ox builds an agent-specific overlay that installs the chosen agent CLI (Claude Code, OpenCode, or Codex)
13-
3. **Container creation** -- A container is created with your code (cloned from GitHub or mounted from your filesystem), credentials injected, and the agent launched
12+
2. **Project setup layer** -- If `projectSetupLayer` is configured, ox runs that script on top of the base image and caches the result as a reusable image/snapshot layer
13+
3. **Agent overlay** -- On top of the base or project setup layer, ox builds an agent-specific overlay that installs the chosen agent CLI (Claude Code, OpenCode, or Codex)
14+
4. **Container creation** -- A container is created with your code (cloned from GitHub or mounted from your filesystem), credentials injected, any runtime init scripts run, and the agent launched
1415

1516
Images are cached locally. The first run pulls and builds the images, which takes a few minutes. Subsequent runs start in seconds.
1617

18+
### Project Setup Layer
19+
20+
Use `projectSetupLayer` when you want to install heavyweight system dependencies once and reuse them across many sessions:
21+
22+
```yaml
23+
# .ox/config.yml
24+
projectSetupLayer: |
25+
apt-get update
26+
apt-get install -y ffmpeg chromium
27+
```
28+
29+
This script runs as root on top of the sandbox base image and is cached as its own layer before the agent overlay is built.
30+
31+
- It runs **without** your repository mounted, so it should only be used for system-level setup
32+
- It is ideal for `apt-get install`, browser/runtime dependencies, Docker tooling, and other expensive base environment changes
33+
- When the script changes, ox automatically rebuilds the cached layer
34+
- It works for both Docker and Cloud providers
35+
36+
For one-off or repo-dependent setup, use `rootInitScript` or `initScript` instead.
37+
1738
### Custom Base Image
1839

1940
Override the base image via config:
@@ -66,6 +87,15 @@ initScript: "npm install"
6687

6788
The init script runs after the working directory is set up, in all modes (async, interactive, plan). Useful for installing dependencies, setting up databases, or any other preparation.
6889

90+
If you need root access at runtime, use `rootInitScript`:
91+
92+
```yaml
93+
# .ox/config.yml
94+
rootInitScript: "apt-get update && apt-get install -y build-essential"
95+
```
96+
97+
`rootInitScript` runs before `initScript` on every session start, resume, and shell creation. Unlike `projectSetupLayer`, it is not cached as an image layer.
98+
6999
## Cloud
70100

71101
Agents run in remote sandboxes powered by Deno Deploy. Useful for offloading work from your machine or running many tasks in parallel without local resource constraints.

0 commit comments

Comments
 (0)