Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions openhands/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# openhands

A standalone agent kit (`kind: agent`) for [OpenHands](https://openhands.dev/), an
open-source AI software engineering agent. The kit installs OpenHands via
[uv](https://astral.sh/uv/), wires LLM API auth through the sandbox proxy, and runs
`openhands --always-approve` as the entrypoint when you attach.

OpenHands defaults to [CodeActAgent](https://docs.all-hands.dev/usage/agents) with
`SANDBOX_TYPE=local` — code executes directly in the sandbox container rather than
spawning nested Docker containers.

## Prerequisites

- An API key for at least one LLM provider. OpenHands works with
[Anthropic](https://console.anthropic.com/),
[OpenAI](https://platform.openai.com/), and
[Google Gemini](https://aistudio.google.com/), among others.
- `sbx` CLI installed and authenticated.
- Go 1.23+ (for running TCK tests locally).

## Setup

Register your LLM API key with `sbx secret set-custom`. The command stores the value
in the host secret store and exposes a placeholder inside every sandbox launched from
this kit:

### Anthropic (default)

```console
$ sbx secret set-custom -g \
--host api.anthropic.com \
--env ANTHROPIC_API_KEY \
--placeholder "sk-ant-{rand}" \
--value "$ANTHROPIC_API_KEY"
```

### OpenAI

```console
$ sbx secret set-custom -g \
--host api.openai.com \
--env OPENAI_API_KEY \
--placeholder "sk-{rand}" \
--value "$OPENAI_API_KEY"
```

Then override the model at run time:

```console
$ sbx run --kit "git+https://github.com/docker/sbx-kits-contrib.git#dir=openhands" \
openhands -e LLM_MODEL=openai/gpt-4o
```

### Google Gemini

```console
$ sbx secret set-custom -g \
--host generativelanguage.googleapis.com \
--env GEMINI_API_KEY \
--placeholder "AIza{rand}" \
--value "$GEMINI_API_KEY"
```

Then override the model:

```console
$ sbx run ... openhands -e LLM_MODEL=gemini/gemini-2.5-pro
```

### Optional: Tavily web search

```console
$ sbx secret set-custom -g \
--host api.tavily.com \
--env TAVILY_API_KEY \
--placeholder "tvly-{rand}" \
--value "$TAVILY_API_KEY"
```

> [!NOTE]
> `sbx secret set-custom` is an experimental command. See the
> [amp kit README](../amp/README.md) for background on how it works.

## Usage

```console
$ sbx run --kit "git+https://github.com/docker/sbx-kits-contrib.git#dir=openhands" openhands
```

Or with a local clone:

```console
$ sbx run --kit ./openhands/ openhands
```

The first launch installs OpenHands (takes ~2 minutes; subsequent starts reuse the
sandbox). Subsequent launches reconnect to the existing sandbox and check for
OpenHands updates in the background.

## How auth works

The kit's `network` block maps each LLM provider's domain to a service identity, and
`serviceAuth` tells the proxy which header to inject on outbound requests to that
domain:

| Provider | Domain | Header |
|---|---|---|
| Anthropic | `api.anthropic.com` | `x-api-key: <key>` |
| OpenAI | `api.openai.com` | `Authorization: Bearer <key>` |
| Gemini | `generativelanguage.googleapis.com` | `x-goog-api-key: <key>` |

OpenHands uses [LiteLLM](https://github.com/BerriAI/litellm) for all LLM calls. The
placeholder value (e.g. `sk-ant-<random>`) is what LiteLLM sends in requests; the
proxy replaces it with the real key before the request leaves the sandbox.

`serviceDomains` is kept narrow — only the API endpoints are listed, not CDNs or
install scripts. Widening it to a wildcard would push the proxy into
TLS-intercepting mode for those additional hosts, which breaks binary downloads
during installation.

## How `SANDBOX_TYPE=local` works

By default, OpenHands spawns a Docker container as its code-execution runtime. Inside
a Docker sandbox that would require Docker-in-Docker. Setting `SANDBOX_TYPE=local`
tells OpenHands to execute code directly within the container instead. The SBX
container is already isolated, so this is safe and eliminates the overhead of
a second container layer.

## Switching the default model

The `LLM_MODEL` environment variable (litellm format: `<provider>/<model-id>`) sets
the model. Override it without recreating the sandbox:

```console
$ sbx run openhands -e LLM_MODEL=anthropic/claude-sonnet-4-5
```

## Cleanup

To remove stored secrets:

```console
$ sbx secret rm -g --host api.anthropic.com
$ sbx secret rm -g --host api.openai.com # if set
$ sbx secret rm -g --host generativelanguage.googleapis.com # if set
$ sbx secret rm -g --host api.tavily.com # if set
```

To remove the sandbox:

```console
$ sbx rm openhands
```
18 changes: 18 additions & 0 deletions openhands/files/home/.openhands/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"language": "en",
"agent": "CodeActAgent",
"security_analyzer": null,
"confirmation_mode": false,
"llm_config": {
"model": "anthropic/claude-opus-4-5",
"api_key": "",
"base_url": null,
"num_retries": 4,
"retry_min_wait": 15,
"retry_max_wait": 120
},
"sandbox": {
"type": "local",
"timeout": 120
}
}
14 changes: 14 additions & 0 deletions openhands/openhands_tck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package openhands_test

import (
"testing"

"github.com/docker/sbx-kits-contrib/tck"
"github.com/stretchr/testify/require"
)

func TestOpenHandsTCK(t *testing.T) {
suite, err := tck.NewSuiteFromDir(".")
require.NoError(t, err)
suite.RunAll(t)
}
124 changes: 124 additions & 0 deletions openhands/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
schemaVersion: "1"
kind: agent
name: openhands
displayName: OpenHands
description: >
AI software engineering agent. Resolves issues, writes code, runs tests,
and opens PRs — all autonomously.

agent:
image: "docker/sandbox-templates:shell-docker"
aiFilename: AGENTS.md
persistence: persistent
entrypoint:
run: ["/home/agent/.local/bin/openhands", "--always-approve"]

# Auth is injected by the SBX proxy. Register your key before first run:
#
# sbx secret set-custom -g \
# --host api.anthropic.com \
# --env ANTHROPIC_API_KEY \
# --placeholder "sk-ant-{rand}" \
# --value "$ANTHROPIC_API_KEY"
#
# See README.md for other LLM providers.
network:
serviceDomains:
api.anthropic.com: anthropic
api.openai.com: openai
generativelanguage.googleapis.com: gemini
serviceAuth:
anthropic:
headerName: x-api-key
valueFormat: "%s"
openai:
headerName: Authorization
valueFormat: "Bearer %s"
gemini:
headerName: x-goog-api-key
valueFormat: "%s"
allowedDomains:
# Source control
- "github.com"
- "api.github.com"
- "raw.githubusercontent.com"
- "objects.githubusercontent.com"
# Python packages
- "pypi.org"
- "files.pythonhosted.org"
# uv installer and releases
- "astral.sh"
- "*.astral.sh"
# Node / npm
- "registry.npmjs.org"
# Docker registries
- "registry-1.docker.io"
- "index.docker.io"
- "auth.docker.io"
- "production.cloudflare.docker.com"
- "ghcr.io"
- "docker.openhands.dev"
# Optional web search (requires Tavily key)
- "api.tavily.com"

environment:
variables:
# litellm model string — override with: sbx run openhands -e LLM_MODEL=openai/gpt-4o
LLM_MODEL: "anthropic/claude-opus-4-5"
# Run code directly in this container; no nested Docker needed
SANDBOX_TYPE: "local"
OPENHANDS_SUPPRESS_BANNER: "1"

commands:
install:
- command: "curl -LsSf https://astral.sh/uv/install.sh | sh"
user: "1000"
description: Install uv

- command: "/home/agent/.local/bin/uv tool install openhands"
user: "1000"
description: Install OpenHands CLI

startup:
- command:
- "/home/agent/.local/bin/uv"
- "tool"
- "upgrade"
- "openhands"
- "--quiet"
user: "1000"
background: true
description: Upgrade OpenHands to latest

memory: |
# OpenHands — Sandbox Context

You are running inside a Docker SBX sandbox.

## Environment

| Item | Value |
|------|-------|
| Workspace | `${WORKDIR}` |
| Sandbox mode | local (code runs in this container) |
| Persistence | enabled — workspace survives restarts |
| Confirmation | always-approve mode is active |

## Network access

Outbound requests are filtered. Reachable: GitHub, PyPI, npm, Docker registries,
the configured LLM API (auth injected by proxy). Tavily search is available
if `TAVILY_API_KEY` is set.

## Working conventions

1. Read the repo README and existing tests before making changes.
2. Make the smallest change that satisfies the task.
3. Run the test suite; fix failures before reporting completion.
4. Commit with a clear message and push to the current branch.
5. Use `gh pr create` for pull requests (GitHub CLI is pre-installed).

## Switching LLM providers

Recreate the sandbox with `LLM_MODEL` overridden and the matching provider
secret registered via `sbx secret set-custom`. See the kit README for details.
Loading