Skip to content

Latest commit

 

History

History
329 lines (274 loc) · 20.9 KB

CLAUDE.md

File metadata and controls

329 lines (274 loc) · 20.9 KB

AGENTS.md

This repo is a monorepo for Orcabot - a sandboxed, multiplayer AI coding platform.

Coding Practices (MANDATORY for ALL repos)

Revision Markers

ALWAYS add a revision marker when modifying code. This eliminates "did you deploy?" debugging.

  1. Comment at top of modified file:

    // REVISION: <feature>-v<N>-<brief>
    
  2. Log on load/startup with runtime timestamp:

    // Frontend (TypeScript)
    const MODULE_REVISION = "feature-v1-desc";
    console.log(`[module] REVISION: ${MODULE_REVISION} loaded at ${new Date().toISOString()}`);
    // Backend (Go)
    const moduleRevision = "feature-v1-desc"
    log.Printf("[module] REVISION: %s loaded at %s", moduleRevision, time.Now().Format(time.RFC3339))
  3. For API responses: Add a revision field so clients can display what version is running.

This is non-negotiable. Never speculate about deployment issues - add logs that prove it.

What Orcabot Does

  • Run Claude Code, Codex, or shell in the browser with zero setup
  • Sandboxed VMs for security (isolated execution)
  • Built-in Chromium browser for testing
  • Secrets broker protects API keys from LLM exfiltration
  • Persistent, background intelligent processes
  • Multiplayer dashboards (Figma-like collaboration)

Structure

  • frontend — Next.js dashboard UI (includes blog, docs, admin)
  • controlplane — Cloudflare Worker control plane
  • sandbox — Go sandbox server
  • bridge — Node.js messaging bridge service (WhatsApp via Baileys), deployed on Fly
  • desktop — Tauri native desktop app (bundles entire stack locally with VM sandbox); also ships the orcabot CLI (a 2nd binary in the same crate) that runs the stack headlessly and drives it from the terminal
  • e2e — Playwright end-to-end tests (external, against running instance)

App Guides

  • Frontend: frontend/CLAUDE.md
  • Control plane: controlplane/CLAUDE.md
  • Sandbox: sandbox/CLAUDE.md
  • Bridge: bridge/CLAUDE.md
  • Desktop: desktop/CLAUDE.md

Deploy (Prod)

cd sandbox && make deploy && cd ../controlplane && wrangler deploy -c wrangler.production.toml && cd ../frontend && npm run workers:deploy && cd ..

Running Locally

Sandbox:

docker run --rm -it \
    -p 8080:8080 \
    --cpus=2 --memory=4g \
    -e SANDBOX_INTERNAL_TOKEN=... \
    -e CONTROLPLANE_URL=http://localhost:8787 \
    -e INTERNAL_API_TOKEN=... \
    -e ALLOWED_ORIGINS=http://localhost:8788 \
    -v orcabot-sandbox-workspace:/workspace \
    orcabot-sandbox

Control plane:

npm run dev

Frontend:

npx wrangler dev

Auth + Security Architecture

Auth Flow

  • Frontend never talks directly to sandbox; all traffic goes through the control plane
  • Cloudflare control plane uses dev auth via headers/query params when DEV_AUTH_ENABLED=true
  • Control plane → sandbox requests are signed per machine (Ed25519, SANDBOX_AUTH_SIGNING_KEY in the control plane; VMs get only SANDBOX_AUTH_PUBLIC_KEY). The legacy static SANDBOX_INTERNAL_TOKEN is accepted only while configured — do not reuse Cloudflare API keys. Desktop generates the key pair per boot and passes the public key on the VM's kernel command line; its VM holds no static token and no INTERNAL_API_TOKEN
  • Personal access tokens (PATs) authenticate the orcabot CLI / scripts against a remote control plane: prefix orca_pat_, sent as Authorization: Bearer, only the SHA-256 hash is stored (api_tokens table). Middleware is prefix-gated (no collision with PTY JWT bearers) and method-gated — a PAT cannot mint/list/revoke other PATs (rejectPatAuth). See controlplane/CLAUDE.md.

Secrets Protection (Security-Critical)

Orcabot has a layered defense system to prevent LLMs from exfiltrating API keys:

  1. Secrets Broker - API keys are NOT set as env vars. Instead, a session-local broker injects keys server-side. LLMs only see placeholder values. Broker configs and approved domains are session-namespaced — two terminals in the same VM cannot see or overwrite each other's keys.

  2. Output Redaction - Any secret values in PTY output are replaced with asterisks before reaching WebSocket clients.

  3. Domain Allowlisting - Built-in providers (Anthropic, OpenAI, ElevenLabs, etc.) have hardcoded target domains. Custom secrets require owner approval per-domain, scoped per-session.

  4. Localhost Auth - The MCP server and event endpoints on localhost require X-MCP-Secret proof-of-possession. Each PTY gets a unique ORCABOT_MCP_SECRET env var at creation. This prevents rogue processes in the sandbox from calling MCP tools or faking agent events.

  5. PTY Token Fail-Closed - INTERNAL_API_TOKEN must be non-empty. If it's missing, PTY token verification rejects all tokens rather than accepting them.

  6. Network Egress Proxy ("Little Snitch for AI Agents") - An HTTP/HTTPS forward proxy on localhost:8083 intercepts all outbound network requests from PTY processes. Known domains (package registries, git hosting, CDNs, LLM APIs) are allowed automatically. Unknown domains are held (the connection hangs) while a toast + approval dialog is shown to the user. Options: Allow Once, Always Allow, Deny, or Deny Always. 60-second timeout = deny (fail-closed). User-approved "Always Allow" domains persist per-dashboard in D1; "Deny Always" domains (e.g. trackers) persist per-dashboard in D1 too (egress_blocklist) and are then blocked immediately without prompting (deny takes precedence over the allowlist).

