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 same GitHub App credentials and can access any repository the App is installed on. See the Security Model for details.
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 |
Web platform choice: Set
web_platformin yourterraform.tfvarsto"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.
Create accounts on these services before continuing:
| Service | Purpose |
|---|---|
| Cloudflare | Control plane hosting (+ web app if using Cloudflare platform) |
| Vercel (optional) | Web application hosting (only if web_platform = "vercel") |
| Modal (optional) | Sandbox infrastructure when sandbox_provider = "modal" |
| Daytona (optional) | Sandbox infrastructure when sandbox_provider = "daytona" |
| GitHub | OAuth + repository access |
| Anthropic | Claude API |
| Slack (optional) | Slack bot integration |
| GitHub App Webhooks (optional) | GitHub bot (PR reviews) |
# Terraform (1.9.0+)
brew install terraform
# Node.js (22+)
brew install node@22
# 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 wranglerFork 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 -Tip: Before proceeding, copy
terraform/environments/production/terraform.tfvars.exampletoterraform.tfvarsand keep it open. As you collect credentials in the following steps, paste them directly into this file.
- Go to Cloudflare Dashboard
- Note your Account ID (visible in the dashboard URL or account overview)
- Note your Workers subdomain: Go to Workers & Pages → Overview, look in the bottom-right
of the panel for
*.YOUR-SUBDOMAIN.workers.dev - Create API Token at API Tokens:
- Use template: "Edit Cloudflare Workers"
- Add permissions: Workers KV Storage (Edit), Workers R2 Storage (Edit)
- Enable R2: Must add payment info, but first 10 GB/month is free
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-stateCreate an R2 API Token:
- Go to R2 → Overview → Manage R2 API Tokens
- Create token with Object Read & Write permission
- Note the Access Key ID and Secret Access Key
Skip this section if you're deploying the web app to Cloudflare Workers. Important: Do not set
vercel_api_tokenorvercel_team_idto empty strings in yourterraform.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.
- Go to Vercel Account Settings → Tokens
- Create a new token with full access
- 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_)
Only required when
sandbox_provider = "modal".
- Go to Modal Settings
- Create a new API token: Settings -> API Tokens -> New Token
- Note the Token ID and Token Secret
- Note your Workspace name (visible in your Modal dashboard URL)
Only required when
sandbox_provider = "daytona".
- 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)
- Note the API URL (e.g.,
https://app.daytona.io/api) and optional target - Seed the named base snapshot before pointing traffic at Daytona:
After initial setup, Terraform automatically rebuilds the snapshot when source files change.
cd packages/daytona-infra pip install daytona # or: uv pip install daytona python -m src.bootstrap --force
- Set
sandbox_provider = "daytona"interraform.tfvars - Set
daytona_api_url,daytona_api_key, anddaytona_base_snapshotinterraform.tfvars
The control plane calls the Daytona REST API directly — no shim service to deploy.
Important: Unlike Modal, the Daytona provider does not automatically inject LLM API keys into sandboxes. If you plan to use Claude models, add
ANTHROPIC_API_KEYas a global secret in Settings > Secrets after deploying. See Secrets Management for details.
- Go to Anthropic Console
- Create an API key
- 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).
You only need one GitHub App - it handles both user authentication (OAuth) and repository access.
-
Go to GitHub Apps
-
Click "New GitHub App"
-
Fill in the basics:
- Name:
Open-Inspect-YourName(must be globally unique) - Homepage URL: Your web app URL (see below)
- Webhook: Uncheck "Active" (not needed)
- Name:
-
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
Important: The callback URL must match your deployed web app URL exactly. The
{deployment_name}is the unique value you set interraform.tfvars(e.g., your GitHub username or company name). - Callback URL:
-
Set Repository permissions:
- Contents: Read & Write
- Issues: Read & Write (required if enabling GitHub bot)
- Pull requests: Read & Write
- Metadata: Read-only
-
Click "Create GitHub App"
-
Note the App ID and Client ID (top of page)
-
Under "Client secrets", click "Generate a new client secret" and note the Client Secret
-
Scroll down to "Private keys" and click "Generate a private key" (downloads a .pem file)
-
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
-
Install the app on your account/organization:
- Click "Install App" in the sidebar
- Select the repositories you want Open-Inspect to access
-
Note the Installation ID from the URL after installing:
https://github.com/settings/installations/INSTALLATION_ID
You should now have:
- App ID (e.g.,
123456) - Client ID (e.g.,
Iv1.abc123...) - Client Secret (e.g.,
abc123...) - Private Key (PKCS#8 format, starts with
-----BEGIN PRIVATE KEY-----) - Installation ID (e.g.,
12345678)
Skip this step if you don't need Slack integration.
- Go to Slack API Apps
- Click "Create New App" → "From scratch"
- Name it (e.g.,
Open-Inspect) and select your workspace
- Go to OAuth & Permissions in the sidebar
- Add Bot Token Scopes:
app_mentions:readchat:writechannels:historychannels:readgroups:historygroups:readim:historyim:readreactions:write
- Click "Install to Workspace"
- 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.
- Go to Basic Information
- Note the Signing Secret
Event Subscriptions require the Slack bot worker to be deployed first for URL verification. You'll configure this in Step 7b after running Terraform.
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)"
# Internal callback secret
echo "internal_callback_secret: $(openssl rand -base64 32)"
# Modal API secret (use hex for this one)
echo "modal_api_secret: $(openssl rand -hex 32)"
# NextAuth secret
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.
cd terraform/environments/production
# Copy the example files
cp terraform.tfvars.example terraform.tfvars
cp backend.tfvars.example backend.tfvarsFill 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"
}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"
# 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"
# 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"
# 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
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
# Slack (set enable_slack_bot = false to disable Slack integration)
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 5 (required if enabled)
github_bot_username = "" # e.g., "my-app[bot]" (your GitHub App's bot login)
# API Keys
anthropic_api_key = "sk-ant-..."
# Security Secrets (from Step 5)
token_encryption_key = "your-generated-value"
repo_secrets_encryption_key = "your-generated-value"
internal_callback_secret = "your-generated-value"
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 = "../../../"
# Initial deployment: set both to false (see Step 7)
enable_durable_object_bindings = false
enable_service_bindings = false
# Access Control (set at least one allowlist for production)
allowed_users = "your-github-username" # Comma-separated GitHub usernames, or empty
allowed_email_domains = "" # Comma-separated domains (e.g., "example.com,corp.io")
# Explicitly opt into open access only if you want any authenticated GitHub user
# to be able to sign in when both allowlists are empty.
unsafe_allow_all_users = falseNote: Review
allowed_usersandallowed_email_domainscarefully - these control who can sign in. Terraform now fails if both are empty unless you explicitly setunsafe_allow_all_users = true.
Deployment requires two phases due to Cloudflare's Durable Object and service binding requirements.
Ensure your terraform.tfvars has:
enable_durable_object_bindings = false
enable_service_bindings = falseImportant: 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-botThen run:
cd terraform/environments/production
# Initialize Terraform with backend config
terraform init -backend-config=backend.tfvars
# Deploy (phase 1 - creates workers without bindings)
terraform applyAfter Phase 1 succeeds, update your terraform.tfvars:
enable_durable_object_bindings = true
enable_service_bindings = trueThen run:
terraform applyTerraform will update the workers with the required bindings.
Now that the Slack bot worker is deployed, configure the App Home and Event Subscriptions.
The App Home provides a settings interface where users can configure their preferred Claude model.
- Go to Slack Apps -> Your Slack App → App Home
- Under Show Tabs, toggle "Home Tab" to On
- Go to Slack Apps -> Your Slack App → Event Subscriptions
- Toggle "Enable Events" to On
- Enter Request URL:
(Replace
https://open-inspect-slack-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/eventsYOUR-SUBDOMAINwith your Cloudflare Workers subdomain and{deployment_name}with your deployment name from terraform.tfvars) - Wait for the green "Verified" checkmark
- Under Subscribe to bot events, add:
app_home_opened(required for App Home settings)app_mentionmessage.channels(optional - if you want the bot to see all channel messages)message.im(enables direct message support)
- Click Save Changes
- Go to Interactivity & Shortcuts
- Toggle "Interactivity" to On
- Enter Request URL:
https://open-inspect-slack-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/interactions - Click Save Changes
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.
Now that the GitHub bot worker is deployed, configure the GitHub App for webhook delivery.
- Go to your GitHub App settings
- Select your Open-Inspect app
- Under Webhook:
- Check "Active"
- Webhook URL:
(Replace
https://open-inspect-github-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/webhooks/githubYOUR-SUBDOMAINwith your Cloudflare Workers subdomain and{deployment_name}with your deployment name from terraform.tfvars) - Webhook secret: Enter the
github_webhook_secretvalue from your terraform.tfvars
- Under Subscribe to events, check:
- Pull requests
- Issue comments
- Pull request review comments
- Click Save changes
Your GitHub App's bot username is its slug with [bot] appended. You can find it by:
- Having the bot perform any action (e.g., a PR review)
- 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.
- Code Review: Assign the bot as a PR reviewer — it performs an automated review
- Comment Actions: @mention the bot in a PR comment with instructions (e.g.,
@my-app[bot] fix the failing test)
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.
Terraform creates the Vercel project and configures environment variables, but does not deploy the code. You have two options:
# From the repository root (replace {deployment_name} with your value from terraform.tfvars)
npx vercel link --project open-inspect-{deployment_name}
npx vercel --prodNote: 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
- Go to Vercel Dashboard
- Find the
open-inspect-{deployment_name}project - Go to Settings → Git
- Click "Connect Git Repository" and select your fork
- 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).
After deployment completes, verify each component:
# Get the verification commands from Terraform
terraform output verification_commandsOr 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. Modal health check (replace YOUR-WORKSPACE)
curl https://YOUR-WORKSPACE--open-inspect-api-health.modal.run
# 3. Web app (should return 200)
# Vercel:
curl -I https://open-inspect-{deployment_name}.vercel.app
# Cloudflare:
curl -I https://open-inspect-web-{deployment_name}.YOUR-SUBDOMAIN.workers.dev- Visit your web app URL
- Sign in with GitHub
- Create a new session with a repository
- Send a prompt and verify the sandbox starts
Enable automatic deployments when you push to main by adding GitHub Secrets.
Go to your fork's Settings → Secrets and variables → Actions, and add:
| Secret Name | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
Your Cloudflare API token |
CLOUDFLARE_ACCOUNT_ID |
Your Cloudflare account ID |
CLOUDFLARE_WORKER_SUBDOMAIN |
Your workers.dev subdomain |
DEPLOYMENT_NAME |
Your deployment name |
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") |
NEXTAUTH_URL |
Your web app URL |
MODAL_TOKEN_ID |
Modal token ID |
MODAL_TOKEN_SECRET |
Modal token secret |
MODAL_WORKSPACE |
Modal workspace name |
GH_OAUTH_CLIENT_ID |
GitHub App OAuth client ID |
GH_OAUTH_CLIENT_SECRET |
GitHub App OAuth client secret |
GH_APP_ID |
GitHub App ID |
GH_APP_PRIVATE_KEY |
GitHub App private key (PKCS#8 format) |
GH_APP_INSTALLATION_ID |
GitHub App 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) |
ANTHROPIC_API_KEY |
Anthropic API key |
TOKEN_ENCRYPTION_KEY |
Generated encryption key (OAuth tokens) |
REPO_SECRETS_ENCRYPTION_KEY |
Generated encryption key (repo secrets) |
INTERNAL_CALLBACK_SECRET |
Generated callback secret |
MODAL_API_SECRET |
Generated Modal API secret |
NEXTAUTH_SECRET |
Generated NextAuth secret |
ALLOWED_USERS |
Comma-separated GitHub usernames (or empty for all users) |
ALLOWED_EMAIL_DOMAINS |
Comma-separated email domains (or empty for all domains) |
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) |
Bulk upload secrets with gh CLI:
Instead of adding secrets one by one, create a .secrets file (don't commit this!):
CLOUDFLARE_API_TOKEN=your-token
CLOUDFLARE_ACCOUNT_ID=your-account-id
ANTHROPIC_API_KEY=sk-ant-...
# ... 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 .secretsIf 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.pemOnce configured, the GitHub Actions workflow will:
- Run
terraform planon pull requests (with PR comment) - Run
terraform applywhen merged to main
To update after pulling changes from upstream:
# 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 applyRe-run init with backend config:
terraform init -backend-config=backend.tfvars- Verify the private key is in PKCS#8 format (starts with
-----BEGIN PRIVATE KEY-----) - Check the Installation ID matches your installation
- Ensure the app has required permissions on the repository
- Verify the callback URL matches your deployed web app URL exactly
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
# 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-inspectThe 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).
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
# 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- Verify Event Subscriptions URL is verified (green checkmark)
- Ensure the bot is invited to the channel (
/invite @BotName) - Check that you're @mentioning the bot in your message
- If you updated bot token scopes, reinstall the app to your workspace
If the bot doesn't see the original message when tagged in a thread reply:
- Verify the bot has
channels:historyscope (for public channels) andgroups:history(for private channels). These are required by theconversations.repliesAPI to fetch thread messages. - Verify the bot has
channels:readandgroups:readscopes. These are required byconversations.infoto fetch channel name and description for context. - If you added missing scopes, reinstall the app to your workspace for the new permissions to take effect.
- Verify the webhook URL matches
https://open-inspect-github-bot-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/webhooks/github - Check the webhook secret matches
github_webhook_secretin terraform.tfvars - Confirm
enable_github_bot = truein terraform.tfvars and the worker is deployed - Check that
github_bot_usernamematches your App's bot login (e.g.,my-app[bot]) - For PR reviews, ensure the bot is assigned as a reviewer (not just mentioned)
- For comment actions, ensure the bot is @mentioned in a PR comment (not an issue)
If sessions fail with "Model not found" when using sandbox_provider = "daytona", the required LLM
API key is likely missing. Unlike Modal (which injects keys automatically), Daytona requires you to
add them as global secrets:
- Go to Settings > Secrets in the web app
- Select All Repositories (Global) from the scope dropdown
- Add the key for your chosen provider (e.g.,
ANTHROPIC_API_KEYfor Claude models) - Click Save
See Secrets Management for more on global and repository secrets.
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).
This occurs on first deployment. Follow the two-phase deployment process:
- Deploy with
enable_durable_object_bindings = falseandenable_service_bindings = false - After success, set both to
trueand runterraform applyagain
- Never commit
terraform.tfvarsorbackend.tfvarsto source control - The
.gitignorealready excludes these files - Use GitHub Secrets for CI/CD, not hardcoded values
- Rotate secrets periodically using
terraform applyafter updatingterraform.tfvars - Review the Security Model - this system is designed for single-tenant deployment
For details on the infrastructure components, see:
- terraform/README.md - Terraform module documentation
- README.md - System architecture overview
- OPENAI_MODELS.md - Configuring OpenAI Codex models