From fc5cb0f649c16bd3319cf4b1dec32d67be0984eb Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:18:48 +0800 Subject: [PATCH] test(code-runtime): complete sandboxed process coverage and catalogs --- .../tests/profiles/headless/tests/ptc.e2e.ts | 2 +- docs/capability-seams.i18n.yaml | 4 +- docs/capability-seams.md | 4 +- docs/capability-seams.zh.md | 4 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 56 ++++----- docs/config-catalog.zh.md | 56 ++++----- docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 12 +- docs/event-producer-consumer.zh.md | 12 +- docs/subsystems/code-runtime.i18n.yaml | 4 +- docs/subsystems/code-runtime.md | 20 +-- docs/subsystems/code-runtime.zh.md | 20 +-- .../code-runtime-node/src/bootstrap.ts | 12 +- .../code-runtime-node/src/channel.ts | 2 + .../code-runtime-node/src/index.ts | 14 ++- .../code-runtime-node/src/json-wire.ts | 20 +-- .../code-runtime-node/src/launch.ts | 1 + .../code-runtime-node/src/process.ts | 2 + .../code-runtime-node/tests/channel.spec.ts | 20 +-- .../tests/host-failures.spec.ts | 116 ++++++++++++++++-- .../tests/process-main.spec.ts | 6 +- .../code-runtime-node/tests/process.spec.ts | 10 +- .../code-runtime-node/tests/runtime.spec.ts | 47 +++++++ .../code-runtime/code-runtime/src/index.ts | 6 +- .../code-runtime/tests/service.spec.ts | 1 + .../code-runtime-python/src/index.ts | 17 ++- .../code-runtime-python/tests/runtime.spec.ts | 21 ++++ .../extensions/tool-cordis/src/api-catalog.ts | 29 +++-- scripts/check-workspace-constraints.ts | 2 + scripts/gen-cordis-catalog.ts | 2 + 31 files changed, 361 insertions(+), 169 deletions(-) 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',