self-hosted sandbox platform

$ sandfort

Run the code your AI agents write, on hosts you control.

REST API · Python SDK · TypeScript SDK, CLI, MCP server on request

Request beta access → API reference

Hosted evaluation beta is open: try the API with zero install on our shared instance. In production, Sandfort runs on your own infrastructure.

the shift

AI agents write code.
Every turn of the loop runs it.

Generated code is only useful once it executes: tests, builds, scripts, dev servers. A coding agent runs code on every turn, and nobody on your team wrote or reviewed it.

untrustedwritten by a model, not a colleague
statefulfiles, repositories, long-running processes
constantmany runs per task, every task
1 · model writes code 2 · code runs somewhere untrusted 3 · output stdout, stderr, exit code 4 · model fixes, retries turn n

the problem

Where does that code run today?

✗ option a

on the agent's own host

One rm -rf or curl | sh away from a bad afternoon. Full access to keys, files and the network. No limits.

✗ option b

on a shared CI or prod box

Untrusted code lands next to secrets, deploy tokens and every other workload on that machine.

✗ option c

in a hosted sandbox service

Runs isolated, but your code, repositories and data leave your network on every call.

When customers, a security review or a contract say “the code stays here”, none of these work.

the answer

$ The sandbox runs on your side of the line.

YOUR INFRASTRUCTURE INTERNET your agent LLM + orchestrator sandfort api REST · SDK · CLI · MCP auth · quotas · audit worker · jobs · reconciler database sqlite · postgres · audit chain docker host sbx · run tests isolated container sbx · build isolated container sbx · live shell isolated container egress proxy allowlisted host any other host ✗
one Python serviceover the Docker hosts you already run
state stays with yousandboxes, keys and the audit chain in your SQLite or Postgres
outbound, per sandboxno network, an allowlist through the proxy, or open: you choose

integrate

Four ways in. Same sandbox.

The whole agent loop — exec, files, git, PTY, sessions, snapshots, preview URLs — behind one API. Non-zero exits come back as values, so the error goes straight back to the model. The REST API and the Python SDK are part of every evaluation package; the TypeScript SDK, the MCP server and the CLI are delivered on request, according to your stack.

from sandfort_sdk import Client

client = Client(base_url=URL, api_key=KEY)
sbx = client.create_sandbox(snapshot="python:3.12-slim", wait=True)

r = sbx.process_exec(argv=["sh", "-c", "echo build; exit 7"])
# r.exit_code == 7  (returned, not raised)

run = sbx.code_run("python", "print(21 * 2)")
# run.exit_code == 0, run.stdout.strip() == "42"

sbx.fs.upload_file("/tmp/hello.txt", b"hello\n")
sbx.delete()
import { SandfortClient } from "@sandfort/sdk";

const client = new SandfortClient({ baseUrl: URL, apiKey: KEY });
const sbx = await client.createSandbox({ snapshot: "python:3.12-slim" });

const r = await sbx.codeRun("python", "print(21 * 2)");
console.log(r.exitCode, r.stdout);  // 0 42

// sessions, async commands, fs, git, snapshots, volumes
$ sandfort login
$ sandfort sandbox create demo --snapshot python:3.12-slim --wait
$ sandfort exec sbx_… python3 -c "print(21 * 2)"
42
$ sandfort cp ./app sbx_…:/work/app
$ sandfort shell sbx_…           # interactive PTY
{ "mcpServers": { "sandfort": {
    "command": ".venv/bin/python",
    "args": ["mcp_server/sandfort_mcp.py"],
    "env": { "SANDFORT_BASE_URL": URL, "SANDFORT_API_KEY": KEY }
} } }

# tools any MCP client gets:
# create_sandbox · run_code · run_command · create_snapshot
# upload_text_file · download_text_file · list_sandboxes · delete_sandbox
  • REST API: 77 HTTP operations on 59 paths in an exported OpenAPI spec
  • Python SDK: sync and async clients, sessions, fs, git, PTY, image builder; in every evaluation package
  • TypeScript SDK (on request, per stack): the same agent workflow, no runtime dependencies
  • MCP server (on request, per stack): sandboxes as tools for any MCP client, no SDK integration
  • CLI (on request, per stack): sandfort for exec, copy, snapshots and a shell
  • Signed webhooks for lifecycle events, with delivery history and redelivery
  • Preview URLs and a web terminal, to show a human what the agent built

isolation

Assume the code is hostile.

