Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion .github/workflows/terraform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ jobs:
TF_VAR_daytona_api_key: ${{ secrets.DAYTONA_API_KEY }}
TF_VAR_daytona_base_snapshot: ${{ secrets.DAYTONA_BASE_SNAPSHOT }}
TF_VAR_daytona_target: ${{ secrets.DAYTONA_TARGET }}
TF_VAR_vercel_sandbox_token: ${{ secrets.VERCEL_SANDBOX_TOKEN }}
TF_VAR_vercel_sandbox_project_id: ${{ secrets.VERCEL_SANDBOX_PROJECT_ID }}
TF_VAR_vercel_sandbox_team_id: ${{ secrets.VERCEL_SANDBOX_TEAM_ID }}
TF_VAR_vercel_base_snapshot_id: ${{ secrets.VERCEL_BASE_SNAPSHOT_ID }}
TF_VAR_vercel_sandbox_runtime: "${{ secrets.VERCEL_SANDBOX_RUNTIME || 'node24' }}"
TF_VAR_vercel_runtime_repo_url: "${{ secrets.VERCEL_RUNTIME_REPO_URL || 'https://github.com/ColeMurray/background-agents.git' }}"
TF_VAR_vercel_runtime_repo_ref: "${{ secrets.VERCEL_RUNTIME_REPO_REF || 'main' }}"
TF_VAR_vercel_snapshot_expiration_ms: "${{ secrets.VERCEL_SNAPSHOT_EXPIRATION_MS || '0' }}"

- name: Post Plan Results
uses: actions/github-script@v8
Expand Down Expand Up @@ -264,7 +272,7 @@ jobs:
apply:
name: Apply
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 45
needs: [validate, check-secrets]
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && needs.check-secrets.outputs.has-secrets == 'true'
environment: production
Expand Down Expand Up @@ -301,6 +309,44 @@ jobs:
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Build Vercel base snapshot
id: vercel_base_snapshot
run: |
if [ "${SANDBOX_PROVIDER}" != "vercel" ]; then
echo "snapshot_id=${VERCEL_BASE_SNAPSHOT_ID:-}" >> "$GITHUB_OUTPUT"
echo "Skipping Vercel base snapshot build because SANDBOX_PROVIDER=${SANDBOX_PROVIDER}"
exit 0
fi

if [ -z "${VERCEL_TOKEN:-}" ] || [ -z "${VERCEL_PROJECT_ID:-}" ]; then
echo "VERCEL_SANDBOX_TOKEN and VERCEL_SANDBOX_PROJECT_ID are required when SANDBOX_PROVIDER=vercel"
exit 1
fi

npm run build:vercel-base-snapshot -w @open-inspect/control-plane

output_file="${RUNNER_TEMP}/vercel-base-snapshot-id"
node packages/control-plane/dist/vercel-base-snapshot.js --output "$output_file"
snapshot_id="$(tr -d '\r\n' < "$output_file")"

if [ -z "$snapshot_id" ]; then
echo "Vercel base snapshot builder did not write a snapshot ID"
exit 1
fi

echo "Built Vercel base snapshot: $snapshot_id"
echo "snapshot_id=$snapshot_id" >> "$GITHUB_OUTPUT"
env:
SANDBOX_PROVIDER: "${{ secrets.SANDBOX_PROVIDER || 'modal' }}"
VERCEL_BASE_SNAPSHOT_ID: ${{ secrets.VERCEL_BASE_SNAPSHOT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_SANDBOX_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_SANDBOX_PROJECT_ID }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_SANDBOX_TEAM_ID }}
VERCEL_RUNTIME: "${{ secrets.VERCEL_SANDBOX_RUNTIME || 'node24' }}"
VERCEL_RUNTIME_REPO_URL: "${{ secrets.VERCEL_RUNTIME_REPO_URL || 'https://github.com/ColeMurray/background-agents.git' }}"
VERCEL_RUNTIME_REPO_REF: "${{ secrets.VERCEL_RUNTIME_REPO_REF || 'main' }}"
VERCEL_SANDBOX_API_BASE_URL: ${{ secrets.VERCEL_SANDBOX_API_BASE_URL }}
Comment on lines +312 to +348
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix error message to use correct environment variable names.

The error message at line 322 references VERCEL_SANDBOX_TOKEN and VERCEL_SANDBOX_PROJECT_ID, but the actual environment variables set at lines 342-343 are VERCEL_TOKEN and VERCEL_PROJECT_ID (without the SANDBOX prefix). This matches the naming expected by the build script (context snippet 1 shows requiredEnv("VERCEL_TOKEN") and requiredEnv("VERCEL_PROJECT_ID")).