Security invariants (non-negotiable):

  • Broker configs keyed by sessionID:provider — never global provider name
  • Broker URLs include session ID: /broker/{sessionID}/{provider}/...
  • Approved domains keyed by sessionID:secretName — never just secret name
  • Empty INTERNAL_API_TOKEN = all PTY tokens rejected (fail-closed)
  • PTY + dashboard tokens are signed with PTY_TOKEN_SECRET, a control-plane-only secret — never INTERNAL_API_TOKEN, which every sandbox VM holds (root can read it from /proc). Without PTY_TOKEN_SECRET set the control plane falls back to INTERNAL_API_TOKEN and logs a SECURITY WARNING
  • Sandbox → control plane /internal/* calls authenticate with the VM's dashboard-scoped token (X-Dashboard-Token, sandbox/internal/cpauth), and every VM-reachable route checks the resource belongs to that dashboard (controlplane/src/auth/sandbox-auth.ts). X-Internal-Token is full trust for control-plane-side callers only; with WITHHOLD_INTERNAL_TOKEN_FROM_SANDBOX=true new VMs never receive INTERNAL_API_TOKEN. New VM-called routes must use authorizeSandboxForDashboard, never requireInternalAuth
  • Every control plane → sandbox request goes through sandboxFetch / sandboxHeaders / SandboxClient so it carries X-Sandbox-Auth: an Ed25519 signature over target machine (FLY_MACHINE_ID), method and path+query, valid 30s plus the VM's 30s skew allowance (60s replay window at most), plus fly-force-instance-id so no other VM is the first hop. Never call a sandbox with a hand-built fetch. A bad signature never falls back to the static token
  • The dashboard browser runs unprivileged, never root: Chromium as orcabot-browser, Xvfb/x11vnc/websockify as orcabot-vnc, on loopback ports 6100-6299 that pool UIDs and orcabot-browser (page JS) are blocked from, so one dashboard's page can't reach another's VNC/DevTools; the X display requires its auth cookie. See sandbox/CLAUDE.md (Browser Block)
  • A dashboard VM only ever receives its owner's secrets (dashboardSecretsOwner, controlplane/src/secrets/handler.ts): one VM is shared by every member's terminals, so pushing whoever acted last would put their keys behind every terminal's broker URL
  • Pool UIDs (2000-2099) are egress default-deny at the kernel: only loopback (incl. the 80/443 REDIRECT), established replies, DNS to resolv.conf nameservers, and EGRESS_EXTRA_TCP_PORTS (opt-in, e.g. 22) are allowed
  • Localhost MCP/event endpoints require valid X-MCP-Secret (no fail-open)
  • Egress proxy fail-closed: 60s timeout = deny; proxy crash = connections fail
  • Localhost always bypasses egress proxy (NO_PROXY + server-side check)
  • Egress decisions come from control plane API (user-authenticated), never from sandbox

Key files:

  • sandbox/internal/broker/ — Broker implementation
  • sandbox/cmd/server/env.go — Env setup with session-scoped broker config
  • controlplane/src/secrets/ — Secrets API + encryption
  • controlplane/src/auth/pty-token.ts — PTY token creation/verification (fail-closed)
  • frontend/src/components/blocks/TerminalBlock.tsx — Secrets UI
  • sandbox/internal/egress/ — Egress proxy + allowlist
  • controlplane/src/egress/handler.ts — Egress approval endpoints + audit logging
  • frontend/src/components/EgressApprovalDialog.tsx — Approval dialog UI
  • frontend/src/components/EgressAllowlistPanel.tsx — Allowlist management UI

Network Egress Proxy Feature Flag

The egress proxy is gated behind a single env var:

  • Global: Set EGRESS_PROXY_ENABLED=true on the Fly machine to enable for all sessions
  • Without this flag, the proxy runs but no PTY processes route through it (iptables rules not installed, UID pool not activated)

OAuth Integrations (Gmail/Drive/GitHub/Calendar/Twitter)

  • Control plane provides connect + callback endpoints:
    • /integrations/google/drive/connect + /callback
    • /integrations/google/gmail/connect + /callback
    • /integrations/google/calendar/connect + /callback
    • /integrations/github/connect + /callback
    • /integrations/twitter/connect + /callback
  • Required env vars on Cloudflare:
    • GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
    • GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
    • TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET
    • OAUTH_REDIRECT_BASE (optional; defaults to request origin)
  • D1 tables: oauth_states, user_integrations, terminal_integrations, integration_policies

Integration Policy Enforcement (Security-Critical)

Orcabot uses a component integration gate model: on the dashboard canvas, users draw edges from terminal blocks to integration blocks (Gmail, GitHub, Drive, Calendar, Twitter). Only attached integrations are visible as MCP tools to the LLM in that terminal. Each attachment has a policy that controls what actions are allowed and what data the LLM can see.

Security Invariants (non-negotiable)

  1. No tool without edge — MCP only exposes tools for integrations attached to the terminal
  2. No policy from request — Policy is loaded from active_policy_id in DB, never from the sandbox
  3. Boolean enforcement — enforcePolicy() uses only if/else logic, no LLM judgment
  4. OAuth tokens stay in control plane — Never sent to sandbox; API calls made server-side
  5. Fail-closed — Missing policy, expired token, or unknown action = deny
  6. Audit before response — Every request logged before response is returned
  7. Filtered responses only — LLM never sees raw API data; responses are filtered + formatted

Request Flow

LLM (in sandbox PTY)
  → calls gmail_search via MCP
  → mcp-bridge forwards to sandbox MCP server (HTTP)
  → sandbox calls control plane gateway: POST /internal/gateway/gmail/execute
    (sends PTY token for auth, action + args only — no OAuth token)
  → control plane:
    1. Verify PTY token (HMAC-SHA256 JWT → terminal_id, dashboard_id, user_id)
    2. Load terminal_integration + active_policy from DB
    3. Check rate limits
    4. enforcePolicy() — boolean logic
    5. Get OAuth access token (refresh if expired)
    6. Call Gmail/GitHub/Drive/Calendar/Twitter API
    7. Filter response based on policy (sender allowlist, repo filter, etc.)
    8. Format response for LLM (decode base64, strip HTML, extract headers)
    9. Log audit entry
    10. Return filtered response
  → sandbox wraps in MCP content format
  → mcp-bridge returns to LLM

Key Files

  • controlplane/src/integration-policies/gateway.ts — Gateway execute endpoint + PTY token verification
  • controlplane/src/integration-policies/handler.ts — Attach/detach integrations, policy CRUD
  • controlplane/src/integration-policies/response-filter.ts — Policy-based response filtering
  • controlplane/src/integration-policies/api-clients/ — Gmail, GitHub, Drive, Calendar, Twitter API wrappers
  • controlplane/src/auth/pty-token.ts — PTY token creation/verification
  • sandbox/cmd/mcp-bridge/main.go — stdio-to-HTTP bridge with tool change notifications
  • sandbox/cmd/server/mcp.go — MCP server with integration tool handling
  • sandbox/internal/mcp/integration_tools.go — Tool definitions for all providers
  • sandbox/internal/mcp/gateway_client.go — Client for control plane gateway calls

Agent Hooks

When an LLM agent (Claude, Gemini, etc.) finishes a turn, stop hooks notify the sandbox so it can broadcast agent_stopped WebSocket events to all connected clients.

  • Hook scripts: sandbox/internal/agenthooks/hooks.go
  • Supported agents: Claude Code, Gemini CLI, OpenCode, Codex, Droid, OpenClaw
  • Settings files generated per-agent (.claude/settings.json, .gemini/settings.json, etc.)
  • All hooks must include X-MCP-Secret header from ORCABOT_MCP_SECRET env var — unauthenticated hook callbacks are rejected

Key Subsystems

Sandbox Behavior

  • Each dashboard gets its own dedicated VM (one sandbox per dashboard)
  • Each terminal creates its own session in the control plane (one session per terminal item)
  • All terminal sessions in a dashboard share the same sandbox VM; each session maps to a PTY inside that VM
  • Sandbox sessions use a shared /workspace by default
  • PTY cwd is set to the session workspace
  • Multiple PTYs per sandbox with turn-taking

Desktop, the orcabot CLI & Surface Switching

  • The desktop Tauri app bundles the whole stack locally: control plane on workerd, a SQLite "D1" shim, and a sandbox VM (Apple Virtualization.framework on macOS / QEMU on Linux).
  • The same crate ships a 2nd binary, orcabot — a terminal CLI that boots the stack headlessly and drives it (interactive TUI + scriptable subcommands: up/down/status/exec/ls/tail/new/connect/attach, plus export/import/push/pull).
  • Owned-session lifecycle: whoever starts the stack tears it down on exit; there is no always-on daemon. up/down are the explicit persist-mode escape hatch.
  • Surface switching moves a live session between cli ↔ desktop ↔ web. The GUI's "Switch to CLI" button (Tauri switch_to_cli command) hands off to the terminal; the TUI can launch the desktop GUI. Desktop boot, the vsock bridges, /debug/exec, and the full CLI reference live in desktop/CLAUDE.md.

Session Packaging

  • A dashboard + its workspace can be packaged for transfer. Locally: orcabot export/import produce/consume an .orcabot bundle. Remotely: orcabot push/pull sync against a remote control plane, authenticated with a PAT.
  • File transfer goes through the control plane file proxy (GET/PUT /sessions/:id/file, POST /sessions/:id/workspace/import), never directly to a sandbox. Pull writes are path-guarded (safe_workspace_dest) so a malicious remote can't escape the workspace.

Browser Block

  • Browser block checks embeddability via control plane /embed-check
  • If embedding is blocked, UI collapses to a small "open in new tab" panel

TTS (Text-to-Speech)

  • Sandbox-side talkito handles TTS via brokered API calls
  • Frontend shows TTS status via WebSocket events
  • Supports OpenAI, ElevenLabs, Deepgram, Google providers

Subagents

  • Saved subagents persist per user in control plane
  • Catalog lives in frontend/src/data/claude-subagents.json

Model Selection (OpenRouter)

  • Per-terminal model picker in the terminal settings dropdown (Claude/Codex/OpenCode/Droid/Gemini)
  • Default = harness's native API; OpenRouter routes through the local secrets broker. Three paths, by harness:
    • OpenCode/Droid: OpenAI-compatible openrouter provider via OPENAI_BASE_URL
    • Codex: per-invocation -c CLI flags injected by buildCodexOpenRouterCommand (model_provider/base_url=broker/wire_api="responses") — the Rust Codex CLI ignores OPENAI_BASE_URL/OPENAI_MODEL
    • Claude Code: Anthropic-compatible openrouter-anthropic provider via ANTHROPIC_BASE_URL
    • Gemini: official CLI GATEWAY auth → local translation shim (sandbox/internal/geminishim/, port 8086) that converts the Gemini wire format to OpenAI Chat Completions → broker → OpenRouter
  • Selection lives in terminalContent.modelSelection (provider/model + catalog-resolved contextWindow/maxOutputTokens) and triggers the existing "restart to apply" banner. Limits flow frontend→controlplane→sandbox to set Codex's -c model_context_window/model_max_output_tokens.
  • Catalog: frontend/src/data/openrouter-models.json (curated, per-model pricing + context + maxOutputTokens + compatibleHarnesses)
  • Drift check: npm run check-catalogs validates pricing, context length, and max output tokens vs upstream (also runs weekly via .github/workflows/check-catalogs.yml)
  • Upstream provider errors (OpenRouter 429/402/401/5xx) are detected in the broker and surfaced as a dashboard toast via a model_provider_error WS event (see classifyProviderError in sandbox/internal/broker/secrets_broker.go)
  • Key files: sandbox/internal/sessions/model_selection.go, sandbox/internal/geminishim/, sandbox/internal/broker/providers.go, frontend/src/components/blocks/TerminalBlock.tsx (Model panel)

Workspace Sidebar

  • File tree is populated via control plane proxy of sandbox filesystem APIs

ASR (Automatic Speech Recognition)

  • Control plane stores ASR API keys encrypted in user_secrets (global scope)
  • Three providers: AssemblyAI (token vending), OpenAI Whisper (HTTP proxy), Deepgram (streaming WS + REST fallback)
  • ASRStreamProxy Durable Object relays WebSocket audio between browser and Deepgram (API key injected server-side)
  • Frontend useASR hook manages provider selection, streaming, and chunked transcription
  • Key files: controlplane/src/asr/, frontend/src/hooks/useASR.ts, frontend/src/components/blocks/ASRSettingsDialog.tsx

Analytics & Metrics

  • Client-side event tracking with batched ingestion (POST /analytics/events, max 50/batch)
  • Server-side event logging for agent type detection and session lifecycle
  • Admin metrics endpoint (GET /admin/metrics): DAU/WAU/MAU, signups, agent type breakdown, integration adoption, 7-day retention
  • D1 table: analytics_events + users.last_active_at column
  • Key files: controlplane/src/analytics/handler.ts, frontend/src/lib/analytics.ts

Messaging Integrations (Send/Receive)

  • Inbound webhook handling for: Slack, Discord, Telegram, WhatsApp, Teams, Matrix, Google Chat
  • Per-platform signature verification (HMAC-SHA256, Ed25519, token header)
  • Inbound policy enforcement: channel filter, sender filter, repliesOnly mode
  • Messages buffered in inbound_messages D1 table, broadcast via Durable Object, can trigger VM wake-up
  • Canvas edges from messaging blocks to terminals enable communication paths
  • Key files: controlplane/src/messaging/webhook-handler.ts

Messaging Bridge Service

  • Separate Node.js service in bridge/ deployed on Fly
  • Handles WhatsApp connectivity via Baileys (unofficial WhatsApp Web API)
  • Session management for persistent WhatsApp connections
  • Control plane communicates with bridge via controlplane/src/bridge/client.ts

Subscriptions & Billing

  • Stripe-based subscription management
  • Paywall checks for feature gating
  • Key files: controlplane/src/subscriptions/

Blog & Documentation

  • Blog posts served via controlplane/src/blog/handler.ts
  • Frontend blog at frontend/src/app/(blog)/ with markdown content in frontend/content/blog/
  • In-app documentation at frontend/src/app/(docs)/ with content in frontend/src/docs/content/
  • AI chat with help docs: controlplane/src/chat/handler.ts, controlplane/src/chat/help-docs.ts

Email

  • Transactional email via Resend: controlplane/src/email/resend.ts

E2E Testing

External Playwright test suite in e2e/ that runs against a live OrcaBot instance.

Running

cd e2e && npm install && npx playwright install chromium
ORCABOT_URL=https://your-instance.com npm test

Configuration

  • Sequential execution (1 worker) — tests share real instance state
  • 120s test timeout, 30s expect timeout (sandbox boot can be slow)
  • Traces on first retry, screenshots on failure

Fixtures

  • auth — API-based login (fast) and UI-based login
  • dashboard — Create/goto/delete with auto-cleanup
  • terminal — Add terminal, wait for prompt, type commands, wait for output
  • api — Direct control plane API client

Test Recipes

  1. 01-auth-flow — Login/logout flows
  2. 02-dashboard-crud — Dashboard create/delete
  3. 03-terminal-basic — Terminal creation
  4. 04-terminal-command — Command execution
  5. 05-integration-blocks — Integration block creation
  6. 06-integration-wiring — Edge wiring (terminals ↔ integrations)
  7. 07-policy-editor — Policy UI
  8. 08-gemini-chess — Full agent scenario (Gemini playing chess)