Single-file executable SDK runtime distribution (single-exe)
单文件可执行的 SDK 运行时分发(single-exe)
DeepSeek Harness needs a dedicated SDK distribution form for the Python library — no Node installation, runs directly on the target platform: a single-file executable (hereafter "the exe") that exposes a stdio JSON-RPC serving interface (`HarnessSdkJsonRpcServer`, the Python SDK's peer), where the plugins and configuration actually booted are decided entirely by a `cordis.yml` supplied from outside the exe. - The JSO
English
Problem
DeepSeek Harness needs a dedicated SDK distribution form for the Python library — no Node installation, runs directly on the target platform: a single-file executable (hereafter "the exe") that exposes a stdio JSON-RPC serving interface (HarnessSdkJsonRpcServer, the Python SDK's peer), where the plugins and configuration actually booted are decided entirely by a cordis.yml supplied from outside the exe.
- The JSONRPC protocol for talking to the Python SDK is already validated
- A standardized way for cordis.yml to load every plugin (ESModule) is needed
- The distribution must carry the Node runtime, and support a locally linked source debugging mode
Decision
Packaging route: @yao-pkg/pkg's --sea mode
The exe is packaged with the --sea (enhanced SEA) mode of @yao-pkg/pkg (the actively maintained fork after vercel/pkg was archived). Relative to Node's native SEA, pkg adds a /snapshot VFS and runtime module hooks on top, hands the ESM entry to Node's default ESM loader unchanged, and depends on no ESM→CJS transpilation.
Measured (macos-arm64, node24 target, pkg 6.21.0): bare-specifier ESM dynamic import inside the VFS (including top-level await), CJS interop,
node:sqlite, fail-loud on package names outside the set, and on-disk ESM import outside the VFS all pass;import.meta.urlcomes through unchanged asfile:///snapshot/....
--sea requires target ≥ node22; the exe uniformly targets node24. One pkg invocation packages exactly one target; multi-platform builds invoke it once per platform.
@yao-pkg/pkg is an exact-pinned root devDependency invoked as pnpm exec pkg, with patches/@[email protected] removing the SEA bootstrap's patchChildProcess call. Unpatched, pkg rewrites spawned commands named node — including the string after a -c//c flag, exactly the Bash tool's bash -c form — to the executable itself and stamps PKG_EXECPATH into every child environment, so a model-issued node --version silently boots the dsh CLI; Node's own SEA layer performs no such rewrite, and a SEA binary cannot impersonate plain Node because it always boots its embedded app. With the call removed, children resolve node through PATH like any other process (a machine without Node reports command-not-found honestly), no PKG_EXECPATH reaches children, absolute process.execPath spawns still re-enter the app, worker threads never applied the hook, and process.pkg sidecar selection is untouched.
The same patch makes the SEA bootstrap's native-addon extraction atomic. On the first dlopen of an addon inside the VFS, pkg's patchDlopen copies the addon's package tree to $PKG_NATIVE_CACHE_PATH/pkg/<sha256 of the .node file>/ (default ~/.cache/pkg/...), a per-user directory that every concurrent process of the same executable shares; upstream writes each file straight to its final path with writeFileSync, which truncates a file another process may already have mapped and lets that process's existsSync/hash check read a half-written file. Concurrent first starts therefore die with SIGBUS on Linux or fail with ERR_DLOPEN_FAILED: Library not loaded on sharp's libvips on macOS (#4664). The patch routes the three write sites through a same-directory temporary file and renameSync, so a cache path is either absent or complete, every process finishes its own copy pass before it loads, and concurrent renames of identical content need no lock. The cache location, hash keying, and per-file comparison are unchanged, so a consumer that isolates the cache per instance through PKG_NATIVE_CACHE_PATH keeps working; worker threads only define the hook and never install it. Measured (linux-x64, node24 target, pkg 6.21.0, a probe that loads sharp with its 18 MB libvips library, 16 concurrent first starts against one empty cache, five rounds): 39 of 80 processes died with SIGBUS before the change and 0 of 80 after it, with no temporary files left behind.
Terminology reminder: pkg's /snapshot VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay expected outputs, $DSH_SNAPSHOT); this document says "VFS" for the former.
The serving interface is a plugin inside the dsh application
The deterministic serving surface is a plugin selected by the packaged dsh application:
packages/sdk/server(@deepseek-ai/dsh-sdk-jsonrpc-server): the pure protocol plugin; on apply it mountsHarnessSdkJsonRpcServerplus a line-delimited JSON-RPC transport on the process stdio, with disposal throughctx.effect(). Whether to serve is decided bycordis.yml; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering and flushing theshutdownresponse it disposes the root runtime so persistence drains, thenexit(0); an HMR-style unload only stops the service without exiting the process).apps/cli(@deepseek-ai/dsh): the packaged application entry; itssdkprofile mountsdsh-sdk-jsonrpc-server, and the CLI owns environment layering, profile composition, stdin/signal shutdown, and process exit.
The Python client supplies an explicit Harness home and selects the sdk profile plus ordered patch files. A missing home, profile, bundle, or server row fails loudly; there is no external complete-config fallback. docs/architecture.md owns this application surface.
Plugin resolution: the VFS holds a real package tree, the closure manifest IS the deploy root
Inside the exe's VFS sits a real package tree in build-artifact form (each package's lib/ plus a real node_modules). The packaged JSON-RPC entry supplies its installed harness base to app-boot's root Include: relative plugin specifiers resolve from the external configuration directory, while bare package names resolve from the VFS, so a configuration inside another Node project cannot shadow the packaged plugin set. The ordinary development bin leaves bare packages configuration-owned. Bare specifiers in the packaged entry resolve upward along node_modules from the entry's position inside the VFS and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails.
The deploy root is python/sdk-runtime/package.json (dsh-python-runtime-closure, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. scripts/verify-runtime-closure.ts reads every shipped packages/bundle/web-app/cordis.patch.yml*/agent.cordis.yml, evaluates disabled conditions that compare process.platform for every target in python/sdk-runtime/platforms.json, and requires each active workspace plugin at the runtime root through an explicit workspace: dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. pnpm run hygiene, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's files, so the shared chunks tsdown splits out must be covered by files.
The deploy root includes @deepseek-ai/dsh-mcp-client as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers. The optional resource service provides MCP Resources; MCP Prompts remain unsupported. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call.
The Office kit and its installed dependency tree live in the explicitly downloaded sidecar’s node_modules, preserving complete package contents and helper permissions. Top-level pkg.ignore excludes kit packages from the SEA archive, including assets selected by dependency files fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node registerHooks to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. The build locks the selected engine and dependency locations to exact npm tarballs; downloads verify their integrity before publication.
Build pipeline and artifacts
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.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false directly into python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/, including python/sdk-runtime/runtime-bootstrap.mjs as the carrier-root runtime-bootstrap.mjs → 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 → verify the deployed bootstrap 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-<platform>-<arch> under dist-exe/ and copy it with its lightweight resource manifest and Office skill assets 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 reference 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. The deploy flags preserve the measured runtime layout: --legacy is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. 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. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
CI: .github/workflows/build-exe-for-python-sdk.yml runs installed-wheel validation on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The public publication workflow calls it for all five targets; workflow_dispatch can still select a subset. Native builds run on linux-x64 / linux-arm64 (ubuntu-24.04-arm) / macos-arm64 / macos-x64 (macos-15-intel) / 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, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; 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 checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five 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 accepts python-v<repository-version> tag pipelines whose version matches the root package.json, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The python/sdk-runtime README owns the Windows target and the explicit exclusion of Windows arm64.
Python SDK distribution: two carriers, exe for production, node for development
The Python SDK lives at python/: python/sdk is the client and python/sdk-runtime is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required -rg sidecar, lightweight download metadata and Office skills, and optional macOS helper, plus the build-injected runtime/node/ closure tree for repository development. resolve_bundled_launch_args() selects the executable by default; explicit DSH_RUNTIME_MODE=node runs runtime/node/runtime-bootstrap.mjs on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete cordis.yml.
scripts/build-python-release.py reads the authoritative X.Y.Z or prerelease version from the repository root package.json, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with deepseek-harness-sdk depending exactly on the matching deepseek-harness-runtime-bin. An optional python-v<repository-version> release tag is a consistency assertion and is rejected when it differs from the repository version; the source pyproject.toml development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a py3-none-any wheel; each wheel-only runtime package contains one exe, lightweight resource metadata and skills, and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use py3-none-manylinux_2_28_x86_64, py3-none-manylinux_2_28_aarch64, py3-none-macosx_14_0_arm64, py3-none-macosx_14_0_x86_64, or py3-none-win_amd64; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published.
The Python client launches the packaged dsh command with the selected profile (sdk by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback.
Naming lineage
dsh-python-runtime-closure is the private deploy manifest and deepseek-harness-sdk-runtime-<platform>-<arch> is the executable family. The wire serverInfo.name is deepseek-harness-sdk-runtime; the Python distribution names are deepseek-harness-sdk / deepseek-harness-runtime-bin, while the import modules are deepseek_harness / deepseek_harness_runtime.
Packaged workflow and code execution
dsh-workflow-ptc sends its self-contained guest program through dsh-ptc-runtime-node. The shared provider starts a managed child through the executable's private Node bootstrap dispatch, with a dedicated control channel; no separate workflow worker asset is required. The sandboxed Node decision owns process and policy lifetime, and workflow sandbox reuse owns the workflow adapter.
Testing
The verification surface has three tiers. Mechanism tier: the measured conclusions for the --sea chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, node:sqlite, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build installs both wheels into a clean venv outside the checkout, proves matching versions and installed module/executable locations, then completes turns against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and single shell-tool catalog and retains shell state across calls. The custom config additionally drives run_code and a zero-agent workflow through the shared packaged Node process bootstrap. The filesystem-search scenario requires the model to call both glob and grep through the target-native -rg sidecar. The spawn-node scenario drives the platform shell tool through a command starting with node and requires the machine's own Node version in the tool result with no PKG_EXECPATH in the child environment, pinning the packaged runtime against a pkg upgrade that re-records the child-process patch. The MCP scenario starts a temporary external stdio server, deliberately delays its initial tools/list response, then immediately starts the first SDK prompt; the prompt must see and call the discovered tool, proving that initialize is a real Loader-settlement readiness boundary rather than a timing sleep. The same installed run compares a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from run_code, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message, agent, workflow-run, and session IDs across the SDK result and notification stream plus the parent and two child JSONL logs. Trusted pull requests add a real-provider two-turn file write/read whose external bytes, tool calls, completed reasons, and persisted log must agree. This harness stays separate from ACP's pnpm run test:snapshot because the protocols and build artifacts differ.
Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends.
The installed-wheel sdk-office scenario relocates the lightweight wheel payload and explicitly downloads the complete npm Office sidecar with its independent Node. It requires exactly one target backend, verifies that the SDK loads the kit from the downloaded sidecar, and converts DOCX through the skill CLI with an empty PATH. Python Office operations use the same CLI. Explicit downloads reuse the existing build preparation implementation; SDK startup performs no downloads. The runtime reference owns cache and activation behavior.
Alternatives considered
Bare Node native SEA. The injected main script must be a single CJS file, and the blob carries no filesystem and no module resolution, so a dynamic import of a bare specifier has nothing to resolve against; the only option is compiling plugins statically into the main script and registering them by hand — bypassing standard module resolution and hardcoding the plugin set, contrary to "configuration decides everything". The final route is in fact "the official SEA foundation + pkg's VFS/module-hook layer"; what was rejected is the bare use, not SEA itself.
pkg standard mode. Killed by the PoC, not a trade-off: it turns ESM into CJS + V8 bytecode via esbuild, the runtime vm compilation wires up no dynamic-import callback, every import() throws ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, and --options experimental-require-module has no effect; it also depends on community-patched Node binaries (no macos-arm64 prebuilt; compiling from source on the spot takes about 10 minutes). Zero viability for this repo's architecture.
Pre-bundling each package ESM→CJS into the VFS. The compromise that keeps real resolution semantics and only downgrades the module format; --sea passed measurement outright, so this layer of build complexity never needed introducing.
jsonrpc-agent carrying the full closure dependencies. The app bin would declare 53+ dependencies it never imports — a "packaging manifest" masquerading as real dependency relationships — and would force constraints to open two exceptions for it, cordis-in-dependencies and a files wildcard. With the closure manifest landing on the python-side manifest package, constraints needs no exception at all and the bin keeps the normal package shape isomorphic to acp-agent.
An open plugin set (loading user plugins from disk). The shipped set is closed; the PoC incidentally confirmed that on-disk ESM import outside the VFS works (through the ctx.baseUrl relative-path channel). It is listed as a future evolution, which must separately solve sharing the cordis instance inside the exe with external plugins.
Consequences
Bought: no system Node.js dependency on supported targets; plugin semantics strictly identical to running from source (the same real package tree, no transpilation, no registry); the serving interface, the plugin set, and the configuration all converge on two sources of truth — cordis.yml plus one dependency manifest; the exe and node carriers share one tree and one semantics, so development verification never waits for packaging; official Node binaries remove the patched-binary supply-chain concern.
Paid: large Office engine resources remain external to the SEA archive, and the executable must travel with its required sidecars; source enters the distribution as-is (no bytecode obfuscation; a closed-source distribution requirement needs a separate evaluation); pkg's VFS/module-hook layer remains community-maintained (@yao-pkg/pkg is an exact-pinned, pnpm-patched root devDependency; upgrading re-records the patch and is an explicit change); --sea is one invocation per target (matching CI's one leg per platform; local multi-platform builds are serial).
中文
问题
DeepSeek Harness 需要为 Python 库专门提供一种无需安装 Node、可直接在目标平台运行的 SDK 分发形态:一个单文件可执行程序(下称 exe),通过 stdio 提供 JSON-RPC 对外服务接口(HarnessSdkJsonRpcServer,Python SDK 的对端),且实际启动的插件与配置完全由 exe 外部输入的 cordis.yml 决定。
- 与 Python SDK 通信的 JSON-RPC 协议已经过验证
- 需要提供一种让
cordis.yml加载所有插件(ES 模块)的标准方式 - 分发物要自带 Node 运行时,并支持本地源码链接的调试模式
决策
打包路线:@yao-pkg/pkg 的 --sea 模式
exe 使用 @yao-pkg/pkg(vercel/pkg 归档后的活跃维护 fork)的 --sea(enhanced SEA)模式打包。相比 Node 原生 SEA,pkg 在其上增加 /snapshot 虚拟文件系统(VFS)与运行时模块钩子,将 ESM 入口原样交给 Node 默认的 ESM loader,不依赖任何 ESM→CJS 转译。
实测(macos-arm64、node24 构建目标、pkg 6.21.0):VFS 内裸包名 ESM 动态
import()(含顶层await)、CJS 互操作、node:sqlite、集合外包名明确报错、VFS 外磁盘 ESMimport()全部通过,import.meta.url原样为file:///snapshot/...。
--sea 要求构建目标 ≥ node22,exe 统一以 node24 为构建目标;每次 pkg 调用只打包一个构建目标,多平台各调用一次。
@yao-pkg/pkg 是精确钉版的根 devDependency,经 pnpm exec pkg 调用,并以 patches/@[email protected] 移除 SEA bootstrap 中的 patchChildProcess 调用。未打补丁时,pkg 会把 spawn 的 node 命令——包括 -c//c 标志后的命令串,恰是 Bash 工具的 bash -c 形态——改写为 exe 自身,并向每个子进程环境注入 PKG_EXECPATH,模型下发的 node --version 会静默启动 dsh CLI;Node 自身的 SEA 层没有这种改写,且 SEA 二进制永远启动内嵌应用、无法充当纯 Node。移除该调用后,子进程像普通进程一样经 PATH 解析 node(无 Node 的机器如实报 command not found),子进程环境不再出现 PKG_EXECPATH,以 process.execPath 绝对路径 spawn 的重入不受影响,worker 线程本来就未应用该钩子,process.pkg 侧车选择也不受影响。
同一补丁还把 SEA bootstrap 的原生插件解包改成原子写入。VFS 内的插件首次 dlopen 时,pkg 的 patchDlopen 会把该插件所在的包目录整棵拷到 $PKG_NATIVE_CACHE_PATH/pkg/<.node 文件的 sha256>/(默认 ~/.cache/pkg/...),这是同一个 exe 的所有并发进程共用的按用户目录;上游用 writeFileSync 直接写目标路径,会把另一个进程可能已经映射的文件截断,并让该进程的 existsSync/哈希比对读到写了一半的文件。因此并发首次启动在 Linux 上以 SIGBUS 崩溃,在 macOS 上对 sharp 的 libvips 报 ERR_DLOPEN_FAILED: Library not loaded(#4664)。补丁让三处写入都经过同目录临时文件加 renameSync,于是缓存路径要么不存在、要么完整,每个进程都在加载前走完自己的拷贝,相同内容的并发 rename 不需要锁。缓存位置、哈希键与逐文件比对均未改变,通过 PKG_NATIVE_CACHE_PATH 按实例隔离缓存的消费方照常工作;worker 线程只定义该钩子而从不安装。 实测(linux-x64、node24 构建目标、pkg 6.21.0,一个加载 sharp 及其 18 MB libvips 库的探针,16 个进程对同一个空缓存并发首次启动,跑五轮):改动前 80 个进程中 39 个以 SIGBUS 崩溃,改动后 80 个全部正常,且无残留临时文件。
术语提醒:pkg 的 /snapshot VFS 与本仓库测试体系的「快照」(ACP(Agent Client Protocol)回放预期输出、$DSH_SNAPSHOT)无关,本文用「VFS」指前者。
对外服务接口是 dsh 应用中的插件
确定性服务接口由打包后的 dsh 应用选择为插件:
packages/sdk/server(@deepseek-ai/dsh-sdk-jsonrpc-server):纯协议插件;执行apply时,在进程 stdio 上挂载HarnessSdkJsonRpcServer与按行分隔的 JSON-RPC 传输层,资源释放走ctx.effect()。是否提供服务由cordis.yml决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保shutdown响应发送完毕后,对根运行时执行 dispose(资源释放),让待处理的持久化操作完成,再调用exit(0);HMR(热模块替换)式卸载只停止服务,不退出进程)。apps/cli(@deepseek-ai/dsh):打包后的应用入口;其sdkprofile 挂载dsh-sdk-jsonrpc-server,CLI 负责环境分层、profile 组合、stdin/signal 关闭与进程退出。
Python 客户端提供显式 Harness home,并选择 sdk profile 与有序 patch 文件。缺失 home、profile、bundle 或 server 配置项都会明确失败;不存在外部完整配置回退。docs/architecture.md负责该应用接口。
插件解析:VFS 装载真实包树,闭包 manifest(元数据清单)就是部署根目录
exe 的 VFS 内是构建产物形态的真实包树(各包的 lib/ + 真实 node_modules)。打包专用 JSON-RPC 入口会向 app-boot 的根 Include 提供自身已安装 harness 的基准位置:相对插件说明符从外部配置目录解析,裸包名则从 VFS 解析,因此位于另一个 Node 项目内的配置无法遮蔽已打包的插件集合。普通开发 bin 仍由配置项目提供裸包。打包入口中的裸包名从该入口在 VFS 内的位置沿 node_modules 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;import() 集合外的名称会失败。
部署根目录是 python/sdk-runtime/package.json(dsh-python-runtime-closure,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。scripts/verify-runtime-closure.ts 读取每个已发布的 packages/bundle/web-app/cordis.patch.yml*/agent.cordis.yml,针对 python/sdk-runtime/platforms.json 中的每个目标解析比较 process.platform 的 disabled 条件,并要求该目标启用的每个工作区插件都通过显式的 workspace: 依赖列在运行时根目录。它还遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列出,并报告“preset 或引用包 → 缺失依赖”的完整链路;无法识别的平台条件会保持启用,避免因不支持的表达式遗漏插件。pnpm run hygiene、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 files 字段打包,因此 tsdown 拆出的共享分片必须被 files 覆盖。
部署根目录显式包含 @deepseek-ai/dsh-mcp-client,将其作为自定义配置可用的插件,即使随附 preset 均未挂载该插件。外部配置因此可以连接由用户提供的 stdio 与 Streamable HTTP MCP server 并注册其工具;分发物不包含这些 server。可选资源服务提供 MCP Resources;MCP Prompts 仍不受支持。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。
Office kit 及其已安装依赖树位于显式下载的 sidecar 的 node_modules,保留完整包内容与辅助程序权限。顶层 pkg.ignore 从 SEA 归档排除 kit 包,包括依赖 files 字段选中的资源;与其他使用方共享的小型依赖仍可在 VFS 中使用。Python 私有 bootstrap 使用 Node registerHooks 从真实目录解析 kit 入口及其包清单。因此原生可执行程序与 URL Worker 保留普通文件路径,无需修改 kit 或公开 CLI。构建将所选引擎和依赖位置锁定到确切的 npm 归档;下载会在发布缓存前校验完整性。
构建流水线与产物
scripts/build-exe-for-python-sdk.ts:运行时闭包校验 → pnpm run build →(清空后)pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false 直接写入 python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/,其中包含作为载体根目录 runtime-bootstrap.mjs 的 python/sdk-runtime/runtime-bootstrap.mjs → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 .bin 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 node-pty addon → 每个构建目标调用一次 pkg --sea → 将 deepseek-harness-sdk-runtime-<platform>-<arch> 写入 dist-exe/,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 @deepseek-ai/dsh-subprocess-local/runner 核心,不改变 CLI 语法,也不增加另一个可执行文件;原生 containment 参考负责这条私有路径。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 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;link-workspace-packages 选择直接工作区依赖。pnpm-workspace.yaml 将传递的 @deepseek-ai/cosmokit 与 @deepseek-ai/schemastery semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
CI 使用 .github/workflows/build-exe-for-python-sdk.yml:安装后 wheel 包验证在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。公开发布工作流调用它构建全部五个目标;workflow_dispatch 仍可选择部分目标。linux-x64、linux-arm64(ubuntu-24.04-arm)、macos-arm64、macos-x64(macos-15-intel)与 win-x64(windows-2025)分别进行原生构建,并在适用平台缓存 ~/.pkg-cache;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。.gitlab-ci.yml 只接受版本与根目录 package.json 匹配的 python-v<repository-version> 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。python/sdk-runtime README负责 Windows 目标及对 Windows arm64 的明确排除。
Python SDK 分发:双载体,exe 用于生产,node 用于开发
Python SDK 位于 python/:python/sdk 是客户端,python/sdk-runtime 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 -rg 伴随文件、轻量下载元数据与 Office skills 和可选的 macOS helper,以及供仓库开发使用的构建注入 runtime/node/ 闭包树。resolve_bundled_launch_args() 默认选择可执行文件;显式设置 DSH_RUNTIME_MODE=node 会在系统 Node 22.19 或更高版本上运行 runtime/node/runtime-bootstrap.mjs。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 cordis.yml。
scripts/build-python-release.py 从仓库根目录的 package.json 读取权威的 X.Y.Z 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 deepseek-harness-sdk 精确依赖匹配版本的 deepseek-harness-runtime-bin。可选的 python-v<repository-version> 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 pyproject.toml 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 py3-none-any wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe、轻量资源元数据与 skills 及架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 py3-none-manylinux_2_28_x86_64、py3-none-manylinux_2_28_aarch64、py3-none-macosx_14_0_arm64、py3-none-macosx_14_0_x86_64 或 py3-none-win_amd64;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。两个 macOS 标签都会特意声明保守的 14.0 安装下限:打包后的 Node 24 可执行文件声明 macOS 13.5,x64 PTY helper 声明 10.7,但发布验证只按照 14.0 wheel 声明证明完整载荷,不会把各组件实测的最低版本承诺为受支持宿主。两个 macOS wheel 包仍按架构分别发布,不发布 universal2 wheel 包。
Python 客户端使用所选 profile(默认 sdk)、有序 patch 文件和显式 Harness home 启动打包后的 dsh 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。
命名血统
dsh-python-runtime-closure 是私有部署 manifest,deepseek-harness-sdk-runtime-<platform>-<arch> 是可执行文件族。协议字段 serverInfo.name 是 deepseek-harness-sdk-runtime;Python 分发包名是 deepseek-harness-sdk / deepseek-harness-runtime-bin,导入模块名是 deepseek_harness / deepseek_harness_runtime。
打包后的工作流与代码执行
dsh-workflow-ptc 通过 dsh-ptc-runtime-node 发送自包含 guest 程序。共享提供方通过可执行文件的私有 Node bootstrap 分派启动受管子进程,并使用专用控制通道;不需要独立的工作流 worker 资源。沙箱 Node 决策负责进程与策略生命周期,工作流沙箱复用负责工作流适配器。
测试
验证面分三层。机制层:--sea 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 import()、单一 Cordis 实例、明确报错的配置链路、node:sqlite、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 cwd 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都会把两个 wheel 包安装进 checkout 外的干净 venv,证明版本相同以及已安装模块/可执行文件的位置,再通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与单个 shell 工具目录,并跨调用保留 shell 状态。自定义配置还会通过共享的打包后 Node 进程 bootstrap 执行 run_code 和不启动 agent 的 workflow。文件系统搜索场景要求模型通过目标平台的 -rg 伴随文件调用 glob 与 grep。spawn-node 场景驱动平台 shell 工具执行以 node 开头的命令,要求工具结果给出机器自身的 Node 版本且子进程环境中无 PKG_EXECPATH,把打包运行时钉死在「pkg 升级重录 child_process 补丁也不得回归」的行为上。MCP 场景会启动临时外部 stdio server,刻意延迟首次 tools/list 响应,随后立即启动第一个 SDK 提示词;该提示词必须看到并调用已发现的工具,从而证明 initialize 是真正以 Loader 插件树完全稳定为准的就绪边界,而不是依赖定时 sleep。同一项安装后运行还会经 Python SDK 比较一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 run_code 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化 SDK 结果与通知流,以及父会话和两个子会话 JSONL 日志中不透明的消息、agent、工作流运行与会话 ID。可信拉取请求会增加真实提供方双轮文件写入/读取,并要求外部字节、工具调用、已完成原因与持久化日志一致。该 harness 与 ACP 的 pnpm run test:snapshot 保持独立,因为二者的协议和构建产物不同。
手工驱动注意:bin 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。
安装后 wheel 的 sdk-office 场景迁移轻量 wheel 载荷,并显式下载完整 npm Office sidecar 及其独立 Node。它要求仅有一个目标后端,验证 SDK 从下载的 sidecar 加载 kit,并通过 skill CLI 在空 PATH 下转换 DOCX。Python Office 操作使用同一 CLI。显式下载复用现有构建准备实现;SDK 启动不执行下载。运行时参考拥有缓存和启用行为的说明。
曾考虑的替代方案
裸用 Node 原生 SEA。 注入的主脚本必须是 CJS 单文件,blob 内没有文件系统与模块解析,因此动态 import() 无法解析裸包名;只能把插件静态编译进主脚本并手工注册。这会绕过标准模块解析并硬编码插件集合,与「配置决定一切」相悖。最终路线实际是「官方 SEA 基础 + pkg 的 VFS/模块钩子层」;否决的是裸用方式,而不是 SEA 本身。
pkg 标准模式。 PoC 证明该模式不可行,而非权衡后放弃:它通过 esbuild 将 ESM 转为 CJS + V8 字节码,但运行时 VM 编译没有接入动态 import() 回调,任何 import() 都会抛出 ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,--options experimental-require-module 也无效;此外,它依赖社区补丁版 Node 二进制(macos-arm64 没有预编译版本,现场从源码编译约需 10 分钟)。该模式不适用于本仓库架构。
每包 ESM→CJS 预打包进 VFS。 保持真实解析语义、只降级模块格式的折中;--sea 直接通过实测,这层构建复杂度无需引入。
让 jsonrpc-agent 承担完整闭包依赖。 应用入口将声明 53 个以上自身并不 import() 的依赖,使「打包 manifest」伪装成真实依赖关系,还会迫使 constraints 为其增加 cordis-in-dependencies 与 files 通配符两个例外。将闭包 manifest 放在 Python 侧的 manifest 包后,constraints 不需要任何例外,bin 也能保持与 acp-agent 同构的正常包形状。
开放插件集(从磁盘加载用户插件)。 交付的集合是封闭的;PoC 同时证实,可以通过 ctx.baseUrl 相对路径通道从 VFS 外的磁盘 import() ESM。该能力列为后续演进,届时还需解决外部插件与 exe 内 Cordis 实例的共享问题。
后果
买到的:受支持目标无需依赖系统 Node.js;插件语义与源码运行严格一致(同一棵真实包树,无转译、无注册表);对外服务接口、插件集与配置全部收敛到 cordis.yml 和一份依赖 manifest 这两个真源;exe 与 node 双载体使用同一棵树和相同语义,开发验证无需等待打包;官方 Node 二进制消除了补丁版二进制的供应链顾虑。
付出的:大型 Office 引擎资源保留在 SEA 归档之外,可执行文件必须与必需的伴随文件一起迁移;源码原样进入分发物(没有字节码混淆;闭源分发诉求需要另行评估);pkg 的 VFS/模块钩子层仍由社区维护(@yao-pkg/pkg 为精确钉版、带 pnpm 补丁的根 devDependency,升级需重录补丁,属显式改动);--sea 每个构建目标调用一次(与 CI 每个平台一个任务相匹配,本地多平台构建串行执行)。