沙箱升级与混沌回归Sandbox upgrade and chaos regression
先说问题The problem
你的 agent 在 10 个黄金任务上 pass@1 = 85%。换一个真实网络:供应商 5xx、输出截断、JSON 损坏、worker 中途被杀、沙箱租约过期——同一套代码 pass@1 掉 50 个百分点。论文的原话:稳健性取决于系统实现,而不是模型能力。
Your agent scores pass@1 85% on ten golden tasks. Swap in the real network: provider 5xx, truncated output, corrupted JSON, a worker killed mid-run, a sandbox lease expiring. The same code drops by up to 50 points. The paper's words: robustness depends on system implementation rather than model capability.
先动手Do it first
先在页面上接线——页面只懂规则。然后跑下面那条判题,那才是真会失败的东西。Wire it on the page — the bench only knows the rules. Then run the check below; that is the part that can actually fail.
workspace_manager=
WorkspaceManagerBase
必填required
决定工具到底在哪里执行、隔离到什么粒度。每次 chat 运行和每个 /workspace 接口都依赖它。Decides where a tool actually runs and how isolated it is. Every chat run and every /workspace route depends on it.
create_app(…)
为什么这样设计Why it is built this way
01换一块积木,升一级隔离Swap one block, gain a layer of isolation
workspace_manager 从 Local 换成 DockerWorkspaceManager,「agent 跑了条命令」变成「agent 在逃不出去的容器里跑了条命令」。create_app 的其余部分一个字都不用改。
Swap workspace_manager from Local to DockerWorkspaceManager and 'the agent ran a command' becomes 'the agent ran a command inside a container it cannot leave'. Nothing else in create_app changes.
02租约有寿命A lease has a lifetime
远端沙箱有租约。一轮长任务可以跑过租期,那一轮就再也恢复不了。这是社区反复报的 bug 类,不是假想。
Remote sandboxes carry leases. A long turn can outlive its lease and that turn becomes unrecoverable. This is a repeatedly reported bug class, not a hypothesis.
03混沌注入进回归Chaos belongs in the regression
做一个拦截器,按概率注入 5xx / 截断 / 损坏 JSON / 杀 worker,然后跑同一套黄金任务,把韧性变成一张红绿表。
Build an interceptor that injects 5xx, truncation, corrupted JSON and worker kills by probability, then run the same golden set and turn resilience into a red/green table.
04评测看轨迹,不看答案Grade the trajectory, not the answer
确定性 tape 模型让同一脚本产出同一轨迹;连续跑两次不一致,就说明有隐藏状态。pass@k 要在同一任务上跑 k 次,不是 k 个任务各跑一次。
A deterministic tape model makes the same script produce the same trajectory; two runs that disagree prove hidden state. pass@k means k runs of one task, not one run of k tasks.
05tape 每次都要重建tape 每次都要重建
实测陷阱:ChatResponse / ToolCallBlock 是可变对象,agent 把执行过的调用就地标成 finished。同一批对象喂第二遍,工具一次都不执行,而最终答案文本一模一样。只看文本的评测会给这两次都打通过。
The measured trap: ChatResponse and ToolCallBlock are mutable and the agent marks the executed call finished in place. Hand the same objects to a second run and the tool never executes while the final answer text stays identical. A text-only evaluation scores both runs as passes.
用到的 APIAPIs in play
代码The code
# 1. chaos table: the fault has to be DECLARED retryable to be absorbed
class ChaosModel(ScriptedModel):
def _get_retryable_exceptions(self): # base default is () — nothing retries
return (openai.APIConnectionError, openai.InternalServerError)
# a 5xx of a type nobody declared escapes on attempt 1 -> [RED]
# 2. determinism, with the tape rebuilt every run (mutable blocks, see the trap)
def golden(): # not a module-level list
return [tool_response("lookup", call_id="lookup-1", order_id="A-1001"),
text_response("order A-1001 is refundable")]
runs = [await trajectory(agent, golden()) for _ in range(3)]
assert len({tuple(r) for r in runs}) == 1
assert "tool_result:success" in runs[0] # or the equality passes vacuously
# 3. the sandbox swap is one block, and eviction is lazy
app = create_app(**kwargs_for({**DEFAULT_ASSEMBLY, "workspace_manager": "docker_workspace"}))
# ttl expiry is checked inside get_workspace: the old lease is closed, a new one built
# local workdir files survive that; a container or remote lease does not
判题The check
python lab.py run c7
它断言了什么What it asserts
workspace_manager local -> docker: 7 create_app kwargs, same key set, only workspace_manager differs — the other 6 blocks are untouchedblock docker_workspace on this host -> ValueError: block 'docker_workspace' needs `pip install aiodocker`block e2b_workspace on this host -> ValueError: block 'e2b_workspace' needs `pip install e2b`isolation grain: PER_AGENT two sessions -> 4bf1218b (shared), PER_SESSION -> 846d9c09/d8228b97 (split), PER_USER two agents -> 48ea3da4 (one tenant-wide sandbox)ttl=0 lease: the next get_workspace evicted and rebuilt the sandbox (w-lease -> new object, cache 1); a local workdir keeps its files across eviction, a container/remote lease does not — that is the turn you cannot resumechaos table, 4 absorbed / 2 fatal, and every row landed where the framework's own retry rules say it should[GREEN] provider 5xx, exception type declared retryable -> 3 attempt(s) for 2 tape step(s), tool still ran, answer intact[RED] provider 5xx, exception type NOT declared -> Mystery500: HTTP 500 nobody declared[RED] stream ends without is_last=True -> AssertionError: the cut-off text became the answer: 'order A-1001 is ref'[GREEN] asyncio.CancelledError mid-model-call -> finished_reason=interrupted, reply 'I notice the interruption. How can', no crash[GREEN] tool arguments cut off mid-JSON -> repaired into {'order_id': 'A-1001'} and executed[GREEN] tool arguments that are not JSON -> tool ran 0x, tool_result:error handed back to the modelsame script rebuilt per run, 3 replays -> one trajectory ['text', 'hint', 'tool_call:lookup', 'tool_result:success', 'text'], 1 distinctreplay trap, the reason each replay above rebuilds its tape: hand the same ChatResponse objects to a second run and the agent sees its own finished marker on the tool call — run 1 executed the tool ([{'order_id': 'A-1001'}]), run 2 executed nothing ([]), yet both answered 'order A-1001 is refundable'. A text-only evaluation scores that as two passes while the side effect quietly disappearedpass@1 measured 5 ways: 5 runs of ONE task under 40% injected 5xx with max_retries=1 -> 4/5 passed (80%); one run of that task alone says True. A single green run is a coin flip.kill -9 during a turn of 24 refunds: 7 landed before death, a second process over the same storage reads status=idle (not running, and nothing detected the death), 10 ledger rows after 3s of waiting, unanswered tool calls in the tail: []
会踩的坑Where it hurts
用「跑一次通过」当发布门槛。一次通过只证明这次运气好;把 k 设成 5,你才第一次看到真实确定率。
Shipping on a single passing run. One pass only proves you got lucky; set k to 5 and you see the real determinism for the first time.
费曼闸门Say it back
为什么换掉 workspace_manager 一个参数就能升级隔离级别?这种设计对「谁能安全地改生产配置」意味着什么?
Why does changing one workspace_manager argument upgrade isolation? What does that design imply about who may safely change production config?
页面只存通行码的哈希。判题没真跑过,这里就解不开。The page stores a hash, not the code. Nothing unlocks until the command really ran.