🧊 self-hosted-agent-sandbox
Disposable gVisor sandboxes for AI coding agents, on k3s nodes you own: a shared warm pool, per-node package caches, and git credentials that never enter the sandbox.
English · 简体中文
The agent keeps running where it runs today. Only the commands it executes go into a sandbox: one per session, claimed from a warm pool in about 0.2 s and deleted when the session ends. The task's code is synced both ways, so the host's working tree stays the git source of truth, and git network commands (push, fetch, clone, …) run back on the host, where the credentials are.
Built on k3s, kubernetes-sigs/agent-sandbox and gVisor. It needs no KVM, so ordinary cloud VMs work.
🧩 What is in this repo
| Part | What it is |
|---|---|
cluster/ |
node install with a hard pod memory budget, gVisor, least-privilege RBAC and kubeconfig, per-tenant warm pools with per-node caches, cache GC |
src/self_hosted_agent_sandbox/ |
the core library (Sandbox): claim / renew / release, environment and file sync, two-way code sync with a .git allowlist, host-command routing. Pure Python, no dependencies |
sbx |
a CLI over the core: sbx exec, sbx shell, sbx release |
🔌 Adapters
| Agent / platform | How | Status |
|---|---|---|
| Hermes Agent | terminal backend plugin: hermes-sandbox-cluster | ✅ in use; acceptance suites pass on Hermes 0.21.5 and 0.22.7 |
| Claude Code | built-in Bash/Read/Write/Edit replaced by MCP tools backed by the sandbox | 🛠️ planned |
| Codex CLI | native remote execution environment (codex exec-server in the sandbox), MCP as fallback |
🛠️ planned |
| Multica runtimes | through the agent adapters above; covers assigned issues, webhook autopilots (e.g. on pull_request) and PR wakeups |
🛠️ planned |
| OmniRoute | agent-sandbox skill runtime: execute_command / eval_code claim a pod from the pool (guide) |
🔄 upstream PR #19130 open, not merged; verified on a lab cluster |
Anything else can call the CLI or the library directly.
🚀 Quick start
1. Nodes (on every Linux machine; the script fetches the official k3s installer, or uses $SBX_DL/install-k3s.sh when air-gapped)
# first node (becomes the k3s server)
SBX_NODE_IP=10.0.0.11 cluster/node-install.sh server
sudo cat /var/lib/rancher/k3s/server/node-token # copy this to the other nodes as ./node-token
# every other node
SBX_NODE_IP=10.0.0.12 cluster/node-install.sh agent https://10.0.0.11:6443 ./node-token
# every node
cluster/gvisor-node.sh
cluster/cache-gc-install.sh # optional: daily cache prune
2. Cluster (once, on the first node, where kubectl is k3s kubectl)
kubectl apply --server-side -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v1.0.9/sandbox-with-extensions.yaml
kubectl create namespace sbx
kubectl apply -f cluster/runtimeclass.yaml -f cluster/rbac.yaml
cluster/render-pool.py --tenant team-a --image ghcr.io/you/agent-host:latest \
--cache-mount /var/cache/pkg | kubectl apply -f -
cluster/make-kubeconfig.sh https://10.0.0.11:6443 > agent-sandbox.kubeconfig # copy to the agent host
3. Agent host
pip install self-hosted-agent-sandbox
export AGENT_SANDBOX_KUBECONFIG=$PWD/agent-sandbox.kubeconfig AGENT_SANDBOX_POOL=sbx-team-a
cd /workspaces/my-repo
sbx exec --session demo -- 'cat /proc/version; make test' # runs in a fresh gVisor sandbox
sbx exec --session demo -- git push origin HEAD # runs on this host, with its credentials
--keep keeps the claim, so later sbx exec --session demo --keep calls share one sandbox until sbx release --session demo. kubectl must be on PATH, or set AGENT_SANDBOX_KUBECTL.
From Python:
from self_hosted_agent_sandbox import Sandbox
sb = Sandbox(cwd="/workspaces/my-repo", session="task-42")
print(sb.execute("pytest -q")["output"])
sb.close()
🧭 What runs where
A command runs on the host only when it is listed host commands plus glue that cannot touch files: a leading cd <dir> && (an existing directory reached without symlinks), echo/printf, and filters fed by a pipe (grep, jq, head, tail, sort, uniq, cut, tr, wc) with short options from a fixed list, plus >/dev/null. It must also stay inside the code dirs: the working directory, every cd and every local path in a git command (a clone target, a file:// remote) must resolve inside them, and $VAR, ~ and unquoted globs are not expanded on the host. Anything with a substitution, subshell, heredoc, file redirection, another program, a variable prefix other than on/off switches such as GIT_TRACE=1, or a git option that runs a program (--upload-pack, clone -u/-c/--template, …) stays in the sandbox.
⚙️ Configuration
Environment variables (adapters may also take them from their own config, e.g. Hermes config.yaml as agent_sandbox_*):
| Variable | Default | Purpose |
|---|---|---|
AGENT_SANDBOX_POOL |
agent-4g |
warm pool to claim from |
AGENT_SANDBOX_NAMESPACE |
sbx |
namespace of pool and claims |
AGENT_SANDBOX_KUBECONFIG |
~/.kube/agent-sandbox.yaml |
least-privilege kubeconfig |
AGENT_SANDBOX_KUBECTL |
kubectl |
kubectl binary |
AGENT_SANDBOX_SYNC_PATHS |
– | config files/dirs pushed at the same path |
AGENT_SANDBOX_CODE_DIRS |
task dir | two-way synced code dirs |
AGENT_SANDBOX_HOST_COMMANDS |
git network ops, git remote add/set-url |
command prefixes that run on the host, matched word by word (replaces the default list) |
AGENT_SANDBOX_FORWARD_ENV |
– (nothing) | host variables to forward, by name or glob, e.g. MY_BUILD_FLAG,NPM_CONFIG_REGISTRY. Never list tokens or keys |
AGENT_SANDBOX_ENV_SKIP |
– | names to withhold even when the allowlist matches them (globs like *_TOKEN work) |
AGENT_SANDBOX_TASK_FILES |
– | files in a task root (/workspaces/<ws>/<task>/) pushed with the workdir, never pulled back |
AGENT_SANDBOX_IDLE_TTL |
1800 |
cluster-side expiry in seconds, renewed while active |
AGENT_SANDBOX_PERSISTENT |
false |
reuse one claim per session label |
AGENT_SANDBOX_CLAIM_TIMEOUT |
180 |
seconds to wait for a sandbox before giving up |
🌐 Network allowlist
Each arrow starts at the side that opens the connection.
Agent host → cluster · the only path the library needs
| Port | To | Why |
|---|---|---|
| TCP 6443 | k3s server | Kubernetes API: claim and release sandboxes, and kubectl exec, which is tunnelled through the API server |
Between cluster nodes
| Port | Direction | Why |
|---|---|---|
| TCP 6443 | agents → server | node registration and API |
| TCP 10250 | server → every node | kubelet: exec and logs are proxied to the node running the pod |
| UDP 8472 | every node ↔ every node | flannel VXLAN pod network |
| TCP 2379–2380 | server ↔ server | only for multi-server (embedded etcd HA) |
Sandboxes → outside: open only what your builds need, such as your Maven/npm/PyPI mirrors, a Go proxy, and container registries for the nodes' image pulls.
🔒 Security
- Sandboxes run under gVisor, as a non-root uid, with no service-account token and no service links.
- Credentials stay on the host. The sandbox runs and builds code; it gets no host variable you have not named in
AGENT_SANDBOX_FORWARD_ENV(empty by default), and git network commands that need credentials run on the host. - Git config reaches the sandbox through an exact key allowlist: from the host
~/.gitconfigonly identity and everyday settings (user.name,user.email,init.defaultBranch,pull.rebase,push.default, line-ending and file-mode options); from each repository's.git/configandconfig.worktreein the code dirs also what git needs there (format, branches, remote URLs and fetch refspecs). Aliases,core.sshCommand,core.askpass, hooks paths, credential helpers, includes,http.*andurl.*never go in, and any value withuser:pass@is dropped. On a reused sandbox the copy is replaced, never kept; if a file cannot be parsed the sandbox gets an empty one. - Other files in the code dirs, in the sync paths and in any seed directories an adapter passes are sent as they are: keep tokens out of the working tree (no
.envwith real keys, no.npmrcwith an auth token there). - Pull-back into
.gitis an allowlist (objects, refs, logs,HEAD,index,packed-refs, …). Config, hooks, gitfiles and everything else stay as the host has them, host-side git runs withsafe.bareRepository=explicit, and writes through host symlinks that lead out of the code dirs are refused, so nothing built in the sandbox can plant a hook that a later host-sidegit pushruns. - Package caches are per tenant, so one tenant cannot poison another tenant's cache.
- The agent host's service account may only manage
sandboxclaimsand exec into pods in one namespace. - Host commands run with the host process's own
PATH; the router checks the command text, not whichgitbinary thatPATHresolves to, so keep the hostPATHfree of directories the agent can write.
🧪 Tests
tests/route_table.py(fixed cases) andtests/route_fuzz.py(random commands run through real bash, asserting nothing is written outside the code dir) check host-command routing offline:python tests/route_table.py.tests/core_unit.pychecks error handling, streaming output,release, the variable allowlist (empty by default: a host API key does not reach the sandbox; dropped names are unset) and the git config allowlists (host and repository) offline with a fakekubectl.tests/e2e.shdrives thesbxCLI against a real cluster: gVisor, variable forwarding, pull-back of files and commits, host routing, claim reuse and release, exit codes.- Adapter repos carry their own acceptance suites (for Hermes: 26 + 15 end-to-end checks).
🔍 What it touches
- Runs
kubectl(the binary you point it at) as a subprocess, with the kubeconfig you name, against that kubeconfig's API server only. - Sends into the sandbox: the host variables named in
AGENT_SANDBOX_FORWARD_ENV(none by default), the files inAGENT_SANDBOX_SYNC_PATHSandAGENT_SANDBOX_TASK_FILES, the code directories, any seed directories an adapter passes (e.g. a read-only skills copy) and the allowlisted part of the host git config. - Writes on the host: only inside the code directories, with files the sandbox created, changed or deleted there (in
.gitonly history and index; never through symlinks that leave the code dirs). - No other network calls, no telemetry, no stored credentials, no background process of its own.
🛣️ Status
Early. The core was extracted from the Hermes plugin, where it went through nine rounds of independent release QA. Linux x86_64 nodes, k3s v1.37.
📄 License
Metadata
Release files for self-hosted-agent-sandbox 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| self_hosted_agent_sandbox-0.3.0.tar.gz | 52.8 kB | With provenance | Details for self_hosted_agent_sandbox-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| self_hosted_agent_sandbox-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for self_hosted_agent_sandbox-0.3.0-py3-none-any.whl |
Total release size: 83.0 kB
Release files / self_hosted_agent_sandbox-0.3.0.tar.gz
| Download URL | self_hosted_agent_sandbox-0.3.0.tar.gz |
|---|---|
| Size | 52.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ef86043d52c5091d5636eded0445e689e455d19fa4b8a4fb9624c1cd0f85724
|
|
BLAKE2b-256 checksum How to use checksums |
4fe468567e9adb0fddf9f0f4beeefb53688bf51401fa6f8e549414bf769fa688
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / self_hosted_agent_sandbox-0.3.0-py3-none-any.whl
| Download URL | self_hosted_agent_sandbox-0.3.0-py3-none-any.whl |
|---|---|
| Size | 30.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
808a921c848b2efef81ca15bce9fa42ff819dac284a62db467c947ef03391a97
|
|
BLAKE2b-256 checksum How to use checksums |
59341bed557895095d06add995aa2472a5e98878b1738909c4beb10625943a7a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for self-hosted-agent-sandbox 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for self_hosted_agent_sandbox-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.