📝 Proposed fix
-          if [ -z "${VERCEL_TOKEN:-}" ] || [ -z "${VERCEL_PROJECT_ID:-}" ]; then
-            echo "VERCEL_SANDBOX_TOKEN and VERCEL_SANDBOX_PROJECT_ID are required when SANDBOX_PROVIDER=vercel"
+          if [ -z "${VERCEL_TOKEN:-}" ] || [ -z "${VERCEL_PROJECT_ID:-}" ]; then
+            echo "VERCEL_TOKEN and VERCEL_PROJECT_ID are required when SANDBOX_PROVIDER=vercel"
             exit 1
           fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Build Vercel base snapshot
id: vercel_base_snapshot
run: |
if [ "${SANDBOX_PROVIDER}" != "vercel" ]; then
echo "snapshot_id=${VERCEL_BASE_SNAPSHOT_ID:-}" >> "$GITHUB_OUTPUT"
echo "Skipping Vercel base snapshot build because SANDBOX_PROVIDER=${SANDBOX_PROVIDER}"
exit 0
fi
if [ -z "${VERCEL_TOKEN:-}" ] || [ -z "${VERCEL_PROJECT_ID:-}" ]; then
echo "VERCEL_SANDBOX_TOKEN and VERCEL_SANDBOX_PROJECT_ID are required when SANDBOX_PROVIDER=vercel"
exit 1
fi
npm run build:vercel-base-snapshot -w @open-inspect/control-plane
output_file="${RUNNER_TEMP}/vercel-base-snapshot-id"
node packages/control-plane/dist/vercel-base-snapshot.js --output "$output_file"
snapshot_id="$(tr -d '\r\n' < "$output_file")"
if [ -z "$snapshot_id" ]; then
echo "Vercel base snapshot builder did not write a snapshot ID"
exit 1
fi
echo "Built Vercel base snapshot: $snapshot_id"
echo "snapshot_id=$snapshot_id" >> "$GITHUB_OUTPUT"
env:
SANDBOX_PROVIDER: "${{ secrets.SANDBOX_PROVIDER || 'modal' }}"
VERCEL_BASE_SNAPSHOT_ID: ${{ secrets.VERCEL_BASE_SNAPSHOT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_SANDBOX_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_SANDBOX_PROJECT_ID }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_SANDBOX_TEAM_ID }}
VERCEL_RUNTIME: "${{ secrets.VERCEL_SANDBOX_RUNTIME || 'node24' }}"
VERCEL_RUNTIME_REPO_URL: "${{ secrets.VERCEL_RUNTIME_REPO_URL || 'https://github.com/ColeMurray/background-agents.git' }}"
VERCEL_RUNTIME_REPO_REF: "${{ secrets.VERCEL_RUNTIME_REPO_REF || 'main' }}"
VERCEL_SANDBOX_API_BASE_URL: ${{ secrets.VERCEL_SANDBOX_API_BASE_URL }}
- name: Build Vercel base snapshot
id: vercel_base_snapshot
run: |
if [ "${SANDBOX_PROVIDER}" != "vercel" ]; then
echo "snapshot_id=${VERCEL_BASE_SNAPSHOT_ID:-}" >> "$GITHUB_OUTPUT"
echo "Skipping Vercel base snapshot build because SANDBOX_PROVIDER=${SANDBOX_PROVIDER}"
exit 0
fi
if [ -z "${VERCEL_TOKEN:-}" ] || [ -z "${VERCEL_PROJECT_ID:-}" ]; then
echo "VERCEL_TOKEN and VERCEL_PROJECT_ID are required when SANDBOX_PROVIDER=vercel"
exit 1
fi
npm run build:vercel-base-snapshot -w `@open-inspect/control-plane`
output_file="${RUNNER_TEMP}/vercel-base-snapshot-id"
node packages/control-plane/dist/vercel-base-snapshot.js --output "$output_file"
snapshot_id="$(tr -d '\r\n' < "$output_file")"
if [ -z "$snapshot_id" ]; then
echo "Vercel base snapshot builder did not write a snapshot ID"
exit 1
fi
echo "Built Vercel base snapshot: $snapshot_id"
echo "snapshot_id=$snapshot_id" >> "$GITHUB_OUTPUT"
env:
SANDBOX_PROVIDER: "${{ secrets.SANDBOX_PROVIDER || 'modal' }}"
VERCEL_BASE_SNAPSHOT_ID: ${{ secrets.VERCEL_BASE_SNAPSHOT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_SANDBOX_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_SANDBOX_PROJECT_ID }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_SANDBOX_TEAM_ID }}
VERCEL_RUNTIME: "${{ secrets.VERCEL_SANDBOX_RUNTIME || 'node24' }}"
VERCEL_RUNTIME_REPO_URL: "${{ secrets.VERCEL_RUNTIME_REPO_URL || 'https://github.com/ColeMurray/background-agents.git' }}"
VERCEL_RUNTIME_REPO_REF: "${{ secrets.VERCEL_RUNTIME_REPO_REF || 'main' }}"
VERCEL_SANDBOX_API_BASE_URL: ${{ secrets.VERCEL_SANDBOX_API_BASE_URL }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/terraform.yml around lines 312 - 348, Update the error
message in the "Build Vercel base snapshot" step so it references the actual env
var names used elsewhere (VERCEL_TOKEN and VERCEL_PROJECT_ID) instead of
VERCEL_SANDBOX_TOKEN and VERCEL_SANDBOX_PROJECT_ID; modify the check block that
currently echoes "VERCEL_SANDBOX_TOKEN and VERCEL_SANDBOX_PROJECT_ID are
required when SANDBOX_PROVIDER=vercel" to echo the correct variable names
(VERCEL_TOKEN and VERCEL_PROJECT_ID) so it matches the env keys set and the
build script's requiredEnv calls.


- name: Terraform Init
run: |
terraform init \
Expand Down Expand Up @@ -357,6 +403,14 @@ jobs:
TF_VAR_daytona_api_key: ${{ secrets.DAYTONA_API_KEY }}
TF_VAR_daytona_base_snapshot: ${{ secrets.DAYTONA_BASE_SNAPSHOT }}
TF_VAR_daytona_target: ${{ secrets.DAYTONA_TARGET }}
TF_VAR_vercel_sandbox_token: ${{ secrets.VERCEL_SANDBOX_TOKEN }}
TF_VAR_vercel_sandbox_project_id: ${{ secrets.VERCEL_SANDBOX_PROJECT_ID }}
TF_VAR_vercel_sandbox_team_id: ${{ secrets.VERCEL_SANDBOX_TEAM_ID }}
TF_VAR_vercel_base_snapshot_id: ${{ steps.vercel_base_snapshot.outputs.snapshot_id || secrets.VERCEL_BASE_SNAPSHOT_ID }}
TF_VAR_vercel_sandbox_runtime: "${{ secrets.VERCEL_SANDBOX_RUNTIME || 'node24' }}"
TF_VAR_vercel_runtime_repo_url: "${{ secrets.VERCEL_RUNTIME_REPO_URL || 'https://github.com/ColeMurray/background-agents.git' }}"
TF_VAR_vercel_runtime_repo_ref: "${{ secrets.VERCEL_RUNTIME_REPO_REF || 'main' }}"
TF_VAR_vercel_snapshot_expiration_ms: "${{ secrets.VERCEL_SNAPSHOT_EXPIRATION_MS || '0' }}"
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}

