Skip to content

feat(workspace): add AgentSandboxWorkspace (Kubernetes, kubernetes-sigs/agent-sandbox) - #4516

Open
aleks-stefanovic wants to merge 8 commits into
OpenHands:mainfrom
volatilemolotov:feat/agent-sandbox-workspace
Open

feat(workspace): add AgentSandboxWorkspace (Kubernetes, kubernetes-sigs/agent-sandbox)#4516
aleks-stefanovic wants to merge 8 commits into
OpenHands:mainfrom
volatilemolotov:feat/agent-sandbox-workspace

Conversation

@aleks-stefanovic

@aleks-stefanovic aleks-stefanovic commented Aug 17, 2026

Copy link
Copy Markdown

HUMAN:

Adds a Kubernetes-backed workspace so teams already running k8s can host agent sandboxes instead of relying on Docker or a hosted runtime. Validated end-to-end on kind and GKE. Reviewed with the agent-sandbox maintainers before opening.


AGENT:

Why

There is currently no way to run the agent server in a Kubernetes-managed sandbox.
The remote workspaces today cover Docker (single host), the hosted runtime API,
OpenHands Cloud, and Apptainer. Teams that already operate Kubernetes therefore
have to either run Docker inside something else or depend on a hosted runtime.

kubernetes-sigs/agent-sandbox
is a Kubernetes SIG-Apps project providing a Sandbox CRD for exactly this shape of
workload: isolated, stateful, single-pod runtimes. Backing a workspace with it gives
capabilities the existing remote backends don't have:

  • Warm pools (SandboxWarmPool): a pre-warmed pod is claimed in about a second,
    instead of paying container cold start per conversation.
  • Native pause/resume: spec.operatingMode: Suspended terminates the pod while
    keeping the PVC, so workspace state survives a suspend. This goes further than
    docker pause.
  • Stronger isolation: gVisor or Kata via runtimeClassName on the template.
  • Runs unchanged from a laptop kind/minikube cluster to a cloud cluster.

The agent server is already just an image on a port, so this needs no changes to the
agent server or to SDK core. It only adds a new workspace implementation.

Summary

  • Add AgentSandboxWorkspace(RemoteWorkspace) in openhands-workspace: claims a pod
    from a SandboxWarmPool, connects over kubectl port-forward (local/kind) or a
    caller-supplied URL (router/gateway/in-cluster DNS), implements pause()/resume()
    via the Sandbox operatingMode, and deletes the SandboxClaim on cleanup.
  • Ship ready-to-apply SandboxTemplate + SandboxWarmPool manifests that run
    ghcr.io/openhands/agent-server, plus README.md, TESTING.md, a runnable example,
    and helper scripts (keyless smoke test, local-Ollama agent run).
  • Add the client as an optional extra (openhands-workspace[agent-sandbox]) so nothing
    changes for existing users; 9 unit tests cover the backend with the client mocked.

Issue Number

Fixes #4519

How to Test

Everything below runs on a local kind cluster and needs no API key.

1. Unit tests (no cluster):

uv run pytest tests/workspace/test_agent_sandbox_workspace.py -v

2. Cluster + workspace smoke test (no LLM). This is the meaningful end-to-end
check: it claims a pod, runs a command in it, suspends and resumes it, and verifies
workspace state survived.

kind create cluster --name ohpr
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v0.5.2/sandbox-with-extensions.yaml
kubectl -n agent-sandbox-system rollout status deploy --timeout=180s

cd openhands-workspace/openhands/workspace/agent_sandbox
kubectl apply -f deploy/sandboxtemplate.yaml -f deploy/sandboxwarmpool.yaml
kubectl get pods -w      # wait for the warm pool pods to be Ready

pip install -e "../../../../openhands-workspace[agent-sandbox]"
python testing/smoke_test.py

3. Full agent run with a local Ollama model (still no API key). See
TESTING.md
section 4a; it deploys Ollama in-cluster and runs testing/agent_ollama_example.py.

Video/Screenshots

Workspace smoke test. It claims a pod, runs a command, pauses, resumes, checks
the state survived, and tears down:

claimed + connected in 6.8s at http://127.0.0.1:30613
command output:
hello-from-pod
openhands
/workspace
aarch64

pause() -> Suspended ...
resume() -> Running ...
state survived suspend/resume: 'persisted-across-suspend\n'
SMOKE TEST PASSED

Full agent run. The agent server runs in the sandbox pod, driven by a local
Ollama model, and creates a file on its own:

status: ConversationExecutionStatus.FINISHED
ActionEvent: tool=terminal command="echo 'hi from the agent' > hello.txt"
ActionEvent: tool=finish message='Command executed successfully.'
RESULT hello.txt (exit=0): 'hi from the agent\n'

The same agent run was also executed on a GKE cluster (in-cluster Ollama) with the
same result, to confirm nothing is kind-specific:

