AS AgentScopeAPI 手册 / API Reference
MODULE C7 · stress · 260 XP · 50 MIN

沙箱升级与混沌回归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.

工作区沙箱Execution sandbox 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.

Empty: the service will not boot空着:服务起不来
你拼出来的东西What you assembled
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

C7 · Python
# 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

<span class="ui-zh">运行这条</span><span class="ui-en">Run this</span>
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 untouched
  • block 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 resume
  • chaos 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 model
  • same script rebuilt per run, 3 replays -> one trajectory ['text', 'hint', 'tool_call:lookup', 'tool_result:success', 'text'], 1 distinct
  • replay 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 disappeared
  • pass@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.

基于官方固定版本源码静态生成Generated from pinned official sources · Python 2.0.11 · Java 2.0.4
签名与文档为静态抽取;如有疑问请以源码为准。Signatures and docs are extracted statically; verify against the source when in doubt.