Expand Down
71 changes: 56 additions & 15 deletions docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ This guide walks you through deploying your own instance of Open-Inspect using T

Open-Inspect uses Terraform to automate deployment across three cloud providers:

| Provider | Purpose | What Terraform Creates |
| -------------------------------------- | -------------------------------- | ----------------------------------------------------------------- |
| **Cloudflare** | Control plane, session state | Workers, KV namespaces, Durable Objects, D1 Database |
| **Vercel** _or_ **Cloudflare Workers** | Web application | Project + env vars (Vercel) _or_ Worker via OpenNext (Cloudflare) |
| **Modal** _or_ **Daytona** | Sandbox execution infrastructure | Modal app deployment _or_ control-plane config for Daytona API |
| Provider | Purpose | What Terraform Creates |
| ------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------ |
| **Cloudflare** | Control plane, session state | Workers, KV namespaces, Durable Objects, D1 Database |
| **Vercel** _or_ **Cloudflare Workers** | Web application | Project + env vars (Vercel) _or_ Worker via OpenNext (Cloudflare) |
| **Modal**, **Daytona**, _or_ **Vercel Sandboxes** | Sandbox execution infrastructure | Modal app deployment, Daytona API config, _or_ Vercel Sandbox API config |

> **Web platform choice**: Set `web_platform` in your `terraform.tfvars` to `"vercel"` (default) or
> `"cloudflare"`. The Cloudflare option deploys the Next.js app as a Cloudflare Worker using
Expand All @@ -36,16 +36,17 @@ Open-Inspect uses Terraform to automate deployment across three cloud providers:

