K8EK8E
首页
  • 什么是 K8E
  • 快速开始
  • 沙箱运行时配置
  • Sandbox CLI
  • 沙箱范例
  • E2B SDK 沙箱
  • DeepSeek Harness 接入
  • 架构
  • 组件
  • 高级安装
  • K8E 对比其他方案
  • 参与贡献
更新日志
  • English
  • 简体中文
GitHub
首页
  • 什么是 K8E
  • 快速开始
  • 沙箱运行时配置
  • Sandbox CLI
  • 沙箱范例
  • E2B SDK 沙箱
  • DeepSeek Harness 接入
  • 架构
  • 组件
  • 高级安装
  • K8E 对比其他方案
  • 参与贡献
更新日志
  • English
  • 简体中文
GitHub
  • 指南

    • 什么是 K8E?
    • 快速开始
    • 沙箱运行时配置
    • Sandbox CLI
    • 沙箱范例 —— 由你的 AI Agent 驱动
    • 使用官方 E2B SDK 操作沙箱
    • DeepSeek Harness + k8e-sandbox 接入指南(分步)
    • 架构
    • 组件
    • 高级安装
    • K8E 对比其他沙箱平台
    • 参与贡献
  • 操作指南

    • 操作指南 (Guides)
    • 部署标准集群
    • 使用kubekey一键部署k8e
    • 内置Nerdctl工具
    • 离线环境下快速部署k8e
    • 默认支持10年有效证书
    • k8e集群备份与恢复
    • k8s安装配置Flannel的方案
    • 使用kube-vip实现集群VIP访问
    • 怎么清理K8E集群
    • 备份和恢复
    • 自定义pause镜像

使用官方 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 / 细粒度错误)。

前置条件

  1. 一个运行中的 K8E server,且内嵌 E2B 面已启用(默认开启;--disable-e2b 关闭)。内嵌服务监听 :3676(可用 --e2b-listen 覆盖);集群中它还通过 Cilium Gateway API 对外暴露沙箱 gRPC 网关 :50051。 本指南在 K8E v1.37.0-20261022-rc2 及更新版本上验证过。

  2. 一个 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-apikeys Secret(每 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)。

  3. 安装官方 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:3676http://<网关地址>:80
envd 数据面(sandboxUrl)http://127.0.0.1:3676/e2b/envdhttp://<网关地址>: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/WatchDir RPC 未实现(官方 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。

两个互相独立的缺口:

  1. 没有 code-interpreter-v1 模板。 Code Interpreter SDK 默认就请求这个模板; 而 K8E 把 template 解析为 RuntimeClass,于是请求失败: 404: template 'code-interpreter-v1' not found。
  2. 没有 /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.x2.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")

参考

  • KIP-18:E2B 兼容沙箱 API
  • 官方 E2B SDK —— Python
  • 官方 E2B SDK —— JavaScript/TypeScript
  • Sandbox CLI · 沙箱范例
最近更新: 2026/10/24 04:45
Prev
沙箱范例 —— 由你的 AI Agent 驱动
Next
DeepSeek Harness + k8e-sandbox 接入指南(分步)