Wiki · Research
OpenHands
Shelf
Research

OpenHands

1. Snapshot

OpenHands’ current open-source product spans two repositories: Agent Canvas owns the user-facing control surface and software-agent-sdk owns the agent, tools, conversations, workspaces, and Agent Server API. The Canvas README names that split explicitly (Canvas architecture).

FieldAgent CanvasAgent runtime and server
RepositoryOpenHands/OpenHandsOpenHands/software-agent-sdk
Pinned commitdc99e98615de4ace821692773b00a7f50d476e5046ad3d43dc385b2e7975c0935f157153930ebb16
Date read2026-08-152026-08-15
LicenceMIT — LICENSEMIT — LICENSE
Primary languageTypeScript/TSXPython
Measured scale1,995 tracked files; 1,701 tracked .ts/.tsx files; 1,128 of those under src1,469 tracked files; 1,256 tracked .py files; 286 Python files in openhands-sdk; 70 in openhands-agent-server; 10 in openhands-workspace

Measured 2026-08-15 at the pinned commits, in PowerShell:

# OpenHands/OpenHands
(git ls-files | Measure-Object -Line).Lines
(git ls-files -- '*.ts' '*.tsx' | Measure-Object -Line).Lines
(git ls-files -- 'src/*.ts' 'src/**/*.ts' 'src/*.tsx' 'src/**/*.tsx' |
  Sort-Object -Unique | Measure-Object -Line).Lines

# OpenHands/software-agent-sdk
(git ls-files | Measure-Object -Line).Lines
(git ls-files -- '*.py' | Measure-Object -Line).Lines
(git ls-files -- 'openhands-sdk/*.py' 'openhands-sdk/**/*.py' |
  Sort-Object -Unique | Measure-Object -Line).Lines
(git ls-files -- 'openhands-agent-server/*.py' 'openhands-agent-server/**/*.py' |
  Sort-Object -Unique | Measure-Object -Line).Lines
(git ls-files -- 'openhands-workspace/*.py' 'openhands-workspace/**/*.py' |
  Sort-Object -Unique | Measure-Object -Line).Lines

The commands produced the counts in the table. They count tracked paths, not logical lines, runtime processes, or test cases.

Read boundary. I traced local Agent Canvas conversation creation through the Agent Server, LocalConversation, the default agent response/action path, terminal and file-editor tools, event persistence, Git read APIs, worktree creation, run-control WebSockets, confirmation controls, sub-agent delegation, the optional DockerWorkspace, and the synthetic concurrent-conversation stress test. I also read the Canvas packaging and documented local/Docker launch boundaries. Hosted OpenHands Cloud implementation, automation internals, provider transports, ACP subprocess implementations, the browser tool implementation, evaluation suites, and frontend surfaces outside run/repository control are outside the boundary. I did not run or deploy the product.

2. The core loop

Agent Canvas creates a local conversation with a selected working directory. Unless the caller explicitly selects the repository in place, Canvas resolves the workspace mode to new_worktree and sends worktree: true; cloud creation instead calls the hosted app-conversation API and waits for its task to become ready (Canvas creation). The adapter adds the agent profile, workspace, client tools, confirmation policy, iteration bound, and stuck detection to StartConversationRequest (request construction, StartConversationRequest).

For a valid Git repository, Agent Server chooses a base ref, creates /tmp/conversation-worktrees/<conversation-id>/<project-name>, and checks out a new openhands/<conversation-id> branch there. A non-Git path or a request with worktrees disabled continues in the supplied workspace (base selection and worktree creation). The service then persists a StoredConversation, creates an EventService, and sends the initial message with run=True (conversation start).

The same run path is used after creation. The browser sends a user message over the conversation WebSocket, or falls back to REST when the socket is unavailable, with run: true (Canvas send path); the authenticated server socket appends it to that conversation’s event service and starts the run (server socket).

LocalConversation.run moves the status to running and repeatedly calls agent.step. It stops on pause, stuck detection, completion, confirmation wait, budget/iteration limits, or an error; bounded limits become explicit error state and events (synchronous run loop). An agent step executes any pending actions, prepares the model messages, requests a response, and dispatches tool calls or final content (agent step and response path). Tool calls are parsed, normalized, validated against the registered tool, annotated with summary and risk, and emitted as ActionEvents; unless policy requires confirmation, the parallel executor runs the action and emits its observation (action construction, response dispatch).

There is no separate “commit the run” phase in the inspected local API. Repository mutation, including git commit, is an agent-selected terminal command. The product’s pull, push, and pull request controls likewise send natural-language instructions to the agent (Git action prompts). After a commit, Canvas reads changes, diffs, and commits through GET-only Git routes (Canvas Git client, Agent Server Git routes). The trace therefore reaches a committed change only when the model chooses and successfully runs that terminal command; conversation completion alone does not guarantee a commit, push, or pull request.

3. State and persistence

ConversationState holds the agent, workspace, configuration, execution status, statistics, message/tool events, and agent-specific state. With a persistence directory it stores base state as JSON and events in an EventLog; public-field changes autosave base state (state fields, save and autosave, autosave hook). ConversationState.create either initializes those stores or reloads base state and the event log, rebuilds derived state, verifies that restored tools are compatible, and substitutes the runtime workspace supplied by the server (create or resume). Secrets are encrypted when a cipher is supplied; without one they are redacted and cannot be recovered from the saved JSON (base-state serialization).

Agent Server binds each loaded conversation to an EventService and its own persistence directory. Live model deltas are sent to subscribers but deliberately are not persisted (event service construction and streaming). On restart, a conversation persisted as running is changed to error. The service scans its event log for an action without a matching observation and appends a non-retryable error saying the restart occurred while that tool was in progress (restart reconciliation).

This is event-consistency recovery, not environment rollback. A LocalWorkspace pause is a no-op because the workspace is the host filesystem (local pause/resume); the restart path records an ambiguous in-flight effect but does not undo it. Persisted events and base state can reconstruct the conversation, while an in-flight model stream, Python stack, and the pre-action filesystem are not snapshots restored by this mechanism. A dedicated Git worktree can make the repository branch disposable, but it cannot roll back effects outside that checkout.

Canvas reconstructs the user view by fetching paginated REST history first and then opening a WebSocket with a since timestamp. It deduplicates replayed event ids and applies live status, statistics, terminal, and browser events (history plus live tail, event handling, reconnect options).

4. Tools and the outside world

The default boundary is the host, not a sandbox

LocalWorkspace explicitly provides direct local-filesystem access and executes commands with a working directory on the host (local workspace). The file editor starts from a workspace root, but its path validation accepts absolute paths and checks existence/command compatibility rather than containment under that root (editor setup, path validation). The product documentation matches the implementation: the default npm/source launch runs Agent Server directly on the machine and warns that the agent has full filesystem access (unsandboxed launch, source launch warning). A per-conversation worktree changes the Git checkout and branch; it does not create an operating system security boundary around either tool.

Two different Docker boundaries

Agent Canvas’ documented Docker option is one all-in-one product container. It mounts persisted settings/conversations and a host PROJECTS_PATH at /projects, and the documentation says the agent can access every project under that mounted directory (Docker launch). The image combines Agent Server, automation, the static frontend, and an ingress proxy; it declares the two mounted volumes and one exposed port (all-in-one image, volumes and entrypoint). That container is a deployment boundary for a server that can host multiple conversations, not a fresh container promised for every Canvas conversation.

The SDK separately offers DockerWorkspace, which starts a prebuilt Agent Server container for one remote-workspace object. Its contract exposes user-chosen volume mounts, an optional Docker network and GPU access, selected environment variables, and a mapped port (workspace configuration, container launch). Cleanup stops that container; pause and resume use docker pause and docker unpause (lifecycle).

