K8EK8E
Home
  • What is K8E
  • Getting Started
  • Sandbox Runtime Setup
  • Sandbox CLI
  • Sandbox Examples
  • Sandbox with E2B SDK
  • DeepSeek Harness
  • Architecture
  • Components
  • Advanced Installation
  • K8E vs Alternatives
  • Contributing
Changelog
  • English
  • ็ฎ€ไฝ“ไธญๆ–‡
GitHub
Home
  • What is K8E
  • Getting Started
  • Sandbox Runtime Setup
  • Sandbox CLI
  • Sandbox Examples
  • Sandbox with E2B SDK
  • DeepSeek Harness
  • Architecture
  • Components
  • Advanced Installation
  • K8E vs Alternatives
  • Contributing
Changelog
  • English
  • ็ฎ€ไฝ“ไธญๆ–‡
GitHub
  • Guide

    • What is K8E?
    • Getting Started
    • Sandbox Runtime Setup
    • Sandbox CLI
    • Sandbox Examples โ€” Driven by Your AI Agent
    • Sandbox with the Official E2B SDK
    • DeepSeek Harness + k8e-sandbox โ€” Step-by-Step
    • Architecture
    • Components
    • Advanced Installation
    • K8E vs Other Sandbox Platforms
    • Contributing

Sandbox with the Official E2B SDK

K8E speaks the E2B wire protocol natively, so the official e2b SDKs (Python and JavaScript, unmodified) work against a K8E cluster with a two-line configuration change. Migrating an existing E2B application to K8E is configuration, not code:

import { Sandbox } from 'e2b';

const sbx = await Sandbox.create({
  apiKey: process.env.E2B_API_KEY, // must be e2b_<hex> โ€” SDK validateApiKey rejects a bare hex token
  apiUrl: 'http://127.0.0.1:3676',                 // control plane (root-mounted)
  sandboxUrl: 'http://127.0.0.1:3676/e2b/envd',    // envd data plane
});

This is the E2B-compatible surface from KIP-18: the control plane REST API at the root, the envd Connect-RPC surface under /e2b/envd, and signed file URLs at /files โ€” plus lifecycle and timeout semantics that follow CubeSandbox (pause / resume / auto-pause / NEVER_TIMEOUT / fine-grained errors).

Prerequisites

  1. A running K8E server with the embedded E2B surface enabled (it is on by default; --disable-e2b turns it off). The embedded server listens on :3676 (--e2b-listen to override) and also fronts the sandbox gRPC gateway on :50051 through the Cilium Gateway API in a cluster. This guide is verified against K8E v1.37.0-20261022-rc2 and newer.

  2. An API key:

    k8e sandbox-apikey create my-agent
    # โ†’ {"name":"my-agent","key":"849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9","e2b_key":"e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9","ttl_days":30,"expires_at":"..."}
    

    The embedded e2b server auto-loads the same sandbox-apikeys Secret the gRPC gateway uses (reloaded every 30 s), so k8e sandbox-apikey create alone is enough for official SDK clients โ€” no separate --e2b-apikey needed. Pass the JSON e2b_key field (or e2b_ + the bare key) to the SDK as apiKey; the SDK's validateApiKey rejects a bare hex token before any request. A static key still works: --e2b-apikey / K8E_E2B_APIKEY (fallback K8E_SANDBOX_APIKEY).

  3. The official SDK in your language of choice โ€” pinned to 2.56.0, the version this guide's examples are written against (the 2.x line renamed a few methods; see SDK version notes):

    pip install "e2b==2.56.0"   # Python
    npm install e2b@2.56.0      # JavaScript / TypeScript
    

Which URL to use

SurfaceOn the server hostFrom another machine (via the Cilium Gateway)
E2B control plane (apiUrl)http://127.0.0.1:3676http://<gateway-host>:80
envd data plane (sandboxUrl)http://127.0.0.1:3676/e2b/envdhttp://<gateway-host>:80/e2b/envd
sandbox CLI (gRPC, mTLS)127.0.0.1:50051<gateway-host>:50051

The Gateway fronts both HTTP surfaces on its :80 listener โ€” the e2b control plane and the envd data plane are two HTTPRoutes bound to the same listener, so one host:port serves both. The :50051 TCP listener is L4 passthrough with mTLS preserved โ€” that is the one the CLI and MCP clients dial.

HTTPS

Releases that include k8e PR #671 also route the e2b surface through the Gateway's :443 listener, so https://<gateway-host> works as well as http://<gateway-host>:80. On earlier releases the :443 listener has no route and answers 404 โ€” use :80 there.

The gateway presents the same private CA the sandbox gRPC gateway uses, so the client has to trust it. Put it in the client's OS trust store โ€” the E2B SDK's transport reads the system store and ignores the SSL_CERT_FILE / REQUESTS_CA_BUNDLE environment variables:

# on the server: export the CA
sudo cat /var/lib/k8e/server/tls/sandbox-ca.crt > sandbox-ca.crt
# on the client: install it into the system trust store
sudo cp sandbox-ca.crt /usr/local/share/ca-certificates/k8e-sandbox-ca.crt
sudo update-ca-certificates

Set it once with environment variables

E2B_API_URL and E2B_SANDBOX_URL are read by both official SDKs, so you can keep the code free of URLs:

export E2B_API_KEY="e2b_<64-hex key>"
export E2B_API_URL="http://<gateway-host>:80"
export E2B_SANDBOX_URL="http://<gateway-host>:80/e2b/envd"

E2B_SANDBOX_URL overrides the port too โ€” the SDK treats it as the complete base URL for the sandbox, so it must include the /e2b/envd path. Leave it unset and the SDK derives a per-sandbox hostname (<port>-<sandbox-id>.<domain>) that a K8E cluster does not serve.

The template argument is a runtime class, not an E2B template

K8E builds the sandbox itself, so the e2b API's template field selects a Kubernetes RuntimeClass, not a prebuilt E2B template image. The default install provides gvisor, kata, firecracker, crun and lunatic; base (or no template) means the default runtime.

sandbox = Sandbox.create(api_key=..., template="gvisor")   # a runtime class

Anything else is a 404: template '<name>' not found โ€” including code-interpreter-v1, which is what the Code Interpreter SDK asks for by default (see Code Interpreter SDK).

Verify it works before you write any more code

python3 - <<'PY'
import os
from e2b import Sandbox

with Sandbox.create(
    api_key=os.environ["E2B_API_KEY"],
    api_url=os.environ["E2B_API_URL"],
    sandbox_url=os.environ["E2B_SANDBOX_URL"],
) as sbx:
    print(sbx.commands.run("python3 -c 'print(40 + 2)'").stdout)
PY
# โ†’ 42

If that prints 42, the control plane, the envd data plane, the API key and the runtime are all working.

Level 1 โ€” Create a sandbox and run a command

Python (the context manager kills the sandbox on exit):

from e2b import Sandbox

with Sandbox.create(
    api_key="e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9",                 # e2b_ prefix + hex-only key (SDK validateApiKey)
    api_url="http://127.0.0.1:3676",             # control plane
    sandbox_url="http://127.0.0.1:3676/e2b/envd",
) as sbx:
    result = sbx.commands.run("python3 -c 'print(40 + 2)'")
    print(result.stdout)   # "42"
    print(result.exit_code)  # 0
    print(sbx.sandbox_id)    # e.g. "e2b-9f3c..."

JavaScript:

import { Sandbox } from 'e2b';

const sbx = await Sandbox.create({
  apiKey: 'e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9',
  apiUrl: 'http://127.0.0.1:3676',
  sandboxUrl: 'http://127.0.0.1:3676/e2b/envd',
});
const { stdout, exitCode } = await sbx.commands.run('echo hello from k8e');
console.log(stdout, exitCode);
await sbx.kill();

The command runs in an isolated sandbox pod (gVisor by default) under /workspace, with egress restricted to an allowlist (pypi.org, registry.npmjs.org, github.com, โ€ฆ). A background run (background=True / background: true) is the same wire โ€” disconnect() never kills and you can connect(pid) later.

Level 2 โ€” Files: write, read, list, stat, rename, makeDir, remove

Sandbox file operations ride native in-pod syscalls (no shell), with paths resolving under /workspace (E2B's /home/user/... default).

SDK 2.x renamed files.stat() to files.get_info() โ€” same size / name / modified_time fields. See SDK version notes. The JS SDK spells it getInfo (camelCase, like its other methods); get_info is the Python name and does not exist in JS.

Python:

from e2b import Sandbox

with Sandbox.create(api_key="e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9",
             api_url="http://127.0.0.1:3676",
             sandbox_url="http://127.0.0.1:3676/e2b/envd") as sbx:
    sbx.files.write("/workspace/notes/hello.txt", "hello from k8e")
    print(sbx.files.read("/workspace/notes/hello.txt"))
    print(sbx.files.list("/workspace/notes"))     # [FileInfo(name="hello.txt", ...)]
    print(sbx.files.get_info("/workspace/notes/hello.txt").size)   # was .stat() pre-2.x
    sbx.files.make_dir("/workspace/notes/sub")
    sbx.files.rename("/workspace/notes/hello.txt", "/workspace/notes/world.txt")
    sbx.files.remove("/workspace/notes/world.txt")

JavaScript:

await sbx.files.write('/workspace/notes/hello.txt', 'hello from k8e');
console.log(await sbx.files.read('/workspace/notes/hello.txt'));
console.log(await sbx.files.list('/workspace/notes'));
console.log((await sbx.files.getInfo('/workspace/notes/hello.txt')).size); // was stat()
await sbx.files.makeDir('/workspace/notes/sub');
await sbx.files.rename('/workspace/notes/hello.txt', '/workspace/notes/world.txt');
await sbx.files.remove('/workspace/notes/world.txt');

Byte round-trips work (write accepts binary; read returns bytes), and signed-URL download/upload (sbx.files.download_url(path) / sbx.files.upload_url(path)) is supported for browser-style access.

Level 3 โ€” Lifecycle: timeout, pause, resume, kill

K8E follows CubeSandbox lifecycle semantics: deadlines are enforced by the embedded e2b server, and -1 means NEVER_TIMEOUT.

Python:

with Sandbox.create(api_key="e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9", timeout=120,
             api_url="http://127.0.0.1:3676",
             sandbox_url="http://127.0.0.1:3676/e2b/envd") as sbx:
    sbx.set_timeout(-1)        # NEVER_TIMEOUT โ€” clear the deadline
    sbx.pause()                # release the pod, keep the PVC + files
    # SDK 2.x has no sbx.resume(): any later call (or Sandbox.connect) wakes
    # the paused sandbox automatically. See the version notes below.
    info = sbx.get_info()
    print(info.state)          # "running" | "paused"
# List sandboxes (v2.x returns a SandboxPaginator; pull a page with next_items())
from e2b import Sandbox
paginator = Sandbox.list(api_key="e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9",
                         api_url="http://127.0.0.1:3676")
for s in paginator.next_items():
    print(s.sandbox_id, s.state)
  • pause() releases the pod (CPU/memory) but keeps the workspace PVC โ€” files survive a resume. Since the unified session model, every session is persistent and carries a PVC, so pause works on any session.
  • NEVER_TIMEOUT is Python-only. sbx.set_timeout(-1) sends -1 as seconds, and the server reads it as "no deadline". The JS SDK's setTimeout(timeoutMs) takes milliseconds and always converts with Math.ceil(ms / 1000), so -1 becomes -0, serialises as 0, and the server rejects it with 400 invalid timeout. From JavaScript, set a real deadline (sbx.setTimeout(120000)) or leave the sandbox timeout alone.
  • Auto-pause at the deadline and auto-resume on the next request (connect / exec / file I/O) both work. In SDK 2.x this auto-resume is the resume path โ€” there is no sbx.resume() call, and a plain Sandbox.connect(sandbox_id, ...) brings a paused sandbox back.
  • A killed sandbox answers 404 โ€” the SDK's kill() === false contract.

Level 4 โ€” Watch file changes (polling trio)

K8E implements the E2B watch polling trio (CreateWatcher / GetWatcherEvents / RemoveWatcher) via inotify in the sandbox daemon.

watch_dir() / watchDir() returns a watch handle, not an iterator. FilesystemEvent.name is the path of the changed entry relative to the watched directory (the SDK documents it as "Relative path to the filesystem object"), so a watcher on /workspace reports a.txt for a change to /workspace/a.txt.

Python:

from e2b import Sandbox

with Sandbox.create(api_key="e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9",
             api_url="http://127.0.0.1:3676",
             sandbox_url="http://127.0.0.1:3676/e2b/envd") as sbx:
    watcher = sbx.files.watch_dir("/workspace")   # polling trio underneath
    sbx.files.write("/workspace/a.txt", "hi")
    events = watcher.get_new_events()              # NOT next(watcher)
    for event in events:
        print(event.name, event.type)              # "a.txt" <FileEventType.CREATE>
    watcher.stop()

JavaScript:

const events = [];
const handle = await sbx.files.watchDir('/workspace', (event) => {
  events.push(`${event.name}:${event.type}`);     // "a.txt:create"
});
await sbx.files.write('/workspace/a.txt', 'hi');
// give the sandbox a moment to deliver the inotify event
await new Promise((r) => setTimeout(r, 1000));
await handle.stop();
console.log(events);

next(watcher) does not work in SDK 2.x

watch_dir() returns a WatchHandle โ€” it defines stop() and get_new_events() and no __next__, so next(watcher) raises TypeError: 'WatchHandle' object is not an iterator. That is a property of the SDK, not of the backend: it fails the same way against E2B's own cloud. Use get_new_events() (Python) or the onEvent callback (JavaScript).

The event name needs a recent sandboxd

Sandboxd older than v1.37.0-20261022-rc2 mis-parsed the inotify record and reported the watched directory (/workspace/) instead of the changed entry. If your events carry a directory path rather than a file name, upgrade the sandbox image to v1.37.0-20261022-rc2 or newer.

The streaming filesystem.Filesystem/WatchDir RPC is not implemented (no SDK uses it); the polling trio is what the official SDKs drive.

Level 5 โ€” Point your agent at K8E instead of a template

The most common migration: an AI agent already wired to E2B targets K8E with just the two URLs โ€” no code changes. For example, in an agent tool wrapper:

# agent/tools/sandbox.py โ€” unchanged E2B code, K8E backend
from e2b import Sandbox

def run_in_sandbox(code: str) -> str:
    with Sandbox.create(api_key="e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9",
                 api_url="http://127.0.0.1:3676",
                 sandbox_url="http://127.0.0.1:3676/e2b/envd") as sbx:
        r = sbx.commands.run(f"python3 -c {code!r}")
        return r.stdout if r.exit_code == 0 else r.stderr

Compare with the harness-driven k8e-sandbox-cli and the /k8e-sandbox skill: the CLI route is agent-harness oriented (Claude Code / Codex / Pi drive it), while the E2B SDK route is for code that already speaks E2B โ€” pick whichever fits your stack. Both hit the same sandbox infrastructure.

Not supported: the Code Interpreter SDK

The e2b-code-interpreter / @e2b/code-interpreter packages are a different SDK from the e2b packages above, and K8E does not implement them. If you are porting from the official E2B cookbook, this is the first thing you will hit โ€” the cookbook's hello-world-python and hello-world-js both use the Code Interpreter SDK.

Two independent gaps:

  1. No code-interpreter-v1 template. The Code Interpreter SDK asks for that template by default; K8E resolves template as a RuntimeClass, so the request fails with 404: template 'code-interpreter-v1' not found.
  2. No /execute route. run_code() speaks the Code Interpreter protocol: the SDK POSTs to {sandboxUrl}/execute with an E2b-Sandbox-Port: 49999 header and expects newline-delimited JSON back ({"type":"stdout","text":"โ€ฆ","timestamp":โ€ฆ}), because a code-interpreter server is supposed to be listening on port 49999 inside the sandbox. The K8E sandbox image ships sandboxd (plus python3 / git / node), not a code interpreter, and neither sandboxd nor the K8E e2b server implements the route.

What to do instead โ€” the plain e2b SDK covers the same ground:

# from e2b import Sandbox                       # NOT e2b_code_interpreter
with Sandbox.create(api_key=..., template="gvisor") as sbx:
    result = sbx.commands.run("python3 -c 'print(\"hello world\")'")
    print(result.stdout)                        # "hello world\n"
    print(sbx.files.list("/"))                  # [] on a fresh sandbox

The same example with the Code Interpreter SDK, for comparison, fails at Sandbox.create() with the template 404 โ€” and, once the template is fixed to gvisor, at run_code() with route POST /e2b/envd/execute not found.

The protocol is not the hard part

The /execute contract is plain NDJSON, and a minimal responder is enough to satisfy the official SDK: POST {code, language, context_id, env_vars}, then stream {"type":"stdout","text":โ€ฆ} lines. A code-interpreter server is not required. Porting it is a three-part change (sandboxd /execute route, the K8E e2b server route that proxies it, and a code-interpreter-* template alias) โ€” see KIP-31.

Paths resolve under /workspace

Sandbox file paths are sandbox-relative, and the sandbox root is/workspace โ€” that is E2B's /home/user/โ€ฆ default, and it is where the workspace PVC is mounted. So:

sbx.files.list("/")            # the workspace root โ€” [] on a fresh sandbox
sbx.files.list("/workspace")   # the same directory
sbx.files.list("/etc")         # FileNotFoundException: path not found: /workspace/etc

A fresh sandbox has an empty workspace โ€” files.list("/") returning [] is correct, not a failure. Absolute paths outside /workspace are not visible to the SDK; reach them through commands.run instead.

What is (and is not) supported

Works out of the box: create โ†’ connect โ†’ getInfo โ†’ list โ†’ kill โ†’ timeout (incl. -1 NEVER_TIMEOUT) โ†’ pause โ†’ resume โ†’ auto-pause โ†’ auto-resume, live commands.run streaming, commands.list, commands.connect, file write/read/list/stat/rename/makeDir/remove (incl. binary round-trips), signed-URL download/upload, watch via the polling trio, metadata.name idempotency.

Honest 501s (SDK methods throw with a machine-readable hint): PTY (pty.create, process.Process/Update, UpdatePTY), StreamInput, streaming WatchDir, metrics (returns []), and the templates registry (only runtime-class names are accepted as templateID). These are K8E-runtime gaps, not protocol gaps โ€” see KIP-18 for the full gap audit and implementation status.

Not implemented at all (the SDK call fails, not with a 501 but with a route/template error): the Code Interpreter SDK โ€” run_code(), create_code_context() and the code-interpreter-v1 template. Use the plain e2b SDK's commands.run instead.

SDK version notes

Examples in this guide are written against E2B SDK 2.56.0 (Python and JavaScript). The 2.x line renamed or moved a few methods compared to the 1.x examples you may see elsewhere, and K8E follows the SDK, so pin the version to avoid surprises:

pip install "e2b==2.56.0"   # Python
npm install e2b@2.56.0      # JavaScript / TypeScript
Pre-2.x (1.x)2.56.0Notes
sbx.files.stat(path)sbx.files.get_info(path)Same size / name / modified_time fields
sbx.pause() / sbx.resume()sbx.pause() onlybeta_resume() was removed; resume is implicit โ€” the next request (connect, exec, file I/O) wakes a paused sandbox
โ€”sbx.files.exists(path)New helper, works on K8E
โ€”sbx.files.write_files([...])Batch write

Two things to know about the 2.x resume model on K8E:

  • pause() keeps the workspace PVC, so files survive the wake.
  • To resume explicitly, call Sandbox.connect(sandbox_id, api_key=..., api_url=..., sandbox_url=...) โ€” K8E's connect auto-resumes and answers 201.

Binary uploads. files.write() carries non-UTF-8 bytes safely: K8E's upload path detects a binary body and transfers it as encoding="base64" (sandboxd's binary-safe chunk format), so the SDK's octet-stream/gzip/multipart uploads all round-trip byte-faithfully. Text payloads keep the direct path.

Large uploads. There is no per-file size limit beyond the sandbox's own disk; a single upload body is bounded at 512 MiB as a memory guard only. The SDK streams the body, so a ~few-hundred-MB write in one call is fine. Past that, stage the file with a shell append โ€” the SDK's own write() always overwrites and exposes no offset, so chunked writes of one file need the command path:

CHUNK = 32 * 1024 * 1024          # 32 MiB per request
with open("model.bin", "rb") as f:
    first = True
    while chunk := f.read(CHUNK):
        if first:
            sbx.commands.run(
                f"base64 -d > /workspace/model.bin <<'EOF'\n"
                f"{base64.b64encode(chunk).decode()}\nEOF")
            first = False
        else:
            sbx.commands.run(
                f"base64 -d >> /workspace/model.bin <<'EOF'\n"
                f"{base64.b64encode(chunk).decode()}\nEOF")

Reference

  • KIP-18: E2B-compatible sandbox API
  • Official E2B SDK โ€” Python
  • Official E2B SDK โ€” JavaScript/TypeScript
  • Sandbox CLI ยท Sandbox Examples
Last Updated: 10/24/26, 4:45 AM
Prev
Sandbox Examples โ€” Driven by Your AI Agent
Next
DeepSeek Harness + k8e-sandbox โ€” Step-by-Step