Parcourir la source

feat(code-runtime): complete sandboxed Node execution and Session fixtures

Tianyi Cui il y a 1 semaine
Parent
commit
8e19ec4962
82 fichiers modifiés avec 1928 ajouts et 345 suppressions
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md
  7. 6 0
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.i18n.yaml
  8. 56 0
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.md
  9. 56 0
      .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.zh.md
  10. 2 2
      .agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml
  11. 17 26
      .agents/notes/implemented/feature/2026-06-15-ptc.md
  12. 17 26
      .agents/notes/implemented/feature/2026-06-15-ptc.zh.md
  13. 2 2
      .agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml
  14. 2 2
      .agents/notes/implemented/testing/2026-09-08-ci-completion-observations.i18n.yaml
  15. 2 2
      .agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md
  16. 2 2
      .agents/notes/implemented/testing/2026-09-08-ci-completion-observations.zh.md
  17. 2 2
      .agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.i18n.yaml
  18. 1 1
      .agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.md
  19. 1 1
      .agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.zh.md
  20. 2 2
      .agents/notes/rejected/simplification/2026-07-04-prune-dead-core-spine-api.i18n.yaml
  21. 2 2
      docs/subsystems/code-runtime.i18n.yaml
  22. 38 10
      docs/subsystems/code-runtime.md
  23. 38 10
      docs/subsystems/code-runtime.zh.md
  24. 2 2
      docs/testing.i18n.yaml
  25. 1 1
      docs/testing.md
  26. 1 1
      docs/testing.zh.md
  27. 2 2
      docs/user/guide/network-proxy.i18n.yaml
  28. 1 1
      docs/user/guide/network-proxy.md
  29. 1 1
      docs/user/guide/network-proxy.zh.md
  30. 2 2
      packages/README.i18n.yaml
  31. 1 1
      packages/README.md
  32. 1 1
      packages/README.zh.md
  33. 2 2
      packages/client/ui-tool/package.json
  34. 18 2
      packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts
  35. 2 2
      packages/code-runtime/README.i18n.yaml
  36. 2 2
      packages/code-runtime/README.md
  37. 2 2
      packages/code-runtime/README.zh.md
  38. 2 2
      packages/code-runtime/code-runtime-node/README.i18n.yaml
  39. 59 62
      packages/code-runtime/code-runtime-node/README.md
  40. 66 69
      packages/code-runtime/code-runtime-node/README.zh.md
  41. 1 0
      packages/code-runtime/code-runtime-node/package.json
  42. 32 9
      packages/code-runtime/code-runtime-node/src/index.ts
  43. 29 0
      packages/code-runtime/code-runtime-node/src/output-stream.ts
  44. 354 0
      packages/code-runtime/code-runtime-node/tests/host-failures.spec.ts
  45. 25 0
      packages/code-runtime/code-runtime-node/tests/launch.spec.ts
  46. 26 0
      packages/code-runtime/code-runtime-node/tests/output-ledger.spec.ts
  47. 35 0
      packages/code-runtime/code-runtime-node/tests/output-stream.spec.ts
  48. 4 2
      packages/code-runtime/code-runtime-node/tests/process.spec.ts
  49. 14 1
      packages/code-runtime/code-runtime-node/tests/runtime.spec.ts
  50. 4 0
      packages/code-runtime/code-runtime-node/tests/setup.ts
  51. 2 2
      packages/code-runtime/code-runtime/README.i18n.yaml
  52. 12 11
      packages/code-runtime/code-runtime/README.md
  53. 12 11
      packages/code-runtime/code-runtime/README.zh.md
  54. 2 2
      packages/core/agent-tool-presentation/README.i18n.yaml
  55. 1 1
      packages/core/agent-tool-presentation/README.md
  56. 1 1
      packages/core/agent-tool-presentation/README.zh.md
  57. 2 2
      packages/experimental/code-runtime-python/README.i18n.yaml
  58. 6 4
      packages/experimental/code-runtime-python/README.md
  59. 6 5
      packages/experimental/code-runtime-python/README.zh.md
  60. 12 31
      packages/spill/spill-policy/tests/spill-policy.spec.ts
  61. 2 2
      packages/util/http-proxy/README.i18n.yaml
  62. 1 1
      packages/util/http-proxy/README.md
  63. 1 1
      packages/util/http-proxy/README.zh.md
  64. 2 2
      pnpm-lock.yaml
  65. 10 0
      scripts/type-equiv.manifest.json
  66. 53 0
      snapshots/session/ptc-node-read-only/cordis.snapshot.yml
  67. 36 0
      snapshots/session/ptc-node-read-only/cordis.yml
  68. 78 0
      snapshots/session/ptc-node-read-only/replay.override.json
  69. 14 0
      snapshots/session/ptc-node-read-only/session.v3.jsonl
  70. 15 0
      snapshots/session/ptc-node-read-only/snapshot.yml
  71. 232 0
      snapshots/session/ptc-node-read-only/system-prompt.expected.md
  72. 26 0
      snapshots/session/ptc-node-read-only/tool-schemas.expected.json
  73. 0 0
      snapshots/session/ptc-node-read-only/workspace.expected/.empty
  74. 53 0
      snapshots/session/ptc-node-workspace/cordis.snapshot.yml
  75. 36 0
      snapshots/session/ptc-node-workspace/cordis.yml
  76. 78 0
      snapshots/session/ptc-node-workspace/replay.override.json
  77. 14 0
      snapshots/session/ptc-node-workspace/session.v3.jsonl
  78. 15 0
      snapshots/session/ptc-node-workspace/snapshot.yml
  79. 232 0
      snapshots/session/ptc-node-workspace/system-prompt.expected.md
  80. 26 0
      snapshots/session/ptc-node-workspace/tool-schemas.expected.json
  81. 1 0
      snapshots/session/ptc-node-workspace/workspace.expected/ptc-created.txt
  82. 2 2
      tsconfig.client.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: e8c38a7b96cbff9feae2271fcd9a5c41087c62c4
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 52ca2e13901d9469e2f9663936acb766a934e8f7
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: 1ef8059e23a414588e9d6d421a07058dfb362fe1
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: e1f24a809d2527bf5c69eacffc183ddf3badbc59

Fichier diff supprimé car celui-ci est trop grand
+ 2 - 2
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md


Fichier diff supprimé car celui-ci est trop grand
+ 2 - 2
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md
-2026-08-27-outbound-proxy-policy.md: 015b6edd3f4153f29db4084c92e8e8da8cad70e2
-2026-08-27-outbound-proxy-policy.zh.md: c7bdcaee138ac41a55733d76da1063b25f9ba633
+2026-08-27-outbound-proxy-policy.md: 719bcac2f1c2322977d381a315de43b62e8b6951
+2026-08-27-outbound-proxy-policy.zh.md: ad1bfce85a1c4d4140863722c84b42d62932080a

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md

@@ -70,7 +70,7 @@ Weighed against that, telemetry is the one outbound channel whose loss costs the
 
 **Read the operating system's proxy settings.** Rejected for this change. Only Codex and Reasonix among six surveyed products do it, and Codex keeps it behind a default-off flag. Measured on the author's machine, it would have found nothing: the proxy application had written the setting to the Wi-Fi service while the primary interface was a USB ethernet adapter with no proxy, so `scutil --proxy` reported none while the exported variables worked. It also needs its own bypass matcher, because an operating system list carries CIDR entries that neither undici nor Node matches.
 
-**Give the `code-runtime` worker the proxy too.** Rejected. Model-authored programs run there with no ambient environment at all — a stronger containment than the scrubbed environment spawned commands get — and a proxy URL may carry credentials. Handing model code a credentialed URL to reach the network is the wrong trade; the exclusion is recorded in that package's limitations.
+**Give model-authored code the proxy too.** Rejected because a proxy URL may carry credentials. Node code-runtime processes and workflow workers keep those settings outside the program environment; direct network use remains subject to the program's execution policy.
 
 ## Consequences
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md

@@ -70,7 +70,7 @@ URL 层策略未受影响:仅 `http(s)`、禁止内嵌凭据、长度上限与
 
 **读取操作系统的代理设置。** 本次变更中被否决。所调研的六个产品中只有 Codex 与 Reasonix 这样做,且 Codex 把它放在默认关闭的开关之后。在作者机器上实测,它什么也读不到:代理软件把设置写在了 Wi-Fi 服务上,而主接口是一块没有代理的 USB 以太网卡,因此 `scutil --proxy` 报告无代理,而导出的环境变量却工作正常。它还需要自带的绕过匹配器,因为操作系统的列表含有 undici 与 Node 都不匹配的 CIDR 条目。
 
-**也把代理给 `code-runtime` worker。** 被否决。模型编写的程序在那里运行时完全没有环境变量——这比派生命令得到的 scrubbed 环境更严——而代理 URL 可能携带凭据。把带凭据的 URL 交给模型代码去访问网络是错误的取舍;该排除已记入那个包的限制清单。
+**也把代理配置交给模型编写的代码。** 不采纳,因为代理 URL 可能携带凭据。Node code-runtime 进程与 workflow worker 不在程序环境中提供这些设置;直接网络访问仍受程序执行策略约束。
 
 ## Consequences
 

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.md
+2026-09-11-sandboxed-node-code-runtime.md: 6f28b3c4656797388366e63dd334597d23a23b84
+2026-09-11-sandboxed-node-code-runtime.zh.md: 2c3d19fbdf261d984fd30baf2c347b4e6c15f64b

+ 56 - 0
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.md

@@ -0,0 +1,56 @@
+# Agent Note: Sandboxed Node execution for PTC
+
+Status: implemented
+
+English | [中文](2026-09-11-sandboxed-node-code-runtime.zh.md)
+
+## Problem
+
+A Node worker isolates JavaScript state but does not apply the calling Session's OS sandbox policy. Model code can import filesystem and subprocess APIs directly, bypassing the tool-policy path even when nested `tools.*` calls receive the correct checks. Terminating the worker also does not establish that its child processes have stopped.
+
+The [PTC foundation](../feature/2026-06-15-ptc.md) remains responsible for registry presentation, generated bindings, dispatch logging and one-shot settlement. This decision supersedes its worker-based execution, trust and budget realization while preserving those consumer rules.
+
+## Decision
+
+`dsh-code-runtime-node` runs each program in one fresh Node process. The host resolves execution choices, confines the launch through the same `ctx.sandbox` provider as Bash, and gives process lifetime to `ctx.subprocess`. The child evaluates erasable TypeScript with direct Node APIs, an empty model environment and host-provided asynchronous bindings. No worker or persistent kernel remains inside this provider.
+
+### Resolved inputs and policy
+
+`CodeRuntime.resolve(request)` validates supported options and supplies a complete `CodeRunSpec`; `run(spec)` does not introduce defaults. PTC passes the calling Session's cwd and resolved standing policy. Direct runtime callers receive deployment defaults through the same resolver. The filesystem and subprocess providers share one execution world, and bootstrap paths cross through the filesystem's explicit host-file mapping or a configured preinstalled bootstrap.
+
+File mode, observed denial and enforcement completeness travel in `CodeRunResult.sandbox` separately from the program outcome. Restricted execution fails if the required sandbox backend cannot launch. Full access is an explicit policy mode. Program success does not prove full enforcement, and neither the `process` descriptor nor the extra control pipe claims multi-tenant isolation.
+
+The private Python provider keeps its existing execution implementation and configured wall deadline. Its resolver accepts cwd but rejects an explicit file policy or per-call timeout override; a capability descriptor never silently grants unsupported protection.
+
+### Control and lifetime
+
+The subprocess owner supplies a dedicated inherited binary control channel, separate from program stdout/stderr and launcher lifecycle IPC. The host bounds frames, queued writes, pending calls and outstanding argument bytes, then validates call identity and the binding allowlist before dispatch. Model code can write to that channel, so its bytes remain untrusted.
+
+Program completion, timeout, cancellation and protocol failure all close execution through the managed process owner. Result selection stops the execution timer; cleanup then waits for the direct outcome and managed-range quiescence. The PTC bridge separately aborts and drains nested tool dispatches before its outer tool result settles. A denial or transport failure never automatically replays a program whose effects may already have occurred.
+
+### Resource limits
+
+The default elapsed deadline is 120 seconds, capped at 600 seconds by default. It includes runtime setup and nested tool or approval waits. V8 old-generation memory, serialized outer output and control traffic have separate configured bounds. The heap limit excludes native allocations and descendant memory, and elapsed time is not a process-tree CPU budget.
+
+## Alternatives considered
+
+**Keep the worker and add tool checks.** Tool checks cannot intercept direct Node imports or establish OS confinement. Keeping a worker inside a confined supervisor process preserves worker metering but adds another execution lifetime without supplying a process-tree CPU limit.
+
+**Use an in-process JavaScript realm.** A language-level realm does not enforce the filesystem and process policy required for direct Node APIs. OS confinement and a host-owned process lifetime are the required protections.
+
+**Copy Codex Code Mode's execution model.** [Codex Code Mode](https://github.com/openai/codex/blob/02a8f038b87ad34d4a1dc5058eda26972ed7aa6c/codex-rs/code-mode-protocol/src/description.rs) exposes fresh raw-JavaScript isolates and host tool callbacks, with yield/wait observations separate from execution lifetime. Removing Node APIs or adding resumable cells would change PTC's programming and logging model. The retained design keeps direct Node access under OS policy and one-shot results.
+
+**Treat worker active time as a CPU limit.** Event-loop utilization counts active wall time in one worker, not CPU consumed by Node and its descendants. The process provider uses a host-owned elapsed deadline and does not make that stronger claim.
+
+## Consequences
+
+Each `run_code` pays for a Node process launch and OS sandbox setup. Long nested tools and approvals consume the same elapsed budget as direct program work. Cleanup can extend the caller's wait beyond that deadline. Stronger descendant containment remains dependent on the selected subprocess backend; its fallback limitations remain visible rather than being upgraded by the runtime label.
+
+`run_code` requires the program and its description. Service callers can resolve supported execution choices; model-facing timeout and approval controls have a separate consumer owner. Every nested tool still passes through its normal policy and logging path.
+
+<a id="deferred-timeout-design"></a>
+## Deferred timeout design
+
+The elapsed defaults are a revisitable deployment choice. Further design must separate responsiveness from termination: yielding output to a model does not itself stop a program, and adding waitable cells introduces ownership, cancellation, partial-output logging, turn-end and resume obligations.
+
+Open questions include whether approval waits consume the program budget, how sequential long-running tools compose, whether a separate total-lifetime backstop is needed, and which process-tree CPU/RSS limits can be enforced consistently. A persistent kernel additionally needs a Session-log representation of retained state. These questions do not silently pause or extend the shipped elapsed timer.

+ 56 - 0
.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.zh.md

@@ -0,0 +1,56 @@
+# Agent Note: PTC 的沙箱 Node 执行
+
+Status: implemented
+
+[English](2026-09-11-sandboxed-node-code-runtime.md) | 中文
+
+## 问题
+
+Node worker 隔离 JavaScript 状态,但不应用调用 Session 的 OS 沙箱策略。模型代码可以直接导入文件系统与子进程 API,绕过工具策略路径,即使嵌套 `tools.*` 调用受到正确检查。终止 worker 也不能证明其子进程已停止。
+
+[PTC 基础](../feature/2026-06-15-ptc.zh.md)继续负责注册表呈现、生成绑定、分派日志和一次性结算。本决策取代其中基于 worker 的执行、信任与预算实现,同时保留消费方规则。
+
+## 决策
+
+`dsh-code-runtime-node` 在一个全新 Node 进程中运行每个程序。Host 解析执行选择,通过与 Bash 相同的 `ctx.sandbox` 提供方约束启动,并将进程生命周期交给 `ctx.subprocess`。子进程以直接 Node API、空模型环境和 Host 提供的异步绑定求值可擦除 TypeScript。本提供方不保留 worker 或持久内核。
+
+### 已解析输入与策略
+
+`CodeRuntime.resolve(request)` 验证支持的选项并补全 `CodeRunSpec`;`run(spec)` 不引入默认值。PTC 传入调用 Session 的 cwd 与已解析常设策略。直接运行时调用方通过同一解析器取得部署默认值。文件系统与子进程提供方共享一个执行世界,bootstrap 路径通过文件系统的显式宿主文件映射或配置的预安装 bootstrap 传递。
+
+文件模式、观察到的拒绝与强制完整性通过 `CodeRunResult.sandbox` 独立于程序结果传递。所需沙箱后端无法启动时,受限执行失败。完整访问是显式策略模式。程序成功不证明完整强制能力,`process` 描述符与额外控制管道也不声明多租户隔离。
+
+私有 Python 提供方保留现有执行实现与配置的经过时间截止。其解析器接受 cwd,但拒绝显式文件策略或单次 timeout 覆盖;能力描述符不会静默授予不受支持的保护。
+
+### 控制与生命周期
+
+子进程所有者提供专用的继承式二进制控制通道,与程序 stdout/stderr 及 launcher 生命周期 IPC 分开。Host 限制帧、排队写入、待处理调用和未完成参数字节,然后在分派前验证调用身份与绑定允许列表。模型代码可以写入该通道,因此其中字节仍不可信。
+
+程序完成、超时、取消与协议失败都通过受管进程所有者关闭执行。选择结果后停止执行计时器;清理随后等待直接结果与受管范围停稳。PTC 桥接在外层工具结果结算前,另行取消并排空嵌套工具分派。拒绝或传输失败绝不自动重放可能已经产生副作用的程序。
+
+### 资源限制
+
+默认经过时间截止为 120 秒,默认上限为 600 秒。它包括运行时准备以及嵌套工具或审批等待。V8 老生代内存、序列化外层输出与控制通信具有独立配置的上限。堆限制不包含原生分配和后代内存,经过时间也不是进程树 CPU 预算。
+
+## 考虑过的替代方案
+
+**保留 worker 并增加工具检查。** 工具检查不能拦截直接 Node 导入,也不能建立 OS 约束。在受限监督进程中保留 worker 可以保留 worker 计量,但会增加一层执行生命周期,仍不能提供进程树 CPU 限制。
+
+**使用同进程 JavaScript realm。** 语言级 realm 不强制直接 Node API 所需的文件系统与进程策略。所需保护是 OS 约束与 Host 拥有的进程生命周期。
+
+**复制 Codex Code Mode 的执行模型。** [Codex Code Mode](https://github.com/openai/codex/blob/02a8f038b87ad34d4a1dc5058eda26972ed7aa6c/codex-rs/code-mode-protocol/src/description.rs) 提供全新原始 JavaScript isolate 与 Host 工具回调,yield/wait 观察与执行生命周期分开。移除 Node API 或增加可恢复单元会改变 PTC 的编程与日志模型。保留的设计在 OS 策略下提供直接 Node 访问,并返回一次性结果。
+
+**把 worker active time 当作 CPU 限制。** 事件循环占用率计量单个 worker 的活跃经过时间,不是 Node 及其后代消耗的 CPU。进程提供方使用 Host 拥有的经过时间截止,不作更强声明。
+
+## 后果
+
+每次 `run_code` 都承担 Node 进程启动与 OS 沙箱准备成本。较长的嵌套工具和审批消耗与直接程序工作相同的经过时间预算。清理可能让调用方在截止后继续等待。更强的后代约束仍取决于所选子进程后端;其 fallback 限制保持可见,不会被运行时标签提升。
+
+`run_code` 要求程序及其描述。服务调用方可以解析支持的执行选择;面向模型的 timeout 与审批控制由独立消费方负责。每个嵌套工具仍经过正常策略与日志路径。
+
+<a id="deferred-timeout-design"></a>
+## 延后评估 timeout 设计
+
+经过时间默认值是可以重新评估的部署选择。后续设计必须将响应及时性与终止分开:向模型交出输出本身不会停止程序;增加可等待单元会引入所有权、取消、部分输出日志、轮次结束和恢复义务。
+
+开放问题包括审批等待是否消耗程序预算、顺序执行的长任务工具如何组合、是否需要独立的总生命周期兜底,以及哪些进程树 CPU/RSS 上限可以一致强制执行。持久内核还需要在 Session 日志中表示保留状态。这些问题不会静默暂停或延长已发布的经过时间计时器。

+ 2 - 2
.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-15-ptc.md
-2026-06-15-ptc.md: 43bd4a4fd5c49a449ceccb7f1889b80b0844214d
-2026-06-15-ptc.zh.md: a6aaf203186ad3023ce39a9df04fc1b233db88eb
+2026-06-15-ptc.md: 18f45d8c0fef79962db233661be05c63b4eba2bb
+2026-06-15-ptc.zh.md: 43e52a438a5dda887deaf8204d8b0f95445c53c8

+ 17 - 26
.agents/notes/implemented/feature/2026-06-15-ptc.md

@@ -12,7 +12,7 @@ For multi-step tool work this is token-heavy and serial. The model cannot compos
 
 Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) proposes an alternative grounded in a simple observation: LLMs are better at writing code than at emitting tool calls, because they have seen millions of lines of real code and comparatively few contrived tool-calling traces. Instead of one tool call per step, the model writes a TypeScript program against a generated API over the tools, the program executes in a sandboxed runtime, and the model curates what comes back — only what it prints or returns — instead of every intermediate result.
 
-Tool presentation belongs to the registry that owns tool visibility: implementing a second presentation as an after-the-fact waterfall transform would make correctness depend on listener order and fight [reconstructable requests](../architecture/2026-07-05-reconstructable-requests.md). The execution substrate is also part of the foundation rather than a placeholder: Node `worker_threads` provides a separate isolate, an empty environment, heap caps, and termination of a hot synchronous loop, while fitting the harness's existing trust model (§Trust posture).
+Tool presentation belongs to the registry that owns tool visibility: implementing another presentation as an after-the-fact waterfall transform would depend on listener order and fight [reconstructable requests](../architecture/2026-07-05-reconstructable-requests.md). Execution must preserve fresh program state, host-owned bindings and cancellable settlement independently of the presentation mode.
 
 ## Decision
 
@@ -20,9 +20,9 @@ Three decisions, each elaborated in its own section below:
 
 1. **PTC mode is a first-class presentation mode of `ToolRuntime`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'ptc'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry constructs its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation.
 2. **Code execution is a capability seam** — `packages/code-runtime/` contains the Service Definition package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); Consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another Service Provider package, not a redesign.
-3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-node`**: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships `dsh-bash-local`, which executes arbitrary model-written shell commands with strictly *more* ambient authority.
+3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-node`**: each program runs in a fresh managed Node process under the calling Session's resolved sandbox policy. Direct Node APIs remain available within that policy; named host bindings cross a separately validated control channel.
 
-This note owns PTC mode's presentation, composition, isolation, and settlement foundation. The later [typed tool-return Agent Note](2026-07-20-ptc-typed-tool-returns.md) owns the generated output map, canonical binding values, `ToolCallError`, and the lossless outer-output boundary.
+This note owns PTC presentation, composition and settlement. The [typed tool-return decision](2026-07-20-ptc-typed-tool-returns.md) owns generated outputs, canonical binding values, `ToolCallError` and lossless outer output. The [sandboxed Node decision](../architecture/2026-09-11-sandboxed-node-code-runtime.md) supersedes the worker execution, trust posture and budget realization while preserving this note's consumer rules.
 
 ### The registry owns the mode
 
@@ -43,7 +43,7 @@ This note owns PTC mode's presentation, composition, isolation, and settlement f
 Under `'ptc'` and `'both'` the registry owns `run_code` as a reserved presentation transport with two required parameters, `{ code: string; description: string }` (the description labels the call in UIs, the bash precedent). It is represented by a normal `ToolDefinition` for dispatch but stays outside the filterable capability layers, so restrictions cannot accidentally remove PTC mode's only entry point. Calls traverse the complete tool pipeline — `tools/pre-execute` → monotonic guards → `tools/execute` around dispatch → `tools/post-execute` → optional definition-owned `finalizeContent` → immutable `tools/result` notification — exactly like native calls; a permission plugin can inspect the program text before it runs, and final-result observers see the normalized outer outcome. Its `execute(args, exec)`:
 
 1. **Build bindings.** One run-scoped signal follows outer cancellation and is aborted whenever the run settles. Each visible tool binding snapshots lossless-JSON arguments, enters the native-contract dispatch pool (the [live-parallel note](2026-07-26-ptc-live-parallel-dispatch.md) owns the scheduling design), executes with a deterministic call id and the outer token as `parent`, defers returned contexts through the outer execution, and logs the `tool/ptc-dispatch-start`/`tool/ptc-dispatch` pair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visible `ToolCallError`. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline.
-2. **Runs the program**: `ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`. The runtime receives the run-scoped signal, not only the caller's outer signal, so any way the outer run settles also aborts work inside the runtime.
+2. **Runs resolved inputs:** `ctx.codeRuntime.run(ctx.codeRuntime.resolve(request))`. The request carries the program, bindings, run-scoped signal, calling Session cwd and applicable resolved file policy. The run-scoped signal connects outer settlement to execution cancellation.
 3. **Settle after quiescence.** When the runtime settles, the bridge aborts outstanding work and drains the dispatch queue before returning. Success returns captured logs and the completion value as canonical output; the registry renders that value into durable `tool/result.content`, which the result card reads directly. A runtime failure becomes `CodeRunFailedError`; backend rejection uses the registry's normal error boundary. Both produce structured error results, and no sub-call can append after `run_code` settles.
 
 **Sub-call contexts are deferred through the parent.** Injecting inside `run_code` would break parent call/result adjacency, so `ToolRunContext.deferContext()` collects every sub-result `additionalContexts` entry in dispatch order. The registry carries that array even when the program later throws, and the loop appends each entry only after the outer result and every sibling result in the step. An outer post-execute block discards tool-deferred entries and exposes only contexts explicitly attached by the blocking decision.
@@ -62,30 +62,21 @@ The [V2-to-V3 PTC specification](../../../../packages/session/session-format-v2-
 
 ### The code-runtime seam
 
-`packages/code-runtime/code-runtime/` — `@deepseek-ai/dsh-code-runtime`, depending only on `cordis`. An abstract `CodeRuntime extends Service` (`super(ctx, 'codeRuntime')`) plus the vocabulary:
+`dsh-code-runtime` owns `CodeRuntime` and the [request, resolved-spec, binding and result types](../../../../docs/subsystems/code-runtime.md). `resolve(request)` validates supported cwd, timeout and policy choices and supplies deployment defaults; `run(spec)` executes complete inputs. Named async bindings and optional rejection constructors keep registry-specific tool names outside the runtime.
 
-- `CodeRunRequest = { program: string; bindings: CodeBindingNamespace[]; signal?: AbortSignal }`
-- `CodeBindingNamespace = { global: string; functions: Record<string, (args: unknown) => Promise<CodeJsonValue>>; errorClass?: { name: string; memberNameProperty: string } }` — the runtime exposes each namespace as a global object of async functions inside the program; the optional descriptor asks the runtime to inject a real program-visible rejection class without teaching the seam consumer-specific names. `CodeJsonValue` is this dependency-light seam's structural lossless-JSON type, so binding arguments and resolutions cross the implementation's serialization boundary whole.
-- `CodeRunResult = { value?: CodeJsonValue; logs: string[]; error?: CodeRunFailure }` — program execution outcomes resolve as the `error` field. `run()` may reject only for caller/seam misuse (for example a duplicate binding namespace); consumers still contain a non-conforming backend rejection at their own error boundary.
-- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'; message: string }` — orthogonal outcomes reported independently per [defensive patterns](../../../../docs/defensive-patterns.md); a timed-out run is not an exception, an abort is not a timeout, a lossy completion is not an overflow, and a substrate exit is none of them.
-- Two readonly backend descriptors, informational not gating: `language` (what the program must be written in — `'typescript'` for the first backend; a Python backend says `'python'` and pairs with its own SDK generator on the presentation side) and `isolation` (`'worker-thread'` for the shipped backend; `'process'`, `'container'`, … for future ones). `dsh-tools` accepts any `language` with a registered SDK renderer and `run_code` flavor (TypeScript and Python ship; see the [language-dispatch note](../../archived/feature/2026-07-31-ptc-language-dispatch.md)) and fails the assembly loudly otherwise, the same misconfiguration idiom as `toolOrder` violations (as when `mode` is non-native with no `ctx.codeRuntime` loaded at all).
+`language` selects the SDK renderer and `run_code` flavor; an unsupported language fails assembly. `isolation` identifies the substrate, not its authority. `sandboxMode` indicates whether the consumer can pass a resolved Session file policy. Missing runtimes fail PTC assembly, while native presentation does not require one.
 
-Requests contain every runtime input; implementations own validated timeout and cap defaults. The registry looks up the optional runtime only when PTC mode is assembled, so native mode does not depend on one. Missing or language-incompatible runtimes fail loudly. Alternate substrates or languages can replace the implementation behind the same seam, paired with the appropriate SDK generator.
+Program outcomes resolve as result fields; caller misuse may reject. Lossless binding values cross under provider-owned transport limits. The sandbox mode, denial and enforcement facts are independent from success or failure.
 
-### The worker-thread runtime
+### The Node process runtime
 
-`@deepseek-ai/dsh-code-runtime-node`, the second package of the `packages/code-runtime/` group. Per `run()`:
+The Node provider strips erasable TypeScript, resolves its executable/bootstrap in the configured filesystem/subprocess world, and launches through the shared sandbox and managed process owners. Each child has an empty model environment and fresh program globals. Typed rejection constructors and the capturing console remain program-visible.
 
-1. **Type-strip host-side** with Node's built-in `stripTypeScriptTypes` (`node:module`; present across the repo's whole engines range, `^22.19.0 || >=24.0.0`, and position-preserving, so runtime error line numbers match the model's source). Strip-only mode rejects non-erasable syntax (`enum`, namespaces) — that rejection returns as `error.kind: 'exception'` with Node's message, the SDK instructions say "erasable TypeScript only", and the model self-corrects like any other program error. A syntax-level failure never spawns a worker.
-2. **Spawn one fresh `Worker` per run** from the package's own bootstrap module: `env: {}` (truly empty — stronger than the scrubbed-env rule for spawned commands), `resourceLimits` from config, `stdout`/`stderr` captured into `logs` rather than inherited. No pooling and no cross-run state: a program's world dies with its worker, which keeps runs reconstructable from the log alone and makes state bleed unrepresentable.
-3. **Execute** in the bootstrap: the stripped program becomes the body of an `AsyncFunction` whose parameters are the binding globals, any consumer-declared rejection classes, and a capturing `console` shim, so top-level `await` and `return` work. PTC mode declares `ToolCallError` with member property `toolName`; the runtime materializes that real constructor without hardcoding tools. A lossless JSON completion crosses exactly; `undefined` remains absence, a lossy value is `invalid-output`, and an oversized outer result is `output-limit` rather than an inspected-string substitute.
-4. **Bridge bindings over the message port**: each binding function in the worker posts `{ id, global, name, args }` and awaits the reply; the host validates the name against the request's bindings, invokes, and replies `{ id, ok, value }` or `{ id, ok: false, message }` (a host-side binding rejection becomes a program-side rejection). The worker-side namespace objects are built null-prototype via `defineProperty`, so a binding named `__proto__`, `constructor`, or `toString` is an ordinary own property, not a prototype collision. Unknown names, duplicate ids, and post-settlement messages are rejected or ignored — the port protocol assumes a hostile peer, because the peer runs model code.
-5. **Enforce independent budgets.** `computeMs` meters worker busy time, allowing slow awaited tools without excusing a hot loop. `maxWallMs` bounds total elapsed time, including unresolved waits. `maxOutputBytes` bounds only the combined serialized outer logs, completion, or diagnostic; intermediate binding values have no byte cap. Expiry, cancellation, and completion terminate the worker, and heap exits or outer overflow are explicit failures.
-6. **Dispose to quiescence**: the service's own disposal terminates in-flight workers and *awaits* their exits before resolving, per [defensive patterns](../../../../docs/defensive-patterns.md).
+Binding traffic uses a dedicated control channel separate from stdout/stderr. The host validates frames, call identities and allowed bindings before dispatch. Output, pending calls and channel buffers have configured limits; completion, deadline, cancellation and protocol failure terminate and await the managed range. The provider README and sandboxed Node decision own exact bounds and platform limits.
 
 ### Trust posture
 
-The worker runtime provides containment, not a security boundary: model code can reach Node APIs and has authority comparable to the bash tool. `worker.terminate()` stops the thread but not OS processes it spawned. PTC mode uses the same `tools/pre-execute` policy gate as bash and adds an empty environment, heap limits, a separate isolate, and hard termination of the program itself. Deployments that need a hard multi-tenant boundary need a container-class backend for both code and bash; the runtime's isolation descriptor lets them distinguish that backend.
+The selected OS backend constrains direct Node effects under the same policy used by Bash. Nested tools retain their registry policy and approval path. Full or partial enforcement is reported separately, and an unavailable required backend fails closed. Cleanup inherits the subprocess provider's documented range; a process descriptor does not imply a stronger multi-tenant guarantee.
 
 ### What the model sees
 
@@ -99,16 +90,16 @@ Deployments switching to `'ptc'` must update any native-only `toolOrder`. Assemb
 
 ## Testing
 
-- **Worker runtime:** Real-worker tests cover typed binding values and failures, every lossless JSON completion root, invalid and over-limit output, exact combined ledger boundaries, compute and wall budgets, hostile binding traffic, empty environment, and disposal to quiescence. A built-package test runs the worker entry under plain Node.
+- **Node runtime:** Provider tests cover direct Node execution, resolved cwd/policy, empty environment, typed bindings, hostile control frames, output limits, deadlines, cancellation and managed cleanup. Bootstrap tests cover source and built execution.
 - **Registry integration:** Tests cover code generation, all presentation modes, reserved-name and restriction rules, scoped visibility, authoritative assembly rewrites, `toolOrder`, runtime compatibility failures, full-pipeline sub-dispatch, parent-token correlation, serialization, cancellation and queue drain, JSON normalization, error propagation, log events, ordered context deferral across successful and failed programs, outer-block suppression, and HMR cleanup.
 - **With-key e2e:** A real model composes two bash calls in one program; another discovers nested workspace instructions through a PTC mode fs dispatch. The tests verify collapsed request headers, correlated dispatch events, resulting files, deferred context, and model behavior.
-- **Snapshot:** The `ptc-turn`, `both-mode-turn`, and `ptc-workspace-context` fixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards. The TypeScript SDK PTC scenario mounts its worker runtime through an explicit test-owned profile patch and pins Session events and JSON-RPC notifications; its expected response and completed-turn checks run before refresh writes.
+- **Snapshot:** The `ptc-turn`, `both-mode-turn`, and `ptc-workspace-context` fixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards. The TypeScript SDK PTC scenario mounts its Node runtime through an explicit test-owned profile patch and pins Session events and JSON-RPC notifications; its expected response and completed-turn checks run before refresh writes.
 
 ## Alternatives considered
 
 **An add-on consumer plugin with zero core changes.** Rejected because `agent/request` is call-config-only under [reconstructable requests](../architecture/2026-07-05-reconstructable-requests.md), while transforming an assembled tool list would have to undo `toolOrder` canonicalization without owning its config and would depend on listener order. Which tools the model is offered, and in which representation, is the registry's single concern: native schemas and the SDK are two projections of one visible store.
 
-**`node:vm` as the reference runtime, with hardening deferred.** Rejected: `node:vm` is not isolation (prototype-chain escapes reach the host realm) and cannot interrupt a hot loop. A worker thread provides a separate isolate, empty environment, `resourceLimits`, and reliable `terminate()` at bash-equivalent trust, so the reference and production implementation are one package without an unsafe-acknowledgement ceremony.
+**Use `node:vm` as the runtime security mechanism.** A language realm does not enforce OS file/process policy on direct Node APIs. The provider therefore uses the shared sandbox and managed process owners; a separate realm is not a substitute for those protections.
 
 **Result elision / summarization over native tool-calling.** Addresses only the context-bloat half of the problem: trimming old `tool-result`s is cheap to add as a logged surface replacement under reconstructable requests, but still pays one model round-trip per call and cannot express loops, branches, or joins. Complementary, not competing; it can layer under PTC mode for residual native calls.
 
@@ -124,7 +115,7 @@ Deployments switching to `'ptc'` must update any native-only `toolOrder`. Assemb
 
 ## Risks
 
-**The worker is not a hard security boundary.** Deliberate and documented (§Trust posture): posture equals the existing bash tool, containment exceeds it, gating uses the same approval and sandbox policies. Deployments needing more need a future `isolation: 'container'` backend — tracked as the seam's designed extension, not a TODO on this design.
+**Platform enforcement and cleanup vary.** The runtime reports the selected sandbox backend's completeness and inherits the managed process owner's range. Unsupported or partial platform guarantees remain explicit rather than being hidden behind the isolation descriptor.
 
 **`stripTypeScriptTypes` is marked experimental.** It is the same engine (amaro/swc) behind Node's own native `.ts` execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite checks position preservation and the required parts of the erasable-only rejection message, the call sits behind one private function, and `amaro`/`sucrase` are direct replacements if the API shifts. The erasable-only subset is a model-facing input restriction, and the error tells the model how to correct the program.
 
@@ -132,8 +123,8 @@ Deployments switching to `'ptc'` must update any native-only `toolOrder`. Assemb
 
 **Registry scope growth.** `dsh-tools` absorbs codegen, a tool, a bridge, and an event. Package modules separate these responsibilities (`ts-types.ts` and `ptc.ts` beside `schema.ts`, `json-schema.ts`, and `presentation.ts`), while `ctx.codeRuntime` owns all code-runtime-specific implementation.
 
-**Large lossless JSON values can exhaust memory.** Tool bindings snapshot lossless JSON before dispatch and return canonical JSON resolutions whole. The runtime validates both sides of the worker port and applies no per-binding byte cap; structured-clone cost and process or worker memory are the practical bounds. The combined outer-output ledger for logs, the completion value, and a failure diagnostic is the only byte-capped boundary.
+**Large binding values can still exhaust memory.** The Node provider bounds control frames, outstanding arguments and queued writes, while a host binding can allocate its result before those checks. Lossless JSON transfer and the outer output ledger do not create a process-wide allocation quota.
 
 **Sub-dispatch overlap is bounded by tool safety claims, not by the caller.** A program's `Promise.all` or `asyncio.gather` buys wall-clock parallelism only across calls the tool itself classifies concurrency-safe; a run of exclusive calls still costs its round-trips in sequence, and models may over-expect. Both flavors' SDK instructions state the real contract. This note shipped the serialized placeholder that made the risk absolute; the [live-parallel Agent Note](2026-07-26-ptc-live-parallel-dispatch.md) owns the scheduler and its overlap cap.
 
-**Budget metering reads the event loop, not a flag.** Busy-time polling (`eventLoopUtilization()`) is coarser than an exact CPU meter — a budget expires up to one poll interval late — and its correctness claim ("a pending dispatch cannot pause it") is load-bearing against a hostile program. Both sides are unit-tested (hot loop with a pending decoy dispatch dies at `computeMs`; idle-on-slow-binding survives to `maxWallMs`), and the poll interval is an internal constant, not config — nothing a deployment could mis-tune into a bypass. `maxWallMs` is config, and it reaches `setTimeout`, which clamps a delay above `MAX_TIMER_DELAY_MS` (2^31-1 ms) to 1 ms; a positivity check alone therefore accepts a 25-day ceiling that expires on the first tick and times out every run. The worker runtime range-checks the field at load for that reason. `computeMs` needs no upper bound because it is compared against measured utilization instead of being handed to a timer.
+**Elapsed deadlines include awaited work.** Nested tools and approval waits consume the program's elapsed budget. The host stops execution on expiry and awaits managed cleanup; it does not meter process-tree CPU consumption. The sandboxed Node decision records current defaults and deferred timeout questions.

+ 17 - 26
.agents/notes/implemented/feature/2026-06-15-ptc.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一种替代方案,基于一个简单的观察:LLM(大语言模型)编写代码的能力优于发出工具调用,因为它们见过数百万行真实代码,而人为构造的工具调用 trace 相对很少。模型不再每步发出一次工具调用,而是针对工具生成的 API 编写一段 TypeScript 程序,程序在沙箱运行时中执行,模型只筛选返回的内容——仅限它 print 或 return 的部分——而非所有中间结果。
 
-工具呈现属于掌管工具可见性的注册表:如果把第二种呈现方式实现为事后的 waterfall(瀑布式事件)变换,正确性将依赖监听器顺序,并与[可重建请求](../architecture/2026-07-05-reconstructable-requests.zh.md)冲突。执行基底同样属于基础设施而非占位实现:Node `worker_threads` 提供独立 isolate、空环境、堆上限以及对热同步循环的终止能力,同时契合 harness 既有的信任模型(§信任姿态)。
+工具呈现属于掌管工具可见性的注册表:将另一种呈现实现为事后的 waterfall(瀑布式事件)变换会依赖监听器顺序,并与[可重建请求](../architecture/2026-07-05-reconstructable-requests.zh.md)冲突。执行必须独立于呈现模式,保留全新程序状态、Host 拥有的绑定和可取消结算。
 
 ## 决策
 
@@ -20,9 +20,9 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
 
 1. **PTC mode 是 `ToolRuntime`(`dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'ptc'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头构建其规范贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。
 2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含 Service Definition 包 `@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`([能力 seam](../architecture/2026-06-13-capability-seams.zh.md);消费方 = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service Provider 包,而非重新设计。
-3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-node`**:每次运行 spawn 一个全新的 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过消息端口桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——无需 unsafe-acknowledgement flag——因为 harness 已经交付了 `dsh-bash-local`,后者以严格*更高*的环境权限执行模型编写的任意 shell 命令。
+3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-node`**:每个程序在全新的受管 Node 进程中,按调用 Session 的已解析沙箱策略运行。直接 Node API 在该策略内仍可使用;具名 Host 绑定通过单独验证的控制通道调用。
 
-本说明负责定义 PTC mode 的呈现、组合、隔离与结算基础。后续的[类型化工具返回值 Agent Note](2026-07-20-ptc-typed-tool-returns.zh.md)负责定义生成的输出映射、规范绑定值、`ToolCallError` 和无损外层输出边界。
+本说明负责 PTC 呈现、组合与结算。[类型化工具返回决策](2026-07-20-ptc-typed-tool-returns.zh.md)负责生成输出、规范绑定值、`ToolCallError` 与无损外层输出。[沙箱 Node 决策](../architecture/2026-09-11-sandboxed-node-code-runtime.zh.md)取代 worker 执行、信任姿态与预算实现,同时保留本说明的消费方规则。
 
 ### 注册表拥有模式
 
@@ -43,7 +43,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
 在 `'ptc'` 和 `'both'` 下,注册表拥有 `run_code` 作为保留的呈现传输通道,带两个必需参数 `{ code: string; description: string }`(description 为 UI 标注该调用,沿用 bash 的先例)。它由一个正常的 `ToolDefinition` 表示以供分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 PTC mode 的唯一入口。调用遍历完整的工具流水线——`tools/pre-execute` → 单调性守卫 → `tools/execute` 包裹分发 → `tools/post-execute` → 由定义拥有的可选 `finalizeContent` → 不可变的 `tools/result` 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是规范化的外层结果。其 `execute(args, exec)`:
 
 1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由[实时并行 Agent Note](2026-07-26-ptc-live-parallel-dispatch.zh.md) 负责),以确定性的 call id 和外层 token 作为 `parent` 执行,通过外层 execution 延后返回的上下文,并记录 `tool/ptc-dispatch-start`/`tool/ptc-dispatch` 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的 `ToolCallError`。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。
-2. **运行程序**:`ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`。运行时接收的是 run 级别的 signal 而非仅调用方的外层 signal,因此外层 run 以任何方式结算都会同时 abort 运行时内部的工作。
+2. **运行已解析输入:** `ctx.codeRuntime.run(ctx.codeRuntime.resolve(request))`。请求携带程序、绑定、运行级信号、调用 Session 的 cwd 与适用的已解析文件策略。运行级信号将外层结算与执行取消关联。
 3. **完全停稳后结算。** 运行时结算后,桥 abort 未完成的工作并排空分发队列后再返回。成功时返回捕获的日志和完成值,将其作为规范输出;注册表再把该值渲染为持久化的 `tool/result.content`,供结果卡片直接读取。运行时失败变为 `CodeRunFailedError`;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且 `run_code` 结算后不允许子调用追加。
 
 **子调用上下文通过父调用延后。** 在 `run_code` 内部注入会破坏父调用/结果的相邻性,因此 `ToolRunContext.deferContext()` 按分发顺序收集每个子结果的 `additionalContexts` 条目。即使程序后来抛出异常,注册表仍携带该数组;循环只在外层结果与步骤中所有兄弟结果之后追加每个条目。外层 post-execute 阻止会丢弃工具延后的条目,只暴露阻止 decision 显式附加的上下文。
@@ -62,30 +62,21 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
 
 ### code-runtime seam
 
-`packages/code-runtime/code-runtime/`——`@deepseek-ai/dsh-code-runtime`,仅依赖 `cordis`。一个抽象的 `CodeRuntime extends Service`(`super(ctx, 'codeRuntime')`)加上词汇:
+`dsh-code-runtime` 负责 `CodeRuntime` 与[请求、已解析 spec、绑定及结果类型](../../../../docs/subsystems/code-runtime.zh.md)。`resolve(request)` 验证支持的 cwd、timeout 与策略选择并补全部署默认值;`run(spec)` 执行完整输入。具名异步绑定和可选拒绝构造器使运行时不必了解注册表特定的工具名称。
 
-- `CodeRunRequest = { program: string; bindings: CodeBindingNamespace[]; signal?: AbortSignal }`
-- `CodeBindingNamespace = { global: string; functions: Record<string, (args: unknown) => Promise<CodeJsonValue>>; errorClass?: { name: string; memberNameProperty: string } }`——运行时将每个命名空间作为程序内部的全局异步函数对象暴露;可选描述符要求运行时注入真正的、程序可见的 reject 类,而无需让 seam 获知消费方专用名称。`CodeJsonValue` 是这个低依赖 seam 的结构化无损 JSON 类型,因此绑定参数与返回值可以完整跨越实现的序列化边界。
-- `CodeRunResult = { value?: CodeJsonValue; logs: string[]; error?: CodeRunFailure }`——程序执行失败时,执行 promise 仍会 fulfill,并通过 `error` 字段返回失败结果。只有调用方/seam 误用(例如重复的绑定命名空间)时,`run()` 才会 reject;消费方仍在自己的错误边界处理不合规后端的拒绝。
-- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'; message: string }`——按[防御性模式](../../../../docs/defensive-patterns.zh.md)独立报告的正交结果;超时的 run 不是异常,abort 不是超时,有损完成值不是溢出,基底退出也与上述情况相互独立。
-- 两个只读的后端描述符,仅供信息参考而非门禁判定:`language`(程序必须使用的语言——首个后端为 `'typescript'`;Python 后端声明 `'python'`,并在呈现侧配对自己的 SDK 生成器)和 `isolation`(交付的后端为 `'worker-thread'`;未来可为 `'process'`、`'container'` 等)。`dsh-tools` 接受任何注册了 SDK 渲染器与 `run_code` flavor 的 `language`(TypeScript 与 Python 已交付;见[语言分发 note](../../archived/feature/2026-07-31-ptc-language-dispatch.md)),否则组装会显式失败,与 `toolOrder` 违规时的配置错误惯用法相同(如 `mode` 为非 native 但根本没有加载 `ctx.codeRuntime`)。
+`language` 选择 SDK 渲染器与 `run_code` flavor;不支持的语言使组装失败。`isolation` 标识执行基底,不标识权限。`sandboxMode` 表示消费方能否传入已解析 Session 文件策略。缺少运行时使 PTC 组装失败,native 呈现则不需要运行时。
 
-请求包含所有运行时输入;实现方拥有经校验的超时和上限默认值。注册表仅在组装 PTC mode 时查找可选的运行时,因此 native 模式不依赖它。缺失或语言不兼容的运行时会显式失败。替代基底或语言可以在同一 seam 背后替换实现,配对相应的 SDK 生成器。
+程序结果以字段返回;调用方误用可以拒绝。无损绑定值在提供方自己的传输限制下跨越。沙箱模式、拒绝与强制事实独立于成功或失败。
 
-### worker-thread 运行时
+### Node 进程运行时
 
-`@deepseek-ai/dsh-code-runtime-node`,`packages/code-runtime/` 组的第二个包。每次 `run()`:
+Node 提供方擦除可擦除 TypeScript,在配置的文件系统/子进程世界中解析可执行文件与 bootstrap,并通过共享沙箱与受管进程所有者启动。每个子进程都有空模型环境和全新程序全局对象。类型化拒绝构造器与捕获 console 仍对程序可见。
 
-1. **宿主侧 type-strip**,使用 Node 内置的 `stripTypeScriptTypes`(`node:module`;在本仓库的整个引擎范围 `^22.19.0 || >=24.0.0` 内可用,且会保留源码位置,因此运行时错误行号与模型源码一致)。仅剥离模式拒绝不可擦除的语法(`enum`、namespaces)——该拒绝以 `error.kind: 'exception'` 加 Node 的消息返回,SDK 说明写明「仅限可擦除 TypeScript」,模型像处理其他程序错误一样自我修正。语法级失败不会 spawn worker。
-2. **每次 run spawn 一个全新 `Worker`**,来自包自身的 bootstrap 模块:`env: {}`(真正为空——比 spawn 命令的 scrubbed-env 规则更严格),`resourceLimits` 来自配置,`stdout`/`stderr` 捕获到 `logs` 而非继承。不做池化,不跨 run 保留状态:程序的世界随 worker 消亡,这使得 run 仅从日志即可重建,状态泄漏不可表达。
-3. **在 bootstrap 中执行**:剥离后的程序成为一个 `AsyncFunction` 的函数体,其参数是绑定全局变量、消费方声明的 reject 类和一个捕获式 `console` shim,因此顶层 `await` 和 `return` 可用。PTC mode 声明 `ToolCallError`,成员属性为 `toolName`;运行时无需硬编码工具即可实体化真正的构造函数。无损 JSON 完成值会精确跨越边界;`undefined` 仍表示缺席,有损值产生 `invalid-output`,过大的外层结果产生 `output-limit`,而不会退化为检查格式化后的字符串替代品。
-4. **通过消息端口桥接绑定**:worker 中的每个绑定函数发送 `{ id, global, name, args }` 并等待回复;宿主根据请求的绑定校验名称、调用、并回复 `{ id, ok, value }` 或 `{ id, ok: false, message }`(宿主侧绑定拒绝变为程序侧 rejection)。worker 侧的命名空间对象通过 `defineProperty` 构建为 null-prototype,因此名为 `__proto__`、`constructor` 或 `toString` 的绑定是普通自有属性,而非原型链碰撞。未知名称、重复 id 和结算后消息被拒绝或忽略——端口协议假设对端是恶意的,因为对端运行的是模型代码。
-5. **强制独立预算。** `computeMs` 计量 worker 忙碌时间,允许慢速的 awaited 工具而不放过热循环。`maxWallMs` 约束总经过时间,包括未解析的等待。`maxOutputBytes` 只约束序列化后的外层日志、完成值或诊断的组合;中间绑定值没有字节数上限。到期、取消和完成都终止 worker,堆退出或外层溢出会作为显式失败报告。
-6. **dispose(资源释放)至完全停稳**:服务自身的 dispose 终止进行中的 worker 并*等待*其退出后再 resolve,遵循[防御性模式](../../../../docs/defensive-patterns.zh.md)。
+绑定通信使用与 stdout/stderr 分开的专用控制通道。Host 在分派前验证帧、调用身份与允许的绑定。输出、待处理调用与通道缓冲有配置上限;完成、截止、取消和协议失败会终止并等待受管范围。提供方 README 与沙箱 Node 决策负责确切上限和平台限制。
 
 ### 信任姿态
 
-worker 运行时只能约束程序的运行,而不构成安全边界:模型代码可以访问 Node API,权限与 bash 工具相当。`worker.terminate()` 停止线程但不停止它 spawn 的 OS 进程。PTC mode 使用与 bash 相同的 `tools/pre-execute` 策略门禁,并额外提供空环境、堆限制、独立 isolate 和对程序本身的硬终止。需要硬多租户边界的部署需要为代码和 bash 都使用容器级后端;运行时的 isolation 描述符让它们能区分该后端。
+所选 OS 后端按照与 Bash 相同的策略约束直接 Node 副作用。嵌套工具保留注册表策略与审批路径。完整或部分强制能力独立报告;所需后端不可用时以失败关闭。清理继承子进程提供方声明的范围;进程描述符不意味着更强的多租户保证。
 
 ### 模型看到的内容
 
@@ -99,16 +90,16 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认
 
 ## 测试
 
-- **Worker 运行时:** 真实 worker 测试覆盖类型化的绑定值与失败、每一种无损 JSON 根类型的完成值、无效和超限输出、精确的组合账本边界、compute 和 wall 预算、恶意绑定流量、空环境以及 dispose 至完全停稳。一个构建后包测试在纯 Node 下运行 worker 入口。
+- **Node 运行时:** 提供方测试覆盖直接 Node 执行、已解析 cwd/策略、空环境、类型化绑定、恶意控制帧、输出上限、截止、取消与受管清理。Bootstrap 测试覆盖源代码与构建后执行。
 - **注册表集成:** 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、scoped 可见性、权威组装重写、`toolOrder`、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 规范化、错误传播、日志事件、成功与失败程序中的有序上下文延后、外层阻止抑制以及 HMR(热模块替换)清理。
 - **带密钥 e2e:** 真实模型在一个程序中组合两次 bash 调用;另一个模型通过 PTC mode fs 分发发现嵌套的工作区指令。测试验证折叠的请求头、关联的分发事件、结果文件、延后上下文和模型行为。
-- **快照:** `ptc-turn`、`both-mode-turn` 和 `ptc-workspace-context` fixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。TypeScript SDK PTC 场景通过测试拥有的显式 profile patch 挂载 worker 运行时,并固定 Session 事件与 JSON-RPC 通知;预期回复和完成轮次检查在 refresh 写入前执行。
+- **快照:** `ptc-turn`、`both-mode-turn` 和 `ptc-workspace-context` fixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。TypeScript SDK PTC 场景通过测试拥有的显式 profile patch 挂载 Node 运行时,并固定 Session 事件与 JSON-RPC 通知;预期回复和完成轮次检查在 refresh 写入前执行。
 
 ## 曾考虑的替代方案
 
 **一个零核心改动的附加消费方插件。** 否决,因为 `agent/request` 在[可重建请求](../architecture/2026-07-05-reconstructable-requests.zh.md)下仅限 call-config,而变换已组装的工具列表需要在不拥有其配置的情况下撤销 `toolOrder` 规范化,并依赖监听器顺序。向模型提供哪些工具、以何种表示形式提供,是注册表的单一关注点:原生 schema 和 SDK 是同一个可见存储的两种投影。
 
-**`node:vm` 作为参考运行时,加固推迟。** 否决:`node:vm` 不是隔离(原型链逃逸可达宿主 realm)且无法中断热循环。worker 线程提供独立 isolate、空环境、`resourceLimits` 和可靠的 `terminate()`,信任等级等同于 bash,因此参考实现和生产实现是同一个包,无需 unsafe-acknowledgement 仪式。
+**将 `node:vm` 作为运行时安全机制。** 语言 realm 不会对直接 Node API 强制 OS 文件/进程策略。因此提供方使用共享沙箱与受管进程所有者;独立 realm 不能代替这些保护。
 
 **在原生工具调用上做结果省略/摘要。** 仅解决问题中上下文膨胀这一半:裁剪旧 `tool-result` 作为可重建请求下的日志化表面替换成本低,但仍需每次调用一次模型往返,且无法表达循环、分支或汇合。互补而非竞争;它可以在 PTC mode 下为残余的原生调用分层。
 
@@ -124,7 +115,7 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认
 
 ## 风险
 
-**Worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,约束能力强于它,门禁使用相同的审批与沙箱策略。需要更强隔离的部署需要未来的 `isolation: 'container'` 后端——作为 seam 设计中预留的扩展进行跟踪,而非本设计的 TODO。
+**平台强制能力与清理能力不同。** 运行时报告所选沙箱后端的完整性,并继承受管进程所有者的范围。不支持或部分的平台保证保持明确,不隐藏在 isolation 描述符之后。
 
 **`stripTypeScriptTypes` 标记为 experimental。** 它与 Node 自身原生 `.ts` 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件会检查位置保持和可擦除限制拒绝消息中的必需部分;调用位于一个私有函数之后,且 `amaro`/`sucrase` 可在 API 变化时直接替换它。仅可擦除子集是面向模型的输入限制,错误消息会告诉模型如何修正程序。
 
@@ -132,8 +123,8 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认
 
 **注册表 scope 增长。** `dsh-tools` 吸收了代码生成、一个工具、一个桥和一个事件。包内模块把这些职责分开(`ts-types.ts`、`ptc.ts` 与 `schema.ts`、`json-schema.ts`、`presentation.ts` 并列),所有 code-runtime 专用实现都由 `ctx.codeRuntime` 提供。
 
-**大型无损 JSON 值可能耗尽内存。** 工具绑定会在分发前对无损 JSON 创建快照,并完整返回规范 JSON 返回值。运行时会校验 worker 端口两侧,但不对单次绑定设置字节数上限;结构化克隆成本以及进程或 worker 内存构成实际边界。只有包含日志、完成值和失败诊断的组合外层输出账本受字节数上限约束。
+**大型绑定值仍可能耗尽内存。** Node 提供方限制控制帧、未完成参数和排队写入,而 Host 绑定可以在这些检查前分配其结果。无损 JSON 传输与外层输出账本不构成进程级分配配额。
 
 **子分发的重叠由工具自身的安全声明限定,而非由调用方决定。** 程序里的 `Promise.all` 或 `asyncio.gather` 只在工具自己分类为并发安全的调用之间换来挂钟并行性;一串 exclusive 调用仍要按顺序付出各自的往返开销,模型可能过度期望。两种 flavor 的 SDK 说明都陈述了真实约定。本 note 交付的是使该风险绝对化的序列化占位实现;调度器及其重叠上限由[实时并行 Agent Note](2026-07-26-ptc-live-parallel-dispatch.zh.md) 负责。
 
-**预算计量读取事件循环,而非 flag。** 忙碌时间轮询(`eventLoopUtilization()`)比精确 CPU 计量更粗糙——预算到期最多延迟一个轮询间隔——且其正确性声明(「pending 的分发不能暂停它」)是抵御恶意程序的关键。两种情况均有单元测试(带 pending 诱饵分发的热循环会在耗尽 `computeMs` 预算时终止;等待慢速绑定的空闲程序则会持续运行至 `maxWallMs`),轮询间隔是内部常量而非配置——部署无法将其误调为绕过手段。`maxWallMs` 是配置项,且会传入 `setTimeout`,后者会把超过 `MAX_TIMER_DELAY_MS`(2^31-1 ms)的延迟夹到 1 ms;因此仅有正数校验会放行一个 25 天的上限,它在第一个 tick 就到期,使每次运行都超时。worker 运行时正因如此在加载时对该字段做范围校验。`computeMs` 不需要上界,因为它对照的是实测占用率,而不是交给定时器。
+**经过时间截止包括被等待的工作。** 嵌套工具和审批等待消耗程序的经过时间预算。Host 在截止到期时停止执行并等待受管清理;它不计量进程树 CPU 消耗。沙箱 Node 决策记录当前默认值与延后评估的 timeout 问题。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md
-2026-07-20-ptc-typed-tool-returns.md: b7d7cc56210b229d7e36f8face888a34c9d1ac3e
-2026-07-20-ptc-typed-tool-returns.zh.md: 20e14cdac6151ceead084ff6a78eb5f7a6277af1
+2026-07-20-ptc-typed-tool-returns.md: 1fec707bc6c5da3a013f1dc6a311f4e5beda6e60
+2026-07-20-ptc-typed-tool-returns.zh.md: af58812941b4de1570f5762c8fa704f0680dfa5d

+ 2 - 2
.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md
-2026-09-08-ci-completion-observations.md: e3de87a70419778407b5eb230cec64ce088dd0c0
-2026-09-08-ci-completion-observations.zh.md: 4e95d6fb9dc9b1ecac2e3d0dfa262b819de16261
+2026-09-08-ci-completion-observations.md: 5b40b92ac19156a017aaafeffc162895eb6db2ae
+2026-09-08-ci-completion-observations.zh.md: 9a73f5351721371493b019f8d8faa69d0085ef99

+ 2 - 2
.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md

@@ -22,7 +22,7 @@ The [whole-queue steering test](../../../../apps/web/tests/steering.e2e.ts) wait
 
 The [workspace-management test](../../../../apps/web/tests/workspace-management.e2e.ts) waits for restored composer focus before the next directory-dialog gesture. Its archive case gives the known seed id an explicit user title through the Session controller, then uses that exact title to identify the row across reload. An unrelated restored row cannot satisfy that locator; the durable archive assertion still checks the seed id and retained log.
 
-The [worker budget tests](../../../../packages/code-runtime/code-runtime-node/tests/budget.spec.ts) retain real worker execution and binding transport while controlling host timers and ELU samples. They acknowledge binding entry before exercising idle, active, and wall-clock decisions, so a bootstrap timeout cannot stand in for a budget decision during a binding. The [real-worker tests](../../../../packages/code-runtime/code-runtime-node/tests/runtime.spec.ts) independently retain actual ELU, idle-binding, and hot-loop coverage.
+The [Node runtime tests](../../../../packages/code-runtime/code-runtime-node/tests/runtime.spec.ts) exercise real managed-process execution, binding transport, elapsed deadlines and cancellation. The [sandboxed Node decision](../architecture/2026-09-11-sandboxed-node-code-runtime.md) supersedes the worker ELU budget and its controlled-sample tests; real process and transport evidence remains necessary.
 
 The [detached-launch tests](../../../../packages/host/open-in-app/tests/launch-detached.spec.ts) control watch time and deliver late process events through the real launcher's registered callbacks. They check one settlement, one unref, and no child kill. Real-process environment and early-exit cases remain in the [resolver tests](../../../../packages/host/open-in-app/tests/resolver.spec.ts).
 
@@ -38,7 +38,7 @@ The [Node import sweep](../../../../packages/experimental/webworker-runtime/test
 
 **Completion inferred from acceptance or a preview.** HTTP 202 and an optimistic image can precede the operation being asserted.
 
-**Controlled samples replacing measured worker coverage.** Rejected because they omit verification of Node's actual ELU and transport behavior.
+**Controlled samples replacing real execution.** Rejected because timer samples alone cannot verify process launch, control transport or managed cleanup.
 
 ## Consequences
 

+ 2 - 2
.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.zh.md

@@ -22,7 +22,7 @@ Status: implemented
 
 [Workspace 管理测试](../../../../apps/web/tests/workspace-management.e2e.ts)在下一次目录对话框操作前等待恢复后的 composer 焦点。归档用例通过 Session controller 为已知 seed id 设置显式用户标题,再用该精确标题跨重载定位行。无关的恢复行无法匹配该定位器;持久化归档断言仍检查 seed id 和保留的日志。
 
-[Worker 预算测试](../../../../packages/code-runtime/code-runtime-node/tests/budget.spec.ts)保留真实 worker 执行与绑定传输,只控制 Host 定时器和 ELU 样本。测试先确认绑定已进入,再检验 idle、active 和壁钟决策,使启动超时不能冒充绑定期间的预算决策。[真实 worker 测试](../../../../packages/code-runtime/code-runtime-node/tests/runtime.spec.ts)独立保留实际 ELU、空闲绑定和热循环覆盖。
+[Node 运行时测试](../../../../packages/code-runtime/code-runtime-node/tests/runtime.spec.ts)执行真实受管进程、绑定传输、经过时间截止与取消。[沙箱 Node 决策](../architecture/2026-09-11-sandboxed-node-code-runtime.zh.md)取代 worker ELU 预算及其受控样本测试;真实进程与传输证据仍然必要。
 
 [分离启动测试](../../../../packages/host/open-in-app/tests/launch-detached.spec.ts)控制观察时间,并通过真实 launcher 登记的回调发送迟到进程事件。测试检查仅完成一次、仅 unref 一次且不终止子进程。[Resolver 测试](../../../../packages/host/open-in-app/tests/resolver.spec.ts)保留真实进程的环境变量和提前退出用例。
 
@@ -38,7 +38,7 @@ Status: implemented
 
 **从接受或预览推断完成。** HTTP 202 和乐观图片可能早于被断言的操作。
 
-**用受控样本替换实测 worker 覆盖。** 拒绝,因为会遗漏对 Node 实际 ELU 与传输行为的验证。
+**用受控样本代替真实执行。** 不采纳,因为仅靠定时器样本无法验证进程启动、控制传输或受管清理。
 
 ## 影响
 

+ 2 - 2
.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.md
-2026-09-08-ci-readiness-and-completion.md: c72048ef90b987e7d27992b1754736b2ce895093
-2026-09-08-ci-readiness-and-completion.zh.md: 1188d66249b3092416435da2e8496a657ad00677
+2026-09-08-ci-readiness-and-completion.md: dc483e0daba361eae2cd74bb6fd8fef360e4d257
+2026-09-08-ci-readiness-and-completion.zh.md: a5b8374e208e0f13e6c14f8bb621c06059a49ad8

+ 1 - 1
.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.md

@@ -28,7 +28,7 @@ The [ACP disconnect tests](../../../../packages/acp/acp/tests/dispose.spec.ts) a
 
 The [subagent teardown decision](2026-09-07-subagent-teardown-test-budgets.md) owns lifecycle cleanup budgets. The [persistent PowerShell decision](2026-09-07-pwsh-ci-observable-completion.md) owns exact versus inferred terminal readiness; a one-shot process's completion promise has different semantics.
 
-The [worker-runtime binding test](../../../../packages/code-runtime/code-runtime-node/tests/runtime.spec.ts) allows five seconds of compute for source-worker initialization and delays the binding for 6.5 seconds. Charging that idle delay would still exceed the entire compute allowance. The case retains its 15-second test limit and 30-second wall ceiling, registers Context and reply-timer cleanup, and leaves the hot-loop, decoy-dispatch, wall-ceiling, and abort controls at their existing limits. Production budgets remain unchanged.
+The [sandboxed Node decision](../architecture/2026-09-11-sandboxed-node-code-runtime.md) supersedes worker active-time accounting. The [Node runtime suite](../../../../packages/code-runtime/code-runtime-node/tests/runtime.spec.ts) exercises the replacement elapsed deadline through real managed processes. The other completion observations and fixture lifecycle rules in this note remain in force.
 
 The [SDK subagent protocol-error test](../../../../packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts) uses the provider's normal shutdown and exit grace periods and registers disposal before its assertions. The [Inspector tree tests](../../../../packages/experimental/inspector/tests/cordis-tree.host.spec.ts) pass the active test budget to Worker startup and register cleanup while startup is still pending. A cancelled test cannot receive a late-ready handle; cleanup awaits initialization and closes a successfully started Worker. Failed initialization already terminates the Worker before rejecting. A controlled late-start test verifies cancellation and closure through the real Worker's HTTP endpoint. Production defaults remain unchanged.
 

+ 1 - 1
.agents/notes/implemented/testing/2026-09-08-ci-readiness-and-completion.zh.md

@@ -28,7 +28,7 @@ Status: implemented
 
 [子 Agent 拆卸决策](2026-09-07-subagent-teardown-test-budgets.zh.md)负责生命周期清理预算。[持久 PowerShell 决策](2026-09-07-pwsh-ci-observable-completion.zh.md)负责精确与推断的终端就绪状态;一次性进程的完成 Promise 具有不同语义。
 
-[Worker runtime binding 测试](../../../../packages/code-runtime/code-runtime-node/tests/runtime.spec.ts)为源码 worker 初始化保留五秒计算额度,并将 binding 延迟设为 6.5 秒。若将该空闲延迟计费,仍会超过整个计算额度。用例保留 15 秒测试期限与 30 秒墙钟上限,登记 Context 和回复定时器的清理,并保持热循环、诱饵 dispatch、墙钟上限及取消控制用例的原有限制。生产预算不变。
+[沙箱 Node 决策](../architecture/2026-09-11-sandboxed-node-code-runtime.zh.md)取代 worker 活跃时间计量。[Node 运行时套件](../../../../packages/code-runtime/code-runtime-node/tests/runtime.spec.ts)通过真实受管进程验证替代的经过时间截止。本说明中其他完成观测与测试生命周期规则保持有效。
 
 [SDK 子 Agent 协议错误测试](../../../../packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts)使用提供方正常的关闭和退出等待时间,并在断言前登记清理。[Inspector 树测试](../../../../packages/experimental/inspector/tests/cordis-tree.host.spec.ts)将当前测试预算传给 Worker 启动,并在启动尚未完成时登记清理。取消后的测试不会收到随后才就绪的实例;清理等待初始化完成,并关闭成功启动的 Worker。初始化失败时,启动操作会在拒绝前终止 Worker。受控的延迟启动测试通过真实 Worker 的 HTTP 端点验证取消和关闭。生产默认值不变。
 

+ 2 - 2
.agents/notes/rejected/simplification/2026-07-04-prune-dead-core-spine-api.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/rejected/simplification/2026-07-04-prune-dead-core-spine-api.md
-2026-07-04-prune-dead-core-spine-api.md: b2f6787e960bb85b25810836bf21476a9abdc0e3
-2026-07-04-prune-dead-core-spine-api.zh.md: 73dd1b94e06002b7b24f3229d8885b964058eed1
+2026-07-04-prune-dead-core-spine-api.md: e2f38579a5bd0e195a0951da4bc56d6b4cd23b51
+2026-07-04-prune-dead-core-spine-api.zh.md: 071f594ad70bdd886a7f2036f3c249a281d082b5

+ 2 - 2
docs/subsystems/code-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/code-runtime.md
-code-runtime.md: 4c7fce42c363c7735d03fcb723bb5c5f1af12bb9
-code-runtime.zh.md: f01e3bccef165a5aeb9130ac983b2e8ff63a81b0
+code-runtime.md: f807bec9971eeb019f56dbea6efd560e07098256
+code-runtime.zh.md: b2d725ed6fba921866a59208ead42701f1865eb9

+ 38 - 10
docs/subsystems/code-runtime.md

@@ -2,20 +2,18 @@
 
 English | [中文](code-runtime.zh.md)
 
-The code-execution seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) whose Service Definition ([dsh-code-runtime](../../packages/code-runtime/code-runtime), `ctx.codeRuntime`) runs one model-written program against host-provided async bindings and reports what it printed and returned. Code execution is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). Backends differ by execution substrate and source language, both readonly descriptors on the service; the worker-thread Service Provider and tool-registry Consumer are specified by the [PTC mode foundation](../../.agents/notes/implemented/feature/2026-06-15-ptc.md) and [typed-return contract](../../.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md).
+The code-execution [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) supplies `ctx.codeRuntime` through [dsh-code-runtime](../../packages/code-runtime/code-runtime). It runs one program against host bindings and reports output, failure and applicable sandbox facts. Code execution is optional rather than part of [the agent-loop spine](core.md). The [PTC foundation](../../.agents/notes/implemented/feature/2026-06-15-ptc.md) owns registry presentation, the [typed-return contract](../../.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md) owns binding values, and the [sandboxed Node decision](../../.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.md) owns the shipped execution provider.
 
 Source: [`packages/code-runtime/code-runtime/src/types.ts`](../../packages/code-runtime/code-runtime/src/types.ts)
 
 ## The run: request in, result out
 
-A `CodeRunRequest` carries **everything the runtime acts on** — per the "explicit > implicit at package boundaries" rule, defaulting (time budgets, output caps) is the implementation's validated config, never a hidden `??` inside `run()`:
+`CodeRunRequest` contains the program, bindings, cancellation and optional execution choices. The provider's `resolve` validates supported choices and applies its deployment defaults; `run` receives a `CodeRunSpec` with an explicit directory and deadline. A provider that cannot enforce a requested policy rejects it before program execution:
 
 ```ts type-equiv
 /**
- * One run: the program source plus everything the runtime acts on. Per the
- * explicit-over-implicit convention, defaulting (time budgets, output caps)
- * is the implementation's validated config — a request carries no optional
- * tuning knobs for a hidden `??` to fill in.
+ * Caller inputs for one program. The provider's resolve method validates supported
+ * options and supplies directory, deadline, and authority before execution.
  */
 interface CodeRunRequest {
   /**
@@ -27,6 +25,12 @@ interface CodeRunRequest {
   program: string
   /** Host functions exposed to the program, one global object per namespace. */
   bindings: CodeBindingNamespace[]
+  /** Working directory in the mounted filesystem and subprocess execution world. */
+  cwd?: string
+  /** Requested elapsed execution time; the provider's resolver validates and caps it. */
+  timeoutMs?: number
+  /** Resolved authority for this execution. Providers without confinement reject an explicit policy. */
+  sandboxPolicy?: SandboxExecutionPolicy
   /**
    * Abort the run: the runtime stops the program (hard, even mid-loop) and
    * resolves with a {@link CodeRunFailure} of kind `'abort'`. In-flight
@@ -36,7 +40,29 @@ interface CodeRunRequest {
 }
 ```
 
-The result reports an error as a **field**, never a rejection of `run()` — reporting a failed program is the caller's job, not an exception path (matching `ShellExecutor.run`'s resolve-on-failure contract):
+```ts type-equiv
+/** Fully resolved execution inputs; run never supplies a missing directory or timeout. */
+interface CodeRunSpec extends CodeRunRequest {
+  /** Absolute directory in the provider's execution world. */
+  cwd: string
+  /** Positive finite execution deadline in milliseconds, after provider capping. */
+  timeoutMs: number
+}
+```
+
+```ts type-equiv
+/** File confinement applied to a program, independently of its terminal outcome. */
+interface CodeRunSandbox {
+  /** File-effect mode used for this execution. */
+  mode: SandboxMode
+  /** Whether an observed failure matches the selected backend's denial diagnostics. */
+  denied: boolean
+  /** Completeness reported by the selected confining backend; absent for full access. */
+  enforcement?: SandboxEnforcement
+}
+```
+
+Program failures resolve through `CodeRunResult.error`; invalid caller inputs may reject before execution. Sandbox mode, observed denial and enforcement completeness are separate facts, so a successful program does not by itself prove that every requested restriction was enforced:
 
 ```ts type-equiv
 /**
@@ -45,6 +71,8 @@ The result reports an error as a **field**, never a rejection of `run()` — rep
  * an exception path.
  */
 interface CodeRunResult {
+  /** Applied file policy and observed denial, when the provider enforces file policy. */
+  sandbox?: CodeRunSandbox
   /**
    * The program's completion value (its top-level `return`), when it ran to
    * completion and the value crossed the runtime's lossless-JSON boundary.
@@ -65,7 +93,7 @@ interface CodeRunResult {
 
 ## Bindings: host functions as program globals
 
-Each `CodeBindingNamespace` becomes one global object of async callables inside the program (the PTC mode consumer passes one: `tools`). Arguments and resolutions must be lossless JSON and cross without a seam-level byte cap; the runtime may bridge them through structured clone. A namespace may declare a program-visible error class without making the runtime know the consumer's names: the runtime injects the real constructor and turns rejected calls into its instances. A runtime also treats binding names as hostile input (`__proto__` is an ordinary own property, never a prototype collision):
+Each `CodeBindingNamespace` becomes a global object of async callables; PTC passes `tools`. Arguments and resolutions must be lossless JSON. Providers enforce their own transport caps; the seam sets no uniform binding-byte limit. An optional error-class descriptor creates program-visible typed rejections without naming a consumer inside the runtime. Binding names are own properties, so `__proto__` cannot traverse a prototype:
 
 ```ts type-equiv
 /**
@@ -154,7 +182,7 @@ Failure kinds are **orthogonal outcomes reported independently** (per [defensive
  */
 interface CodeRunFailure {
   /** The failure class (see the interface doc for each kind's meaning). */
-  kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'
+  kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit' | 'protocol' | 'sandbox-unavailable'
   /** Human-readable detail, suitable for feeding back to a model to self-correct. */
   message: string
 }
@@ -162,7 +190,7 @@ interface CodeRunFailure {
 
 ## The service
 
-`CodeRuntime` (`ctx.codeRuntime`, abstract — defined in [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)) is `run(request)` plus two readonly descriptors: `language` (what the program must be written in — `'typescript'` and `'python'` are the well-known values, those `dsh-tools` presents, the TypeScript backend released and the Python backend experimental and private (not published); a consumer generating language-specific presentation switches on it and fails loud on one it cannot present) and `isolation` (the execution substrate — `'worker-thread'`, `'process'`, `'container'`; a diagnostic label, **not a security claim**). Implementations must keep runs isolated from each other (no cross-run state) and dispose to quiescence: in-flight runs are terminated and awaited before teardown completes.
+`CodeRuntime` is defined in [`src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts). `resolve(request)` returns complete execution inputs, and `run(spec)` executes them. `language` selects supported program presentation; `isolation` describes the substrate without claiming security. `sandboxMode` advertises file-policy support, with `undefined` for a provider that does not supply confinement. Each implementation keeps program state separate between runs and terminates and awaits active executions during disposal.
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 38 - 10
docs/subsystems/code-runtime.zh.md

@@ -2,20 +2,18 @@
 
 [English](code-runtime.md) | 中文
 
-代码执行 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md):其 Service Definition([dsh-code-runtime](../../packages/code-runtime/code-runtime),`ctx.codeRuntime`)使用宿主提供的异步绑定运行一段模型编写的程序,并报告其打印内容与返回值。代码执行是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.zh.md) 中。各后端的执行基底与源语言不同,这两项均为服务上的只读描述符;worker-thread Service Provider 与工具注册表 Consumer 的约定见 [PTC mode 基础设计](../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md) 和[类型化返回约定](../../.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md)。
+代码执行[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)通过 [dsh-code-runtime](../../packages/code-runtime/code-runtime) 提供 `ctx.codeRuntime`。它针对 Host 绑定运行一个程序,报告输出、失败与适用的沙箱事实。代码执行是可选能力,不属于[智能体循环主干](core.zh.md)。[PTC 基础](../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md)负责注册表呈现,[类型化返回约定](../../.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md)负责绑定值,[沙箱 Node 决策](../../.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.zh.md)负责已发布的执行提供方。
 
 源码:[`packages/code-runtime/code-runtime/src/types.ts`](../../packages/code-runtime/code-runtime/src/types.ts)
 
 ## 运行:请求进,结果出
 
-`CodeRunRequest` 携带**运行时要处理的一切内容**。按照「包边界处显式优于隐式」的规则,默认值(时间预算、输出上限)来自实现的已校验配置,绝不是 `run()` 内部隐藏的 `??`:
+`CodeRunRequest` 包含程序、绑定、取消和可选执行选择。提供方的 `resolve` 验证支持的选择并应用部署默认值;`run` 接收目录与截止时间明确的 `CodeRunSpec`。无法强制执行所请求策略的提供方在程序执行前拒绝请求:
 
 ```ts type-equiv
 /**
- * One run: the program source plus everything the runtime acts on. Per the
- * explicit-over-implicit convention, defaulting (time budgets, output caps)
- * is the implementation's validated config — a request carries no optional
- * tuning knobs for a hidden `??` to fill in.
+ * Caller inputs for one program. The provider's resolve method validates supported
+ * options and supplies directory, deadline, and authority before execution.
  */
 interface CodeRunRequest {
   /**
@@ -27,6 +25,12 @@ interface CodeRunRequest {
   program: string
   /** Host functions exposed to the program, one global object per namespace. */
   bindings: CodeBindingNamespace[]
+  /** Working directory in the mounted filesystem and subprocess execution world. */
+  cwd?: string
+  /** Requested elapsed execution time; the provider's resolver validates and caps it. */
+  timeoutMs?: number
+  /** Resolved authority for this execution. Providers without confinement reject an explicit policy. */
+  sandboxPolicy?: SandboxExecutionPolicy
   /**
    * Abort the run: the runtime stops the program (hard, even mid-loop) and
    * resolves with a {@link CodeRunFailure} of kind `'abort'`. In-flight
@@ -36,7 +40,29 @@ interface CodeRunRequest {
 }
 ```
 
-结果将错误报告为一个**字段**,而不是让 `run()` 返回被拒绝的 Promise。报告程序失败是调用方的职责,不走异常路径(与 `ShellExecutor.run` 失败时仍正常完成的约定一致):
+```ts type-equiv
+/** Fully resolved execution inputs; run never supplies a missing directory or timeout. */
+interface CodeRunSpec extends CodeRunRequest {
+  /** Absolute directory in the provider's execution world. */
+  cwd: string
+  /** Positive finite execution deadline in milliseconds, after provider capping. */
+  timeoutMs: number
+}
+```
+
+```ts type-equiv
+/** File confinement applied to a program, independently of its terminal outcome. */
+interface CodeRunSandbox {
+  /** File-effect mode used for this execution. */
+  mode: SandboxMode
+  /** Whether an observed failure matches the selected backend's denial diagnostics. */
+  denied: boolean
+  /** Completeness reported by the selected confining backend; absent for full access. */
+  enforcement?: SandboxEnforcement
+}
+```
+
+程序失败通过 `CodeRunResult.error` 返回;无效调用输入可能在执行前拒绝。沙箱模式、观察到的拒绝与强制完整性是独立事实,因此程序成功本身不能证明每项请求限制均已强制执行:
 
 ```ts type-equiv
 /**
@@ -45,6 +71,8 @@ interface CodeRunRequest {
  * an exception path.
  */
 interface CodeRunResult {
+  /** Applied file policy and observed denial, when the provider enforces file policy. */
+  sandbox?: CodeRunSandbox
   /**
    * The program's completion value (its top-level `return`), when it ran to
    * completion and the value crossed the runtime's lossless-JSON boundary.
@@ -65,7 +93,7 @@ interface CodeRunResult {
 
 ## 绑定:宿主函数作为程序全局变量
 
-每个 `CodeBindingNamespace` 在程序内成为一个由异步可调用函数组成的全局对象(PTC mode Consumer 传入一个:`tools`)。参数与返回值必须是无损 JSON,且跨越边界时不受 seam 层字节上限约束;运行时可以通过结构化克隆桥接它们。命名空间可以声明程序可见的错误类,而无需让运行时知道 Consumer 的名称:运行时会注入真实构造函数,并将被拒绝的调用转为该类的实例。运行时也将绑定名视为不可信输入(`__proto__` 是普通自有属性,绝不会发生原型碰撞):
+每个 `CodeBindingNamespace` 成为一个异步可调用函数的全局对象;PTC 传入 `tools`。参数与返回值必须是无损 JSON。提供方强制各自的传输上限;seam 不设统一的绑定字节上限。可选错误类描述符创建程序可见的类型化拒绝,无需在运行时内点名消费方。绑定名是自有属性,因此 `__proto__` 不能遍历原型:
 
 ```ts type-equiv
 /**
@@ -154,7 +182,7 @@ type CodeBindingFunction = (args: unknown) => Promise<CodeJsonValue>
  */
 interface CodeRunFailure {
   /** The failure class (see the interface doc for each kind's meaning). */
-  kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'
+  kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit' | 'protocol' | 'sandbox-unavailable'
   /** Human-readable detail, suitable for feeding back to a model to self-correct. */
   message: string
 }
@@ -162,7 +190,7 @@ interface CodeRunFailure {
 
 ## 服务
 
-`CodeRuntime`(`ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,已知值为 `'typescript'` 与 `'python'`,即 `dsh-tools` 能呈现的那些,TypeScript 后端已发布、Python 后端为实验性且私有(未发布);生成语言相关展示的 Consumer 据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose(资源释放)时等待系统完全停稳:teardown 要等到所有进行中的运行均已终止并结算后才完成。
+`CodeRuntime` 定义于 [`src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)。`resolve(request)` 返回完整执行输入,`run(spec)` 执行它们。`language` 选择支持的程序呈现;`isolation` 描述执行基底,不作安全声明。`sandboxMode` 声明文件策略支持,不提供约束的提供方返回 `undefined`。每个实现将各次运行的程序状态分离,并在资源释放期间终止且等待活跃执行。
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 2 - 2
docs/testing.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/testing.md
-testing.md: bbf7db5d788667dc85c34dfdac4784e2b0dccca8
-testing.zh.md: 7d386494bb8fd712b93aeeeb4f8c7b196349a1db
+testing.md: f976b2cad238bc9bacee488f7168e5deb5e9b1a7
+testing.zh.md: 88784b16ae8b5b104de2f3cb20c7f285585871d6

+ 1 - 1
docs/testing.md

@@ -38,7 +38,7 @@ An e2e assertion re-runs the command or re-reads the file externally; a keyword
 
 - Product-visible plugins require a non-unit REAL-composition test. Hand-built `ctx.plugin(...)` suites are insufficient: boot test-only `cordis.yml` through Loader and app/process, mock only external services or nondeterministic inputs, and assert model-visible request/log, durable state, or user-visible output. Keep opt-ins out of shipped defaults.
 - A guard only guards if the regression fails it. For a plugin without `inject` (bundle/composition plugins), a Loader smoke stays green when a default export replaces the required named exports — add an explicit `expect('default' in mod).toBe(false)` plus an `unwrapExports` round-trip assertion, and prove it: introduce the regression, watch red, revert.
-- "Real entry path" means the published artifact: a package `bin` runs built `lib/bin.js` under plain `node`, exposing failures tsx masks (settle races, module resolution, swallowed load failures). The same applies to non-index runtime entries (the worker-thread sibling `lib/worker.cjs`) and singleton modules shared across bundles (`packages/sdk/server/tests/built-scope-carrier.e2e.ts`). Keep the built-artifact smokes green (`packages/examples/*/tests/built-bin.e2e.ts`, `packages/code-runtime/code-runtime-node/tests/built-lib.e2e.ts`), and assert a genuinely-missing config exits non-zero.
+- "Real entry path" means the published artifact: a package `bin` runs built `lib/bin.js` under plain `node`, exposing failures tsx masks (settle races, module resolution, swallowed load failures). The same applies to non-index runtime entries (the Node program bootstrap `lib/process.js`) and singleton modules shared across bundles (`packages/sdk/server/tests/built-scope-carrier.e2e.ts`). Keep the built-artifact smokes green (`packages/examples/*/tests/built-bin.e2e.ts`, `packages/code-runtime/code-runtime-node/tests/built-lib.e2e.ts`), and assert a genuinely-missing config exits non-zero.
 
 ## Test resolution: source plane only
 

+ 1 - 1
docs/testing.zh.md

@@ -38,7 +38,7 @@ e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身
 
 - 产品可见的插件必须有一个非单元的真实组合测试。手动构建的 `ctx.plugin(...)` 套件不够:通过 Loader 和 app/process 启动仅用于测试的 `cordis.yml`,只 mock 外部服务或非确定性输入,断言模型可见的请求/日志、持久状态或用户可见输出。不要把 opt-in 选项混入交付默认值。
 - 一个守卫只有在回归能让它失败时才有效。对于没有 `inject` 的插件(bundle/组合插件),Loader 冒烟测试在默认导出替换必需的具名导出时仍然绿着——需要添加显式的 `expect('default' in mod).toBe(false)` 加 `unwrapExports` 往返断言,并证明它有效:引入回归、观察变红、回退。
-- 「真实入口路径」指已发布的产物:包的 `bin` 所运行的是构建后的 `lib/bin.js`,并由普通 `node` 执行,从而暴露 tsx 会掩盖的失败(结算竞态、模块解析、被吞掉的加载失败)。同样的规则适用于非 index 运行时入口(worker-thread 的同级文件 `lib/worker.cjs`),也适用于多个 bundle 共享的单例模块(`packages/sdk/server/tests/built-scope-carrier.e2e.ts`)。保持构建产物冒烟测试绿色(`packages/examples/*/tests/built-bin.e2e.ts`、`packages/code-runtime/code-runtime-node/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零状态退出。
+- 「真实入口路径」指已发布的产物:包的 `bin` 所运行的是构建后的 `lib/bin.js`,并由普通 `node` 执行,从而暴露 tsx 会掩盖的失败(结算竞态、模块解析、被吞掉的加载失败)。同样的规则适用于非 index 运行时入口(Node 程序 bootstrap `lib/process.js`),也适用于多个 bundle 共享的单例模块(`packages/sdk/server/tests/built-scope-carrier.e2e.ts`)。保持构建产物冒烟测试绿色(`packages/examples/*/tests/built-bin.e2e.ts`、`packages/code-runtime/code-runtime-node/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零状态退出。
 
 ## 测试解析:仅限源码
 

+ 2 - 2
docs/user/guide/network-proxy.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/user/guide/network-proxy.md
-network-proxy.md: d53d48688490c741f0ed7f950c5ff39db02dc1e9
-network-proxy.zh.md: 1eee1e67abb700e15f3b2cea1b22bf036c694302
+network-proxy.md: 5696cf6e42e2a1e2c5f817a3544b279dce15565e
+network-proxy.zh.md: 6fbe2669e2105c9d297c3a63eb8f280e0d431325

+ 1 - 1
docs/user/guide/network-proxy.md

@@ -66,7 +66,7 @@ Node reads that variable only at process start, so export it before running `dsh
 Not every request DSH makes goes through the proxy:
 
 - **Anything on this machine.** Loopback is always direct: `localhost`, the whole `127.0.0.0/8` range, `::1`, and `0.0.0.0`. A proxy cannot usefully reach a service that only listens locally.
-- **Code the model writes.** The workflow and code-runtime workers never receive the proxy settings, so a script the model authors cannot read a proxy URL that may carry a password. Such a script reaches the network only if it configures that itself.
+- **Code the model writes.** Workflow workers and Node code-runtime processes receive no proxy settings, so model-authored scripts cannot read a proxy URL that may carry a password. Direct requests must configure any required proxy themselves and remain subject to the execution sandbox.
 - **Usage telemetry.** The OTLP exporter uses Node's own HTTP client rather than the one a proxy configures, so telemetry connects directly and simply fails where direct egress is blocked. Nothing you do in DSH depends on it. Set `DSH_TELEMETRY_MODE=DISABLED` to turn it off entirely.
 - **`web_fetch` to a literal private address.** A URL naming an address like `http://10.0.0.5/` is refused rather than handed to the proxy, the same refusal it gets with no proxy configured.
 

+ 1 - 1
docs/user/guide/network-proxy.zh.md

@@ -66,7 +66,7 @@ Node 只在进程启动时读取该变量,所以要在运行 `dsh` 之前导
 并非 DSH 发出的每个请求都会走代理:
 
 - **本机上的一切。** loopback 始终直连:`localhost`、整个 `127.0.0.0/8` 段、`::1` 与 `0.0.0.0`。代理无法有意义地访问一个只在本地监听的服务。
-- **模型编写的代码。** workflow 与 code-runtime worker 从不接收代理配置,因此模型编写的脚本读不到可能携带密码的代理 URL。这类脚本只有自行配置才能联网。
+- **模型编写的代码。** Workflow worker 与 Node code-runtime 进程不接收代理配置,因此模型脚本读不到可能携带密码的代理 URL。直接请求必须自行配置所需代理,并继续受到执行沙箱的约束。
 - **使用情况遥测。** OTLP 导出器用的是 Node 自带的 HTTP 客户端,而不是代理所配置的那个,因此遥测直连;在禁止直连出网的环境里它只会失败。DSH 的任何功能都不依赖它。设 `DSH_TELEMETRY_MODE=DISABLED` 可完全关闭。
 - **`web_fetch` 访问字面量私网地址。** 形如 `http://10.0.0.5/` 的 URL 会被拒绝而非交给代理,与未配置代理时得到的拒绝相同。
 

+ 2 - 2
packages/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/README.md
-README.md: 22e440b98f8440548c02d48ca3d56890718eafb6
-README.zh.md: 60f033cf01124187c123692883635b64d2bc9754
+README.md: 7d8dee81ef6db50e2ac7e13bdac7834c694dfcbf
+README.zh.md: 14134c3f87fe511844696f85ead9fdb9e94ebbfb

+ 1 - 1
packages/README.md

@@ -39,7 +39,7 @@ Every package lives in exactly one group; new packages join existing groups, and
 | [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider |
 | [`shell/`](shell/README.md) | Bash capability family: executor seam, local impl, model-facing tools |
 | [`terminal/`](terminal/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, model-facing tools |
-| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: Service Definition + worker-thread provider + PTC mode Consumer |
+| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: Service Definition + sandboxed Node provider + PTC mode Consumer |
 | [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends |
 | [`fs/`](fs/README.md) | Filesystem capability family: seam, local impl, model-facing file tools, discovery tools |
 | [`lsp/`](lsp/README.md) | LSP capability family: seam, generic stdio provider, and the `lsp` tool |

+ 1 - 1
packages/README.zh.md

@@ -39,7 +39,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 | [`subprocess/`](subprocess/README.zh.md) | 子进程能力系列:Service Definition + 本地进程树提供方 |
 | [`shell/`](shell/README.zh.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 |
 | [`terminal/`](terminal/README.zh.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现、面向模型的工具 |
-| [`code-runtime/`](code-runtime/README.zh.md) | 代码执行能力系列:Service Definition + worker 线程提供方 + PTC mode Consumer |
+| [`code-runtime/`](code-runtime/README.zh.md) | 代码执行能力族:Service Definition + 沙箱 Node 提供方 + PTC mode Consumer |
 | [`sandbox/`](sandbox/README.zh.md) | 进程限制 seam;bwrap、Landlock、Seatbelt 后端 |
 | [`fs/`](fs/README.zh.md) | 文件系统能力系列:seam、本地实现、面向模型的文件工具、发现工具 |
 | [`lsp/`](lsp/README.zh.md) | LSP 能力系列:seam、通用 stdio 提供方和 `lsp` 工具 |

+ 2 - 2
packages/client/ui-tool/package.json

@@ -52,7 +52,6 @@
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-spill": "workspace:^",
-    "@deepseek-ai/dsh-code-runtime-node": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
@@ -71,7 +70,8 @@
     "@deepseek-ai/dsh-client-ui-session": "workspace:^",
     "@deepseek-ai/dsh-util-workspace-path": "workspace:^",
     "@deepseek-ai/dsh-attachment": "workspace:^",
-    "clsx": "^2.0.0"
+    "clsx": "^2.0.0",
+    "@deepseek-ai/dsh-code-runtime": "workspace:^"
   },
   "files": [
     "lib/index.js",

+ 18 - 2
packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts

@@ -2,7 +2,8 @@
 import { Context } from '@deepseek-ai/cordis'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import type { ToolResultNode } from '@deepseek-ai/dsh-client-ui-chat/client'
-import { mountRuntime } from '../../../code-runtime/code-runtime-node/tests/setup.ts'
+import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
+import type { CodeRunRequest, CodeRunSpec, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
 import { ToolCallId } from '@deepseek-ai/dsh-llm'
 import { Session, SessionId } from '@deepseek-ai/dsh-session'
 import { SpillLocator, SpillStore, type SaveTextSpill, type SpillRef } from '@deepseek-ai/dsh-spill'
@@ -40,7 +41,22 @@ async function executeShell(text: string, nested: boolean, name = 'bash', maxInl
     await ctx.plugin(ToolRuntime, { mode: 'both' })
     await ctx.plugin(MemorySpillStore)
     await ctx.plugin(SpillPolicy, { maxInlineBytes })
-    if (nested) await mountRuntime(ctx, {})
+    if (nested) {
+      // The real registry and spill policy own nested output; evaluator execution has its own process suite.
+      class BindingRuntime extends CodeRuntime {
+        readonly language = 'typescript'
+        readonly isolation = 'fixture'
+        resolve(request: CodeRunRequest): CodeRunSpec {
+          return { ...request, cwd: process.cwd(), timeoutMs: 120_000 }
+        }
+        async run(spec: CodeRunSpec): Promise<CodeRunResult> {
+          const tool = spec.bindings.find(binding => binding.global === 'tools')?.functions[name]
+          if (tool === undefined) throw new Error('missing fixture binding')
+          return { logs: [], value: await tool(shellArgs) }
+        }
+      }
+      await ctx.plugin(BindingRuntime)
+    }
     ctx.effect(() => ctx.tools.register(defineContentToolFixture({
       name,
       description: 'Return deterministic shell text without spawning a process.',

+ 2 - 2
packages/code-runtime/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/code-runtime/README.md
-README.md: 3c99d44c6152b80e17d4349c4daf5cb459de7823
-README.zh.md: f9151a5094cc623fc65afe91b55fbd69da5aa0fa
+README.md: 13c0d0e16816a8f50376658fb943a12068909997
+README.zh.md: cb69ed488f83a61776d383225e61a86b61d2d433

+ 2 - 2
packages/code-runtime/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The `code-runtime/` group lets a model write one program that calls host-provided functions as ordinary async calls, then returns only the program's printed output and return value. Choose the TypeScript backend for execution in an isolated Node worker, or the experimental Python backend when a CPython process is required. Each run starts without state from earlier programs. Failures are returned as results so callers can diagnose them or provide them to the model.
+The `code-runtime/` group lets a model write one program that calls host-provided functions as ordinary async calls, then returns only the program's printed output and return value. Choose the TypeScript backend for execution in a fresh Node process under the configured sandbox policy, or the experimental Python backend when a CPython process is required. Each run starts without state from earlier programs. Failures are returned as results so callers can diagnose them or provide them to the model.
 
 ## Table of Contents
 
@@ -27,7 +27,7 @@ These three packages together provide program execution; each README describes w
 | Package | Role | ctx key |
 |---|---|---|
 | [`code-runtime/`](code-runtime/README.md) | Defines what a code runtime does: run one program against host-provided bindings and report what it printed and returned | `ctx.codeRuntime` |
-| [`code-runtime-node/`](code-runtime-node/README.md) | Executes TypeScript programs, each in a fresh Node worker thread | registers `ctx.codeRuntime` |
+| [`code-runtime-node/`](code-runtime-node/README.md) | Executes TypeScript in fresh managed Node processes under the resolved sandbox policy | registers `ctx.codeRuntime` |
 | [`experimental/code-runtime-python/`](../experimental/code-runtime-python/README.md) | The experimental Python backend: owns the fd-3 wire protocol between a Node host and a CPython subprocess and the CPython runtime implementation | — |
 
 -----

+ 2 - 2
packages/code-runtime/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-group"
 
 ## 概述
 
-`code-runtime/` 组让模型编写一个程序,以普通异步调用的方式调用宿主提供的函数,然后只返回程序的打印输出和返回值。如需在隔离的 Node Worker 中执行,请选择 TypeScript 后端;如需 CPython 进程,请选择实验性 Python 后端。每次运行都不会保留之前程序的状态。失败会作为结果返回,供调用方诊断或提供给模型。
+`code-runtime/` 组让模型编写一个程序,以普通异步调用的方式调用宿主提供的函数,然后只返回程序的打印输出和返回值。如需在全新 Node 进程中按所配沙箱策略执行,请选择 TypeScript 后端;如需 CPython 进程,请选择实验性 Python 后端。每次运行都不会保留之前程序的状态。失败会作为结果返回,供调用方诊断或提供给模型。
 
 ## 目录
 
@@ -27,7 +27,7 @@ kind: "package-group"
 | 包 | 角色 | ctx 键 |
 |---|---|---|
 | [`code-runtime/`](code-runtime/README.zh.md) | 定义代码运行时做什么:针对宿主提供的绑定运行一个程序,并报告其打印和返回的内容 | `ctx.codeRuntime` |
-| [`code-runtime-node/`](code-runtime-node/README.zh.md) | 在全新的 Node Worker 线程中执行 TypeScript 程序 | 注册 `ctx.codeRuntime` |
+| [`code-runtime-node/`](code-runtime-node/README.zh.md) | 在全新受管 Node 进程中按已解析沙箱策略执行 TypeScript | 注册 `ctx.codeRuntime` |
 | [`experimental/code-runtime-python/`](../experimental/code-runtime-python/README.zh.md) | 实验性 Python 后端:负责 Node 宿主与 CPython 子进程之间的 fd-3 协议,以及 CPython 运行时实现 | — |
 
 -----

+ 2 - 2
packages/code-runtime/code-runtime-node/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-node/README.md
-README.md: d03fd010db7d1475cc153632ca28fc83f782704b
-README.zh.md: bfc58f8ba0a9adff4a687cc233cc7da56224564a
+README.md: 02064e378310c2d571caca61912f6b599329714e
+README.zh.md: d23df399936ec4f20e1dc6c802c6832f32a4ab42

+ 59 - 62
packages/code-runtime/code-runtime-node/README.md

@@ -1,5 +1,5 @@
 ---
-description: "Worker-thread code execution for users and maintainers composing, sizing, or debugging the shipped TypeScript backend that runs each program in a fresh Node worker."
+description: "Run TypeScript programs in fresh Node processes with the session filesystem sandbox, managed cleanup, and configurable execution and output limits."
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This package lets PTC compositions execute model-written TypeScript with host-provided bindings and receive the completion value, ordered logs, or a structured failure. Each request starts with no state from earlier runs, and failures such as syntax errors, budget expiry, aborts, memory exhaustion, and output overflow are returned instead of thrown. Treat executed code as bash-equivalent: the package limits environment exposure and resource use, but does not isolate code from the host. Configurable compute, wall-clock, heap, and output limits terminate the run and bound its results.
+Execute model-written TypeScript under the same platform sandbox policy as Bash, with host-provided functions available as async bindings. Each call starts a fresh Node process and returns captured logs, an exact JSON value, or a structured failure. Direct Node APIs remain available within the selected restrictions. Elapsed deadlines, output bounds and a V8 heap limit constrain execution; cancellation and completion terminate the managed process range. A requested restricted mode fails when its sandbox backend is unavailable.
 
 ## Table of Contents
 
@@ -25,40 +25,51 @@ This package lets PTC compositions execute model-written TypeScript with host-pr
 <a id="use-this-package"></a>
 ## Use this package
 
-Mount this backend with the code-runtime seam when a composition should execute model-written TypeScript programs; PTC mode in `dsh-tools` then drives it through `ctx.codeRuntime` whenever the model calls `run_code`. Every execution cap is validated config, so you can size the runtime for your deployment from `cordis.yml`.
+Mount this provider in a composition that supplies `fs`, `subprocess`, `sandbox` and `sandboxPolicy`. PTC mode in `dsh-tools` supplies the calling Session's directory and standing policy; direct runtime consumers resolve those options before execution.
 
-### Minimal configuration
+### Configuration
+
+Configure the provider row after its required services are available:
 
 ```yaml
-- name: '@deepseek-ai/dsh-code-runtime'
 - name: '@deepseek-ai/dsh-code-runtime-node'
   config:
-    computeMs: 60000            # busy-time budget (measured event-loop active time)
-    maxWallMs: 600000           # wall-clock ceiling; never pauses for anything
-    maxOutputBytes: 67108864    # combined serialized outer-output cap (64 MiB)
-    maxOldGenerationSizeMb: 512 # worker heap cap
+    timeoutMs: 120000
+    maxTimeoutMs: 600000
+    maxOutputBytes: 67108864
+    maxOldGenerationSizeMb: 512
+    maxMessageBytes: 134217728
+    maxPendingCalls: 128
+    graceMs: 3000
 ```
 
 | Field | Default | Meaning |
 |---|---|---|
-| `computeMs` | `60,000` | Busy-time budget: the run fails with `timeout` once the worker's measured event-loop active time exceeds it |
-| `maxWallMs` | `600,000` | Wall-clock ceiling, the backstop for waits that busy time cannot see; at most `2_147_483_647` |
-| `maxOutputBytes` | `67,108,864` | Hard cap for serialized logs plus the completion value or failure message; at least `4` |
-| `maxOldGenerationSizeMb` | `512` | Worker heap cap; overflow kills the worker and surfaces as `worker-exit` |
+| `timeoutMs` | `120,000` | Default elapsed execution deadline, including nested tool and approval waits |
+| `maxTimeoutMs` | `600,000` | Elapsed deadline ceiling applied by the resolver |
+| `maxOutputBytes` | `67,108,864` | Combined serialized logs and completion or diagnostic budget |
+| `maxOldGenerationSizeMb` | `512` | V8 old-generation heap limit in MiB |
+| `maxMessageBytes` | `134,217,728` | Limit for a control frame, outstanding argument bytes and queued control writes |
+| `maxPendingCalls` | `128` | Maximum simultaneous host binding calls |
+| `graceMs` | `3,000` | Managed termination and output-drain grace |
+| `nodeExecutable` | Current Node executable | Executable resolved in the subprocess execution world |
+| `bootstrapPath` | Package bootstrap | Optional absolute path to a preinstalled built bootstrap in that world |
+
+The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-code-runtime-node) defines accepted config fields. `resolve(request)` supplies cwd, the capped timeout and the execution policy; `run(spec)` accepts those resolved inputs and does not fill missing values.
 
-Every field is validated and defaulted at load; there are no other tunables. The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-code-runtime-node) is the exhaustive source for every accepted field.
+### Execution and results
 
-### What a run returns
+Programs are async function bodies: top-level `await` and `return` work, and only erasable TypeScript is accepted. A successful call returns its lossless-JSON value as `result.value` and captured text as `result.logs`. `result.sandbox` reports the selected mode, observed denial and the backend's full or partial enforcement independently of the program outcome.
 
-A successful run returns the program's lossless-JSON completion value as `result.value` and the text it printed, in order, as `result.logs`. Top-level `await` and `return` work, and the program can call the host-provided binding functions (PTC mode exposes one `tools` object) as ordinary async calls.
+Direct filesystem, network and subprocess operations remain Node operations, subject to the selected OS sandbox. Nested host bindings cross the control channel; PTC tool calls retain the registry's visibility, ordering, logging and approval rules. Running a program does not change the Session's standing policy or automatically replay it after a denial.
 
-### Containment, not a security boundary
+### Deadlines and cancellation
 
-A program runs with authority comparable to the bash tool: it can reach Node APIs, and the backend deliberately does not promise isolation from the host. What it does provide is containment — a separate isolate, an empty environment (no ambient credentials, no inherited loader flags), a configurable heap cap, and hard termination that also stops a hot synchronous loop. OS processes a program spawns survive `terminate()` and need deployment-level cleanup.
+The elapsed deadline covers runtime setup and execution, including time awaiting nested tools or approval. It is not a CPU meter. Timeout or cancellation stops a synchronous loop through the host's managed process owner; successful completion also cleans that managed range. The timer stops when an outcome is selected, before cleanup, so the returned call can take longer than its execution deadline while cleanup settles.
 
-### What can go wrong
+### Failures
 
-Every program outcome resolves as a result, so a failed run is a `result.error`, not a rejection: a syntax error or non-erasable TypeScript (`enum`, namespaces) fails as `exception` before any worker spawns; budget expiry is `timeout`; the abort signal is `abort`; a heap overflow or other worker death is `worker-exit`; a completion value that is not lossless JSON is `invalid-output`; and serialized output beyond the cap is `output-limit` — with the fitting captured log prefix retained. Rejection means caller misuse, such as a run submitted after disposal.
+Program parse errors and thrown exceptions are `exception`; deadline expiry is `timeout`; cancellation is `abort`; malformed or excessive control traffic is `protocol`; unavailable confinement is `sandbox-unavailable`; early process exit or failed managed cleanup is `worker-exit`. The substrate-independent failure name remains `worker-exit` for process providers. Lossy completions are `invalid-output`, and an oversized outer result is `output-limit`, retaining the fitting log prefix. Invalid or unsupported options and calls after disposal reject as caller misuse.
 
 -----
 
@@ -68,43 +79,29 @@ Every program outcome resolves as a result, so a failed run is a `result.error`,
 <details>
 <summary>Implementation internals — click to expand</summary>
 
-This section explains the design behind the backend; observable behavior is fully covered in [Use this package](#use-this-package).
-
-### Design concept
-
-The backend rests on one separation: **containment, not a security boundary**. Model code has bash-equivalent trust (the [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) Trust posture), so the design optimizes for reconstructability and bounded resource use rather than for a hard multi-tenant boundary — that awaits a container-class backend. Each run gets one fresh worker, so a program's world dies with its worker: no cross-run state exists to leak or to log, and a run is reconstructable from the session log alone.
-
-### Execution flow
+The host owns policy, deadlines, binding lookup and process cleanup. The child owns program evaluation and binding proxies; model-written code is an untrusted peer even when its messages use the expected control descriptor.
 
-A run is type-stripped host-side (`node:module`'s `stripTypeScriptTypes`, position-preserving), wrapped as the body of an async function so top-level `await`/`return` work, and sent to a fresh worker whose bootstrap materializes the binding namespaces. Binding calls cross the message port as lossless JSON and are answered at most once per call id. Log text streams to the host eagerly so a killed program still shows what it printed. Exactly one outcome settles the run — a `done` frame, a budget expiry, an abort, or worker death — after which the host terminates the worker and awaits its exit.
+### Launch and control
 
-### Hostile-peer port
+The host strips erasable types, resolves the executable and bootstrap in the configured execution world, wraps the argv through `ctx.sandbox`, and spawns through `ctx.subprocess`. The child adopts the inherited control channel and clears its environment before evaluating the program. Explicit Node arguments avoid inheriting host loader or inspector flags.
 
-Model code can reach `parentPort` and forge traffic, so every inbound message is shape-validated and rebuilt field by field before anything reads it: forged extra fields never ride along, a non-number call id can never be echoed into a reply, binding names resolve as own properties only (a forged `constructor` cannot walk a prototype chain), and junk drops silently. Worker-side namespaces are null-prototype, so `__proto__`-shaped binding names are ordinary keys.
+Length-framed JSON travels separately from stdout/stderr. The host bounds frames and queued writes, validates call identity and declared binding names before dispatch, and refuses invalid traffic. Output capture meters serialized logs plus the completion or diagnostic; fixed result-envelope fields and sandbox metadata are outside that ledger.
 
-### Budgets
+### Source and built bootstraps
 
-Two independent budgets exist because the peer is hostile: `computeMs` meters the worker's measured busy time (`eventLoopUtilization()` polling every 25 ms), so a hot loop expires it whether or not a decoy dispatch is in flight, while a program idling on a slow binding accrues nothing; `maxWallMs` backstops what busy time cannot see, such as a promise nobody resolves. Both funnel into `worker.terminate()`. `maxWallMs` is range-checked at load against `MAX_TIMER_DELAY_MS` because `setTimeout` clamps a longer delay to 1 ms.
-
-### Output ledger
-
-`maxOutputBytes` accounts the JSON serialization of the outer `logs` array plus the completion value or failure-message payload; fixed `CodeRunResult` field names and envelope syntax are outside that ledger. At or below the cap the exact value returns; a lossy completion is `invalid-output`, and a combined overflow is `output-limit` rather than a substituted inspected string. The failure retains a fitting captured prefix of the logs.
+Source execution loads an erasable-only bootstrap closure without relying on sibling built exports. Built execution uses the packaged `process.js` entry. An execution world that cannot map the host bootstrap requires a preinstalled compatible `bootstrapPath`; a host path is never assumed to name the same remote file.
 
 ### Source map
 
 | File | Role |
 |---|---|
-| [`src/index.ts`](src/index.ts) | Plugin entry: `Config` schema, `NodeCodeRuntime`, run orchestration, output ledger |
-| [`src/worker.ts`](src/worker.ts) | Source-mode worker entry (erasable TypeScript, no `lib/` dependency) |
-| [`src/bootstrap.ts`](src/bootstrap.ts) | Worker-side bootstrap: namespace materialization, console shim, log capture |
-| [`src/protocol.ts`](src/protocol.ts) | Port message vocabulary between host and worker |
-| [`src/worker-json.ts`](src/worker-json.ts) | Worker-side lossless-JSON encode/decode |
-| [`src/output-json.ts`](src/output-json.ts) | Byte metering and truncation for the outer ledger |
-| — | No runtime invariant companion is published; this process-boundary implementation exposes no same-process event relation; worker protocol and built-worker tests cover it. |
-
-### The worker entry, unbuilt and built
-
-Source mode loads erasable-only `src/worker.ts` through Node's native type stripping; its transitive runtime closure contains only Node built-ins and relative source modules, so a fresh checkout never requires a sibling workspace package's unbuilt `lib/` export. Built mode passes the sibling `lib/worker.cjs` as a filesystem path because pkg's VFS Worker hook expects CommonJS; the same path works under ordinary Node.
+| [`src/index.ts`](src/index.ts) | Configuration, resolution, policy, bindings and managed execution |
+| [`src/launch.ts`](src/launch.ts) | Executable/bootstrap arguments and execution-world asset mapping |
+| [`src/process.ts`](src/process.ts) | Child handshake, environment clearing and program lifecycle |
+| [`src/bootstrap.ts`](src/bootstrap.ts) | Program evaluation, binding proxies and output capture |
+| [`src/channel.ts`](src/channel.ts) | Framing, bounded writes and protocol failures |
+| [`src/output-ledger.ts`](src/output-ledger.ts) | Host accounting for the outer result |
+| — | No runtime invariant companion is published; framing and process cleanup are enforced across the process boundary rather than through independent same-process observations. |
 
 </details>
 
@@ -113,19 +110,19 @@ Source mode loads erasable-only `src/worker.ts` through Node's native type strip
 <a id="further-exploration"></a>
 ## Further Exploration
 
-Read these when the backend contract is not enough. They move from the seam definition to the consumer and the configuration surface.
+Read the service contract before using the provider directly; the decisions explain policy and consumer ownership.
 
-- [Code runtime seam](../code-runtime/README.md) — the abstract contract this backend implements.
-- [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) — how `dsh-tools` consumes `ctx.codeRuntime` and presents `run_code`.
-- [Code runtime subsystem reference](../../../docs/subsystems/code-runtime.md) — request/result vocabulary, bindings, and failure taxonomy.
-- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-code-runtime-node) — every accepted config field and its source declaration.
+- [Code runtime service](../code-runtime/README.md) — requests, resolved specs and results.
+- [Sandboxed Node decision](../../../.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.md) — security, lifecycle and timeout tradeoffs.
+- [PTC foundation](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) — registry presentation and nested tool dispatch.
+- [Subprocess provider](../../subprocess/subprocess-local/README.md) — managed process ranges and platform limitations.
 
 -----
 
 <a id="model-experience"></a>
 ## Model Experience
 
-Indirectly, through PTC mode in `dsh-tools`, which renders the exact outer value when it fits or an explicit `invalid-output` / `output-limit` failure, while only the outer `run_code` result enters model context under its ordinary spill policy and binding traffic plus intermediate values remain execution-local.
+Indirectly, through PTC mode in `dsh-tools`, which returns captured logs and the completion value or a failure with sandbox facts. Intermediate binding traffic stays outside model history; the outer result follows the ordinary tool spill policy.
 
 #### KV Cache effect
 
@@ -135,15 +132,15 @@ No direct invalidation; the named consumer owns any request-prefix changes.
 
 <a id="known-limitations-and-deferred-work"></a>
 
+These limits qualify the execution guarantees and retained output.
 
-These limits define when the backend is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
-
-- **OS processes a program spawns survive termination** — `worker.terminate()` ends the thread only, weaker than bash-local's process-group kill; orphan cleanup is a deployment concern until a container backend exists.
-- **Type-strip rides Node's experimental `stripTypeScriptTypes` API** — amaro or sucrase are the named drop-in replacements if the relied-on behavior shifts.
-- **`computeMs` expiry can overshoot by up to one poll interval** — busy time is sampled every 25 ms (an internal constant, deliberately not config).
-- **Programs get a five-method `console` shim** (`log`/`info`/`warn`/`error`/`debug`) — deliberately not Node's full console API.
-- **Intermediate binding values have no byte cap** — a program can exhaust process or worker memory with a value that never becomes outer output.
-- **The default 64 MiB cap is a rejection boundary, not recoverable storage** — outer spill can save only the bounded logs and diagnostic returned after `output-limit`; bytes rejected beyond the runtime cap never reach the spill layer.
+- **Confinement inherits the selected backend's limits** — full and partial enforcement are reported separately; sandbox policy and managed-process containment are distinct guarantees.
+- **The heap cap is not a process-tree memory limit** — native allocations and descendant memory are outside the V8 old-generation bound. No process-tree CPU meter is supplied.
+- **Cleanup inherits subprocess observability** — escaped descendants on a fallback platform may remain outside the managed range; see the subprocess provider's stated limits.
+- **Execution is one-shot** — no yield/wait API, live result stream or retained program state exists between calls.
+- **Output caps reject rather than retain every byte** — spill can preserve only the bounded result delivered by this provider.
+- **Bindings are bounded at transport admission** — control limits do not bound the memory a host binding allocates while producing its result.
+- **The console shim has five methods** — `log`, `info`, `warn`, `error` and `debug`.
 
 <a id="dev-note"></a>
 ### Dev Note
@@ -151,6 +148,6 @@ These limits define when the backend is a poor fit or needs special operational
 <details>
 <summary>Working context for maintainers — click to expand</summary>
 
-None.
+The [timeout discussion](../../../.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.md#deferred-timeout-design) records open choices about yielding, total lifetime, approval wait accounting and process-tree CPU/RSS limits. Those choices do not change the configured elapsed deadline.
 
 </details>

+ 66 - 69
packages/code-runtime/code-runtime-node/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "Worker 线程代码执行,面向组装、容量规划或调试已发布 TypeScript 后端的用户与维护者;该后端在全新的 Node worker 中运行每个程序。"
+description: "在全新 Node 进程中运行 TypeScript 程序,使用会话文件系统沙箱、受管清理以及可配置的执行与输出限制。"
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-本包让 PTC 组合能够使用宿主提供的绑定执行模型编写的 TypeScript,并取得完成值、顺序日志或结构化失败。每次请求都不继承先前运行的状态;语法错误、预算到期、中止、内存耗尽和输出溢出等失败会作为结果返回,而不是抛出。应将执行的代码视为与 bash 拥有同等权限:本包限制环境暴露和资源使用,但不将代码与宿主隔离。可配置的计算时间、墙钟时间、堆和输出上限会终止运行并限制其结果大小。
+在与 Bash 相同的平台沙箱策略下执行模型编写的 TypeScript,并通过异步绑定调用 Host 提供的函数。每次调用启动一个全新的 Node 进程,返回捕获日志、精确 JSON 值或结构化失败。直接 Node API 在所选限制内仍可使用。经过时间截止、输出上限和 V8 堆限制约束执行;取消和完成都会终止受管进程范围。请求受限模式但沙箱后端不可用时,执行失败。
 
 ## 目录
 
@@ -17,7 +17,7 @@ kind: "package-reference"
 - [理解实现](#understand-the-implementation)
 - [进一步探索](#further-exploration)
 - [模型体验](#model-experience)
-- [已知限制与延期工作](#known-limitations-and-deferred-work)
+- [已知限制与延后工作](#known-limitations-and-deferred-work)
 - [开发备注](#dev-note)
 
 -----
@@ -25,40 +25,51 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-当组合需要执行模型编写的 TypeScript 程序时,连同 code-runtime seam 一起挂载此后端;只要模型调用 `run_code`,`dsh-tools` 中的 PTC mode 就会通过 `ctx.codeRuntime` 驱动它。每个执行上限都是已验证的配置,因此你可以从 `cordis.yml` 为部署调整运行时规模。
+在提供 `fs`、`subprocess`、`sandbox` 与 `sandboxPolicy` 的组合中挂载本提供方。`dsh-tools` 的 PTC 模式传入调用 Session 的目录和常设策略;直接运行时消费方在执行前解析这些选项。
 
-### 最小配置
+### 配置
+
+在所需服务可用后,配置提供方条目:
 
 ```yaml
-- name: '@deepseek-ai/dsh-code-runtime'
 - name: '@deepseek-ai/dsh-code-runtime-node'
   config:
-    computeMs: 60000            # busy-time budget (measured event-loop active time)
-    maxWallMs: 600000           # wall-clock ceiling; never pauses for anything
-    maxOutputBytes: 67108864    # combined serialized outer-output cap (64 MiB)
-    maxOldGenerationSizeMb: 512 # worker heap cap
+    timeoutMs: 120000
+    maxTimeoutMs: 600000
+    maxOutputBytes: 67108864
+    maxOldGenerationSizeMb: 512
+    maxMessageBytes: 134217728
+    maxPendingCalls: 128
+    graceMs: 3000
 ```
 
 | 字段 | 默认值 | 含义 |
 |---|---|---|
-| `computeMs` | `60,000` | 忙碌时间预算:worker 实测事件循环活跃时间超过该值时,运行以 `timeout` 失败 |
-| `maxWallMs` | `600,000` | 墙钟上限,为忙碌时间无法观测的等待兜底;最大 `2_147_483_647` |
-| `maxOutputBytes` | `67,108,864` | 序列化日志加完成值或失败消息的硬上限;至少 `4` |
-| `maxOldGenerationSizeMb` | `512` | worker 堆上限;溢出会杀死 worker,并以 `worker-exit` 呈现 |
+| `timeoutMs` | `120,000` | 默认经过时间截止,包括嵌套工具与审批等待 |
+| `maxTimeoutMs` | `600,000` | 解析器应用的经过时间截止上限 |
+| `maxOutputBytes` | `67,108,864` | 序列化日志与完成值或诊断的合计预算 |
+| `maxOldGenerationSizeMb` | `512` | V8 老生代堆上限,单位 MiB |
+| `maxMessageBytes` | `134,217,728` | 控制帧、未完成参数字节和排队控制写入的上限 |
+| `maxPendingCalls` | `128` | 同时进行的 Host 绑定调用数量上限 |
+| `graceMs` | `3,000` | 受管终止与输出排空宽限时间 |
+| `nodeExecutable` | 当前 Node 可执行文件 | 在子进程执行世界中解析的可执行文件 |
+| `bootstrapPath` | 包内 bootstrap | 该执行世界中预先安装的构建后 bootstrap 的可选绝对路径 |
+
+[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-code-runtime-node)定义可接受的配置字段。`resolve(request)` 补全 cwd、封顶后的 timeout 与执行策略;`run(spec)` 接受这些已解析输入,不补缺省值。
 
-每个字段在加载时都会验证并提供默认值;没有其他可调项。生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-code-runtime-node)是所有受支持字段的完整参考。
+### 执行与结果
 
-### 运行返回什么
+程序是异步函数体:支持顶层 `await` 与 `return`,且只接受可擦除 TypeScript。成功调用以 `result.value` 返回无损 JSON 值,以 `result.logs` 返回捕获文本。`result.sandbox` 独立于程序结果报告所选模式、观察到的拒绝,以及后端完整或部分的强制能力。
 
-成功的运行把程序的无损 JSON 完成值作为 `result.value` 返回,把程序打印的文本按顺序作为 `result.logs` 返回。顶层 `await`/`return` 可用,程序可以把宿主提供的绑定函数(PTC mode 暴露一个 `tools` 对象)当作普通异步调用。
+直接文件系统、网络与子进程操作仍是 Node 操作,受所选 OS 沙箱约束。嵌套 Host 绑定通过控制通道调用;PTC 工具调用保留注册表的可见性、排序、日志和审批规则。运行程序不会改变 Session 的常设策略,也不会在拒绝后自动重放程序。
 
-### 隔离措施,而非安全边界
+### 截止时间与取消
 
-程序运行时的权限与 bash 工具相当:它可以访问 Node API,后端也刻意不承诺与宿主的隔离。它提供的是隔离措施:独立 isolate、空环境(没有环境变量凭据,也不继承 loader 标志)、可配置堆上限,以及也能终止同步热循环的强制终止。程序派生的 OS 进程在 `terminate()` 后仍然存活,需要部署层面的清理。
+经过时间截止覆盖运行时准备和执行,包括等待嵌套工具或审批的时间。它不是 CPU 计量器。超时或取消通过 Host 的受管进程所有者停止同步循环;成功完成也会清理该受管范围。选择结果后、清理前停止计时器,因此调用可能要在执行截止之后等待清理结算才返回。
 
-### 可能出什么问题
+### 失败
 
-每次程序运行都会通过 resolve 返回结果,因此运行失败会体现在 `result.error` 中,而不是触发 rejection:语法错误或不可擦除的 TypeScript(`enum`、namespace)在任何 worker 启动前就以 `exception` 失败;预算到期是 `timeout`;中止信号是 `abort`;堆溢出或其他 worker 终止是 `worker-exit`;不是无损 JSON 的完成值是 `invalid-output`;超出上限的序列化输出是 `output-limit`——并保留能容纳的已捕获日志前缀。只有调用方误用才会触发 rejection,例如在 dispose(资源释放)后提交运行。
+程序解析错误与抛出异常为 `exception`;截止到期为 `timeout`;取消为 `abort`;畸形或超量控制通信为 `protocol`;约束不可用为 `sandbox-unavailable`;进程提前退出或受管清理失败为 `worker-exit`。进程提供方保留与执行基底无关的失败名 `worker-exit`。有损完成值为 `invalid-output`,外层结果超限为 `output-limit`,并保留能容纳的日志前缀。无效或不支持的选项,以及资源释放后的调用,以调用方误用拒绝。
 
 -----
 
@@ -66,45 +77,31 @@ kind: "package-reference"
 ## 理解实现
 
 <details>
-<summary>实现细节——点击展开</summary>
-
-本节解释后端背后的设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
-
-### 设计理念
-
-后端基于一项明确区分:**这是隔离措施,而非安全边界**。模型代码按与 bash 等价的信任等级处理([PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md) 的 Trust posture),因此设计追求可重建性与有界资源使用,而非硬性的多租户边界——那需要等待容器级后端。每次运行使用一个全新的 worker,程序的世界随 worker 一同终止:不存在可泄漏、也无需记录的跨运行状态,仅凭会话日志即可重建一次运行。
+<summary>实现内部——点击展开</summary>
 
-### 执行流程
+Host 负责策略、截止时间、绑定查找和进程清理。子进程负责程序求值与绑定代理;即使使用预期的控制描述符,模型编写的代码仍是不可信对端。
 
-一次运行在宿主侧剥离类型(`node:module` 的 `stripTypeScriptTypes`,保持字节位置不变),包裹为异步函数的函数体使顶层 `await`/`return` 可用,然后发送给全新的 worker,由 bootstrap 物化绑定命名空间。绑定调用以无损 JSON 跨消息端口传递,每个调用 id 至多应答一次。日志文本主动流向宿主,因此被终止的程序仍会显示已打印的内容。恰好一个结果结算运行——`done` 帧、预算到期、中止或 worker 终止——之后宿主终止 worker 并等待其退出。
+### 启动与控制
 
-### 把对端视为不可信
+Host 擦除可擦除类型,在配置的执行世界中解析可执行文件与 bootstrap,通过 `ctx.sandbox` 包装 argv,并通过 `ctx.subprocess` 启动。子进程接管继承的控制通道,在求值程序前清空环境。显式 Node 参数避免继承 Host 的加载器或调试器标志。
 
-模型代码能够访问 `parentPort` 并伪造通信,因此任何代码读取入站消息前,系统都会逐字段验证并重建:伪造的额外字段绝不随行,非数字的 call id 绝不会被回显进 reply,绑定名称只解析为自有属性(伪造的 `constructor` 无法沿原型链访问),垃圾被静默丢弃。worker 侧命名空间使用 null-prototype,因此形似 `__proto__` 的绑定名称只是普通键。
+带长度分帧的 JSON 与 stdout/stderr 分开传输。Host 限制帧与排队写入,在分派前验证调用身份和已声明的绑定名,并拒绝无效通信。输出捕获计量序列化日志加完成值或诊断;固定结果信封字段与沙箱元数据不计入该账本。
 
-### 预算
+### 源代码与构建后 bootstrap
 
-存在两个独立预算,因为对端不可信:`computeMs` 计量 worker 的实测忙碌时间(每 25 ms 轮询一次 `eventLoopUtilization()`),因此热循环无论是否有诱饵 dispatch 在途都会到期,而等待慢绑定的程序不累计;`maxWallMs` 为忙碌时间无法观测的情况兜底,例如永远不会 resolve 的 promise。二者最终都会调用 `worker.terminate()`。`maxWallMs` 在加载时对照 `MAX_TIMER_DELAY_MS` 做范围校验,因为 `setTimeout` 会把更长的延迟限制为 1 ms。
+源代码执行加载仅含可擦除语法的 bootstrap 依赖,不依赖同级包的构建后导出。构建后执行使用包内 `process.js` 入口。无法映射 Host bootstrap 的执行世界需要预先安装兼容的 `bootstrapPath`;不会假设 Host 路径对应同一个远程文件。
 
-### 输出账本
-
-`maxOutputBytes` 统计外层 `logs` 数组加完成值或失败消息载荷的 JSON 序列化;固定的 `CodeRunResult` 字段名与外层封装语法不计入这份账本。未超过上限时返回精确值;有损完成值属于 `invalid-output`,组合溢出属于 `output-limit`,不会用 inspected string 代替。失败会保留日志中能容纳的已捕获前缀。
-
-### 源码地图
+### 源码索引
 
 | 文件 | 职责 |
 |---|---|
-| [`src/index.ts`](src/index.ts) | 插件入口:`Config` schema、`NodeCodeRuntime`、运行编排、输出账本 |
-| [`src/worker.ts`](src/worker.ts) | 源码模式 worker 入口(可擦除 TypeScript,不依赖 `lib/`) |
-| [`src/bootstrap.ts`](src/bootstrap.ts) | worker 侧 bootstrap:命名空间物化、console shim、日志捕获 |
-| [`src/protocol.ts`](src/protocol.ts) | host 与 worker 之间的端口消息词汇 |
-| [`src/worker-json.ts`](src/worker-json.ts) | worker 侧无损 JSON 编解码 |
-| [`src/output-json.ts`](src/output-json.ts) | 外层账本的字节计量与截断 |
-| — | 不发布运行时不变式伴生入口;这个进程边界实现不暴露同进程事件关系,worker 协议测试与构建后 worker 测试负责覆盖。 |
-
-### 未构建与已构建的 worker 入口
-
-源代码模式通过 Node 原生类型剥离加载只包含可擦除语法的 `src/worker.ts`;其传递运行时闭包只包含 Node 内置模块和相对源模块,因此全新 checkout 绝不需要兄弟工作区包尚未构建的 `lib/` 导出。构建模式会把兄弟文件 `lib/worker.cjs` 作为文件系统路径传入,因为 pkg 的虚拟文件系统(VFS)Worker hook 要求 CommonJS;同一路径也可在普通 Node 下使用。
+| [`src/index.ts`](src/index.ts) | 配置、解析、策略、绑定与受管执行 |
+| [`src/launch.ts`](src/launch.ts) | 可执行文件/bootstrap 参数与执行世界资源映射 |
+| [`src/process.ts`](src/process.ts) | 子进程握手、环境清空与程序生命周期 |
+| [`src/bootstrap.ts`](src/bootstrap.ts) | 程序求值、绑定代理与输出捕获 |
+| [`src/channel.ts`](src/channel.ts) | 分帧、有界写入与协议失败 |
+| [`src/output-ledger.ts`](src/output-ledger.ts) | Host 外层结果计量 |
+| — | 不发布运行时不变式配套模块;分帧与进程清理跨进程边界强制执行,不依靠同进程中的独立观测。 |
 
 </details>
 
@@ -113,44 +110,44 @@ kind: "package-reference"
 <a id="further-exploration"></a>
 ## 进一步探索
 
-当后端约定不够用时阅读以下内容。它们从 seam 定义进入消费方与配置面。
+直接使用提供方前先读服务约定;决策记录解释策略与消费方职责。
 
-- [代码运行时 seam](../code-runtime/README.zh.md)——此后端实现的抽象约定。
-- [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md)——`dsh-tools` 如何消费 `ctx.codeRuntime` 并呈现 `run_code`。
-- [代码运行时子系统参考](../../../docs/subsystems/code-runtime.zh.md)——请求/结果词汇、绑定与失败分类体系。
-- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-code-runtime-node)——每个受支持配置字段及其源声明。
+- [代码运行时服务](../code-runtime/README.zh.md)——请求、已解析 spec 与结果。
+- [沙箱 Node 决策](../../../.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.zh.md)——安全、生命周期与 timeout 取舍。
+- [PTC 基础](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md)——注册表呈现与嵌套工具分派。
+- [子进程提供方](../../subprocess/subprocess-local/README.zh.md)——受管进程范围与平台限制。
 
 -----
 
 <a id="model-experience"></a>
 ## 模型体验
 
-通过 `dsh-tools` 中的 PTC mode 间接提供,如果外层值能容纳则原样渲染,否则返回明确的 `invalid-output`/`output-limit` 失败,且只有外层 `run_code` 结果在其普通 spill 策略下进入模型上下文,绑定通信与中间值始终只存在于执行环境中。
+通过 `dsh-tools` 的 PTC 模式间接提供,返回捕获日志与完成值,或带沙箱事实的失败。中间绑定通信不进入模型历史;外层结果遵循普通工具溢出策略。
 
-#### KV Cache 影响
+#### KV Cache effect
 
-不会直接失效;由上述消费方负责请求前缀变更。
+不直接失效;具名消费方负责请求前缀的任何变更。
 
-## 已知限制与延期工作
+## 已知限制与延后工作
 
 <a id="known-limitations-and-deferred-work"></a>
 
+这些限制界定执行保证与保留的输出。
 
-这些限制说明此后端何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
-
-- **程序派生的 OS 进程在程序终止后仍会存活**——`worker.terminate()` 只结束线程,比 bash-local 的进程组终止更弱;在容器后端出现前,孤儿进程清理属于部署职责。
-- **类型剥离依赖 Node 的实验性 `stripTypeScriptTypes` API**——如依赖的行为发生变化,amaro 或 sucrase 是已经点名的直接替代品。
-- **`computeMs` 到期最多可能超过一个轮询间隔**——系统每 25 ms 采样一次忙碌时间(内部常量,有意不做成配置)。
-- **程序获得一个含 5 个方法的 `console` shim**(`log`/`info`/`warn`/`error`/`debug`)——有意不提供 Node 的完整 console 接口。
-- **中间绑定值没有字节上限**——程序可以用永远不会成为外层输出的值耗尽进程或 worker 内存。
-- **默认 64 MiB 上限是拒绝边界,不是可恢复存储**——外层 spill 机制只能保存发生 `output-limit` 后返回的有界日志和诊断;在运行时上限之外被拒绝的字节永远不会到达 spill 层。
+- **约束继承所选后端的限制**——完整与部分强制能力分开报告;沙箱策略与受管进程约束是不同保证。
+- **堆上限不是进程树内存限制**——原生分配与后代进程内存不属于 V8 老生代上限。不提供进程树 CPU 计量器。
+- **清理继承子进程的可观测范围**——使用 fallback 的平台上,逃逸的后代可能仍在受管范围之外;参见子进程提供方声明的限制。
+- **执行是一次性的**——没有 yield/wait API、实时结果流或跨调用保留的程序状态。
+- **输出上限拒绝超量内容,而不保留每个字节**——溢出只能保存本提供方交付的有界结果。
+- **绑定在传输接纳时受限**——控制限制不约束 Host 绑定生成结果期间分配的内存。
+- **console shim 有五个方法**——`log`、`info`、`warn`、`error` 与 `debug`。
 
 <a id="dev-note"></a>
 ### 开发备注
 
 <details>
-<summary>维护者的工作上下文——点击展开</summary>
+<summary>维护者工作上下文——点击展开</summary>
 
-无。
+[timeout 讨论](../../../.agents/notes/implemented/architecture/2026-09-11-sandboxed-node-code-runtime.zh.md#deferred-timeout-design)记录 yield、总生命周期、审批等待计时和进程树 CPU/RSS 上限的开放选择。这些选择不改变已配置的经过时间截止。
 
 </details>

+ 1 - 0
packages/code-runtime/code-runtime-node/package.json

@@ -20,6 +20,7 @@
     },
     "./package.json": "./package.json",
     "./process": {
+      "types": "./lib/types/process-entry.d.ts",
       "default": "./lib/process.js"
     }
   },

+ 32 - 9
packages/code-runtime/code-runtime-node/src/index.ts

@@ -18,6 +18,7 @@ import { JsonChannel } from './channel.ts'
 import { bootstrapArgs } from './launch.ts'
 import type { LaunchConfig } from './launch.ts'
 import { OutputLedger } from './output-ledger.ts'
+import { drainOutput } from './output-stream.ts'
 import { decodeCodeJsonWire, encodeCodeJsonWire } from './json-wire.ts'
 import type { ProgramBootData } from './protocol.ts'
 
@@ -98,6 +99,7 @@ export class NodeCodeRuntime extends CodeRuntime {
    * @returns Complete execution inputs with a capped deadline.
    */
   resolve(request: CodeRunRequest): CodeRunSpec {
+    if (this.disposed) throw new Error('code-runtime-node: resolve after disposal')
     const sandboxPolicy = request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
     const cwd = request.cwd ?? sandboxPolicy.workspaceRoot
     if (!isAbsolute(cwd)) throw new Error('code-runtime-node: cwd must be absolute')
@@ -148,6 +150,7 @@ export class NodeCodeRuntime extends CodeRuntime {
     let settled = false
     let timedOut = false
     let outputOverflow = false
+    let overflowResult: CodeRunResult | undefined
     let stderr = ''
     let parsing = true
     const wallTimer = setTimeout(() => { timedOut = true; controller.abort('execution deadline reached') }, spec.timeoutMs)
@@ -159,9 +162,16 @@ export class NodeCodeRuntime extends CodeRuntime {
       channel?.close()
       void (async () => {
         if (handle !== undefined) {
-          handle.terminate()
           try {
+            handle.terminate()
             await Promise.all([handle.done.catch(() => {}), handle.waitForExit()])
+            const drained = await Promise.all([
+              drainOutput(handle.stdout, this.config.graceMs),
+              drainOutput(handle.stderr, this.config.graceMs),
+            ])
+            if (drained.includes(false) && failure === undefined) {
+              failure = { kind: 'worker-exit', message: 'Node process output did not close cleanly' }
+            }
           } catch (error: unknown) {
             failure = { kind: 'worker-exit', message: `managed process cleanup failed: ${messageOf(error)}` }
           } finally {
@@ -169,7 +179,7 @@ export class NodeCodeRuntime extends CodeRuntime {
             handle.stderr?.destroy()
           }
         }
-        const outcome = outputOverflow ? output.limit(logs)
+        const outcome = outputOverflow ? overflowResult ?? output.limit(logs)
           : failure === undefined ? output.success(logs, value) : output.failure(logs, failure)
         result.resolve({ ...outcome, sandbox: { ...sandbox } })
       })()
@@ -203,24 +213,38 @@ export class NodeCodeRuntime extends CodeRuntime {
       if ('pkg' in process && this.config.bootstrapPath === undefined) env.DSH_CODE_RUNTIME_NODE = '1'
       handle = this.ctx.subprocess.spawn({ argv: confined?.argv ?? argv, cwd: spec.cwd, env, stdio: { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', control: 'pipe' }, graceMs: this.config.graceMs, signal })
       const launched = handle
-      if (launched.control === undefined) throw new Error('subprocess provider did not supply the requested control channel')
+      if (launched.control === undefined || launched.stdout === undefined || launched.stderr === undefined) {
+        throw new Error('subprocess provider did not supply the requested control and output pipes')
+      }
       const admit = (text: string): void => {
-        if (outputOverflow || text.length === 0) return
+        if (outputOverflow) return
         if (!output.admit(text, logs)) {
           outputOverflow = true
+          overflowResult = output.limit([...logs, text])
           finish({ kind: 'output-limit', message: `outer output exceeded ${this.config.maxOutputBytes} bytes` })
         }
       }
       const stdoutDecoder = new StringDecoder('utf8')
       const stderrDecoder = new StringDecoder('utf8')
-      handle.stdout?.on('data', (chunk: Buffer) => { admit(stdoutDecoder.write(chunk)) })
-      handle.stdout?.on('end', () => { admit(stdoutDecoder.end()) })
+      handle.stdout?.on('data', (chunk: Buffer) => {
+        const text = stdoutDecoder.write(chunk)
+        if (text.length > 0) admit(text)
+      })
+      handle.stdout?.on('end', () => {
+        const text = stdoutDecoder.end()
+        if (text.length > 0) admit(text)
+      })
+      handle.stdout?.on('error', (error: Error) => { finish({ kind: 'worker-exit', message: messageOf(error) }) })
       handle.stderr?.on('data', (chunk: Buffer) => {
         const text = stderrDecoder.write(chunk)
         stderr = (stderr + text).slice(-this.config.maxOutputBytes)
-        admit(text)
+        if (text.length > 0) admit(text)
+      })
+      handle.stderr?.on('end', () => {
+        const text = stderrDecoder.end()
+        if (text.length > 0) admit(text)
       })
-      handle.stderr?.on('end', () => { admit(stderrDecoder.end()) })
+      handle.stderr?.on('error', (error: Error) => { finish({ kind: 'worker-exit', message: messageOf(error) }) })
       let ready = false
       let nextId = 1
       let pending = 0
@@ -233,7 +257,6 @@ export class NodeCodeRuntime extends CodeRuntime {
           : { kind: 'worker-exit', message: `Node process exited before completing (${String(outcome.exitCode)})${stderr ? `: ${stderr}` : ''}` })
       }
       const transport: JsonChannel = new JsonChannel(launched.control, this.config.maxMessageBytes, (raw, bytes) => {
-        if (settled) return
         if (!record(raw)) { protocolFailure('invalid control frame'); return }
         if (!ready) {
           if (raw.type !== 'ready') { protocolFailure('program frame arrived before bootstrap readiness'); return }

+ 29 - 0
packages/code-runtime/code-runtime-node/src/output-stream.ts

@@ -0,0 +1,29 @@
+/** Bounded drainage for raw process output after managed execution ends. */
+import type { Readable } from 'node:stream'
+
+/**
+ * Wait for queued bytes without letting an inherited descriptor retain a run forever.
+ * @param stream - Caller-owned raw process output, when provided.
+ * @param graceMs - Maximum wait after managed process termination.
+ * @returns Whether the complete stream ended without a transport error.
+ */
+export function drainOutput(stream: Readable | undefined, graceMs: number): Promise<boolean> {
+  if (stream === undefined || stream.readableEnded) return Promise.resolve(true)
+  if (stream.destroyed) return Promise.resolve(false)
+  return new Promise((resolve) => {
+    const finish = (complete: boolean): void => {
+      clearTimeout(timer)
+      stream.off('end', onEnd)
+      stream.off('close', onClose)
+      stream.off('error', onError)
+      resolve(complete)
+    }
+    const onEnd = (): void => { finish(true) }
+    const onClose = (): void => { finish(false) }
+    const onError = (): void => { finish(false) }
+    const timer = setTimeout(() => { finish(false) }, graceMs)
+    stream.once('end', onEnd)
+    stream.once('close', onClose)
+    stream.once('error', onError)
+  })
+}

+ 354 - 0
packages/code-runtime/code-runtime-node/tests/host-failures.spec.ts

@@ -0,0 +1,354 @@
+import { Duplex, PassThrough } from 'node:stream'
+import { setImmediate } from 'node:timers/promises'
+import { Context } from '@deepseek-ai/cordis'
+import { describe, expect, it, onTestFinished, vi } from 'vitest'
+import type { CodeBindingFunction, CodeRunRequest } from '@deepseek-ai/dsh-code-runtime'
+import type { SubprocessHandle, SubprocessOutcome } from '@deepseek-ai/dsh-subprocess'
+import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
+import type { ConfinedArgv } from '@deepseek-ai/dsh-sandbox'
+import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
+import type { Config } from '../src/index.ts'
+import { JsonChannel } from '../src/channel.ts'
+import { encodeCodeJsonWire } from '../src/json-wire.ts'
+import { mountRuntime } from './setup.ts'
+
+const request: CodeRunRequest = { program: 'return 1', bindings: [] }
+const NO_INITIAL_FRAME = Symbol('no initial frame')
+
+async function setup(config: Config = {}, mode: 'read-only' | 'danger-full-access' = 'danger-full-access') {
+  const ctx = new Context()
+  const runtime = await mountRuntime(ctx, config, { mode, workspaceRoot: process.cwd() })
+  const control = new Duplex({
+    read() {},
+    write(chunk: Buffer, _encoding, callback) {
+      if (childControl.destroyed) { callback(new Error('peer closed')); return }
+      childControl.push(chunk)
+      callback()
+    },
+  })
+  const childControl = new Duplex({
+    read() {},
+    write(chunk: Buffer, _encoding, callback) {
+      if (control.destroyed) { callback(new Error('peer closed')); return }
+      control.push(chunk)
+      callback()
+    },
+  })
+  const stdout = new PassThrough()
+  const stderr = new PassThrough()
+  const direct = Promise.withResolvers<SubprocessOutcome>()
+  const messages: unknown[] = []
+  const handle: SubprocessHandle = {
+    stdin: undefined,
+    stdout,
+    stderr,
+    control,
+    collected: {},
+    done: direct.promise,
+    terminate: vi.fn(() => {
+      stdout.end()
+      stderr.end()
+      direct.resolve({ exitCode: 0, signal: null })
+    }),
+    waitForExit: vi.fn(async () => true),
+  }
+  const writes = new Set<Promise<void>>()
+  let receive: (message: unknown) => void = () => {}
+  const peer = new JsonChannel(childControl, 1024 * 1024, (message) => {
+    messages.push(message)
+    receive(message)
+  }, () => {})
+  const emit = (message: unknown): void => {
+    const write = peer.send(message).catch(() => {}).finally(() => { writes.delete(write) })
+    writes.add(write)
+  }
+  const resolveExecutable = vi.spyOn(ctx.subprocess, 'resolveExecutable').mockResolvedValue(process.execPath)
+  const spawn = vi.spyOn(ctx.subprocess, 'spawn').mockReturnValue(handle)
+  onTestFinished(async () => {
+    peer.close()
+    control.destroy()
+    stdout.destroy()
+    stderr.destroy()
+    direct.resolve({ exitCode: 0, signal: null })
+    await Promise.all(writes)
+  })
+  const start = (input: CodeRunRequest = request, first: unknown = { type: 'ready' }) => {
+    const result = runtime.run(runtime.resolve(input))
+    if (first !== NO_INITIAL_FRAME) queueMicrotask(() => { emit(first) })
+    return result
+  }
+  const onBoot = (callback: () => void): void => {
+    receive = (message) => {
+      if (typeof message === 'object' && message !== null && 'type' in message && message.type === 'boot') callback()
+    }
+  }
+  return {
+    ctx, runtime, handle, direct, stdout, stderr, control, peer, messages, spawn, resolveExecutable, emit, start, onBoot,
+    receive: (callback: typeof receive) => { receive = callback },
+  }
+}
+
+function call(id: number, value: string = '') {
+  return { type: 'call', id, global: 'tools', name: 'test', args: encodeCodeJsonWire(value) }
+}
+
+function withBinding(fn: CodeBindingFunction): CodeRunRequest {
+  return { ...request, bindings: [{ global: 'tools', functions: { test: fn } }] }
+}
+
+function confinement(argv: string[]): ConfinedArgv {
+  return { argv, enforcement: 'partial', denialSignatures: ['EACCES'], runnerFailureRules: [{ fatalSignatures: ['sandbox-fatal:'] }] }
+}
+
+describe('Node runtime host failures', () => {
+  it.each<[Config, string]>([
+    [{ timeoutMs: 0 }, 'timeoutMs'],
+    [{ maxPendingCalls: -1 }, 'maxPendingCalls'],
+    [{ maxOldGenerationSizeMb: Infinity }, 'maxOldGenerationSizeMb'],
+    [{ timeoutMs: MAX_TIMER_DELAY_MS + 1 }, 'timeoutMs'],
+    [{ maxTimeoutMs: MAX_TIMER_DELAY_MS + 1 }, 'maxTimeoutMs'],
+    [{ graceMs: MAX_TIMER_DELAY_MS + 1 }, 'graceMs'],
+    [{ maxOutputBytes: 3 }, 'maxOutputBytes'],
+    [{ maxOutputBytes: 4.5 }, 'maxOutputBytes'],
+    [{ maxMessageBytes: 1.5 }, 'maxMessageBytes'],
+    [{ maxMessageBytes: 0x1_0000_0000 }, 'maxMessageBytes'],
+    [{ maxPendingCalls: 1.5 }, 'maxPendingCalls'],
+    [{ maxOldGenerationSizeMb: 1.5 }, 'maxOldGenerationSizeMb'],
+    [{ nodeExecutable: '' }, 'nodeExecutable'],
+    [{ bootstrapPath: 'relative-bootstrap.js' }, 'bootstrapPath'],
+  ])('rejects deployment configuration %j', async (config, field) => {
+    await expect(mountRuntime(new Context(), config)).rejects.toThrow(field)
+  })
+
+  it('reports the deployment mode and rejects unsupported resolved inputs before spawning', async () => {
+    const h = await setup({}, 'read-only')
+    expect(h.runtime.sandboxMode).toBe('read-only')
+    expect(() => h.runtime.resolve({ ...request, cwd: 'relative' })).toThrow('cwd must be absolute')
+    await expect(h.runtime.run({ ...request, cwd: process.cwd(), timeoutMs: 1 })).rejects.toThrow('resolved sandbox policy')
+    const spec = h.runtime.resolve(request)
+    await expect(h.runtime.run({ ...spec, timeoutMs: Infinity })).rejects.toThrow('resolved cwd and timeout')
+    expect(h.spawn).not.toHaveBeenCalled()
+    await h.ctx.fiber.dispose()
+    expect(() => h.runtime.resolve(request)).toThrow('resolve after disposal')
+    await expect(h.runtime.run(spec)).rejects.toThrow('run after disposal')
+  })
+
+  it('fails a required unavailable sandbox before spawning', async () => {
+    const h = await setup({}, 'read-only')
+    vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(() => { throw new SandboxUnavailableError('read-only') })
+    expect((await h.start()).error?.kind).toBe('sandbox-unavailable')
+    expect(h.spawn).not.toHaveBeenCalled()
+  })
+
+  it('keeps partial enforcement separate from a successful program', async () => {
+    const h = await setup({}, 'read-only')
+    vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
+    h.onBoot(() => { h.emit({ type: 'done', value: encodeCodeJsonWire(42) }) })
+    expect(await h.start()).toEqual({ logs: [], value: 42, sandbox: { mode: 'read-only', denied: false, enforcement: 'partial' } })
+  })
+
+  it.each([['EACCES: blocked', true], ['EPERM: unrelated dialect', false]] as const)('uses only the selected denial dialect for %s', async (message, denied) => {
+    const h = await setup({}, 'read-only')
+    vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
+    h.onBoot(() => { h.emit({ type: 'done', error: { kind: 'exception', message } }) })
+    const result = await h.start()
+    expect(result.error).toEqual({ kind: 'exception', message })
+    expect(result.sandbox).toEqual({ mode: 'read-only', denied, enforcement: 'partial' })
+  })
+
+  it('distinguishes fatal sandbox startup output from a program denial', async () => {
+    const h = await setup({}, 'read-only')
+    vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
+    h.onBoot(() => {
+      h.stderr.write('sandbox-fatal: runner could not initialize')
+      h.direct.resolve({ exitCode: 1, signal: null })
+    })
+    const result = await h.start()
+    expect(result.error?.kind).toBe('sandbox-unavailable')
+    expect(result.sandbox?.denied).toBe(false)
+  })
+
+  it('reports bootstrap assets that cannot map into the execution world', async () => {
+    const h = await setup()
+    vi.spyOn(h.ctx.fs, 'processPathFromHostPath').mockReturnValue(undefined)
+    expect((await h.start()).error?.message).toContain('bootstrap is unavailable')
+    expect(h.spawn).not.toHaveBeenCalled()
+  })
+
+  it('cleans up a provider that fails to supply its requested control pipe', async () => {
+    const h = await setup()
+    h.spawn.mockReturnValue({ ...h.handle, control: undefined })
+    expect((await h.start()).error?.message).toContain('did not supply the requested control')
+    expect(h.handle.terminate).toHaveBeenCalledOnce()
+    expect(h.handle.waitForExit).toHaveBeenCalledOnce()
+  })
+
+  it('reports an executable lookup failure without allocating a process', async () => {
+    const h = await setup()
+    h.resolveExecutable.mockRejectedValue(new Error('Node executable missing'))
+    expect((await h.start()).error).toEqual({ kind: 'worker-exit', message: 'Node executable missing' })
+    expect(h.spawn).not.toHaveBeenCalled()
+  })
+
+  it('honors an already-aborted run without beginning executable lookup', async () => {
+    const h = await setup()
+    const result = await h.start({ ...request, signal: AbortSignal.abort('already stopped') }, NO_INITIAL_FRAME)
+    expect(result.error).toEqual({ kind: 'abort', message: 'already stopped' })
+    expect(h.resolveExecutable).not.toHaveBeenCalled()
+  })
+
+  it('does not launch after cancellation races executable lookup completion', async () => {
+    const h = await setup()
+    const controller = new AbortController()
+    h.resolveExecutable.mockImplementation(async () => {
+      controller.abort('lookup canceled')
+      return process.execPath
+    })
+    expect((await h.start({ ...request, signal: controller.signal }, NO_INITIAL_FRAME)).error?.kind).toBe('abort')
+    expect(h.spawn).not.toHaveBeenCalled()
+  })
+
+  it('reports an early control EOF using the direct process result', async () => {
+    const h = await setup()
+    h.spawn.mockImplementation(() => {
+      queueMicrotask(() => {
+        h.control.push(null)
+        h.direct.resolve({ exitCode: 7, signal: null })
+      })
+      return h.handle
+    })
+    expect((await h.start(request, NO_INITIAL_FRAME)).error).toEqual({
+      kind: 'worker-exit', message: 'Node process exited before completing (7)',
+    })
+  })
+
+  it('reports startup transport loss when the direct process outcome rejects', async () => {
+    const h = await setup()
+    h.spawn.mockImplementation(() => {
+      queueMicrotask(() => {
+        h.control.emit('error', new Error('control transport closed'))
+        h.direct.reject(new Error('runner connection broken'))
+      })
+      return h.handle
+    })
+    expect((await h.start(request, NO_INITIAL_FRAME)).error).toEqual({ kind: 'worker-exit', message: 'runner connection broken' })
+  })
+
+  it('reports control loss after readiness as a substrate failure', async () => {
+    const h = await setup()
+    h.onBoot(() => { h.control.emit('error', new Error('control transport closed')) })
+    expect((await h.start()).error).toEqual({ kind: 'worker-exit', message: 'control transport closed' })
+  })
+
+  it('classifies invalid UTF-8 on the live control stream as a protocol failure', async () => {
+    const h = await setup()
+    h.onBoot(() => { h.control.push(Buffer.from([0, 0, 0, 1, 0xff])) })
+    expect((await h.start()).error?.kind).toBe('protocol')
+  })
+
+  it('rejects traffic before readiness without invoking host bindings', async () => {
+    const h = await setup()
+    const binding = vi.fn(async () => null)
+    const result = await h.start(withBinding(binding), call(1))
+    expect(result.error?.message).toContain('before bootstrap readiness')
+    expect(binding).not.toHaveBeenCalled()
+  })
+
+  it.each<[unknown, string]>([
+    [null, 'invalid control frame'],
+    [{ type: 'log', text: 1 }, 'invalid log frame'],
+    [{ type: 'done', error: null }, 'invalid terminal error'],
+    [{ type: 'done', error: { kind: 'exception', message: 1 } }, 'invalid terminal error'],
+    [{ type: 'done', error: { kind: 'timeout', message: 'forged' } }, 'invalid terminal error'],
+    [{ type: 'call', id: 0, global: 'tools', name: 'test' }, 'invalid binding call identity'],
+    [{ ...call(1), name: 'constructor' }, 'undeclared binding'],
+    [{ type: 'call', id: 1, global: 'tools', name: 'test' }, 'arguments must be lossless JSON'],
+    [{ type: 'unknown' }, 'unknown control message'],
+  ])('rejects malformed program frame %j', async (frame, message) => {
+    const h = await setup()
+    const binding = vi.fn(async () => null)
+    h.onBoot(() => { h.emit(frame) })
+    const result = await h.start(withBinding(binding))
+    expect(result.error).toEqual({ kind: 'protocol', message: expect.stringContaining(message) })
+    expect(binding).not.toHaveBeenCalled()
+  })
+
+  it('refuses a repeated call id instead of dispatching it twice', async () => {
+    const h = await setup()
+    const binding = vi.fn(async () => null)
+    h.onBoot(() => { h.emit(call(1)); h.emit(call(1)) })
+    expect((await h.start(withBinding(binding))).error?.kind).toBe('protocol')
+    expect(binding).toHaveBeenCalledOnce()
+  })
+
+  it.each<Config>([{ maxPendingCalls: 1 }, { maxMessageBytes: 512 }])('bounds unresolved host calls with %j', async (config) => {
+    const h = await setup(config)
+    const release = Promise.withResolvers<null>()
+    onTestFinished(() => { release.resolve(null) })
+    const binding = vi.fn(async () => await release.promise)
+    h.onBoot(() => { h.emit(call(1, 'a'.repeat(300))); h.emit(call(2, 'b'.repeat(300))) })
+    expect((await h.start(withBinding(binding))).error?.message).toContain('pending binding calls exceed')
+    expect(binding).toHaveBeenCalledOnce()
+    release.resolve(null)
+    await setImmediate()
+  })
+
+  it('rejects an untransferable completion instead of returning a substituted value', async () => {
+    const h = await setup()
+    h.onBoot(() => { h.emit({ type: 'done', value: { invalid: true } }) })
+    expect((await h.start()).error?.kind).toBe('invalid-output')
+  })
+
+  it('returns non-lossless binding resolutions as program-visible binding failures', async () => {
+    const h = await setup()
+    h.receive((message) => {
+      if (typeof message !== 'object' || message === null || !('type' in message)) return
+      if (message.type === 'boot') h.emit(call(1))
+      if (message.type === 'reply') h.emit({ type: 'done' })
+    })
+    expect((await h.start(withBinding(async () => Number.NaN))).error).toBeUndefined()
+    expect(h.messages).toContainEqual({ type: 'reply', id: 1, ok: false, message: expect.stringContaining('lossless JSON') })
+  })
+
+  it('does not publish a late binding reply after the program has settled', async () => {
+    const h = await setup()
+    const release = Promise.withResolvers<null>()
+    onTestFinished(() => { release.resolve(null) })
+    h.onBoot(() => { h.emit(call(1)); h.emit({ type: 'done' }) })
+    expect((await h.start(withBinding(async () => await release.promise))).error).toBeUndefined()
+    release.resolve(null)
+    await setImmediate()
+    expect(h.messages).not.toContainEqual(expect.objectContaining({ type: 'reply' }))
+    expect(h.control.destroyed).toBe(true)
+  })
+
+  it('reports failed managed cleanup even when the program returns successfully', async () => {
+    const h = await setup()
+    vi.mocked(h.handle.waitForExit).mockRejectedValue(new Error('range cannot be observed'))
+    h.onBoot(() => { h.emit({ type: 'done', value: encodeCodeJsonWire(42) }) })
+    expect((await h.start()).error).toEqual({ kind: 'worker-exit', message: 'managed process cleanup failed: range cannot be observed' })
+  })
+
+  it('fails when the configured frame budget cannot carry bootstrap data', async () => {
+    const h = await setup({ maxMessageBytes: 32 })
+    expect((await h.start()).error?.message).toContain('control output exceeds 32 queued bytes')
+  })
+
+  it('turns an oversized binding reply into a protocol failure', async () => {
+    const h = await setup({ maxMessageBytes: 256 })
+    h.onBoot(() => { h.emit(call(1)) })
+    const result = await h.start(withBinding(async () => 'x'.repeat(512)))
+    expect(result.error).toEqual({ kind: 'protocol', message: 'control output exceeds 256 queued bytes' })
+  })
+
+  it('retains admitted logs when the program reports its own output limit', async () => {
+    const h = await setup()
+    h.onBoot(() => {
+      h.emit({ type: 'log', text: 'retained' })
+      h.emit({ type: 'done', error: { kind: 'output-limit', message: 'child ledger exhausted' } })
+    })
+    const result = await h.start()
+    expect(result.error?.kind).toBe('output-limit')
+    expect(result.logs).toEqual(['retained'])
+  })
+})

+ 25 - 0
packages/code-runtime/code-runtime-node/tests/launch.spec.ts

@@ -0,0 +1,25 @@
+import { expect, it } from 'vitest'
+import type { FileSystem } from '@deepseek-ai/dsh-fs'
+import { bootstrapArgs } from '../src/launch.ts'
+
+it('uses an explicit installed bootstrap without pretending it maps the host file', () => {
+  const fs = { processPathFromHostPath: () => { throw new Error('must not map') } } as unknown as FileSystem
+  expect(bootstrapArgs(fs, { bootstrapPath: '/remote/process.js' }, 2048)).toEqual(['/remote/process.js', '2048'])
+})
+
+it('fails when the execution world cannot read the source bootstrap', () => {
+  const fs = { processPathFromHostPath: () => undefined } as unknown as FileSystem
+  expect(() => bootstrapArgs(fs, {}, 2048)).toThrow('unavailable in the subprocess execution world')
+})
+
+it('selects the private packaged bootstrap through the existing executable', () => {
+  const prior = Object.getOwnPropertyDescriptor(process, 'pkg')
+  try {
+    Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    const fs = { processPathFromHostPath: () => { throw new Error('pkg does not expose a host bootstrap file') } } as unknown as FileSystem
+    expect(bootstrapArgs(fs, {}, 2048)).toEqual(['2048'])
+  } finally {
+    if (prior === undefined) Reflect.deleteProperty(process, 'pkg')
+    else Object.defineProperty(process, 'pkg', prior)
+  }
+})

+ 26 - 0
packages/code-runtime/code-runtime-node/tests/output-ledger.spec.ts

@@ -0,0 +1,26 @@
+import { expect, it } from 'vitest'
+import { OutputLedger } from '../src/output-ledger.ts'
+
+it('accounts for exact JSON escaping, separators and optional completion values', () => {
+  const ledger = new OutputLedger(20)
+  const logs: string[] = []
+  expect(ledger.admit('first', logs)).toBe(true)
+  expect(ledger.admit('second', logs)).toBe(true)
+  expect(ledger.admit('third', logs)).toBe(false)
+  expect(ledger.success(logs)).toEqual({ logs: ['first', 'second'] })
+  expect(ledger.success(logs, 1)).toEqual({ logs: ['first', 'second'], value: 1 })
+  expect(ledger.success(logs, 'too long').error?.kind).toBe('output-limit')
+})
+
+it('retains a bounded prefix when logs or a failure diagnostic exceed the limit', () => {
+  for (const maxBytes of [4, 8, 40, 80]) {
+    const ledger = new OutputLedger(maxBytes)
+    const result = ledger.limit(['a', 'b', '你好🙂'.repeat(50)])
+    expect(result.error?.kind).toBe('output-limit')
+    const bytes = Buffer.byteLength(JSON.stringify(result.logs)) + Buffer.byteLength(JSON.stringify(result.error?.message))
+    expect(bytes).toBeLessThanOrEqual(maxBytes)
+  }
+  const ledger = new OutputLedger(80)
+  expect(ledger.failure([], { kind: 'exception', message: 'short' })).toEqual({ logs: [], error: { kind: 'exception', message: 'short' } })
+  expect(ledger.failure([], { kind: 'exception', message: 'x'.repeat(100) }).error?.kind).toBe('output-limit')
+})

+ 35 - 0
packages/code-runtime/code-runtime-node/tests/output-stream.spec.ts

@@ -0,0 +1,35 @@
+import { PassThrough } from 'node:stream'
+import { afterEach, expect, it, vi } from 'vitest'
+import { drainOutput } from '../src/output-stream.ts'
+
+afterEach(() => vi.useRealTimers())
+
+it('waits for queued output and accepts an already ended or absent stream', async () => {
+  const stream = new PassThrough()
+  const chunks: string[] = []
+  stream.on('data', chunk => chunks.push(String(chunk)))
+  const pending = drainOutput(stream, 1000)
+  stream.end('last output')
+  expect(await pending).toBe(true)
+  expect(chunks).toEqual(['last output'])
+  expect(await drainOutput(stream, 1000)).toBe(true)
+  expect(await drainOutput(undefined, 1000)).toBe(true)
+})
+
+it('bounds an output descriptor retained after the process exits', async () => {
+  vi.useFakeTimers()
+  const stream = new PassThrough()
+  const pending = drainOutput(stream, 100)
+  await vi.advanceTimersByTimeAsync(100)
+  expect(await pending).toBe(false)
+  stream.destroy()
+  expect(await drainOutput(stream, 100)).toBe(false)
+})
+
+it.each(['close', 'error'])('reports incomplete output on %s', async (event) => {
+  const stream = new PassThrough()
+  const pending = drainOutput(stream, 1000)
+  stream.emit(event, new Error('pipe failed'))
+  expect(await pending).toBe(false)
+  stream.destroy()
+})

+ 4 - 2
packages/code-runtime/code-runtime-node/tests/process.spec.ts

@@ -3,7 +3,7 @@ import { copyFile, mkdtemp, rm } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { pathToFileURL } from 'node:url'
-import type { Duplex } from 'node:stream'
+import { Duplex } from 'node:stream'
 import { expect, it, onTestFinished } from 'vitest'
 import { JsonChannel } from '../src/channel.ts'
 import { decodeCodeJsonWire, encodeCodeJsonWire } from '../src/json-wire.ts'
@@ -25,7 +25,9 @@ it('boots an unbuilt source closure outside the workspace and exchanges tool rep
   const completed = Promise.withResolvers<unknown>()
   child.once('error', error => completed.reject(error))
   child.once('exit', (code) => { if (code !== 0) completed.reject(new Error(`child exit ${code}: ${stderr}`)) })
-  const channel = new JsonChannel(child.stdio[7] as Duplex, 100_000, (raw) => {
+  const control = Array.from(child.stdio)[7]
+  if (!(control instanceof Duplex)) { child.kill(); await finished; throw new Error('missing child control channel') }
+  const channel = new JsonChannel(control, 100_000, (raw) => {
     const message = raw as { type: string; id?: number; args?: unknown; value?: unknown; error?: unknown }
     if (message.type === 'ready') {
       void channel.send({ type: 'boot', data: {

+ 14 - 1
packages/code-runtime/code-runtime-node/tests/runtime.spec.ts

@@ -100,12 +100,14 @@ describe('Node program process', () => {
 
   it('disposes active programs and rejects later execution', async () => {
     const { ctx, run, runtime } = await setup()
+    const spec = runtime.resolve({ program: '', bindings: [] })
     const entered = Promise.withResolvers<undefined>()
     const active = run({ program: 'await tools.enter({}); await new Promise(() => {})', bindings: bindings({ enter: async () => { entered.resolve(undefined); return null } }) })
     await entered.promise
     await ctx.fiber.dispose()
     expect((await active).error?.kind).toBe('abort')
-    await expect(run({ program: '', bindings: [] })).rejects.toThrow('disposal')
+    await expect(runtime.run(spec)).rejects.toThrow('disposal')
+    expect(() => runtime.resolve({ program: '', bindings: [] })).toThrow('disposal')
     expect(runtime.isolation).toBe('process')
   })
 
@@ -140,3 +142,14 @@ describe('Node program process', () => {
     await expect(readFile(join(outside, 'denied.txt'))).rejects.toMatchObject({ code: 'ENOENT' })
   })
 })
+
+it('preserves empty console entries and bounds native output overflow', async () => {
+  const { run } = await setup({ maxOutputBytes: 100 })
+  const lines = await run({ program: 'console.log("a"); console.log(""); console.log("b")', bindings: [] })
+  expect(lines.logs).toEqual(['a', '', 'b'])
+  const overflow = await run({ program: 'const fs=await import("node:fs"); fs.writeSync(1,"HEAD-"+"x".repeat(100000));', bindings: [] })
+  expect(overflow.error?.kind).toBe('output-limit')
+  expect(overflow.logs.join('')).toContain('HEAD-')
+  const bytes = Buffer.byteLength(JSON.stringify(overflow.logs)) + Buffer.byteLength(JSON.stringify(overflow.error?.message))
+  expect(bytes).toBeLessThanOrEqual(100)
+})

+ 4 - 0
packages/code-runtime/code-runtime-node/tests/setup.ts

@@ -1,4 +1,6 @@
 import { Context } from '@deepseek-ai/cordis'
+import { onTestFinished } from 'vitest'
+import SessionStore from '@deepseek-ai/dsh-session'
 import FileSystem from '@deepseek-ai/dsh-fs-local'
 import Subprocess from '@deepseek-ai/dsh-subprocess-local'
 import Sandbox from '@deepseek-ai/dsh-sandbox-local'
@@ -9,6 +11,8 @@ import NodeRuntime from '../src/index.ts'
 import type { Config } from '../src/index.ts'
 
 export async function mountRuntime(ctx: Context, config: Config = {}, policy: { mode?: SandboxMode; workspaceRoot?: string } = {}) {
+  onTestFinished(async () => { await ctx.fiber.dispose() })
+  if (!ctx.get('sessions')) await ctx.plugin(SessionStore)
   if (!ctx.get('fs')) await ctx.plugin(FileSystem)
   if (!ctx.get('subprocess')) await ctx.plugin(Subprocess)
   if (!ctx.get('sandbox')) await ctx.plugin(Sandbox, {})

+ 2 - 2
packages/code-runtime/code-runtime/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime/README.md
-README.md: 70f29b0508a4466366735f1badad02ec2d85b463
-README.zh.md: 6edb2b700a5f7861a36e15b5783bb9296b8467f3
+README.md: 32f5b76fb88c4880cef16abbfa11da52ae13d5ab
+README.zh.md: 28718c23b8449592ccc0c8c03fa4a9b38963c673

+ 12 - 11
packages/code-runtime/code-runtime/README.md

@@ -29,19 +29,20 @@ Choose this package when you compose a deployment that executes model-written pr
 
 ### Run a program
 
-Give the runtime a program source and one or more binding namespaces. Each namespace becomes one global object of async functions inside the program — PTC mode passes one under `tools`. The program runs as the body of an async function, so top-level `await` and `return` work; a lossless-JSON completion becomes `result.value`, each output channel preserves its own order in `result.logs` while cross-channel interleaving is backend-dependent, and any failure is reported in `result.error` with a kind you can branch on. The runtime never rejects for a program failure — rejection means you misused the seam, for example by submitting a run after disposal.
+Give the runtime a program and binding namespaces, then call `resolve(request)` followed by `run(spec)`. Resolution validates optional cwd, timeout and sandbox policy against the provider's capabilities and fills deployment defaults. The program runs as an async function body, so top-level `await` and `return` work; a lossless-JSON completion becomes `result.value`, captured text becomes `result.logs`, and program failures become `result.error`. Each output channel preserves its own order, while cross-channel interleaving is backend-dependent.
 
 ```text
-const result = await ctx.codeRuntime.run({
+const spec = ctx.codeRuntime.resolve({
   program: 'return await tools.add({ a: 1, b: 2 })',
   bindings: [{ global: 'tools', functions: { add: async (args) => args.a + args.b } }],
 })
+const result = await ctx.codeRuntime.run(spec)
 // result.value === 3
 ```
 
 ### Choose a backend
 
-Backends declare two descriptors you can rely on: `language` — what the program must be written in, with `'typescript'` and `'python'` as the well-known values — and `isolation` — the execution substrate (`'worker-thread'`, `'process'`, `'container'`), a label for deployments and diagnostics, not a security claim. [`dsh-code-runtime-node`](../code-runtime-node/README.md) executes TypeScript in a fresh Node worker thread; the private [`dsh-experimental-code-runtime-python`](../../experimental/code-runtime-python/README.md) package executes Python in a fresh CPython subprocess for opt-in compositions.
+Backends expose `language` and `isolation` as diagnostic descriptors; neither grants authority or proves confinement. [`dsh-code-runtime-node`](../code-runtime-node/README.md) executes erasable TypeScript in a fresh managed Node process under the resolved sandbox policy. The private [`dsh-experimental-code-runtime-python`](../../experimental/code-runtime-python/README.md) provider executes Python in a fresh CPython subprocess without file confinement. `sandboxMode` advertises a provider's deployment file-policy mode, or is absent when that capability is unsupported.
 
 ### Name your bindings portably
 
@@ -49,7 +50,7 @@ Binding-global and error-class names are language-portable: they must match `[A-
 
 ### What can go wrong
 
-Failures arrive as `result.error` with an orthogonal `kind`: the program threw or failed to parse (`exception`), a budget expired (`timeout`), the run was aborted (`abort`), the execution substrate died (`worker-exit`), the completion value was not lossless JSON (`invalid-output`), or the serialized output exceeded the cap (`output-limit`). Each kind carries a model-feedable message. `run()` rejects only for seam misuse, such as a run submitted after disposal or a binding name that fails the portable-identifier rules.
+Failures arrive as `result.error` with an orthogonal `kind`: `exception`, `timeout`, `abort`, `worker-exit`, `invalid-output`, `output-limit`, `protocol` or `sandbox-unavailable`. Providers return applicable `result.sandbox` facts separately from success or failure. Invalid or unsupported execution options fail during `resolve`; `run` rejects caller misuse, such as unresolved inputs, invalid binding names or a call after disposal.
 
 -----
 
@@ -63,17 +64,17 @@ This section explains the design behind the seam; observable behavior is fully c
 
 ### Design concept
 
-The package is the Service Definition role of the code-execution capability seam ([capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract `CodeRuntime extends Service` registered as `ctx.codeRuntime`, plus the vocabulary both backends and the consumer share. Providers subclass `CodeRuntime`, implement `run`, and register the service; the consumer (PTC mode in `dsh-tools`) generates the model-facing SDK and bridges tool dispatch. The runtime stays ignorant of tools and sessions by contract: it receives a program and named async bindings and returns `{ value, logs, error? }`.
+The package is the Service Definition role of the code-execution capability seam ([capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract `CodeRuntime extends Service` registered as `ctx.codeRuntime`, plus the vocabulary both backends and the consumer share. Providers subclass `CodeRuntime`, implement `resolve` and `run`, and register the service; the consumer (PTC mode in `dsh-tools`) generates the model-facing SDK and bridges tool dispatch. The runtime stays ignorant of tools and sessions by contract: it receives a program, named async bindings and resolved execution options, then returns captured output, the outcome and applicable sandbox facts.
 
 ### Service API
 
-The contract is three members a backend implements: `run(request)` executes one program against the request's bindings and resolves every program outcome — parse/transform failure, thrown exception, invalid completion, output overflow, budget expiry, abort, or substrate death — as a result `error` field, with rejection reserved for caller misuse such as a run submitted after disposal; `language` and `isolation` are read-only descriptors labeling the source language and execution substrate for deployments and diagnostics.
+`resolve(request)` owns supported option validation and deployment defaulting. `run(spec)` executes the complete inputs and resolves program outcomes after cleanup. Language and substrate descriptors guide presentation; `sandboxMode` indicates whether the consumer can pass a resolved file policy. Neither descriptors nor a successful program result substitute for the backend's reported enforcement facts.
 
 The exhaustive semantics live in the [code runtime subsystem reference](../../../docs/subsystems/code-runtime.md); the exact signatures are in [`src/index.ts`](src/index.ts).
 
 ### Vocabulary
 
-`CodeRunRequest` (`program`, `bindings`, `signal?`) carries everything the runtime acts on; defaulting (time budgets, output caps) is each provider's validated config, never a hidden `??` inside `run()`. `bindings` is a list of `CodeBindingNamespace`s (`global` + `functions` + optional `errorClass`), each exposed to the program as one global object of async callables returning `CodeJsonValue` — the seam's structural lossless-JSON type. An `errorClass` descriptor names a real program-global constructor and the own property that receives the rejected member name, so backends never learn consumer terms such as `ToolCallError`. `CodeRunResult` reports the lossless-JSON completion `value?`, per-channel-ordered `logs: string[]` with backend-dependent cross-channel interleaving, and `error?` (`CodeRunFailure`: orthogonal `kind` + model-feedable `message`). See `src/types.ts` for the full contracts.
+`CodeRunRequest` carries the program, host bindings, cancellation and optional execution choices. `CodeRunSpec` requires the resolved cwd and elapsed deadline. `CodeBindingNamespace` declares program globals and optional typed rejection constructors. `CodeRunResult` separates logs/value, failure and `CodeRunSandbox` facts; exact fields and provider obligations live in [`src/types.ts`](src/types.ts).
 
 ### Portable identifiers
 
@@ -84,7 +85,7 @@ Binding-global and error-class names are language-portable: they must match the
 | File | Role |
 |---|---|
 | [`src/index.ts`](src/index.ts) | Plugin entry: abstract `CodeRuntime` service and the portable-identifier exclusion sets |
-| [`src/types.ts`](src/types.ts) | Vocabulary: `CodeRunRequest`, `CodeBindingNamespace`, `CodeJsonValue`, `CodeRunResult`, `CodeRunFailure` |
+| [`src/types.ts`](src/types.ts) | Vocabulary: `CodeRunRequest`, `CodeRunSpec`, bindings, results, failures and sandbox facts |
 | — | No runtime invariant companion is published; this package exposes no independent event sequence or mutable data relation beyond contracts enforced at its owning seam. |
 
 </details>
@@ -97,7 +98,7 @@ Binding-global and error-class names are language-portable: they must match the
 Read these when the package-level contract is not enough. They move from the PTC mode consumer to the backends and the capability-seam model.
 
 - [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) — how the tool registry consumes `ctx.codeRuntime` and presents `run_code` to the model.
-- [Worker-thread backend](../code-runtime-node/README.md) — the shipped TypeScript execution backend.
+- [Node process backend](../code-runtime-node/README.md) — the shipped TypeScript execution backend.
 - [Experimental Python backend](../../experimental/code-runtime-python/README.md) — the private CPython subprocess provider and its fd-3 protocol.
 - [Code runtime subsystem reference](../../../docs/subsystems/code-runtime.md) — request/result vocabulary, bindings, and the `ctx.codeRuntime` cordis surface.
 - [Capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) — the Service Definition / Service Provider / Consumer split.
@@ -122,8 +123,8 @@ These limits define what the seam cannot do; they are current package constraint
 
 - **`run()` is one-shot** — `logs` arrive only on the resolved `CodeRunResult`; the seam exposes no streaming-log or progress API for a live program's output.
 - **No state survives between runs** — every request runs against a fresh world; a persistent REPL-style kernel is deferred until a backend brings its own logging story.
-- **The worker-thread backend ships; the Python process backend is private experimental; `'container'` has no implementation** — a hard security boundary awaits a container backend.
-- **Intermediate binding values have no byte cap** — implementations remain subject to structured-clone cost and process memory, while a provider or executor may already have imposed its own acquisition bound.
+- **Providers have different confinement capabilities** — the shipped Node provider enforces a resolved file policy, while the private experimental Python provider rejects an explicit policy. No container provider is supplied.
+- **No uniform binding byte cap applies across providers** — each provider owns its transport limits; a binding can still allocate memory before its result reaches those limits.
 
 <a id="dev-note"></a>
 ### Dev Note

+ 12 - 11
packages/code-runtime/code-runtime/README.zh.md

@@ -29,19 +29,20 @@ kind: "package-reference"
 
 ### 运行一个程序
 
-向运行时提供程序源码与一个或多个绑定命名空间。每个命名空间会成为程序内的一个全局异步函数对象——PTC mode 在 `tools` 下传入一个。程序作为异步函数的函数体运行,因此顶层 `await`/`return` 可用;无损 JSON 完成值成为 `result.value`,每个输出通道在 `result.logs` 中保留自身顺序而跨通道交错由后端决定,任何失败都以 `result.error` 报告并带有可分支的 kind。运行时绝不会因程序失败而 reject——reject 意味着你误用了 seam,例如在 dispose(资源释放)后提交运行。
+向运行时提供程序与绑定命名空间,然后依次调用 `resolve(request)` 和 `run(spec)`。解析根据提供方能力验证可选 cwd、timeout 与沙箱策略,并填入部署默认值。程序作为异步函数体运行,支持顶层 `await` 与 `return`;无损 JSON 完成值成为 `result.value`,捕获文本成为 `result.logs`,程序失败成为 `result.error`。每个输出通道保持自身顺序,跨通道交错由后端决定。
 
 ```text
-const result = await ctx.codeRuntime.run({
+const spec = ctx.codeRuntime.resolve({
   program: 'return await tools.add({ a: 1, b: 2 })',
   bindings: [{ global: 'tools', functions: { add: async (args) => args.a + args.b } }],
 })
+const result = await ctx.codeRuntime.run(spec)
 // result.value === 3
 ```
 
 ### 选择后端
 
-后端声明两个你可以依赖的描述符:`language`——程序必须使用的源语言,已知值为 `'typescript'` 与 `'python'`——以及 `isolation`——执行基底(`'worker-thread'`、`'process'`、`'container'`),仅供部署与诊断使用,不构成安全声明。[`dsh-code-runtime-node`](../code-runtime-node/README.zh.md) 在全新的 Node Worker 线程中执行 TypeScript;私有的 [`dsh-experimental-code-runtime-python`](../../experimental/code-runtime-python/README.zh.md) 包在全新的 CPython 子进程中执行 Python,供选择性组合使用。
+后端以 `language` 与 `isolation` 提供诊断描述符;两者都不授予权限或证明约束。[`dsh-code-runtime-node`](../code-runtime-node/README.zh.md) 在全新的受管 Node 进程中按已解析沙箱策略执行可擦除 TypeScript。私有的 [`dsh-experimental-code-runtime-python`](../../experimental/code-runtime-python/README.zh.md) 提供方在全新 CPython 子进程中执行 Python,不提供文件约束。`sandboxMode` 声明提供方的部署文件策略模式;不支持该能力时则缺省。
 
 ### 可移植地命名绑定
 
@@ -49,7 +50,7 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配 `[A-Za
 
 ### 可能出什么问题
 
-失败以 `result.error` 返回,并带正交的 `kind`:程序抛出或解析失败(`exception`)、预算到期(`timeout`)、运行被中止(`abort`)、执行基底终止(`worker-exit`)、完成值不是无损 JSON(`invalid-output`),或序列化输出超过上限(`output-limit`)。每种 kind 都带一条可反馈给模型的消息。`run()` 只在 seam 误用时 reject,例如在 dispose 后提交运行,或绑定名称不符合可移植标识符规则。
+失败以 `result.error` 返回,带正交的 `kind`:`exception`、`timeout`、`abort`、`worker-exit`、`invalid-output`、`output-limit`、`protocol` 或 `sandbox-unavailable`。提供方在成功或失败之外,单独返回适用的 `result.sandbox` 事实。无效或不支持的执行选项在 `resolve` 期间失败;`run` 拒绝调用方误用,例如未解析输入、无效绑定名或资源释放后的调用。
 
 -----
 
@@ -63,17 +64,17 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配 `[A-Za
 
 ### 设计理念
 
-本包是代码执行能力 seam 的 Service Definition 角色([能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)):一个注册为 `ctx.codeRuntime` 的抽象 `CodeRuntime extends Service`,加上两个后端与消费方共享的词汇。提供方继承 `CodeRuntime`、实现 `run` 并注册服务;消费方(`dsh-tools` 中的 PTC mode)生成面向模型的 SDK 并桥接工具分发。按约定,运行时不了解工具与会话:它接收程序与具名异步绑定,返回 `{ value, logs, error? }`。
+本包是代码执行能力 seam 的 Service Definition 角色([能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)):一个注册为 `ctx.codeRuntime` 的抽象 `CodeRuntime extends Service`,加上两个后端与消费方共享的词汇。提供方继承 `CodeRuntime`、实现 `resolve` 和 `run` 并注册服务;消费方(`dsh-tools` 中的 PTC mode)生成面向模型的 SDK 并桥接工具分发。按约定,运行时不了解工具与会话:它接收程序、具名异步绑定和已解析执行选项,然后返回捕获输出、执行结果与适用的沙箱事实。
 
 ### 服务 API
 
-约定是后端实现的三个成员:`run(request)` 针对请求的绑定执行一段程序,并把每个程序结果——解析/转换失败、抛出异常、无效完成值、输出溢出、预算到期、中止或基底终止——都作为结果 `error` 字段 resolve,reject 只留给调用方误用,例如在 dispose 后提交运行;`language` 与 `isolation` 是只读描述符,为部署与诊断标注源语言与执行基底。
+`resolve(request)` 负责支持选项的验证与部署默认值。`run(spec)` 执行完整输入,并在清理后返回程序结果。语言和执行基底描述符指导呈现;`sandboxMode` 表示消费方能否传入已解析文件策略。描述符与程序成功结果都不能代替后端报告的强制能力事实。
 
 穷尽式语义见[代码运行时子系统参考](../../../docs/subsystems/code-runtime.zh.md);确切签名见 [`src/index.ts`](src/index.ts)。
 
 ### 词汇
 
-`CodeRunRequest`(`program`、`bindings`、`signal?`)携带运行时操作所需的全部内容;默认值(时间预算、输出上限)来自各提供方的已验证配置,绝不是 `run()` 内部隐藏的 `??`。`bindings` 是 `CodeBindingNamespace` 列表(`global` + `functions` + 可选 `errorClass`),每个命名空间作为程序内的一个全局异步可调用函数对象公开,返回 `CodeJsonValue`——seam 的结构性无损 JSON 类型。`errorClass` 描述符点名真实的程序全局构造器,以及用于接收被拒绝成员名称的自有属性,因此后端永远不会得知 `ToolCallError` 之类的 Consumer 术语。`CodeRunResult` 报告无损 JSON 完成值 `value?`、通道内有序且跨通道交错由后端决定的 `logs: string[]`,以及 `error?`(`CodeRunFailure`:正交 `kind` + 可反馈给模型的 `message`)。完整约定见 `src/types.ts`。
+`CodeRunRequest` 携带程序、Host 绑定、取消和可选执行选择。`CodeRunSpec` 要求已解析的 cwd 与经过时间截止。`CodeBindingNamespace` 声明程序全局对象与可选的类型化拒绝构造器。`CodeRunResult` 将日志/值、失败与 `CodeRunSandbox` 事实分开;确切字段与提供方义务见 [`src/types.ts`](src/types.ts)。
 
 ### 可移植标识符
 
@@ -84,7 +85,7 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配标识
 | 文件 | 职责 |
 |---|---|
 | [`src/index.ts`](src/index.ts) | 插件入口:抽象 `CodeRuntime` 服务与可移植标识符排除集 |
-| [`src/types.ts`](src/types.ts) | 词汇:`CodeRunRequest`、`CodeBindingNamespace`、`CodeJsonValue`、`CodeRunResult`、`CodeRunFailure` |
+| [`src/types.ts`](src/types.ts) | 词汇:`CodeRunRequest`、`CodeRunSpec`、绑定、结果、失败与沙箱事实 |
 | — | 不发布运行时不变式伴生入口;本包不公开任何独立的事件序列或可变数据关系,相关约束仅由其所属 seam 的约定实施。 |
 
 </details>
@@ -97,7 +98,7 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配标识
 当包级约定不够用时阅读以下内容。它们从 PTC mode 消费方进入后端与能力 seam 模型。
 
 - [PTC mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md)——工具注册表如何消费 `ctx.codeRuntime` 并把 `run_code` 呈现给模型。
-- [Worker 线程后端](../code-runtime-node/README.zh.md)——已发布的 TypeScript 执行后端。
+- [Node 进程后端](../code-runtime-node/README.zh.md)——已发布的 TypeScript 执行后端。
 - [实验性 Python 后端](../../experimental/code-runtime-python/README.zh.md)——私有的 CPython 子进程提供方及其 fd-3 协议。
 - [代码运行时子系统参考](../../../docs/subsystems/code-runtime.zh.md)——请求/结果词汇、绑定与 `ctx.codeRuntime` 的 cordis 接口面。
 - [能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)——Service Definition / Service Provider / Consumer 拆分。
@@ -122,8 +123,8 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配标识
 
 - **`run()` 是一次性的**——`logs` 只有在 `CodeRunResult` resolve 后才能获得;seam 不提供正在运行的程序所产生输出的流式日志或进度接口。
 - **运行之间不保留状态**——每次请求都在全新环境中运行;持久 REPL 风格内核在某个后端带来自己的日志方案之前保持延期。
-- **worker 线程后端已发布;Python process 后端是私有实验包;`'container'` 没有实现**——强安全边界需要等待容器后端。
-- **中间 binding 值没有字节上限**——实现仍受 structured-clone 成本与进程内存约束,而提供方或执行器可能已经应用自己的获取上限。
+- **提供方的约束能力不同**——已发布 Node 提供方强制执行已解析文件策略,私有实验性 Python 提供方拒绝显式策略。不提供容器提供方。
+- **提供方之间没有统一的绑定字节上限**——各提供方负责自己的传输限制;绑定仍可能在结果到达这些限制前分配内存。
 
 <a id="dev-note"></a>
 ### 开发备注

+ 2 - 2
packages/core/agent-tool-presentation/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/core/agent-tool-presentation/README.md
-README.md: f4df9eb4e0752cb643435ec61d63ba1b9387845f
-README.zh.md: 72c0e31a63e7ebf2578bdf34ede32eccc8b768ab
+README.md: ccada947d31f7e6ae1df248fd11839845b1df59e
+README.zh.md: 9b36edc9e15fd98e1e2492e40c199fb6ea55731d

+ 1 - 1
packages/core/agent-tool-presentation/README.md

@@ -85,7 +85,7 @@ The package-level contract is enough for most consumers; read these when you nee
 
 - [tools package](../tools/README.md) — the tool presentation modes and `presentAs` API.
 - [agent-presets package](../../preset/agent-presets/README.md) — how presets compose agents and their standing mounts.
-- [code-runtime worker-thread package](../../code-runtime/code-runtime-node/README.md) — the TypeScript runtime a PTC mode needs.
+- [Node code-runtime package](../../code-runtime/code-runtime-node/README.md) — the TypeScript runtime a PTC mode needs.
 - [PTC mode executor-collapse note](../../../.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.md) — why the announced and callable surfaces stay the same.
 - [Core group map](../README.md) — how the core packages compose.
 

+ 1 - 1
packages/core/agent-tool-presentation/README.zh.md

@@ -85,7 +85,7 @@ kind: "package-reference"
 
 - [tools 包](../tools/README.zh.md)——工具呈现模式与 `presentAs` API。
 - [agent-presets 包](../../preset/agent-presets/README.zh.md)——preset 如何组合 agent 及其常驻挂载。
-- [code-runtime worker-thread 包](../../code-runtime/code-runtime-node/README.zh.md)——PTC 模式所需的 TypeScript 运行时。
+- [Node code-runtime 包](../../code-runtime/code-runtime-node/README.zh.md)——PTC 模式所需的 TypeScript 运行时。
 - [PTC mode 执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.zh.md)——通告面与可调用面为何保持一致。
 - [core 分组地图](../README.zh.md)——core 各包如何组合。
 

+ 2 - 2
packages/experimental/code-runtime-python/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/experimental/code-runtime-python/README.md
-README.md: 23595845e41d77b3a204e786c0d878b044ef6d4b
-README.zh.md: a9b758089a10954abb8c4f3861eaa6816cb9ee45
+README.md: 123f53fba415a28dff0730bd5b6e5ed45debfcd7
+README.zh.md: 8de7cc3cfd10b23650285ab23469d4509fc12c4a

+ 6 - 4
packages/experimental/code-runtime-python/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-This private experimental package lets source-checkout compositions run model-generated Python in a fresh CPython 3.10+ subprocess for each request. Programs can use top-level `await` and `return`, call configured bindings, and write normal stdout/stderr while receiving explicit completion or failure results. Resource budgets and process-group teardown contain runaway work, but the subprocess is not a security boundary: model code has bash-equivalent trust, no state persists across runs, and no shipped profile enables this runtime.
+This private experimental package lets source-checkout compositions run model-generated Python in a fresh CPython 3.10+ subprocess for each request. Programs can use top-level `await` and `return`, call configured bindings, and write normal stdout/stderr while receiving explicit completion or failure results. Resource budgets and process-group teardown contain runaway work, but the subprocess is not a security boundary: direct Python operations have no filesystem sandbox, no state persists across runs, and no shipped profile enables this runtime.
 
 ## Table of Contents
 
@@ -25,7 +25,9 @@ This private experimental package lets source-checkout compositions run model-ge
 <a id="use-this-package"></a>
 ## Use this package
 
-Choose this private experimental package only in an explicit source-checkout composition. Register `PythonCodeRuntime` beside `dsh-tools` and `run()` executes each program in a fresh CPython 3.10+ subprocess, resolving with `result.value` on success and `result.error` on failure (the orthogonal `CodeRunFailure.kind` taxonomy classifies parse failures, thrown exceptions, invalid completions, output overflows, budget expiry, aborts, and substrate death). It rejects only for seam misuse — a malformed binding namespace, or a call after disposal. Configuration is rejected at load: a non-Unix platform; an explicit `pythonBin` that is not an executable regular file or a bare name that does not resolve on `PATH`; a non-CPython, pre-3.10, or probe-failing interpreter; a non-positive or non-integer budget; a `maxLogBytes` below the truncation-marker floor (64); a timer value `setTimeout` would clamp; a budget larger than the effective fd-3 frame cap (lowered when the host heap cannot safely parse a near-cap frame); or an `addressSpaceMb`/output-budget pair whose worst-case peak would breach `RLIMIT_AS`.
+Choose this private experimental package only in an explicit source-checkout composition. Register `PythonCodeRuntime` beside `dsh-tools`; `run(resolve(request))` executes each program in a fresh CPython 3.10+ subprocess, resolving with `result.value` on success and `result.error` on failure (the orthogonal `CodeRunFailure.kind` taxonomy classifies parse failures, thrown exceptions, invalid completions, output overflows, budget expiry, aborts, and substrate death). It rejects only for seam misuse — a malformed binding namespace, or a call after disposal. Configuration is rejected at load: a non-Unix platform; an explicit `pythonBin` that is not an executable regular file or a bare name that does not resolve on `PATH`; a non-CPython, pre-3.10, or probe-failing interpreter; a non-positive or non-integer budget; a `maxLogBytes` below the truncation-marker floor (64); a timer value `setTimeout` would clamp; a budget larger than the effective fd-3 frame cap (lowered when the host heap cannot safely parse a near-cap frame); or an `addressSpaceMb`/output-budget pair whose worst-case peak would breach `RLIMIT_AS`.
+
+`resolve(request)` accepts an absolute `cwd` and uses the provider's configured `maxWallMs` deadline (600,000 ms by default). Explicit `timeoutMs` overrides and sandbox policies are unsupported and reject before execution. This provider does not advertise `sandboxMode` or return confinement facts.
 
 ### What you get
 
@@ -89,7 +91,7 @@ Read these when the runtime contract is not enough. They move from the seam defi
 - [Code runtime seam](../../code-runtime/code-runtime/README.md) — the abstract contract this backend implements.
 - [fd-3 protocol Agent Note](../../../.agents/notes/implemented/architecture/2026-07-31-code-runtime-python-fd3-protocol.md) — design rationale and wire contract.
 - [Settlement-fixes Agent Note](../../../.agents/notes/archived/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md) — settlement, metering, and containment fixes and their regression cases.
-- [Worker-thread backend](../../code-runtime/code-runtime-node/README.md) — the released TypeScript sibling.
+- [Node process backend](../../code-runtime/code-runtime-node/README.md) — the released TypeScript sibling.
 - [Code runtime subsystem reference](../../../docs/subsystems/code-runtime.md) — request/result vocabulary, bindings, and failure taxonomy.
 
 -----
@@ -114,7 +116,7 @@ These limits define what the package does and does not cover; they are current p
 - **A descendant that escapes the child's process group with `setsid()` is not reaped by the group teardown** — `kill(-pid)` cannot reach it; the run still settles on the value the done frame decided, and the close-deadline backstop forces settlement if the orphan holds the pipes open, but the orphan itself outlives the fiber until it exits on its own.
 - **A `log` frame that arrives after settlement is dropped** — once the run has settled, host-side capture is closed; a late fd-3 `log` frame (from a thread that outlived the done frame) is discarded rather than appended to `logs`.
 - **A binding REPLY value has no seam-level byte or depth cap** — `maxValueBytes` meters only the done frame's completion value; a wide binding reply is rebuilt host-side (`snapshotJsonValue` traversal) and encoded whole, bounded on both sides only by process memory (like a binding argument, which has no child-side budget either).
-- **No shipped profile mounts this provider** — the keyless `ptc-python-turn` snapshot replaces the headless PTC runtime through the real Loader; released profiles continue to use the worker-thread backend.
+- **No shipped profile mounts this provider** — the keyless `ptc-python-turn` snapshot replaces the headless PTC runtime through the real Loader; released profiles use the sandboxed Node process backend.
 - **Cross-channel log interleaving is backend-dependent** — Python stdout, stderr, and fd-3 log frames travel independently; each channel preserves its own order, while their total order in `result.logs` may differ.
 - **CPython 3.10 or newer is required** — the configured executable is resolved and version-probed at load; unsupported interpreters fail before `ctx.codeRuntime` is registered.
 - **The truncation-marker text and the tempdir prefix keep the pre-rename short names** — the marker `[dsh-code-runtime-python] log capture truncated at <N> bytes` and the `dsh-code-runtime-python-` tempdir prefix are byte-anchored by tests and are independent of the npm package name; promotion (dropping the `experimental-` prefix) does not rename them.

+ 6 - 5
packages/experimental/code-runtime-python/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-这个私有实验包可让源码检出组合在每次请求时都用全新的 CPython 3.10+ 子进程运行模型生成的 Python。程序可以使用顶层 `await` 和 `return`、调用已配置的 binding、正常写入 stdout/stderr,并获得明确的完成或失败结果。资源预算和进程组拆卸会约束失控的工作,但子进程不是安全边界:模型代码具有与 bash 同等的信任,运行之间不保留状态,且没有已发布 profile 启用此 runtime。
+这个私有实验包可让源码检出组合在每次请求时都用全新的 CPython 3.10+ 子进程运行模型生成的 Python。程序可以使用顶层 `await` 和 `return`、调用已配置的 binding、正常写入 stdout/stderr,并获得明确的完成或失败结果。资源预算和进程组拆卸会约束失控的工作,但子进程不是安全边界:直接 Python 操作没有文件系统沙箱,运行之间不保留状态,且没有已发布 profile 启用此 runtime。
 
 ## 目录
 
@@ -25,7 +25,9 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-仅在显式源码检出组合中选择这个私有实验包。将 `PythonCodeRuntime` 与 `dsh-tools` 一起注册后,`run()` 会在全新的 CPython 3.10+ 子进程中执行每个程序;成功时以 `result.value` resolve,失败时以 `result.error` resolve(正交的 `CodeRunFailure.kind` 分类涵盖解析失败、抛出异常、无效完成值、输出溢出、预算到期、中止与执行基底终止)。仅有 seam 误用会 reject——binding 命名空间不合法,或在 dispose 后调用。配置在加载期拒绝:非 Unix 平台;不是可执行普通文件的显式 `pythonBin`,或无法在 `PATH` 上解析的裸名;非 CPython、低于 3.10 或探测失败的解释器;非正或非整数预算;低于截断标记下限(64)的 `maxLogBytes`;会被 `setTimeout` 截断的定时器值;超过有效 fd-3 帧上限的预算(宿主堆无法安全解析接近上限的帧时,该上限会降低);或最坏峰值会突破 `RLIMIT_AS` 的 `addressSpaceMb`/输出预算组合。
+仅在显式源码检出组合中选择这个私有实验包。将 `PythonCodeRuntime` 与 `dsh-tools` 一起注册后,`run(resolve(request))` 会在全新的 CPython 3.10+ 子进程中执行每个程序;成功时以 `result.value` resolve,失败时以 `result.error` resolve(正交的 `CodeRunFailure.kind` 分类涵盖解析失败、抛出异常、无效完成值、输出溢出、预算到期、中止与执行基底终止)。仅有 seam 误用会 reject——binding 命名空间不合法,或在 dispose 后调用。配置在加载期拒绝:非 Unix 平台;不是可执行普通文件的显式 `pythonBin`,或无法在 `PATH` 上解析的裸名;非 CPython、低于 3.10 或探测失败的解释器;非正或非整数预算;低于截断标记下限(64)的 `maxLogBytes`;会被 `setTimeout` 截断的定时器值;超过有效 fd-3 帧上限的预算(宿主堆无法安全解析接近上限的帧时,该上限会降低);或最坏峰值会突破 `RLIMIT_AS` 的 `addressSpaceMb`/输出预算组合。
+
+`resolve(request)` 接受绝对 `cwd`,并使用提供方配置的 `maxWallMs` 截止时间(默认 600,000 ms)。显式 `timeoutMs` 覆盖与沙箱策略不受支持,会在执行前拒绝。本提供方不声明 `sandboxMode`,也不返回约束事实。
 
 ### 你得到什么
 
@@ -89,7 +91,7 @@ kind: "package-reference"
 - [Code runtime seam](../../code-runtime/code-runtime/README.zh.md) — 本后端实现的抽象契约。
 - [fd-3 协议 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-31-code-runtime-python-fd3-protocol.zh.md) — 设计理由与 wire 契约。
 - [结算修复 Agent Note](../../../.agents/notes/archived/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md) — 结算、计量与隔离修复及其回归用例。
-- [Worker 线程后端](../../code-runtime/code-runtime-node/README.zh.md) — 已发布的 TypeScript 兄弟。
+- [Node 进程后端](../../code-runtime/code-runtime-node/README.zh.md) — 已发布的 TypeScript 兄弟。
 - [Code runtime 子系统参考](../../../docs/subsystems/code-runtime.zh.md) — 请求/结果词汇、binding 与失败分类。
 
 -----
@@ -114,12 +116,11 @@ kind: "package-reference"
 - **以 `setsid()` 逃出子进程组后代不被组拆卸回收**——`kill(-pid)` 够不到它;运行仍按 done 帧决定的值结算,若该孤儿持有管道,close 截止兜底会强制结算,但孤儿本身在自行退出前一直存活到 fiber 之外。
 - **结算后到达的 `log` 帧被丢弃**——运行一旦结算,宿主侧捕获即关闭;迟到的 fd-3 `log` 帧(来自比 done 帧存活更久的线程)会被丢弃,而不是追加到 `logs`。
 - **binding 回复值没有 seam 级字节或深度上限**——`maxValueBytes` 只计量 done 帧的完成值;宽 binding 回复在宿主侧重建(`snapshotJsonValue` 遍历)并整帧编码,两侧都只受进程内存约束(与没有子进程侧预算的 binding 实参一样)。
-- **已发布 profile 均不挂载本提供方**——keyless `ptc-python-turn` 快照通过真实 Loader 替换 headless PTC 运行时;已发布 profile 继续使用 Worker 线程后端。
+- **已发布 profile 均不挂载本提供方**——keyless `ptc-python-turn` 快照通过真实 Loader 替换 headless PTC 运行时;已发布 profile 使用沙箱 Node 进程后端。
 - **跨通道日志交错由后端决定**——Python stdout、stderr 与 fd-3 日志帧彼此独立传输;每个通道保留自身顺序,但它们在 `result.logs` 中的总顺序可能不同。
 - **需要 CPython 3.10 或更高版本**——配置的可执行文件会在加载期完成解析与版本探测;不受支持的解释器会在 `ctx.codeRuntime` 注册前失败。
 - **截断标记文本与临时目录前缀保留改名前的短名**——标记 `[dsh-code-runtime-python] log capture truncated at <N> bytes` 与 `dsh-code-runtime-python-` 临时目录前缀被测试逐字节锚定,且独立于 npm 包名;promotion(去掉 `experimental-` 前缀)不会重命名它们。
 - **`run()` 是一次性的**——`logs` 只有在 `CodeRunResult` resolve 后才能获得;没有为运行中程序产生的输出提供流式日志或进度接口。
-
 - **运行之间不保留状态**——每次请求都在全新子进程中执行;持久 REPL 风格内核在某个后端带来自己的日志方案之前保持延期。
 - **原始长度超过有效帧解析上限的 fd-3 帧会让本次运行以 worker-exit 结算**——上限为 64 MiB,或当宿主的配置堆无法安全解析接近上限的帧时更低(`hostFrameParseCeiling`);`maxLogBytes`/`maxValueBytes` 在加载期被限制到同一上限,因此诚实子进程的帧总能放得下;模型构造的超过该上限的 binding 实参(一个在 seam 层没有预算的值)会触发同一上限——这是该 OOM 防护的已接受残余。
 - **停止读取回复的子进程会在回复积压超过 1024 帧时以 worker-exit 结算运行**——宿主每次写一条回复,管道满时等待 `drain`;只持续发送调用而不消费回复的子进程会让保留的积压(及其钉住的 binding 结果)一直增长到墙钟,因此积压上限让运行提前失败。binding 结果在 seam 层没有字节上限,所以这是计数上限而非字节上限。

+ 12 - 31
packages/spill/spill-policy/tests/spill-policy.spec.ts

@@ -23,6 +23,12 @@ import type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill'
 import * as SpillPolicy from '@deepseek-ai/dsh-spill-policy'
 import { mountRuntime } from '../../../code-runtime/code-runtime-node/tests/setup.ts'
 
+function observedAgent(ctx: Context, id: string, observe: (type: string, data: unknown) => void) {
+  const session = ctx.sessions.create(SessionId(id), { meta: { cwd: process.cwd() } })
+  ctx.on('session/event', (owner, event) => { if (owner === session) observe(event.type, event.data) })
+  return { session }
+}
+
 const testToolSignal = new AbortController().signal
 
 /** A stub spill backend recording its saves; `fail` exercises the best-effort fallback. */
@@ -195,12 +201,7 @@ describe('outer PTC mode failure capture', () => {
     await ctx.plugin(SpillPolicy, { maxInlineBytes: 200 })
     await mountRuntime(ctx, { maxOutputBytes: 500 })
     const events: unknown[] = []
-    const agent = {
-      session: {
-        header: { id: SessionId('code-spill'), cwd: '/workspace' },
-        append: (_type: string, data: unknown) => { events.push(data) },
-      },
-    }
+    const agent = observedAgent(ctx, 'code-spill', (_type: string, data: unknown) => { events.push(data) })
 
     const result = await ctx.tools.execute({
       signal: testToolSignal,
@@ -235,7 +236,7 @@ describe('read skip', () => {
 })
 
 describe('the durable dispatch-log arm', () => {
-  /** Boot code mode + the policy + the worker runtime; run one program via the real bridge. */
+  /** Boot code mode + the policy + the Node runtime; run one program via the real bridge. */
   async function runCodeWith(program: string, maxInlineBytes: number, extraTools: ToolDefinition[] = []) {
     const ctx = new Context()
     await ctx.plugin(SystemPrompt)
@@ -244,12 +245,7 @@ describe('the durable dispatch-log arm', () => {
     await ctx.plugin(SpillPolicy, { maxInlineBytes })
     await mountRuntime(ctx, {})
     const events: { type: string; data: unknown }[] = []
-    const agent = {
-      session: {
-        header: { id: SessionId('dispatch-spill'), cwd: '/workspace' },
-        append: (type: string, data: unknown) => { events.push({ type, data }) },
-      },
-    }
+    const agent = observedAgent(ctx, 'dispatch-spill', (type: string, data: unknown) => { events.push({ type, data }) })
     ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000)))
     ctx.tools.register(textTool('small_read', 'tiny'))
     for (const tool of extraTools) ctx.tools.register(tool)
@@ -327,12 +323,7 @@ describe('the durable dispatch-log arm', () => {
       return realSave(input)
     }
     const events: { type: string; data: unknown }[] = []
-    const agent = {
-      session: {
-        header: { id: SessionId('dispatch-slow-spill'), cwd: '/workspace' },
-        append: (type: string, data: unknown) => { events.push({ type, data }) },
-      },
-    }
+    const agent = observedAgent(ctx, 'dispatch-slow-spill', (type: string, data: unknown) => { events.push({ type, data }) })
     ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000)))
     ctx.tools.register(textTool('small_read', 'tiny'))
     let smallAfterHuge = false
@@ -385,12 +376,7 @@ describe('the durable dispatch-log arm', () => {
     const releases: (() => void)[] = []
     store.gate = () => new Promise<void>((resolve) => { releases.push(resolve) })
     const events: { type: string; data: unknown }[] = []
-    const agent = {
-      session: {
-        header: { id: SessionId('dispatch-spill-bound'), cwd: '/workspace' },
-        append: (type: string, data: unknown) => { events.push({ type, data }) },
-      },
-    }
+    const agent = observedAgent(ctx, 'dispatch-spill-bound', (type: string, data: unknown) => { events.push({ type, data }) })
     ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000)))
     const started = (n: number): boolean => events.some(event => event.type === 'tool/ptc-dispatch-start'
       && (event.data as { subCallId: string }).subCallId.endsWith(`:ptc:${n}`))
@@ -437,12 +423,7 @@ describe('the durable dispatch-log arm', () => {
     ;(ctx.spillStore as StubStore).fail = true
     const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
     const events: { type: string; data: unknown }[] = []
-    const agent = {
-      session: {
-        header: { id: SessionId('dispatch-spill-fail'), cwd: '/workspace' },
-        append: (type: string, data: unknown) => { events.push({ type, data }) },
-      },
-    }
+    const agent = observedAgent(ctx, 'dispatch-spill-fail', (type: string, data: unknown) => { events.push({ type, data }) })
     ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000)))
     const result = await ctx.tools.execute({
       signal: testToolSignal,

+ 2 - 2
packages/util/http-proxy/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/util/http-proxy/README.md
-README.md: 66c0b026ee8b26f23fd39a25fd7e4adc44077dbe
-README.zh.md: c3ee9fc5815b754027c54648d527c2f34fdc96c0
+README.md: 75a5326407b00031aa7c9f079c8c0c875c61ab5d
+README.zh.md: c390174d7e61d3ff45a9e7820fb60e7be6858faa

+ 1 - 1
packages/util/http-proxy/README.md

@@ -108,7 +108,7 @@ These limits define when the package is a poor fit. They are current package con
 - **No custom certificate authority** — a TLS-intercepting corporate proxy needs `NODE_EXTRA_CA_CERTS` set on the process before launch, which this package neither sets nor validates.
 - **A spawned child honors the policy only on a new enough runtime, and only when every value it inherits is one Node accepts** — it reads the published environment through Node's `NODE_USE_ENV_PROXY` (22.21+, 24+), and the engines range admits 22.19 and 22.20, where such a child stays direct. A user whose environment also names a SOCKS or otherwise refused proxy leaves every child Node direct: the flag is withheld so the child can start at all. A child also matches bypass entries with Node's own `NO_PROXY` rules, which differ from this package's in their separators and IPv4-range support. Nothing in this process depends on a Node version: every in-process request reaches the global dispatcher.
 - **Telemetry is direct by design** — the OTLP exporter posts through `node:http`, which no global dispatcher reaches. Routing it would need either an `http.Agent` whose `proxyEnv` option post-dates the lowest supported Node, or the SDK's `fetch` transport, which has no compression while the shipped profile enables gzip. Telemetry is the one channel whose loss costs the user nothing, so it stays where it was; `DSH_TELEMETRY_MODE=DISABLED` turns it off.
-- **A worker that executes model-authored code gets no proxy at all** — neither the `code-runtime` worker nor the `workflow` worker receives proxy configuration, so their own requests go direct. A proxy URL may carry `user:password`, and both run scripts the model wrote.
+- **Model-authored programs receive no proxy settings** — the Node code-runtime process and workflow worker do not inherit a proxy URL that may contain `user:password`. Their direct requests need their own configuration and remain subject to the execution sandbox.
 - **The regression gate sees source, not dependencies** — `verify-no-bare-dispatcher` parses `packages/*/*/src` and `apps/*/src`; tests, scripts, and the internals of a third-party SDK are outside it. That is why every outbound call site also carries an `egress.spec.ts`.
 
 <a id="dev-note"></a>

+ 1 - 1
packages/util/http-proxy/README.zh.md

@@ -108,7 +108,7 @@ loopback 始终被绕过——`localhost`、整个 `127.0.0.0/8` 段、`::1`、`
 - **不支持自定义证书颁发机构**——做 TLS 拦截的企业代理需要在启动前为进程设置 `NODE_EXTRA_CA_CERTS`,本包既不设置也不校验它。
 - **spawn 出的子进程只在足够新的运行时上遵循策略,且仅当它继承的每个值都是 Node 接受的**——它通过 Node 的 `NODE_USE_ENV_PROXY` 读取已发布的环境(22.21+、24+),而 engines 范围允许 22.19 与 22.20,在这两个版本上这样的子进程保持直连。若用户环境里还有 SOCKS 或其他被拒的代理,所有子 Node 都保持直连:不设置该标志,子进程才起得来。子进程还会按 Node 自己的 `NO_PROXY` 规则匹配绕过条目,其分隔符与 IPv4 区间处理与本包不同。本进程内不依赖任何 Node 版本:每一次进程内请求都会落到全局 dispatcher。
 - **遥测按设计直连**——OTLP 导出器通过 `node:http` 投递,全局 dispatcher 触及不到。要让它走代理,要么依赖 `http.Agent` 的 `proxyEnv`,而该选项晚于本项目支持的最低 Node 版本;要么改用 SDK 的 `fetch` 传输,但它没有压缩能力,而随附配置启用了 gzip。遥测是唯一一条丢失后不会让用户付出任何代价的通道,因此维持原状;`DSH_TELEMETRY_MODE=DISABLED` 可关闭它。
-- **执行由模型编写的代码的 worker 完全不获得代理**——`code-runtime` worker 与 `workflow` worker 都不接收代理配置,它们自身的请求直连。代理 URL 可能携带 `user:password`,而两者运行的都是模型写的脚本。
+- **模型编写的程序不接收代理配置**——Node code-runtime 进程与 workflow worker 不继承可能含有 `user:password` 的代理 URL。其直接请求需要自行配置,并继续受到执行沙箱的约束。
 - **防回归门禁只看源码,看不到依赖内部**——`verify-no-bare-dispatcher` 解析 `packages/*/*/src` 与 `apps/*/src`;测试、脚本以及第三方 SDK 的内部都在其之外。这正是每个出网点还各配一份 `egress.spec.ts` 的原因。
 
 <a id="dev-note"></a>

+ 2 - 2
pnpm-lock.yaml

@@ -4242,9 +4242,9 @@ importers:
       '@deepseek-ai/dsh-client-ui-slots':
         specifier: workspace:^
         version: link:../ui-slots
-      '@deepseek-ai/dsh-code-runtime-node':
+      '@deepseek-ai/dsh-code-runtime':
         specifier: workspace:^
-        version: link:../../code-runtime/code-runtime-node
+        version: link:../../code-runtime/code-runtime
       '@deepseek-ai/dsh-llm':
         specifier: workspace:^
         version: link:../../llm/llm

+ 10 - 0
scripts/type-equiv.manifest.json

@@ -2110,6 +2110,16 @@
       "doc": "packages/client/ui-conversation/README.md",
       "symbol": "ComposerChainProps",
       "source": "packages/client/ui-conversation/src/client/contract/slots.ts"
+    },
+    {
+      "doc": "docs/subsystems/code-runtime.md",
+      "symbol": "CodeRunSpec",
+      "source": "packages/code-runtime/code-runtime/src/types.ts"
+    },
+    {
+      "doc": "docs/subsystems/code-runtime.md",
+      "symbol": "CodeRunSandbox",
+      "source": "packages/code-runtime/code-runtime/src/types.ts"
     }
   ]
 }

+ 53 - 0
snapshots/session/ptc-node-read-only/cordis.snapshot.yml

@@ -0,0 +1,53 @@
+# Keyless PTC mode combines the runtime/registry changes with the
+# DeepSeek-to-replay swap in one profile patch.
+- insert:
+    - id: literal-sdk
+      name: '../../../packages/core/tools/tests/fixtures/literal-sdk.ts'
+
+- id: llm-deepseek
+  name: '@deepseek-ai/dsh-llm-deepseek'
+  disabled: true
+
+- id: plugin-package-inventory-deepseek
+  disabled: true
+
+- id: agent-default-model
+  name: '@deepseek-ai/dsh-agent-default-model'
+  config:
+    provider: deepseek-official
+    model: deepseek-v4-flash
+
+- id: session-persistence-jsonl
+  name: '@deepseek-ai/dsh-session-persistence-jsonl'
+  config:
+    root: !!js dshHomePath('sessions')
+    compression: none
+
+- id: agent-instructions
+  name: '@deepseek-ai/dsh-agent-instructions'
+  config:
+    maxBytes: 65536
+
+- id: tools
+  name: '@deepseek-ai/dsh-tools'
+  config:
+    mode: ptc
+
+- id: system-prompt
+  name: '@deepseek-ai/dsh-system-prompt'
+  config:
+    personaPrefix: |
+      You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
+
+      Verify your work by running the code or tests. Keep answers brief and factual.
+
+- insert:
+    - id: llm-replay
+      name: '@deepseek-ai/dsh-llm-replay'
+      config:
+        providers:
+          - id: deepseek-official
+            name: DeepSeek
+            models:
+              - id: deepseek-v4-flash
+              - id: deepseek-v4-pro

+ 36 - 0
snapshots/session/ptc-node-read-only/cordis.yml

@@ -0,0 +1,36 @@
+# PTC mode adds `ctx.codeRuntime` and changes the registry to one wire tool,
+# `run_code`, plus its generated TypeScript SDK prompt. The demo and snapshot
+# recorder apply this profile patch; replay applies its sibling patch.
+- insert:
+    - id: literal-sdk
+      name: '../../../packages/core/tools/tests/fixtures/literal-sdk.ts'
+
+- id: agent-default-model
+  name: '@deepseek-ai/dsh-agent-default-model'
+  config:
+    provider: deepseek-official
+    model: deepseek-v4-pro
+
+- id: session-persistence-jsonl
+  name: '@deepseek-ai/dsh-session-persistence-jsonl'
+  config:
+    root: !!js dshHomePath('sessions')
+    compression: !!js 'process.env.DSH_SNAPSHOT === undefined ? ''zstd'' : ''none'''
+
+- id: agent-instructions
+  name: '@deepseek-ai/dsh-agent-instructions'
+  config:
+    maxBytes: 65536
+
+- id: tools
+  name: '@deepseek-ai/dsh-tools'
+  config:
+    mode: ptc
+
+- id: system-prompt
+  name: '@deepseek-ai/dsh-system-prompt'
+  config:
+    personaPrefix: |
+      You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
+
+      Verify your work by running the code or tests. Keep answers brief and factual.

+ 78 - 0
snapshots/session/ptc-node-read-only/replay.override.json

@@ -0,0 +1,78 @@
+[
+  {
+    "kind": "chunks",
+    "chunks": [
+      {
+        "type": "block-start",
+        "index": 0,
+        "blockType": "tool-call"
+      },
+      {
+        "type": "tool-call-delta",
+        "index": 0,
+        "id": "call_node_policy",
+        "name": "run_code",
+        "argumentsDelta": "{\"code\":\"const fs = await import('node:fs/promises'); let denied = false; try { await fs.writeFile('ptc-denied.txt', 'forbidden') } catch (error) { denied = ['EPERM', 'EACCES', 'EROFS'].includes(error.code) }; const shell = await tools.bash({ command: 'pwd', description: 'Report the current workspace directory' }); return { denied, emptyEnvironment: Object.keys(process.env).length === 0, sameWorkspace: process.cwd() === shell.stdout.text.trim() };\",\"description\":\"Verify Node file policy and execution context\"}"
+      },
+      {
+        "type": "block-end",
+        "index": 0,
+        "block": {
+          "type": "tool-call",
+          "id": "call_node_policy",
+          "name": "run_code",
+          "arguments": "{\"code\":\"const fs = await import('node:fs/promises'); let denied = false; try { await fs.writeFile('ptc-denied.txt', 'forbidden') } catch (error) { denied = ['EPERM', 'EACCES', 'EROFS'].includes(error.code) }; const shell = await tools.bash({ command: 'pwd', description: 'Report the current workspace directory' }); return { denied, emptyEnvironment: Object.keys(process.env).length === 0, sameWorkspace: process.cwd() === shell.stdout.text.trim() };\",\"description\":\"Verify Node file policy and execution context\"}"
+        }
+      },
+      {
+        "type": "usage",
+        "usage": {
+          "inputTokens": 10,
+          "outputTokens": 5
+        }
+      },
+      {
+        "type": "finish",
+        "reason": {
+          "kind": "tool-calls"
+        }
+      }
+    ]
+  },
+  {
+    "kind": "chunks",
+    "chunks": [
+      {
+        "type": "block-start",
+        "index": 0,
+        "blockType": "text"
+      },
+      {
+        "type": "text-delta",
+        "index": 0,
+        "text": "Node policy and execution context verified."
+      },
+      {
+        "type": "block-end",
+        "index": 0,
+        "block": {
+          "type": "text",
+          "text": "Node policy and execution context verified."
+        }
+      },
+      {
+        "type": "usage",
+        "usage": {
+          "inputTokens": 10,
+          "outputTokens": 2
+        }
+      },
+      {
+        "type": "finish",
+        "reason": {
+          "kind": "stop"
+        }
+      }
+    ]
+  }
+]

Fichier diff supprimé car celui-ci est trop grand
+ 14 - 0
snapshots/session/ptc-node-read-only/session.v3.jsonl


+ 15 - 0
snapshots/session/ptc-node-read-only/snapshot.yml

@@ -0,0 +1,15 @@
+version: 1
+scenario: ptc-node-read-only
+profile: headless
+composition: ptc-node-read-only
+recording: authored
+header:
+  class: ptc-node-read-only
+  pin: true
+replay:
+  override: true
+platform: posix
+permission: read-only
+workspace:
+  final: true
+  parent: outside-temp

Fichier diff supprimé car celui-ci est trop grand
+ 232 - 0
snapshots/session/ptc-node-read-only/system-prompt.expected.md


+ 26 - 0
snapshots/session/ptc-node-read-only/tool-schemas.expected.json

@@ -0,0 +1,26 @@
+{
+  "initial": [
+    {
+      "name": "run_code",
+      "description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run.",
+      "parameters": {
+        "type": "object",
+        "properties": {
+          "code": {
+            "type": "string",
+            "description": "The program: the body of an async TypeScript function."
+          },
+          "description": {
+            "type": "string",
+            "description": "Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."
+          }
+        },
+        "required": [
+          "code",
+          "description"
+        ]
+      }
+    }
+  ],
+  "changes": []
+}

+ 0 - 0
snapshots/session/ptc-node-read-only/workspace.expected/.empty


+ 53 - 0
snapshots/session/ptc-node-workspace/cordis.snapshot.yml

@@ -0,0 +1,53 @@
+# Keyless PTC mode combines the runtime/registry changes with the
+# DeepSeek-to-replay swap in one profile patch.
+- insert:
+    - id: literal-sdk
+      name: '../../../packages/core/tools/tests/fixtures/literal-sdk.ts'
+
+- id: llm-deepseek
+  name: '@deepseek-ai/dsh-llm-deepseek'
+  disabled: true
+
+- id: plugin-package-inventory-deepseek
+  disabled: true
+
+- id: agent-default-model
+  name: '@deepseek-ai/dsh-agent-default-model'
+  config:
+    provider: deepseek-official
+    model: deepseek-v4-flash
+
+- id: session-persistence-jsonl
+  name: '@deepseek-ai/dsh-session-persistence-jsonl'
+  config:
+    root: !!js dshHomePath('sessions')
+    compression: none
+
+- id: agent-instructions
+  name: '@deepseek-ai/dsh-agent-instructions'
+  config:
+    maxBytes: 65536
+
+- id: tools
+  name: '@deepseek-ai/dsh-tools'
+  config:
+    mode: ptc
+
+- id: system-prompt
+  name: '@deepseek-ai/dsh-system-prompt'
+  config:
+    personaPrefix: |
+      You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
+
+      Verify your work by running the code or tests. Keep answers brief and factual.
+
+- insert:
+    - id: llm-replay
+      name: '@deepseek-ai/dsh-llm-replay'
+      config:
+        providers:
+          - id: deepseek-official
+            name: DeepSeek
+            models:
+              - id: deepseek-v4-flash
+              - id: deepseek-v4-pro

+ 36 - 0
snapshots/session/ptc-node-workspace/cordis.yml

@@ -0,0 +1,36 @@
+# PTC mode adds `ctx.codeRuntime` and changes the registry to one wire tool,
+# `run_code`, plus its generated TypeScript SDK prompt. The demo and snapshot
+# recorder apply this profile patch; replay applies its sibling patch.
+- insert:
+    - id: literal-sdk
+      name: '../../../packages/core/tools/tests/fixtures/literal-sdk.ts'
+
+- id: agent-default-model
+  name: '@deepseek-ai/dsh-agent-default-model'
+  config:
+    provider: deepseek-official
+    model: deepseek-v4-pro
+
+- id: session-persistence-jsonl
+  name: '@deepseek-ai/dsh-session-persistence-jsonl'
+  config:
+    root: !!js dshHomePath('sessions')
+    compression: !!js 'process.env.DSH_SNAPSHOT === undefined ? ''zstd'' : ''none'''
+
+- id: agent-instructions
+  name: '@deepseek-ai/dsh-agent-instructions'
+  config:
+    maxBytes: 65536
+
+- id: tools
+  name: '@deepseek-ai/dsh-tools'
+  config:
+    mode: ptc
+
+- id: system-prompt
+  name: '@deepseek-ai/dsh-system-prompt'
+  config:
+    personaPrefix: |
+      You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}.
+
+      Verify your work by running the code or tests. Keep answers brief and factual.

+ 78 - 0
snapshots/session/ptc-node-workspace/replay.override.json

@@ -0,0 +1,78 @@
+[
+  {
+    "kind": "chunks",
+    "chunks": [
+      {
+        "type": "block-start",
+        "index": 0,
+        "blockType": "tool-call"
+      },
+      {
+        "type": "tool-call-delta",
+        "index": 0,
+        "id": "call_node_policy",
+        "name": "run_code",
+        "argumentsDelta": "{\"code\":\"const fs = await import('node:fs/promises'); await fs.writeFile('ptc-created.txt', 'created by sandboxed Node\\\\n'); const written = await fs.readFile('ptc-created.txt', 'utf8') === 'created by sandboxed Node\\\\n'; const shell = await tools.bash({ command: 'pwd', description: 'Report the current workspace directory' }); return { written, emptyEnvironment: Object.keys(process.env).length === 0, sameWorkspace: process.cwd() === shell.stdout.text.trim() };\",\"description\":\"Verify Node file policy and execution context\"}"
+      },
+      {
+        "type": "block-end",
+        "index": 0,
+        "block": {
+          "type": "tool-call",
+          "id": "call_node_policy",
+          "name": "run_code",
+          "arguments": "{\"code\":\"const fs = await import('node:fs/promises'); await fs.writeFile('ptc-created.txt', 'created by sandboxed Node\\\\n'); const written = await fs.readFile('ptc-created.txt', 'utf8') === 'created by sandboxed Node\\\\n'; const shell = await tools.bash({ command: 'pwd', description: 'Report the current workspace directory' }); return { written, emptyEnvironment: Object.keys(process.env).length === 0, sameWorkspace: process.cwd() === shell.stdout.text.trim() };\",\"description\":\"Verify Node file policy and execution context\"}"
+        }
+      },
+      {
+        "type": "usage",
+        "usage": {
+          "inputTokens": 10,
+          "outputTokens": 5
+        }
+      },
+      {
+        "type": "finish",
+        "reason": {
+          "kind": "tool-calls"
+        }
+      }
+    ]
+  },
+  {
+    "kind": "chunks",
+    "chunks": [
+      {
+        "type": "block-start",
+        "index": 0,
+        "blockType": "text"
+      },
+      {
+        "type": "text-delta",
+        "index": 0,
+        "text": "Node policy and execution context verified."
+      },
+      {
+        "type": "block-end",
+        "index": 0,
+        "block": {
+          "type": "text",
+          "text": "Node policy and execution context verified."
+        }
+      },
+      {
+        "type": "usage",
+        "usage": {
+          "inputTokens": 10,
+          "outputTokens": 2
+        }
+      },
+      {
+        "type": "finish",
+        "reason": {
+          "kind": "stop"
+        }
+      }
+    ]
+  }
+]

Fichier diff supprimé car celui-ci est trop grand
+ 14 - 0
snapshots/session/ptc-node-workspace/session.v3.jsonl


+ 15 - 0
snapshots/session/ptc-node-workspace/snapshot.yml

@@ -0,0 +1,15 @@
+version: 1
+scenario: ptc-node-workspace
+profile: headless
+composition: ptc-node-workspace
+recording: authored
+header:
+  class: ptc-node-workspace
+  pin: true
+replay:
+  override: true
+platform: posix
+permission: workspace-write
+workspace:
+  final: true
+  parent: outside-temp

Fichier diff supprimé car celui-ci est trop grand
+ 232 - 0
snapshots/session/ptc-node-workspace/system-prompt.expected.md


+ 26 - 0
snapshots/session/ptc-node-workspace/tool-schemas.expected.json

@@ -0,0 +1,26 @@
+{
+  "initial": [
+    {
+      "name": "run_code",
+      "description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run.",
+      "parameters": {
+        "type": "object",
+        "properties": {
+          "code": {
+            "type": "string",
+            "description": "The program: the body of an async TypeScript function."
+          },
+          "description": {
+            "type": "string",
+            "description": "Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."
+          }
+        },
+        "required": [
+          "code",
+          "description"
+        ]
+      }
+    }
+  ],
+  "changes": []
+}

+ 1 - 0
snapshots/session/ptc-node-workspace/workspace.expected/ptc-created.txt

@@ -0,0 +1 @@
+created by sandboxed Node

+ 2 - 2
tsconfig.client.json

@@ -41,8 +41,8 @@
     "packages/client/*/tests/**/*.host.spec.ts"
   ],
   "references": [
-    // Producer-to-UI spill tests execute the real nested dispatch worker.
-    { "path": "./packages/code-runtime/code-runtime-node" },
+    // Producer-to-UI spill tests use the code-runtime contract with the real tool registry.
+    { "path": "./packages/code-runtime/code-runtime" },
     // Shared leaf: web e2e boots the real host webserver (fixture + real-host
     // smoke policy). webserver has zero workspace deps and no cordis merge,
     // so it cannot drag host-side Context augmentation into this program.

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff