kotakpasir
Self-hosted sandboxes for AI agents. Disposable Linux environments where an LLM can write files, run shell commands, and reach a curated set of hosts, all bounded by a YAML policy you wrote, on a $5 VPS you control.
The Problem
LLM agents need somewhere to actually run code. The hosted options work well, but they cost real money per tenant and pull every workload into someone else's runtime. Running it yourself usually means microVMs (Firecracker, Kata) that demand KVM and nested virtualization, which a small VPS does not expose.
I wanted the middle path: something hardened enough to host real agent workloads, cheap enough to put on a $5 VPS, and shaped like the API agents already speak.
The Approach
Hardened Docker containers wrapped in a control plane. Cap-drop ALL, no-new-privs, read-only rootfs, pids/cpu/memory limits, network=none by default. You opt in to anything looser through a YAML policy file the daemon refuses to start without (in strict mode).
Containers are not a security boundary for fully untrusted code, and the docs say so plainly. gVisor, Kata, and Firecracker backends are on the roadmap behind a pluggable Runtime interface. For everything in between, hardened Docker is enough, and it runs on the hardware most people actually have.
Architecture
A single Go process (kpd) backed by a local SQLite store, plus a per-sandbox egress proxy on a per-sandbox internal network. The agent talks to the control plane; the control plane talks to Docker; sandboxes that opt into egress: allowlist can reach their own proxy and nothing else.
agent (LLM)
|
v
kpd / kpmcp (control plane)
|--- SQLite store
|--- warm pool
v
Docker daemon
|--- sandbox(es)
|--- per-sandbox egress proxy The proxy enforces HTTPS CONNECT only (no MITM, no path-level inspection). Cloud-metadata IPs are always denied. A global deny list always wins over per-sandbox allowlists.
Key Decisions
- Hardened Docker before microVMs. The whole point was running on a $5 VPS. Picking a stack that needs KVM would have killed the project on day one. Pluggable runtime keeps the door open for gVisor and Firecracker without coupling the control plane to either.
- Policy as a real file, not flags.
YAML policy with image allowlist, named profiles, egress modes, and a global deny list.
KP_REQUIRE_POLICY=1refuses to start without one, so a forgotten policy file fails loud instead of silently running with permissive defaults. - Warm pool for cold-start. Per-image pre-started containers turn ~150 ms cold-starts into ~1 ms claims for default-spec requests. The pool fills eagerly, refills async, and cleans up orphans on startup.
- Multiple surfaces, one source of truth.
HTTP API, MCP server, CLI, and a public Go SDK. The CLI is a thin wrapper over the SDK so behavior cannot drift between them. Errors carry actionable hints (
run kp ls,check kotakpasir.yaml,set KPD_TOKEN) rather than just dumping stack traces. - Observability from day one.
Prometheus metrics on
kpdand the proxy, structured logs withsandbox_idcorrelation, per-subsystem/healthz, and a ring buffer of every exec. Cheap to add now, painful to retrofit later.
Surfaces
HTTP API (kpd)
REST + SSE on Fiber v3, bearer auth via KPD_TOKEN. Full sandbox lifecycle, buffered and streaming exec, exec-output ring buffer with optional follow.
MCP server (kpmcp)
Six typed tools over stdio (sandbox_create, sandbox_list, sandbox_get, sandbox_exec, sandbox_stop, sandbox_delete). Drops into Claude Desktop, Cursor, and any other MCP-compatible client.
CLI (kp)
kp ls / run / exec / logs / watch / inspect / stop / rm. Exits with the in-sandbox command's exit code, so scripts compose naturally.
Go SDK (pkg/kotakpasir)
Typed errors (ErrNotFound, ErrUnauthorized, ErrPolicyDenied, ErrBadRequest) that work with errors.Is. Streaming exec yields stdout, stderr, and exit events as separate types.
Security Model, Honestly
What it protects against: an LLM agent running rm -rf /, a misbehaving sandbox process exhausting host resources, accidental exfiltration to arbitrary hosts, container processes escaping common Linux defaults.
What it does not protect against: kernel exploits, deliberately adversarial container escapes, side-channel attacks. If you are running fully untrusted code, wait for the gVisor or Firecracker backend, or pick a hosted microVM service. Lying about the threat model would do users more harm than the project itself.
Status
Pre-1.0. The core is in daily use locally, and everything in the [Unreleased] section of the changelog is shipped and tested. Breaking changes are possible until a version is tagged. Roadmap items are split into Now, Next, Later, and Open Questions in ROADMAP.md, so contributors can see where the lines are before they invest time.
Origin
Built after seeing a thread about agent sandboxing. The hosted options were great but pricey; the microVM stacks needed KVM that a $5 VPS does not expose. So: hardened Docker, own control plane, ship it. The MVP went from 19:30 to 23:00 WIB on day one, and it has been growing since.
Lessons Learned
Be honest in the docs. Container escape is real, kernel CVEs are real, and writing "secure by default" without qualifiers makes you the next post-mortem. The README leads with what the project does not protect against, and that has been more useful for credibility than any benchmark.
Pick the constraint that scopes the project. "$5 VPS, no KVM" killed half the design space and made the rest of the decisions almost mechanical. Without that constraint, this would have been another half-finished microVM clone.
Related Notes
Need agent infrastructure or a Go control plane built?
I ship hardened, observable systems that hold up in production. Let's talk.
Book an Intro Call