diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index ceb0eae37a..18e4bcf009 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: 826194e0d5bd1f0260400c036f8affaf1549629f -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: e4b17a1f3951f36af88564d5365ab7952d6281a5 +2026-07-10-single-file-executable-sdk-runtime-distribution.md: 29aa14db2b3b64cefb7960ee8f316682ba1ac736 +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: f10d72b394246e9e57033a5382ec977bdc6f3ab8 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index 826194e0d5..29aa14db2b 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -34,19 +34,19 @@ Config discovery has two channels and fails loudly when both are missing: the `D ### 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 JSON-RPC bin 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. Bare specifiers resolve upward along `node_modules` from the Loader'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. +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`](../../../../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`](../../../../scripts/verify-runtime-closure.ts) traverses every workspace package covered by that manifest and requires every non-optional workspace peer at the runtime root, reporting the complete referencing-package → missing-peer chain; `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`. ### Build pipeline and artifacts -[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-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 symlink tree and rejecting any remaining manifest gap → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/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--` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. 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 yields a zero-symlink package payload (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources. +[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-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 symlink tree and rejecting any remaining manifest gap → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-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--` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. 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 yields a zero-symlink package payload (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources. CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. 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 GLIBC requirements and runs in a manylinux 2.28 container. 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`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` 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/README.md): `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 and optional 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-jsonrpc-demo/lib/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. +The Python SDK lives at [`python/`](../../../../python/README.md): `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 and optional 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-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`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with `deepseek-harness-sdk` depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` 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. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, and the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra helpers, and unsupported platforms. diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index e4b17a1f39..f10d72b394 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -34,19 +34,19 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后 ### 插件解析:VFS 装载真实包树,闭包 manifest(元数据清单)就是部署根目录 -exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真实 `node_modules`)。JSON-RPC bin 会向 app-boot 的根 Include 提供自身已安装 harness 的基准位置:相对插件说明符从外部配置目录解析,裸包名则从 VFS 解析,因此位于另一个 Node 项目内的配置无法遮蔽已打包的插件集合。裸包名从 VFS 内 loader 所在位置沿 `node_modules` 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;`import()` 集合外的名称会失败。 +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`](../../../../python/sdk-runtime/package.json)(`dsh-jsonrpc-agent-pkg`,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列在运行时根目录,并报告“引用包 → 缺失对等依赖”的完整链路;`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。 ### 构建管线与产物 -[`scripts/build-exe-for-python-sdk.ts`](../../../../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 缺口 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略该副作用目录,因此构建器会把它从根安装目录复制到暂存闭包;macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 产出无符号链接的包载荷(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。 +[`scripts/build-exe-for-python-sdk.ts`](../../../../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 缺口 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略该副作用目录,因此构建器会把它从根安装目录复制到暂存闭包;macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 产出无符号链接的包载荷(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。 CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),且只允许显式触发:手动派发 `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 还会检查 GLIBC 依赖,并在 manylinux 2.28 容器中运行。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。 ### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发 -Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。 +Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。 [`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 `deepseek-harness-sdk` 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe,macOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64` 三种标签之一;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。 diff --git a/packages/boot/app-boot/tests/app-boot.spec.ts b/packages/boot/app-boot/tests/app-boot.spec.ts index ab0089fd2c..dc4e92dc4a 100644 --- a/packages/boot/app-boot/tests/app-boot.spec.ts +++ b/packages/boot/app-boot/tests/app-boot.spec.ts @@ -1,6 +1,7 @@ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve, sep } from 'node:path' +import { pathToFileURL } from 'node:url' import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' @@ -559,9 +560,12 @@ describe('boot', () => { it('can resolve bare plugins from the harness when the config project shadows their package name', async () => { const dir = tmp() + const harness = tmp() const absolutePlugin = join(dir, 'absolute.mjs') const shadow = join(dir, 'node_modules', '@deepseek-ai', 'dsh-system-prompt') + const harnessPlugin = join(harness, 'node_modules', '@deepseek-ai', 'dsh-system-prompt') mkdirSync(shadow, { recursive: true }) + mkdirSync(harnessPlugin, { recursive: true }) writeFileSync(join(shadow, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-system-prompt', type: 'module', @@ -573,6 +577,17 @@ describe('boot', () => { '}', '', ].join('\n')) + writeFileSync(join(harnessPlugin, 'package.json'), JSON.stringify({ + name: '@deepseek-ai/dsh-system-prompt', + type: 'module', + exports: './index.mjs', + })) + writeFileSync(join(harnessPlugin, 'index.mjs'), [ + 'export function apply(ctx) {', + ' ctx.provide("harnessPluginLoaded", true)', + '}', + '', + ].join('\n')) writeFileSync(join(dir, 'relative.mjs'), 'export function apply(ctx) { ctx.provide("relativePluginLoaded", true) }\n') writeFileSync(absolutePlugin, 'export function apply(ctx) { ctx.provide("absolutePluginLoaded", true) }\n') writeFileSync(join(dir, 'cordis.yml'), [ @@ -593,9 +608,10 @@ describe('boot', () => { } finally { await configOwned.fiber.dispose() } - const ctx = await boot(NAME, join(dir, 'cordis.yml'), undefined, undefined, import.meta.url) + const harnessBaseUrl = pathToFileURL(join(harness, 'entry.mjs')).href + const ctx = await boot(NAME, join(dir, 'cordis.yml'), undefined, undefined, harnessBaseUrl) try { - expect(ctx.get('systemPrompt')).toBeDefined() + expect(ctx.get('harnessPluginLoaded')).toBe(true) expect(ctx.get('shadowPluginLoaded')).toBeUndefined() expect(ctx.get('relativePluginLoaded')).toBe(true) expect(ctx.get('absolutePluginLoaded')).toBe(true) diff --git a/packages/examples/jsonrpc-demo/README.i18n.yaml b/packages/examples/jsonrpc-demo/README.i18n.yaml index c47938ce3b..ae8f55a8cd 100644 --- a/packages/examples/jsonrpc-demo/README.i18n.yaml +++ b/packages/examples/jsonrpc-demo/README.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 packages/examples/jsonrpc-demo/README.md -README.md: fff8e78698cd3d6320606084ef5c533be7c52633 -README.zh.md: 75382b97ea1837cf1415e8a7f5004206596e168c +README.md: 40ced3ee1fe2d3eac69b82501d417130267d7634 +README.zh.md: 451bdf7428f8265750082af240418d4653bb8595 diff --git a/packages/examples/jsonrpc-demo/README.md b/packages/examples/jsonrpc-demo/README.md index fff8e78698..40ced3ee1f 100644 --- a/packages/examples/jsonrpc-demo/README.md +++ b/packages/examples/jsonrpc-demo/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Bin-only app that boots an external `cordis.yml`; its [`jsonrpc`](../../scaffold/server/README.md) entry serves SDK clients over newline-delimited stdio. The config composes the spine, backends, and serving plugin. The published bin is `dsh-jsonrpc-agent`, and `lib/bin.js` also ships as the `dsh-jsonrpc-agent-pkg` [single-executable runtime](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) used by the Python SDK. +Bin-only app that boots an external `cordis.yml`; its [`jsonrpc`](../../scaffold/server/README.md) entry serves SDK clients over newline-delimited stdio. The config composes the spine, backends, and serving plugin. The published `dsh-jsonrpc-agent` bin resolves bare plugins from the configuration project. The Python SDK's `dsh-jsonrpc-agent-pkg` [single-executable runtime](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) uses `lib/packaged-bin.js` instead: packaged bare plugins resolve from its closed runtime tree, while relative plugins remain configuration-relative. ## Config discovery diff --git a/packages/examples/jsonrpc-demo/README.zh.md b/packages/examples/jsonrpc-demo/README.zh.md index 75382b97ea..451bdf7428 100644 --- a/packages/examples/jsonrpc-demo/README.zh.md +++ b/packages/examples/jsonrpc-demo/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -只包含 bin 的应用,启动外部 `cordis.yml`;其 [`jsonrpc`](../../scaffold/server/README.md) 入口通过按换行分隔的 stdio 为 SDK 客户端提供服务。配置负责组合主干、后端和服务插件。发布的 bin 名为 `dsh-jsonrpc-agent`,`lib/bin.js` 还会作为 Python SDK 使用的 `dsh-jsonrpc-agent-pkg` [单文件可执行运行时](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)交付。 +只包含 bin 的应用,启动外部 `cordis.yml`;其 [`jsonrpc`](../../scaffold/server/README.md) 入口通过按换行分隔的 stdio 为 SDK 客户端提供服务。配置负责组合主干、后端和服务插件。发布的 `dsh-jsonrpc-agent` bin 从配置项目解析裸插件。Python SDK 的 `dsh-jsonrpc-agent-pkg` [单文件可执行运行时](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)改用 `lib/packaged-bin.js`:已打包的裸插件从封闭运行时包树解析,相对插件仍以配置目录为基准。 ## 配置发现 diff --git a/packages/examples/jsonrpc-demo/package.json b/packages/examples/jsonrpc-demo/package.json index 155660d190..e8605741d1 100644 --- a/packages/examples/jsonrpc-demo/package.json +++ b/packages/examples/jsonrpc-demo/package.json @@ -22,6 +22,10 @@ "types": "./lib/types/bin.d.ts", "default": "./lib/bin.js" }, + "./packaged-bin": { + "types": "./lib/types/packaged-bin.d.ts", + "default": "./lib/packaged-bin.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, @@ -29,6 +33,7 @@ "lib/index.js", "lib/invariant.js", "lib/bin.js", + "lib/packaged-bin.js", "lib/types/**/*.d.ts" ], "license": "BSD-3-Clause", diff --git a/packages/examples/jsonrpc-demo/src/bin.ts b/packages/examples/jsonrpc-demo/src/bin.ts index cecc93dac4..532c1ba1b9 100644 --- a/packages/examples/jsonrpc-demo/src/bin.ts +++ b/packages/examples/jsonrpc-demo/src/bin.ts @@ -1,54 +1,11 @@ #!/usr/bin/env node /** - * Boots an external `cordis.yml`; its `@deepseek-ai/dsh-jsonrpc` entry serves - * newline-delimited JSON-RPC on stdio. `$DSH_CORDIS_CONFIG` wins over `argv[2]`; - * empty or missing paths exit 1, with no default config or `DSH_SNAPSHOT` mode. - * App-boot owns env loading, Loader guards, and settled-tree startup. - * stdin EOF and SIGTERM dispose the root context and exit 0; SIGINT exits 130. - * Protocol `shutdown` belongs to the server plugin. Stdout is reserved for frames. + * Generic JSON-RPC agent bin. External configurations own their bare plugin + * packages; the packaged runtime uses `packaged-bin.ts` instead. * * @module @deepseek-ai/dsh-jsonrpc-demo/bin */ -import { existsSync } from 'node:fs' -import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' +import { runJsonrpcAgent } from './runner.ts' -const NAME = 'dsh-jsonrpc-agent' - -/* v8 ignore start -- composition over tested app-boot/jsonrpc and executable acceptance paths */ -installFailLoud(NAME) -loadEnv(NAME) - -// Env wins over argv; empty values are absent. External config defines the deployment. -const fromEnv = process.env['DSH_CORDIS_CONFIG'] -const fromArgv = process.argv[2] -const requested = fromEnv !== undefined && fromEnv !== '' - ? fromEnv - : fromArgv !== undefined && fromArgv !== '' ? fromArgv : undefined -const configPath = requested === undefined ? undefined : resolveConfigPath(requested, undefined) -if (configPath === undefined || !existsSync(configPath)) { - process.stderr.write( - `usage: ${NAME} (or set DSH_CORDIS_CONFIG=, which wins); the config is required — there is no built-in fallback\n`, - ) - process.exit(1) -} - -// The executable owns a closed plugin set; config-adjacent node_modules must -// not shadow the packages embedded beside this bin in the VFS. -const ctx = await boot(NAME, configPath, undefined, undefined, import.meta.url) -let exiting = false - -async function disposeAndExit(code: number): Promise { - if (exiting) return - exiting = true - try { - await ctx.fiber.dispose() - } finally { - process.exit(code) - } -} - -process.stdin.on('end', () => { void disposeAndExit(0) }) -process.on('SIGTERM', () => { void disposeAndExit(0) }) -process.on('SIGINT', () => { void disposeAndExit(130) }) -/* v8 ignore stop */ +await runJsonrpcAgent() diff --git a/packages/examples/jsonrpc-demo/src/index.ts b/packages/examples/jsonrpc-demo/src/index.ts index d4a4017f62..eb85517431 100644 --- a/packages/examples/jsonrpc-demo/src/index.ts +++ b/packages/examples/jsonrpc-demo/src/index.ts @@ -1,7 +1,8 @@ /** - * Bin-only app package: `bin.ts` discovers an external `cordis.yml` and owns - * process exit. This module exports no composition plugin; the config chooses - * whether to load the {@link @deepseek-ai/dsh-jsonrpc} serving plugin. + * Bin-only app package: its generic and packaged entries discover an external + * `cordis.yml` and own process exit. This module exports no composition plugin; + * the config chooses whether to load the + * {@link @deepseek-ai/dsh-jsonrpc} serving plugin. * * @module @deepseek-ai/dsh-jsonrpc-demo */ diff --git a/packages/examples/jsonrpc-demo/src/packaged-bin.ts b/packages/examples/jsonrpc-demo/src/packaged-bin.ts new file mode 100644 index 0000000000..0151b18576 --- /dev/null +++ b/packages/examples/jsonrpc-demo/src/packaged-bin.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +/** + * Closed-runtime JSON-RPC agent bin. Bare plugins resolve from the installed + * runtime closure while relative plugins remain configuration-relative. + * + * @module @deepseek-ai/dsh-jsonrpc-demo/packaged-bin + */ + +import { runJsonrpcAgent } from './runner.ts' + +await runJsonrpcAgent(import.meta.url) diff --git a/packages/examples/jsonrpc-demo/src/runner.ts b/packages/examples/jsonrpc-demo/src/runner.ts new file mode 100644 index 0000000000..d094707514 --- /dev/null +++ b/packages/examples/jsonrpc-demo/src/runner.ts @@ -0,0 +1,55 @@ +/** + * Shared process lifecycle for the generic and closed-runtime JSON-RPC bins. + * + * @module @deepseek-ai/dsh-jsonrpc-demo/runner + */ + +import { existsSync } from 'node:fs' +import { boot, installFailLoud, loadEnv, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' + +const NAME = 'dsh-jsonrpc-agent' + +/** + * Boot the explicitly selected external configuration and own process exit. + * @param bareModuleBaseUrl - optional installed-runtime base for bare plugins; + * omit it when the configuration project owns its plugin packages. + * @returns after process handlers are installed; process lifetime then belongs + * to stdin and signal events. + */ +export async function runJsonrpcAgent(bareModuleBaseUrl?: string): Promise { + /* v8 ignore start -- composition over tested app-boot/jsonrpc and executable acceptance paths */ + installFailLoud(NAME) + loadEnv(NAME) + + // Env wins over argv; empty values are absent. External config defines the deployment. + const fromEnv = process.env['DSH_CORDIS_CONFIG'] + const fromArgv = process.argv[2] + const requested = fromEnv !== undefined && fromEnv !== '' + ? fromEnv + : fromArgv !== undefined && fromArgv !== '' ? fromArgv : undefined + const configPath = requested === undefined ? undefined : resolveConfigPath(requested, undefined) + if (configPath === undefined || !existsSync(configPath)) { + process.stderr.write( + `usage: ${NAME} (or set DSH_CORDIS_CONFIG=, which wins); the config is required — there is no built-in fallback\n`, + ) + process.exit(1) + } + + const ctx = await boot(NAME, configPath, undefined, undefined, bareModuleBaseUrl) + let exiting = false + + async function disposeAndExit(code: number): Promise { + if (exiting) return + exiting = true + try { + await ctx.fiber.dispose() + } finally { + process.exit(code) + } + } + + process.stdin.on('end', () => { void disposeAndExit(0) }) + process.on('SIGTERM', () => { void disposeAndExit(0) }) + process.on('SIGINT', () => { void disposeAndExit(130) }) + /* v8 ignore stop */ +} diff --git a/packages/examples/jsonrpc-demo/tsdown.config.ts b/packages/examples/jsonrpc-demo/tsdown.config.ts index a8864a84a9..3609ebbc93 100644 --- a/packages/examples/jsonrpc-demo/tsdown.config.ts +++ b/packages/examples/jsonrpc-demo/tsdown.config.ts @@ -1,15 +1,21 @@ import { defineConfig } from 'tsdown' -/** - * Build the doc-only module and CLI entry; `tsc -b` supplies declarations. - */ -export default defineConfig({ - entry: ['lib/types/index.js', 'lib/types/invariant.js', 'lib/types/bin.js'], - outDir: 'lib', - format: ['esm'], - platform: 'node', - target: 'es2024', - fixedExtension: false, - dts: false, - clean: false, -}) +/** Builds each published entry as a self-contained file admitted by the package whitelist. */ +export default defineConfig([ + { + entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', + fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, + }, + { + entry: ['lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', + fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, + }, + { + entry: ['lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', + fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, + }, + { + entry: ['lib/types/packaged-bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', + fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, + }, +]) diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index d06131b7e7..8af8d3e1b9 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/python/sdk-runtime/README.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 python/sdk-runtime/README.md -README.md: 5c7c6f66083a1b56cc6b4aed9565e8b1be014ccc -README.zh.md: cef1478710d20d7faa612e50d0c2f8ec19e8716a +README.md: 912d88938c1a8b3c79dad19ab449c7f912d57e22 +README.zh.md: 5b82f33cfe1413e4fb6ceded04d9b6feca4c94ca diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index 5c7c6f6608..912d88938c 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -9,7 +9,7 @@ Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`, Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injected by the repo's `scripts/build-exe-for-python-sdk.ts` build and both gitignored: - **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg--` (platform: `linux`/`macos`; arch: `x64`/`arm64`). macOS builds also ship the native `-spawn-helper` sibling that `node-pty` uses there. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists. -- **node (dev-only)** — the full deploy closure under `runtime/node/` (`package.json` + `node_modules/`), executed as `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` on a system Node >= 22.19. It is the current checkout's source build, meant for repo-local development and verification only; it is never selected automatically and is excluded from distributions. +- **node (dev-only)** — the full deploy closure under `runtime/node/` (`package.json` + `node_modules/`), executed as `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js` on a system Node >= 22.19. It is the current checkout's source build, meant for repo-local development and verification only; it is never selected automatically and is excluded from distributions. Both carriers hold the same content, defined once: the [package.json](package.json) at this package's root is the deploy root of the single-exe pipeline — a pure dependency manifest (no code of its own) whose dependency closure IS both the plugin set compiled into the exe and the tree materialized into `runtime/node/`. Adding a plugin to the distribution means adding one dependency line there and rebuilding. diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index cef1478710..5b82f33cfe 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -9,7 +9,7 @@ Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`, 两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略: - **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg--`(platform:`linux`/`macos`;arch:`x64`/`arm64`)。macOS 构建还会随附 `node-pty` 在该平台使用的原生 `-spawn-helper` 伴随文件。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。 -- **node(仅限开发)**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。 +- **node(仅限开发)**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。 两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。 diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py index a3aa53ae80..5f3a94b3e3 100644 --- a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py +++ b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py @@ -9,7 +9,7 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's needs no Node installation. - **node (dev-only)**: the full deploy closure under ``runtime/node/`` (``package.json`` + ``node_modules/``), executed as ``node - runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`` on a + runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`` on a system Node >= 22.19. It is the current checkout's source build, never selected automatically, and excluded from wheel/sdist distributions. @@ -131,7 +131,12 @@ def _current_platform_tag() -> str: def _node_launch_args() -> tuple[str, str]: node_root = bundled_package_dir() / "runtime" / "node" bin_js = ( - node_root / "node_modules" / "@deepseek-ai" / "dsh-jsonrpc-demo" / "lib" / "bin.js" + node_root + / "node_modules" + / "@deepseek-ai" + / "dsh-jsonrpc-demo" + / "lib" + / "packaged-bin.js" ) if not bin_js.is_file(): raise FileNotFoundError( diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index 5f525ec358..34ecdda1c0 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -16,8 +16,8 @@ const root = resolve(import.meta.dirname, '..') /** The closure manifest whose dependencies define the executable. */ const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg' -/** The app entry inside the deployed closure. */ -const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js' +/** The closed-runtime app entry inside the deployed closure. */ +const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js' const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg' /** Default Node major; SEA mode requires at least Node 22. */ const DEFAULT_NODE_RANGE = 'node24'