OpenHands (formerly OpenDevin) is an autonomous AI software development agent platform. Agents write code, execute terminal commands, browse the web, and interact with APIs inside sandboxed runtime environments. This guide covers architecture, deployment, configuration, security, and developer internals for administrators and contributors operating OpenHands at any scale.
1. Architecture Overview
1.1 System Architecture
┌──────────────────────────────────┐
│ Frontend (React) │
│ Vite 7 / React 19 / Zustand 5 │
│ React Router 7 / i18n │
└──────────────┬───────────────────┘
│ WebSocket / REST
┌──────────────▼───────────────────┐
│ Application Server │
│ ┌─────────┐ ┌────────────────┐ │
│ │ V1 App │ │ V0 Legacy API │ │
│ │ Server │ │ (FastAPI) │ │
│ └────┬────┘ └───────┬────────┘ │
│ │ │ │
│ ┌────▼───────────────▼────────┐ │
│ │ Agent Controller │ │
│ │ (execution loop, state) │ │
│ └────────────┬─────────────────┘ │
│ │ │
│ ┌────────────▼─────────────────┐ │
│ │ Event Stream │ │
│ │ (append-only, subscribers) │ │
│ └────────────┬─────────────────┘ │
└───────────────┼───────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
┌─────────▼──────────┐ ┌───────────▼──────────┐ ┌──────────▼─────────┐
│ LLM Layer │ │ Runtime Layer │ │ Storage Layer │
│ litellm (100+ │ │ ┌─────────────────┐ │ │ Local / S3 / GCS │
│ providers) │ │ │ Docker Runtime │ │ │ Memory / Webhook │
│ LLM / AsyncLLM / │ │ │ K8s Runtime │ │ └────────────────────┘
│ StreamingLLM │ │ │ Local Runtime │ │
│ LLMRegistry │ │ │ CLI Runtime │ │
└────────────────────┘ │ │ Remote Runtime │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Sandboxed │ │
│ │ Container │ │
│ │ (code exec, │ │
│ │ browser, tools) │ │
│ └─────────────────┘ │
└───────────────────────┘
1.2 Component Inventory
| Component | Path | Role |
|---|---|---|
| Core Config | openhands/core/config/ |
TOML config parsing (14 files, 106K chars) |
| Agent Controller | openhands/controller/ |
Agent execution loop, state management |
| Runtime | openhands/runtime/ |
Sandbox execution (Docker, K8s, Local, CLI, Remote) |
| LLM Layer | openhands/llm/ |
LLM integration via litellm (LLM, AsyncLLM, StreamingLLM) |
| Events | openhands/events/ |
Event system (Event, EventStream, EventFilter) |
| MCP | openhands/mcp/ |
Model Context Protocol client |
| Storage | openhands/storage/ |
File stores (Local, S3, Google Cloud, Memory, Webhook) |
| V0 Server (Legacy) | openhands/server/ |
Legacy FastAPI server (deprecated v1.0.0) |
| V1 App Server | openhands/app_server/ |
New application server |
| Frontend | frontend/ |
React/TypeScript UI (Vite, React Router, Zustand) |
| Enterprise | enterprise/ |
SaaS features (auth, billing, orgs) |
| UI Components | openhands-ui/ |
Shared React component library (@openhands/ui) |
| Containers | containers/ |
Docker build configs |
| Kind | kind/ |
Local Kubernetes development |
1.3 Technology Stack
| Layer | Technologies |
|---|---|
| Backend | Python 3.12+, FastAPI, uvicorn, Pydantic, SQLAlchemy (enterprise) |
| Frontend | React 19, TypeScript 5.9, Vite 7, React Router 7, Zustand 5, TailwindCSS 4, i18next |
| LLM | litellm (supports 100+ LLM providers) |
| Container | Docker, Kubernetes (KIND for local dev) |
| Storage | Local filesystem, S3, Google Cloud Storage |
| Enterprise | Keycloak (auth), Stripe (billing), PostgreSQL, Redis, Alembic (migrations) |
| Testing | pytest (backend), Vitest (frontend), Playwright (e2e) |
| Build | Poetry (Python), npm (frontend) |
1.4 V0 vs V1 Architecture
OpenHands is undergoing a major architectural migration from V0 to V1 (Software Agent SDK).
| Aspect | V0 (Legacy) | V1 (Current) |
|---|---|---|
| Server | openhands/server/ (FastAPI) |
openhands/app_server/ |
| Feature flag | Default | ENABLE_V1=true |
| Status | Deprecated as of v1.0.0 | Active development |
| Resolvers | Monolithic | ENABLE_V1_GITHUB_RESOLVER, ENABLE_V1_SLACK_RESOLVER, ENABLE_V1_GITLAB_RESOLVER |
Important
The V0 server in
openhands/server/is deprecated as of v1.0.0. New deployments should use the V1 app server withENABLE_V1=true. The V0 code path remains for backward compatibility but receives no new features.
2. System Requirements
2.1 Hardware Requirements
| Deployment | CPU | Memory | Storage | Notes |
|---|---|---|---|---|
| Development (single agent) | 2+ cores | 4 GB | 10 GB | Docker Desktop required |
| Small team (1-5 concurrent) | 4 cores | 16 GB | 50 GB | SSD recommended |
| Production (10+ concurrent) | 8+ cores | 32+ GB | 100+ GB | Kubernetes recommended |
| Enterprise | 16+ cores | 64+ GB | 500+ GB | Multi-node K8s cluster |
2.2 Software Dependencies
| Dependency | Version | Purpose |
|---|---|---|
| Python | 3.12+ | Backend runtime |
| Docker | 20.10+ | Container runtime for sandboxes |
| Node.js | 18+ | Frontend build |
| Poetry | Latest | Python dependency management |
| Git | 2.x | Version control, repo cloning |
2.3 Runtime Resource Limits
The runtime sandbox enforces configurable resource constraints:
- Memory: Controlled by
RUNTIME_MAX_MEMORY_GB(no default limit unless set) - Memory monitoring: Enable with
RUNTIME_MEMORY_MONITOR=true - CPU: Kubernetes deployments use
resource_cpu_request(default:1) - Storage: Kubernetes PVC default
2Gi, configurable viapvc_storage_size
Note
Each agent session spawns its own sandboxed container. Plan capacity for N concurrent sessions * per-session resource limits. The
SANDBOX_BASE_CONTAINER_IMAGEdefaults to the official OpenHands runtime image fromghcr.io/openhands/runtime.
3. Configuration Reference
3.1 Configuration System
OpenHands uses TOML-based configuration with 14 config files in openhands/core/config/. Configuration is loaded in this precedence order:
- Environment variables (highest priority)
config.tomlfile- Default values (lowest priority)
The config system uses Pydantic models for validation. Key config classes:
| Class | File | Purpose |
|---|---|---|
AppConfig |
app_config.py |
Top-level application config |
LLMConfig |
llm_config.py |
LLM provider settings |
AgentConfig |
agent_config.py |
Agent behavior settings |
SandboxConfig |
sandbox_config.py |
Sandbox/container settings |
SecurityConfig |
security_config.py |
Security policies |
MCPConfig |
mcp_config.py |
Model Context Protocol |
CondenserConfig |
condenser_config.py |
Context condensation strategies |
3.2 Core / Server Variables
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO |
Logging verbosity (DEBUG, INFO, WARNING, ERROR) |
DEBUG |
false |
Enable debug mode |
DEBUG_LLM |
false |
Log raw LLM request/response payloads |
LOG_JSON |
false |
Output structured JSON logs |
LOG_TO_FILE |
false |
Write logs to file |
LOG_ALL_EVENTS |
false |
Log every event in the event stream |
DEBUG_RUNTIME |
false |
Verbose runtime container logging |
SERVE_FRONTEND |
true |
Serve the React frontend from the backend |
port |
3000 |
Server listen port |
OH_PERSISTENCE_DIR |
~/.openhands |
Persistent data directory |
FILE_STORE_PATH |
— | Override file store root path |
JWT_SECRET |
— | Secret for JWT token signing |
ENABLE_V1 |
false |
Enable V1 app server |
REDIS_HOST |
— | Redis server hostname |
REDIS_PASSWORD |
— | Redis authentication password |
REDIS_PORT |
6379 |
Redis server port |
REDIS_DB |
0 |
Redis database number |
3.3 LLM Variables
| Variable | Default | Description |
|---|---|---|
OPENHANDS_FORCE_VISION |
— | Force vision/multimodal mode |
OPENHANDS_PROVIDER_BASE_URL |
— | Override LLM provider base URL |
LLM_BASE_URL |
— | Alternative LLM base URL override |
LLM retry configuration (set in config.toml under [llm]):
[llm]
model = "claude-sonnet-4-20250514"
api_key = "sk-..."
num_retries = 8
retry_min_wait = 15
retry_max_wait = 120
retry_multiplier = 2
Warning
Issue #14028 documents a known bug where Gemini model base URLs get doubled, causing 404 errors. If using Google Gemini models, verify your
base_urldoes not include the/v1suffix — litellm appends it automatically.
3.4 Runtime Variables
| Variable | Default | Description |
|---|---|---|
RUNTIME |
docker |
Runtime type: docker, kubernetes, local, cli, remote |
SESSION_API_KEY |
— | API key for runtime session auth |
RUNTIME_MAX_MEMORY_GB |
— | Max memory per sandbox container (GB) |
RUNTIME_ID |
— | Explicit runtime instance identifier |
NO_CHANGE_TIMEOUT_SECONDS |
10 |
Seconds to wait when no output changes |
INIT_PLUGIN_TIMEOUT |
120 |
Plugin initialization timeout (seconds) |
RUNTIME_MEMORY_MONITOR |
false |
Enable memory usage monitoring |
DISABLE_VSCODE_PLUGIN |
false |
Disable VS Code server in sandbox |
SANDBOX_HOST_PORT |
— | Host port binding for sandbox |
SANDBOX_CONTAINER_URL_PATTERN |
— | URL pattern for sandbox containers |
SANDBOX_STARTUP_GRACE_SECONDS |
— | Grace period for sandbox startup |
SANDBOX_VOLUMES |
— | Additional volume mounts (host:container format) |
SANDBOX_BASE_CONTAINER_IMAGE |
— | Base Docker image for sandboxes |
SANDBOX_GROUP_ID |
root |
Group ID inside sandbox |
OH_RUNTIME_RUNTIME_IMAGE_REPO |
ghcr.io/openhands/runtime |
Runtime image repository |
RUNTIME_USERNAME |
— | Username inside sandbox |
SU_TO_USER |
true |
Switch to non-root user in sandbox |
RUNTIME_UID |
— | UID for sandbox user |
3.5 Storage Variables
| Variable | Default | Description |
|---|---|---|
FILE_STORE |
local |
Storage backend: local, s3, google_cloud, memory, webhook |
AWS_ACCESS_KEY_ID |
— | AWS access key for S3 storage |
AWS_SECRET_ACCESS_KEY |
— | AWS secret key for S3 storage |
AWS_S3_ENDPOINT |
— | Custom S3 endpoint (for MinIO, etc.) |
AWS_S3_SECURE |
true |
Use HTTPS for S3 connections |
SHARED_EVENT_STORAGE_PROVIDER |
— | Shared event storage backend |
DOCKER_CONTAINER |
— | Docker container name for storage |
3.6 Agent & Skill Variables
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | OpenAI API key for agent skills |
SANDBOX_ENV_OPENAI_API_KEY |
— | OpenAI key injected into sandbox |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
OpenAI API base URL |
OPENAI_MODEL |
gpt-4o |
OpenAI model for skills |
MAX_TOKEN |
500 |
Max tokens for skill responses |
TAVILY_API_KEY |
— | Tavily search API key |
SEARCH_API_KEY |
— | Alternative search API key |
3.7 Secret Management Variables
| Variable | Default | Description |
|---|---|---|
OH_MAX_API_SECRETS_COUNT |
50 |
Max number of stored API secrets |
OH_MAX_API_SECRET_NAME_LENGTH |
256 |
Max secret name length (chars) |
OH_MAX_API_SECRET_VALUE_LENGTH |
65536 |
Max secret value length (chars) |
ALLOW_INSECURE_GIT_ACCESS |
false |
Allow HTTP (non-HTTPS) Git operations |
3.8 Enterprise Variables
| Variable | Default | Description |
|---|---|---|
OPENHANDS_CONFIG_CLS |
— | Custom config class for enterprise |
POSTHOG_CLIENT_KEY |
— | PostHog analytics key |
GITHUB_APP_CLIENT_ID |
— | GitHub App OAuth client ID |
GITHUB_APP_CLIENT_SECRET |
— | GitHub App OAuth client secret |
GITHUB_APP_PRIVATE_KEY |
— | GitHub App private key (PEM) |
GITHUB_APP_WEBHOOK_SECRET |
— | GitHub webhook HMAC secret |
AUTH_URL |
— | Authentication service URL |
ENABLE_BILLING |
false |
Enable Stripe billing |
HIDE_LLM_SETTINGS |
false |
Hide LLM config from UI |
LITELLM_DEFAULT_MODEL |
— | Default model for LiteLLM proxy |
LITE_LLM_API_URL |
— | LiteLLM proxy URL |
LITE_LLM_TEAM_ID |
— | LiteLLM team identifier |
LITE_LLM_API_KEY |
— | LiteLLM API key |
STRIPE_API_KEY |
— | Stripe secret key |
REQUIRE_PAYMENT |
0 |
Require payment before usage |
KEYCLOAK_SERVER_URL |
— | Keycloak server URL |
KEYCLOAK_CLIENT_ID |
— | Keycloak OAuth client ID |
KEYCLOAK_CLIENT_SECRET |
— | Keycloak OAuth client secret |
KEYCLOAK_ADMIN_PASSWORD |
— | Keycloak admin password |
KEYCLOAK_REALM_NAME |
— | Keycloak realm |
GITLAB_APP_* |
— | GitLab App OAuth credentials |
BITBUCKET_APP_* |
— | Bitbucket Cloud App credentials |
BITBUCKET_DATA_CENTER_* |
— | Bitbucket Data Center credentials |
ENABLE_JIRA |
false |
Enable Jira integration |
ENABLE_LINEAR |
false |
Enable Linear integration |
ENABLE_ENTERPRISE_SSO |
false |
Enable enterprise SSO |
SLACK_CLIENT_ID |
— | Slack app client ID |
SLACK_CLIENT_SECRET |
— | Slack app client secret |
SLACK_SIGNING_SECRET |
— | Slack request signing secret |
SLACK_WEBHOOKS_ENABLED |
false |
Enable Slack webhook processing |
RESEND_API_KEY |
— | Resend email API key |
RESEND_FROM_EMAIL |
— | Sender email address |
RESEND_AUDIENCE_ID |
— | Resend audience ID |
RECAPTCHA_PROJECT_ID |
— | Google reCAPTCHA project |
RECAPTCHA_SITE_KEY |
— | reCAPTCHA site key |
RECAPTCHA_HMAC_SECRET |
— | reCAPTCHA HMAC secret |
RECAPTCHA_BLOCK_THRESHOLD |
0.3 |
reCAPTCHA score threshold |
AUTOMATION_SERVICE_URL |
— | External automation service URL |
AUTOMATION_WEBHOOK_SECRET |
— | Automation webhook HMAC secret |
AUTOMATION_EVENT_FORWARDING_ENABLED |
false |
Forward events to automation service |
AUTOMATION_SERVICE_TIMEOUT |
30 |
Automation service timeout (seconds) |
ENABLE_PROACTIVE_CONVERSATION_STARTERS |
false |
Enable AI-initiated conversations |
ENABLE_SOLVABILITY_ANALYSIS |
false |
Enable issue solvability scoring |
ENABLE_V1_GITHUB_RESOLVER |
false |
V1 GitHub issue resolver |
ENABLE_V1_SLACK_RESOLVER |
false |
V1 Slack resolver |
ENABLE_V1_GITLAB_RESOLVER |
false |
V1 GitLab resolver |
ENABLE_MCP_SEARCH_ENGINE |
false |
Enable MCP-based tool search |
AUTOMATIONS_SERVICE_KEY |
— | Automations service API key |
FRONTEND_DIRECTORY |
./frontend/build |
Path to frontend build output |
3.9 Enterprise Database Variables
| Variable | Default | Description |
|---|---|---|
DB_USER |
postgres |
PostgreSQL username |
DB_PASS |
— | PostgreSQL password |
DB_HOST |
localhost |
PostgreSQL host |
DB_PORT |
5432 |
PostgreSQL port |
DB_NAME |
openhands |
PostgreSQL database name |
GCP_DB_INSTANCE |
— | GCP Cloud SQL instance |
GCP_PROJECT |
— | GCP project ID |
GCP_REGION |
— | GCP region |
DB_POOL_SIZE |
25 |
Connection pool size |
DB_MAX_OVERFLOW |
10 |
Max overflow connections |
3.10 MCP Variables
| Variable | Default | Description |
|---|---|---|
OPENHANDS_MCP_CONFIG_CLS |
openhands.core.config.mcp_config.OpenHandsMCPConfig |
MCP config class |
3.11 Sample config.toml
[core]
workspace_base = "./workspace"
run_as_openhands = true
max_iterations = 100
max_budget_per_task = 4.0
[llm]
model = "claude-sonnet-4-20250514"
api_key = "sk-ant-..."
temperature = 0.0
num_retries = 8
retry_min_wait = 15
retry_max_wait = 120
[agent]
codeact_enable_browsing = true
codeact_enable_jupyter = true
codeact_enable_llm_editor = false
[sandbox]
base_container_image = "ghcr.io/openhands/runtime:latest"
timeout = 120
enable_auto_lint = true
[security]
confirmation_mode = false
security_analyzer = ""
[mcp]
# MCP servers configuration
4. Deployment & Scaling
4.1 Docker Deployment (Single Node)
The simplest production deployment uses Docker directly:
# Pull the latest image
docker pull ghcr.io/openhands/openhands:latest
# Run with Docker socket mounted (required for sandbox containers)
docker run -d \
--name openhands \
-p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/home/openhands/.openhands \
-e JWT_SECRET="$(openssl rand -hex 32)" \
-e LOG_LEVEL=INFO \
ghcr.io/openhands/openhands:latest
Caution
Mounting the Docker socket (
/var/run/docker.sock) grants the container full Docker access on the host. In production, consider using Docker-in-Docker or a remote Docker daemon with TLS authentication. Never expose the Docker socket to untrusted workloads.
4.2 Docker Compose Deployment
version: "3.8"
services:
openhands:
image: ghcr.io/openhands/openhands:latest
ports:
- "3000:3000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- openhands-data:/home/openhands/.openhands
environment:
- JWT_SECRET=${JWT_SECRET}
- LOG_LEVEL=INFO
- SERVE_FRONTEND=true
restart: unless-stopped
volumes:
openhands-data:
4.3 Kubernetes Deployment
OpenHands supports native Kubernetes runtime for sandbox containers. Set RUNTIME=kubernetes and configure the Kubernetes section:
[kubernetes]
namespace = "openhands"
ingress_domain = "openhands.example.com"
pvc_storage_size = "2Gi"
pvc_storage_class = "standard"
resource_cpu_request = "1"
resource_memory_request = "1Gi"
resource_memory_limit = "2Gi"
image_pull_secret = "ghcr-pull-secret"
ingress_tls_secret = "openhands-tls"
node_selector_key = "workload-type"
node_selector_val = "compute"
tolerations_yaml = ""
privileged = false
Each agent session creates a Kubernetes Pod with:
- A PersistentVolumeClaim for workspace data
- Resource requests and limits as configured
- Optional node selectors and tolerations for scheduling
- Optional ingress with TLS termination
4.4 KIND (Local Kubernetes Development)
The kind/ directory provides a complete local Kubernetes development environment:
# Create KIND cluster
kind create cluster --config kind/kind-config.yaml
# Apply manifests
kubectl apply -f kind/manifests/
# Set runtime to kubernetes
export RUNTIME=kubernetes
4.5 Runtime Types
_DEFAULT_RUNTIME_CLASSES = {
'eventstream': DockerRuntime,
'docker': DockerRuntime,
'remote': RemoteRuntime,
'local': LocalRuntime,
'kubernetes': KubernetesRuntime,
'cli': CLIRuntime,
}
| Runtime | Use Case | Docker Required | Network Isolation |
|---|---|---|---|
docker |
Default, production single-node | Yes | Container network |
kubernetes |
Production multi-node | No (K8s manages) | Pod network policies |
local |
Development/testing | No | None (host process) |
cli |
Headless/scripted usage | Depends on config | Varies |
remote |
Distributed deployments | No (remote host) | Remote network |
Third-party runtimes are auto-discovered from third_party.runtime.impl via pkgutil, enabling custom runtime implementations without modifying core code.
Tip
For production deployments serving multiple users, Kubernetes runtime is strongly recommended. It provides proper resource isolation, automatic cleanup of terminated sessions, and horizontal scaling via standard Kubernetes primitives (HPA, node autoscaling).
4.6 Enterprise Deployment
Enterprise deployments add authentication, billing, and organization management:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Keycloak │ │ PostgreSQL │ │ Redis │
│ (Auth) │ │ (Data) │ │ (Cache) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌────────▼────────┐
│ OpenHands │
│ Enterprise │
│ App Server │
└────────┬────────┘
│
┌────────▼────────┐
│ Stripe │
│ (Billing) │
└─────────────────┘
Required services for enterprise:
- Keycloak: Authentication and SSO (
KEYCLOAK_*variables) - PostgreSQL: User data, organizations, API keys (Alembic migrations)
- Redis: Session cache, rate limiting (
REDIS_*variables) - Stripe: Subscription and credit billing (
STRIPE_API_KEY,ENABLE_BILLING=true)
4.7 Container Build System
The containers/ directory holds Docker build configurations for:
- Runtime sandbox images
- Application server images
- Development images
Custom sandbox images extend the base runtime:
FROM ghcr.io/openhands/runtime:latest
RUN apt-get update && apt-get install -y your-custom-tools
5. Operations
5.1 Health Monitoring
The health endpoint provides system status:
curl http://localhost:3000/api/v1/health
For enterprise deployments, a readiness probe is also available:
curl http://localhost:3000/api/v1/ready
5.2 Log Management
Configure logging granularity with these variables:
# Standard logging
export LOG_LEVEL=INFO
# Debug everything
export DEBUG=true
export DEBUG_LLM=true
export DEBUG_RUNTIME=true
export LOG_ALL_EVENTS=true
# Production structured logging
export LOG_JSON=true
export LOG_TO_FILE=true
Key log categories:
- LLM interactions: Enable
DEBUG_LLMto see raw request/response payloads. Useful for diagnosing prompt issues or token budget overruns. - Runtime operations: Enable
DEBUG_RUNTIMEfor sandbox container lifecycle events. - Event stream: Enable
LOG_ALL_EVENTSto trace every action and observation through the system.
Warning
DEBUG_LLM=truelogs full LLM payloads including any secrets or sensitive content passed in prompts. Never enable in production environments with user data. Rotate any API keys that appear in debug logs.
5.3 Data Persistence
All persistent data lives under OH_PERSISTENCE_DIR (default: ~/.openhands):
~/.openhands/
├── sessions/ # Active/historical session data
├── file_store/ # Default local file store
└── config.toml # User configuration
For S3-backed persistence:
export FILE_STORE=s3
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_S3_ENDPOINT=https://s3.amazonaws.com
For Google Cloud Storage:
export FILE_STORE=google_cloud
# Authenticate via GOOGLE_APPLICATION_CREDENTIALS or workload identity
5.4 Session Management
Each agent interaction creates a session with:
- A unique session ID
- An event stream (append-only log of all actions and observations)
- A sandboxed runtime container
- LLM metrics (cost, token counts)
Sessions persist to the configured file store. The event stream supports:
- Subscriber-based real-time notifications
- Filtered queries by type, source, date range, or content
- Batch retrieval for bulk operations
5.5 Backup and Recovery
Local file store: Back up OH_PERSISTENCE_DIR with standard filesystem tools.
S3 file store: Enable S3 versioning on the bucket for point-in-time recovery.
Enterprise PostgreSQL: Standard pg_dump / pg_restore procedures. Alembic migrations track schema version for safe upgrades.
# PostgreSQL backup
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME > backup_$(date +%Y%m%d).sql
# Restore
psql -h $DB_HOST -U $DB_USER -d $DB_NAME < backup_20260506.sql
5.6 Upgrade Procedures
- Pull the new image:
docker pull ghcr.io/openhands/openhands:latest - Stop the running container gracefully (allow active sessions to complete)
- Run database migrations (enterprise):
alembic upgrade head - Start the new container
- Verify health endpoint returns OK
6. Performance
6.1 LLM Performance Tuning
The LLM layer is typically the performance bottleneck. Key tuning parameters:
| Parameter | Impact | Recommendation |
|---|---|---|
num_retries |
Resilience to transient failures | 5-10 for production |
retry_min_wait |
Minimum backoff between retries | 15s for rate-limited APIs |
retry_max_wait |
Maximum backoff ceiling | 120s |
retry_multiplier |
Exponential backoff factor | 2 |
temperature |
Response determinism | 0.0 for code generation |
max_iterations |
Max agent steps per task | Balance thoroughness vs cost |
max_budget_per_task |
Dollar cap per task | Set based on model pricing |
6.2 Context Window Management
OpenHands implements multiple context condensation strategies to manage long conversations:
| Condenser | Behavior |
|---|---|
NoOpCondenser |
No condensation (full history) |
ObservationMaskingCondenser |
Masks large observation payloads |
RecentEventsCondenser |
Keeps only N most recent events |
LLMSummarizingCondenser |
Uses an LLM to summarize older context |
AmortizedForgettingCondenser |
Gradually forgets older events |
BrowserOutputCondenser |
Condenses browser interaction output |
Configure in config.toml:
[agent.condenser]
type = "llm_summarizing"
# or
type = "recent_events"
keep_last = 50
6.3 Runtime Performance
- Container startup:
SANDBOX_STARTUP_GRACE_SECONDScontrols how long to wait for sandbox readiness. Increase for slow Docker hosts. - Plugin initialization:
INIT_PLUGIN_TIMEOUT(default 120s) governs plugin setup. Increase for environments with slow network access. - Memory: Set
RUNTIME_MAX_MEMORY_GBto prevent runaway containers. EnableRUNTIME_MEMORY_MONITORto track usage.
6.4 Model Routing (Experimental)
OpenHands supports experimental model routing for optimizing which model handles different request types:
| Router | Behavior |
|---|---|
noop_router |
All requests go to the configured model (default) |
multimodal_router |
Routes multimodal requests to vision-capable models |
6.5 Database Connection Pooling (Enterprise)
For high-concurrency enterprise deployments, tune the PostgreSQL connection pool:
export DB_POOL_SIZE=25 # Base pool size
export DB_MAX_OVERFLOW=10 # Additional connections under load
Total max connections = DB_POOL_SIZE + DB_MAX_OVERFLOW = 35 per application instance.
7. Security
7.1 Sandbox Isolation
Every agent session runs inside an isolated sandbox container. The security model:
┌─────────────────────────────────────────┐
│ Host System │
│ │
│ ┌───────────────────────────────────┐ │
│ │ OpenHands App Server │ │
│ │ (no direct code execution) │ │
│ └───────────────┬───────────────────┘ │
│ │ Docker API / K8s API │
│ ┌───────────────▼───────────────────┐ │
│ │ Sandbox Container │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Agent code execution │ │ │
│ │ │ Terminal commands │ │ │
│ │ │ Web browsing │ │ │
│ │ │ File operations │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ Resource limits enforced │ │
│ │ No host filesystem access │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
Key isolation controls:
- User separation:
SU_TO_USER=true(default) runs agent code as non-root inside the sandbox - Network: Container networking provides L3 isolation; Kubernetes adds network policies
- Filesystem: Sandboxes get ephemeral filesystems;
SANDBOX_VOLUMEScontrols explicit mounts - Resources: Memory and CPU limits enforced by container runtime
- Privileged mode:
privileged = falseby default in Kubernetes config
7.2 Authentication
| Mode | Mechanism | Variables |
|---|---|---|
| Local/Dev | JWT tokens | JWT_SECRET |
| Enterprise SSO | Keycloak OAuth | KEYCLOAK_*, ENABLE_ENTERPRISE_SSO |
| GitHub App | OAuth2 | GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET |
| GitLab App | OAuth2 | GITLAB_APP_* |
| Bitbucket | OAuth2 | BITBUCKET_APP_*, BITBUCKET_DATA_CENTER_* |
| API Keys | Header-based | Managed via enterprise API key endpoints |
| OAuth Device Flow | Device authorization | /authorize, /token, /verify-authenticated |
7.3 Secret Management
OpenHands enforces limits on stored secrets:
- Maximum 50 secrets per account (
OH_MAX_API_SECRETS_COUNT) - Secret names up to 256 characters (
OH_MAX_API_SECRET_NAME_LENGTH) - Secret values up to 64KB (
OH_MAX_API_SECRET_VALUE_LENGTH)
Secrets injected into the sandbox environment (e.g., SANDBOX_ENV_OPENAI_API_KEY) are scoped to that session's container and destroyed when the session ends.
7.4 Git Security
# Default: HTTPS-only git operations
export ALLOW_INSECURE_GIT_ACCESS=false
Caution
Setting
ALLOW_INSECURE_GIT_ACCESS=truepermits HTTP (unencrypted) Git operations. This exposes credentials and source code in transit. Only enable for air-gapped environments with no TLS capability.
7.5 Webhook Security
All webhook integrations use HMAC signature verification:
| Integration | Secret Variable |
|---|---|
| GitHub | GITHUB_APP_WEBHOOK_SECRET |
| Automation | AUTOMATION_WEBHOOK_SECRET |
| Slack | SLACK_SIGNING_SECRET |
7.6 reCAPTCHA (Enterprise)
Enterprise deployments can enable Google reCAPTCHA to prevent automated abuse:
export RECAPTCHA_PROJECT_ID="project-id"
export RECAPTCHA_SITE_KEY="6Le..."
export RECAPTCHA_HMAC_SECRET="..."
export RECAPTCHA_BLOCK_THRESHOLD=0.3 # Block scores below this threshold
7.7 Security Analyzer
The SecurityConfig supports pluggable security analyzers that evaluate agent actions before execution:
[security]
confirmation_mode = true # Require confirmation for sensitive ops
security_analyzer = "invariant" # Pluggable analyzer name
When confirmation_mode is enabled, potentially dangerous operations (file deletion, network access, system commands) require explicit user approval.
8. Developer Internals
8.1 Event System Deep Dive
The event system is the backbone of OpenHands. Every agent interaction produces events flowing through the EventStream:
User Input → UserAction (Event)
→ AgentController processes
→ Agent produces AgentAction (Event)
→ Runtime executes
→ Observation (Event) returned
→ Agent processes observation
→ ... (loop until AgentFinishAction or max_iterations)
Event hierarchy:
Event (base)
├── Action
│ ├── CmdRunAction # Shell command execution
│ ├── FileEditAction # Edit file content
│ ├── FileReadAction # Read file content
│ ├── FileWriteAction # Write entire file
│ ├── IPythonRunCellAction # Execute Jupyter cell
│ ├── BrowseURLAction # Navigate to URL
│ ├── BrowseInteractiveAction # Interactive browser
│ ├── AgentFinishAction # Agent declares task complete
│ ├── AgentDelegateAction # Delegate to sub-agent
│ └── MessageAction # Send message to user
│
└── Observation
├── CmdOutputObservation # Command output
├── FileEditObservation # Edit result
├── FileReadObservation # File content
├── FileWriteObservation # Write confirmation
├── IPythonRunCellObservation # Jupyter output
├── ErrorObservation # Error details
├── BrowserOutputObservation # Browser page content
└── FileDownloadObservation # Downloaded file info
Event properties:
id: Unique event identifiertimestamp: ISO 8601 creation timesource:EventSource.AGENT,EventSource.USER, orEventSource.ENVIRONMENTcause: ID of the event that triggered this onetimeout: Optional timeout for the actionllm_metrics: Token usage and cost tracking
EventFilter supports filtering by:
- Event type (action vs observation, specific subtypes)
- Source (agent, user, environment)
- Date range
- Content query (text search within event data)
8.2 Agent Architecture
Agents follow a registry pattern:
class Agent(ABC):
"""Abstract base class for all agents."""
_registry: dict[str, type[Agent]] = {}
@classmethod
def register(cls, name: str, agent_cls: type[Agent]):
cls._registry[name] = agent_cls
@classmethod
def get_cls(cls, name: str) -> type[Agent]:
return cls._registry[name]
The default agent is CodeActAgent, which uses a ReAct-style loop with tool calling. Agent capabilities:
| Tool | Purpose | Config Flag |
|---|---|---|
cmd |
Execute shell commands | Always enabled |
jupyter |
Run Python in Jupyter | codeact_enable_jupyter |
browsing |
Web browsing | codeact_enable_browsing |
editor |
File editing (str_replace or LLM) | codeact_enable_llm_editor |
think |
Chain-of-thought reasoning | Always enabled |
finish |
Declare task complete | Always enabled |
mcp |
Model Context Protocol tools | MCP config |
task_tracker |
Track subtask progress | Agent config |
Prompt management: Templates use Jinja2 (system_prompt.j2, system_prompt_long_horizon.j2). Microagents extend base behavior and can be disabled via disabled_microagents in agent config.
8.3 AgentController Execution Loop
The AgentController orchestrates the agent lifecycle:
1. Initialize session (create runtime, load config)
2. Set up event stream with subscribers
3. Enter main loop:
a. Get agent's next action (LLM call)
b. Validate action against security policy
c. Execute action in runtime
d. Collect observation
e. Append both to event stream
f. Check termination conditions:
- AgentFinishAction received
- max_iterations exceeded
- max_budget_per_task exceeded
- Error threshold reached
4. Cleanup (destroy runtime, persist session)
8.4 LLM Layer Internals
Three LLM client classes handle different execution models:
| Class | Use Case |
|---|---|
LLM |
Synchronous, blocking calls |
AsyncLLM |
Async/await for concurrent operations |
StreamingLLM |
Server-sent events for real-time output |
LLMRegistry manages multiple named LLM instances, allowing different models for different purposes (e.g., a cheap model for summarization, a capable model for code generation).
Function call conversion (fn_call_converter.py): Converts between different tool-calling formats (OpenAI function calling, Anthropic tool use, raw XML) to ensure compatibility across LLM providers.
Model features detection (model_features.py): Auto-detects model capabilities (vision, function calling, streaming) to adapt behavior.
Metrics tracking: The Metrics class accumulates:
- Total tokens (input + output)
- Cost in dollars
- Number of API calls
- Cache hit/miss rates
Note
Issue #13843 documents that LLM cost tracking stops working after a session resume. Costs accumulated before the resume are preserved, but the counter resets for the resumed portion. This is a known limitation tracked for future fix.
8.5 MCP Integration
Model Context Protocol enables agents to use external tools via standardized interfaces:
[mcp]
# Stdio-based MCP server
[[mcp.stdio_servers]]
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
# Remote MCP server (HTTP/SSE)
[[mcp.remote_servers]]
name = "custom-tools"
url = "https://mcp.example.com/sse"
The MCP client:
- Connects via Stdio (subprocess) or HTTP/SSE (remote) transport
- Wraps discovered tools as
MCPClientToolobjects for agent consumption - Handles connection lifecycle (connect, disconnect, reconnect)
- Collects errors via
MCPErrorCollectorfor diagnostic reporting - Supports dynamic server updates via
POST /update_mcp_serverendpoint
8.6 Storage Layer
All storage backends implement the FileStore interface:
class FileStore(ABC):
@abstractmethod
def write(self, path: str, contents: str | bytes) -> None: ...
@abstractmethod
def read(self, path: str) -> str: ...
@abstractmethod
def list(self, path: str) -> list[str]: ...
@abstractmethod
def delete(self, path: str) -> None: ...
@abstractmethod
def exists(self, path: str) -> bool: ...
| Backend | Class | Use Case |
|---|---|---|
| Local filesystem | LocalFileStore |
Default, single-node |
| AWS S3 | S3FileStore |
Distributed, durable |
| Google Cloud | GoogleCloudFileStore |
GCP deployments |
| In-memory | InMemoryFileStore |
Testing only |
| Webhook | WebHookFileStore |
Event-driven storage |
| Batched Webhook | BatchedWebHookFileStore |
Buffered webhook writes |
8.7 Code Intelligence
The src/code_intelligence.py module provides AST-based extraction:
- Function/method definitions
- Class hierarchies
- Import graphs
- CLI command registration patterns
The src/repo_analyzer.py handles repository cloning and intelligent triage, scoring folders as HIGH/LOW/SKIP to focus analysis on the most important code paths.
8.8 Project Structure
openhands/
├── core/
│ ├── config/ # 14 config files, Pydantic models
│ ├── logger/ # Structured logging
│ └── exceptions.py # Error hierarchy
├── controller/ # AgentController, state machine
├── events/ # Event, EventStream, EventFilter
├── llm/ # LLM, AsyncLLM, StreamingLLM, LLMRegistry
├── runtime/ # Docker, K8s, Local, CLI, Remote runtimes
├── storage/ # FileStore implementations
├── mcp/ # Model Context Protocol client
├── agent/ # Agent base class, CodeActAgent
├── server/ # V0 legacy FastAPI server
├── app_server/ # V1 application server
└── utils/ # Shared utilities
frontend/ # React/TypeScript UI
enterprise/ # SaaS features
openhands-ui/ # Shared component library
containers/ # Docker build configs
kind/ # Kubernetes dev environment
tests/ # Test suites
8.9 Testing Strategy
| Layer | Framework | Location | Run Command |
|---|---|---|---|
| Backend unit | pytest | tests/unit/ |
poetry run pytest tests/unit/ |
| Backend integration | pytest | tests/integration/ |
poetry run pytest tests/integration/ |
| Frontend unit | Vitest | frontend/src/**/*.test.ts |
cd frontend && npm test |
| E2E | Playwright | tests/e2e/ |
npx playwright test |
Key testing patterns:
InMemoryFileStorefor storage tests (no filesystem side effects)- Mock LLM responses for agent behavior tests
- Docker-based integration tests for runtime validation
- Playwright for full browser-to-sandbox E2E flows
8.10 Build System
Backend (Poetry):
poetry install # Install dependencies
poetry run python -m openhands.core.main # Run server
poetry run pytest # Run tests
Frontend (npm):
cd frontend
npm install # Install dependencies
npm run dev # Development server (Vite HMR)
npm run build # Production build
npm test # Run Vitest suite
Docker images:
# Build runtime sandbox image
docker build -f containers/runtime/Dockerfile -t openhands-runtime .
# Build app server image
docker build -f containers/app/Dockerfile -t openhands-app .
9. Integration Points
9.1 API Endpoint Reference
Core App Server
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /search |
Session | Search events in event stream |
| GET | /count |
Session | Count events matching filter |
| GET | (batch) | Session | Batch retrieve events |
| GET | /settings |
Session | Load user settings |
| POST | /settings |
Session | Store user settings |
| GET | /agent-schema |
Session | Load agent settings JSON schema |
| GET | /conversation-schema |
Session | Load conversation settings schema |
Runtime Action Execution Server
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /server_info |
API Key | Runtime server metadata |
| POST | /execute_action |
API Key | Execute an action in the sandbox |
| POST | /update_mcp_server |
API Key | Update MCP server configuration |
| POST | /upload_file |
API Key | Upload file to sandbox workspace |
| GET | /download_files |
API Key | Download files from sandbox |
| GET | /alive |
None | Liveness probe |
| GET | /vscode/connection_token |
API Key | Get VS Code server connection token |
| POST | /list_files |
API Key | List files in sandbox workspace |
| GET | / |
None | File viewer root |
| GET | /view |
None | File viewer interface |
Enterprise: Authentication
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /keycloak/callback |
None | Keycloak OAuth callback |
| POST | /authenticate |
None | Authenticate user |
| POST | /accept_tos |
Session | Accept terms of service |
| POST | /complete_onboarding |
Session | Complete user onboarding |
| POST | /logout |
Session | End session |
| GET | /refresh-tokens |
Session | Refresh auth tokens |
Enterprise: Billing
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /credits |
Session | Get credit balance |
| GET | /subscription-access |
Session | Check subscription status |
| POST | /create-checkout-session |
Session | Create Stripe checkout |
| GET | /success |
Session | Payment success callback |
| GET | /cancel |
Session | Payment cancel callback |
Enterprise: API Keys
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | (create) | Session | Create new API key |
| GET | (list) | Session | List API keys |
| DELETE | /{key_id} |
Session | Delete API key |
| GET | /current |
API Key | Get current key info |
| GET | /llm/byor |
Session | Get BYOR LLM config |
| POST | /llm/byor/refresh |
Session | Refresh BYOR LLM config |
Enterprise: Organizations
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | (list) | Session | List user's organizations |
| POST | (create) | Session | Create organization |
| GET | /{org_id} |
Session | Get organization details |
| DELETE | /{org_id} |
Session | Delete organization |
| PATCH | /{org_id} |
Session | Update organization |
| GET | /{org_id}/members |
Session | List org members |
| POST | /{org_id}/switch |
Session | Switch active organization |
Enterprise: Email
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | (update) | Session | Update email address |
| PUT | /resend |
Session | Resend verification email |
| GET | /verified |
Session | Check verification status |
Enterprise: Feedback
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /conversation |
Session | Submit conversation feedback |
| GET | /conversation/{id}/batch |
Session | Get batch feedback |
Enterprise: OAuth Device Flow
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /authorize |
None | Start device authorization |
| POST | /token |
None | Poll for token |
| POST | /verify-authenticated |
Session | Verify device auth |
Enterprise: Invitations
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /invite |
Session | Send organization invitation |
| GET | /accept |
Session | View invitation |
| POST | /accept |
Session | Accept invitation |
Enterprise: Service
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
None | Health check |
| GET | /ready |
None | Readiness probe |
9.2 LLM Provider Integration
OpenHands supports 100+ LLM providers through litellm. Configuration per provider:
# OpenAI
[llm]
model = "gpt-4o"
api_key = "sk-..."
# Anthropic
[llm]
model = "claude-sonnet-4-20250514"
api_key = "sk-ant-..."
# AWS Bedrock
[llm]
model = "bedrock/anthropic.claude-3-sonnet"
aws_access_key_id = "AKIA..."
aws_secret_access_key = "..."
aws_region_name = "us-east-1"
# Azure OpenAI
[llm]
model = "azure/gpt-4o"
api_key = "..."
base_url = "https://your-resource.openai.azure.com/"
api_version = "2024-02-15-preview"
# Local (Ollama)
[llm]
model = "ollama/llama3"
base_url = "http://localhost:11434"
api_key = "dummy"
9.3 Git Provider Integration (Enterprise)
| Provider | Config Variables | Features |
|---|---|---|
| GitHub | GITHUB_APP_* |
Issue resolution, PR creation, webhooks |
| GitLab | GITLAB_APP_* |
Issue resolution, MR creation |
| Bitbucket Cloud | BITBUCKET_APP_* |
Repository integration |
| Bitbucket Data Center | BITBUCKET_DATA_CENTER_* |
On-premise Bitbucket |
Important
Issue #14096 documents that the Integrations page renders blank when
GITHUB_APP_SLUGis not configured. Ensure all required GitHub App variables are set if enabling the GitHub integration.
9.4 External Service Integration
| Service | Variables | Purpose |
|---|---|---|
| Slack | SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET |
Chat-based agent interaction |
| Jira | ENABLE_JIRA |
Issue tracking integration |
| Linear | ENABLE_LINEAR |
Issue tracking integration |
| PostHog | POSTHOG_CLIENT_KEY |
Product analytics |
| Resend | RESEND_API_KEY, RESEND_FROM_EMAIL |
Transactional email |
| Automation Service | AUTOMATION_SERVICE_URL, AUTOMATION_WEBHOOK_SECRET |
External workflow automation |
9.5 Automation Event Forwarding
When AUTOMATION_EVENT_FORWARDING_ENABLED=true, OpenHands forwards agent events to an external automation service:
export AUTOMATION_SERVICE_URL="https://automation.example.com"
export AUTOMATION_WEBHOOK_SECRET="hmac-secret"
export AUTOMATION_EVENT_FORWARDING_ENABLED=true
export AUTOMATION_SERVICE_TIMEOUT=30
10. Troubleshooting
10.1 Agent Errors
| Error | Cause | Resolution |
|---|---|---|
AgentAlreadyRegisteredError |
Duplicate agent registration | Check for duplicate @Agent.register decorators |
AgentNotRegisteredError |
Unknown agent name in config | Verify agent name matches a registered agent class |
AgentStuckInLoopError |
Agent repeating the same action | Reduce max_iterations, check prompt quality, try a different model |
10.2 LLM Errors
| Error | Cause | Resolution |
|---|---|---|
LLMMalformedActionError |
LLM output didn't match expected format | Check function call format, try lowering temperature |
LLMNoActionError |
LLM response contained no tool call | Model may need stronger system prompt; check token limits |
LLMNoResponseError |
Empty response from LLM API | Check API key validity, network connectivity, rate limits |
LLMContextWindowExceedError |
Input exceeded model's context window | Enable a condenser (e.g., recent_events or llm_summarizing) |
UserCancelledError |
User cancelled the operation | Normal flow — no action needed |
10.3 Function Call Errors
| Error | Cause | Resolution |
|---|---|---|
FunctionCallConversionError |
Failed to convert between tool-calling formats | Check model supports function calling; verify tool schemas |
FunctionCallValidationError |
Tool call arguments failed validation | Review agent prompt for correct parameter types |
FunctionCallNotExistsError |
Agent called a non-existent tool | Check enabled tools match agent configuration |
10.4 Runtime Errors
| Error | Cause | Resolution |
|---|---|---|
AgentRuntimeBuildError |
Failed to build sandbox container | Check Docker daemon, Dockerfile syntax, network access |
AgentRuntimeTimeoutError |
Sandbox didn't start in time | Increase SANDBOX_STARTUP_GRACE_SECONDS, check resource availability |
AgentRuntimeUnavailableError |
No runtime available | Verify Docker/K8s is running and accessible |
AgentRuntimeNotReadyError |
Runtime started but not initialized | Increase INIT_PLUGIN_TIMEOUT, check plugin dependencies |
AgentRuntimeDisconnectedError |
Lost connection to sandbox | Check network stability, container health, resource limits |
AgentRuntimeNotFoundError |
Sandbox container was deleted/evicted | Check K8s pod eviction policies, Docker cleanup crons |
10.5 Browser Errors
| Error | Cause | Resolution |
|---|---|---|
BrowserInitException |
Failed to start browser in sandbox | Ensure chromium is installed in sandbox image |
BrowserUnavailableException |
Browser crashed or is unresponsive | Increase sandbox memory, check codeact_enable_browsing |
10.6 Storage Errors
| Error | Cause | Resolution |
|---|---|---|
FileNotFoundError (S3) |
Object doesn't exist in S3 bucket | Verify bucket name, prefix, and IAM permissions |
FileNotFoundError (GCS) |
Object doesn't exist in GCS bucket | Verify bucket and service account permissions |
FileNotFoundError (Memory) |
Key not in in-memory store | Only persists for session lifetime; expected after restart |
10.7 MCP Errors
| Error | Cause | Resolution |
|---|---|---|
RuntimeError (MCPClient) |
Failed to connect to MCP server | Verify server URL/command, check network/process |
ValueError (missing tool) |
MCP tool not found on server | Ensure tool name matches server's published tools |
ValueError (missing session) |
MCP session not established | Check MCP server is running and connection succeeded |
10.8 Common Operational Issues
Web UI stuck at "Starting" for old conversations (Issue #12684, CLOSED): Old sessions may fail to reconnect. Clear browser storage or start a new conversation.
Gemini models returning 404 (Issue #14028, OPEN):
The base_url gets doubled when using Google Gemini models. Remove any /v1 suffix from your configured base URL.
LLM cost tracking stops after session resume (Issue #13843, CLOSED): Costs accumulated before resume are preserved, but the counter resets for the resumed session portion.
Accumulated token usage returns empty dict (Issue #14039, OPEN):
The accumulated_token_usage field in the events API may return {}. This is tracked for fix.
Integrations page blank (Issue #14096, OPEN):
The integrations page renders blank without GITHUB_APP_SLUG being set. Ensure all GitHub App configuration is complete.
Auto-titling broken for webhook conversations (Issue #14100, CLOSED): Conversations initiated via webhooks were not receiving auto-generated titles. Fixed in recent releases.
Tip
For full event history loading in long-running sessions, see Issue #12616 (OPEN) which tracks improvements to how events are loaded and retained across session boundaries.
10.9 Docker-Specific Issues
"Cannot connect to Docker daemon":
# Verify Docker is running
docker info
# Check socket permissions
ls -la /var/run/docker.sock
# If using rootless Docker, ensure DOCKER_HOST is set
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
Sandbox container won't start:
# Check available disk space
df -h
# Check Docker resource limits
docker system info | grep -i memory
# Increase startup timeout
export SANDBOX_STARTUP_GRACE_SECONDS=60
10.10 Kubernetes-Specific Issues
Pod eviction:
Sandbox pods may be evicted under memory pressure. Increase resource_memory_limit or add dedicated node pools with appropriate taints/tolerations.
PVC provisioning failures:
Verify the pvc_storage_class exists in your cluster:
kubectl get storageclasses
Image pull errors:
Configure image_pull_secret if using private registries:
kubectl create secret docker-registry ghcr-pull-secret \
--docker-server=ghcr.io \
--docker-username=USERNAME \
--docker-password=TOKEN
10.11 Performance Debugging
Slow agent responses:
- Enable
DEBUG_LLM=trueto measure LLM latency - Check if context condensation is needed (large event histories)
- Verify network latency to LLM provider
- Consider a faster model for iterative tasks
High memory usage:
- Enable
RUNTIME_MEMORY_MONITOR=true - Set
RUNTIME_MAX_MEMORY_GBto cap per-session usage - Check for memory leaks in long-running sessions
- Review condenser settings to prevent unbounded event accumulation
10.12 Enterprise Database Issues
Connection pool exhaustion:
# Increase pool size
export DB_POOL_SIZE=50
export DB_MAX_OVERFLOW=20
# Monitor active connections
psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname='openhands';"
Migration failures:
# Check current migration state
alembic current
# Run pending migrations
alembic upgrade head
# Rollback one step
alembic downgrade -1
11. Error Classes Reference
11.1 Agent Errors
| Error Class | Description |
|---|---|
AgentAlreadyRegisteredError |
Attempted to register an agent name that already exists |
AgentNotRegisteredError |
Requested agent name not found in registry |
AgentStuckInLoopError |
Agent detected repeating the same action pattern |
11.2 LLM Errors
| Error Class | Description |
|---|---|
LLMMalformedActionError |
LLM response doesn't conform to expected action format |
LLMNoActionError |
LLM response contained no actionable tool call |
LLMNoResponseError |
LLM API returned empty or null response |
LLMContextWindowExceedError |
Input tokens exceed model's maximum context length |
UserCancelledError |
User explicitly cancelled the running operation |
11.3 Function Call Errors
| Error Class | Description |
|---|---|
FunctionCallConversionError |
Failed to convert between tool-calling formats |
FunctionCallValidationError |
Tool call arguments failed schema validation |
FunctionCallNotExistsError |
Agent invoked a tool that doesn't exist |
11.4 Runtime Errors
| Error Class | Description |
|---|---|
AgentRuntimeBuildError |
Sandbox container image build failed |
AgentRuntimeTimeoutError |
Sandbox failed to become ready within timeout |
AgentRuntimeUnavailableError |
No runtime backend available |
AgentRuntimeNotReadyError |
Runtime exists but hasn't finished initialization |
AgentRuntimeDisconnectedError |
Lost connection to running sandbox |
AgentRuntimeNotFoundError |
Referenced sandbox container no longer exists |
11.5 Browser Errors
| Error Class | Description |
|---|---|
BrowserInitException |
Browser process failed to start |
BrowserUnavailableException |
Browser is not accessible or has crashed |
12. Module Reference
12.1 openhands/core/config/ (14 files, 106K chars)
The configuration subsystem parses TOML files, environment variables, and CLI arguments into validated Pydantic models. All config classes support serialization to/from TOML and JSON.
Key files:
app_config.py— Top-levelAppConfigaggregating all sub-configsllm_config.py—LLMConfigwith model, API key, retry, and token settingsagent_config.py—AgentConfigwith tool enables and behavior flagssandbox_config.py—SandboxConfigfor container runtime settingssecurity_config.py—SecurityConfigfor confirmation mode and analyzersmcp_config.py—MCPConfigfor MCP server definitionscondenser_config.py— Condenser strategy configs (6 types)
12.2 openhands/controller/
The AgentController manages the agent execution lifecycle:
- Initializes runtime and event stream
- Runs the agent loop (action → execute → observe → repeat)
- Enforces iteration and budget limits
- Handles delegation to sub-agents
- Manages state transitions (RUNNING, PAUSED, STOPPED, ERROR)
12.3 openhands/runtime/
Runtime implementations share a common interface for:
- Container/process lifecycle management
- Action execution (commands, file ops, browser, Jupyter)
- File upload/download
- Plugin initialization
- Health checking
The DockerRuntime and KubernetesRuntime provide full production isolation. LocalRuntime runs directly on the host (development only). CLIRuntime enables headless scripted usage.
12.4 openhands/llm/
llm.py— SynchronousLLMclass wrapping litellmasync_llm.py—AsyncLLMfor concurrent operationsstreaming_llm.py—StreamingLLMfor real-time token streamingllm_registry.py—LLMRegistrymanaging named LLM instancesfn_call_converter.py— Cross-format function call conversionmodel_features.py— Auto-detect model capabilitiesmetrics.py—Metricsclass for cost/token trackingbedrock.py— AWS Bedrock-specific integration
12.5 openhands/events/
event.py— BaseEventclass with metadatastream.py—EventStreamappend-only store with subscribersfilter.py—EventFilterfor querying eventsaction/— All action typesobservation/— All observation typesserialization/— Event serialization/deserialization
12.6 openhands/storage/
local.py—LocalFileStore(filesystem-backed)s3.py—S3FileStore(AWS S3/MinIO)google_cloud.py—GoogleCloudFileStorememory.py—InMemoryFileStore(testing)webhook.py—WebHookFileStore,BatchedWebHookFileStore
12.7 openhands/mcp/
client.py—MCPClientconnecting via Stdio or HTTP/SSEtool.py—MCPClientToolwrapping MCP tools for agent consumptionerror_collector.py—MCPErrorCollectorfor diagnostic aggregation
12.8 enterprise/
Enterprise service layer:
| Module | Classes | Purpose |
|---|---|---|
| Auth | AuthTokenStore |
Token management, Keycloak integration |
| Organizations | OrgService, OrgStore |
Multi-tenant org management |
| Users | UserStore |
User profiles, preferences |
| API Keys | ApiKeyStore |
API key CRUD and validation |
| Billing | Stripe integration | Subscriptions, credits, checkout |
| LLM Proxy | LiteLlmManager |
Centralized LLM access control |
| Analysis | SolvabilityClassifier |
Issue solvability scoring |
| Maintenance | MaintenanceTaskProcessor |
Background maintenance tasks |
13. Known Issues & Limitations
13.1 Open Issues
| Issue | Summary | Impact |
|---|---|---|
| #14039 | accumulated_token_usage returns empty dict in events API |
Token tracking incomplete |
| #14028 | Gemini models 404 — base_url doubled | Google Gemini models unusable without workaround |
| #14096 | Integrations page blank without GITHUB_APP_SLUG |
Enterprise UI broken without full config |
| #14095 | Bitbucket Data Center webhook handler | Bitbucket DC integration incomplete |
| #12616 | Full-history events loading/retention | Long sessions may lose early events |
13.2 Closed Issues (Recent, Noteworthy)
| Issue | Summary | Resolution |
|---|---|---|
| #14107 | GitlabV1CallbackProcessor deserialization | Fixed deserialization logic |
| #14100 | Auto-titling broken for webhook conversations | Fixed title generation |
| #12684 | Web UI stuck at "Starting" for old conversations | Fixed session reconnection |
| #13843 | LLM cost tracking stops after session resume | Partial fix — counter resets on resume |
| #13412 | MyPy doesn't check SQLAlchemy type stubs | Added type stub configuration |
| #13150 | Authority separation for autonomous agents | Security model documented |
Note
MyPy type checking for SQLAlchemy models was not fully configured (Issue #13412). If contributing to the enterprise layer, install
sqlalchemy-stubsorsqlalchemy[mypy]to get proper type checking in your editor.
14. Contributing
14.1 Development Setup
# Clone the repository
git clone https://github.com/All-Hands-AI/OpenHands.git
cd OpenHands
# Backend setup
poetry install
# Frontend setup
cd frontend && npm install && cd ..
# Run development server
poetry run python -m openhands.core.main
# Run frontend dev server (separate terminal)
cd frontend && npm run dev
14.2 Code Patterns
Registry pattern (agents, runtimes):
# Register a new agent
@Agent.register("my_agent")
class MyAgent(Agent):
def step(self, state: State) -> Action:
...
Event-driven communication:
# Subscribe to events
event_stream.subscribe(EventStreamSubscriber.AGENT_CONTROLLER, callback)
# Append events
event_stream.add_event(CmdRunAction(command="ls -la"), EventSource.AGENT)
Pydantic config models:
class MyConfig(BaseModel):
setting_a: str = "default"
setting_b: int = 42
model_config = ConfigDict(extra="forbid")
14.3 Testing Conventions
- Unit tests: pure logic, mock external dependencies
- Integration tests: require Docker, test runtime interactions
- E2E tests: require full stack (backend + frontend + Docker)
- Use
InMemoryFileStorein unit tests to avoid filesystem side effects - Mock LLM responses for deterministic agent behavior tests
14.4 Frontend Architecture
The frontend uses a modern React stack:
| Library | Version | Purpose |
|---|---|---|
| React | 19 | UI framework |
| React Router | 7 | Client-side routing |
| Zustand | 5 | State management |
| TailwindCSS | 4 | Styling |
| i18next | — | Internationalization |
| Vite | 7 | Build tool and dev server |
The openhands-ui/ package provides shared components published as @openhands/ui, used by both the main frontend and enterprise UI.
For basic usage, quick start guides, and user-facing feature documentation, see the OpenHands User Guide.