Skip to content

fix(workflows): enforce sandbox execution timeout across async continuations (#285) - #287

Open
hasak21 wants to merge 2 commits into
openpi-dev:mainfrom
hasak21:fix/workflows-sandbox-timeout-after-await
Open

fix(workflows): enforce sandbox execution timeout across async continuations (#285)#287
hasak21 wants to merge 2 commits into
openpi-dev:mainfrom
hasak21:fix/workflows-sandbox-timeout-after-await

Conversation

@hasak21

@hasak21 hasak21 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Problem

Workflow sandbox only enforced { timeout: 1000 } on the initial synchronous compilation/invocation in runInContext. Once user code reached an await (e.g. await Promise.resolve(); while (true) {} or await agent(...); while (true) {}), subsequent continuations executed in microtasks outside runInContext, 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

  1. Defined SANDBOX_SYNC_TIMEOUT_MS = 1_000 in extensions/workflows/sandbox.ts.
  2. Added an execution watchdog on the host runner that arms during synchronous execution windows and disarms while waiting for pending agent() requests.
  3. Automatically re-arms the watchdog when in-flight agent requests settle back to 0.
  4. Cleanly disarms the watchdog on normal completion, cancellation, and error cleanup.
  5. Added deterministic regression tests in tests/extensions/workflows/sandbox.test.ts for non-yielding code after await Promise.resolve() and await agent().

Validation

  • node --test --experimental-strip-types tests/extensions/workflows/sandbox.test.ts - 29/29 passed
  • bun run check - passed (format, lint, typecheck)
  • bun run test - 1,000 tests passed (970 Node + 30 Vitest, 0 failures)
  • git diff --check - clean

Impact

  • User-visible behavior: Workflows entering synchronous infinite loops after await now fail with a clear timeout error rather than hanging indefinitely.
  • Model-visible context/tools: None.
  • Runtime/lifecycle: Child process is cleanly terminated and resources reaped on timeout.
  • Persisted config/data: None.
  • Compatibility or risk: None; legitimate long-running agent calls remain unbounded.

@somewan820 somewan820 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #285 要求修复 Workflow sandbox 在 await 后进入不让出 CPU 的代码可绕过 VM timeout 的问题,同时保留长时间 agent 等待、明确终态证据和子进程清理,并为失败场景增加带外部 cleanup 上限的回归测试。

阻塞问题

  1. tests/extensions/workflows/sandbox.test.ts:208-221 新增的两个死循环测试没有外部超时和清理兜底。它们直接等待 assert.rejects(run(...));测试辅助函数内部创建的 AbortController 未暴露,也没有测试级 deadline 或 finally 清理。触发 watchdog、IPC 或子进程终止路径回归时,测试会永久挂起,并留下持续占用 CPU 的 sandbox 子进程。这直接未满足 Issue 验收中‘测试自身应带外部 cleanup 上限’的要求。

非阻塞简化建议

  1. extensions/workflows/sandbox.ts:23-27SANDBOX_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 checkbun run test 或实际子进程 smoke,以上实现判断均为静态审查结论。
  • 新测试未断言 timeout 后的子进程退出、请求 abort 或 terminal evidence 持久化。
  • 宿主事件循环被阻塞时 watchdog 自身也无法调度,这需要指定 head 上的实际运行验证。

本轮未发现除测试清理缺口外的阻塞运行时逻辑问题。

@somewan820 somewan820 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 的回归验证。静态审查,未运行测试。

@hasak21

hasak21 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @somewan820!

Addressed in commit 8f4d30b:

  1. Initial invocation & microtask yield coverage: armWatchdog() is now explicitly and synchronously armed right before dispatching child.send({ kind: "init" }), ensuring the watchdog actively bounds execution from the very start of the process through all initial microtask yields.
  2. Full async continuation lifecycle:
    • Synchronously armed on init (covers initial execution and pre/post-yield microtasks);
    • Disarmed when raw.kind === "agent" arrives (allows unbounded duration for legitimate long-running LLM/network calls);
    • Automatically re-armed in sendResult when all in-flight agent requests settle back to 0;
    • Disarmed and reaped in cleanup().
  3. Expanded regression test coverage: Added comprehensive regression tests in tests/extensions/workflows/sandbox.test.ts covering:
    • Synchronous loops after await Promise.resolve();
    • Synchronous loops after chained .then() microtasks;
    • Synchronous loops after single await agent();
    • Synchronous loops between sequential agent() calls;
    • Synchronous loops after await parallel(...) agent calls.

All 32/32 sandbox tests and the full 1,003-test suite pass cleanly (bun run check && bun run test). Please re-review when you have a moment!

@tt-a1i tt-a1i left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

按精确 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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 这里仅在全局 activeAgentRequests.size === 0 时重新 arm,会漏掉“一个并行 agent 已返回并恢复脚本执行、另一个仍 pending”的执行窗口。此时恢复的分支可进入 while(true),但另一个 pending 请求让 watchdog 永久关闭。请跟踪 child 的实际执行/等待边界,而不是用全局 pending 数推断,并新增 fast 分支返回后死循环、slow 分支持续等待的对抗测试。

@somewan820 somewan820 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

针对我在 review 5058013325 中提出的阻塞项重新复核后,这部分目前仍未解决,因此暂不能 approve。

我提出的要求是:新增的死循环回归测试必须有测试级的外部 deadline 和 cleanup 兜底,避免 watchdog、IPC 或子进程终止路径回归时测试永久挂起并遗留持续占用 CPU 的 sandbox 子进程。

当前 tests/extensions/workflows/sandbox.test.tsrun() helper 仍在内部创建 AbortController,但没有向测试暴露;相关测试仍直接等待 assert.rejects(run(...)),没有 Promise.race/测试级 timeout,也没有 finally 中的 abort/清理。因此作者回复中新增的 watchdog 覆盖和“全量测试通过”并不能满足这个要求:它们只能证明当前实现会结束,不能保证测试在实现失效时自身能够有界退出并清理。

请为这些非让出 CPU 的测试增加外部超时与 abort/子进程 cleanup 兜底后再请复核。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants