DSH / Atlas
2026-07-10implementedarchitecture

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.url comes through unchanged as file:///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.

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: the two packages sdk/server + examples/jsonrpc-demo

The deterministic protocol implementation (server.ts / transport.ts) lands as two packages on the existing acp/acp + examples/acp-demo pattern — the serving surface is itself a plugin:

  • packages/sdk/server (@deepseek-ai/dsh-sdk-jsonrpc-server): the pure protocol plugin; on apply it mounts HarnessSdkJsonRpcServer plus a line-delimited JSON-RPC transport on the process stdio, with disposal through ctx.effect(). Whether to serve is decided by cordis.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 the shutdown response it disposes the root runtime so persistence drains, then exit(0); an HMR-style unload only stops the service without exiting the process).
  • packages/examples/jsonrpc-demo (@deepseek-ai/dsh-sdk-jsonrpc-demo): a thin app bin — installFailLoud + loadEnv + config discovery + boot() from dsh-app-boot, done once boot completes; the server is brought up by the dsh-sdk-jsonrpc-server entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the bin (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130).

Config discovery has two channels and fails loudly when both are missing: the DSH_CORDIS_CONFIG environment variable first (the SDK client convention), then an argv positional argument; no default path and no built-in fallback whatsoever — "the plugins actually booted are decided by an external cordis.yml" is a hard semantic.

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-jsonrpc-agent-pkg, 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 apps/cli/config/agent-presets/*/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 or extend the bridge to MCP Resources and Prompts. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call.

Build pipeline and artifacts

scripts/build-exe-for-python-sdk.ts: runtime closure verification → pnpm run build → (after clearing) pnpm --filter dsh-jsonrpc-agent-pkg 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 any direct workspace package that legacy deploy hoisted back under the source manifest's node_modules, omitting its package-local dependency tree and rejecting any remaining manifest gap → replace every staged dependency symlink with its target bytes, remove package-manager .bin links, and fail if any symlink remains → inject the pkg configuration (bin points at node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js inside the closure, assets is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target node-pty addon → one pkg --sea per target → the executables dsh-jsonrpc-agent-pkg-<platform>-<arch> land in dist-exe/ and are copied back into the runtime directory. Linux installs build pty.node from source; CI rebuilds that addon inside the matching manylinux 2.28 container before packaging, and the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory. 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. CI treats these products as intermediate test inputs and retains their platform wheels. 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 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, called for linux-x64 by the required Python runtime pull-request validation, triggered explicitly by workflow_dispatch or the build-exe label for selected targets, and called for all targets by the public publication workflow. Native builds run on linux-x64 / linux-arm64 (ubuntu-24.04-arm) / macos-arm64, with ~/.pkg-cache cached, and pkg handles macOS ad-hoc signing. Each leg drives a mock SSE model through the SDK with the default config and a custom cordis.yml, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without runtime_bin; Linux additionally inspects both the executable and native addon's GLIBC requirements and runs in a manylinux 2.28 container, while macOS verifies that the executable's deployment target fits the wheel tag. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three 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 three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.

Python SDK distribution: two carriers, exe for production, node for development

The Python SDK lives at python/: python/sdk (the client) + python/sdk-runtime (the runtime carrier package). The runtime package's data directory holds the checked-in default runtime/cordis.yml, the build-injected platform exe with its required -rg sidecar and optional macOS helper, and the build-injected runtime/node/ closure tree. resolve_bundled_launch_args() automatic resolution finds the exe only; the node carrier is enabled only by an explicit DSH_RUNTIME_MODE=node (running runtime/node/node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.

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 and its architecture-matched -rg sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use one of py3-none-manylinux_2_28_x86_64, py3-none-manylinux_2_28_aarch64, or the conservative py3-none-macosx_14_0_arm64 tag for the Node 24 executable's macOS 13.5 deployment target; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms.

The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no cordis, named no explicit runtime, and the environment has no DSH_CORDIS_CONFIG, the client explicitly injects the checked-in default cordis.yml (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the dsh-sdk-jsonrpc-server serving entry, with !!js environment-variable fallbacks) via DSH_CORDIS_CONFIG.

Naming lineage

@deepseek-ai/dsh-sdk-jsonrpc-demo (the package) → dsh-jsonrpc-agent (the bin) → dsh-jsonrpc-agent-pkg (the closure manifest; no scope prefix, deliberately sidestepping the constraints' package-shape rules for @deepseek-ai/dsh-*) → dsh-jsonrpc-agent-pkg-<platform>-<arch> (the exe artifacts). The wire serverInfo.name stays deepseek-harness-sdk-runtime (a protocol-stable value); the Python distribution names are deepseek-harness-sdk / deepseek-harness-runtime-bin, while the import modules remain deepseek_harness / deepseek_harness_runtime.

Disposition of worker-style plugins

dsh-workflow-worker-thread and dsh-code-runtime-worker-thread are supported inside the exe. Their built hosts convert the sibling lib/worker.cjs URL with fileURLToPath() and pass the resulting filesystem string to Worker, which is the form pkg's Worker hook resolves inside the VFS. The worker entries are CommonJS because that hook compiles VFS worker files as CommonJS. The workflow engine keeps its data-URL bootstrap for unbuilt source execution; only its built sibling entry uses the filesystem string. The custom-config executable smoke loads both backends, invokes a real run_code call and a zero-agent workflow call, and requires each worker to return 42 from inside pkg's VFS.

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 completes a turn 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 two-tool catalog, retains Bash state across calls, and invokes the editor. The custom config additionally drives run_code and a zero-agent workflow through their real worker files inside the packaged VFS. The filesystem-search scenario requires the model to call both glob and grep through the target-native -rg sidecar. 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 build leg runs 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. This harness stays separate from ACP's pnpm run test:snapshot because the protocols and build artifacts differ. The platform wheel is then installed in a clean venv and run without runtime_bin.

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.

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: zero-dependency single-file distribution on target platforms; 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: artifacts on the order of 174MB with source entering the blob as-is (no bytecode obfuscation; a closed-source distribution requirement needs a separate evaluation); pkg's VFS/module-hook layer remains community-maintained (the build script pins @yao-pkg/pkg@6.21.0; upgrading 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 外磁盘 ESM import() 全部通过,import.meta.url 原样为 file:///snapshot/...

--sea 要求构建目标 ≥ node22,exe 统一以 node24 为构建目标;每次 pkg 调用只打包一个构建目标,多平台各调用一次。

术语提醒:pkg 的 /snapshot VFS 与本仓库测试体系的「快照」(ACP(Agent Client Protocol)回放预期输出、$DSH_SNAPSHOT)无关,本文用「VFS」指前者。

对外服务接口也是插件:sdk/server + examples/jsonrpc-demo 两个包

确定性协议实现(server.ts / transport.ts)按 acp/acp + examples/acp-demo 的既有模式落为两包——对外服务接口本身也是插件:

  • packages/sdk/server@deepseek-ai/dsh-sdk-jsonrpc-server):纯协议插件;执行 apply 时,在进程 stdio 上挂载 HarnessSdkJsonRpcServer 与按行分隔的 JSON-RPC 传输层,资源释放走 ctx.effect()。是否提供服务由 cordis.yml 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 shutdown 响应发送完毕后,对根运行时执行 dispose(资源释放),让待处理的持久化操作完成,再调用 exit(0);HMR(热模块替换)式卸载只停止服务,不退出进程)。
  • packages/examples/jsonrpc-demo@deepseek-ai/dsh-sdk-jsonrpc-demo):轻量应用入口——installFailLoud + loadEnv + 配置发现 + dsh-app-bootboot()boot() 完成后入口即完成,服务器由 cordis.yml 中的 dsh-sdk-jsonrpc-server 条目启动。它只依赖 app-boot。进程级退出归 bin 所有(stdin EOF/SIGTERM → dispose 后返回 0,SIGINT → 130)。

配置发现有两个通道,均缺失时立即报错:优先使用 DSH_CORDIS_CONFIG 环境变量(SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——「实际启动的插件由外部 cordis.yml 决定」是硬语义。

插件解析: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.jsondsh-jsonrpc-agent-pkg,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。scripts/verify-runtime-closure.ts 读取每个已发布的 apps/cli/config/agent-presets/*/agent.cordis.yml,针对 python/sdk-runtime/platforms.json 中的每个目标解析比较 process.platformdisabled 条件,并要求该目标启用的每个工作区插件都通过显式的 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 和 Prompts。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。

构建流水线与产物

scripts/build-exe-for-python-sdk.ts:运行时闭包校验 → pnpm run build →(清空后)pnpm --filter dsh-jsonrpc-agent-pkg 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 的 node_modules 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 .bin 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(bin 指向闭包内的 node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.jsassets 使用全量 glob,因为动态 import() 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 node-pty addon → 每个构建目标调用一次 pkg --sea → 可执行文件 dsh-jsonrpc-agent-pkg-<platform>-<arch> 写入 dist-exe/,并拷回运行时目录。Linux 安装会从源码构建 pty.node;CI 会在打包前进入匹配架构的 manylinux 2.28 容器重新构建该 addon,而 --legacy 部署会省略这一副作用目录,因此构建器会把它从根安装目录复制到暂存闭包。每个目标都会把对应的原生 @vscode/ripgrep 二进制复制到可执行文件旁,作为必需的 -rg 伴随文件;pkg 运行时通过 process.pkg 选择该伴随文件,普通 Node 执行则直接使用 @vscode/ripgrep。macOS 使用对应目标的预构建产物,并额外生成所需的 -spawn-helper。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 inject-workspace-packages 时必须使用 --legacyhoisted 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;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必需的 Python 运行时拉取请求验证调用它构建 linux-x64,手动派发 workflow_dispatch 或 PR(Pull Request)的 build-exe 标签可以显式选择构建目标,公开发布工作流则调用它构建全部目标。linux-x64、linux-arm64(ubuntu-24.04-arm)和 macos-arm64 三个平台分别进行原生构建,并缓存 ~/.pkg-cache;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用 mock SSE(Server-Sent Events)模型,分别通过默认配置和自定义 cordis.yml 驱动 SDK,再通过 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 runtime_bin 的情况下运行。Linux 还会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并在 manylinux 2.28 容器中运行;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。.gitlab-ci.yml 只接受版本与根目录 package.json 匹配的 python-v<repository-version> 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。

Python SDK 分发:双载体,exe 用于生产,node 用于开发

Python SDK 位于 python/python/sdk 是客户端,python/sdk-runtime 是运行时载体包。运行时包的数据目录包含检入的默认 runtime/cordis.yml、构建注入的平台 exe 及其必需的 -rg 伴随文件和可选的 macOS helper,以及构建注入的 runtime/node/ 闭包树。resolve_bundled_launch_args() 的自动解析只查找 exenode 载体仅在显式设置 DSH_RUNTIME_MODE=node 时启用(运行 runtime/node/node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。

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 及其架构匹配的 -rg 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 py3-none-manylinux_2_28_x86_64py3-none-manylinux_2_28_aarch64,或针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 py3-none-macosx_14_0_arm64 标签;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。

exe「必须显式配置」的硬语义不变;零配置体验由包装层恢复:调用方没有提供 cordis、没有显式指定运行时,且环境中没有 DSH_CORDIS_CONFIG 时,客户端将检入的默认 cordis.ymlagent-core + 预载的 llm-deepseek + JSONL 持久化 + bash-local + dsh-sdk-jsonrpc-server 对外服务条目,并通过 !!js 使用环境变量兜底)显式注入 DSH_CORDIS_CONFIG

命名血统

@deepseek-ai/dsh-sdk-jsonrpc-demo(包)→ dsh-jsonrpc-agentbin)→ dsh-jsonrpc-agent-pkg(闭包 manifest;没有作用域前缀,刻意避开 constraints@deepseek-ai/dsh-* 的包形状规则)→ dsh-jsonrpc-agent-pkg-<platform>-<arch>(exe 产物)。协议字段 serverInfo.name 保持为 deepseek-harness-sdk-runtime(协议稳定值);Python 分发包名为 deepseek-harness-sdk / deepseek-harness-runtime-bin,导入模块名仍为 deepseek_harness / deepseek_harness_runtime

工作线程插件

exe 内支持 dsh-workflow-worker-threaddsh-code-runtime-worker-thread。两个后端构建后的宿主都通过 fileURLToPath() 转换相邻 lib/worker.cjs 的 URL,再将所得文件系统字符串传给 Worker;pkg 的 Worker 钩子可以用这种形式解析 VFS 内文件。该钩子会把 VFS 内的工作线程文件作为 CommonJS 编译,所以工作线程入口采用 CommonJS。工作流引擎在未构建的源码执行中仍保留 data: URL 引导程序,只有构建后的相邻入口使用文件系统字符串。自定义配置的可执行文件冒烟测试会加载两个后端,实际调用 run_code 与不启动 agent(智能体)的 workflow,并要求两个工作线程都从 pkg 的 VFS 内返回 42

测试

验证面分三层。机制层:--sea 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 import()、单一 Cordis 实例、明确报错的配置链路、node:sqlite、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 cwd 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与双工具目录,跨调用保留 Bash 状态,并调用编辑器。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 run_code 和不启动 agent 的 workflow。文件系统搜索场景要求模型通过目标平台的 -rg 伴随文件调用 globgrep。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 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv,并在不传 runtime_bin 的情况下运行。

手工驱动注意:bin 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。

曾考虑的替代方案

裸用 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-dependenciesfiles 通配符两个例外。将闭包 manifest 放在 Python 侧的 manifest 包后,constraints 不需要任何例外,bin 也能保持与 acp-agent 同构的正常包形状。

开放插件集(从磁盘加载用户插件)。 交付的集合是封闭的;PoC 同时证实,可以通过 ctx.baseUrl 相对路径通道从 VFS 外的磁盘 import() ESM。该能力列为后续演进,届时还需解决外部插件与 exe 内 Cordis 实例的共享问题。

后果

买到的:目标平台零依赖的单文件分发;插件语义与源码运行严格一致(同一棵真实包树,无转译、无注册表);对外服务接口、插件集与配置全部收敛到 cordis.yml 和一份依赖 manifest 这两个真源;exe 与 node 双载体使用同一棵树和相同语义,开发验证无需等待打包;官方 Node 二进制消除了补丁版二进制的供应链顾虑。

付出的:产物约 174MB,且源码原样进入 blob(没有字节码混淆;闭源分发诉求需要另行评估);pkg 的 VFS/模块钩子层仍由社区维护(构建脚本钉死 @yao-pkg/pkg@6.21.0,升级需要显式改动);--sea 每个构建目标调用一次(与 CI 每个平台一个任务相匹配,本地多平台构建串行执行)。