From e5dbccce8c2122fd8ec24b1cb809847a40e557fb Mon Sep 17 00:00:00 2001 From: pku-xht Date: Thu, 3 Sep 2026 19:59:26 +0800 Subject: [PATCH] fix(subprocess): move SEA bootstrap to runtime packaging --- ...cutable-sdk-runtime-distribution.i18n.yaml | 4 +- ...ile-executable-sdk-runtime-distribution.md | 2 +- ...-executable-sdk-runtime-distribution.zh.md | 2 +- ...-single-dsh-application-launcher.i18n.yaml | 4 +- ...6-08-22-single-dsh-application-launcher.md | 2 +- ...8-22-single-dsh-application-launcher.zh.md | 2 +- ...3-python-sdk-dsh-profile-runtime.i18n.yaml | 4 +- ...26-08-23-python-sdk-dsh-profile-runtime.md | 2 +- ...08-23-python-sdk-dsh-profile-runtime.zh.md | 2 +- ...28-subprocess-native-containment.i18n.yaml | 4 +- ...026-08-28-subprocess-native-containment.md | 2 +- ...-08-28-subprocess-native-containment.zh.md | 2 +- apps/cli/package.json | 2 +- apps/cli/src/bin.ts | 63 +++++++++++-------- apps/cli/tsconfig.json | 3 - apps/cli/tsdown.config.ts | 10 ++- pnpm-lock.yaml | 6 +- .../sdk-runtime/runtime-bootstrap.mjs | 9 +-- scripts/build-exe-for-python-sdk.spec.ts | 29 ++++++++- scripts/build-exe-for-python-sdk.ts | 25 +++++++- .../verify-application-entrypoints.spec.ts | 9 +++ scripts/verify-application-entrypoints.ts | 5 +- 22 files changed, 127 insertions(+), 66 deletions(-) rename apps/cli/src/runtime-bootstrap.ts => python/sdk-runtime/runtime-bootstrap.mjs (63%) diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index 16f0c9fc87..aa877ef599 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: f1e8d6ce56d4d8eb49ec0fd7218885234778f9f0 -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 852dcedeae11e253b23b57efbd6526309d9fcdaf +2026-07-10-single-file-executable-sdk-runtime-distribution.md: 6ab54fe40007808cc8b9a3c89061738df647a6dd +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 2dde9b51c456b234b628f0460e54816b9fd32faf diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index f1e8d6ce56..6ab54fe400 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -44,7 +44,7 @@ The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supporte ### Build pipeline and artifacts -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject pkg configuration whose bin is `node_modules/@deepseek-ai/dsh/lib/runtime-bootstrap.js` and whose assets cover dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime--` under `dist-exe/` and copy it into the runtime directory. The packaging-owned bootstrap imports the public CLI for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → stage [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime--` under `dist-exe/` and copy it into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all four targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS verifies that the executable's deployment target fits the wheel tag. A full four-target run retains five artifacts, each containing one release file: the platform-independent SDK wheel and four native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and four native runtime wheels, then a single serialized job checks and publishes all five to the project PyPI registry. The [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth target and the explicit exclusion of Windows arm64. diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index 852dcedeae..2dde9b51c4 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -44,7 +44,7 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真 ### 构建流水线与产物 -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置,其中 bin 为 `node_modules/@deepseek-ai/dsh/lib/runtime-bootstrap.js`,assets 覆盖动态读取的 profile、bundle、前端、preset、原生库与配置文件 → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/` 并拷回运行时目录。由打包层拥有的 bootstrap 在普通启动时导入公开 CLI,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 决策](2026-08-28-subprocess-native-containment.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为载体根目录的 `runtime-bootstrap.mjs`,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/` 并拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 决策](2026-08-28-subprocess-native-containment.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部四个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64 与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建四个目标时保留 5 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 4 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 4 个原生运行时 wheel 包,再由单个串行任务校验并将这 5 个文件发布到项目的 PyPI 注册表。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个目标及对 Windows arm64 的明确排除。 diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml index 6d05137fba..3c8447434f 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md -2026-08-22-single-dsh-application-launcher.md: 352b6076191b03493f75df469b7f7fd8e3df098a -2026-08-22-single-dsh-application-launcher.zh.md: ae3b8c1984f973560a083bc046d868058db8d9ef +2026-08-22-single-dsh-application-launcher.md: 66749e2e23e638243e9238e4fa7012199a425e97 +2026-08-22-single-dsh-application-launcher.zh.md: 6948a80b2c8c03755eec3bd97fd70d6d96cea213 diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md index 352b607619..66749e2e23 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md @@ -46,7 +46,7 @@ Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inher ### Python runtime -The Python runtime wheel packages `node_modules/@deepseek-ai/dsh/lib/runtime-bootstrap.js` through the private `dsh-python-runtime-closure` deploy manifest. Its ordinary branch imports the public CLI; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. The [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar and the separately packaged `web` application. +The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. The [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar and the separately packaged `web` application. The executable family is `deepseek-harness-sdk-runtime--`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. The [Python profile-runtime decision](2026-08-23-python-sdk-dsh-profile-runtime.md) owns this launch, and the [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth carrier. diff --git a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md index ae3b8c1984..6948a80b2c 100644 --- a/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md @@ -46,7 +46,7 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile ...` 管 ### Python 运行时 -Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest 打包 `node_modules/@deepseek-ai/dsh/lib/runtime-bootstrap.js`。其普通分支导入公开 CLI;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。[原生 containment 决策](2026-08-28-subprocess-native-containment.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用。 +Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。[原生 containment 决策](2026-08-28-subprocess-native-containment.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用。 可执行文件族是 `deepseek-harness-sdk-runtime--`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。[Python profile 运行时决策](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责该启动方式,[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个载体。 diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml index d649a92344..30b0013c24 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-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 .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md -2026-08-23-python-sdk-dsh-profile-runtime.md: e6dbe4c5a81093201edfb476a8f1b9594a923903 -2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 888fcb097d592d295df88c56f11cca3e4dec81c2 +2026-08-23-python-sdk-dsh-profile-runtime.md: 89aa19bfbf20ceb9a5b82cbca450df8057e28a81 +2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 9095831952656087dea6cbfb3f2158cf3e6623be diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md index e6dbe4c5a8..89aa19bfbf 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md @@ -30,7 +30,7 @@ The runtime wheel installs a `dsh` console command. Ordinary profile and SDK exe ### Executable packaging -The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/runtime-bootstrap.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime--`. That packaging-owned bootstrap imports the ordinary public CLI when no private selection is present; for a selected subprocess runner it consumes the one private environment value and enters `@deepseek-ai/dsh-subprocess-local/runner` without parsing a hidden CLI argument or adding a second Node executable. The [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private dispatch and runner protocol. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged. +The zero-code deployment manifest is `dsh-python-runtime-closure`. The build stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) with profile, bundle, preset, native-addon, and shared-library assets in `deepseek-harness-sdk-runtime--`. That Python runtime-owned bootstrap calls the ordinary public CLI export when no private selection is present; for a selected subprocess runner it consumes the one private environment value and enters `@deepseek-ai/dsh-subprocess-local/runner` without parsing a hidden CLI argument or adding a second Node executable. The [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private dispatch and runner protocol. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged. Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. A complete matching generation returns without acquiring the cross-process writer lock. A missing or stale entry acquires the lock, rechecks the generation, and repairs it without exposing partial proxies; either carrier can replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module. diff --git a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md index 888fcb097d..9095831952 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md @@ -30,7 +30,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml ### 可执行程序打包 -零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/runtime-bootstrap.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime--`。没有私有选择值时,这个由打包层拥有的 bootstrap 会导入普通公共 CLI;选择了 subprocess runner 时,它会消费唯一的私有环境值并进入 `@deepseek-ai/dsh-subprocess-local/runner`,不解析隐藏 CLI 参数,也不增加第二个 Node 可执行程序。[原生收容决策](2026-08-28-subprocess-native-containment.zh.md)拥有这项私有分派与 runner 协议。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。 +零代码部署 manifest 是 `dsh-python-runtime-closure`。构建流程将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 连同 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime--`。没有私有选择值时,这个由 Python runtime 拥有的 bootstrap 会调用普通公共 CLI export;选择了 subprocess runner 时,它会消费唯一的私有环境值并进入 `@deepseek-ai/dsh-subprocess-local/runner`,不解析隐藏 CLI 参数,也不增加第二个 Node 可执行程序。[原生收容决策](2026-08-28-subprocess-native-containment.zh.md)拥有这项私有分派与 runner 协议。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。 普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。完整且匹配的 generation 不会获取跨进程写入锁。缺失或过期的配置项会获取该锁、重新检查 generation,并在不暴露半成品代理的前提下修复;任一载体都可以替换另一载体留下的受管配置项。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。 diff --git a/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.i18n.yaml index cfb6233913..d8cb5bd69d 100644 --- a/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.md -2026-08-28-subprocess-native-containment.md: 108ace2d42cbc7b5286818b420aef584dc3b515a -2026-08-28-subprocess-native-containment.zh.md: e945d3160cfe543736846758365cbdd0cb605c52 +2026-08-28-subprocess-native-containment.md: 078665c5c9067ff73b2ab25314cb3d430246b695 +2026-08-28-subprocess-native-containment.zh.md: 91ed7a34140172a1b56ab35bb71743d98208cf19 diff --git a/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.md b/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.md index 108ace2d42..078665c5c9 100644 --- a/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.md +++ b/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.md @@ -36,7 +36,7 @@ The parent permanently latches a validated numeric `target-exit` as soon as it a ### Private dispatch and protocol -Source launches execute the package runner entry through the TypeScript source launcher, built launches resolve the `@deepseek-ai/dsh-subprocess-local/runner` export, and the Python SDK single-file executable enters through `@deepseek-ai/dsh`'s packaging-owned `runtime-bootstrap.js`. That bootstrap imports the public CLI when the private selector is absent; otherwise it removes the selector and dispatches to the same subprocess runner core. The public `dsh` argument parser has no hidden runner mode, and packaging ships no second Node executable. +Source launches execute the package runner entry through the TypeScript source launcher, built launches resolve the `@deepseek-ai/dsh-subprocess-local/runner` export, and the Python SDK single-file executable enters through [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs). The Python runtime owns that bootstrap. It calls the public CLI export when the private selector is absent; otherwise it removes the selector and dispatches to the same subprocess runner core. The public `dsh` argument parser has no hidden runner mode, `apps/cli` builds only its public entry, and packaging ships no second Node executable. The selector is a per-spawn locator or sentinel, not a credential or persistent format. Linux uses one strict request plus one optional strict startup-error file. Windows uses one IPC channel with closed `start` and `terminate` requests and exactly two result branches: `target-exit` with a numeric `exitCode`, and `error` with required `name` and `message` plus only optional `code`, `syscall`, and `path`; the parent derives `signal: null`. Pre-commit cancellation uses the same ordinary `error` record. The cancellation reason never crosses the wire, so a parent cancellation latch restores its first local reason exactly, including `null` or `undefined`. Missing, extra, mistyped, or unknown fields fail closed. Target environments may contain the selector name, including Windows case variants, because the provider transmits target state separately and restores it only after private selection is consumed. diff --git a/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.zh.md b/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.zh.md index e945d3160c..91ed7a3414 100644 --- a/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.zh.md @@ -36,7 +36,7 @@ parent 会在收到经过校验、只含数字的 `target-exit` 时立即永久 ### 私有分派与协议 -source 启动通过 TypeScript source launcher 执行包内 runner 入口,built 启动解析 `@deepseek-ai/dsh-subprocess-local/runner` export,Python SDK 单文件可执行程序则从 `@deepseek-ai/dsh` 由打包层拥有的 `runtime-bootstrap.js` 进入。私有 selector 不存在时,该 bootstrap 导入公共 CLI;否则会删除 selector,并分派到同一 subprocess runner core。公共 `dsh` 参数解析器没有隐藏 runner mode,打包也不提供第二个 Node 可执行程序。 +source 启动通过 TypeScript source launcher 执行包内 runner 入口,built 启动解析 `@deepseek-ai/dsh-subprocess-local/runner` export,Python SDK 单文件可执行程序则从 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 进入。该 bootstrap 由 Python runtime 拥有;私有 selector 不存在时,它调用公共 CLI export;否则会删除 selector,并分派到同一 subprocess runner core。公共 `dsh` 参数解析器没有隐藏 runner mode,`apps/cli` 只构建公共入口,打包也不提供第二个 Node 可执行程序。 selector 是 per-spawn locator 或 sentinel,不是凭据或持久格式。Linux 使用一个严格 request 与一个可选严格 startup-error 文件。Windows 使用一条 IPC channel,承载闭集的 `start` 与 `terminate` request,以及恰好两个 result 分支:只含数字 `exitCode` 的 `target-exit`,以及必含 `name`、`message` 且只允许可选 `code`、`syscall`、`path` 的 `error`;parent 会派生 `signal: null`。提交前取消使用同一种普通 `error` record。取消 reason 不跨 wire 传递,因此 parent cancellation latch 会原样恢复第一个本地 reason,包括 `null` 或 `undefined`。缺失、额外、类型错误或未知字段都会 fail closed。target 环境可以包含 selector 名称及其 Windows 大小写变体,因为 provider 会单独传递 target 状态,并且只在私有选择值消费后才恢复该状态。 diff --git a/apps/cli/package.json b/apps/cli/package.json index e496d70071..9a17e1dcfb 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -67,7 +67,6 @@ "@deepseek-ai/dsh-session-reference": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-skill-filesystem": "workspace:^", - "@deepseek-ai/dsh-subprocess-local": "workspace:^", "@deepseek-ai/dsh-terminal": "workspace:^", "@deepseek-ai/dsh-terminal-bash": "workspace:^", "@deepseek-ai/dsh-time-context": "workspace:^", @@ -136,6 +135,7 @@ "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-subagent-fork-in-process": "workspace:^", "@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^", + "@deepseek-ai/dsh-subprocess-local": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-user-approval": "workspace:^", diff --git a/apps/cli/src/bin.ts b/apps/cli/src/bin.ts index 321849f2d9..9659f76861 100644 --- a/apps/cli/src/bin.ts +++ b/apps/cli/src/bin.ts @@ -7,7 +7,8 @@ /* v8 ignore file -- built-bin acceptance exercises this self-executing dispatch. */ import { readFileSync } from 'node:fs' -import { fileURLToPath } from 'node:url' +import { resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' import { loadLayeredEnv } from '@deepseek-ai/dsh-app-boot' import { parseDshArgs } from './args.ts' @@ -21,30 +22,42 @@ function readVersion(): string { return typeof manifest.version === 'string' ? manifest.version : '0.0.0' } -const invocation = parseDshArgs(process.argv.slice(2), readVersion()) +/** + * Run the public dsh command-line interface. + * @param argv - user arguments after the executable name. + * @returns a promise that settles when the selected command mode finishes. + */ +export async function runCli(argv: string[] = process.argv.slice(2)): Promise { + const invocation = parseDshArgs(argv, readVersion()) -switch (invocation.mode) { - case 'profile': { - const { runProfile } = await import('./profile-boot.ts') - await runProfile({ - environment: loadLayeredEnv('dsh'), - profile: invocation.profile, - patchFiles: invocation.patches, - args: invocation.args, - }) - break + switch (invocation.mode) { + case 'profile': { + const { runProfile } = await import('./profile-boot.ts') + await runProfile({ + environment: loadLayeredEnv('dsh'), + profile: invocation.profile, + patchFiles: invocation.patches, + args: invocation.args, + }) + break + } + case 'plugin': { + const { runPlugin } = await import('./plugin.ts') + process.exit(runPlugin(invocation.profile, invocation.args)) + break + } + case 'dump-config': { + const { runDumpConfig } = await import('./dump-config.ts') + runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches) + break + } + default: + invocation satisfies never + throw new Error(`dsh: unhandled invocation mode ${JSON.stringify(invocation)}`) } - case 'plugin': { - const { runPlugin } = await import('./plugin.ts') - process.exit(runPlugin(invocation.profile, invocation.args)) - break - } - case 'dump-config': { - const { runDumpConfig } = await import('./dump-config.ts') - runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches) - break - } - default: - invocation satisfies never - throw new Error(`dsh: unhandled invocation mode ${JSON.stringify(invocation)}`) +} + +const invokedPath = process.argv[1] +if (invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href) { + await runCli() } diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 80522294c7..7b0a769721 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -70,9 +70,6 @@ }, { "path": "../../packages/shell/tool-bash" - }, - { - "path": "../../packages/subprocess/subprocess-local" } ] } diff --git a/apps/cli/tsdown.config.ts b/apps/cli/tsdown.config.ts index 4fb14b0646..51dec0dc6c 100644 --- a/apps/cli/tsdown.config.ts +++ b/apps/cli/tsdown.config.ts @@ -1,15 +1,13 @@ import { defineConfig } from 'tsdown' /** - * The public package bin remains `bin`; `runtime-bootstrap` is selected only - * by the Python single-file packaging pipeline. + * The dsh CLI ships one entry: the `bin` referenced by package.json `bin`. + * The root tsdown builds only `lib/types/index.js`, so this override points at + * `lib/types/bin.js` instead; its reachable mode modules bundle with it. * Declarations come from `tsc -b` (dts: false), matching every package. */ export default defineConfig({ - entry: { - bin: 'lib/types/bin.js', - 'runtime-bootstrap': 'lib/types/runtime-bootstrap.js', - }, + entry: ['lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c85328a215..a726e4225f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,9 +244,6 @@ importers: '@deepseek-ai/dsh-skill-filesystem': specifier: workspace:^ version: link:../../packages/skill/skill-filesystem - '@deepseek-ai/dsh-subprocess-local': - specifier: workspace:^ - version: link:../../packages/subprocess/subprocess-local '@deepseek-ai/dsh-terminal': specifier: workspace:^ version: link:../../packages/terminal/terminal @@ -446,6 +443,9 @@ importers: '@deepseek-ai/dsh-subagent-spawn-in-process': specifier: workspace:^ version: link:../../packages/subagent/subagent-spawn-in-process + '@deepseek-ai/dsh-subprocess-local': + specifier: workspace:^ + version: link:../../packages/subprocess/subprocess-local '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../packages/core/system-prompt diff --git a/apps/cli/src/runtime-bootstrap.ts b/python/sdk-runtime/runtime-bootstrap.mjs similarity index 63% rename from apps/cli/src/runtime-bootstrap.ts rename to python/sdk-runtime/runtime-bootstrap.mjs index b45fc89f2e..bc2fbede75 100644 --- a/apps/cli/src/runtime-bootstrap.ts +++ b/python/sdk-runtime/runtime-bootstrap.mjs @@ -1,15 +1,12 @@ #!/usr/bin/env node -/** Packaging-only entry that keeps private runner dispatch outside the public CLI. */ - -/* v8 ignore file -- packaged-runtime smoke exercises this physical entry. */ +/** Private entry owned by the Python single-file runtime packaging. */ const selectorName = 'DSH_SUBPROCESS_RUNNER' const selection = process.env[selectorName] -export {} - if (selection === undefined) { - await import('./bin.ts') + const { runCli } = await import('@deepseek-ai/dsh/lib/bin.js') + await runCli() } else { Reflect.deleteProperty(process.env, selectorName) const { runSelectedSubprocessRunner } = await import('@deepseek-ai/dsh-subprocess-local/runner') diff --git a/scripts/build-exe-for-python-sdk.spec.ts b/scripts/build-exe-for-python-sdk.spec.ts index 06c3f7fce3..0b2829d472 100644 --- a/scripts/build-exe-for-python-sdk.spec.ts +++ b/scripts/build-exe-for-python-sdk.spec.ts @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process' -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' @@ -23,6 +23,31 @@ function run(env: NodeJS.ProcessEnv, ...args: string[]) { } describe('Python runtime executable builder CLI', () => { + it('keeps the single-file dispatcher on the Python packaging surface', () => { + const bootstrapPath = resolve(root, 'python/sdk-runtime/runtime-bootstrap.mjs') + const bootstrap = readFileSync(bootstrapPath, 'utf8') + const cliConfig = readFileSync(resolve(root, 'apps/cli/tsdown.config.ts'), 'utf8') + const cliTsconfig = readFileSync(resolve(root, 'apps/cli/tsconfig.json'), 'utf8') + const cliManifest = JSON.parse(readFileSync(resolve(root, 'apps/cli/package.json'), 'utf8')) as { + dependencies?: Record + devDependencies?: Record + } + const runtimeManifest = JSON.parse(readFileSync(resolve(root, 'python/sdk-runtime/package.json'), 'utf8')) as { + dependencies?: Record + } + + expect(existsSync(resolve(root, 'apps/cli/src/runtime-bootstrap.ts'))).toBe(false) + expect(cliConfig).not.toContain('runtime-bootstrap') + expect(cliTsconfig).not.toContain('packages/subprocess/subprocess-local') + expect(cliManifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subprocess-local') + expect(cliManifest.devDependencies).toHaveProperty('@deepseek-ai/dsh-subprocess-local') + expect(runtimeManifest.dependencies).toHaveProperty('@deepseek-ai/dsh-subprocess-local') + expect(bootstrap).toContain("import('@deepseek-ai/dsh/lib/bin.js')") + expect(bootstrap).toContain('await runCli()') + expect(bootstrap).toContain("import('@deepseek-ai/dsh-subprocess-local/runner')") + expect(bootstrap).toContain('await runSelectedSubprocessRunner(selection)') + }) + it('runs pnpm through its JavaScript entrypoint without a command shell', () => { const result = run( { npm_execpath: 'C:\\tools\\pnpm.cjs' }, @@ -34,6 +59,8 @@ describe('Python runtime executable builder CLI', () => { expect(result.status).toBe(0) expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs run verify-runtime-closure`) expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs --filter dsh-python-runtime-closure deploy`) + expect(result.stdout).toContain('python/sdk-runtime/runtime-bootstrap.mjs') + expect(result.stdout).toContain('runtime/node/runtime-bootstrap.mjs') expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs exec pkg`) expect(result.stdout).not.toMatch(/pnpm\.cmd/i) }) diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index f26c4c8e91..c451e3ce00 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -17,8 +17,10 @@ const root = resolve(import.meta.dirname, '..') /** The closure manifest whose dependencies define the executable. */ const DEPLOY_ROOT_PACKAGE = 'dsh-python-runtime-closure' -/** The sole application launcher inside the deployed closure. */ -const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh/lib/runtime-bootstrap.js' +/** The Python runtime-owned source staged as the single-file entry. */ +const ENTRY_SOURCE = 'python/sdk-runtime/runtime-bootstrap.mjs' +/** The sole executable entry inside the deployed closure. */ +const ENTRY_BIN = 'runtime-bootstrap.mjs' /** Python-visible executable basename. */ const OUTPUT_BASENAME = 'deepseek-harness-sdk-runtime' /** Default Node major; SEA mode requires at least Node 22. */ @@ -306,6 +308,22 @@ class SingleExeBuild { } } + /** Copy the Python runtime-owned dispatcher into the deployed closure root. */ + async stageRuntimeBootstrap(): Promise { + const source = resolve(root, ENTRY_SOURCE) + const destination = join(this.staging, ENTRY_BIN) + if (!existsSync(source)) { + throw new Error(`build-exe-for-python-sdk: packaging bootstrap is missing at ${source}.`) + } + if (this.cli.dryRun) { + console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`) + return + } + await copyFile(source, destination) + await chmod(destination, 0o755) + console.log(`build-exe-for-python-sdk: staged ${destination}`) + } + /** * Restore direct packages that pnpm's legacy hoister places beside the deploy * source instead of in the target. The runtime manifest supplies every peer, @@ -406,7 +424,7 @@ class SingleExeBuild { throw new Error(`build-exe-for-python-sdk: ${manifestPath} missing — pnpm deploy did not produce a staged package.`) } if (!existsSync(join(this.staging, ENTRY_BIN))) { - throw new Error(`build-exe-for-python-sdk: ${join(this.staging, ENTRY_BIN)} missing — run without --skip-build so lib/ artifacts exist.`) + throw new Error(`build-exe-for-python-sdk: staged bootstrap ${join(this.staging, ENTRY_BIN)} is missing.`) } const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Record await writeFile(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`) @@ -613,6 +631,7 @@ async function main(): Promise { await pipeline.verifyClosure() await pipeline.build() await pipeline.deployStaging() + await pipeline.stageRuntimeBootstrap() await pipeline.injectPkgConfig() const products: string[] = [] for (const target of cli.targets) products.push(...await pipeline.pack(target)) diff --git a/scripts/verify-application-entrypoints.spec.ts b/scripts/verify-application-entrypoints.spec.ts index a03240eb1c..0c84043dfc 100644 --- a/scripts/verify-application-entrypoints.spec.ts +++ b/scripts/verify-application-entrypoints.spec.ts @@ -74,6 +74,15 @@ describe('application entrypoints', () => { ]) }) + it('rejects a packaging dispatcher owned by the CLI workspace', () => { + const root = fixture() + write(root, 'apps/cli/src/runtime-bootstrap.ts', '#!/usr/bin/env node\n') + + expect(applicationEntrypointViolations(root)).toEqual([ + 'apps/cli/src/runtime-bootstrap.ts: executable source has no application/build/test classification', + ]) + }) + it('rejects a private Python application carrier outside dsh', () => { const root = fixture() write(root, 'packages/sdk/rogue-python-runtime/package.json', JSON.stringify({ private: true })) diff --git a/scripts/verify-application-entrypoints.ts b/scripts/verify-application-entrypoints.ts index dbc63f5870..1ec5342b4b 100644 --- a/scripts/verify-application-entrypoints.ts +++ b/scripts/verify-application-entrypoints.ts @@ -29,10 +29,9 @@ const MANIFEST_BIN_ALLOWLIST = new Map([ ['packages/experimental/webworker-packer/package.json', { 'dsh-pack-vfs-image': './bin.js' }], ]) -/** Every executable in a Node application workspace has one explicit role. */ +/** Every JavaScript executable in an application or packaging workspace has one explicit role. */ const EXECUTABLE_SOURCE_ALLOWLIST = new Map([ ['apps/cli/src/bin.ts', 'supported dsh application launcher'], - ['apps/cli/src/runtime-bootstrap.ts', 'private packaging-only runtime dispatcher'], ['packages/context/time-context/tests/fixtures/driver.ts', 'test-only subprocess driver'], ['packages/experimental/webworker-packer/bin.js', 'private build-only wrapper'], ['packages/experimental/webworker-packer/src/bin.ts', 'private build-only implementation'], @@ -45,6 +44,7 @@ const EXECUTABLE_SOURCE_ALLOWLIST = new Map([ ['packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/driver.ts', 'test-only subprocess driver'], ['packages/test-support/loader-smoke/tests/fixtures/headless-driver.ts', 'test-only subprocess driver'], ['packages/test-support/llm-mock-server/src/bin.ts', 'test-only model server'], + ['python/sdk-runtime/runtime-bootstrap.mjs', 'private packaging-only runtime dispatcher'], ]) /** Root demos are application wrappers and therefore must visibly select dsh. */ @@ -66,6 +66,7 @@ const SOURCE_PATTERNS = [ 'packages/**/*.js', 'packages/**/*.mjs', 'packages/**/*.cjs', + 'python/sdk-runtime/*.mjs', ] const SOURCE_EXCLUDES = [