Create accounts on these services before continuing:

| Service | Purpose |
| ------------------------------------------------ | -------------------------------------------------------------- |
| [Cloudflare](https://dash.cloudflare.com) | Control plane hosting (+ web app if using Cloudflare platform) |
| [Vercel](https://vercel.com) _(optional)_ | Web application hosting (only if `web_platform = "vercel"`) |
| [Modal](https://modal.com) _(optional)_ | Sandbox infrastructure when `sandbox_provider = "modal"` |
| [Daytona](https://app.daytona.io) _(optional)_ | Sandbox infrastructure when `sandbox_provider = "daytona"` |
| [GitHub](https://github.com/settings/developers) | OAuth + repository access |
| [Anthropic](https://console.anthropic.com) | Claude API |
| [Slack](https://api.slack.com/apps) _(optional)_ | Slack bot integration |
| GitHub App Webhooks _(optional)_ | GitHub bot (PR reviews) |
| Service | Purpose |
| --------------------------------------------------- | -------------------------------------------------------------- |
| [Cloudflare](https://dash.cloudflare.com) | Control plane hosting (+ web app if using Cloudflare platform) |
| [Vercel](https://vercel.com) _(optional)_ | Web application hosting (only if `web_platform = "vercel"`) |
| [Modal](https://modal.com) _(optional)_ | Sandbox infrastructure when `sandbox_provider = "modal"` |
| [Daytona](https://app.daytona.io) _(optional)_ | Sandbox infrastructure when `sandbox_provider = "daytona"` |
| [Vercel Sandboxes](https://vercel.com) _(optional)_ | Sandbox infrastructure when `sandbox_provider = "vercel"` |
| [GitHub](https://github.com/settings/developers) | OAuth + repository access |
| [Anthropic](https://console.anthropic.com) | Claude API |
| [Slack](https://api.slack.com/apps) _(optional)_ | Slack bot integration |
| GitHub App Webhooks _(optional)_ | GitHub bot (PR reviews) |

### Required Tools

Expand Down Expand Up @@ -181,6 +182,32 @@ The control plane calls the Daytona REST API directly — no shim service to dep
> sandboxes. If you plan to use Claude models, add `ANTHROPIC_API_KEY` as a **global secret** in
> Settings > Secrets after deploying. See [Secrets Management](SECRETS.md) for details.

### Vercel Sandboxes

> Only required when `sandbox_provider = "vercel"`.

1. Create a [Vercel API token](https://vercel.com/account/tokens) that can access your sandbox
project.
2. Note the **Project ID** for the project that will own sandbox sessions.
3. Note the **Team/Account ID** if you use a Vercel team. Leave it unset for personal accounts where
the token can access the project directly.
4. Set `sandbox_provider = "vercel"` in `terraform.tfvars`.
5. Set `vercel_sandbox_token`, `vercel_sandbox_project_id`, and optionally `vercel_sandbox_team_id`
in `terraform.tfvars`.

The control plane calls the Vercel Sandbox API directly from Cloudflare Workers. No Modal-style shim
service is deployed. Vercel supports filesystem snapshots and repo prebuilt images; if you have a
reusable base snapshot, set `vercel_base_snapshot_id` to skip runtime bootstrapping on every fresh
sandbox.

When the Terraform GitHub Actions apply job runs with `SANDBOX_PROVIDER=vercel`, it builds a fresh
immutable Vercel base-runtime snapshot before `terraform apply` and passes the generated snapshot ID
into the Worker deployment. This keeps the Vercel base runtime aligned with the configured
`VERCEL_RUNTIME_REPO_URL`/`VERCEL_RUNTIME_REPO_REF`. The `vercel_base_snapshot_id` setting is still
available for local Terraform runs or as a manual fallback. See
[Vercel Sandbox Provider](VERCEL_SANDBOX_PROVIDER.md) for the full runtime, snapshot, and resource
configuration model.

### Anthropic

1. Go to [Anthropic Console](https://console.anthropic.com)
Expand Down Expand Up @@ -362,11 +389,20 @@ modal_workspace = "your-modal-workspace"
modal_environment = "your-modal-environment"
modal_environment_web_suffix = "your-modal-web-suffix" # Lowercase letters, digits, dashes; empty for https://workspace--... endpoints

# Sandbox provider: "modal" (default), "daytona", or "vercel"
# sandbox_provider = "modal"

# Daytona (only required when sandbox_provider = "daytona")
# daytona_api_url = "https://app.daytona.io/api"
# daytona_api_key = "your-daytona-api-key"
# daytona_base_snapshot = "your-snapshot-name"

# Vercel Sandboxes (only required when sandbox_provider = "vercel")
# vercel_sandbox_token = "your-vercel-token"
# vercel_sandbox_project_id = "prj_xxxxx"
# vercel_sandbox_team_id = "team_xxxxx" # Optional
# vercel_base_snapshot_id = "snapshot_xxxxx" # Optional manual fallback; CI usually generates this

# GitHub App (used for both OAuth and repository access)
github_client_id = "Iv1.abc123..." # From GitHub App settings
github_client_secret = "your-client-secret" # Generated in GitHub App settings
Expand Down Expand Up @@ -680,6 +716,11 @@ Go to your fork's Settings → Secrets and variables → Actions, and add:
| `MODAL_WORKSPACE` | Modal workspace name |
| `MODAL_ENVIRONMENT` | Modal environment name (defaults to `main`) |
| `MODAL_ENVIRONMENT_WEB_SUFFIX` | Modal environment web suffix for endpoint URLs; lowercase letters, digits, dashes, or empty |
| `SANDBOX_PROVIDER` | `modal`, `daytona`, or `vercel` |
| `VERCEL_SANDBOX_TOKEN` | Vercel API token _(only if `sandbox_provider = "vercel"`)_ |
| `VERCEL_SANDBOX_PROJECT_ID` | Vercel project ID for sandbox sessions _(only if `sandbox_provider = "vercel"`)_ |
| `VERCEL_SANDBOX_TEAM_ID` | Optional Vercel team/account ID for sandbox sessions |
| `VERCEL_BASE_SNAPSHOT_ID` | Optional manual Vercel base-runtime snapshot; Terraform CI generates one for Vercel applies |
| `GH_OAUTH_CLIENT_ID` | GitHub App OAuth client ID |
| `GH_OAUTH_CLIENT_SECRET` | GitHub App OAuth client secret |
| `GH_APP_ID` | GitHub App ID |
Expand Down
15 changes: 11 additions & 4 deletions docs/HOW_IT_WORKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,11 @@ Open-Inspect supports two backend patterns:

- **Modal**: near-instant startup plus filesystem snapshot restore
- **Daytona**: persistent stop/start sandboxes via direct REST API calls
- **Vercel Sandboxes**: filesystem snapshot restore and repo-image builds via the Vercel Sandbox API

Modal is still the only backend with repo-image builds and live filesystem snapshot restore. Daytona
uses persistent sandboxes instead: the control plane stops the sandbox on inactivity or stale
heartbeat, then resumes that same sandbox later with the same logical sandbox ID and auth token.
Modal and Vercel support repo-image builds and live filesystem snapshot restore. Daytona uses
persistent sandboxes instead: the control plane stops the sandbox on inactivity or stale heartbeat,
then resumes that same sandbox later with the same logical sandbox ID and auth token.

### Clients

Expand Down Expand Up @@ -408,7 +409,7 @@ That's potentially minutes before the agent can start working.

### How Snapshots Solve This

Modal's filesystem snapshots let us capture a sandbox's state after setup:
Modal and Vercel filesystem snapshots let us capture a sandbox's state after setup:

```
First session: Clone ─▶ Install/Build ─▶ Start Runtime ─▶ [Snapshot] ─▶ Work
Expand All @@ -420,6 +421,12 @@ Later sessions: [Restore Snapshot] ─▶ Quick sync ─▶ Start Runtime ─▶

The first session for a repo pays the setup cost. Subsequent sessions restore in seconds.

For Vercel, the Terraform GitHub Actions deploy builds a base-runtime snapshot before applying the
control plane and wires that generated snapshot ID into `VERCEL_BASE_SNAPSHOT_ID`. Fresh Vercel
sandboxes normally start from this managed runtime snapshot instead of cloning and installing the
sandbox runtime on every session. See [Vercel Sandbox Provider](VERCEL_SANDBOX_PROVIDER.md) for the
full provider flow.

### Image Prebuilding

For frequently-used repositories, images can be prebuilt on a schedule:
Expand Down
4 changes: 4 additions & 0 deletions docs/IMAGE_PREBUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ minutes of changes.

## Getting Started

Pre-built images are available when the deployment uses `sandbox_provider = "modal"` or
`sandbox_provider = "vercel"`. Daytona deployments use persistent sandboxes instead, so the Images
settings page is disabled for that backend.

### Enable for a Repository

1. Open **Settings > Images** in the web dashboard
Expand Down
Loading
Loading