ソースを参照

fix(subprocess): move SEA bootstrap to runtime packaging

pku-xht 2 週間 前
コミット
e5dbccce8c
22 ファイル変更125 行追加64 行削除
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
  2. 0 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
  3. 0 0
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml
  8. 1 1
      .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md
  9. 1 1
      .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md
  10. 2 2
      .agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.i18n.yaml
  11. 1 1
      .agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.md
  12. 1 1
      .agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.zh.md
  13. 1 1
      apps/cli/package.json
  14. 38 25
      apps/cli/src/bin.ts
  15. 0 3
      apps/cli/tsconfig.json
  16. 4 6
      apps/cli/tsdown.config.ts
  17. 3 3
      pnpm-lock.yaml
  18. 3 6
      python/sdk-runtime/runtime-bootstrap.mjs
  19. 28 1
      scripts/build-exe-for-python-sdk.spec.ts
  20. 22 3
      scripts/build-exe-for-python-sdk.ts
  21. 9 0
      scripts/verify-application-entrypoints.spec.ts
  22. 3 2
      scripts/verify-application-entrypoints.ts

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: 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

ファイルの差分が大きいため隠しています
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md


ファイルの差分が大きいため隠しています
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md


+ 2 - 2
.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

+ 1 - 1
.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-<platform>-<arch>`. 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.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md

@@ -46,7 +46,7 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile <name> ...` 管
 
 ### 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-<platform>-<arch>`。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)负责第四个载体。
 

+ 2 - 2
.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

+ 1 - 1
.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-<platform>-<arch>`. 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-<platform>-<arch>`. 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.
 

+ 1 - 1
.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-<platform>-<arch>`。没有私有选择值时,这个由打包层拥有的 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-<platform>-<arch>`。没有私有选择值时,这个由 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 和每个内置模块的单一实例。
 

+ 2 - 2
.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

+ 1 - 1
.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.
 

+ 1 - 1
.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 状态,并且只在私有选择值消费后才恢复该状态。
 

+ 1 - 1
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:^",

+ 38 - 25
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<void> {
+  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
-  }
-  case 'plugin': {
-    const { runPlugin } = await import('./plugin.ts')
-    process.exit(runPlugin(invocation.profile, 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 '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()
 }

+ 0 - 3
apps/cli/tsconfig.json

@@ -70,9 +70,6 @@
     },
     {
       "path": "../../packages/shell/tool-bash"
-    },
-    {
-      "path": "../../packages/subprocess/subprocess-local"
     }
   ]
 }

+ 4 - 6
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',

+ 3 - 3
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

+ 3 - 6
apps/cli/src/runtime-bootstrap.ts → 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')

+ 28 - 1
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<string, string>
+      devDependencies?: Record<string, string>
+    }
+    const runtimeManifest = JSON.parse(readFileSync(resolve(root, 'python/sdk-runtime/package.json'), 'utf8')) as {
+      dependencies?: Record<string, string>
+    }
+
+    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)
   })

+ 22 - 3
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<void> {
+    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<string, unknown>
     await writeFile(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`)
@@ -613,6 +631,7 @@ async function main(): Promise<void> {
   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))

+ 9 - 0
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 }))

+ 3 - 2
scripts/verify-application-entrypoints.ts

@@ -29,10 +29,9 @@ const MANIFEST_BIN_ALLOWLIST = new Map<string, ManifestBin>([
   ['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<string, string>([
   ['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<string, string>([
   ['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 = [

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません