OpenHands Agent Canvas is a self-hosted control plane that dispatches coding agents to heterogeneous execution environments. You can route Claude Code, Codex, Gemini, or any ACP-compatible agent to local Docker containers, remote VMs, or company infrastructure from a single interface. The project (85,455 stars, trending #11 in TypeScript) treats agents as persistent infrastructure rather than ephemeral chat sessions.
The interesting plumbing is in the backend abstraction layer and the Agent Communication Protocol (ACP) compatibility shim. Agent Canvas normalizes tool calls, session state, and response formats across different agent types, then routes execution to whatever backend you configure. Prebuilt automations handle recurring tasks like GitHub issue decomposition or Slack report publishing without manual intervention.
Architecture: Control Plane and Backend Routing
Agent Canvas runs as a TypeScript service that maintains a registry of agent backends and a queue of automation jobs. The control plane does not execute agent code itself. It dispatches requests to backends and manages session state in a local SQLite database (or Postgres for production).
Backend types:
- Local Docker: Default. Spins up containers on the host machine with mounted workspace volumes.
- Remote VM: Connects over SSH, runs agents in isolated environments with resource limits.
- Cloud infrastructure: Integrates with Kubernetes or AWS ECS to schedule agent pods on demand.
- Company infrastructure: Custom backend adapter that calls internal APIs or connects to on-prem clusters.
Each backend exposes a common interface:
interface AgentBackend {
start(agentType: string, config: AgentConfig): Promise<SessionId>;
sendMessage(sessionId: SessionId, message: string): Promise<void>;
getState(sessionId: SessionId): Promise<SessionState>;
stop(sessionId: SessionId): Promise<void>;
}
The control plane selects a backend based on automation configuration or user preference. If a backend is unavailable, the control plane queues the request and retries with exponential backoff.
ACP Compatibility Layer: Normalizing Tool Calls
Agent Communication Protocol (ACP) is OpenHands' abstraction over different agent APIs. Claude Code uses Anthropic's tool-use format, Codex uses OpenAI function calling, and Gemini uses Google's function declarations. ACP translates these into a unified schema.
ACP message structure:
{
"role": "assistant",
"content": "I'll create the file.",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "write_file",
"arguments": "{\"path\": \"src/main.ts\", \"content\": \"console.log('hello');\"}"
}
}
]
}
The ACP adapter for each agent type maps inbound tool calls to this format, then translates responses back to the agent's native format. For example, Claude Code expects tool_use blocks with tool_use_id, while Codex expects function_call with name and arguments. The adapter handles this bidirectional translation.
Tool execution flow:
- Agent Canvas receives a tool call in ACP format.
- The backend executor validates the tool name against a whitelist.
- The executor runs the tool in a sandboxed environment (Docker container or VM).
- The result is wrapped in ACP format and sent back to the agent.
- The agent continues or terminates based on the result.
If a tool call fails, the executor returns an error message in ACP format. The agent can retry, request clarification, or abort the task.
Prebuilt Automations: GitHub Issue Decomposition
Automations are YAML files that define triggers, agent selection, and output destinations. The GitHub issue decomposition automation watches for new issues with a specific label, then dispatches an agent to break the issue into subtasks.
Automation definition:
name: decompose-github-issue
trigger:
type: webhook
source: github
event: issues.labeled
filter:
label: "needs-breakdown"
agent:
type: claude-code
backend: remote-vm
config:
model: claude-3-5-sonnet-20241022
max_tokens: 4096
steps:
- action: read-issue
tool: github_get_issue
- action: decompose
prompt: "Break this issue into 3-5 subtasks. Each subtask should be actionable and testable."
- action: create-subtasks
tool: github_create_issues
output:
type: github-comment
template: "Created subtasks: {{subtask_urls}}"
The control plane polls the GitHub webhook endpoint, matches the event against registered automations, and queues a job. The job executor selects the configured backend (remote-vm in this case), starts a Claude Code session, and runs the steps sequentially. If a step fails, the executor logs the error and optionally retries or sends a notification.
Slack report automation works similarly but uses a cron trigger instead of a webhook. The agent queries a database or API, formats the results, and posts to a Slack channel via the Slack API tool.
Session State Management
Agent Canvas stores session state in a relational schema:
| Table | Columns | Purpose |
|---|---|---|
| sessions | id, agent_type, backend_id, status, created_at | Tracks active and completed sessions |
| messages | id, session_id, role, content, tool_calls, timestamp | Stores conversation history |
| tool_executions | id, message_id, tool_name, arguments, result, status | Logs tool calls and results |
| automations | id, name, trigger, agent_config, steps, status | Defines automation workflows |
| jobs | id, automation_id, session_id, status, error, created_at | Tracks automation execution |
When a backend crashes or a VM is terminated, the control plane marks the session as failed and retries the job on a different backend if configured. Session history is preserved, so you can inspect what the agent did before the failure.
State recovery: If the control plane itself restarts, it reads the jobs table and resumes any in-progress automations. This requires backends to support session resumption (not all do). Docker backends can resume if the container is still running. Remote VM backends require the agent process to write checkpoints to a shared volume.
Security Boundaries
Agent Canvas enforces three security layers:
- Tool whitelisting: Each backend has a list of allowed tools. Agents cannot execute arbitrary commands.
- Sandboxing: Docker and VM backends run agents in isolated environments with no network access by default.
- Credential isolation: API keys and secrets are injected into the backend environment at runtime, never stored in session state.
Failure mode: If an agent requests a tool that is not whitelisted, the backend returns an error. The agent can request a different tool or abort. If an agent attempts to escape the sandbox (e.g., by writing to /etc or opening a reverse shell), the backend terminates the session and logs the attempt.
Company infrastructure backends require additional configuration to integrate with internal secret managers (HashiCorp Vault, AWS Secrets Manager). The backend adapter fetches credentials at session start and injects them as environment variables.
Observability: Tracing Agent Execution
Agent Canvas emits structured logs and OpenTelemetry traces. Each session gets a trace ID that propagates through tool calls, backend requests, and automation steps.
Key metrics:
- Session duration (p50, p95, p99)
- Tool call latency by tool name
- Backend availability and error rate
- Automation success rate by automation name
The control plane exposes a /metrics endpoint in Prometheus format. You can scrape this with Prometheus and visualize in Grafana.
Trace example:
span: session.start (session_id=abc123, agent_type=claude-code)
span: backend.select (backend_type=remote-vm)
span: backend.start (vm_id=vm-456)
span: agent.send_message (message="Decompose this issue")
span: tool.execute (tool_name=github_get_issue)
span: tool.execute (tool_name=github_create_issues)
span: session.stop
If a session hangs, you can inspect the trace to see which tool call is blocking.
Deployment Shape
Local development: Run npx @openhands/agent-canvas to start the control plane on localhost:3000. The Docker backend is enabled by default. No external dependencies.
Production: Deploy the control plane as a container or systemd service. Use Postgres instead of SQLite. Configure remote VM or cloud backends in config.yaml. Set up a reverse proxy (Nginx, Caddy) for HTTPS.
High availability: Run multiple control plane instances behind a load balancer. Use a distributed lock (Redis, etcd) to prevent duplicate job execution. Backends should be stateless or support session resumption.
Failure Modes and Mitigations
| Failure | Impact | Mitigation |
|---|---|---|
| Backend unavailable | Automation jobs queue up | Configure multiple backends, enable retries |
| Agent timeout | Session hangs indefinitely | Set max_execution_time in agent config |
| Tool call error | Agent cannot complete task | Return structured error, let agent retry or abort |
| Control plane crash | In-progress jobs lost | Enable job persistence, resume on restart |
| ACP translation bug | Agent receives malformed response | Log raw messages, add schema validation |
Observability gap: If a backend crashes without reporting status, the control plane does not know the session failed until the health check times out (default 30 seconds). You can reduce this by lowering the health check interval, but this increases network overhead.
Technical Verdict
Use Agent Canvas when:
- You need to run multiple agent types (Claude Code, Codex, Gemini) from one interface.
- You want to automate recurring tasks like issue triage or report generation.
- You need to route agents to different execution environments (local, remote, cloud).
- You want to self-host and control where agent code runs.
Avoid Agent Canvas when:
- You only use one agent type and do not need backend abstraction.
- You need real-time collaboration (Agent Canvas is optimized for batch automation).
- You require sub-second latency (backend routing adds 100-500ms overhead).
- You need built-in human-in-the-loop approval (you will need to build this yourself).
The ACP compatibility layer is the most fragile part. If an agent API changes (e.g., Anthropic adds a new tool format), the adapter breaks until OpenHands updates it. Monitor the OpenHands release notes and test automations after upgrading.
Top comments (0)