ActionEvent: tool=terminal command="echo 'hi from the agent on GKE' > hello.txt"
RESULT hello.txt (exit=0): 'hi from the agent on GKE\n'

Cleanup / leak check. An earlier revision leaked the claim when the connect phase
failed after create_sandbox() had succeeded. Reproduced it by pointing the workspace
at a dead port so the health check fails, and confirmed the fix:

# before the fix
constructor raised as expected: RuntimeError: Could not reach agent server after 5 attempts
claims after: ['sandboxclaim.extensions.agents.x-k8s.io/sandbox-claim-d27afa5b']   # leaked

# after the fix
constructor raised as expected: RuntimeError: Could not reach agent server after 5 attempts
claims after: []
NO LEAK - claim was cleaned up

Checks: uv run pre-commit run --files <changed files> passes all hooks (yamlfmt,
ruff format/lint, pycodestyle, pyright, import rules, tool registration), and
uv run pytest tests/workspace/ passes (193 tests).

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • No changes to SDK core or the agent server. This is additive: one new workspace
    implementation plus docs/manifests. The only shared-file edits are the
    openhands.workspace export and a new optional extra in pyproject.toml.
  • Optional dependency. k8s-agent-sandbox is only imported inside
    model_post_init, and a missing install raises a message pointing at the extra, so
    users who never touch this backend are unaffected.
  • Environment specifics stay in the workspace layer, per CONTRIBUTING: connection
    mode, namespace, warm pool, and timeouts are explicit constructor params with
    validation; infrastructure concerns (CPU/memory, image, runtimeClassName, network
    policy, volumes) live in the SandboxTemplate rather than in Python.
  • Reviewed upstream first. An agent-sandbox maintainer reviewed this against
    kubernetes-sigs/agent-sandbox main and confirmed the client API usage and the
    pause/resume approach; their review comments are addressed in this branch.
  • Follow-ups (not blocking): agent-sandbox
    #1160 (claim-level
    idle lifecycle) and
    #1296
    (traffic-triggered resume) will make pause/resume a claim-level concern; there is a
    TODO in the code to move to the claim API when those land. The pod-name lookup would
    also be tidier once the client exposes a public refresh.

…agent-sandbox pod

New RemoteWorkspace backend that claims a pod from a SandboxWarmPool, runs the
OpenHands agent server in it, and connects via kubectl port-forward (local/kind)
or a direct URL. Supports native pause/resume through the Sandbox operatingMode
and teardown by deleting the SandboxClaim. Ships example manifests, a runnable
example with a kind walkthrough, and unit tests. Depends on the optional
k8s-agent-sandbox client (openhands-workspace[agent-sandbox]).
Live testing on kind surfaced two resume() failures: the local port was
reused while still in TIME_WAIT, and kubectl port-forward could attach while
the resumed pod's network namespace was still churning. Reconnect now retries
with a fresh local port (_connect_port_forward_with_retry) and rebuilds the
HTTP client, which is also used for the initial connect.
…helper scripts

Documents three test tiers (unit / keyless workspace smoke / full agent e2e with a
local Ollama model, no API key) with the setup that works: relax the sandbox network
policy to reach an in-cluster LLM, use a model that emits structured tool calls
(qwen2.5:7b / llama3.1:8b), reasoning_effort='none', and a minimal terminal-only
agent for small models. Adds testing/smoke_test.py, testing/agent_ollama_example.py,
and testing/ollama.yaml.
…cess

- model_post_init now wraps everything after create_sandbox() in try/except so a
  failed connect/health phase terminates the claim instead of leaking it (the
  constructor raising means the caller never gets an object to close).
- Track the caller's explicit host_port separately so resume() always reconnects
  on a freshly allocated port instead of retrying the previous (TIME_WAIT) one.
- Resolve the pod name via the public client API (k8s_helper.get_sandbox +
  POD_NAME_ANNOTATION) instead of clearing the handle's private _pod_name.
- Note the agent-sandbox OpenHands#1160/OpenHands#1296 follow-up on _patch_operating_mode.
- Pin the agent-server image to 1.38.0-python and ship a commented scoped-egress
  networkPolicy example in the template; point TESTING.md at it.
- Add tests for the claim-cleanup path and the fresh-port reconnect.
…python

Rebased onto v1.42.1: bump the pinned agent-server tag to match the SDK version,
and reformat the manifests with the repo's yamlfmt hook (re-aligning comments so
the commented networkPolicy example still uncomments to valid YAML).
A dead kubectl port-forward exits immediately, so a flat 2s sleep spent the whole
retry budget in ~10s -- shorter than a freshly resumed pod sometimes needs to
become forwardable. Widen to 8 attempts with escalating backoff (~50s).
@all-hands-bot

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

The PR-description check failed before the linked issue was added and passed on
re-run, but the stale failing run is still attached to the previous commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Kubernetes-backed workspace using kubernetes-sigs/agent-sandbox

2 participants