Skip to content

Latest commit

 

History

History
1720 lines (1335 loc) · 85.1 KB

File metadata and controls

1720 lines (1335 loc) · 85.1 KB

Getting Started with Open-Inspect

This guide walks you through deploying your own instance of Open-Inspect using Terraform.

Looking for local development setup (without full infra deployment)? Start with SETUP_GUIDE.md.

Important: This system is designed for single-tenant deployment only. All users share the deployment's GitHub App installation and workspace catalog. GitHub sandbox credentials reach only the session's repositories, further restricted by current grants for team-owned sessions. See the Security Model for details.

Sandbox Repository Access

GitHub credentials issued to a sandbox cover its primary repository and session members, including the repository snapshot copied from an environment at creation. Workspace-owned sessions are scoped the same way. For team-owned sessions, repositories no longer covered by the team's current grants are removed from the next credential scope. An installation grant retains all session members but does not grant the sandbox access to other repositories in the installation.

Private submodules, repository-backed private dependencies, and setup scripts that clone sibling repositories now fail unless those repositories are included in the session's environment. Team sessions also need grants for those members. Add the dependencies to the environment before creating the session; editing an environment does not expand an existing session's repository snapshot.

Sessions with no repositories, no remaining granted members, or unresolved repository IDs receive no token. Scopes with more than 500 unique IDs are refused. Repository image builds use that repository alone, while environment image builds use their member repositories and the environment owner's current grants. Metadata and catalog calls retain installation-wide credentials. GitLab deployments still use one deployment-wide PAT and cannot provide this credential boundary.

Removing a grant does not revoke a credential already issued to a sandbox. Installation tokens remain valid until expiry, and credential helpers refresh shortly before expiry. Legacy session or environment rows without numeric repository IDs require an identity-matching cached repository catalog; loading the repository list populates that cache. Credentials fail closed if an ID cannot be resolved from it.


Teams Enforcement

Cloudflare production Terraform's teams_enforcement sets the control-plane TEAMS_ENFORCEMENT value. Accepted values are off, shadow, and on, with shadow as the default. For a fresh deployment, explicitly set teams_enforcement = "on" in terraform.tfvars (as in Step 5) so the control plane runs with TEAMS_ENFORCEMENT=on and enforces non-private Team-visibility reads. AWS production is separate and defaults to shadow: add TEAMS_ENFORCEMENT = "on" to its config map in terraform/environments/aws-production/terraform.tfvars to opt in, then apply Terraform and restart the service as described in AWS Bring-Up. Do not assume a deployed instance has full team read isolation simply because it includes Teams.

For an existing deployment, review its production shadow_denied:* authorization audit entries before opting into on. Resolve unexpected would-deny decisions and verify membership, repository grants, and bot bindings before changing the deployed value. A default flip remains gated on that production audit review; this guide does not establish that the gate has passed. Runtime and Terraform defaults remain shadow, independently of the fresh-deployment opt-in shown here.

Private-session access and current owning-team membership for non-read session actions remain enforced in every mode. Environment/automation ownership, repository grants, and team-secret checks also remain enforced. See Enforcement and Access Paths.

Separately, administrators can enable requireTeamOnCreate in Settings > Teams. It defaults off and requires a team for new sessions, environments, and automation definitions, including teamless bot session creation requests. It does not migrate existing workspace resources or prevent existing workspace automations from running when their runtime authorization checks pass. New teams start without repository grants; configure named or installation-wide grants before selecting repositories.

Overview

Open-Inspect uses Terraform to automate deployment across multiple 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, Daytona, Vercel Sandboxes, OpenComputer, or E2B Sandbox execution infrastructure Modal app deployment, Daytona/Vercel API config, OpenComputer template/API config, or E2B template/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 OpenNext, so you don't need a Vercel account.

Your job: Create accounts, gather credentials, and configure one file (terraform.tfvars). Terraform's job: Create all infrastructure and configure services.

How this guide is organized: Steps 1–10 are the minimal deploy path: one web platform, the default Modal sandbox provider, a GitHub App for repository access and sign-in, and the Slack, Linear, and GitHub bots turned off. The Optional Sections after Step 10 cover alternative sandbox providers, Google login, Slack, Linear, the GitHub bot, a custom domain, CI/CD, branding, updating, and troubleshooting.


Prerequisites

Required Accounts

Create accounts on these services before continuing. Rows marked (optional) are only needed for the matching optional section:

