A self-hostable, Kubernetes-native sandbox for agent code execution — pod-per-session isolation plus a complete bash/python/file tool surface.
Project description
boxkite
The missing batteries-included, self-hostable sandbox for agent code execution.
Most "agent sandbox" projects give you raw isolation — a pod, a VM, a
container — and leave you to build the actual tool surface an LLM agent
needs on top of it. boxkite is the other half: a complete
bash/python/file/search/process tool surface (14 ready-to-wire,
framework-agnostic tools — LangChain, LangGraph, CrewAI, AutoGen, or plain
OpenAI-style function calling — plus an opt-in git tool set of 8 more)
running inside real Kubernetes pod isolation, built on industry-standard hardening
practices — non-root execution, all Linux capabilities dropped, a read-only
root filesystem, network egress denied by default, and output scrubbed for
leaked API keys and cloud credentials before it ever reaches your agent.
boxkite itself is early-stage (v0.1.0) — the isolation approach is proven,
the project's production track record is not, yet.
Self-host it, point your agent framework at it, and you have a real sandbox in minutes instead of weeks.
Who this is for: boxkite is a self-hostable sandbox for teams building
their own agent products that need isolated, multi-tenant code execution at
scale — one Kubernetes pod per session, many sessions, many tenants. It is
not a single-user local dev-session sandbox like the built-in bash tool
in an IDE or CLI coding agent — if you just want your own coding assistant to
run shell commands on your own machine, boxkite is the wrong layer; it's for
when you are the one operating the sandbox infrastructure behind an agent
product other people use.
Contents
- Why this exists
- What's actually in here
- The isolation model
- Security
- Self-hosting
- The
boxkiteCLI - Cookbook / examples
- Docs map
- Extending it: AuditSink and SessionMetadataStore
- What's not included
- License
- Contributing
Why this exists
Raw pod-per-session isolation is no longer whitespace on its own —
kubernetes-sigs/agent-sandbox
(a Kubernetes SIG-Apps project) already does that well, and GKE's managed
version of it is faster to cold-start than this project's warm-pool claim.
boxkite is not a competitor to that primitive — it's designed to sit
compatibly alongside it, and a boxkite-style tool layer running on top of
agent-sandbox's pod lifecycle is a reasonable future direction.
What boxkite fills in is the layer above raw isolation that, as of this writing, nothing else ships as a complete, self-hostable package:
- Daytona was, in our assessment, the most credible open self-hosted alternative here, with a large GitHub star count. As of this writing (mid-2026) we believe it has privatized its core and that its OSS repo is no longer actively maintained — verify current licensing and repo activity directly on Daytona's GitHub page and website before relying on this, as we have not linked a snapshot/citation here and project status can change.
- LangChain's own native sandbox integration appears, as of this writing, to have been archived, with users redirected to hosted-only services — check LangChain's repo/changelog directly for the current status rather than treating this as a permanent fact.
- E2B, Modal, Riza and similar are excellent, but hosted-first — self-hosting isn't really the point of their product.
boxkite's pitch is narrow and specific: if you want the whole bash/python/file tool surface, hardened and wired to a real K8s pod, running entirely on infrastructure you control — this is the reference implementation to start from, not a novel isolation primitive.
| boxkite | Daytona | E2B | Modal | |
|---|---|---|---|---|
| Self-hostable | Yes | No — core reportedly privatized mid-2026, OSS repo believed unmaintained (verify current status) | No — hosted-first | No — hosted-first |
| Batteries-included, framework-agnostic agent tool surface (bash/python/file tools, no LangChain dependency required) | Yes | Not the product focus | Not the product focus | Not the product focus |
| Isolation primitive | Kubernetes pod (one per session) | Unmaintained as OSS | Own hosted sandbox runtime1 | Own hosted sandbox runtime1 |
| Open source | Source-available (FSL-1.1, converts to Apache-2.0 after 2 years — see License) | No longer — core is privatized | No | No |
What's actually in here
This is one repo with several independently-versioned pieces in it, kept together deliberately (see CONTRIBUTING.md for why) rather than split across repos. If you only want to self-host the sandbox, the Core section below is the entire surface you need — everything else is optional, and mostly exists to talk to a hosted control-plane instead of embedding the library directly.
Core — embed the sandbox directly against your own Kubernetes cluster:
src/boxkite/— the Python package (pip install boxkite-sandbox):SandboxManager(K8s pod lifecycle + HTTP routing to the sidecar),WarmPoolManager(a pre-warmed pod pool so session start is a claim, not a cold boot),LazySandboxRuntime(defers pod creation until a tool call actually needs one), andboxkite.tools— fifteen framework-agnostic tools (LangChainBaseTool, LlamaIndexFunctionTool, OpenAI-style function-calling schema, or plain callables for CrewAI/AutoGen/anything else — seeboxkite.tools.adapters):bash_tool,python_interpreter,file_create,view,str_replace,present_files,ls,glob,grep,start_process,get_process_output,send_process_input,stop_process,list_processes,watch_directory(a long-poll filesystem watcher, seedocs/FILE-WATCHER-DESIGN.md). Tested by the roottests/. An optional, opt-in sixteenth-through-twenty-third set (create_sandbox_tools(enable_git_tools=True)) addsgit_clone/git_status/git_add/git_commit/git_push/git_pull/git_branch/git_checkout— seedocs/GIT-OPERATIONS-DESIGN.mdandsrc/boxkite/tools/git_tools.pyfor the credential-handling and network-egress caveats before enabling it.sidecar/main.py— the FastAPI service that runs alongside the sandbox container in every pod. It owns the actual filesystem I/O, runs agent commands viansenterinto the sandbox's namespace (dropping to a non-root UID before exec), and syncs files to S3 or Azure Blob storage.deploy/— Kubernetes manifests (pod-template.yaml,rbac.yaml,network-policy.yaml), the two container images (sandbox.Dockerfile,sidecar.Dockerfile), adocker-compose.ymlfor local dev without a cluster, anddeploy/local-kind/for a real (if small) Kubernetes dev loop on your laptop.
Optional — run or talk to a hosted control-plane instead of embedding
SandboxManager yourself:
control-plane/— a separate FastAPI service (ownpyproject.toml, ownsrc/control_plane, owntests/) that puts signup/API-key auth, multi-tenant accounts, and fair-use rate limits in front ofSandboxManager. Only needed if you want a hosted-API surface rather than embedding the library directly. Seedocs/API.md.sdk-python/(boxkite-clienton PyPI) andsdk-js/(boxkite-clienton npm) — thin HTTP clients for someone's running control-plane (yours or a hosted one), not forsrc/boxkiteitself.mcp-server/(boxkite-mcp) — wrapssdk-pythonas an MCP tool source, for pointing Claude Code, Claude Desktop, Cursor, or any other MCP client at a control-plane directly.
Everything else:
site/— the marketing site and/developersdocs (Next.js). Not part of the published packages; deployed separately.docs/— API reference, configuration reference, benchmarks, and the extension-points guide (linked from the Docs map below).examples/— the runnable cookbook (LangGraph, LangChain, raw HTTP, hosted control-plane).scripts/— one-off maintenance/benchmark scripts, not part of any published package.tests/— tests for the rootsrc/boxkitepackage specifically; each ofcontrol-plane/,sdk-python/,sdk-js/, andmcp-server/has its owntests/alongside its own source, run independently (see CONTRIBUTING.md).
The isolation model
Every session is one Kubernetes pod with two containers sharing volumes:
sandbox container |
sidecar container |
|
|---|---|---|
| Runs | Agent-generated code | The HTTP API + storage sync |
| User | Non-root (UID 1001), runAsNonRoot: true |
Root (needed for nsenter) |
| Capabilities | All dropped (capabilities.drop: [ALL]) |
Only SYS_PTRACE/SYS_ADMIN, needed for nsenter — never inherited by agent code |
| Filesystem | Read-only root filesystem | Read-write (owns the shared volumes) |
| Network | Denied by default (NetworkPolicy + a fresh network namespace per exec) |
Only what storage sync needs |
| Privilege escalation | allowPrivilegeEscalation: false |
Same |
The sidecar drops to the sandbox's UID/GID via nsenter --setuid --setgid
before executing any agent command — the elevated capabilities it needs
for nsenter itself are never available to the code it runs. Command
output is scrubbed for AWS/Azure keys, JWTs, and generic secret-shaped
strings before it's returned (src/boxkite/tools/bash_tool.py). Sensitive
host paths (~/.ssh, ~/.aws, ~/.kube, the Docker socket) are outside
every mount the sandbox container can see.
See deploy/network-policy.yaml and deploy/rbac.yaml for the actual
manifests — they're written as real starting points with inline "change
this for your cluster" comments, not illustrative snippets.
Security
boxkite executes arbitrary, agent-generated code, so its security posture is layered defense in depth — no single control below is sufficient on its own.
- Sidecar HTTP API authentication. The sidecar (
sidecar/main.py) has no authentication of its own beyond a per-pod shared secret (SIDECAR_AUTH_TOKEN, sent as theX-Sidecar-Auth-Tokenheader on every route except/health).SandboxManager/WarmPoolManager(src/boxkite/sidecar_auth.py) generate this fresh per pod at pod-creation time — never a static, repo-wide value — and store it in a dedicated per-pod Kubernetes Secret (referenced by the pod's env var viasecretKeyRef, never a literal value) so any manager process can recover it later via a separatesecrets: getRBAC grant, not merely thepods: get/listthe manager already needs for routine lifecycle management. The sidecar fails closed: ifSIDECAR_AUTH_TOKENisn't set, every protected route returns 503 rather than silently running unauthenticated. This exists in addition to NetworkPolicy, not instead of it — NetworkPolicy enforcement is CNI-dependent (GKE Autopilot without Dataplane V2, EKS's default VPC CNI, anddeploy/local-kind/all ship without an enforced NetworkPolicy), and even where enforced, a broad egress rule on the pod also governs what can reach the sidecar's own ingress, since both containers in a pod share one network namespace. - Per-exec network isolation. Every
/execcall in K8s runtime mode runs inside a freshly created, empty network namespace (unshare -nbeforensenter— seebuild_k8s_exec_commandinsidecar/main.py), controlled bySANDBOX_EXEC_NETWORK_ISOLATION_ENABLED(defaulttrue). This is the actual network isolation boundary for agent-executed commands — notdeploy/network-policy.yamlalone. If you set that flag tofalse, the NetworkPolicy becomes the only backstop against exec'd commands reaching the network, so make sure it's genuinely restrictive (see the next point) before doing so. - NetworkPolicy egress must be scoped to your actual storage endpoint,
not left as a blanket allow.
deploy/network-policy.yamlships with a placeholder you must fill in (in-cluster storage via pod/namespace selector, or your cloud provider's storage CIDR/FQDN) — read the comments in that file before applying it. A separate, explicitly-namedsandbox-network-policy-permissivepolicy — kept in its own file,deploy/network-policy-permissive-optin.yaml, specifically so it can't be applied as a side effect ofkubectl apply -f deploy/network-policy.yaml— is available as an opt-in for self-hosters who want fully open egress instead; it is never applied by default and must not be layered on top of the scoped policy (overlapping NetworkPolicy selectors are additive, so applying both silently reintroduces the wide-open behavior). - The sidecar holds CAP_SYS_ADMIN for the pod's entire lifetime. The
nsenter-based isolation design requires it (
setns()needsCAP_SYS_ADMIN, not justCAP_SYS_PTRACE) — see the security comment on the sidecar container indeploy/pod-template.yamlandsrc/boxkite/manager.py. This is a near-root capability: any future RCE-class bug in the sidecar's own FastAPI request handling would be equivalent to fullCAP_SYS_ADMINroot within the pod. This is inherent to the current design, not a misconfiguration, which is exactly why the two layers above (sidecar auth, NetworkPolicy) matter — they're what stands between "reachable" and "exploitable." - Command output redaction is a cosmetic backstop, not a security
boundary.
src/boxkite/tools/bash_tool.py's regex-based scrubbing catches accidentally-echoed labeled secrets (api_key=...,password: ..., etc.) in command output before it reaches the calling agent. It cannot catch a deliberately evasive agent (unlabeled values, encoding, splitting a secret across multiple tool calls). Do not rely on it to protect a secret you've decided to expose to agent-visible command execution — see the next point instead. - No credential injection into agent-visible
/execcalls. The sidecar's/execendpoint does not accept caller-supplied environment variables. If your integration needs a credentialed operation performed on behalf of a sandbox session, do it as a server-side broker call the manager makes on the sandbox's behalf — never by handing real secret values to an environment that LLM-generated shell commands can read.
See SECURITY.md for the full reporting scope and, importantly,
its "Known follow-ups" section — a list of specific gaps that exist today and
aren't fixed yet (no checksum verification on two Dockerfile downloads,
compose-mode's weaker per-exec network isolation compared to K8s, and the
inherent limits of command-name allowlisting). We'd rather you read that
list before deploying than discover one of them yourself. Manager-to-sidecar
traffic is TLS by default now (a fresh, short-lived, self-signed cert
per pod, pinned by the manager — see docs/SIDECAR-TRANSPORT-TLS-DESIGN.md
and SIDECAR_TLS_DISABLED in docs/CONFIGURATION.md for the escape hatch).
Self-hosting
boxkite is self-host-only today. The control-plane/ hosted multi-tenant API
(accounts, API keys, authenticated sandbox exec) lives in this repo and has
its own test suite, but there is no publicly running boxkite-hosted service
to sign up for — boxkite signup and "Hosted mode" below assume you've
deployed control-plane/ yourself. Everything in this section runs entirely
on infrastructure you control, whether that's docker-compose on your laptop
or a real Kubernetes cluster.
Quickstart: docker-compose, no Kubernetes needed
Before you run this: docker-compose mode is single-developer local dev only, never production or multi-tenant, and the reason is a real, currently-unmitigated CRITICAL finding, not boilerplate caution.
deploy/docker-compose.ymlbind-mounts the host's/var/run/docker.sockinto the sidecar container so it candocker execinto the sandbox container. Anyone with a live connection to that socket can trivially escalate to full host-root compromise (e.g.docker run --privileged -v /:/host ...) — this was verified directly, including that the standarddocker-socket-proxymitigation does not close it. The only thing standing between sandboxed code and this socket isSIDECAR_AUTH_TOKENgating the sidecar's own HTTP API; any future auth-bypass, path-traversal, or command-injection bug escalates straight to host compromise, not just container compromise. The sidecar itself logs a loud startup warning when it detects the socket mounted, as a last-resort reminder. See SECURITY.md's "Known follow-ups" section for the full writeup. This does not exist in the Kubernetes runtime at all (no docker socket, no docker-in-docker) — if you want the real isolation model from the start, or anything beyond a single local developer, skip ahead to Quickstart: real Kubernetes, via kind instead.
The fastest path for pure local iteration is the boxkite CLI (pip install -e . in this repo, or
pip install boxkite-sandbox once published — see "The boxkite CLI" below
for the full command reference).
Note: the PyPI package name is
boxkite-sandbox, notboxkite— the plainboxkitename was already taken on PyPI. This only affects thepip installcommand; the Python import path is unchanged (import boxkite), and so is theboxkiteCLI command. Same split asbeautifulsoup4(PyPI name) vs.bs4(import name) —pip install boxkitewill either fail or install an unrelated package, so make sure to installboxkite-sandboxinstead.
git clone https://github.com/HarshitKmr10/boxkite.git boxkite && cd boxkite
# NOT "pip install boxkite" — that's a different, unrelated package on PyPI.
pip install -e .
boxkite up
boxkite exec "python3 -c 'print(1 + 1)'"
boxkite files create hello.txt --content "hello from boxkite\n"
boxkite files view hello.txt
First-run build time: until pre-built images are published to GHCR,
boxkite upbuilds thesandboximage from scratch, which includes downloading a pinned Chrome-for-Testing build (~300MB across the browser and headless-shell zips) plus FFmpeg. On a fast connection with a warm Docker cache this typically takes several minutes, not seconds —docker pswill show no running containers until the build finishes. Subsequent runs reuse the cached image layers and start in seconds. The "minutes instead of weeks" framing above is about the infrastructure work you skip (writing your own isolation, hardening, and tool surface), not about this first-run image build.
boxkite up builds and starts the sandbox container, the sidecar HTTP
API, and a local MinIO for S3-compatible storage — generating a fresh
SIDECAR_AUTH_TOKEN and storing it in ~/.boxkite/local.env so boxkite exec/boxkite files pick it up automatically. No more manually exporting
SIDECAR_AUTH_TOKEN yourself.
Or do it manually, without the CLI (raw HTTP contract)
git clone https://github.com/HarshitKmr10/boxkite.git boxkite && cd boxkite
cp .env.example .env
# Generate a secret for the sidecar's HTTP API and put it in .env — see the
# "Security" section above for why this is required, not optional.
echo "SIDECAR_AUTH_TOKEN=$(openssl rand -hex 32)" >> .env
docker compose -f deploy/docker-compose.yml up -d --build
This builds and starts the sandbox container, the sidecar HTTP API, and
a local MinIO for S3-compatible storage. Check it's healthy:
curl http://localhost:8080/health
Exercise it directly, before wiring up an agent framework (every route
except /health requires the X-Sidecar-Auth-Token header):
export SIDECAR_AUTH_TOKEN=$(grep ^SIDECAR_AUTH_TOKEN= .env | cut -d= -f2)
curl -X POST http://localhost:8080/exec \
-H "Content-Type: application/json" -H "X-Sidecar-Auth-Token: $SIDECAR_AUTH_TOKEN" \
-d '{"command": "python3 -c \"print(1 + 1)\""}'
curl -X POST http://localhost:8080/file-create \
-H "Content-Type: application/json" -H "X-Sidecar-Auth-Token: $SIDECAR_AUTH_TOKEN" \
-d '{"path": "hello.txt", "content": "hello from boxkite\n"}'
curl -X POST http://localhost:8080/view \
-H "Content-Type: application/json" -H "X-Sidecar-Auth-Token: $SIDECAR_AUTH_TOKEN" \
-d '{"path": "hello.txt"}'
Then, from Python, wire the sandbox tools into an agent. SandboxManager
picks up compose vs. Kubernetes mode from the environment, so point it at
the sidecar you just started (with the same token boxkite up generated,
or the one you put in .env manually):
export RUNTIME_MODE=compose SIDECAR_URL=http://localhost:8080
# If you used `boxkite up`, the token is in ~/.boxkite/local.env:
export SIDECAR_AUTH_TOKEN=$(grep ^SIDECAR_AUTH_TOKEN= ~/.boxkite/local.env | cut -d= -f2)
# If you started compose manually, it's in .env instead:
# export SIDECAR_AUTH_TOKEN=$(grep ^SIDECAR_AUTH_TOKEN= .env | cut -d= -f2)
boxkite's tool surface (bash_tool, python_interpreter, file_create,
view, str_replace, present_files, ls, glob, grep,
start_process, get_process_output, send_process_input, stop_process,
list_processes, plus an opt-in git tool set) is framework-agnostic by
default —
create_sandbox_tool_specs() returns plain ToolSpecs (a name,
description, JSON-schema parameters, and a normal async callable) with no
LangChain/LangGraph dependency at all:
from uuid import uuid4
from boxkite import SandboxManager
from boxkite.tools import create_sandbox_tool_specs
manager = SandboxManager()
session_id = str(uuid4())
await manager.create_session(organization_id=uuid4(), session_id=session_id)
specs = create_sandbox_tool_specs(sandbox_manager=manager, session_id=session_id)
# specs = [ToolSpec(name="bash_tool", ...), ToolSpec(name="file_create", ...), ...]
bash_tool = next(s for s in specs if s.name == "bash_tool")
result = await bash_tool.handler(command="echo hello from boxkite")
# Call handler(**kwargs) directly, wire specs into CrewAI/AutoGen/a hand-rolled
# tool-calling loop, or convert the whole list with an adapter:
# boxkite.tools.adapters.to_openai_functions(specs) -- OpenAI-style
# function-calling schema, stdlib only, no extra dependency.
Prefer LangChain or LangGraph? boxkite.tools.adapters.to_langchain_tools
converts the same specs into LangChain BaseTool objects (requires the
langchain extra: pip install boxkite-sandbox[langchain]) — or use the
backward-compatible create_sandbox_tools(), which does exactly that
internally:
from boxkite.tools import create_sandbox_tools
tools = create_sandbox_tools(sandbox_manager=manager, session_id=session_id)
# tools = [bash_tool, python_interpreter, file_create, view, str_replace,
# present_files, ls, glob, grep, start_process, get_process_output,
# send_process_input, stop_process, list_processes]
# Hand these to any LangChain/LangGraph agent. Pass enable_git_tools=True
# to additionally include the opt-in git tool set.
Prefer LlamaIndex? boxkite.tools.adapters.to_llamaindex_tools converts
the same specs into LlamaIndex FunctionTool objects (requires the
llamaindex extra: pip install boxkite-sandbox[llamaindex]), ready to
hand to a ReActAgent/FunctionAgent — see examples/llamaindex_agent/.
The full integration surface, in one place (this used to be scattered
across the codebase with no single pointer — see
docs/E2B-COMPARISON.md §4 for the competitive context):
| Framework/provider | How |
|---|---|
| LangChain / LangGraph | to_langchain_tools() (langchain extra) |
| LlamaIndex | to_llamaindex_tools() (llamaindex extra) — see examples/llamaindex_agent/ |
| OpenAI Agents SDK | to_openai_agents_tools() (openai-agents extra) — see examples/openai_agents_sdk/ |
| CrewAI, AutoGen/AG2, hand-rolled loops | Plain ToolSpec.handler(**kwargs) — no adapter needed |
| OpenAI function calling | to_openai_functions() — stdlib, no openai dependency — see examples/openai_function_calling/ |
| Anthropic tool use | Same ToolSpec.parameters/handler shape as OpenAI above |
| Google Gemini | Same shape, unwrapped into Gemini's FunctionDeclaration — see examples/gemini_function_calling/ |
| Mistral | Same shape, Mistral's own tool-calling API — see examples/mistral_function_calling/ |
| Groq | Same shape, genuinely OpenAI-compatible — see examples/groq_function_calling/ |
| Vercel AI SDK (JS) | sdk-js's createSandboxTools() from boxkite-client/vercel-ai (ai v5 extra) |
| MCP (Claude Desktop, Claude Code, Cursor) | mcp-server/ (boxkite-mcp) — a standalone MCP server over the hosted control-plane, zero custom integration code |
| Claude Code CLI, headless, inside a sandbox | docs/CLAUDE-CODE-SANDBOX-QUICKSTART.md + examples/claude_code_sandbox/, or examples/claude_code_declarative_builder/ for the hosted-control-plane API path instead of a hand-maintained Dockerfile |
See examples/ for a runnable version of every row above.
Quickstart: real Kubernetes, via kind
./deploy/local-kind/setup.sh
kubectl proxy --context kind-boxkite-dev --reject-paths='' &
export RUNTIME_MODE=k8s SANDBOX_USE_K8S_PROXY=true \
SANDBOX_IMAGE=boxkite-sandbox:local SIDECAR_IMAGE=boxkite-sidecar:local
See deploy/local-kind/README.md for the
full walkthrough, verification steps, and teardown.
Known limitation — Apple Silicon / arm64:
setup.shbuilds theboxkite-sandboximage locally, and that build intentionally fails on arm64 hosts (including Apple Silicon Macs). This is a deliberate security control, not a bug: the sandbox's bundled Chromium is replaced with a pinned Chrome-for-Testing build to clear known-vulnerability-scanner findings, and Chrome for Testing does not publish a Linux arm64 build for any version — so there's no pin that would fix this, and silently falling back to the older bundled Chromium would reintroduce the vulnerability the pin exists to close. Seedeploy/local-kind/README.md's "Known limitations" for the full explanation and workarounds (build on an amd64 CI runner/cloud host and push to a registry, or develop on an actual amd64 machine).
For a real (non-kind) cluster, apply deploy/rbac.yaml,
deploy/network-policy.yaml, and deploy/pod-security-policy.yaml (edit
the namespace and service account references first), build and push the
two images from deploy/, and point SANDBOX_IMAGE/SIDECAR_IMAGE at
your registry. deploy/pod-security-policy.yaml is the cluster-level
backstop for deploy/rbac.yaml's own disclosed limitation (RBAC can't
scope pod/secret verbs to sandbox-labeled resources only) -- apply it even
if you also follow the dedicated-namespace mitigation deploy/rbac.yaml
recommends, since that mitigation is a deployment convention, not something
Kubernetes enforces on its own.
Pre-built images: once the first GitHub Release is cut, this repo's
release automation (.github/workflows/publish-images.yml) publishes
container images to GHCR:
ghcr.io/harshitkmr10/boxkite-sandboxghcr.io/harshitkmr10/boxkite-sidecarghcr.io/harshitkmr10/boxkite-control-plane
As of now, no release has been cut yet, so none of these images exist in
the registry yet — until then, build them yourself from deploy/ (and
control-plane/Dockerfile for the control-plane image) as described above.
The boxkite CLI
boxkite up/boxkite exec above are two of the CLI's commands (also:
config, session, files, keys, whoami, log, watch, allowlist,
signup). The CLI works in two modes, auto-detected from what's configured:
- Local mode — nothing configured beyond
boxkite up. Every command talks directly to the single docker-compose sidecar thatboxkite upstarted, using the token it wrote to~/.boxkite/local.env. - Hosted mode — once
boxkite config set-url/set-key(orboxkite signup) has stored a control-plane URL and API key in~/.boxkite/config.toml, every command switches to calling thatcontrol-plane/API instead, authenticated asAuthorization: Bearer <api-key>. As covered in "Self-hosting" above, that control-plane is something you deploy yourself —boxkite signuptalks to whatever URL you've pointed it at, not a boxkite-run service.
# Local mode
boxkite up # start docker-compose (sandbox + sidecar + MinIO)
boxkite exec "ls /workspace" # runs against the local sidecar
boxkite files view hello.txt
boxkite files create notes.txt --content "hi"
boxkite files edit notes.txt --old "hi" --new "hello"
# Hosted mode (against a control-plane you've deployed)
boxkite signup # signup -> login token -> create API key, in one step
# or: boxkite config set-url https://your-control-plane.example.com
# boxkite config set-key bxk_live_...
boxkite session create --label "demo" # defaults to size=small, one sandbox
boxkite session create --label "build" --size medium \
--storage-gb 20 --lifetime-minutes 120 --count 3 # optional overrides, all fair-use bounded
boxkite session ls
boxkite exec "python3 -c 'print(1 + 1)'" # auto-picks the session if exactly one is active
boxkite exec "ls /workspace" --session <id> # or pick one explicitly
boxkite session rm <id>
boxkite log # paginated exec/file-op audit history for the active session
boxkite log --limit 20 --offset 0 --session <id>
boxkite watch # live feed of new exec/file-op entries; blocks until Ctrl-C
boxkite allowlist get # unrestricted by default for every account
boxkite allowlist set ./allowed-commands.json
boxkite allowlist clear # back to unrestricted
--size is a CPU/memory preset (small by default, or medium/large);
--storage-gb and --lifetime-minutes override the sandbox's volume size
and how long it stays alive before the reaper tears it down; --count
creates a batch of sandboxes in one call (default 1). All four are
optional and capped by fair-use ceilings on your account, never a paid upgrade.
boxkite allowlist (hosted mode only) lets an account persist an opt-in
command allowlist enforced on every future boxkite exec call: get shows
the current rules (empty means unrestricted, the default for every
account), set <path-to-json-file> replaces them wholesale from a JSON
array of plain command-name strings or {command, args_allow?, args_deny?}
objects, and clear restores the unrestricted default. This is a guardrail
against accidental/unexpected commands, not a sandbox-escape boundary —
allowing a general-purpose interpreter (python3, bash, node) through
the allowlist still permits arbitrary code to run once it starts; the
sandbox pod's own isolation (see "The isolation model" above) is what
actually constrains what that code can do. See
docs/API.md for the underlying
/v1/account/allowed-commands endpoints.
boxkite log/boxkite watch (hosted mode only) read the same
GET .../log/GET .../watch audit-trail routes the SDKs' get_log/watch
methods call — see docs/SANDBOX-OBSERVABILITY-DESIGN.md and the
"Audit log & takeover" developer guide for what's actually recorded. Like
exec/files, both auto-pick the active session if exactly one exists, or
take an explicit --session.
Hosted mode is session-scoped because the control-plane manages multiple
concurrent sandboxes per account; local mode is intentionally not — a
single boxkite up stack has no session concept of its own, so boxkite session */boxkite log/boxkite watch refuse to run against it with an
explanation rather than inventing session management that doesn't exist
locally. Run boxkite --help (or --help on any subcommand) for the full
reference.
Cookbook / examples
examples/ has runnable, verified examples: a full
LangGraph agent wired to all 5 sandbox tools, a minimal single-tool
LangChain agent, native OpenAI function-calling with no agent framework at
all, a LlamaIndex ReActAgent, Claude Code running headlessly inside a
sandbox, plain curl/Python-requests scripts against the sidecar's raw HTTP
API, and a walkthrough of the hosted control-plane API (signup -> sandbox
-> exec -> teardown). Start there if you want working code to copy from
rather than piecing it together from this README.
Docs map
This README covers the self-hosted boxkite package end to end. For
everything else, start here:
docs/API.md— the control-plane's REST API (hosted mode): signup/login, API-key management, and sandbox-session lifecycle.docs/EXTENDING.md— the two optional extension points (AuditSink,SessionMetadataStore) in more depth than the summary below.docs/CONFIGURATION.md— every environment variable and config option across the sidecar, manager, and control-plane.docs/BENCHMARKS.md— cold-start/warm-pool latency and throughput numbers, and how to reproduce them.mcp-server/README.md— the MCP server that exposes a hosted control-plane as a native tool source for Claude Code, Claude Desktop, Cursor, and other MCP clients.sdk-python/README.md— the Python client for a hosted control-plane (boxkite-client): sessions, exec, files, LangChain tools, over HTTP.sdk-js/README.md— the TypeScript/JavaScript client for a hosted control-plane, mirroring the Python SDK for Node and browser, plus a Vercel AI SDK tool factory.docs/CLAUDE-CODE-SANDBOX-QUICKSTART.md— running the Claude Code CLI headlessly inside a boxkite sandbox.docs/E2B-COMPARISON.md— a research/scoping gap analysis against E2B, including the model-provider/agent-framework/MCP integration landscape and where boxkite already competes on it.examples/README.md— the runnable cookbook: a full LangGraph agent, a minimal LangChain agent, native OpenAI/Gemini/Mistral/Groq function calling, a LlamaIndex agent, an OpenAI Agents SDK agent, Claude Code in a sandbox (both a hand-maintained image and the declarative-builder API path), raw HTTP calls, and a hosted control-plane walkthrough.
Extending it: AuditSink and SessionMetadataStore
boxkite works with zero external dependencies — every tool operates against the sidecar's own S3/Azure storage. Two optional hooks exist for callers who want more:
boxkite.audit.AuditSink— mirror file writes into your own system of record (a database-backed file browser, an audit log, a webhook). Every method is best-effort; a broken sink can never fail a sandbox operation.boxkite.session_store.SessionMetadataStore— reconstruct session ownership (org/work-item IDs, storage prefix) if a pod is lost before its K8s labels/annotations can be read during error recovery. Most callers don't need this — K8s pod metadata already covers the common recovery path.
Both default to no-ops. Implement only what you need; see the docstrings in
src/boxkite/audit.py and src/boxkite/session_store.py.
Ready-to-use, SQLite-backed reference implementations of both ship in the
same modules — SQLiteAuditSink and SQLiteSessionMetadataStore — for
callers who want a working durable store without writing one. See
Extending boxkite for usage.
What's not included
This is a clean extraction, not a repackaging of a larger internal system. Deliberately left out:
- Any specific agent-orchestration framework. The tools are plain
LangChain
@tool-decorated functions — wire them into LangGraph, a custom agent loop, or anything else that accepts LangChain tools. - A "skills injection" system. The sidecar's
/ensure-skillsendpoint (materializing read-only skill files under/mnt/skills) is included because it's genuinely part of the sandbox's own file model, but the policy of which skills to inject for which agent is your application's concern, not this package's. - Any database or auth layer. Session ownership lives on Kubernetes pod
labels/annotations, which is enough to run this standalone. If you want
cross-restart recovery after a pod is already gone, see
SessionMetadataStoreabove.
License
FSL-1.1-Apache-2.0 — the Sentry-style Functional Source License. You can use, modify, and self-host boxkite for effectively any purpose except building a competing hosted/managed version of it. Every version converts automatically to the fully permissive Apache License 2.0 two years after its release. See LICENSE for the exact terms.
Contributing
See CONTRIBUTING.md — we use the Developer Certificate of
Origin (git commit -s), not a CLA. Found a security issue? Please see
SECURITY.md and report it privately rather than filing a
public issue — this project executes arbitrary code, and a sandbox-escape
report deserves a fast, private path.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file boxkite_sandbox-0.1.0.tar.gz.
File metadata
- Download URL: boxkite_sandbox-0.1.0.tar.gz
- Upload date:
- Size: 270.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4debabcdcb205a2399d5a9527327c89f8bdea9f217e715814bc083f1ff17dbb8
|
|
| MD5 |
b13ba1547fecb2cd5225d05b8fcc2baa
|
|
| BLAKE2b-256 |
6f49eb428b5f29b28e32176f486698aeef6293bc37870696fc633122b48db3bc
|
File details
Details for the file boxkite_sandbox-0.1.0-py3-none-any.whl.
File metadata
- Download URL: boxkite_sandbox-0.1.0-py3-none-any.whl
- Upload date:
- Size: 183.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1c77b7277fb19cbe60562cf1c5af07e48cb50084190799b3e9460b7d765239a
|
|
| MD5 |
931376d83f43b7fda66d1b2cff9032a9
|
|
| BLAKE2b-256 |
58a2d77b21623a01b869f45e3160028a9e2bd91286718d8382b59c24fb5e4d82
|