Bounded absence check, 2026-08-15. rg -n 'memory|cpus|cpu-shares|read-only|pids-limit' openhands-workspace/openhands/workspace/docker/workspace.py found no CPU, memory, PID, or read-only-filesystem limit in this class. The search says only that the inspected launcher does not set those controls; an operator or another workspace implementation can impose limits outside it.

Repository ingress and egress

For an existing local repository, Canvas supplies a working directory and normally asks Agent Server to create the dedicated worktree described above. For a remote repository, the run-control bar first stores repository metadata and then asks the agent in chat to clone the repository and check out the selected branch; clone failure is therefore reported through the conversation (repository selection). Changes leave by the same tool boundary: the agent runs Git/provider commands, while Canvas asks it to pull, push, or open a pull request and reads resulting diffs and commits. The inspected Git API has no write route (prompted egress, GET routes).

5. Composition

At the product level, Agent Canvas can connect to multiple backends, while each Agent Server offers multiple agents on one host and port (documented backend composition, server boundary). Conversation identity partitions persistence, event subscribers, run state, and—on Canvas’ default local path—a Git worktree and branch. It does not partition the host filesystem or operating-system process boundary.

The opt-in delegate tool adds in-run composition. It spawns child LocalConversations with copied LLM instances, the parent’s workspace path and confirmation policy, and child persistence beneath the parent’s directory (sub-agent construction). Delegated tasks run concurrently in threads; their final responses and usage metrics return to the parent (parallel delegation). Because those children receive the same workspace path, delegation isolates conversation/event state but not concurrent filesystem writes inside the parent’s checkout.

Concurrent top-level runs

The Agent Server stress suite contains a synthetic 16-conversation contract. It starts separate conversations against one workspace path and scripted slow LLMs with asyncio.gather, then checks completion, per-conversation persistence, lack of scripted-response leakage, wall time below four times a single conversation, and RSS growth below twice the baseline (concurrency test, budgets). That test does not create worktrees, execute filesystem tools, call real models, or establish a production capacity. It is evidence for concurrent scheduling and conversation/persistence separation under its fixture, not for repository-write isolation or cost per real run.

Opinion. The direct comparison with rungs is no longer containers-versus-worktrees. Current Canvas itself chooses a worktree by default, gaining a separate checkout, index, and branch at the cost of another checkout while sharing the Git object database and wider host. The all-in-one Docker launch moves that shared host boundary into one container; DockerWorkspace can instead spend a container per workspace. The pinned source provides no controlled cost measurement with which to call one arrangement better.

6. The human in the loop

Canvas exposes a reconstructed event history plus a live stream, execution state and usage, terminal commands/output, browser screenshots and URLs, Git diffs/commits, and message steering (run event handling, Git client). Local stop uses immediate interrupt, which cancels the in-flight request; cloud stop pauses its sandbox. Resume starts the conversation again (Canvas stop/resume, resume). The server distinguishes pause, which waits for the current model call, from interrupt, which cancels it immediately and leaves the conversation resumable (server controls). LocalConversation catches that cancellation, persists paused state, and emits an interrupt event (interrupt path, interrupt API).

Confirmation policy evaluates each action’s declared risk before execution and moves the conversation to waiting when any action in the batch requires approval; lone think/finish actions are exempt (policy gate). Canvas shows the latest awaiting action, calls out high risk, and offers accept/reject (confirmation UI).

The protocol’s authority boundary is weaker than the display. The server request contains only accept: bool and a rejection reason, not the action id or arguments (request model); acceptance resumes the conversation and rejection rejects all pending actions (response handling). The UI tracks the selected event id to avoid double submission, but the server does not bind the decision payload to that displayed action identity. The conversation socket authenticates its API connection (socket authentication). Opinion. That authenticates access to the conversation, but the inspected approval request does not establish which human made the decision.

7. The abstraction bargain

Opinion. OpenHands makes the operational layer tangible: a conversation is not only an agent loop but a selected backend, workspace, persistence directory, event stream, Git view, and set of run controls. That coherence costs a larger state machine and recovery protocol. Even then, persisting an event log cannot provide exactly-once tool effects; the restart handler is strongest where it admits the ambiguity and records which action was in flight.

Opinion. “Sandbox” must name a boundary and its exceptions. Current local Canvas has no OS boundary, its worktree is repository coordination rather than a security control, the all-in-one Docker option exposes every mounted project to all conversations in that server, and DockerWorkspace allows explicit mounts and networks. The product gives operators several useful deployment shapes, but the word alone does not establish least privilege, rollback, or resource limits.

Opinion. The product delegates repository ingress and egress back through the agent loop. This keeps one tool surface and lets the user observe failures in chat, but clone, commit, push, and pull request creation are model-mediated operations rather than deterministic workflow stages. A scaffold that requires a guaranteed branch or commit still needs a mechanical gate outside the conversation.

What the product has that the five libraries do not

Opinion. Relative to the inspected boundaries in SWE-agent, LangGraph, OpenAI Agents SDK, Pydantic AI, and Microsoft Agent Framework, the distinctive residue is the composed product layer, not a sixth variation of the loop:

  • a backend registry and control center that starts and switches between local, remote, and cloud agent servers, supported by the documented multi-backend boundary above;
  • durable conversation storage joined to a reconnectable history-plus-live event protocol, rather than only an application-facing state/session abstraction;
  • repository selection, per-conversation worktree creation, and a rendered Git changes/commit surface around the agent’s terminal operations;
  • a user-owned run-control surface for stop, resume, steering, risk confirmation, terminal output, browser state, and usage while the run is live; and
  • distributable packaging that joins frontend, ingress, Agent Server, automation, credentials, and persistence volumes in one self-hostable image (image composition, persisted credentials and state).

8. What rungs takes

These verdicts are inputs to WI-017; they do not change the catalogue here.

Pattern idVerdictPinned evidenceReason
worktree-lifecycletake-as-warningdefault Canvas worktree, server worktreeOpinion. A shipped product independently selects worktrees for concurrent repository sessions, but the pattern must never imply process, filesystem, credential, or network isolation.
candidate: isolation-boundary-declarationtakeunsandboxed warning, Docker mounts, DockerWorkspaceOpinion. Any agent execution option should state the unit isolated, the host paths/environment/network that cross it, and the controls it does not supply. “Sandboxed” is not a sufficient contract.
candidate: event-log-plus-live-tailtakeREST preload, socket replayOpinion. Long-running work needs a durable history query joined to an id-deduplicated live tail; a terminal alone and an un-replayable socket each leave a different recovery gap.
candidate: run-control-surfacetakeCanvas events, stop/resumeOpinion. A long-running agent product needs observable state plus steering, interrupt, resume, pending-decision, and artifact views; those controls are product work that loop libraries correctly leave to their hosts.
candidate: approval-bound-to-requesttake-as-warningboolean response, reject-all behaviorOpinion. The counter-example strengthens the candidate: a decision should bind server-side to the exact pending action and arguments, not rely on the UI’s selected id while sending only a boolean.
candidate: event-stream-not-audit-logtake-as-warningpersisted state/events, confirmation requestOpinion. A rich durable event history still needs actor identity, authorization, retention, and bound decision data before it is an accountability record.
candidate: shared-workspace-subagentsleaveshared workspace construction, parallel threadsOpinion. Parallel children sharing one writable checkout can be a useful deliberate optimization, but it is not a general scaffold default; ownership or serialization must be explicit first.

The strongest counter-evidence is the name “Docker Sandbox” beside a documented host-project mount, and “worktree” beside tools that still have host-level absolute-path access. Both mechanisms are valuable; neither name substitutes for a declaration of the actual trust and recovery boundary.