Ver Fonte

test(code-runtime): complete sandboxed process coverage and catalogs

Tianyi Cui há 6 dias atrás
pai
commit
fc5cb0f649
31 ficheiros alterados com 361 adições e 169 exclusões
  1. 1 1
      apps/cli/tests/profiles/headless/tests/ptc.e2e.ts
  2. 2 2
      docs/capability-seams.i18n.yaml
  3. 2 2
      docs/capability-seams.md
  4. 2 2
      docs/capability-seams.zh.md
  5. 2 2
      docs/config-catalog.i18n.yaml
  6. 27 29
      docs/config-catalog.md
  7. 27 29
      docs/config-catalog.zh.md
  8. 2 2
      docs/event-producer-consumer.i18n.yaml
  9. 6 6
      docs/event-producer-consumer.md
  10. 6 6
      docs/event-producer-consumer.zh.md
  11. 2 2
      docs/subsystems/code-runtime.i18n.yaml
  12. 12 8
      docs/subsystems/code-runtime.md
  13. 12 8
      docs/subsystems/code-runtime.zh.md
  14. 6 6
      packages/code-runtime/code-runtime-node/src/bootstrap.ts
  15. 2 0
      packages/code-runtime/code-runtime-node/src/channel.ts
  16. 9 5
      packages/code-runtime/code-runtime-node/src/index.ts
  17. 10 10
      packages/code-runtime/code-runtime-node/src/json-wire.ts
  18. 1 0
      packages/code-runtime/code-runtime-node/src/launch.ts
  19. 2 0
      packages/code-runtime/code-runtime-node/src/process.ts
  20. 10 10
      packages/code-runtime/code-runtime-node/tests/channel.spec.ts
  21. 104 12
      packages/code-runtime/code-runtime-node/tests/host-failures.spec.ts
  22. 3 3
      packages/code-runtime/code-runtime-node/tests/process-main.spec.ts
  23. 5 5
      packages/code-runtime/code-runtime-node/tests/process.spec.ts
  24. 47 0
      packages/code-runtime/code-runtime-node/tests/runtime.spec.ts
  25. 3 3
      packages/code-runtime/code-runtime/src/index.ts
  26. 1 0
      packages/code-runtime/code-runtime/tests/service.spec.ts
  27. 8 9
      packages/experimental/code-runtime-python/src/index.ts
  28. 21 0
      packages/experimental/code-runtime-python/tests/runtime.spec.ts
  29. 22 7
      packages/extensions/tool-cordis/src/api-catalog.ts
  30. 2 0
      scripts/check-workspace-constraints.ts
  31. 2 0
      scripts/gen-cordis-catalog.ts

+ 1 - 1
apps/cli/tests/profiles/headless/tests/ptc.e2e.ts

@@ -270,7 +270,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
     await ctx.plugin(ToolCordis)
     const agent = {
       id: SessionId('ptc-cordis'),
-      session: { append: vi.fn() },
+      session: ctx.sessions.create(SessionId('ptc-cordis'), { meta: { cwd: process.cwd() } }),
     } as unknown as Agent
 
     const value = completion(await runCode(ctx, `

+ 2 - 2
docs/capability-seams.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/capability-seams.md
-capability-seams.md: 547414830411d1d80781c614e75d839f8a9547fb
-capability-seams.zh.md: 8b89c0240a9881625f3d6a65bb0f1a31e887e1ef
+capability-seams.md: 2fbae75691dba96b7b2ab03b1a800d8ddd48f3e1
+capability-seams.zh.md: 2e9c785f05fe53f3976e4655b60786f2c571d147

+ 2 - 2
docs/capability-seams.md

@@ -163,7 +163,7 @@ flowchart LR
   svc_permissionPresets["ctx.permissionPresets<br/>Permission presets"]
   pkg_code_runtime["code-runtime"]
   svc_codeRuntime["ctx.codeRuntime<br/>Code-execution seam"]
-  pkg_code_runtime_worker_thread["code-runtime-node"]
+  pkg_code_runtime_node["code-runtime-node"]
   pkg_experimental_code_runtime_python["experimental-code-runtime-python"]
   pkg_fs["fs"]
   svc_fs["ctx.fs<br/>Filesystem provider seam"]
@@ -241,7 +241,7 @@ flowchart LR
   pkg_client_file_upload --> svc_fileUploads
   pkg_client_modules --> svc_clientModules
   pkg_code_runtime --> svc_codeRuntime
-  pkg_code_runtime_worker_thread --> svc_codeRuntime
+  pkg_code_runtime_node --> svc_codeRuntime
   pkg_command_feedback --> svc_sessionFeedback
   pkg_commands --> svc_commands
   pkg_compaction --> svc_compaction

+ 2 - 2
docs/capability-seams.zh.md

@@ -165,7 +165,7 @@ flowchart LR
   svc_permissionPresets["ctx.permissionPresets<br/>Permission presets"]
   pkg_code_runtime["code-runtime"]
   svc_codeRuntime["ctx.codeRuntime<br/>Code-execution seam"]
-  pkg_code_runtime_worker_thread["code-runtime-node"]
+  pkg_code_runtime_node["code-runtime-node"]
   pkg_experimental_code_runtime_python["experimental-code-runtime-python"]
   pkg_fs["fs"]
   svc_fs["ctx.fs<br/>Filesystem provider seam"]
@@ -243,7 +243,7 @@ flowchart LR
   pkg_client_file_upload --> svc_fileUploads
   pkg_client_modules --> svc_clientModules
   pkg_code_runtime --> svc_codeRuntime
-  pkg_code_runtime_worker_thread --> svc_codeRuntime
+  pkg_code_runtime_node --> svc_codeRuntime
   pkg_command_feedback --> svc_sessionFeedback
   pkg_commands --> svc_commands
   pkg_compaction --> svc_compaction

+ 2 - 2
docs/config-catalog.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/config-catalog.md
-config-catalog.md: 7e53ba35aa57d87f2ab5f6456f21b28070be6162
-config-catalog.zh.md: 4010623711532fbff2db578604af4fe07fb0e4d0
+config-catalog.md: da6b091110a6e4dc7afb8955ea223cdf537c00ea
+config-catalog.zh.md: cc2bd99c13ce37a2d8c13c9abf6930e250c73cf0

+ 27 - 29
docs/config-catalog.md

@@ -405,38 +405,37 @@ Source: [`packages/client/hmr/src/index.ts:31`](../packages/client/hmr/src/index
 
 ## `@deepseek-ai/dsh-code-runtime-node`
 
+Requires: `fs` · `subprocess` · `sandbox` · `sandboxPolicy`
+
 ```ts config-catalog
-/** Plugin config: every execution cap, changeable from `cordis.yml` (no hardcoded tunables). */
-export interface Config {
-  /**
-   * Busy-time budget in milliseconds: the run fails with kind `'timeout'`
-   * once the worker's MEASURED event-loop active time
-   * (`worker.performance.eventLoopUtilization()`) exceeds this. Metering
-   * measured busy time — not wall time, not host-side pending-call
-   * bookkeeping — is what makes the budget both fair (a program awaiting a
-   * slow tool accrues nothing) and ungameable (a hot loop accrues whether
-   * or not a decoy dispatch is in flight).
-   */
-  computeMs?: number
-  /**
-   * Wall-clock ceiling in milliseconds; never pauses for anything. The
-   * backstop for what busy-time cannot see (a program awaiting a promise
-   * nobody will resolve). At most `2_147_483_647` (Node's maximum
-   * `setTimeout` delay, about 24.9 days): a longer value is rejected at load
-   * because `setTimeout` would clamp it to 1 ms.
-   */
-  maxWallMs?: number
-  /**
-   * Hard cap for serialized log-array, completion-value, and failure-message payloads;
-   * fixed result-envelope syntax is excluded.
-   */
+/** Deployment-varying runtime bounds and launch choices. */
+export interface Config extends LaunchConfig {
+  /** Default elapsed deadline, including nested tool and approval waits. */
+  timeoutMs?: number
+  /** Maximum elapsed deadline accepted by resolve. */
+  maxTimeoutMs?: number
+  /** Combined serialized logs, completion and diagnostic byte cap. */
   maxOutputBytes?: number
-  /** The worker's max old-generation heap in MiB (`resourceLimits`); overflow kills the worker, surfacing as kind `'worker-exit'`. */
+  /** V8 old-generation heap limit in MiB; native allocations are excluded. */
   maxOldGenerationSizeMb?: number
+  /** Maximum control frame, outstanding argument and queued control-output bytes. */
+  maxMessageBytes?: number
+  /** Maximum simultaneous host binding calls accepted from a program. */
+  maxPendingCalls?: number
+  /** Managed process termination and output-drain grace in milliseconds. */
+  graceMs?: number
+}
+
+/** Deployment-owned Node executable and optional preinstalled built bootstrap. */
+export interface LaunchConfig {
+  /** Executable in the subprocess world; defaults to the current Node executable. */
+  nodeExecutable?: string
+  /** Absolute preinstalled built bootstrap in the execution world. */
+  bootstrapPath?: string
 }
 ```
 
-Source: [`packages/code-runtime/code-runtime-node/src/index.ts:25`](../packages/code-runtime/code-runtime-node/src/index.ts)
+Source: [`packages/code-runtime/code-runtime-node/src/index.ts:26`](../packages/code-runtime/code-runtime-node/src/index.ts)
 
 <a id="deepseek-aidsh-compaction-basic"></a>
 
@@ -576,8 +575,7 @@ export interface Config {
    * rejects a float). The child sets the soft limit to `cpuSeconds` and the
    * hard limit to `cpuSeconds + 1`: the kernel delivers SIGXCPU at the soft
    * limit, which the host classifies as a `timeout`; the +1s hard limit is a
-   * SIGKILL backstop for a program that traps SIGXCPU. Granularity is seconds —
-   * a coarser counterpart to the worker backend's millisecond `computeMs`.
+   * SIGKILL backstop for a program that traps SIGXCPU. Granularity is whole seconds.
    */
   cpuSeconds?: number
   /** Wall-clock ceiling in milliseconds; backstops CPU time for programs awaiting a promise nobody resolves. */
@@ -3168,7 +3166,7 @@ export interface Config {
 export type ToolPresentationMode = 'native' | 'ptc' | 'both'
 ```
 
-Source: [`packages/core/tools/src/index.ts:647`](../packages/core/tools/src/index.ts)
+Source: [`packages/core/tools/src/index.ts:648`](../packages/core/tools/src/index.ts)
 
 <a id="deepseek-aidsh-typert-loader"></a>
 

+ 27 - 29
docs/config-catalog.zh.md

@@ -407,38 +407,37 @@ export interface Config {
 
 ## `@deepseek-ai/dsh-code-runtime-node`
 
+需要: `fs` · `subprocess` · `sandbox` · `sandboxPolicy`
+
 ```ts config-catalog
-/** Plugin config: every execution cap, changeable from `cordis.yml` (no hardcoded tunables). */
-export interface Config {
-  /**
-   * Busy-time budget in milliseconds: the run fails with kind `'timeout'`
-   * once the worker's MEASURED event-loop active time
-   * (`worker.performance.eventLoopUtilization()`) exceeds this. Metering
-   * measured busy time — not wall time, not host-side pending-call
-   * bookkeeping — is what makes the budget both fair (a program awaiting a
-   * slow tool accrues nothing) and ungameable (a hot loop accrues whether
-   * or not a decoy dispatch is in flight).
-   */
-  computeMs?: number
-  /**
-   * Wall-clock ceiling in milliseconds; never pauses for anything. The
-   * backstop for what busy-time cannot see (a program awaiting a promise
-   * nobody will resolve). At most `2_147_483_647` (Node's maximum
-   * `setTimeout` delay, about 24.9 days): a longer value is rejected at load
-   * because `setTimeout` would clamp it to 1 ms.
-   */
-  maxWallMs?: number
-  /**
-   * Hard cap for serialized log-array, completion-value, and failure-message payloads;
-   * fixed result-envelope syntax is excluded.
-   */
+/** Deployment-varying runtime bounds and launch choices. */
+export interface Config extends LaunchConfig {
+  /** Default elapsed deadline, including nested tool and approval waits. */
+  timeoutMs?: number
+  /** Maximum elapsed deadline accepted by resolve. */
+  maxTimeoutMs?: number
+  /** Combined serialized logs, completion and diagnostic byte cap. */
   maxOutputBytes?: number
-  /** The worker's max old-generation heap in MiB (`resourceLimits`); overflow kills the worker, surfacing as kind `'worker-exit'`. */
+  /** V8 old-generation heap limit in MiB; native allocations are excluded. */
   maxOldGenerationSizeMb?: number
+  /** Maximum control frame, outstanding argument and queued control-output bytes. */
+  maxMessageBytes?: number
+  /** Maximum simultaneous host binding calls accepted from a program. */
+  maxPendingCalls?: number
+  /** Managed process termination and output-drain grace in milliseconds. */
+  graceMs?: number
+}
+
+/** Deployment-owned Node executable and optional preinstalled built bootstrap. */
+export interface LaunchConfig {
+  /** Executable in the subprocess world; defaults to the current Node executable. */
+  nodeExecutable?: string
+  /** Absolute preinstalled built bootstrap in the execution world. */
+  bootstrapPath?: string
 }
 ```
 
-来源:[`packages/code-runtime/code-runtime-node/src/index.ts:25`](../packages/code-runtime/code-runtime-node/src/index.ts)
+来源: [`packages/code-runtime/code-runtime-node/src/index.ts:26`](../packages/code-runtime/code-runtime-node/src/index.ts)
 
 <a id="deepseek-aidsh-compaction-basic"></a>
 
@@ -578,8 +577,7 @@ export interface Config {
    * rejects a float). The child sets the soft limit to `cpuSeconds` and the
    * hard limit to `cpuSeconds + 1`: the kernel delivers SIGXCPU at the soft
    * limit, which the host classifies as a `timeout`; the +1s hard limit is a
-   * SIGKILL backstop for a program that traps SIGXCPU. Granularity is seconds —
-   * a coarser counterpart to the worker backend's millisecond `computeMs`.
+   * SIGKILL backstop for a program that traps SIGXCPU. Granularity is whole seconds.
    */
   cpuSeconds?: number
   /** Wall-clock ceiling in milliseconds; backstops CPU time for programs awaiting a promise nobody resolves. */
@@ -3170,7 +3168,7 @@ export interface Config {
 export type ToolPresentationMode = 'native' | 'ptc' | 'both'
 ```
 
-来源:[`packages/core/tools/src/index.ts:647`](../packages/core/tools/src/index.ts)
+来源:[`packages/core/tools/src/index.ts:648`](../packages/core/tools/src/index.ts)
 
 <a id="deepseek-aidsh-typert-loader"></a>
 

+ 2 - 2
docs/event-producer-consumer.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/event-producer-consumer.md
-event-producer-consumer.md: 239b93480a70e6b4d2107f7bdd177019d501fa5e
-event-producer-consumer.zh.md: 94ad41bfedcdbf4c63af5f9f346213fb0973cf34
+event-producer-consumer.md: a176adbb357926a0c1547ced64dd2e178e539738
+event-producer-consumer.zh.md: a42ad92f7df6571ecb2ea67adfef2953faef6579

+ 6 - 6
docs/event-producer-consumer.md

@@ -61,12 +61,12 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:161`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
 | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
 | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
-| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:155`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
-| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:167`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
-| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:144`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs) |
-| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:181`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
-| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:189`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/fs/tool-present) |
+| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:200`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:156`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
+| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:168`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
+| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:145`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs) |
+| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:182`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
+| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/fs/tool-present) |
 | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:85`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
 | `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules` |
 | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |

+ 6 - 6
docs/event-producer-consumer.zh.md

@@ -63,12 +63,12 @@
 | `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:159`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
 | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
 | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
-| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:155`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
-| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:167`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
-| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:144`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs) |
-| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:181`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
-| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:189`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/fs/tool-present) |
+| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:200`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:156`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
+| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:168`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
+| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:145`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs) |
+| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:182`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
+| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/fs/tool-present) |
 | `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:85`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
 | `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules` |
 | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |

+ 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: f807bec9971eeb019f56dbea6efd560e07098256
-code-runtime.zh.md: b2d725ed6fba921866a59208ead42701f1865eb9
+code-runtime.md: 6f01cc446b7e60d8053845bf9df0ecb2547ad918
+code-runtime.zh.md: 9f09e3019ca65957f9722ba8baf4e97e432f7361

+ 12 - 8
docs/subsystems/code-runtime.md

@@ -208,15 +208,19 @@ Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and subs
 
 ```ts cordis-catalog
 /**
- * Execute one program against the request's bindings and capture what it
- * emitted. See the class doc for the resolution contract (error is a result
- * field; rejection means Service Definition contract misuse only).
- * @param request - the program, its bindings, and the abort signal; the
- *   request carries everything the runtime acts on, with no hidden defaults.
- * @returns the run's outcome: completion value (when transferable), the
- *   ordered log capture, and the failure (if any).
+ * Resolve supported options and provider defaults before execution.
+ * @param request - Program, bindings, cancellation and optional execution choices.
+ * @returns Complete directory, deadline and supported authority for run.
+ * @throws When an explicit choice is invalid or unsupported by this provider.
  */
-abstract run(request: CodeRunRequest): Promise<CodeRunResult>
+abstract resolve(request: CodeRunRequest): CodeRunSpec
+
+/**
+ * Execute resolved inputs; program outcomes resolve as result fields.
+ * @param spec - directory, deadline, program, bindings, cancellation and supported policy.
+ * @returns Captured output and the execution outcome.
+ */
+abstract run(spec: CodeRunSpec): Promise<CodeRunResult>
 ```
 
 Source: [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)

+ 12 - 8
docs/subsystems/code-runtime.zh.md

@@ -208,15 +208,19 @@ Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and subs
 
 ```ts cordis-catalog
 /**
- * Execute one program against the request's bindings and capture what it
- * emitted. See the class doc for the resolution contract (error is a result
- * field; rejection means Service Definition contract misuse only).
- * @param request - the program, its bindings, and the abort signal; the
- *   request carries everything the runtime acts on, with no hidden defaults.
- * @returns the run's outcome: completion value (when transferable), the
- *   ordered log capture, and the failure (if any).
+ * Resolve supported options and provider defaults before execution.
+ * @param request - Program, bindings, cancellation and optional execution choices.
+ * @returns Complete directory, deadline and supported authority for run.
+ * @throws When an explicit choice is invalid or unsupported by this provider.
  */
-abstract run(request: CodeRunRequest): Promise<CodeRunResult>
+abstract resolve(request: CodeRunRequest): CodeRunSpec
+
+/**
+ * Execute resolved inputs; program outcomes resolve as result fields.
+ * @param spec - directory, deadline, program, bindings, cancellation and supported policy.
+ * @returns Captured output and the execution outcome.
+ */
+abstract run(spec: CodeRunSpec): Promise<CodeRunResult>
 ```
 
 Source: [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)

+ 6 - 6
packages/code-runtime/code-runtime-node/src/bootstrap.ts

@@ -20,7 +20,7 @@ function defineBindingErrorField(error: Error, key: string, value: string): void
   capturedObjectDefineProperty(error, key, attributes)
 }
 
-/** The port API the bootstrap needs — satisfied by `parentPort` and by the tests' fake. */
+/** Program messages and host replies transported by the private process channel. */
 export interface BootstrapPort {
   postMessage(message: ProgramToHost): void
   on(event: 'message', listener: (message: ReplyMessage) => void): void
@@ -76,7 +76,7 @@ export class LogBuffer {
       if (prefix.length > 0) {
         const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes)
         /* v8 ignore next -- truncateJsonStringBytes guarantees the returned prefix fits. */
-        if (prefixBytes === undefined) throw new CapturedError('worker output ledger produced an oversized log prefix')
+        if (prefixBytes === undefined) throw new CapturedError('program output ledger produced an oversized log prefix')
         this.bytes += prefixBytes + separatorBytes
         this.entries += 1
         this.sink(prefix)
@@ -127,7 +127,7 @@ export function makeConsoleShim(logs: LogBuffer): Record<(typeof CONSOLE_LEVELS)
  * @param logs - the buffer captured writes are pushed into.
  * @param stream - the stream whose `write` slot is patched.
  * @returns the restore function (the in-process tests un-patch; the real
- *   worker never needs to).
+ *   child never needs to).
  */
 export function captureStreamWrites(logs: LogBuffer, stream: PatchableStream): () => void {
   // The slot's VALUE is stored for restore and reassigned — never invoked
@@ -154,7 +154,7 @@ const INSPECT_OPTIONS = { depth: 4, maxArrayLength: 100, maxStringLength: 10_000
  * Prepare the program's completion value for the done message. Only lossless
  * JSON crosses, and a value that does not fit the remaining combined outer
  * budget reports `output-limit`; the host revalidates hostile traffic and
- * remains authoritative for native pipe writes the worker cannot observe.
+ * remains authoritative for native pipe writes the program shim cannot observe.
  *
  * @param value - the program's completion value.
  * @param remainingOutputBytes - exact bytes left after captured logs.
@@ -205,7 +205,7 @@ function prepareFailure(
 
 /**
  * Prepare a thrown program value without sending an unbounded stack or
- * string across the worker port.
+ * string across the process channel.
  * @param error - the value thrown by the program.
  * @param remainingOutputBytes - exact bytes left after captured logs.
  * @param maxOutputBytes - the configured cap named in an overflow diagnostic.
@@ -306,7 +306,7 @@ export function wireReplies(port: BootstrapPort, pending: Map<number, PendingCal
  * @param data - the boot payload's namespace declarations (globals + names).
  * @param port - the port binding calls are posted to.
  * @param pending - the id-keyed map each posted call parks its handles in.
- * @param nextId - the shared mutable id counter (worker-issued correlation ids).
+ * @param nextId - the shared mutable id counter (program-issued correlation ids).
  * @param errorClasses - per-namespace constructors shared with program globals.
  * @returns one namespace object per declaration, in declaration order.
  */

+ 2 - 0
packages/code-runtime/code-runtime-node/src/channel.ts

@@ -42,6 +42,8 @@ export class JsonChannel {
     if (this.closed) return
     try {
       let offset = 0
+      // receive can synchronously close the channel while dispatching a preceding frame.
+      // oxlint-disable-next-line typescript/no-unnecessary-condition
       while (offset < chunk.length && !this.closed) {
         if (this.payload === undefined) {
           const bytes = Math.min(4 - this.headerBytes, chunk.length - offset)

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

@@ -192,6 +192,8 @@ export class NodeCodeRuntime extends CodeRuntime {
     signal.addEventListener('abort', onAbort, { once: true })
     if (signal.aborted) onAbort()
     try {
+      // Abort callbacks can settle execution before or during an awaited operation.
+      // oxlint-disable-next-line typescript/no-unnecessary-condition
       if (settled) return await result.promise
       const stripped = stripTypeScriptTypes(STRIP_PREFIX + spec.program + STRIP_SUFFIX)
       parsing = false
@@ -205,6 +207,8 @@ export class NodeCodeRuntime extends CodeRuntime {
         maxOutputBytes: this.config.maxOutputBytes,
       }
       const executable = await this.ctx.subprocess.resolveExecutable(this.config.nodeExecutable, undefined, signal)
+      // Abort callbacks can settle execution before or during an awaited operation.
+      // oxlint-disable-next-line typescript/no-unnecessary-condition
       if (settled) return await result.promise
       const argv = [executable, `--max-old-space-size=${this.config.maxOldGenerationSizeMb}`, ...bootstrapArgs(this.ctx.fs, this.config, this.config.maxMessageBytes)]
       confined = policy.mode === 'danger-full-access' ? undefined : this.ctx.sandbox.confine(argv, { ...policy, mode: policy.mode })
@@ -261,7 +265,7 @@ export class NodeCodeRuntime extends CodeRuntime {
         if (!ready) {
           if (raw.type !== 'ready') { protocolFailure('program frame arrived before bootstrap readiness'); return }
           ready = true
-          void transport.send({ type: 'boot', data }).catch((error) => { protocolFailure(messageOf(error)) })
+          void transport.send({ type: 'boot', data }).catch((error: unknown) => { protocolFailure(messageOf(error)) })
           return
         }
         switch (raw.type) {
@@ -296,7 +300,7 @@ export class NodeCodeRuntime extends CodeRuntime {
             const args = decodeCodeJsonWire(raw.args)
             if (args === undefined) { protocolFailure('binding arguments must be lossless JSON'); return }
             if (++pending > this.config.maxPendingCalls || (pendingBytes += bytes) > this.config.maxMessageBytes) { protocolFailure('pending binding calls exceed configured limits'); return }
-            const id = raw.id as number
+            const id = raw.id
             void (async () => {
               let reply: unknown
               try {
@@ -310,7 +314,7 @@ export class NodeCodeRuntime extends CodeRuntime {
                 pendingBytes -= bytes
               }
               if (!settled) await transport.send(reply)
-            })().catch((error) => { protocolFailure(messageOf(error)) })
+            })().catch((error: unknown) => { protocolFailure(messageOf(error)) })
             return
           }
           default: protocolFailure('unknown control message')
@@ -318,10 +322,10 @@ export class NodeCodeRuntime extends CodeRuntime {
       }, (error, kind) => {
         if (kind === 'protocol') protocolFailure(messageOf(error))
         else if (ready) finish({ kind: 'worker-exit', message: messageOf(error) })
-        else void launched.done.then(processFinished, (failure) => { finish({ kind: 'worker-exit', message: messageOf(failure) }) })
+        else void launched.done.then(processFinished, (failure: unknown) => { finish({ kind: 'worker-exit', message: messageOf(failure) }) })
       })
       channel = transport
-      void launched.done.then((outcome) => { setImmediate(() => { processFinished(outcome) }) }, (error) => { finish({ kind: confined !== undefined && isRunnerSpawnFailure(error, confined.argv[0], spec.cwd) ? 'sandbox-unavailable' : 'worker-exit', message: messageOf(error) }) })
+      void launched.done.then((outcome) => { setImmediate(() => { processFinished(outcome) }) }, (error: unknown) => { finish({ kind: confined !== undefined && isRunnerSpawnFailure(error, confined.argv[0], spec.cwd) ? 'sandbox-unavailable' : 'worker-exit', message: messageOf(error) }) })
     } catch (error: unknown) {
       finish({ kind: error instanceof SandboxUnavailableError ? 'sandbox-unavailable' : parsing ? 'exception' : 'worker-exit', message: messageOf(error) })
     }

+ 10 - 10
packages/code-runtime/code-runtime-node/src/json-wire.ts

@@ -1,11 +1,11 @@
 /**
- * Lossless-JSON snapshots for the dependency-free source worker closure.
+ * Lossless-JSON snapshots for the dependency-free source bootstrap closure.
  * @module @deepseek-ai/dsh-code-runtime-node/json-wire
  */
 
 import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
 
-/* jscpd:ignore-start -- the source worker mirrors session JSON helpers without workspace runtime imports */
+/* jscpd:ignore-start -- the source bootstrap mirrors session JSON helpers without workspace runtime imports */
 type IntrinsicCallable = (this: unknown, ...args: unknown[]) => unknown
 
 const intrinsicFunctionToString = Reflect.get(Function.prototype, 'toString') as IntrinsicCallable
@@ -139,9 +139,9 @@ type SnapshotTask =
   | { kind: 'leave'; source: object }
 
 /**
- * Validate and detach one worker-boundary value without loading another
+ * Validate and detach one process-boundary value without loading another
  * workspace package at runtime. This mirrors the session-owned canonical
- * JSON boundary while remaining safe to import from the unbuilt worker.
+ * JSON boundary while remaining safe to import from the unbuilt bootstrap.
  * Its iterative traversal adds no JavaScript call-stack depth limit.
  *
  * @param value - the candidate completion value.
@@ -242,17 +242,17 @@ interface ObjectWireToken {
   keys: string[]
 }
 
-type WorkerJsonToken = null | boolean | number | string | ArrayWireToken | ObjectWireToken
+type CodeJsonToken = null | boolean | number | string | ArrayWireToken | ObjectWireToken
 
 /**
  * A pre-order, bounded-depth transport for one lossless JSON value. Container
- * markers and scalar leaves share one flat token array, so `worker_threads`
- * never has to structured-clone the value's application nesting.
+ * markers and scalar leaves share one flat token array, so JSON serialization
+ * does not recurse through the value's application nesting.
  */
-export type CodeJsonWire = WorkerJsonToken[]
+export type CodeJsonWire = CodeJsonToken[]
 
 /**
- * Flatten one validated JSON value for the worker-thread message port.
+ * Flatten one validated JSON value for the process control channel.
  * @param value - the lossless JSON value to transport.
  * @returns a pre-order token stream whose own nesting is bounded.
  */
@@ -340,7 +340,7 @@ function containerToken(value: object): ArrayWireToken | ObjectWireToken | undef
 }
 
 /**
- * Rebuild one lossless JSON value from the flat worker-thread wire format.
+ * Rebuild one lossless JSON value from the flat process wire format.
  * Malformed or incomplete traffic returns `undefined`; traversal is iterative
  * and therefore independent of the transported value's application depth.
  * @param input - untrusted message-port payload.

+ 1 - 0
packages/code-runtime/code-runtime-node/src/launch.ts

@@ -26,6 +26,7 @@ export function bootstrapArgs(fs: FileSystem, config: LaunchConfig, maxMessageBy
     if (result === undefined) throw new Error(`code runtime bootstrap is unavailable in the subprocess execution world: ${path}`)
     return result
   }
+  /* v8 ignore next 3 -- built-lib.e2e.ts executes the bundled provider and sibling process.js under plain Node. */
   if (!new URL(import.meta.url).pathname.endsWith('.ts')) {
     return [mapped(fileURLToPath(new URL('./process.js', import.meta.url))), String(maxMessageBytes)]
   }

+ 2 - 0
packages/code-runtime/code-runtime-node/src/process.ts

@@ -64,6 +64,8 @@ export async function runNodeMain(stream: Duplex, maxMessageBytes: number, proce
     await channel.drain()
   } finally {
     channel.close()
+    // Transport callbacks can set failed while the awaited program executes.
+    // oxlint-disable-next-line typescript/no-unnecessary-condition
     if (failed) processState.exitCode = 1
   }
 }

+ 10 - 10
packages/code-runtime/code-runtime-node/tests/channel.spec.ts

@@ -25,8 +25,8 @@ describe('bounded process frames', () => {
   it('reassembles split binary headers and UTF-8 bodies', async () => {
     const { a, b } = pair()
     const received = Promise.withResolvers<unknown>()
-    const channel = new JsonChannel(a, 1000, value => received.resolve(value), error => received.reject(error))
-    onTestFinished(() => channel.close())
+    const channel = new JsonChannel(a, 1000, (value) => { received.resolve(value) }, (error) => { received.reject(error) })
+    onTestFinished(() => { channel.close() })
     const bytes = frame({ text: '你好🙂' })
     for (const byte of bytes) b.write(Buffer.from([byte]))
     expect(await received.promise).toEqual({ text: '你好🙂' })
@@ -39,8 +39,8 @@ describe('bounded process frames', () => {
     const channel = new JsonChannel(a, 1000, (value) => {
       received.push(value)
       if (received.length === 2) done.resolve(undefined)
-    }, error => done.reject(error))
-    const peer = new JsonChannel(b, 1000, () => {}, error => done.reject(error))
+    }, (error) => { done.reject(error) })
+    const peer = new JsonChannel(b, 1000, () => {}, (error) => { done.reject(error) })
     onTestFinished(() => { channel.close(); peer.close() })
     await Promise.all([peer.send({ n: 1 }), peer.send({ n: 2 })])
     await done.promise
@@ -51,8 +51,8 @@ describe('bounded process frames', () => {
   it.each([0, 65])('rejects a declared %i-byte frame before accepting its body', async (length) => {
     const { a, b } = pair()
     const failure = Promise.withResolvers<Error>()
-    const channel = new JsonChannel(a, 64, () => { throw new Error('must not dispatch') }, error => failure.resolve(error))
-    onTestFinished(() => channel.close())
+    const channel = new JsonChannel(a, 64, () => { throw new Error('must not dispatch') }, (error) => { failure.resolve(error) })
+    onTestFinished(() => { channel.close() })
     const header = Buffer.alloc(4)
     header.writeUInt32BE(length)
     b.write(header)
@@ -63,7 +63,7 @@ describe('bounded process frames', () => {
     for (const payload of [Buffer.from('{'), Buffer.from([0xff]), Buffer.from('{}')]) {
       const { a, b } = pair()
       const failure = Promise.withResolvers<Error>()
-      const channel = new JsonChannel(a, 64, () => { throw new Error('receiver failed') }, error => failure.resolve(error))
+      const channel = new JsonChannel(a, 64, () => { throw new Error('receiver failed') }, (error) => { failure.resolve(error) })
       const header = Buffer.alloc(4)
       header.writeUInt32BE(payload.length)
       b.write(Buffer.concat([header, payload]))
@@ -84,8 +84,8 @@ describe('bounded process frames', () => {
   it('reports peer EOF while a program is active', async () => {
     const { a, b } = pair()
     const failure = Promise.withResolvers<Error>()
-    const channel = new JsonChannel(a, 64, () => {}, error => failure.resolve(error))
-    onTestFinished(() => channel.close())
+    const channel = new JsonChannel(a, 64, () => {}, (error) => { failure.resolve(error) })
+    onTestFinished(() => { channel.close() })
     b.end()
     expect((await failure.promise).message).toContain('ended')
   })
@@ -117,7 +117,7 @@ it('ignores callbacks already captured by an emission when an earlier listener c
     const { a } = pair()
     let failures = 0
     const channel = new JsonChannel(a, 64, () => { throw new Error('closed channel dispatched') }, () => { failures += 1 })
-    a.prependOnceListener(event, () => channel.close())
+    a.prependOnceListener(event, () => { channel.close() })
     a.emit(event, frame({}))
     a.emit('error', new Error('late closed stream error'))
     expect(failures).toBe(0)

+ 104 - 12
packages/code-runtime/code-runtime-node/tests/host-failures.spec.ts

@@ -38,6 +38,12 @@ async function setup(config: Config = {}, mode: 'read-only' | 'danger-full-acces
   const stderr = new PassThrough()
   const direct = Promise.withResolvers<SubprocessOutcome>()
   const messages: unknown[] = []
+  const terminate = vi.fn(() => {
+    stdout.end()
+    stderr.end()
+    direct.resolve({ exitCode: 0, signal: null })
+  })
+  const waitForExit = vi.fn(async () => true)
   const handle: SubprocessHandle = {
     stdin: undefined,
     stdout,
@@ -45,12 +51,8 @@ async function setup(config: Config = {}, mode: 'read-only' | 'danger-full-acces
     control,
     collected: {},
     done: direct.promise,
-    terminate: vi.fn(() => {
-      stdout.end()
-      stderr.end()
-      direct.resolve({ exitCode: 0, signal: null })
-    }),
-    waitForExit: vi.fn(async () => true),
+    terminate,
+    waitForExit,
   }
   const writes = new Set<Promise<void>>()
   let receive: (message: unknown) => void = () => {}
@@ -83,7 +85,8 @@ async function setup(config: Config = {}, mode: 'read-only' | 'danger-full-acces
     }
   }
   return {
-    ctx, runtime, handle, direct, stdout, stderr, control, peer, messages, spawn, resolveExecutable, emit, start, onBoot,
+    ctx, runtime, handle, terminate, waitForExit, direct, stdout, stderr, control, peer, messages,
+    spawn, resolveExecutable, emit, start, onBoot,
     receive: (callback: typeof receive) => { receive = callback },
   }
 }
@@ -179,8 +182,8 @@ describe('Node runtime host failures', () => {
     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()
+    expect(h.terminate).toHaveBeenCalledOnce()
+    expect(h.waitForExit).toHaveBeenCalledOnce()
   })
 
   it('reports an executable lookup failure without allocating a process', async () => {
@@ -269,7 +272,7 @@ describe('Node runtime host failures', () => {
     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(result.error).toEqual({ kind: 'protocol', message: expect.stringContaining(message) as unknown })
     expect(binding).not.toHaveBeenCalled()
   })
 
@@ -307,7 +310,7 @@ describe('Node runtime host failures', () => {
       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') })
+    expect(h.messages).toContainEqual({ type: 'reply', id: 1, ok: false, message: expect.stringContaining('lossless JSON') as unknown })
   })
 
   it('does not publish a late binding reply after the program has settled', async () => {
@@ -324,7 +327,7 @@ describe('Node runtime host failures', () => {
 
   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'))
+    vi.mocked(h.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' })
   })
@@ -351,4 +354,93 @@ describe('Node runtime host failures', () => {
     expect(result.error?.kind).toBe('output-limit')
     expect(result.logs).toEqual(['retained'])
   })
+
+  it.each([undefined, { kind: 'exception', message: 'program failed' }] as const)('bounds incomplete raw output after completion with %j', async (failure) => {
+    const h = await setup({ graceMs: 5 })
+    const cleaning = Promise.withResolvers<undefined>()
+    vi.mocked(h.terminate).mockImplementation(() => { h.direct.resolve({ exitCode: 0, signal: null }) })
+    vi.mocked(h.waitForExit).mockImplementation(async () => { cleaning.resolve(undefined); return true })
+    h.onBoot(() => { h.emit(failure === undefined ? { type: 'done' } : { type: 'done', error: failure }) })
+    vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
+    try {
+      const pending = h.start()
+      await cleaning.promise
+      await vi.advanceTimersByTimeAsync(5)
+      expect((await pending).error).toEqual(failure ?? { kind: 'worker-exit', message: 'Node process output did not close cleanly' })
+      expect(h.stdout.destroyed).toBe(true)
+      expect(h.stderr.destroyed).toBe(true)
+    } finally { vi.useRealTimers() }
+  })
+
+  it('decodes fragmented native output and preserves incomplete final UTF-8 bytes', async () => {
+    const h = await setup()
+    h.onBoot(() => {
+      for (const stream of [h.stdout, h.stderr]) {
+        stream.write(Buffer.from([0xe2]))
+        stream.write(Buffer.from([0x82, 0xac]))
+        stream.end(Buffer.from([0xe2]))
+      }
+      h.emit({ type: 'done' })
+    })
+    const result = await h.start()
+    expect(result.error).toBeUndefined()
+    expect(result.logs.filter(text => text === '€')).toHaveLength(2)
+    expect(result.logs.filter(text => text === '�')).toHaveLength(2)
+  })
+
+  it('keeps the fitting native-output prefix and ignores later overflow chunks', async () => {
+    const h = await setup({ maxOutputBytes: 256 })
+    vi.mocked(h.terminate).mockImplementation(() => {
+      h.stderr.end('later output')
+      h.stdout.end()
+      h.direct.resolve({ exitCode: 0, signal: null })
+    })
+    h.onBoot(() => { h.stdout.write('x'.repeat(1024)) })
+    const result = await h.start()
+    expect(result.error?.kind).toBe('output-limit')
+    expect(result.logs.join('')).toMatch(/^x+$/)
+    expect(Buffer.byteLength(JSON.stringify(result.logs))).toBeLessThanOrEqual(256)
+  })
+
+  it.each(['stdout', 'stderr'] as const)('reports a broken raw %s pipe', async (name) => {
+    const h = await setup()
+    h.onBoot(() => { h[name].emit('error', new Error(`${name} closed`)) })
+    expect((await h.start()).error).toEqual({ kind: 'worker-exit', message: `${name} closed` })
+  })
+
+  it('retains stderr when a ready process exits without a completion frame', async () => {
+    const h = await setup()
+    h.onBoot(() => {
+      h.stderr.write('native fatal detail')
+      h.direct.resolve({ exitCode: 9, signal: null })
+    })
+    expect((await h.start()).error).toEqual({ kind: 'worker-exit', message: 'Node process exited before completing (9): native fatal detail' })
+  })
+
+  it.each([true, false])('attributes a failed confined spawn only with runner evidence (%s)', async (runnerFailed) => {
+    const h = await setup({}, 'read-only')
+    const runner = '/sandbox-runner'
+    vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([runner, ...argv]))
+    h.onBoot(() => {
+      h.direct.reject(Object.assign(new Error('spawn rejected'), runnerFailed ? { code: 'ENOENT', path: runner, syscall: `spawn ${runner}` } : {}))
+    })
+    expect((await h.start()).error).toEqual({ kind: runnerFailed ? 'sandbox-unavailable' : 'worker-exit', message: 'spawn rejected' })
+  })
+
+  it('selects the private packaged bootstrap without leaking ambient environment', async () => {
+    const h = await setup()
+    h.onBoot(() => { h.emit({ type: 'done' }) })
+    const prior = Object.getOwnPropertyDescriptor(process, 'pkg')
+    try {
+      Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+      expect((await h.start()).error).toBeUndefined()
+      const spec = h.spawn.mock.calls[0]?.[0]
+      expect(spec?.env?.DSH_CODE_RUNTIME_NODE).toBe('1')
+      expect(spec?.argv.at(-1)).toBe('134217728')
+      expect(Object.entries(spec?.env ?? {}).filter(([, value]) => value !== undefined)).toEqual([['DSH_CODE_RUNTIME_NODE', '1']])
+    } finally {
+      if (prior === undefined) Reflect.deleteProperty(process, 'pkg')
+      else Object.defineProperty(process, 'pkg', prior)
+    }
+  })
 })

+ 3 - 3
packages/code-runtime/code-runtime-node/tests/process-main.spec.ts

@@ -27,7 +27,7 @@ it('clears process environment, dispatches a binding reply and flushes the termi
     if (message.type === 'ready') void peer.send({ type: 'boot', data: { code: 'console.log("ready"); return await tools.echo({});', namespaces: [{ global: 'tools', names: ['echo'] }], maxOutputBytes: 1024 } })
     if (message.type === 'call') void peer.send({ type: 'reply', id: message.id, ok: true, value: encodeCodeJsonWire(42) })
   }, () => {})
-  onTestFinished(() => peer.close())
+  onTestFinished(() => { peer.close() })
   await runNodeMain(child, 4096, state)
   expect(state.env).toEqual({})
   expect(state.exitCode).toBeUndefined()
@@ -42,7 +42,7 @@ it.each([0, -1, 1.5, 4294967296])('rejects an invalid bootstrap frame limit %i',
 it('rejects an unexpected first control frame', async () => {
   const { child, host } = endpoints()
   const peer = new JsonChannel(host, 4096, () => { void peer.send({ type: 'reply' }) }, () => {})
-  onTestFinished(() => peer.close())
+  onTestFinished(() => { peer.close() })
   await expect(runNodeMain(child, 4096, processState())).rejects.toThrow('expected program boot')
 })
 
@@ -60,7 +60,7 @@ it('contains program writes that exceed queued control output', async () => {
   const peer = new JsonChannel(host, 1024, (raw) => {
     if ((raw as { type: string }).type === 'ready') void peer.send({ type: 'boot', data: { code: 'for(let i=0;i<30;i++) console.log("x".repeat(100));', namespaces: [], maxOutputBytes: 8000 } })
   }, () => {})
-  onTestFinished(() => peer.close())
+  onTestFinished(() => { peer.close() })
   await runNodeMain(child, 1024, state)
   expect(state.exitCode).toBe(1)
 })

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

@@ -17,13 +17,13 @@ it('boots an unbuilt source closure outside the workspace and exchanges tool rep
   const source = `import {Socket} from 'node:net';import {runNodeMain} from ${JSON.stringify(pathToFileURL(join(directory, 'process.ts')).href)};await runNodeMain(new Socket({fd:7,readable:true,writable:true}),100000,process);`
   const child = spawn(process.execPath, ['--input-type=module', '--eval', source], {
     env: { PLACEHOLDER_SECRET: 'fixture-only' },
-    stdio: ['ignore', 'pipe', 'pipe', 'ignore', 'ignore', 'ignore', 'ignore', 'pipe'],
+    stdio: ['ignore', 'pipe', 'pipe', 'ignore', 'ignore', 'ignore', 'ignore', 'overlapped'],
   })
   let stderr = ''
   child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8') })
   const finished = new Promise<void>((resolve) => { child.once('close', () => { resolve() }) })
   const completed = Promise.withResolvers<unknown>()
-  child.once('error', error => completed.reject(error))
+  child.once('error', (error) => { completed.reject(error) })
   child.once('exit', (code) => { if (code !== 0) completed.reject(new Error(`child exit ${code}: ${stderr}`)) })
   const control = Array.from(child.stdio)[7]
   if (!(control instanceof Duplex)) { child.kill(); await finished; throw new Error('missing child control channel') }
@@ -34,15 +34,15 @@ it('boots an unbuilt source closure outside the workspace and exchanges tool rep
         code: 'const answer = await tools.echo({ n: 21 }); return { answer, env: { ...process.env } }',
         namespaces: [{ global: 'tools', names: ['echo'] }],
         maxOutputBytes: 10_000,
-      } }).catch(error => completed.reject(error))
+      } }).catch((error: unknown) => { completed.reject(error) })
     } else if (message.type === 'call') {
       expect(decodeCodeJsonWire(message.args)).toEqual({ n: 21 })
-      void channel.send({ type: 'reply', id: message.id, ok: true, value: encodeCodeJsonWire(42) }).catch(error => completed.reject(error))
+      void channel.send({ type: 'reply', id: message.id, ok: true, value: encodeCodeJsonWire(42) }).catch((error: unknown) => { completed.reject(error) })
     } else if (message.type === 'done') {
       if (message.error !== undefined) completed.reject(new Error(JSON.stringify(message.error)))
       else completed.resolve(decodeCodeJsonWire(message.value))
     }
-  }, error => completed.reject(error))
+  }, (error) => { completed.reject(error) })
   onTestFinished(async () => { channel.close(); child.kill(); await finished })
   expect(await completed.promise).toEqual({ answer: 42, env: {} })
   await finished

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

@@ -1,5 +1,6 @@
 import { mkdtemp, mkdir, readFile, rm, symlink } from 'node:fs/promises'
 import { homedir } from 'node:os'
+import { createServer, type Socket } from 'node:net'
 import { join } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
 import { describe, expect, it, onTestFinished } from 'vitest'
@@ -98,6 +99,52 @@ describe('Node program process', () => {
     expect((await active).error).toEqual({ kind: 'abort', message: 'stop' })
   })
 
+  it('closes a still-running descendant before returning the program result', async () => {
+    const { run } = await setup()
+    const connected = Promise.withResolvers<undefined>()
+    const disconnected = Promise.withResolvers<undefined>()
+    let peer: Socket | undefined
+    const server = createServer((socket) => {
+      peer = socket
+      // A terminated peer can reset its connection instead of sending FIN.
+      socket.on('error', () => {})
+      socket.once('close', () => { disconnected.resolve(undefined) })
+      connected.resolve(undefined)
+    })
+    onTestFinished(async () => {
+      peer?.destroy()
+      if (server.listening) await new Promise<void>((resolve, reject) => {
+        server.close((error) => { if (error) reject(error); else resolve() })
+      })
+    })
+    await new Promise<void>((resolve, reject) => {
+      server.once('error', reject)
+      server.listen(0, '127.0.0.1', resolve)
+    })
+    const address = server.address()
+    if (address === null || typeof address === 'string') throw new Error('expected bound TCP listener')
+    const child = `require('node:net').connect(${address.port},'127.0.0.1')`
+    const result = await run({
+      program: `const {spawn}=await import("node:child_process"); spawn(process.execPath,["-e",${JSON.stringify(child)}],{stdio:"ignore"}); await tools.connected({}); return 42;`,
+      bindings: bindings({ connected: async () => { await connected.promise; return null } }),
+    })
+    expect(result.error).toBeUndefined()
+    expect(result.value).toBe(42)
+    await disconnected.promise
+  })
+
+  it('applies the configured V8 old-generation ceiling to each fresh Node process', async () => {
+    const limits: number[] = []
+    for (const maxOldGenerationSizeMb of [32, 64]) {
+      const { run } = await setup({ maxOldGenerationSizeMb })
+      const result = await run({ program: 'return (await import("node:v8")).getHeapStatistics().heap_size_limit;', bindings: [] })
+      expect(result.error).toBeUndefined()
+      if (typeof result.value !== 'number') throw new Error('expected V8 heap limit')
+      limits.push(result.value)
+    }
+    expect(Number(limits[1]) - Number(limits[0])).toBe(32 * 1024 * 1024)
+  })
+
   it('disposes active programs and rejects later execution', async () => {
     const { ctx, run, runtime } = await setup()
     const spec = runtime.resolve({ program: '', bindings: [] })

+ 3 - 3
packages/code-runtime/code-runtime/src/index.ts

@@ -22,13 +22,13 @@ export type {
 
 /**
  * Binding globals EVERY backend refuses because SOME backend owns the slot in
- * the program's namespace: `console` (the worker's log capture), and
+ * the program's namespace: `console` (Node's log capture), and
  * `__dsh_main__`/`__builtins__`/`__name__` (the Python backend's bootstrap
  * wrapper and seeded module globals),
  * and `__debug__`. One shared set — rather than each backend refusing only its
  * own slots — keeps the portability promise real: a namespace list valid on
  * one backend is valid on all, so a caller cannot pick a name that works on
- * the worker and collides on Python (or vice versa). `__name__` et al. ARE
+ * Node and collides on Python (or vice versa). `__name__` et al. ARE
  * valid portable identifiers, so the identifier rule on
  * `CodeBindingNamespace.global` never rejects them — hence this explicit set.
  * (Error members differ: {@link DUNDER_MEMBER} refuses every dunder form
@@ -68,7 +68,7 @@ export const DUNDER_MEMBER = /^__.+__$/
 /**
  * Reserved words of every portable target language (ECMAScript ∪ Python),
  * refused as {@link CodeBindingNamespace.global} / error-class names by all
- * backends, one per language: the released TypeScript worker thread and the
+ * backends, one per language: the released TypeScript Node process and the
  * experimental, private CPython subprocess. The portable-identifier contract
  * promises a namespace list valid on one backend is valid on every backend; a
  * per-language check would let `lambda` pass the TypeScript backend and fail

+ 1 - 0
packages/code-runtime/code-runtime/tests/service.spec.ts

@@ -43,6 +43,7 @@ describe('CodeRuntime service seam', () => {
     const { runtime } = await setup()
     expect(runtime.language).toBe('typescript')
     expect(runtime.isolation).toBe('in-process-stub')
+    expect(runtime.sandboxMode).toBeUndefined()
 
     const calls: unknown[] = []
     const result = await runtime.run(runtime.resolve({

+ 8 - 9
packages/experimental/code-runtime-python/src/index.ts

@@ -45,8 +45,7 @@ export interface Config {
    * rejects a float). The child sets the soft limit to `cpuSeconds` and the
    * hard limit to `cpuSeconds + 1`: the kernel delivers SIGXCPU at the soft
    * limit, which the host classifies as a `timeout`; the +1s hard limit is a
-   * SIGKILL backstop for a program that traps SIGXCPU. Granularity is seconds —
-   * a coarser counterpart to the worker backend's millisecond `computeMs`.
+   * SIGKILL backstop for a program that traps SIGXCPU. Granularity is whole seconds.
    */
   cpuSeconds?: number
   /** Wall-clock ceiling in milliseconds; backstops CPU time for programs awaiting a promise nobody resolves. */
@@ -115,7 +114,7 @@ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
 const RESERVED_NAMES = PORTABLE_RESERVED_WORDS
 
 /**
- * The seam's shared backend-owned globals (`console` is the worker's slot;
+ * The seam's shared backend-owned globals (`console` is the Node provider's slot;
  * `__dsh_main__`/`__builtins__`/`__name__` are this bootstrap's wrapper and
  * seeded module globals). Shared so a namespace list valid on one backend is
  * valid on all — colliding with an owned slot would be silently overwritten
@@ -125,7 +124,7 @@ const RUNTIME_OWNED_GLOBALS = RESERVED_BINDING_GLOBALS
 
 /**
  * The seam's shared error-member exclusions (`RESERVED_ERROR_MEMBERS` +
- * dunder-form names) — enforced identically here and in the worker backend so
+ * dunder-form names) — enforced identically here and in the Node backend so
  * an errorClass valid on one backend is valid on all. Several dunders are
  * constrained CPython descriptors whose `setattr` raises while constructing
  * the very rejection it was meant to carry; the exact set is an interpreter
@@ -826,7 +825,7 @@ export class PythonCodeRuntime extends CodeRuntime {
   private readonly live = new Set<LiveRun>()
   private disposed = false
 
-  /* jscpd:ignore-start -- parallel to code-runtime-worker: sibling backends keep symmetric constructor/teardown/run shapes. */
+  /* jscpd:ignore-start -- parallel to code-runtime-node: sibling backends keep symmetric constructor/teardown/run shapes. */
   constructor(ctx: Context, config: Config) {
     super(ctx)
     // Reject at load on Windows: the bootstrap imports the POSIX-only `resource`
@@ -924,7 +923,7 @@ export class PythonCodeRuntime extends CodeRuntime {
       // which silently floors a float, so `maxLogBytes: 3.5` would truncate at 3
       // bytes child-side while the host meters and marks at 3.5 — the two sides
       // enforcing different public config. Reject the float at load, as the
-      // worker backend does for its byte budgets.
+      // Node backend does for its byte budgets.
       if (!Number.isInteger(this.config[key])) {
         throw new Error(`dsh-code-runtime-python: config.${key} must be a positive integer (the child reads it as an int, so a float diverges from the host), got ${String(this.config[key])}`)
       }
@@ -1126,7 +1125,7 @@ export class PythonCodeRuntime extends CodeRuntime {
         }
         // Any non-empty own attribute name is settable via setattr (the
         // program reads exotic names like `tool-name` with getattr), matching
-        // the seam contract and the worker backend — only the seam-excluded
+        // the seam contract and the Node backend — only the seam-excluded
         // and protocol-reserved members below are refused.
         if (memberNameProperty.length === 0) {
           throw new Error('dsh-code-runtime-python: errorClass.memberNameProperty must be a non-empty attribute name')
@@ -1931,7 +1930,7 @@ export class PythonCodeRuntime extends CodeRuntime {
                 // The seam requires a lossy resolution to REJECT descriptively,
                 // not silently coerce: a raw JSON.stringify would turn NaN/
                 // Infinity into null and drop undefined fields. Snapshot through
-                // the same lossless-JSON boundary the worker backend uses (also
+                // the same lossless-JSON boundary the Node backend uses (also
                 // iterative, so a deeply nested value cannot overflow the stack).
                 const value = snapshotJsonValue(resolved)
                 if (value === undefined) {
@@ -2390,7 +2389,7 @@ export class PythonCodeRuntime extends CodeRuntime {
       child.stdout.on('error', silenceStreamError)
       child.stderr.on('error', silenceStreamError)
 
-      /* jscpd:ignore-start -- wall-timer/abort/live-run wiring deliberately parallels code-runtime-worker; see the constructor note. */
+      /* jscpd:ignore-start -- wall-timer/abort/live-run wiring deliberately parallels code-runtime-node; see the constructor note. */
       const wallTimer = setTimeout(() => {
         finish({ error: { kind: 'timeout', message: `wall-clock ceiling reached (${this.config.maxWallMs}ms)` } })
       }, this.config.maxWallMs)

+ 21 - 0
packages/experimental/code-runtime-python/tests/runtime.spec.ts

@@ -104,6 +104,27 @@ describe('PythonCodeRuntime — seam descriptors and misuse', () => {
     expect(runtime.isolation).toBe('process')
   })
 
+  it('resolves its configured deadline and cwd while refusing unsupported execution choices', async () => {
+    const { runtime, fiber } = await setup({ maxWallMs: 30_000 })
+    try {
+      const request = { program: 'return 1', bindings: [] }
+      expect(runtime.sandboxMode).toBeUndefined()
+      expect(runtime.resolve(request)).toEqual({ ...request, cwd: process.cwd(), timeoutMs: 30_000 })
+      const cwd = await makeTempDir('dsh-py-resolved-cwd-')
+      const spec = runtime.resolve({ ...request, cwd })
+      expect(spec.cwd).toBe(cwd)
+      expect(() => runtime.resolve({ ...request, cwd: 'relative' })).toThrow('cwd must be absolute')
+      expect(() => runtime.resolve({ ...request, timeoutMs: 1 })).toThrow('per-call timeout is unsupported')
+      const sandboxPolicy = { mode: 'danger-full-access' as const, workspaceRoot: cwd }
+      expect(() => runtime.resolve({ ...request, sandboxPolicy })).toThrow('sandbox policy is unsupported')
+      await expect(runtime.run({ ...spec, sandboxPolicy })).rejects.toThrow('unsupported execution policy or timeout')
+      await expect(runtime.run({ ...spec, timeoutMs: 1 })).rejects.toThrow('unsupported execution policy or timeout')
+      const result = await runtime.run(runtime.resolve({ ...request, cwd, program: 'import os\nreturn os.getcwd()' }))
+      expect(result.error).toBeUndefined()
+      expect(result.value).toBe(realpathSync(cwd))
+    } finally { await fiber.dispose() }
+  })
+
   it('rejects non-positive config as seam misuse', async () => {
     const ctx = new Context()
     await expect(ctx.plugin(PythonCodeRuntime, { cpuSeconds: 0 }))

+ 22 - 7
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -658,10 +658,17 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         parameters: [],
       },
       {
-        signature: 'abstract run(request: CodeRunRequest): Promise<CodeRunResult>',
-        description: 'Execute one program against the request\'s bindings and capture what it emitted. See the class doc for the resolution contract (error is a result field; rejection means Service Definition contract misuse only).',
-        parameters: [{ name: 'request', description: 'the program, its bindings, and the abort signal; the request carries everything the runtime acts on, with no hidden defaults.' }],
-        returns: 'the run\'s outcome: completion value (when transferable), the ordered log capture, and the failure (if any).',
+        signature: 'abstract resolve(request: CodeRunRequest): CodeRunSpec',
+        description: 'Resolve supported options and provider defaults before execution.',
+        parameters: [{ name: 'request', description: 'Program, bindings, cancellation and optional execution choices.' }],
+        returns: 'Complete directory, deadline and supported authority for run.',
+        throws: ['When an explicit choice is invalid or unsupported by this provider.'],
+      },
+      {
+        signature: 'abstract run(spec: CodeRunSpec): Promise<CodeRunResult>',
+        description: 'Execute resolved inputs; program outcomes resolve as result fields.',
+        parameters: [{ name: 'spec', description: 'directory, deadline, program, bindings, cancellation and supported policy.' }],
+        returns: 'Captured output and the execution outcome.',
       },
     ],
   },
@@ -3808,15 +3815,23 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'CodeRunFailure',
-    declaration: 'export interface CodeRunFailure {\n    kind: \'exception\' | \'timeout\' | \'abort\' | \'worker-exit\' | \'invalid-output\' | \'output-limit\';\n    message: string;\n}',
+    declaration: 'export interface CodeRunFailure {\n    kind: \'exception\' | \'timeout\' | \'abort\' | \'worker-exit\' | \'invalid-output\' | \'output-limit\' | \'protocol\' | \'sandbox-unavailable\';\n    message: string;\n}',
   },
   {
     name: 'CodeRunRequest',
-    declaration: 'export interface CodeRunRequest {\n    program: string;\n    bindings: CodeBindingNamespace[];\n    signal?: AbortSignal;\n}',
+    declaration: 'export interface CodeRunRequest {\n    program: string;\n    bindings: CodeBindingNamespace[];\n    cwd?: string;\n    timeoutMs?: number;\n    sandboxPolicy?: SandboxExecutionPolicy;\n    signal?: AbortSignal;\n}',
   },
   {
     name: 'CodeRunResult',
-    declaration: 'export interface CodeRunResult {\n    value?: CodeJsonValue;\n    logs: string[];\n    error?: CodeRunFailure;\n}',
+    declaration: 'export interface CodeRunResult {\n    sandbox?: CodeRunSandbox;\n    value?: CodeJsonValue;\n    logs: string[];\n    error?: CodeRunFailure;\n}',
+  },
+  {
+    name: 'CodeRunSandbox',
+    declaration: 'export interface CodeRunSandbox {\n    mode: SandboxMode;\n    denied: boolean;\n    enforcement?: SandboxEnforcement;\n}',
+  },
+  {
+    name: 'CodeRunSpec',
+    declaration: 'export interface CodeRunSpec extends CodeRunRequest {\n    cwd: string;\n    timeoutMs: number;\n}',
   },
   {
     name: 'CollectedOutput',

+ 2 - 0
scripts/check-workspace-constraints.ts

@@ -161,6 +161,8 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
   '@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
   // The CPython side ships as source .py files, published as-is rather than built.
   '@deepseek-ai/dsh-experimental-code-runtime-python': ['py/**/*.py'],
+  // The isolated Node bootstrap is a separately launched bundle.
+  '@deepseek-ai/dsh-code-runtime-node': ['lib/process.js'],
   // The shipped preset compositions travel inside the roster package.
   '@deepseek-ai/dsh-agent-presets': ['presets'],
   // The Web Host mounts the default-off settings owner independently of each

+ 2 - 0
scripts/gen-cordis-catalog.ts

@@ -396,6 +396,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
   SubprocessTerminalHandle: 'subprocess.md',
   SubprocessTerminalSpawnSpec: 'subprocess.md',
   CodeRunRequest: 'code-runtime.md',
+  CodeRunSpec: 'code-runtime.md',
+  CodeRunSandbox: 'code-runtime.md',
   CodeRunResult: 'code-runtime.md',
   CompactionResult: 'compaction.md',
   CompactionTrigger: 'compaction.md',