执行摘要
- 一句话:E2B 模板构建固定 root 用户并纳入别名摘要
- 推荐动作:建议快速精读。变更范围小但设计完整:把影响产物内容的因素(构建用户)纳入 alias digest,与既有“资源变更重烘焙”策略一致,是 E2B 模板缓存正确性的通用模式。同时值得学习作者在 PR 描述中区分“修复”与“契约声明”的表达方式。团队在使用 openenv/e2b 后端时无需额外操作,仅注意首次构建会因 alias 变化触发一次性重建。
功能与动机
PR body 明确指出根因与隐蔽性:"E2B Cloud runs template-build commands as a non-root user, so every layer of the per-task TB2 recipe fails there — apt-get exits 100 and /opt is not writable. A self-hosted AgentENV builds as root, which is why the e2b backend passed its bring-up without this surfacing." 同一套代码在自托管环境自测通过,却在云环境处处失败,属于典型 provider 行为差异导致的隐性故障。作者还特意区分了两处改动的性质:模板构建的 set_user("root") 是真正修复;server exec 固定用户 "That one is not a fix — both endpoints were measured to already default to root at runtime",作用是声明契约,防止 provider 默认值改变后,agent 在期望 root 的任务镜像里静默失权(TB2 的 solution 和测试会自由 apt-install)。
实现拆解
- 新增构建用户常量:在
examples/experimental/openenv/tb2_sandbox_e2b.py 模块顶层新增 _BUILD_USER = "root",构建与运行时共用。注释明确:TB2 任务镜像按 root agent 设计,任何非 root 用户都会改变任务环境本身,而不仅是构建过程。
- 修复模板构建:
ensure_task_template 中把 Template().from_image(base) 改为链式 Template().from_image(base).set_user(_BUILD_USER),保证用户切换发生在任何 run_cmd 之前——set_user 只影响其后的命令,顺序是正确性的关键。
- 摘要纳入构建用户:
template_alias 的 digest 输入由 [base, *commands, resources] 扩展为 [base, _BUILD_USER, *commands, resources]。构建用户是烘焙产物的组成部分,变化时 alias 必须跟着变,才能强制重新构建而不是继续使用第一个构建者留下的旧模板。这与既有“任务资源变化触发重烤”的策略一脉相承。
- 固定 server 运行用户:
create_task_sandbox 中 sandbox.commands.run(..., background=True, user=_BUILD_USER)。如动机所述这是契约而非修复,防止 provider 默认值漂移。
- 测试配套:
tests/test_tb2_sandbox_e2b.py 扩展 _FakeTemplateCls(新增有序 steps 记录)与 _FakeSandbox(新增 ran 记录),新增 3 个用例:test_ensure_template_builds_as_root(断言 user == "root" 且 steps[0] == ("set_user", "root"))、test_template_alias_tracks_the_build_user(monkeypatch _BUILD_USER 后 alias 变化)、test_create_runs_the_env_server_as_root(断言 server exec 的 user、background、命令内容)。离线 60 通过 2 跳过,ruff 与 black 通过。
关键文件:
examples/experimental/openenv/tb2_sandbox_e2b.py(模块 E2B 沙箱;类别 source;类型 core-logic;符号 _BUILD_USER, template_alias, ensure_task_template, create_task_sandbox): 修复主路径:新增 _BUILD_USER 常量、ensure_task_template 链式 set_user、template_alias 摘要纳入构建用户、server exec 固定用户。
examples/experimental/openenv/tests/test_tb2_sandbox_e2b.py(模块 E2B 沙箱;类别 test;类型 test-coverage;符号 set_user, test_ensure_template_builds_as_root, test_template_alias_tracks_the_build_user, test_create_runs_the_env_server_as_root): 新增 3 个测试锁定 set_user 顺序、digest 对构建用户敏感、server exec 用户参数;fake 类扩展用于断言调用顺序。
关键符号:template_alias, ensure_task_template, create_task_sandbox, test_ensure_template_builds_as_root, test_template_alias_tracks_the_build_user, test_create_runs_the_env_server_as_root
关键源码片段
examples/experimental/openenv/tb2_sandbox_e2b.py
修复主路径:新增 _BUILD_USER 常量、ensure_task_template 链式 set_user、template_alias 摘要纳入构建用户、server exec 固定用户。
# 构建与运行时统一使用的用户。TB2 任务镜像按 root agent 设计
# (其测试会直接 apt-install),任何非 root 用户都会改变任务
# 环境本身。构建阶段是真正修复:E2B Cloud 默认以非 root 执行
# 构建命令,导致 apt-get 退出码 100、/opt 不可写;自托管
# AgentENV 以 root 构建,所以此前未暴露。运行时两端实测本就
# 默认 root,显式传入只为固定契约,防止 provider 改变默认值
# 时静默收窄 agent 权限。
_BUILD_USER = "root"
def ensure_task_template(
task_dir: Path,
*,
force: bool = False,
build_timeout_s: float = 1800.0,
on_logs=None,
) -> str:
"""按任务构建模板,alias 已存在则直接复用;返回 alias。"""
# 惰性导入:e2b SDK 是可选依赖,离线单测用 sys.modules 里的
# fake 替换,模块导入本身不能依赖真实 SDK。
from e2b import Template
task_dir = Path(task_dir)
alias = template_alias(task_dir)
with _build_lock(alias):
# alias 已存在且未强制时短路,避免重复支付数分钟构建。
if not force and Template.alias_exists(alias, **_connection_opts()):
return alias
base = resolve_docker_image(task_dir, None)
# set_user 必须先于第一条 build command,它只影响其后的
# 命令;alias 摘要已纳入 _BUILD_USER,改变构建用户会得到
# 新 alias,强制重烤而不是复用旧产物。
template = Template().from_image(base).set_user(_BUILD_USER)
for command in server_layer_commands(task_dir):
template = template.run_cmd(command)
def _build() -> None:
Template.build(
template,
alias,
**task_build_resources(task_dir),
skip_cache=force,
on_build_logs=on_logs,
**_connection_opts(),
)
# 不用 with:其 __exit__ 会 join worker,等于等完整个
# 本应被 timeout 放弃的构建。
pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
try:
pool.submit(_build).result(timeout=build_timeout_s)
finally:
pool.shutdown(wait=False)
return alias
cmd = server_cmd(command_timeout_s, default_task_id=task_dir.name)
# 环境服务器执行 agent 的命令,它运行的用户就是任务环境
# 的用户。E2B 默认非 root,而 TB2 任务镜像预期 root,
# 因此显式固定,避免 agent 静默丢失应有的权限(例如
# apt-install)。
sandbox.commands.run(
f"bash -c {shlex.quote(cmd)} > /tmp/openenv-server.log 2>&1",
background=True,
user=_BUILD_USER,
)
评论区精华
本 PR 没有任何 review 评论或要求变更,Shi-Dong 直接 APPROVED(LGTM!)。讨论价值集中在 PR 作者自己的设计说明:正文明确写出 That one is not a fix,主动区分“修复”与“契约声明”两处改动。这种自述避免了维护者把 server exec 的 user 参数误读为另一项功能修复,从而放松对 provider 默认值漂移的警惕——是 bugfix PR 中值得借鉴的沟通方式。
风险与影响
- 风险:
- 一次性重建成本:
template_alias 摘要新增 _BUILD_USER 后,所有既有 E2B 模板的 alias 都会变化,已烘焙模板会被视为缺失并重新构建一次。作者合并后实测 4 个模板 2m57s 重建干净,成本可接受,但大规模使用前需注意首次触发的构建并发。
- 模块级硬编码 root:
_BUILD_USER 同时约束构建与运行时两处语义。若未来引入非 root 设计的任务镜像,该常量需要演进为按任务配置,否则会改变任务环境而非仅构建过程。
- SDK 参数兼容性:
sandbox.commands.run(..., user=...) 依赖 E2B SDK 支持该参数。作者已对 E2B Cloud 与自托管 AgentENV 实测通过,但离线测试使用 fake 模块,无法捕获真实 SDK 版本间的参数差异。
- 影响范围:仅限
examples/experimental/openenv 的 e2b backend;daytona 后端与训练主路径不受影响。
- 影响:对用户而言,修复使 e2b backend 在 E2B Cloud 上真正可用——此前模板构建必然失败,训练无法走通;自托管 AgentENV 用户无行为变化。对系统而言,alias digest 变化会触发一次性的模板重建,之后恢复常态。对团队而言,这是 openenv 实验线从“单 provider 自测可用”走向“多 provider 契约固定”的关键一步,为后续 provider 扩展提供了可复制的用户语义处理模式。
- 风险标记:跨 provider 行为差异, 模板一次性重建, SDK 参数兼容, 模块级硬编码 root
关联脉络
- PR #2274 refactor(openenv): move duplicated sandbox helpers into the TB2 recipe: 同一 openenv/tb2_sandbox 系列,本 PR 的 ensure_task_template 依赖共享 recipe 的 server_layer_commands、task_build_resources 等;两 PR 共同收敛 e2b/daytona 后端逻辑。
- PR #2220 Add the GLM-5.2 744B x terminal-bench-2 Daytona example: tbench2 openenv 实验线的 Daytona 示例;本次修复使 e2b backend 在 E2B Cloud 可运行,扩展同一实验线的 provider 覆盖面。
- PR #2276 : PR body 明确提到 Details in #2276(合并后四个模板 2m57s 重建 + GRPO 训练走通),是本次修复的配套验证;该 PR 未包含在提供的历史列表中,标题未知。
参与讨论