diff --git a/openhands/README.md b/openhands/README.md new file mode 100644 index 0000000..342e3bf --- /dev/null +++ b/openhands/README.md @@ -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: ` | +| OpenAI | `api.openai.com` | `Authorization: Bearer ` | +| Gemini | `generativelanguage.googleapis.com` | `x-goog-api-key: ` | + +OpenHands uses [LiteLLM](https://github.com/BerriAI/litellm) for all LLM calls. The +placeholder value (e.g. `sk-ant-`) 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: `/`) 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 +``` diff --git a/openhands/files/home/.openhands/settings.json b/openhands/files/home/.openhands/settings.json new file mode 100644 index 0000000..b293028 --- /dev/null +++ b/openhands/files/home/.openhands/settings.json @@ -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 + } +} diff --git a/openhands/openhands_tck_test.go b/openhands/openhands_tck_test.go new file mode 100644 index 0000000..470cb6c --- /dev/null +++ b/openhands/openhands_tck_test.go @@ -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) +} diff --git a/openhands/spec.yaml b/openhands/spec.yaml new file mode 100644 index 0000000..6be1014 --- /dev/null +++ b/openhands/spec.yaml @@ -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.