inside a sandbox · container tier (the beta default is gvisor)
$ :(){ :|:& };:                      # fork bomb
bash: fork: retry: Resource temporarily unavailable
$ curl -m 3 http://169.254.169.254/  # cloud credentials
curl: (7) Failed to connect to 169.254.169.254 port 80
$ env | grep SANDFORT_
(no output)
$ ls /var/run/docker.sock
ls: cannot access '/var/run/docker.sock': No such file
processespids_limit=512, 4096 open files
networkmetadata and private ranges rejected by the host firewall script; no container-to-container traffic
secretsSANDFORT_* names refused at create; server settings never passed in
dockerthe socket is never mounted into a sandbox
privilegecap_drop=ALL (7 added back), no-new-privileges, CPU and memory caps, no swap
tierschosen per sandbox: container (runc), gvisor (a user-space kernel, the default on the hosted beta) or microvm (Kata Containers: its own guest kernel on KVM)
hostseccomp profile; prod refuses to start on a weak host

On the container tier sandboxes share the host kernel. On the gvisor tier a user-space kernel sits between the code and the host kernel; it is not a hardware boundary. These controls reduce the risk of an escape; they do not remove it.

This output is from the container tier; the hosted beta default is gvisor, and the output differs per tier (under gvisor, for example, uname -r reports a gVisor kernel and exceeding the memory or process limit usually stops the whole sandbox: exit code 137, sandbox_stopped true, files kept).

network

Reach the API it needs. Nothing else.

POST /v1/sandboxes
{
  "snapshot": "python:3.12-slim",
  "egress_allowlist": ["example.com"]
}
inside that sandbox
$ curl -sI https://example.com
HTTP/2 200   # CONNECT tunnel through the proxy
$ curl -sI https://other.example.net
curl: (56) CONNECT tunnel failed, response 403
# same proxy, forged credential
$ curl -sI --proxy-user sbx:wrong https://example.com
curl: (56) CONNECT tunnel failed, response 407
  • Fail-closed: with no proxy configured, an allowlisted create is refused (409), never run with open egress.
  • network.block_all: no network at all, for code that needs none.
  • On Linux hosts the firewall chain admits only the proxy address.
  • The cloud metadata address is blocked for every sandbox, always.

guardrails

Built for agents that misbehave.

  • Per-org quotas kept in a transactional ledger; only a platform admin can raise them (self-hosted; no org quotas are set on the hosted beta)
  • Per-org caps: 8 live terminals and 16 running async commands by default
  • Rate limits per user and key; JSON request bodies capped at 16 MiB
  • Idle sandboxes auto-stop; forgotten ones auto-delete
  • Reconciler and GC remove orphaned containers, images and volumes
  • Webhook delivery shared fairly between tenants: one slow endpoint cannot stall the rest

An agent stuck in a loop burns its own quota, not your host.

speed

Fast enough for the inner loop.

107 mscreate a sandbox · p50 · macOS laptop
p95 145
248 mscreate a sandbox · p50 · Linux beta VM
p95 323
18 msrun a command · p50 · macOS laptop
p95 25
52 msrun a command · p50 · Linux beta VM
p95 60
0100200300400 ms
warm poolstandby sandboxes per org, snapshot and size; a matching create takes one (opt-in)
snapshotsstart from a prepared environment instead of installing again
reproduce itpython examples/benchmark.py on your own hardware

Two measurements, each one run on one host, same script (20 creates waiting for started, 50 short commands in one sandbox, alpine:3.20 pre-pulled). macOS laptop, 2026-11-01: arm64, Docker 29.8.2. Linux beta VM, 2026-11-03: a KubeVirt VM with 6 vCPU on a shared Hetzner host (nested virtualization, not bare metal), Ubuntu 24.04, Docker 29.9.2, XFS project-quota storage. Neither is an SLO, and neither is a comparison with any other product. The hosted beta is a shared evaluation instance, not a benchmark environment: measure on your own host.

operations

Runs where you already run.

day one

a laptop

  • One Python process and a SQLite file
  • A local Docker daemon
  • Your own instance in under an hour with the install guide
production

your cluster

  • Kubernetes kit with separate api and worker roles, Postgres
  • Multi-node placement and cross-node routing
  • Prometheus /metrics, OTel tracing, JSON logs with request ids
  • Read-only web console at /console (self-hosted; not exposed on the hosted beta)
  • Runbook: backup, restore, upgrade, scaling

Same codebase and the same API from the first experiment to the security review. TLS terminates in front of the API.

identity and access

Your identity provider decides who gets in.

identity providerOkta or Entra ID assigns a person to the app
SCIM →
member createdSCIM 2.0 Users with a per-org token; default role viewer
→
signs inOIDC identity token exchanged for a short-lived session
role checkedevery session request re-checks membership and the current role
→
worksadmin · developer · viewer, mapped to API scopes
offboard →
deactivatedactive: false removes the membership; audit history stays
scoped API keys, hashed, shown once no one grants scopes they do not hold the last admin cannot be removed SCIM: Users only, no groups

SSO and SCIM are self-hosted configuration features; they are not enabled on the hosted evaluation beta, which uses API keys.

engineering

Measured, not promised.

3094tests in the suite
10acceptance gates passing, end to end on real Docker
11delivery phases shipped
77REST API operations, on 59 paths
CI greenlint mypy --strictimport-layer contracts unit testsdocker tests load and chaos suites

Tests: pytest --collect-only, 2026-11-04. Operations: counted in the exported OpenAPI spec, 2026-11-04 (includes /healthz and /readyz). Gates: scripts in examples/. CI: green on GitHub as of 2026-11-03. The audit log is a per-organization hash chain; audit-verify exits non-zero on any break, so a self-hosted operator can put it in cron. On the hosted evaluation beta that check runs operator-side; customers read their events through the audit API.

compared with e2b

If you use E2B today.

Both can be self-hosted. They differ in what the host needs, how a sandbox is isolated by default, and what ships with the platform.

isolationE2B: Firecracker microVMs. Sandfort: the default tier is gvisor, a user-space kernel and not a hardware boundary; the hosted beta also offers a microvm tier (Kata Containers on nested KVM). We do not claim stronger isolation than microVMs.
hostE2B: the runtime needs Linux with KVM. Sandfort: the container and gvisor tiers run on any host with a Docker daemon, with no KVM; only the microvm tier needs KVM.
governanceSandfort: audit hash chain, RBAC, OIDC SSO, per-org quotas and metering are part of the same package (SSO is a self-hosted configuration feature). E2B: we have not checked which of these its self-hosted stack includes.
self-hostE2B: a public runtime under Apache-2.0; the full stack on one Linux host, Terraform for GCP, AWS and Azure, or a Kubernetes manifest. Sandfort: one Docker host or a Kubernetes kit, source-available under FSL-1.1, not open source.

E2B facts checked 2026-11-04 against github.com/e2b-dev/infra (README and license); they can change. This is not a feature-by-feature comparison, and it says nothing about E2B's hosted service.

honest limits

What it does not do (yet).

Three tiers, three trade-offs. container shares the host kernel; gvisor is a user-space kernel, not a hardware boundary, and runs spawn- and small-file-heavy work several times slower; microvm (Kata Containers on the beta's nested KVM) starts in about 4 s. Per-sandbox disk size (disk_gib) is enforced on all three tiers.
Docker socket on sandbox nodes. Server processes mount it and it grants node control: use dedicated nodes.
Unencrypted node-to-node hop. Requires a private network or an encrypted overlay.
No built-in TLS, and /metrics has no authentication: terminate TLS in front and keep metrics internal.
A sandbox lives on one node. Placement spreads sandboxes; it does not move them.
SSO maps into one org. SCIM covers Users, not groups. Both are self-hosted configuration features and are not enabled on the hosted evaluation beta.
Version 0.1.0. No tagged release, built in late 2026.
No production references and no certifications yet. Source-available under FSL-1.1, not open source (Apache-2.0 after two years).

The hosted beta is a shared evaluation instance: no SLA, best-effort support, no production secrets, not for benchmarks. The governance details behind the connect guide (scopes per endpoint, rate-limit buckets, usage metering, what the audit log records and redacts, webhook delivery) come in a governance reference shipped with the evaluation package.

next step

Looking for 2–3 design partners.

primary

teams building AI coding agents

Your agent needs exec, files, git and a terminal. Your customers need the code to stay on your side.

secondary

platform and DevEx teams

You answer to security and compliance, and need auditable, multi-tenant execution in your own cluster.

the pilot

  • Free for a limited number of partners; the commercial model is decided after the pilots
  • Start on our hosted evaluation beta — zero install, try the API in an afternoon
  • Then your own instance, in under an hour with the install guide
  • In return: feedback sessions and the right to name you as a reference
  • Best-effort support, directly with the maintainer; no SLA
hosted beta→your instance→pilot

Request beta access →

sandfort

$ run untrusted code.

Keep it in-house.

Self-hosted sandboxes for AI-generated code: exec, files, git, terminals and snapshots, with quotas, SSO and an audit chain.