Service Purpose
Cloudflare Control plane hosting (+ web app if using Cloudflare platform)
Vercel (optional) Web application hosting (only if web_platform = "vercel")
Modal Sandbox infrastructure (default sandbox_provider = "modal")
GitHub OAuth + repository access
Anthropic (optional) Claude API (the Slack/Linear bot classifier's default provider)
Daytona (optional) Sandbox infrastructure when sandbox_provider = "daytona"
Vercel Sandboxes (optional) Sandbox infrastructure when sandbox_provider = "vercel"
OpenComputer (optional) Sandbox infrastructure when sandbox_provider = "opencomputer"
E2B (optional) Sandbox infrastructure when sandbox_provider = "e2b"
Slack (optional) Slack bot integration
Linear (optional) Linear Agent integration
GitHub App Webhooks (optional) GitHub bot (PR reviews)

Required Tools

# Terraform (1.14.0+; see terraform/environments/production/versions.tf)
brew install terraform

# Node.js (24+). node@24 is keg-only, so put it on PATH (add the export to
# your shell profile to keep it across sessions).
brew install node@24
export PATH="$(brew --prefix node@24)/bin:$PATH"
node --version  # must print v24 or newer

# Python 3.12+ and uv (Modal CLI is installed via uv sync below)
brew install python@3.12 uv

# Wrangler CLI (for initial R2 bucket setup)
npm install -g wrangler

Step 1: Fork the Repository

Fork ColeMurray/background-agents to your GitHub account or organization.

# Clone your fork
git clone https://github.com/YOUR-USERNAME/background-agents.git
cd background-agents
npm install

# Build the shared package (required before Terraform deployment)
npm run build -w @open-inspect/shared

# Install Python dependencies for Modal deployment (includes sandbox-runtime)
cd packages/modal-infra && uv sync --frozen && cd -

# Create your Terraform config files (edited in Step 5; never commit them)
cp terraform/environments/production/terraform.tfvars.example terraform/environments/production/terraform.tfvars
cp terraform/environments/production/backend.tfvars.example terraform/environments/production/backend.tfvars

Tip: Keep terraform/environments/production/terraform.tfvars open. As you collect credentials in the following steps, paste them directly into this file.


Step 2: Create Cloud Provider Credentials

Cloudflare

  1. Go to Cloudflare Dashboard
  2. Note your Account ID (visible in the dashboard URL or account overview)
  3. Note your Workers subdomain: Go to Workers & Pages → Overview, look in the bottom-right of the panel for *.YOUR-SUBDOMAIN.workers.dev
  4. Create API Token at API Tokens:
    • Use template: "Edit Cloudflare Workers"
    • Verify it has these permissions:
      • Account | Workers KV Storage | Edit (should be included with template)
      • Account | Workers R2 Storage | Edit (should be included with template)
      • Account | D1 | Edit
      • Account | Queues | Edit (required for durable image-build finalization)
    • Set "Account Resources" to include your account
    • Set "Zone Resources" to include all zones from your account
    • Click "Continue to summary" and "Update token"
  5. Enable R2: Must add payment info, but first 10 GB/month is free

Cloudflare R2 (Terraform State Backend)

Terraform needs a place to store its state. We use Cloudflare R2.

# Login to Cloudflare
wrangler login


# Create the state bucket
wrangler r2 bucket create open-inspect-terraform-state

Create an R2 API Token:

  1. Go to R2 → Overview → Manage R2 API Tokens
  2. Create token with Object Read & Write permission
  3. Note the Access Key ID and Secret Access Key

Vercel (only if web_platform = "vercel")

Skip this section if you're deploying the web app to Cloudflare Workers. Important: Do not set vercel_api_token or vercel_team_id to empty strings in your terraform.tfvars — leave them unset so the dummy defaults are used. The Vercel Terraform provider validates the token on init even when no Vercel resources are created.

  1. Go to Vercel Account Settings → Tokens
  2. Create a new token with full access
  3. Note your Team/Account ID:
    • Go to Settings (Account Settings or Team Settings)
    • Look for "Your ID" or find it in the URL: vercel.com/{YOUR_TEAM_ID}/...
    • Even personal accounts have an ID (usually starts with team_)

Modal

Only required when sandbox_provider = "modal" (the default, used by the core path) or "modal-vm". Select modal-vm for Docker-capable VMs; see Modal VM setup. To use Daytona, Vercel Sandboxes, OpenComputer, or E2B instead, skip this section and follow Alternative Sandbox Providers.

  1. Go to Modal Settings
  2. Create a new API token: Settings -> API Tokens -> New Token
  3. Note the Token ID and Token Secret
  4. Note your Workspace and Environment name (visible in your Modal dashboard URL, https://modal.com/apps/<modal_workspace>/<modal_environment>)
  5. Note the environment's Web suffix from Modal's environment settings. Use the normalized lowercase suffix made of letters, digits, and dashes. Leave it empty for the environment whose endpoints use https://<workspace>--...modal.run.

Anthropic (Optional)

Optional for the core path, which sets enable_slack_bot = false and enable_linear_bot = false in Step 5. Terraform's own default enables the Slack bot, and its classifier runs on Claude, so terraform apply fails without this key unless both bots are disabled or classification_model points at an OpenAI model. If you enable Slack or Linear later, set either classification_anthropic_api_key or anthropic_api_key then; prefer the classifier-only key unless you also want a deployment-wide sandbox key, because it is never injected into sandboxes. Coding sessions themselves need no key here — those model credentials can be added as secrets in the web app after deploying. With Modal, a key set in anthropic_api_key is also injected into session sandboxes as a deployment-wide default.

  1. Go to Anthropic Console
  2. Create an API key
  3. Note the API Key (starts with sk-ant-)

Want to use your OpenAI ChatGPT subscription? See Using OpenAI Models for setup instructions (can be configured after deployment).

Want to use your xAI SuperGrok subscription? See Using Grok with a SuperGrok Subscription. Grok is opt-in and can also be configured after deployment.


Step 3: Create GitHub App

Every deployment needs one GitHub App for repository access. The same App can also provide GitHub OAuth sign-in, but its client pair is optional when Google is the only sign-in provider.

  1. Go to GitHub Apps

  2. Click "New GitHub App"

  3. Fill in the basics:

    • Name: Open-Inspect-YourName (must be globally unique)
    • Homepage URL: Your web app URL (see below)
    • Webhook: Leave "Active" unchecked for now. GitHub Bot (Optional) enables it when enable_github_bot = true for GitHub automations or bot commands.
  4. If enabling GitHub sign-in, configure Identifying and authorizing users (OAuth):

    • Callback URL: {your-web-app-url}/api/auth/callback/github

    Your web app URL depends on web_platform:

    • Vercel: https://open-inspect-{deployment_name}.vercel.app
    • Cloudflare: https://open-inspect-web-{deployment_name}.{your-subdomain}.workers.dev
    • Cloudflare with cloudflare_custom_domain set: https://{your-custom-domain}

    Important: The callback URL must match your deployed web app URL exactly. The {deployment_name} is the unique value you set in terraform.tfvars (e.g., your GitHub username or company name).

    Keep "User-to-server token expiration" active (GitHub App → Optional Features; it is the default for newly created Apps, but activate it if yours predates that default). Expiring user tokens are what make GitHub return a refresh token at sign-in, and Open-Inspect stores that per-user credential for attributed GitHub operations such as pull-request creation. Clone, fetch, and push authentication still use the shared GitHub App installation. With expiration deactivated — or on an OAuth App, which never issues a refresh token — no per-user credential is captured, so supported attributed operations fall back to the shared GitHub App bot identity.

  5. Set Repository permissions:

    • Contents: Read & Write
    • Pull requests: Read & Write (also authorizes creating and applying labels to session-created pull requests)
    • Metadata: Read-only
    • If enabling the GitHub bot, also grant Actions: Read-only (workflow-run automations), Checks: Read-only (check-suite automations), and Issues: Read & Write.
  6. If using ALLOWED_GITHUB_ORGS/allowed_github_orgs, set Organization permissions:

    • Members: Read-only
    • For existing GitHub Apps, republish the permission change and request/approve installation updates before testing org membership sign-in.
  7. If enabling GitHub sign-in, set Account permissions:

    • Email addresses: Read-only (every GitHub sign-in requires a verified email, including username-only, org-only, and intentionally open deployments)
    • For existing GitHub Apps, republish the permission change and request/approve installation updates, otherwise the added permission does not apply to current installs.
  8. Click "Create GitHub App"

  9. Note the App ID (top of page). If enabling GitHub sign-in, also note the Client ID.

  10. If enabling GitHub sign-in, under "Client secrets", click "Generate a new client secret" and note the Client Secret.

  11. Scroll down to "Private keys" and click "Generate a private key" (downloads a .pem file)

  12. Convert the key to PKCS#8 format (required for Cloudflare Workers):

    openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \
      -in ~/Downloads/your-app-name.*.private-key.pem \
      -out private-key-pkcs8.pem
  13. Install the app on your account/organization:

    • Click "Install App" in the sidebar
    • Select the repositories you want Open-Inspect to access
  14. Note the Installation ID from the URL after installing:

    https://github.com/settings/installations/INSTALLATION_ID
    

You should now always have:

  • App ID (e.g., 123456)
  • Private Key (PKCS#8 format, starts with -----BEGIN PRIVATE KEY-----)
  • Installation ID (e.g., 12345678)

For GitHub sign-in, you should also have:

  • Client ID (e.g., Iv1.abc123...)
  • Client Secret (e.g., abc123...)

The App private key belongs in the control plane, not in a Modal github-app secret or a session secret. Fresh and restored sandboxes obtain scoped Git credentials from the control plane on demand. The optional GitHub bot Worker still needs its own App credential bindings; Terraform supplies them when enabled. Modal's llm-api-keys and internal-api secrets remain required and are provisioned by Terraform.


Step 4: Generate Security Secrets

Generate these random secrets (you'll need them for terraform.tfvars):

# Token encryption key
echo "token_encryption_key: $(openssl rand -base64 32)"

# Repo secrets encryption key
echo "repo_secrets_encryption_key: $(openssl rand -base64 32)"

# Modal API secret (use hex for this one)
echo "modal_api_secret: $(openssl rand -hex 32)"

# Browser authentication secret (Terraform retains the legacy input name)
echo "nextauth_secret: $(openssl rand -base64 32)"

# GitHub webhook secret (only if enabling GitHub bot)
echo "github_webhook_secret: $(openssl rand -hex 32)"

Save these values somewhere secure—you'll need them in the next step.


Step 5: Configure Terraform

Edit the backend.tfvars and terraform.tfvars files you created in Step 1, in terraform/environments/production.

Configure backend.tfvars

Fill in your R2 credentials:

access_key = "your-r2-access-key-id"
secret_key = "your-r2-secret-access-key"
endpoints = {
  s3 = "https://YOUR_CLOUDFLARE_ACCOUNT_ID.r2.cloudflarestorage.com"
}

Configure terraform.tfvars

Fill in all the values you gathered. Here's the structure:

# Provider Authentication
cloudflare_api_token        = "your-cloudflare-api-token"
cloudflare_account_id       = "your-account-id"
cloudflare_worker_subdomain = "your-subdomain"  # e.g., "twilight-unit-b2cf" (without .workers.dev)

# Web platform: "vercel" (default) or "cloudflare" (OpenNext)
web_platform                = "vercel"

# Optional custom domain for the web app (only when web_platform = "cloudflare")
# cloudflare_zone_id       = "your-zone-id"
# cloudflare_custom_domain = "app.example.com"

# Vercel (only required when web_platform = "vercel")
# If using Cloudflare, do NOT set these — leave them out so the dummy defaults are used.
vercel_api_token            = "your-vercel-token"
vercel_team_id              = "team_xxxxx"       # Your Vercel ID (even personal accounts have one)
modal_token_id              = "your-modal-token-id"
modal_token_secret          = "your-modal-token-secret"
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), "modal-vm", "daytona", "vercel", "opencomputer", or "e2b"
# 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"
# daytona_base_snapshot_memory_gib = 2

# 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 override; skips managed snapshot builds
# vercel_sandbox_runtime    = "node24"
# vercel_snapshot_expiration_ms = 0

# OpenComputer (only required when sandbox_provider = "opencomputer")
# opencomputer_api_url      = "https://app.opencomputer.dev/api"
# opencomputer_api_key      = "your-opencomputer-api-key"
# opencomputer_template     = ""  # Empty lets Terraform build the runtime template

# E2B (only required when sandbox_provider = "e2b")
# e2b_api_key               = "your-e2b-api-key"        # runtime REST API key (also auths the build)
# e2b_template_id           = "open-inspect-sandbox"

# GitHub App repository access (required in every deployment)
github_app_id              = "123456"
github_app_installation_id = "12345678"
github_app_private_key     = <<-EOF
-----BEGIN PRIVATE KEY-----
... paste your PKCS#8 key here ...
-----END PRIVATE KEY-----
EOF

# GitHub OAuth sign-in (optional pair; leave both empty for Google-only)
github_client_id     = "Iv1.abc123..."      # From GitHub App settings
github_client_secret = "your-client-secret" # Generated in GitHub App settings

# Google OAuth sign-in (optional pair; may be used alone or with GitHub)
google_client_id     = ""
google_client_secret = ""

# Slack. Terraform defaults enable_slack_bot to true; keep false for the core path
# (see "Slack Bot (Optional)" to enable it later).
enable_slack_bot     = false
slack_bot_token      = ""
slack_signing_secret = ""

# GitHub Bot (set enable_github_bot = true to deploy the webhook worker)
enable_github_bot      = false
github_webhook_secret  = ""          # From Step 4 (required if enabled)
github_bot_username    = ""          # e.g., "my-app[bot]" (your GitHub App's bot login)

# Linear Agent (set enable_linear_bot = true to deploy the webhook worker)
enable_linear_bot      = false
linear_client_id       = ""          # From the Linear app (required if enabled)
linear_client_secret   = ""          # From the Linear app (required if enabled)
linear_webhook_secret  = ""          # From the Linear app (required if enabled)

# API Keys. Optional: leave blank to add model credentials as secrets in the web
# app instead. Required only when the Slack/Linear classifier runs on Anthropic.
anthropic_api_key = ""
# classification_anthropic_api_key = ""   # Classifier-only key; never reaches sandboxes

# Slack/Linear classifier provider, chosen by classification_model.
# An OpenAI model requires classification_openai_api_key. An Anthropic model is
# served by classification_anthropic_api_key, falling back to anthropic_api_key.
# classification_model = "claude-haiku-4-5"   # e.g. "gpt-5.4-mini" to classify on OpenAI
classification_openai_api_key = ""   # Required when classification_model is an OpenAI id
# classification_reasoning_effort = "low"     # OpenAI ids only; blank keeps the model default

# Security Secrets (from Step 4)
token_encryption_key          = "your-generated-value"
repo_secrets_encryption_key   = "your-generated-value"
# provider_accounts_encryption_key = "existing-key" # Optional override; Terraform generates one
modal_api_secret               = "your-generated-value"
nextauth_secret                = "your-generated-value"

# Configuration
# IMPORTANT: deployment_name must be globally unique for Vercel URLs
# Use your GitHub username, company name, or a random string
deployment_name = "your-unique-name"  # e.g., "acme", "johndoe", "mycompany"
project_root    = "../../../"

# Branding (optional — defaults shown)
# Display name shown in the web UI tab title, sign-in page, landing hero, bot
# messages (Slack/Linear), PR body footer, and outbound HTTP User-Agent.
# app_name = "Open-Inspect"
# Short brand label shown only in the sidebar header.
# Optional URL (absolute or root-relative) to a custom logo/favicon override.
# Leave empty to keep the built-in favicon and default in-app icon.
# app_icon_url = ""

# Initial deployment: set both to false (see Step 6)
enable_durable_object_bindings = false
enable_service_bindings        = false

# Access Control (set at least one allowlist for production). A user is admitted
# if they match ANY allowlist below.
allowed_users         = "your-github-username"  # Comma-separated GitHub usernames, or empty
allowed_email_domains = ""                      # Comma-separated domains (e.g., "example.com,corp.io")
allowed_emails        = ""                      # Exact addresses (e.g., "pm@gmail.com") — for users on shared domains
allowed_github_orgs   = ""                      # Comma-separated orgs whose active members can sign in

# Explicitly opt into open access only if you want any authenticated user to be
# able to sign in when all allowlists are empty.
unsafe_allow_all_users = false

# Fresh deployment opt-in: sets control-plane TEAMS_ENFORCEMENT=on.
# The Terraform/runtime default remains shadow. Existing deployments must review
# production shadow_denied audit entries before opting in.
teams_enforcement = "on"

Core path bot settings: The snippet above deploys with enable_slack_bot = false, enable_github_bot = false, and enable_linear_bot = false. Keep enable_slack_bot = false explicit: Terraform defaults it to true, which requires Slack credentials and an Anthropic (or OpenAI classifier) key. With all three bots off, anthropic_api_key is optional. Turn bots on later with the Slack, Linear, and GitHub bot sections.

Choose Sign-In Providers

Complete credential pairs are the enablement policy. Terraform rejects partial pairs and rejects a deployment with no sign-in provider.

Configuration GitHub client pair Google client pair Compatible admission
GitHub-only Set Empty GitHub username/org, verified email/domain, or unsafe
Google-only Empty Set Verified email/domain, or explicit unsafe allow-all
GitHub and Google Set Set Verified email/domain, or explicit unsafe allow-all

The GitHub App ID, PKCS#8 private key, and installation ID remain required in all three configurations because they authorize repository operations; they do not enable GitHub sign-in. The /login page reads the enabled provider set from the control plane on every request.

The core path uses GitHub sign-in. To add or switch to Google, see Enable Google Login (Optional).

Note: Review allowed_users, allowed_email_domains, allowed_emails, and allowed_github_orgs carefully — these control who can sign in. Terraform fails if all are empty unless you explicitly set unsafe_allow_all_users = true. Allowlists use OR semantics: matching any configured username, email domain, exact email, or active GitHub org membership grants access. Use allowed_emails for individual users on shared domains (e.g. a specific person@gmail.com) where allowed_email_domains would admit too many. allowed_github_orgs checks membership at sign-in only with the signing-in user's OAuth token; existing sessions last until session expiry. The read:org OAuth scope is requested only when org access is configured, and GitHub Apps using org access need Organization permissions: Members read-only.


Step 6: Deploy with Terraform

Deployment requires two phases due to Cloudflare's Durable Object and service binding requirements.

Phase 1: Initial Deployment

Ensure your terraform.tfvars has:

enable_durable_object_bindings = false
enable_service_bindings        = false

Important: Build the workers before running Terraform (Terraform references the built bundles):

# From the repository root
npm run build -w @open-inspect/control-plane -w @open-inspect/slack-bot -w @open-inspect/github-bot -w @open-inspect/linear-bot

Then run:

cd terraform/environments/production

# Initialize Terraform with backend config
terraform init -backend-config=backend.tfvars

# Deploy (phase 1 - creates workers without bindings)
terraform apply

Phase 2: Enable Bindings

After Phase 1 succeeds, update your terraform.tfvars:

enable_durable_object_bindings = true
enable_service_bindings        = true

Then run:

terraform apply

Terraform will update the workers with the required bindings.


Step 7: Deploy the Web App

If using Cloudflare (web_platform = "cloudflare")

Terraform handles the full build and deploy automatically — the web app is built with OpenNext and deployed as a Cloudflare Worker during terraform apply. No manual step needed.

To serve the web app on your own hostname, see Custom Domain for the Web App (Optional).

If using Vercel (web_platform = "vercel")

Terraform creates the Vercel project and configures environment variables, but does not deploy the code. You have two options:

Option A: Deploy via CLI (Recommended for First Deploy)

# From the repository root (replace {deployment_name} with your value from terraform.tfvars)
npx vercel link --project open-inspect-{deployment_name}
npx vercel --prod

Note: The Vercel project is configured with custom build commands for the monorepo structure. Terraform sets these automatically:

  • Install: cd ../.. && npm install && npm run build -w @open-inspect/shared
  • Build: next build

Option B: Link Git Repository (For Automatic Deployments)

  1. Go to Vercel Dashboard
  2. Find the open-inspect-{deployment_name} project
  3. Go to Settings → Git
  4. Click "Connect Git Repository" and select your fork
  5. Vercel will automatically deploy on push to main

Note: If you link Git, ensure the build settings match those configured by Terraform (Settings → General → Build & Development Settings).


Step 8: Verify Deployment

After deployment completes, verify each component from the repository root:

# Get the verification commands from Terraform
terraform -chdir=terraform/environments/production output verification_commands

Or manually:

# 1. Control Plane health check (replace {deployment_name} and YOUR-SUBDOMAIN)
curl https://open-inspect-control-plane-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/health

# 2. Sandbox backend health check
# Modal exposes a health endpoint. Prefer the exact URL from terraform output verification_commands.
# Manual form: https://<workspace>[-<modal_environment_web_suffix>]--open-inspect-api-health.modal.run
MODAL_WORKSPACE_SLUG="YOUR-WORKSPACE" # or "YOUR-WORKSPACE-YOUR-MODAL-WEB-SUFFIX"
curl https://${MODAL_WORKSPACE_SLUG}--open-inspect-api-health.modal.run
# Daytona, Vercel, OpenComputer, and E2B use their provider APIs directly, so there is no
# Open-Inspect shim health URL.

# 3. Web app (should return 200)
curl -I "$(terraform -chdir=terraform/environments/production output -raw web_app_url)"

Visit your web app URL and sign in with each configured provider. New users get the Member role, which cannot manage secrets, so the end-to-end session test comes after Owner and team setup in Steps 9 and 10.


Step 9: Bootstrap the Workspace Owner

Owner assignment is an explicit operator action. After the web app is deployed and verified:

  1. Have the intended Owner sign in to the deployed web application once. This creates their canonical user and default role assignment.
  2. While signed in, open /api/auth/get-session on the web application origin and record the 32-character lowercase hexadecimal user.id. The bootstrap command accepts this canonical ID, never an email address.
  3. Obtain the D1 database name with terraform output -raw d1_database_name from terraform/environments/production.
  4. From the repository root, run the remote dry run (the default):
npm run rbac:bootstrap-owner -- \
  --database "$(terraform -chdir=terraform/environments/production output -raw d1_database_name)" \
  --user "<canonical-user-id>"
  1. If the preflight is ready, confirm the target and execute the same command with --execute:
npm run rbac:bootstrap-owner -- \
  --database "$(terraform -chdir=terraform/environments/production output -raw d1_database_name)" \
  --user "<canonical-user-id>" \
  --execute

If the preflight is no-op, the target is already the current unsuspended Owner; skip execution. If it is refused, stop and resolve the reported reason before retrying.

The command uses Wrangler credentials (CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID, or wrangler login) and targets remote D1. It refuses a suspended/missing user, a missing or ambiguous assignment, or another unsuspended Owner. There is no force option. Execution is one atomic Wrangler --command batch: it writes one redacted workspace.owner_bootstrapped service audit event, replaces the target's assignment, and returns the exact audit-bound postcondition from that same transaction. A no-op writes nothing. A lost batch response can leave the outcome uncertain; the command does not automatically retry writes or claim success without the postcondition.

A successful execution prints the postcondition row as JSON with "status":"executed".

  1. Rerun the dry run from step 4 and expect no-op for the selected unsuspended Owner:
npm run rbac:bootstrap-owner -- \
  --database "$(terraform -chdir=terraform/environments/production output -raw d1_database_name)" \
  --user "<canonical-user-id>"

The preflight row should report "status":"no-op" with the detail selected user is already the current unsuspended Owner. If it reports refused instead, the command exits non-zero and the detail field gives the reason. The control-plane /health endpoint reports service liveness, not Owner status.

Step 10: Create the First Team and Test a Session

  1. As the bootstrapped Owner, open Settings > Teams > Create team, enter a name and slug, and create your first team. The creator becomes its first Lead automatically; Owner bootstrap alone does not create a team or membership.
  2. Open the team's settings. Choose Join policy: Invite only (the creation default) or Open for workspace users to join themselves. Choose Default visibility for new sessions: Team, Workspace, or Private. Use Team for team-readable work, and confirm the deployed control plane has TEAMS_ENFORCEMENT=on before relying on that read boundary.
  3. In Teams > your team > Repositories, grant the repositories the team needs. New teams have no grants. Prefer named repositories; use All installation repositories only when that broad access is intended. Include every repository required by a multi-repository environment.
  4. Have the other operators sign in once, then add them in the team's member list. For an open team, they can instead use Join team from Teams. Assign Lead to operators who need team-management duties. Workspace Owner/Administrator status is not a substitute for joining the team to perform non-read actions on its sessions.
  5. Add a model credential in the team's Secrets tab, or use Settings > Secrets for a global or repository key (e.g. ANTHROPIC_API_KEY for Claude). Skip this if a suitable deployment-wide credential is already configured. See Secrets Management.
  6. Create a session with this team selected, a granted repository, the intended visibility, and a model whose credential is configured. Send a prompt and verify sandbox startup, repository checkout, and streamed results. Have another team member verify the intended collaboration.
  7. If new sessions, environments, and automation definitions must be team-owned, enable Require a team for new sessions in Settings > Teams after membership and grants are ready. This separate requireTeamOnCreate policy also covers new environments and automation definitions; it does not migrate existing workspace resources.

Optional Sections

Your core deployment is complete. Everything below is optional; follow only the sections you need.


Alternative Sandbox Providers (Optional)

The core path uses Modal, the default sandbox_provider. To run sessions on another provider, set sandbox_provider in terraform.tfvars and follow the matching section below instead of the Modal credentials in Step 2. Terraform accepts modal, modal-vm, daytona, vercel, opencomputer, or e2b. The modal-vm backend uses the same Modal credentials; see Modal VM backend.

Daytona

Only required when sandbox_provider = "daytona".

  1. Create a Daytona account and generate an API key with the following permissions:
    • Sandboxes: Read, Write (runtime sandbox management and preview URLs)
    • Snapshots: Read, Write, Delete (automated snapshot builds via Terraform)
  2. Note the API URL (e.g., https://app.daytona.io/api) and optional target
  3. Terraform builds and verifies a new base snapshot before switching the Worker to it. See the sandbox image workflow for dependency updates and manual builds.
  4. Set sandbox_provider = "daytona" in terraform.tfvars
  5. Set daytona_api_url, daytona_api_key, and daytona_base_snapshot in terraform.tfvars. daytona_base_snapshot_memory_gib controls the memory inherited by sandboxes created from the snapshot and defaults to 2.

The control plane calls the Daytona REST API directly — no shim service to deploy.

Two optional settings:

  • daytona_toolbox_api_url overrides the per-sandbox toolbox proxy. Leave it empty on a deployment whose sandboxes report their own.
  • daytona_prebuilds_enabled admits new Daytona prebuilt-image builds and lets fresh sessions boot from one. It defaults to false; see Daytona prebuilds for the gates an operator should clear against their own organization and target before turning it on.

Important: the Daytona provider has no fleet-wide key of its own. Add the key for the models you plan to use — ANTHROPIC_API_KEY for Claude — as a global secret in Settings > Secrets after deploying. See Secrets Management for details.

Vercel Sandboxes

Only required when sandbox_provider = "vercel".

  1. Create a Vercel API token 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 prebuilt images; if you have a reusable base snapshot, set vercel_base_snapshot_id to use it instead of Terraform's managed base snapshot build.

When Terraform runs with sandbox_provider = "vercel", it builds a managed immutable Vercel base-runtime snapshot from the checked-out sandbox runtime and Vercel bootstrap source, then passes a deterministic snapshot name into the Worker deployment. The control plane resolves that name to the latest created Vercel snapshot at sandbox creation time. The vercel_base_snapshot_id setting is still available as a manual override. See Vercel Sandbox Provider for the full runtime, snapshot, and resource configuration model.

Important: the Vercel provider has no fleet-wide key of its own. Add the key for the models you plan to use — ANTHROPIC_API_KEY for Claude — as a global secret in Settings > Secrets after deploying. See Secrets Management for details.

OpenComputer

Only required when sandbox_provider = "opencomputer".

  1. Create an OpenComputer API key.
  2. Set sandbox_provider = "opencomputer" in terraform.tfvars.
  3. Set opencomputer_api_url and opencomputer_api_key.
  4. Leave opencomputer_template = "" to let Terraform build the OpenInspect runtime template, or set it to an existing OpenComputer template name.
  5. Run terraform apply.

For the full template build and runtime details, see OpenComputer Sandbox Provider.

E2B

Only required when sandbox_provider = "e2b".

E2B needs a single credential — the API key (e2b_api_key). The control plane uses it at runtime for the E2B REST API, and the e2b-infra module uses it to build the sandbox template (via the E2B Template SDK). Create it at the E2B dashboard → API Keys.

  1. Set sandbox_provider = "e2b" in terraform.tfvars.
  2. Set e2b_api_key and e2b_template_id (e.g. open-inspect-sandbox).
  3. Terraform's e2b-infra module builds the template automatically on terraform apply, and rebuilds it when packages/e2b-infra or packages/sandbox-runtime change. To build manually:
    cd packages/e2b-infra
    uv sync --frozen
    E2B_API_KEY=e2b_… E2B_TEMPLATE_ID=open-inspect-sandbox uv run python build-template.py

The control plane calls the E2B REST API directly from Cloudflare Workers. Each session runs in a single long-lived sandbox: when its TTL (e2b_sandbox_timeout_seconds, default 7200) expires the sandbox is paused rather than killed (e2b_auto_pause, default true), so sessions survive idle gaps; the next prompt resumes it through the control plane. On the Hobby tier (~1h runtime cap) lower e2b_sandbox_timeout_seconds to 3300. Set e2b_auto_pause = false to kill on timeout instead.

For the full runtime, lifecycle, and configuration model, see E2B Sandbox Provider.

Important: the E2B provider has no fleet-wide key of its own. Add the key for the models you plan to use — ANTHROPIC_API_KEY for Claude — as a global secret in Settings > Secrets after deploying. See Secrets Management for details.


Enable Google Login (Optional)

Google login lets non-developer users (PMs, support agents) sign in without a GitHub account. Git operations still use the shared GitHub App, and their PRs fall back to the App bot (no personal GitHub attribution unless the same verified email is also a linked GitHub identity).

  1. In the Google Cloud Console, create an OAuth client ID of type Web application.
  2. Add the authorized redirect URI {your-web-app-url}/api/auth/callback/google (e.g. https://open-inspect-yourname.vercel.app/api/auth/callback/google). It must match the deployed URL exactly.
  3. On the OAuth consent screen, request only the openid, email, and profile scopes — these are non-sensitive, so Google requires no app-verification review.
  4. Set google_client_id and google_client_secret (both required together). For restricted access, admit Google users through allowed_emails (exact addresses) or allowed_email_domains; for intentionally open access with no allowlists, set unsafe_allow_all_users = true. Leave the GitHub client pair empty for Google-only sign-in, or keep it configured to offer both providers. The next request to /login reflects the deployed pair without a web flag or rebuild.

Security note: Under restricted access, Google sign-in is admitted only for verified emails that match an allowlist. Because addresses on shared domains like gmail.com are generic, prefer allowed_emails (exact match) over allowed_email_domains for those users.


Slack Bot (Optional)

Skip this section if you don't need Slack integration.

Create the Slack App

  1. Go to Slack API Apps
  2. Click "Create New App" → "From scratch"
  3. Name it (e.g., Open-Inspect) and select your workspace

After deploying the Slack worker, you can configure the app from packages/slack-bot/slack-app-manifest.yaml instead of entering the settings below individually. Replace SLACK_EVENTS_URL with the worker's /events URL and SLACK_INTERACTIONS_URL with its /interactions URL before applying the manifest. The template includes message.channels and message.groups for channel-message automations; remove those subscriptions if the deployment will not use that feature.

Before terraform apply, configure the OAuth scopes below, install the app, and collect its bot token and signing secret. Apply the URL-dependent manifest after deployment, when Slack can verify the worker's /events and /interactions endpoints.

Configure OAuth & Permissions

  1. Go to OAuth & Permissions in the sidebar
  2. Add Bot Token Scopes:
    • assistant:write
    • app_mentions:read
    • chat:write
    • channels:history
    • channels:read
    • groups:history
    • groups:read
    • im:history
    • files:read (lets the bot read images attached to messages and forward them to sessions)
    • files:write
    • reactions:write
    • users:read
    • users:read.email
  3. Click "Install to Workspace"
  4. Note the Bot Token (xoxb-...)

Important: If you update bot token scopes later, you must reinstall the app to your workspace for the new permissions to take effect.

Upgrade an Existing Slack Deployment

Queued delivery applies to every Slack completion, including text-only replies. Before the first terraform apply after upgrading:

  1. Add Account | Queues | Edit to the Cloudflare API token used by Terraform. Terraform needs this permission to create the completion queue, dead-letter queue, Worker binding, and consumer.
  2. Ensure the Slack app has assistant:write, users:read, users:read.email, files:read, and files:write. Reinstall the app once for the workspace, and update slack_bot_token if Slack issued a replacement. These scopes enable Agent view, user identity resolution, inbound images, and generated-media delivery.
  3. Run terraform apply, then verify a text completion, an inbound image attached to a prompt, and a generated-media attachment. If the token lacks Queue access, the apply fails while provisioning the new resources; grant the permission and rerun the apply.

No individual Slack user needs to reauthorize the app. Deployments with enable_slack_bot = false still create the image-build finalization Queue and dead-letter Queue.

Get Signing Secret

  1. Go to Basic Information
  2. Note the Signing Secret

Enable the Slack Bot in Terraform

Set these in terraform.tfvars, then run terraform apply from terraform/environments/production:

enable_slack_bot                 = true
slack_bot_token                  = "xoxb-..."
slack_signing_secret             = "your-signing-secret"
classification_anthropic_api_key = "sk-ant-..." # Classifier-only; or set anthropic_api_key

The Slack classifier needs an Anthropic key unless classification_model points at an OpenAI model; see Anthropic (Optional).

Event Subscriptions (Configure After Deployment)

Event Subscriptions require the Slack bot worker to be deployed first for URL verification. You'll configure this in Complete Slack Setup after running Terraform.

Complete Slack Setup

Now that the Slack bot worker is deployed, configure the agent experience, App Home, and event subscriptions.

Enable Agents

  1. Go to Slack Apps -> Your Slack App → Agents
  2. Enable the agent feature and use AI coding assistant for your codebase as the agent description

Enable App Home

The App Home provides settings for users' preferred model, reasoning effort, and branch. The writable Messages tab lets users start direct-message sessions.

  1. Go to Slack Apps -> Your Slack App → App Home
  2. Under Show Tabs, toggle "Home Tab" to On
  3. Toggle "Messages Tab" to On and allow users to send messages

Configure Event Subscriptions

  1. Go to Slack Apps -> Your Slack App → Event Subscriptions
  2. Toggle "Enable Events" to On
  3. Enter the Request URL shown by terraform output -raw slack_bot_events_url:
    https://open-inspect-slack-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/events
    
    (Replace YOUR-SUBDOMAIN with your Cloudflare Workers subdomain and {deployment_name} with your deployment name from terraform.tfvars)
  4. Wait for the green "Verified" checkmark
  5. Under Subscribe to bot events, add:
    • app_home_opened (required for App Home settings)
    • app_mention
    • message.channels (optional - if you want the bot to see all channel messages)
    • message.groups (optional - if you want automations in private channels)
    • message.im (enables direct message support)
  6. Click Save Changes

Configure Interactivity

  1. Go to Interactivity & Shortcuts
  2. Toggle "Interactivity" to On
  3. Enter the Request URL shown by terraform output -raw slack_bot_interactions_url:
    https://open-inspect-slack-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/interactions
    
  4. Under Select Menus, enter Options Load URL using the same endpoint:
    https://open-inspect-slack-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/interactions
    
    This is required for searchable Slack repository pickers that use external data sources.
  5. Click Save Changes

Invite the Bot to Channels

In Slack, for each channel where you want the bot to respond:

  • Type /invite @YourBotName, or
  • Click the channel name → Integrations → Add apps

The bot only responds to @mentions in channels it has been invited to.

Choose Team Routing

In Open-Inspect, bind each Slack channel in Teams > your team > Channels. Then open Settings > Integrations > Slack and save Unbound channels: workspace (the default, Create workspace-level sessions) or reject (Reject requests until the channel is bound). This is the integration's workspace-wide unboundChannels setting, not an environment variable or Terraform input. Slack DMs are personal conversations and bypass the unbound-channel rejection policy; session creation still checks other policies, including requireTeamOnCreate. A workspace fallback does not override a requirement to choose a team. See Slack Integration.


Linear Agent (Optional)

Skip this section if you don't need the Linear Agent integration.

Create a Linear OAuth App

  1. Create an application in Linear Settings → API → Applications.
  2. Enable webhooks and subscribe to Agent session events. Permission changes and Inbox notifications are also useful operational signals.
  3. Enable Client credentials tokens. This Linear-side setting is not managed by Terraform.
  4. Configure these URLs, replacing the deployment name and Workers subdomain:
    • Callback URL: https://open-inspect-linear-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/oauth/callback
    • Webhook URL: https://open-inspect-linear-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/webhook
  5. Record the client ID, client secret, and webhook signing secret for terraform.tfvars.
  6. Set enable_linear_bot = true, linear_client_id, linear_client_secret, and linear_webhook_secret in terraform.tfvars, then run terraform apply. Like Slack, the Linear classifier needs an Anthropic key unless classification_model points at an OpenAI model; see Anthropic (Optional).

The app is installed after deployment in Install the Linear Agent. Runtime access uses replaceable client-credentials tokens; authorization-code refresh tokens are not stored as runtime credentials.

Install the Linear Agent

After the Linear bot Worker is deployed, visit:

https://open-inspect-linear-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/oauth/authorize

A Linear workspace admin must approve the installation. After installation, the agent appears in mention and assignment menus. Before testing, bind the external Linear team in Open-Inspect's Teams > your team > Channels. In Settings > Integrations > Linear, save Unbound Linear teams: workspace (the default, Create workspace-level sessions) or reject (Reject requests until bound). This workspace-wide integration setting is named unboundChannels; it is not an environment variable or Terraform input. Workspace fallback still must satisfy other creation policies, including requireTeamOnCreate.

Test by mentioning the agent on an issue in the bound Linear team, then use View Session to verify the corresponding Open-Inspect session has the intended owning team and repository.

For upgrades, enable Client credentials tokens before deploying. No reinstall is expected for an eligible existing installation, but allow already-running sessions to finish before upgrading because older callback contexts may not contain the installed app-user identity.

For configuration and troubleshooting, see Linear Integration.


GitHub Bot (Optional)

The GitHub bot handles PR review assignments and @mention commands. To deploy it, set these in terraform.tfvars, then run terraform apply:

enable_github_bot     = true
github_webhook_secret = "your-generated-value" # From Step 4
github_bot_username   = "my-app[bot]"          # See "Find Your Bot Username" below

The GitHub App also needs the GitHub bot permissions listed in Step 3 (Actions, Checks, and Issues).

After the GitHub bot worker is deployed, configure the GitHub App for webhook delivery.

Configure Webhook on GitHub App

  1. Go to your GitHub App settings
  2. Select your Open-Inspect app
  3. Under Webhook:
    • Check "Active"
    • Webhook URL:
      https://open-inspect-github-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/webhooks/github
      
      (Replace YOUR-SUBDOMAIN with your Cloudflare Workers subdomain and {deployment_name} with your deployment name from terraform.tfvars)
    • Webhook secret: Enter the github_webhook_secret value from your terraform.tfvars
  4. Under Subscribe to events, check:
    • Pull requests
    • Issues
    • Issue comments
    • Pull request reviews
    • Pull request review comments
    • Check suites
    • Workflow runs (required for GitHub workflow-run automations)
  5. Click Save changes

Find Your Bot Username

Your GitHub App's bot username is its slug with [bot] appended. You can find it by:

  1. Having the bot perform any action (e.g., a PR review)
  2. Checking the actor's login in the webhook payload

Or construct it from your App's slug: if your app is named My-Inspect-App, the bot username is my-inspect-app[bot]. Ensure this matches the github_bot_username value in your terraform.tfvars.

Usage

  • Code Review: Open a non-draft PR in a repository where auto-review is enabled — it performs an automated review
  • Comment Actions: @mention the bot in a PR comment with instructions (e.g., @my-app[bot] explain why this test is failing)

For day-to-day workflows, see GitHub Integration.


Custom Domain for the Web App (Optional)

Custom domains require web_platform = "cloudflare". By default the web app is served from https://open-inspect-web-{deployment_name}.YOUR-SUBDOMAIN.workers.dev. To use your own hostname, set both of these in terraform.tfvars:

cloudflare_zone_id       = "your-zone-id"    # zone that owns the hostname
cloudflare_custom_domain = "app.example.com" # bare hostname, no scheme

Cloudflare provisions the DNS record and edge certificate automatically. Notes:

  • The canonical browser-auth origin and the links the bots send become https://{your-custom-domain}, and the workers.dev route for the web Worker is disabled so the app has a single canonical origin.
  • Update the GitHub App callback URL (and the Google redirect URI, if Google login is enabled) to the new hostname, or sign-in will fail with a redirect URI mismatch.
  • The Cloudflare API token needs zone-level Workers Routes: Edit permission to attach the domain.

Set Up CI/CD (Optional)

Enable automatic deployments when you push to main by configuring GitHub Actions secrets and variables under your fork's Settings → Secrets and variables → Actions.

Use the Variables tab for the following non-secret settings (only configure the providers and features you use):

# Deployment and Cloudflare
DEPLOYMENT_NAME
WEB_PLATFORM
SANDBOX_PROVIDER
CLOUDFLARE_ACCOUNT_ID
CLOUDFLARE_WORKER_SUBDOMAIN
R2_MEDIA_LOCATION
R2_MEDIA_BUCKET_NAME
SANDBOX_INACTIVITY_TIMEOUT_MS
SANDBOX_BOOT_TIMEOUT_MS
ENABLE_DURABLE_OBJECT_BINDINGS
ENABLE_SERVICE_BINDINGS

# Vercel web app
VERCEL_TEAM_ID
VERCEL_PROJECT_ID

# Documentation site (packages/docs)
DOCS_SITE_ENABLED
DOCS_CUSTOM_DOMAIN

# Modal
MODAL_WORKSPACE
MODAL_ENVIRONMENT
MODAL_ENVIRONMENT_WEB_SUFFIX

# Application IDs and bot configuration
GH_OAUTH_CLIENT_ID
GOOGLE_CLIENT_ID
GH_APP_ID
GH_APP_INSTALLATION_ID
ENABLE_SLACK_BOT
ENABLE_GITHUB_BOT
GH_BOT_USERNAME
ENABLE_LINEAR_BOT
LINEAR_CLIENT_ID

# Access control and branding
TEAMS_ENFORCEMENT
ALLOWED_USERS
ALLOWED_EMAIL_DOMAINS
ALLOWED_EMAILS
ALLOWED_GITHUB_ORGS
UNSAFE_ALLOW_ALL_USERS
APP_NAME
APP_ICON_URL

# Daytona
DAYTONA_API_URL
DAYTONA_BASE_SNAPSHOT
DAYTONA_BASE_SNAPSHOT_MEMORY_GIB
DAYTONA_TARGET
DAYTONA_TOOLBOX_API_URL
DAYTONA_PREBUILDS_ENABLED

# Vercel Sandbox
VERCEL_SANDBOX_PROJECT_ID
VERCEL_SANDBOX_TEAM_ID
VERCEL_SANDBOX_API_BASE_URL
VERCEL_BASE_SNAPSHOT_ID
VERCEL_SANDBOX_RUNTIME
VERCEL_SNAPSHOT_EXPIRATION_MS

# OpenComputer
OPENCOMPUTER_API_URL
OPENCOMPUTER_TEMPLATE
OPENCOMPUTER_PROJECT_ID
OPENCOMPUTER_TARGET

# E2B
E2B_TEMPLATE_ID
E2B_API_URL
E2B_SANDBOX_TIMEOUT_SECONDS
E2B_AUTO_PAUSE
E2B_TEMPLATE_CPU
E2B_TEMPLATE_MEMORY_MB

These settings resolve as non-empty variable → same-named secret → existing default, where a workflow default exists. Existing secret-only deployments need no migration. If both are set, the variable wins; delete it to return to the secret. An empty variable does not clear an existing secret. Values such as false and 0 are strings in Actions variables and are preserved.

For Teams, both Terraform plan and apply use the repository variable TEAMS_ENFORCEMENT, then the same-named secret, then shadow. Set the variable to on when enabling non-private team read enforcement through CI; a local terraform.tfvars choice alone does not configure the workflow.

Keep credentials in the Secrets tab: API tokens/keys, OAuth client secrets, signing secrets, private keys, encryption keys, and both MODAL_TOKEN_ID and MODAL_TOKEN_SECRET. Allowlist values may contain personal information; leave them in secrets if you prefer masking in workflow logs.

The table below describes deployment settings and credentials; use Variables for the names above and Secrets for credentials:

Setting Name Value
CLOUDFLARE_API_TOKEN Your Cloudflare API token
CLOUDFLARE_ACCOUNT_ID Your Cloudflare account ID
CLOUDFLARE_WORKER_SUBDOMAIN Your workers.dev subdomain
R2_MEDIA_LOCATION R2 location hint for the media bucket (defaults to ENAM)
R2_MEDIA_BUCKET_NAME Optional media bucket name override for a pre-created bucket
DEPLOYMENT_NAME Your deployment name
TEAMS_ENFORCEMENT Session enforcement: off, shadow (default), or on; prefer a repository variable
R2_ACCESS_KEY_ID R2 access key ID
R2_SECRET_ACCESS_KEY R2 secret access key
WEB_PLATFORM vercel or cloudflare
VERCEL_API_TOKEN Vercel API token (only if web_platform = "vercel")
VERCEL_TEAM_ID Vercel team/account ID (only if web_platform = "vercel")
VERCEL_PROJECT_ID Vercel project ID (only if web_platform = "vercel")
DOCS_SITE_ENABLED true keeps the docs site Vercel project (default: false; see packages/docs/README.md)
DOCS_CUSTOM_DOMAIN Optional docs site hostname, e.g. docs.example.com (default: the vercel.app URL only)
MODAL_TOKEN_ID Modal token ID
MODAL_TOKEN_SECRET Modal token secret
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 (default), modal-vm, daytona, vercel, opencomputer, or e2b
SANDBOX_INACTIVITY_TIMEOUT_MS Idle milliseconds before a sandbox is snapshotted and stopped (defaults to 600000)
SANDBOX_BOOT_TIMEOUT_MS Milliseconds a connected sandbox may keep booting before it fails (defaults to 1800000)
DAYTONA_API_URL Daytona API URL (only if sandbox_provider = "daytona")
DAYTONA_API_KEY Daytona API key (only if sandbox_provider = "daytona")
DAYTONA_BASE_SNAPSHOT Daytona base snapshot name prefix (only if sandbox_provider = "daytona")
DAYTONA_BASE_SNAPSHOT_MEMORY_GIB Base snapshot memory in GiB (defaults to 2)
DAYTONA_TARGET Optional Daytona target name
DAYTONA_TOOLBOX_API_URL Optional Daytona toolbox proxy override; empty uses the proxy each sandbox reports
DAYTONA_PREBUILDS_ENABLED true to admit new Daytona prebuilt-image builds and boot from them (default: false)
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; skips Terraform-managed snapshot builds
VERCEL_SANDBOX_RUNTIME Optional Vercel Sandbox runtime (defaults to node24)
VERCEL_SNAPSHOT_EXPIRATION_MS Optional Vercel runtime snapshot expiration in milliseconds (0 means no expiration)
VERCEL_SANDBOX_API_BASE_URL Optional advanced Vercel Sandbox API base URL override
OPENCOMPUTER_API_KEY OpenComputer API key (only if sandbox_provider = "opencomputer")
E2B_API_KEY E2B API key (only if sandbox_provider = "e2b")
GH_OAUTH_CLIENT_ID Optional GitHub sign-in client ID; set with GH_OAUTH_CLIENT_SECRET
GH_OAUTH_CLIENT_SECRET Optional GitHub sign-in client secret; set with GH_OAUTH_CLIENT_ID
GOOGLE_CLIENT_ID Optional Google sign-in client ID; set with GOOGLE_CLIENT_SECRET
GOOGLE_CLIENT_SECRET Optional Google sign-in client secret; set with GOOGLE_CLIENT_ID
GH_APP_ID Required GitHub App repository-access ID
GH_APP_PRIVATE_KEY Required GitHub App repository-access private key (PKCS#8 format)
GH_APP_INSTALLATION_ID Required GitHub App repository-access installation ID
ENABLE_SLACK_BOT true to deploy Slack bot, false to skip (default: true)
SLACK_BOT_TOKEN Slack bot token (required if enabled)
SLACK_SIGNING_SECRET Slack signing secret (required if enabled)
ENABLE_LINEAR_BOT true to deploy Linear bot, false to skip (default: false)
LINEAR_CLIENT_ID Linear OAuth application client ID (required if Linear enabled)
LINEAR_CLIENT_SECRET Linear OAuth application client secret (required if Linear enabled)
LINEAR_WEBHOOK_SECRET Linear webhook signing secret (required if Linear enabled)
LINEAR_API_KEY Optional Linear API key used as a comment-posting fallback
ANTHROPIC_API_KEY Optional; reaches Modal and OpenComputer sandboxes; classifier fallback
CLASSIFICATION_ANTHROPIC_API_KEY Optional classifier-only Anthropic key; never reaches sandboxes
CLASSIFICATION_OPENAI_API_KEY Classifier OpenAI key (required when classification_model is an OpenAI id)
OPENAI_API_KEY Optional OpenAI API key used when a session selects API-key authentication
XAI_API_KEY Optional xAI API key used when a session selects API-key authentication
DEEPSEEK_API_KEY DeepSeek API key (optional, required only for DeepSeek models)
TOKEN_ENCRYPTION_KEY Generated encryption key (OAuth tokens)
REPO_SECRETS_ENCRYPTION_KEY Generated encryption key (repo secrets)
PROVIDER_ACCOUNTS_ENCRYPTION_KEY Optional existing provider-account key override; Terraform generates one when omitted
MODAL_API_SECRET Generated Modal API secret
NEXTAUTH_SECRET Generated browser-auth secret (legacy Actions secret name)
ALLOWED_USERS Comma-separated GitHub usernames (empty = list unused; see note below)
ALLOWED_EMAIL_DOMAINS Comma-separated email domains (empty = list unused; see note below)
ALLOWED_EMAILS Comma-separated exact email addresses (for individual users on shared domains)
ALLOWED_GITHUB_ORGS Comma-separated GitHub orgs whose active members can sign in
UNSAFE_ALLOW_ALL_USERS true to allow any authenticated user when every allowlist is empty (defaults to false)
ENABLE_DURABLE_OBJECT_BINDINGS Optional Terraform CI flag for Durable Object phase 1 (defaults to true)
ENABLE_SERVICE_BINDINGS Optional Terraform CI flag for service-binding phase 1 (defaults to true)
ENABLE_GITHUB_BOT true to deploy GitHub bot worker (or empty to skip)
GH_WEBHOOK_SECRET GitHub webhook secret (required if GitHub bot enabled)
GH_BOT_USERNAME GitHub App bot username, e.g., my-app[bot] (required if GitHub bot enabled)
APP_NAME Optional display name for whitelabeling (default: Open-Inspect)
APP_ICON_URL Optional URL to a custom logo/favicon (default: built-in icon)

An empty allowlist only means that list is not used; it does not admit everyone. Terraform fails the plan unless at least one of ALLOWED_USERS, ALLOWED_EMAIL_DOMAINS, ALLOWED_EMAILS, or ALLOWED_GITHUB_ORGS is set, or UNSAFE_ALLOW_ALL_USERS is true. Each enabled sign-in provider also needs a compatible allowlist; see Choose Sign-In Providers.

CLASSIFICATION_MODEL is an optional Actions variable, not a secret — add it under Settings → Secrets and variables → Actions → Variables to point the Slack/Linear classifiers at a different model (for example gpt-5.4-mini). Leave it unset to keep the Terraform default. An OpenAI value also requires the CLASSIFICATION_OPENAI_API_KEY secret; an Anthropic value is served by CLASSIFICATION_ANTHROPIC_API_KEY, falling back to ANTHROPIC_API_KEY. To keep the classifier key out of Modal and OpenComputer sandboxes, set CLASSIFICATION_ANTHROPIC_API_KEY and leave ANTHROPIC_API_KEY unset; sandboxes then take model credentials from Open-Inspect's secret store. The optional CLASSIFICATION_REASONING_EFFORT variable sets the reasoning effort an OpenAI classifier requests (for example low); leave it unset to use the model's default.

When enabling or upgrading the Linear bot, also enable Client credentials tokens on the OAuth application in Linear Settings → API → Applications. This provider-side setting is not managed by Terraform. Existing eligible single-workspace installations transition on their next request without uninstalling or reinstalling the app.

Bulk upload with gh CLI:

For non-secret settings, create a .variables file:

CLOUDFLARE_ACCOUNT_ID=your-account-id
DEPLOYMENT_NAME=my-deployment
WEB_PLATFORM=cloudflare
ENABLE_SLACK_BOT=false

Upload it with gh variable set -f .variables. For credentials, create a .secrets file (don't commit this!):

CLOUDFLARE_API_TOKEN=your-token
ANTHROPIC_API_KEY=sk-ant-...
DEEPSEEK_API_KEY=sk-...
# ... add all secrets

Then upload all at once (run from your fork's directory, or use -R {your_github_username}/{background-agents}):

gh secret set -f .secrets

If you bulk upload from a file, set multiline secrets like GH_APP_PRIVATE_KEY separately so the PEM formatting is preserved:

gh secret set GH_APP_PRIVATE_KEY < private-key-pkcs8.pem

Once configured, the GitHub Actions workflow will:

  • Run terraform plan on pull requests (with PR comment)
  • Run terraform apply when merged to main

Terraform generates and persists PROVIDER_ACCOUNTS_ENCRYPTION_KEY when no override is configured. Existing local Terraform installations retain an existing key through the provider_accounts_encryption_key input; Actions deployments retain it through the PROVIDER_ACCOUNTS_ENCRYPTION_KEY repository or production-environment secret. Changing the key makes stored provider credentials unreadable. Preserve backups of the remote Terraform state because it is the recovery source for automatically generated keys.


Customizing the App Name and Icon (Optional)

Open-Inspect can be whitelabeled by overriding the brand name and logo. Both values are optional and default to the built-in Open-Inspect brand.

Add these to your terraform.tfvars:

# Display name shown in:
#   - Web tab title, sign-in page, landing hero
#   - Slack App Home settings page
#   - Linear OAuth success page and completion comments
#   - PR body footer ("Created with [<app_name>](<session-url>)")
#   - Outbound HTTP User-Agent headers (GitHub, GitLab API)
app_name = "Acme Bot"

# Optional URL to a custom logo image (SVG/PNG). When set, replaces the icon in
# the command menu and favicon. Leave empty to keep the built-in favicon.
# Use an absolute URL or a root-relative path served from packages/web/public/.
app_icon_url = "/branding/acme-logo.svg"   # or "https://cdn.example.com/logo.svg"

After changing any of these values, run terraform apply and (for Vercel) redeploy the web app so the new build picks up the NEXT_PUBLIC_APP_NAME and NEXT_PUBLIC_APP_ICON_URL env vars (Cloudflare's web deploy is rebuilt automatically by Terraform).

Note: NEXT_PUBLIC_* vars are inlined into the client bundle at build time, so changes require a fresh web build. The bot/control-plane workers read APP_NAME at request time, so they pick up the new value immediately after terraform apply.


Updating Your Deployment

To update after pulling changes from upstream:

Terraform generates the provider-account credential key for installations without an existing override. For local applies, keep any existing provider_accounts_encryption_key input unchanged. For Actions deployments, keep any existing PROVIDER_ACCOUNTS_ENCRYPTION_KEY repository or production-environment secret unchanged so stored provider credentials remain readable.

# Pull latest changes
git pull upstream main

# Rebuild shared package if it changed
npm run build -w @open-inspect/shared

# Re-run Terraform (it only changes what's needed)
cd terraform/environments/production
terraform apply

Configure Provider Accounts

Open Settings > Provider Accounts to add and verify accounts. Setting a provider default changes only sessions created afterward; existing sessions remain pinned to legacy scoped OAuth, a specific account, or API-key mode. Legacy credentials may coexist during rollout, and the settings page lists their locations. Remove them only after dependent legacy-bound sessions are no longer needed. Rebuild all sandbox runtime images, templates, and provider snapshots so new sessions use the generic broker.


Troubleshooting

"Backend initialization required"

Re-run init with backend config:

terraform init -backend-config=backend.tfvars

GitHub App authentication fails

  1. Verify the private key is in PKCS#8 format (starts with -----BEGIN PRIVATE KEY-----)
  2. Check the Installation ID matches your installation
  3. Ensure the app has required permissions on the repository
  4. Verify the callback URL matches your deployed web app URL exactly

GitHub OAuth "redirect_uri is not associated with this application"

The callback URL in your GitHub App settings doesn't match your deployed URL. Update the callback URL to match your web app URL:

  • Vercel: https://open-inspect-{deployment_name}.vercel.app/api/auth/callback/github
  • Cloudflare: https://open-inspect-web-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/api/auth/callback/github
  • Cloudflare with a custom domain: https://{your-custom-domain}/api/auth/callback/github

Modal deployment fails

# Check Modal CLI is working (from packages/modal-infra)
cd packages/modal-infra
uv run modal token show

# View Modal logs
uv run modal app logs open-inspect

Modal deployment fails with "No module named 'sandbox_runtime'"

The sandbox_runtime package is a sibling package that must be installed before deploying. From the repository root:

cd packages/modal-infra && uv sync --frozen && cd -

This installs all Modal deployment dependencies including sandbox_runtime (resolved via [tool.uv.sources] in pyproject.toml).

Worker deployment fails / "no such file or directory" for dist/index.js

Terraform references the built worker bundles. Build them before running terraform apply:

# Build shared package first
npm run build -w @open-inspect/shared

# Build workers (required before Terraform)
npm run build -w @open-inspect/control-plane -w @open-inspect/slack-bot -w @open-inspect/github-bot -w @open-inspect/linear-bot

# Verify bundles exist
ls packages/control-plane/dist/index.js
ls packages/slack-bot/dist/index.js
ls packages/github-bot/dist/index.js  # Only if enable_github_bot = true
ls packages/linear-bot/dist/index.js  # Only if enable_linear_bot = true

Slack bot not responding

  1. Verify Event Subscriptions URL is verified (green checkmark)
  2. Ensure the bot is invited to the channel (/invite @BotName)
  3. Check that you're @mentioning the bot in your message
  4. If you updated bot token scopes, reinstall the app to your workspace

Slack bot ignores thread context

If the bot doesn't see the original message when tagged in a thread reply:

  1. Verify the bot has channels:history scope (for public channels) and groups:history (for private channels). These are required by the conversations.replies API to fetch thread messages.
  2. Verify the bot has channels:read and groups:read scopes. These are required by conversations.info to fetch channel name and description for context, and by conversations.list to populate the automation channel picker. If the picker shows no channels, check these scopes and that the bot is invited to the target channel.
  3. If you added missing scopes, reinstall the app to your workspace for the new permissions to take effect.

Slack image attachment does not reach the agent

  1. Verify the bot has the files:read scope and reinstall the app after adding it. The files:write scope is for generated media posted back to Slack, not images sent to the agent.
  2. Use PNG, JPEG, WebP, or GIF images no larger than 10 MiB. Open-Inspect forwards at most six images per message.
  3. In a channel, @mention the bot with the image. DMs do not require a mention. Watched-channel automations do not forward file attachments.
  4. For channel mentions and replies, verify channels:history for public channels or groups:history for private channels. Slack may omit files from the mention event, so the bot uses conversation history to retrieve them.
  5. Check the Slack thread for a warning about images that were too large or could not be downloaded or uploaded. Other images and any text are still sent when possible.

Slack completion does not attach generated media

  1. Verify the bot has the files:write scope and reinstall the app after adding it.
  2. Confirm the agent registered the image or video as a session artifact; repository files are not uploaded automatically.
  3. Check that the file is PNG, JPEG, WebP, or MP4 and no larger than 10 MiB. A completion attaches at most five files and 25 MiB total; other media remains available through View Session.
  4. Check Slack workspace policies for disabled uploads, prohibited file types, or exhausted storage.

GitHub bot not responding to webhooks

  1. Verify the webhook URL matches https://open-inspect-github-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/webhooks/github
  2. Check the webhook secret matches github_webhook_secret in terraform.tfvars
  3. Confirm enable_github_bot = true in terraform.tfvars and the worker is deployed
  4. Check that github_bot_username matches your App's bot login (e.g., my-app[bot])
  5. For PR reviews, ensure auto-review is enabled for the repository and the PR is not a draft
  6. For comment actions, ensure the bot is @mentioned in a PR comment (not an issue)

"Model not found" errors

If sessions fail with "Model not found", the API key for the selected model is missing. Deployments that set no fleet-wide key in Terraform supply model credentials as secrets instead:

  1. Go to Settings > Secrets in the web app
  2. Select All Repositories (Global) from the scope dropdown
  3. Add the key for your chosen provider (e.g., ANTHROPIC_API_KEY for Claude models or DEEPSEEK_API_KEY for DeepSeek models, ZHIPU_API_KEY for Z.AI Coding Plan models, or OPENCODE_API_KEY for OpenCode Zen and OpenCode Go models)
  4. Click Save

See Secrets Management for more on global and repository secrets.

Vercel provider error when using web_platform = "cloudflare"

The Vercel Terraform provider validates its API token on initialization, even when no Vercel resources are created. If you set vercel_api_token = "" in your terraform.tfvars, the provider will reject it. Fix: Remove the vercel_api_token and vercel_team_id lines from your terraform.tfvars entirely — the built-in defaults ("unused") satisfy the provider's non-empty validation. This is a known Terraform limitation (providers validate credentials on init regardless of whether any resources use them).

Durable Objects / Service Binding errors

This occurs on first deployment. Follow the two-phase deployment process:

  1. Deploy with enable_durable_object_bindings = false and enable_service_bindings = false
  2. After success, set both to true and run terraform apply again

Security Notes

  • Never commit terraform.tfvars or backend.tfvars to source control
  • The .gitignore already excludes these files
  • Use GitHub Secrets for CI/CD, not hardcoded values
  • Rotate secrets periodically using terraform apply after updating terraform.tfvars
  • Review the Security Model - this system is designed for single-tenant deployment

Architecture Reference

For details on the infrastructure components, see: