使用官方 E2B SDK 操作沙箱
K8E 原生支持 E2B 线协议,因此 官方 e2b SDK(Python 与 JavaScript,零修改) 只需两行配置即可对接 K8E 集群。迁移已有 E2B 应用到 K8E 只是改配置,不是改代码:
import { Sandbox } from 'e2b';
const sbx = await Sandbox.create({
apiKey: process.env.E2B_API_KEY, // 必须是 e2b_<hex> —— SDK validateApiKey 会拒绝裸 hex token
apiUrl: 'http://127.0.0.1:3676', // 控制面(挂载在根路径)
sandboxUrl: 'http://127.0.0.1:3676/e2b/envd', // envd 数据面
});
这是 KIP-18 实现的 E2B 兼容面:根路径下的控制面 REST API、/e2b/envd 下的 envd Connect-RPC 面、/files 的签名文件 URL——以及遵循 CubeSandbox 的生命周期与超时语义(pause / resume / auto-pause / NEVER_TIMEOUT / 细粒度错误)。
前置条件
一个运行中的 K8E server,且内嵌 E2B 面已启用(默认开启;
--disable-e2b关闭)。内嵌服务监听:3676(可用--e2b-listen覆盖);集群中它还通过 Cilium Gateway API 对外暴露沙箱 gRPC 网关:50051。 本指南在 K8Ev1.37.0-20261022-rc2及更新版本上验证过。一个 API key:
k8e sandbox-apikey create my-agent # → {"name":"my-agent","key":"849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9","e2b_key":"e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9","ttl_days":30,"expires_at":"..."}内嵌 e2b server 会自动加载与 gRPC 网关同一个
sandbox-apikeysSecret(每 30 秒重载),所以k8e sandbox-apikey create一条命令就够了,无需再单独配--e2b-apikey。把 JSON 里的e2b_key字段(或e2b_+ 裸key)作为 SDK 的apiKey;SDK 的validateApiKey会在发请求前拒绝裸 hex token。静态 key 仍然可用:--e2b-apikey/K8E_E2B_APIKEY(兜底K8E_SANDBOX_APIKEY)。安装官方 SDK —— 锁到 2.56.0,即本指南示例所针对的版本(2.x 改了几个方法名,见 SDK 版本说明):
pip install "e2b==2.56.0" # Python npm install e2b@2.56.0 # JavaScript / TypeScript
用哪个 URL
| 面 | 在服务器本机 | 从别的机器(经 Cilium Gateway) |
|---|---|---|
E2B 控制面(apiUrl) | http://127.0.0.1:3676 | http://<网关地址>:80 |
envd 数据面(sandboxUrl) | http://127.0.0.1:3676/e2b/envd | http://<网关地址>:80/e2b/envd |
| sandbox CLI(gRPC,mTLS) | 127.0.0.1:50051 | <网关地址>:50051 |
Gateway 把两个 HTTP 面都挂在它的 :80 listener 上——e2b 控制面和 envd 数据面是绑到同一个 listener 的两条 HTTPRoute,所以同一个 host:port 同时提供两者。:50051 是 L4 透传、保留 mTLS 的 TCP listener——CLI 和 MCP 客户端连的是它。
HTTPS
包含 k8e PR #671 的版本还会把 e2b 面接到 Gateway 的 :443 listener 上, 所以 https://<网关地址> 和 http://<网关地址>:80 都能用。更早的版本里 :443 listener 没有挂任何 route、会返回 404——那种情况下请用 :80。
网关用的是私有 CA(和沙箱 gRPC 网关同一张),所以客户端必须信任它。请装进客户端的 系统信任库——E2B SDK 的 transport 读的是系统信任库,并不理会 SSL_CERT_FILE / REQUESTS_CA_BUNDLE 这两个环境变量:
# 在服务器上导出 CA
sudo cat /var/lib/k8e/server/tls/sandbox-ca.crt > sandbox-ca.crt
# 在客户端装进系统信任库
sudo cp sandbox-ca.crt /usr/local/share/ca-certificates/k8e-sandbox-ca.crt
sudo update-ca-certificates
用环境变量一次性配好
两个官方 SDK 都读 E2B_API_URL 和 E2B_SANDBOX_URL,这样代码里就不必写 URL:
export E2B_API_KEY="e2b_<64 位 hex key>"
export E2B_API_URL="http://<网关地址>:80"
export E2B_SANDBOX_URL="http://<网关地址>:80/e2b/envd"
E2B_SANDBOX_URL连端口一起覆盖——SDK 把它当作沙箱的完整 base URL,所以必须带上/e2b/envd路径。不设它的话,SDK 会推导出一个按沙箱区分的主机名 (<port>-<sandbox-id>.<domain>),而 K8E 集群并不提供这种寻址。
template 参数是 runtime class,不是 E2B 模板
K8E 自己构建沙箱,所以 e2b API 的 template 字段选的是 Kubernetes RuntimeClass, 不是预制的 E2B 模板镜像。默认安装提供 gvisor、kata、firecracker、crun 和 lunatic;base(或不传 template)表示默认运行时。
sandbox = Sandbox.create(api_key=..., template="gvisor") # 一个 runtime class
其他任何值都会得到 404: template '<name>' not found——包括 Code Interpreter SDK 默认请求的 code-interpreter-v1(见 不支持:Code Interpreter SDK)。
写更多代码之前先验证一下
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
如果打出 42,说明控制面、envd 数据面、API key 和运行时全部工作正常。
第 1 级 —— 创建沙箱并执行命令
Python(上下文管理器在退出时销毁沙箱):
from e2b import Sandbox
with Sandbox.create(
api_key="e2b_849a5302e66f98d1d5064ef8501703574af4053b7bf9cf5337f9533326ce2bc9", # e2b_ 前缀 + 纯 hex key(SDK validateApiKey 要求)
api_url="http://127.0.0.1:3676", # 控制面
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) # 例如 "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();
命令跑在一个隔离的沙箱 Pod 里(默认 gVisor),工作目录是 /workspace,出网受白名单限制 (pypi.org、registry.npmjs.org、github.com …)。后台运行(background=True / background: true)走同一条线路——disconnect() 不会杀掉进程,之后可以 connect(pid) 接回来。
第 2 级 —— 文件:write / read / list / stat / rename / makeDir / remove
沙箱文件操作走 Pod 内原生系统调用(无 shell),路径解析在 /workspace 下(E2B 的 /home/user/... 默认即 K8E 的 /workspace)。
SDK 2.x 把
files.stat()改名为files.get_info()——size/name/modified_time字段不变。 见 SDK 版本说明。 JS SDK 拼作getInfo(驼峰,和它其它方法一致);get_info是 Python 的写法,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) # 2.x 之前是 .stat()
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); // 2.x 之前是 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');
字节往返可用(write 接受二进制,read 返回字节);同时支持签名 URL 下载/上传(sbx.files.download_url(path) / sbx.files.upload_url(path)),适合浏览器式访问。
第 3 级 —— 生命周期:timeout / pause / resume / kill
K8E 遵循 CubeSandbox 生命周期语义:deadline 由内嵌 e2b 服务强制执行,-1 表示 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 —— 清除 deadline
sbx.pause() # 释放 Pod,保留 PVC 和文件
# SDK 2.x 没有 sbx.resume():后续任何调用(或 Sandbox.connect)都会自动唤醒
# 被暂停的沙箱。详见下面的版本说明。
info = sbx.get_info()
print(info.state) # "running" | "paused"
# 列出沙箱(2.x 返回 SandboxPaginator;用 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()释放 Pod(CPU/内存)但保留 workspace PVC——文件在唤醒后依然在。由于统一的会话模型, 每个会话都是持久化的并带 PVC,所以任何会话都能 pause。- 到 deadline 自动 pause、下次请求(connect / exec / 文件 I/O)自动 resume 都可用。在 SDK 2.x 里 这个自动 resume 就是 resume 路径——没有
sbx.resume()调用,普通Sandbox.connect(sandbox_id, ...)就能把暂停的沙箱唤醒。 - 被 kill 的沙箱返回 404——符合 SDK 的
kill() === false约定。 - NEVER_TIMEOUT 只有 Python 能用。
sbx.set_timeout(-1)按秒原样发送-1, 服务端把它读作「无 deadline」。而 JS SDK 的setTimeout(timeoutMs)收的是毫秒, 并且无条件用Math.ceil(ms / 1000)换算,于是-1变成-0,序列化成0, 服务端直接拒绝:400 invalid timeout。从 JavaScript 调用时请设一个真实的 deadline (sbx.setTimeout(120000)),或者干脆别动沙箱超时。
第 4 级 —— 监听文件变化(轮询三件套)
K8E 通过沙箱守护进程内的 inotify 实现了 E2B 的 watch 轮询三件套(CreateWatcher / GetWatcherEvents / RemoveWatcher)。
watch_dir() / watchDir() 返回的是一个监听句柄,不是迭代器。 FilesystemEvent.name 是变化条目相对于被监视目录的路径(SDK 文档原文是 "Relative path to the filesystem object"),所以监视 /workspace 时, /workspace/a.txt 的变化会以 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") # 底层即轮询三件套
sbx.files.write("/workspace/a.txt", "hi")
events = watcher.get_new_events() # 不是 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');
// 给沙箱一点时间把 inotify 事件送出来
await new Promise((r) => setTimeout(r, 1000));
await handle.stop();
console.log(events);
SDK 2.x 里 next(watcher) 不可用
watch_dir() 返回 WatchHandle——它只有 stop() 和 get_new_events(),没有 __next__, 所以 next(watcher) 会抛 TypeError: 'WatchHandle' object is not an iterator。 这是 SDK 本身的性质,与后端无关:对着 E2B 官方云也一样会报。 请改用 get_new_events()(Python)或 onEvent 回调(JavaScript)。
事件名需要较新的 sandboxd
早于 v1.37.0-20261022-rc2 的 sandboxd 把 inotify 记录解析错了,上报的是被监视的目录 (/workspace/)而不是变化的文件。如果你的事件里是目录路径而不是文件名, 请把沙箱镜像升级到 v1.37.0-20261022-rc2 或更高。
流式
filesystem.Filesystem/WatchDirRPC 未实现(官方 SDK 并不使用它);驱动官方 SDK 的正是轮询三件套。
第 5 级 —— 让已有 Agent 直接改用 K8E
最常见的迁移:已接好 E2B 的 AI Agent 只需改两个 URL 即可指向 K8E,无需改代码。例如一个 agent 工具包装器:
# agent/tools/sandbox.py —— E2B 代码原样不动,后端换成 K8E
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
和 harness 驱动的 k8e-sandbox-cli 以及 /k8e-sandbox skill 做个对比:CLI 这条路面向 agent harness (Claude Code / Codex / Pi 来驱动),E2B SDK 这条路面向已经会说 E2B 的代码——按你的技术栈选一个。 两者打到的是同一套沙箱基础设施。
不支持:Code Interpreter SDK
e2b-code-interpreter / @e2b/code-interpreter 和上面的 e2b 包是不同的 SDK, K8E 没有实现它们。如果你正在从 官方 E2B cookbook 迁移,这会是第一个撞上的问题——cookbook 里的 hello-world-python 和 hello-world-js 用的都是 Code Interpreter SDK。
两个互相独立的缺口:
- 没有
code-interpreter-v1模板。 Code Interpreter SDK 默认就请求这个模板; 而 K8E 把template解析为 RuntimeClass,于是请求失败:404: template 'code-interpreter-v1' not found。 - 没有
/execute路由。run_code()走的是 Code Interpreter 协议: SDK POST 到{sandboxUrl}/execute,带上E2b-Sandbox-Port: 49999头, 并期待返回换行分隔的 JSON ({"type":"stdout","text":"…","timestamp":…})——因为按设计,沙箱内部的 49999 端口 应该有一个 code-interpreter 服务在监听。K8E 的沙箱镜像装的是 sandboxd (外加 python3 / git / node),不是 code interpreter;sandboxd 和 K8E e2b server 都没有实现这个路由。
替代方案 —— 普通 e2b SDK 覆盖了同样的功能:
# from e2b import Sandbox # 不是 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("/")) # 新沙箱里是 []
同一个例子改用 Code Interpreter SDK 会在 Sandbox.create() 处就因模板 404 失败; 把模板改成 gvisor 之后,又会在 run_code() 处报 route POST /e2b/envd/execute not found。
协议本身不是难点
/execute 的约定就是朴素的 NDJSON,一个最小实现足以满足官方 SDK: POST {code, language, context_id, env_vars},然后流出 {"type":"stdout","text":…} 这样的行。不需要真的跑一个 code-interpreter 服务。 移植它分三部分(sandboxd 的 /execute 路由、K8E e2b server 里转发它的路由、 以及 code-interpreter-* 模板别名)——见 KIP-31。
路径解析在 /workspace 下
沙箱文件路径是相对于沙箱解析的,而沙箱根目录就是 /workspace——这是 E2B 的 /home/user/… 默认值,也是 workspace PVC 挂载的位置。所以:
sbx.files.list("/") # workspace 根目录 —— 新沙箱里是 []
sbx.files.list("/workspace") # 同一个目录
sbx.files.list("/etc") # FileNotFoundException: path not found: /workspace/etc
全新沙箱的 workspace 是空的——files.list("/") 返回 [] 是正确行为,不是故障。 /workspace 之外的绝对路径对 SDK 不可见;需要访问它们请走 commands.run。
支持与不支持
开箱即用: create → connect → getInfo → list → kill → timeout(含 -1 NEVER_TIMEOUT)→ pause → resume → auto-pause → auto-resume、commands.run 实时流式输出、commands.list、commands.connect、文件 write/read/list/stat/rename/makeDir/remove、字节往返、签名 URL 下载/上传、轮询三件套 watch、metadata.name 幂等。
诚实的 501(SDK 方法抛出带机器可读提示的错误):PTY(pty.create、process.Process/Update、UpdatePTY)、StreamInput、流式 WatchDir、metrics(返回 [])、模板注册表(仅接受 runtime-class 名称作为 templateID)。这些是 K8E 运行时缺口,而非协议缺口——完整差距审计与实现状态见 KIP-18。
完全未实现(不是 501,而是路由/模板层面的报错):Code Interpreter SDK—— run_code()、create_code_context() 以及 code-interpreter-v1 模板。 请改用普通 e2b SDK 的 commands.run。
SDK 版本说明
本指南的示例针对 E2B SDK 2.56.0(Python 与 JavaScript)。2.x 相比你在别处见过的 1.x 示例改名或挪动过几个方法,而 K8E 跟随 SDK,所以请锁版本以免意外:
pip install "e2b==2.56.0" # Python
npm install e2b@2.56.0 # JavaScript / TypeScript
| 1.x | 2.56.0 | 说明 |
|---|---|---|
sbx.files.stat(path) | sbx.files.get_info(path) | size / name / modified_time 字段不变(JS 是 getInfo) |
sbx.pause() / sbx.resume() | 只有 sbx.pause() | beta_resume() 已移除;resume 是隐式的——下一次请求(connect、exec、文件 I/O)就会唤醒暂停的沙箱 |
| — | sbx.files.exists(path) | 新增辅助函数,在 K8E 上可用 |
| — | sbx.files.write_files([...]) | 批量写 |
K8E 上 2.x resume 模型的两点说明:
pause()保留 workspace PVC,所以文件在唤醒后依然在。- 显式 resume 的方式是调用
Sandbox.connect(sandbox_id, api_key=..., api_url=..., sandbox_url=...)——K8E 的 connect 会自动 resume 并返回201。
二进制上传。 files.write() 能安全携带非 UTF-8 字节:K8E 的上传路径会识别二进制 body 并以 encoding="base64" 传输(sandboxd 的二进制安全分块格式),所以 SDK 的 octet-stream / gzip / multipart 上传都能字节级往返。文本负载走直接路径。
大文件上传。 除了沙箱自身磁盘之外没有单文件大小上限;单次上传 body 以 512 MiB 为内存保护 上限。SDK 是流式发送 body 的,所以单次写几百 MB 没问题。再大就用 shell 追加分块—— SDK 自己的 write() 总是覆盖且不暴露 offset,所以同一个文件要分块写就得走命令路径:
CHUNK = 32 * 1024 * 1024 # 每次 32 MiB
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")