Skip to content

bug(workflows): sandbox timeout can be bypassed after await #285

Description

@loading2332

问题

Workflow sandbox 使用 vm.Script(...).runInContext(..., { timeout: 1000 }) 限制不让出 CPU 的用户代码,但这个限制只覆盖首次同步执行。只要 Workflow 先经过一次 await,后续 Promise continuation 就在该 runInContext 调用之外执行,可以无限占用 sandbox 子进程的 CPU,且不会产生 result/error IPC。

最小示例:

await Promise.resolve();
while (true) {}

更贴近真实使用的形式是:

await agent("inspect the repository");
while (true) {}

模型生成的聚合或重试逻辑一旦在 await 后误入死循环,Workflow 会一直保持 running,并持续占用一个 CPU 核,直到用户显式取消或 Session 关闭。

当前行为

在当前 main a15a0d88837f580187cbb02d7fcab91bff8f0d34、Node v24.19.0、macOS 上对同一 sandbox child 做受控复现:

Workflow source 1 秒后结果
while (true) {} 收到 Error: Script execution timed out after 1000ms
await Promise.resolve(); while (true) {} 1.6 秒后仍无 result/error;子进程约占用 95% CPU,只能从父进程强制终止

相关实现:

  • extensions/workflows/sandbox-child.cjs 中 bootstrap 和初次 invoke 分别使用 runInContext(..., { timeout: 1000 })
  • context.__workflowPromise 的后续 Promise continuation 由 V8 microtask 执行,不再受这两个同步 runInContext timeout 约束。
  • tests/extensions/workflows/sandbox.test.ts 目前只覆盖首个 await 之前的 while (true) {}

预期行为

Workflow 在任意 await 前后进入不让出 CPU 的用户代码时,都应在有界时间内失败并给出明确错误,不能无限占用 sandbox 子进程。

这不要求给整个 Workflow 或 agent() 调用增加固定 wall-clock deadline。正常的长时间 agent 等待仍应被允许;需要约束的是 sandbox 中连续执行、无法响应 IPC 或取消的用户代码。

建议验收

  • await Promise.resolve(); while (true) {} 在有界时间内失败,而不是持续 running。
  • await agent(...) 返回后进入死循环也得到相同保护。
  • 现有首段同步死循环保护继续生效。
  • 长时间等待 agent() 的 Workflow 不会因为固定总时长而被误杀。
  • 失败路径保留明确的 terminal evidence,并完成子进程清理。
  • 增加回归测试;测试自身应带外部 cleanup 上限,避免失败时留下占用 CPU 的子进程。

去重

已检索开放和关闭的 issue/PR,关键词包括 workflow sandbox timeoutScript execution timed outafter awaitinfinite loopvm.Script timeoutsandbox CPU,未发现覆盖该问题的条目。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions