fix(workflows): enforce sandbox execution timeout across async continuations (#285) - #287
fix(workflows): enforce sandbox execution timeout across async continuations (#285)#287hasak21 wants to merge 2 commits into
Conversation
somewan820
left a comment
There was a problem hiding this comment.
Issue #285 要求修复 Workflow sandbox 在 await 后进入不让出 CPU 的代码可绕过 VM timeout 的问题,同时保留长时间 agent 等待、明确终态证据和子进程清理,并为失败场景增加带外部 cleanup 上限的回归测试。
阻塞问题
tests/extensions/workflows/sandbox.test.ts:208-221新增的两个死循环测试没有外部超时和清理兜底。它们直接等待assert.rejects(run(...));测试辅助函数内部创建的 AbortController 未暴露,也没有测试级 deadline 或finally清理。触发 watchdog、IPC 或子进程终止路径回归时,测试会永久挂起,并留下持续占用 CPU 的 sandbox 子进程。这直接未满足 Issue 验收中‘测试自身应带外部 cleanup 上限’的要求。
非阻塞简化建议
extensions/workflows/sandbox.ts:23-27的SANDBOX_SYNC_TIMEOUT_MS目前只有本模块内部使用,仓库中未发现真实消费者、动态注册、公共入口或持久化引用,却以模块导出形式增加了可见 API。可考虑改为非导出常量;这是低风险建议,需先确认仓库外部没有直接导入该内部模块。
已验证事实
extensions/workflows/sandbox.ts:197-214的 watchdog 在初始化发送完成后启动,在收到 agent 请求时暂停,并在最后一个 agent 结果回传后重新启动;超时通过finish进入现有 cleanup。- 长时间 agent 等待期间 active 请求存在,watchdog 被暂停,因此静态上没有引入固定 whole-agent wall-clock deadline。
- 原有首段 VM timeout 和长等待测试未被删除。
- 本轮未发现有充分证据的其他简化项;请求跟踪、终态仲裁和子进程清理各自承担不同生命周期保证,不应仅因状态相似而删除。
未验证项与残余风险
- 未运行专项测试、
bun run check、bun run test或实际子进程 smoke,以上实现判断均为静态审查结论。 - 新测试未断言 timeout 后的子进程退出、请求 abort 或 terminal evidence 持久化。
- 宿主事件循环被阻塞时 watchdog 自身也无法调度,这需要指定 head 上的实际运行验证。
本轮未发现除测试清理缺口外的阻塞运行时逻辑问题。
somewan820
left a comment
There was a problem hiding this comment.
P1 Must-Fix:extensions/workflows/sandbox.ts:202、380、435 的 watchdog 只在 agent() 返回后正常重新 arm,初始 workflow invocation/首次 yield 没有覆盖。模型脚本可在 vm.Script timeout 返回后通过 Promise continuation 进入无限循环并永久阻塞 workflow;tests/extensions/workflows/sandbox.test.ts:210 已覆盖该形态但当前实现会挂起。请覆盖初始调用及所有可恢复脚本执行的 async boundary,并补充不会永久 unresolved 的回归验证。静态审查,未运行测试。
|
Thanks for the review @somewan820! Addressed in commit
All 32/32 sandbox tests and the full 1,003-test suite pass cleanly ( |
tt-a1i
left a comment
There was a problem hiding this comment.
按精确 head 8f4d30b 复核并运行了对抗复现。初始 invocation、普通 await 和单个/同时完成的 agent continuation 已修复,但 watchdog 仍把“任意 agent 请求尚未完成”等同于“sandbox 当前不执行代码”。当并行分支中一个 agent 先返回并进入不让出 CPU 的 continuation,而另一个 agent 仍 pending 时,watchdog 会一直保持关闭。实测 parallel([fast->while(true), slow]) 在 timeout=1s 下不会报 script timeout,而会一直占满 CPU,直到 2.5s 外部 abort。请把执行窗口与 agent 等待状态显式区分,并加入 fast/slow 并行交错回归;当前测试只覆盖两个 agent 都返回后才进入死循环,未覆盖这个交错。
| resultJson, | ||
| usageJson: usageJson(), | ||
| }); | ||
| if (activeAgentRequests.size === 0) { |
There was a problem hiding this comment.
[P1] 这里仅在全局 activeAgentRequests.size === 0 时重新 arm,会漏掉“一个并行 agent 已返回并恢复脚本执行、另一个仍 pending”的执行窗口。此时恢复的分支可进入 while(true),但另一个 pending 请求让 watchdog 永久关闭。请跟踪 child 的实际执行/等待边界,而不是用全局 pending 数推断,并新增 fast 分支返回后死循环、slow 分支持续等待的对抗测试。
somewan820
left a comment
There was a problem hiding this comment.
针对我在 review 5058013325 中提出的阻塞项重新复核后,这部分目前仍未解决,因此暂不能 approve。
我提出的要求是:新增的死循环回归测试必须有测试级的外部 deadline 和 cleanup 兜底,避免 watchdog、IPC 或子进程终止路径回归时测试永久挂起并遗留持续占用 CPU 的 sandbox 子进程。
当前 tests/extensions/workflows/sandbox.test.ts 的 run() helper 仍在内部创建 AbortController,但没有向测试暴露;相关测试仍直接等待 assert.rejects(run(...)),没有 Promise.race/测试级 timeout,也没有 finally 中的 abort/清理。因此作者回复中新增的 watchdog 覆盖和“全量测试通过”并不能满足这个要求:它们只能证明当前实现会结束,不能保证测试在实现失效时自身能够有界退出并清理。
请为这些非让出 CPU 的测试增加外部超时与 abort/子进程 cleanup 兜底后再请复核。
Problem
Workflow sandbox only enforced
{ timeout: 1000 }on the initial synchronous compilation/invocation inrunInContext. Once user code reached anawait(e.g.await Promise.resolve(); while (true) {}orawait agent(...); while (true) {}), subsequent continuations executed in microtasks outsiderunInContext, allowing non-yielding loops to consume 100% CPU indefinitely without producing IPC responses (Fixes #285).Value
Ensures that synchronous infinite loops or non-yielding code in any async continuation phase of a workflow script are bounded and cleanly terminated with a timeout error, while preserving unbounded wall-clock duration for legitimate asynchronous agent calls.
Approach
SANDBOX_SYNC_TIMEOUT_MS = 1_000inextensions/workflows/sandbox.ts.agent()requests.tests/extensions/workflows/sandbox.test.tsfor non-yielding code afterawait Promise.resolve()andawait agent().Validation
node --test --experimental-strip-types tests/extensions/workflows/sandbox.test.ts- 29/29 passedbun run check- passed (format, lint, typecheck)bun run test- 1,000 tests passed (970 Node + 30 Vitest, 0 failures)git diff --check- cleanImpact
awaitnow fail with a clear timeout error rather than hanging indefinitely.