diff --git a/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts b/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts
index 3fc18ad818..da0c857eb4 100644
--- a/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts
+++ b/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, `
diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml
index 692f58e674..e96e86e583 100644
--- a/docs/capability-seams.i18n.yaml
+++ b/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
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index 4e6ed70df3..2fbae75691 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -163,7 +163,7 @@ flowchart LR
svc_permissionPresets["ctx.permissionPresets
Permission presets"]
pkg_code_runtime["code-runtime"]
svc_codeRuntime["ctx.codeRuntime
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
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
diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md
index 9ccf2db027..2e9c785f05 100644
--- a/docs/capability-seams.zh.md
+++ b/docs/capability-seams.zh.md
@@ -165,7 +165,7 @@ flowchart LR
svc_permissionPresets["ctx.permissionPresets
Permission presets"]
pkg_code_runtime["code-runtime"]
svc_codeRuntime["ctx.codeRuntime
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
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
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index bd12f0af73..bf047f88a3 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/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
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 1db8a25032..da6b091110 100644
--- a/docs/config-catalog.md
+++ b/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)
@@ -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)
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index 8d478cc0f2..cc2bd99c13 100644
--- a/docs/config-catalog.zh.md
+++ b/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)
@@ -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)
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index 8e5c8e385b..c6bcf0f659 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/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
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index 239b93480a..a176adbb35 100644
--- a/docs/event-producer-consumer.md
+++ b/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) |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index 94ad41bfed..a42ad92f7d 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/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) |
diff --git a/docs/subsystems/code-runtime.i18n.yaml b/docs/subsystems/code-runtime.i18n.yaml
index 17ee0a4c32..af6869f659 100644
--- a/docs/subsystems/code-runtime.i18n.yaml
+++ b/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
diff --git a/docs/subsystems/code-runtime.md b/docs/subsystems/code-runtime.md
index f807bec997..6f01cc446b 100644
--- a/docs/subsystems/code-runtime.md
+++ b/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
+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
```
Source: [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)
diff --git a/docs/subsystems/code-runtime.zh.md b/docs/subsystems/code-runtime.zh.md
index b2d725ed6f..9f09e3019c 100644
--- a/docs/subsystems/code-runtime.zh.md
+++ b/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
+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
```
Source: [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)
diff --git a/packages/code-runtime/code-runtime-node/src/bootstrap.ts b/packages/code-runtime/code-runtime-node/src/bootstrap.ts
index acf876edb7..26371745e2 100644
--- a/packages/code-runtime/code-runtime-node/src/bootstrap.ts
+++ b/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 { 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) })
}
diff --git a/packages/code-runtime/code-runtime-node/src/json-wire.ts b/packages/code-runtime/code-runtime-node/src/json-wire.ts
index b2f750c7c6..a0578e62be 100644
--- a/packages/code-runtime/code-runtime-node/src/json-wire.ts
+++ b/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.
diff --git a/packages/code-runtime/code-runtime-node/src/launch.ts b/packages/code-runtime/code-runtime-node/src/launch.ts
index ade86dbfa8..ab6646c491 100644
--- a/packages/code-runtime/code-runtime-node/src/launch.ts
+++ b/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)]
}
diff --git a/packages/code-runtime/code-runtime-node/src/process.ts b/packages/code-runtime/code-runtime-node/src/process.ts
index 616a91ab1a..b9b81deb7a 100644
--- a/packages/code-runtime/code-runtime-node/src/process.ts
+++ b/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
}
}
diff --git a/packages/code-runtime/code-runtime-node/tests/channel.spec.ts b/packages/code-runtime/code-runtime-node/tests/channel.spec.ts
index 511eaa9f2f..4f074f840b 100644
--- a/packages/code-runtime/code-runtime-node/tests/channel.spec.ts
+++ b/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()
- 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()
- 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()
- 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()
- 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)
diff --git a/packages/code-runtime/code-runtime-node/tests/host-failures.spec.ts b/packages/code-runtime/code-runtime-node/tests/host-failures.spec.ts
index 43faf1a6df..59e62be2fb 100644
--- a/packages/code-runtime/code-runtime-node/tests/host-failures.spec.ts
+++ b/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()
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>()
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()
+ 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)
+ }
+ })
})
diff --git a/packages/code-runtime/code-runtime-node/tests/process-main.spec.ts b/packages/code-runtime/code-runtime-node/tests/process-main.spec.ts
index 8ac0681481..d9f6efecba 100644
--- a/packages/code-runtime/code-runtime-node/tests/process-main.spec.ts
+++ b/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)
})
diff --git a/packages/code-runtime/code-runtime-node/tests/process.spec.ts b/packages/code-runtime/code-runtime-node/tests/process.spec.ts
index 74f25280ba..99561cad36 100644
--- a/packages/code-runtime/code-runtime-node/tests/process.spec.ts
+++ b/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((resolve) => { child.once('close', () => { resolve() }) })
const completed = Promise.withResolvers()
- 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
diff --git a/packages/code-runtime/code-runtime-node/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-node/tests/runtime.spec.ts
index 089bdc8406..8714fcd221 100644
--- a/packages/code-runtime/code-runtime-node/tests/runtime.spec.ts
+++ b/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()
+ const disconnected = Promise.withResolvers()
+ 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((resolve, reject) => {
+ server.close((error) => { if (error) reject(error); else resolve() })
+ })
+ })
+ await new Promise((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: [] })
diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts
index a7e58a78c0..3cf90313bd 100644
--- a/packages/code-runtime/code-runtime/src/index.ts
+++ b/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
diff --git a/packages/code-runtime/code-runtime/tests/service.spec.ts b/packages/code-runtime/code-runtime/tests/service.spec.ts
index 3b4685d37d..7e1ac04f47 100644
--- a/packages/code-runtime/code-runtime/tests/service.spec.ts
+++ b/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({
diff --git a/packages/experimental/code-runtime-python/src/index.ts b/packages/experimental/code-runtime-python/src/index.ts
index 186c9c2102..9e2b461ac1 100644
--- a/packages/experimental/code-runtime-python/src/index.ts
+++ b/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()
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)
diff --git a/packages/experimental/code-runtime-python/tests/runtime.spec.ts b/packages/experimental/code-runtime-python/tests/runtime.spec.ts
index b2861b0a6a..2780004a8b 100644
--- a/packages/experimental/code-runtime-python/tests/runtime.spec.ts
+++ b/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 }))
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index bb40e5ddc7..c0760c6396 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/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',
- 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',
+ 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',
diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts
index b84797e22e..104f02decc 100644
--- a/scripts/check-workspace-constraints.ts
+++ b/scripts/check-workspace-constraints.ts
@@ -161,6 +161,8 @@ const packageFileExtras: Readonly> = {
'@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
diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts
index 4b571544db..340b2f2093 100644
--- a/scripts/gen-cordis-catalog.ts
+++ b/scripts/gen-cordis-catalog.ts
@@ -396,6 +396,8 @@ export const LINK_MAP: Readonly> = {
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',