Repos Worth Reading · · 1,658 words · 8 min read
OpenHands architecture explained, read at commit f7fb0c4b
How OpenHands is built at v1.16.0. The agent step loop, workspaces and sandboxes, the condenser, the confirmation gate, and what the README now calls Agent Canvas.
openhands architecture sandboxing agent sdk
This is a reading of the OpenHands repository at a named commit, not a review of a product I ran. You get the shape of the system as the code and docs describe it on 2026-09-05: what the repository has become, where the agent loop actually lives, how commands reach a sandbox, how long conversations are kept inside a context window, and how the confirmation gate works. I also say what I did not do, which is install or run any of it.
The repository on 2026-09-05
The GitHub API answers for All-Hands-AI/OpenHands now redirect to OpenHands/OpenHands. Read on 2026-09-05, the repository had 86,272 stars, 11,320 forks, 663 open issues, an MIT license, and TypeScript as its top language. The latest release was v1.16.0, published 2026-08-27. The latest commit on main was f7fb0c4b21f5 from 2026-09-05, with the message "docs: correct OH_*_GIT_REF defaults and merge duplicated bullet in DEVELOPMENT.md (#17167)". Everything below is read at that commit.
The first surprise is the README. Its H1 is not "OpenHands" but "Agent Canvas", described as "The self-hosted developer control center for coding agents and automations." The README says Agent Canvas "runs the open source OpenHands agent out-of-the-box, but can use any third-party agent like Claude Code and Codex," and that it "turns your coding agents into a self-hosted, always-on engineering team." The status badge on the README says beta. So the repository that most people still think of as "the open-source coding agent" is, at this commit, primarily a control plane that hosts agents, one of which is OpenHands' own.
The quickstart in the README gives three routes:
Option one, no sandbox:
npm install -g @openhands/agent-canvas
agent-canvas
Option two, Docker, pinned to the release image:
docker run -it --rm -p 8000:8000 -v "$HOME/.openhands:/home/openhands/.openhands" \
-v "${PROJECTS_PATH}:/projects" ghcr.io/openhands/agent-canvas:1.16.0
Option three, from source:
git clone https://github.com/OpenHands/OpenHands.git
cd OpenHands && npm install && npm run dev
Note the language mix. The control center is TypeScript; the agent itself is Python and lives in a separate package family, which the docs call the Software Agent SDK.
Where the agent actually is
The SDK "Getting Started" page describes the Software Agent SDK as "a modular framework for building AI agents that interact with code, files, and system commands. Agents can execute bash commands, edit files, browse the web, and more." The install line is one command, and the page warns that both packages must be installed together to keep versions aligned:
pip install -U openhands-sdk openhands-tools
The minimal program has four parts. An LLM is built from a model name and key. An Agent is built from the LLM and a tools list, with TerminalTool, FileEditorTool, and TaskTrackerTool named as the standard set. A Conversation takes the agent and a workspace, and is driven with send_message() and run(). The workspace is "the execution environment where agents operate (local, Docker, or remote)." An optional openhands-agent-server package enables "sandboxed workspaces in Docker or remote servers."
That split is the key to reading the repository: Agent Canvas is a frontend that "connects to these services and displays the state of whichever backend is selected," as the Backends page puts it, and a backend is whatever runs an Agent Server. The Backends page describes the Agent Server as running "conversations and tools in a workspace: the folder, mounted project directory, container, or cloud sandbox where the agent reads and writes files." It also clarifies that "Remote" means connectivity, not location: "A remote backend can be a separate process on the same machine, a self-hosted deployment on a VM or container platform, or a managed Cloud or Enterprise service."
The step loop
The SDK architecture page for the agent describes a "single-step execution model" in which "each step() call processes one reasoning cycle." The order inside one step, as the page lists it:
- Pending actions check: "If actions awaiting confirmation exist, execute them and return."
- Condensation: if a condenser is configured, it processes the event history and either returns condensed events for the LLM call or emits a Condensation event.
- LLM query: "Query LLM with messages from event history."
- Response parsing: the response becomes
ActionEvents (tool calls) orMessageEvents (text). - Confirmation gate: "If actions need user approval: Set conversation status to
WAITING_FOR_CONFIRMATIONand return." - Execution: "Execute tools and create
ObservationEvent(s)."
Three properties are stated outright: "Stateless: Agent holds no mutable state between steps," "Event-Driven: Reads from event history, writes new events," and "Interruptible: Each step is atomic and can be paused/resumed." The Conversation "orchestrates step execution, provides event history," and the agent returns new events to it.
If you compare this with the loop in what a coding-agent harness actually is, the difference is where state lives. Many harnesses keep a growing message list in memory and call the model on it. OpenHands keeps an event log and rebuilds the model's view from it on each step, which is what makes pause, resume, and confirmation a matter of reading and writing events rather than special-casing the loop. The page I read does not state a termination rule (no maximum iterations or stuck detection are described there), so I do not claim one.
Workspaces: where the shell command goes
The Workspace page is the clearest description of the sandbox boundary. There are three concrete workspace types plus a fourth for existing servers:
| Workspace | Execution | Isolation (as the page states it) |
|---|---|---|
LocalWorkspace |
"Direct subprocess" via subprocess.run() |
"Process-level", "Host system access" |
RemoteWorkspace |
"HTTP API-based execution via agent-server" | "Container/VM-level" |
DockerWorkspace |
extends RemoteWorkspace, "Auto-spawn containers" | container-level |
RemoteAPIWorkspace |
connects to an existing remote server | "Remote server" |
The execution flow on the page is a single fork: a tool "invokes execute_command()", which routes either to "subprocess.run() Direct execution" or to "POST /command HTTP API", and both return a CommandResult with stdout, stderr, exit code, a timeout flag, and duration. That one fork is the whole sandboxing story from the agent's point of view. The agent code does not know whether it is talking to the host or to a container; the workspace object does.
This is worth borrowing. If your own harness has the model's tool call and the OS process in the same function, you cannot later move execution into a container without rewriting the loop. OpenHands put an HTTP seam between them from the start.
The condenser: how long sessions stay inside the window
Agentic sessions accumulate tool output faster than anything else. The Condenser page describes the component that manages this as performing "conversation history compression to keep agent context within LLM token limits," reducing "long event histories into condensed summaries while preserving critical information for reasoning." Four implementations are listed:
NoOpCondenser, "Pass-through condenser that performs no compression"LLMSummarizingCondenser, "Uses an LLM to generate summaries of conversation history"PipelineCondenser, "Chains multiple condensers in sequence"RollingCondenser, threshold-based triggering for a rolling window
Two parameters carry the defaults: max_size, the "Event count threshold before condensation triggers (default: 120)", and keep_first, the "Number of initial events to preserve verbatim (default: 4)". Condensation triggers automatically when the event threshold is exceeded during a step, or manually through a CondensationRequest event, "typically when an 'LLM context error' occurs."
The keep_first default is the detail I would copy. The first events are the task statement and the system setup; summarizing them is how an agent forgets what it was asked to do. Keeping them verbatim and summarizing the middle is a cheap rule that other harnesses arrive at independently, which I discuss in when to stop the agent.
The confirmation gate
The Security guide describes three confirmation policies:
AlwaysConfirm() # "Require approval for all actions"
NeverConfirm() # "Execute all actions without approval"
ConfirmRisky() # "Only require approval for risky actions (requires security analyzer)"
The analyzer classifies each action as LOW ("Safe operations with minimal security impact"), MEDIUM ("Moderate security impact, review recommended"), HIGH ("Significant security impact, requires confirmation"), or UNKNOWN ("Risk level could not be determined"). The primary implementation is an LLM security analyzer that "Reviews each action before execution" and "Flags potentially dangerous operations." The example code polls the conversation state, and when the status is waiting for confirmation, it lists the pending actions and lets the user approve them or call conversation.reject_pending_actions(). Policies can be switched at runtime with conversation.set_confirmation_policy().
Note what this is and is not. It is a second model judging the first model's proposed action, plus a human gate on the risky ones. It is not an OS-level sandbox; that is the workspace's job. The two layers are separate objects, which is how a comparison across harnesses in permission models compared can line them up.
What v1.16.0 changed
The v1.16.0 release notes, dated 2026-08-27, are mostly about the control center, which fits the README. The first bullets read: "feat(settings): select supported LLM providers", "feat: add linux desktop installer build (CI + deb maintainer fix)", "feat(automations): show a run's live phase", "feat(settings): expose the LLM-switching toggle in Agent settings", and "feat(skills): replace the all-on skill catalog with an explicit allow-list". Further down: "feat: land the Canvas Extensions frontend (load pages, sidebar, customize)" and a fix to "forward disabled_skills into agent_context so backend excludes them". Nothing in the first fifteen bullets touches the step loop, the condenser, or the workspace types, which live in the SDK packages rather than this repository.
What it does well, and what to borrow
Three things stand out from the reading. The execution seam (tool call in one process, command execution behind an HTTP API in another) is the cleanest sandbox boundary among the harnesses I have read. The event log as the source of truth makes confirmation and resumption ordinary rather than exceptional. And the condenser's keep_first default encodes a lesson most people learn by watching an agent lose the plot.
What I could not verify: I did not install openhands-sdk, did not start Agent Canvas, and did not run a conversation. I have no numbers for how much the condenser saves or how often the analyzer flags HIGH. The docs pages I quote are the current versions on 2026-09-05 and are not pinned to the commit; the repository metadata, README, and release notes are pinned as stated. If you want the repository the SWE-bench trajectory papers refer to, note that AgentLens analyzed 2,614 OpenHands trajectories, which is the agent, not the canvas.
Sources
- OpenHands/OpenHands, "README at commit f7fb0c4b21f5 and repository metadata (read 2026-09-05)"
- OpenHands/OpenHands, "Release v1.16.0 (2026-08-27)"
- OpenHands docs, "Getting Started" (Software Agent SDK, read 2026-09-05)
- OpenHands docs, "Agent" (SDK architecture, read 2026-09-05)
- OpenHands docs, "Workspace" (SDK architecture, read 2026-09-05)
- OpenHands docs, "Condenser" (SDK architecture, read 2026-09-05)
- OpenHands docs, "Security & Action Confirmation" (read 2026-09-05)
- OpenHands docs, "Backends" (Agent Canvas, read 2026-09-05)