mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
Merge remote-tracking branch 'origin/master' into dshw/pr-deepseek-harness-deepseek-harness-2300
# Conflicts: # python/sdk-runtime/package.json
This commit is contained in:
commit
18b0edb664
37 files changed
+1032
-145
No files matched your search
+2
-2
@@ -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: fa2f86893b730aa1ba020bd568d268ec8d9d6239
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 509bec18edb9923dd4d60d4ecf30d4fbcd9cc6d5
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.md: 40433d99e5d1aa569c3fdf094a280d3de62ad588
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 54030fa4b0742fbc282bc327b0ca22747e6a20bd
|
||||
+7
-5
@@ -36,19 +36,21 @@ Config discovery has two channels and fails loudly when both are missing: the `D
|
||||
|
||||
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`.
|
||||
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) 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`](../../../../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. 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 gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry.
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-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`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry.
|
||||
|
||||
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), called for linux-x64 by the [required Python runtime pull-request validation](../testing/2026-08-12-required-python-runtime-pull-request-ci.md), triggered explicitly by `workflow_dispatch` or the `build-exe` label for selected targets, and called for all targets by the [public publication workflow](../process/2026-08-11-python-publication-workflow.md). 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`](../../../../.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/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-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.
|
||||
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 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`](../../../../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 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 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 helpers, and unsupported platforms.
|
||||
[`scripts/build-python-release.py`](../../../../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`.
|
||||
|
||||
@@ -62,7 +64,7 @@ The exe's "must be explicitly configured" hard semantic is unchanged; the zero-c
|
||||
|
||||
## 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 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`.
|
||||
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.
|
||||
|
||||
|
||||
+7
-5
@@ -36,19 +36,21 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后
|
||||
|
||||
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` 覆盖。
|
||||
部署根目录是 [`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) 读取每个已发布的 `apps/cli/config/agent-presets/*/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 和 Prompts。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。
|
||||
|
||||
### 构建流水线与产物
|
||||
|
||||
[`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 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js`;`assets` 使用全量 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` 部署会省略这一副作用目录,因此构建器会把它从根安装目录复制到暂存闭包。macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-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.js`;`assets` 使用全量 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` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。
|
||||
|
||||
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[必需的 Python 运行时拉取请求验证](../testing/2026-08-12-required-python-runtime-pull-request-ci.md)调用它构建 linux-x64,手动派发 `workflow_dispatch` 或 PR(Pull Request)的 `build-exe` 标签可以显式选择构建目标,[公开发布工作流](../process/2026-08-11-python-publication-workflow.md)则调用它构建全部目标。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`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 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-sdk-jsonrpc-demo/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
|
||||
Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `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`](../../../../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,macOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`,或针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。
|
||||
[`scripts/build-python-release.py`](../../../../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_64`、`py3-none-manylinux_2_28_aarch64`,或针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。
|
||||
|
||||
exe「必须显式配置」的硬语义不变;零配置体验由包装层恢复:调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml`(`agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-sdk-jsonrpc-server` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`。
|
||||
|
||||
@@ -62,7 +64,7 @@ exe 内支持 `dsh-workflow-worker-thread` 与 `dsh-code-runtime-worker-thread`
|
||||
|
||||
## 测试
|
||||
|
||||
验证面分三层。机制层:`--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`。同一构建任务还会经 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` 的情况下运行。
|
||||
验证面分三层。机制层:`--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` 伴随文件调用 `glob` 与 `grep`。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 打开,直到轮次结束。
|
||||
|
||||
@@ -261,17 +261,20 @@ jobs:
|
||||
name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
|
||||
path: dist-python
|
||||
|
||||
- name: Install only the SDK into a clean venv and run zero-config
|
||||
- name: Install local SDK and runtime wheels into a clean venv
|
||||
env:
|
||||
VERSION: ${{ needs.plan.outputs.version }}
|
||||
RUNTIME_WHEEL: ${{ steps.runtime.outputs.wheel }}
|
||||
SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m venv "$RUNNER_TEMP/dsh-sdk-smoke"
|
||||
"$RUNNER_TEMP/dsh-sdk-smoke/bin/python" -m pip install \
|
||||
--find-links dist-python \
|
||||
deepseek-harness-sdk=="$VERSION"
|
||||
"dist-python/$SDK_WHEEL" \
|
||||
"dist-python/$RUNTIME_WHEEL"
|
||||
"$RUNNER_TEMP/dsh-sdk-smoke/bin/python" scripts/smoke-python-runtime.py \
|
||||
--scenario sdk-default
|
||||
"$RUNNER_TEMP/dsh-sdk-smoke/bin/python" scripts/smoke-python-runtime.py \
|
||||
--scenario sdk-mcp
|
||||
|
||||
- name: Check Linux GLIBC requirements
|
||||
if: runner.os == 'Linux'
|
||||
@@ -297,7 +300,8 @@ jobs:
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
RUNNER_ARCH: ${{ runner.arch }}
|
||||
VERSION: ${{ needs.plan.outputs.version }}
|
||||
RUNTIME_WHEEL: ${{ steps.runtime.outputs.wheel }}
|
||||
SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$RUNNER_ARCH" in
|
||||
@@ -305,10 +309,11 @@ jobs:
|
||||
ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
|
||||
*) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
|
||||
esac
|
||||
docker run --rm -e VERSION -e DSH_TELEMETRY_DISABLED -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
|
||||
docker run --rm -e RUNTIME_WHEEL -e SDK_WHEEL -e DSH_TELEMETRY_DISABLED -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
|
||||
/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
|
||||
/tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness-sdk=="$VERSION"
|
||||
/tmp/dsh-sdk/bin/python -m pip install "/work/dist-python/$SDK_WHEEL" "/work/dist-python/$RUNTIME_WHEEL"
|
||||
/tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default
|
||||
/tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp
|
||||
'
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -68,10 +68,15 @@ jobs:
|
||||
print(f"version={release['pep440_version'](repository_version)}")
|
||||
PY
|
||||
|
||||
- name: Install and run the published entry path
|
||||
- name: Install local release wheels and run the public entry path
|
||||
env:
|
||||
VERSION: ${{ steps.compatibility-version.outputs.version }}
|
||||
run: |
|
||||
python -m pip install --find-links dist "deepseek-harness-sdk==${{ steps.compatibility-version.outputs.version }}"
|
||||
python -m pip install \
|
||||
"dist/deepseek_harness_sdk-$VERSION-py3-none-any.whl" \
|
||||
"dist/deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl"
|
||||
python scripts/smoke-python-runtime.py --scenario sdk-default
|
||||
python scripts/smoke-python-runtime.py --scenario sdk-mcp
|
||||
|
||||
validate:
|
||||
name: Validate release candidate
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@ sdk-wheel:
|
||||
- python -m venv .wheel-smoke
|
||||
- .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness-sdk=="$DSH_WHEEL_VERSION"
|
||||
- .wheel-smoke/bin/python scripts/smoke-python-runtime.py --scenario sdk-default
|
||||
- .wheel-smoke/bin/python scripts/smoke-python-runtime.py --scenario sdk-mcp
|
||||
- |
|
||||
if [ "${PLATFORM#linux-}" != "$PLATFORM" ]; then
|
||||
readelf --version-info "$EXE" > glibc-versions.txt
|
||||
@@ -56,7 +57,7 @@ sdk-wheel:
|
||||
linux-arm64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
|
||||
*) echo "Unsupported Linux platform $PLATFORM"; exit 1 ;;
|
||||
esac
|
||||
docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness-sdk==$DSH_WHEEL_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default"
|
||||
docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness-sdk==$DSH_WHEEL_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp"
|
||||
fi
|
||||
- |
|
||||
if [ "$PLATFORM" = macos-arm64 ]; then
|
||||
|
||||
@@ -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/fs/tool-fs-search/README.md
|
||||
README.md: 83290df98260e977a8cd3ea808f491ea63e75857
|
||||
README.zh.md: b33fe002fc4ccc2bfe5fefec342f2044e3511078
|
||||
README.md: 84a3adc31f9c1580b90c038b0902e88b050f0340
|
||||
README.zh.md: 06ee6c7c1c1296c7c23b2c65cf244c1449def313
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The **model-facing filesystem discovery tools**—`glob`, `grep`—are backed by the **packaged ripgrep binary** (`@vscode/ripgrep`), not by `ctx.fs` provider methods and not by a system `rg` install. Registration is unconditional: the binary ships inside the npm dependency, so there is no load-time availability probe. Each call spawns the binary through the `ctx.subprocess` seam with a fixed argv vector (`--no-config` prepended so a host `RIPGREP_CONFIG_PATH` cannot inject a `--pre` preprocessor into the unconfined spawn; model-controlled values are plain argv elements — no shell layer exists, so no quoting applies), parses the raw `rg` output, and returns a workdir-relative canonical value. The package injects `tools`, `systemPrompt`, and `subprocess`—deliberately **not** `fs`; `ctx.spillStore` is read opportunistically with `ctx.get()` because formatted-result spill is optional.
|
||||
The **model-facing filesystem discovery tools**—`glob`, `grep`—are backed by a packaged ripgrep binary, not by `ctx.fs` provider methods and not by a system `rg` install. Ordinary Node deployments resolve the platform binary from `@vscode/ripgrep`; a pkg single-file runtime resolves the executable's co-located `-rg` sidecar and falls back to the dependency binary when that sidecar is absent. Registration is unconditional because both carriers package ripgrep, so there is no load-time availability probe. Each call spawns the resolved binary through the `ctx.subprocess` seam with a fixed argv vector (`--no-config` prepended so a host `RIPGREP_CONFIG_PATH` cannot inject a `--pre` preprocessor into the unconfined spawn; model-controlled values are plain argv elements — no shell layer exists, so no quoting applies), parses the raw `rg` output, and returns a workdir-relative canonical value. The package injects `tools`, `systemPrompt`, and `subprocess`—deliberately **not** `fs`; `ctx.spillStore` is read opportunistically with `ctx.get()` because formatted-result spill is optional.
|
||||
|
||||
```ts ignore-check
|
||||
// A deployment chooses how over-cap glob pages are selected.
|
||||
@@ -16,7 +16,7 @@ Why spawn-backed: local workspace discovery is naturally a process-backed `rg` w
|
||||
|
||||
## Deployment requirement: no host rg, co-located workdir/filesystem
|
||||
|
||||
The binary ships with the package on every supported platform (macOS/Linux/Windows, x64/arm64), so no host `rg` install is required and the tools register on every deployment. Returned paths are displayed relative to the resolved workdir (the calling agent's session cwd when present, else `process.cwd()`) and are follow-up-readable with `read` only when that workdir and the filesystem root are the same workspace. That co-location requirement carries no runtime cross-service validation; remote or virtual filesystem search waits for a shared workspace contract or a provider-specific search backend.
|
||||
Node deployments receive the `@vscode/ripgrep` platform package on supported macOS, Linux, and Windows x64/arm64 targets. Python SDK Linux and macOS wheels copy the target-native binary beside the single-file runtime as `<runtime>-rg`; `deepseek_harness_runtime.bundled_runtime_path()` rejects an incomplete wheel before launch. No carrier requires a host `rg` install. Returned paths are displayed relative to the resolved workdir (the calling agent's session cwd when present, else `process.cwd()`) and are follow-up-readable with `read` only when that workdir and the filesystem root are the same workspace. That co-location requirement carries no runtime cross-service validation; remote or virtual filesystem search waits for a shared workspace contract or a provider-specific search backend.
|
||||
|
||||
## Config
|
||||
|
||||
@@ -129,6 +129,6 @@ Append-only; newly visible content follows the reusable request prefix and does
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Search and file access have no shared-workspace proof** — returned paths are follow-up-readable only when the workdir and filesystem root denote the same workspace; the package performs no runtime cross-service validation.
|
||||
- **The packaged binary is fixed at dependency version** — `@vscode/ripgrep` covers the platforms it ships (macOS/Linux/Windows, x64/arm64); an unsupported platform or a corrupted install fails calls with `SEARCH_FAILED`. Remote or virtual filesystems need a co-located workspace or another search consumer.
|
||||
- **The packaged binary is fixed at dependency version** — Node deployments use the version selected by `@vscode/ripgrep`; Python single-file runtimes copy that target-native version into the required `-rg` sidecar. An unsupported platform or a corrupted installation fails with `SEARCH_FAILED`, while the Python runtime package rejects a missing sidecar before launch. Remote or virtual filesystems need a co-located workspace or another search consumer.
|
||||
- **The schemas expose one bounded page** — offset pagination, case-mode switches, alternate output modes, and provider-backed discovery remain outside this package; capped complete output requires a spill backend.
|
||||
- **Sampling, when enabled, groups by first path segment beneath the search root only** — an over-cap `glob` page balances across those top-level entries, so a result concentrated deeper (one busy directory inside an otherwise even tree) is still shown unevenly below that level; recursive balancing is deferred.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
**面向模型的文件系统发现工具**(`glob`、`grep`)由 **打包的 ripgrep 二进制**(`@vscode/ripgrep`)支持,而不是由 `ctx.fs` 提供方方法或系统 `rg` 安装支持。注册是无条件的:二进制随 NPM 依赖一起交付,因此没有加载期可用性探针。每次调用都通过 `ctx.subprocess` seam 以固定 argv 向量 spawn 该二进制(前缀 `--no-config`,使宿主的 `RIPGREP_CONFIG_PATH` 无法向不受约束的 spawn 注入 `--pre` 预处理器;模型控制的值是普通 argv 元素——不存在 shell 层,因此不涉及 shell 引号处理),解析原始 `rg` 输出,并返回相对于工作目录的规范值。本包注入 `tools`、`systemPrompt` 和 `subprocess`,有意**不**注入 `fs`;格式化结果 spill 为可选功能,因此机会性读取 `ctx.spillStore`,调用方式为 `ctx.get()`。
|
||||
**面向模型的文件系统发现工具**(`glob`、`grep`)由打包的 ripgrep 二进制支持,而不是由 `ctx.fs` 提供方方法或系统 `rg` 安装支持。普通 Node 部署从 `@vscode/ripgrep` 解析平台二进制;pkg 单文件运行时解析与可执行程序共置的 `-rg` 伴随文件,伴随文件缺失时回退到依赖中的二进制。两种载体均打包 ripgrep,因此注册是无条件的,没有加载期可用性探针。每次调用都通过 `ctx.subprocess` seam 以固定 argv 向量 spawn 解析出的二进制(前缀 `--no-config`,使宿主的 `RIPGREP_CONFIG_PATH` 无法向不受约束的 spawn 注入 `--pre` 预处理器;模型控制的值是普通 argv 元素——不存在 shell 层,因此不涉及 shell 引号处理),解析原始 `rg` 输出,并返回相对于工作目录的规范值。本包注入 `tools`、`systemPrompt` 和 `subprocess`,有意**不**注入 `fs`;格式化结果 spill 为可选功能,因此机会性读取 `ctx.spillStore`,调用方式为 `ctx.get()`。
|
||||
|
||||
```ts ignore-check
|
||||
// A deployment chooses how over-cap glob pages are selected.
|
||||
@@ -16,7 +16,7 @@ await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-
|
||||
|
||||
## 部署要求:无需宿主 rg,但工作目录与文件系统需共置
|
||||
|
||||
二进制随包交付,覆盖所有受支持平台(macOS/Linux/Windows,x64/arm64),因此无需宿主 `rg` 安装,工具在每个部署上都注册。返回路径会相对于解析后的工作目录显示(调用方 agent(智能体)有会话 cwd 时使用该 cwd,否则使用 `process.cwd()`);只有该工作目录与文件系统根目录是同一工作区时,才能用 `read` 继续读取。这项共置要求不附带运行时跨服务校验;远程或虚拟文件系统搜索需等待共享工作区约定或特定提供方的搜索后端。
|
||||
Node 部署在受支持的 macOS、Linux 与 Windows x64/arm64 目标上获得 `@vscode/ripgrep` 平台包。Python SDK 的 Linux 与 macOS wheel 将目标原生二进制复制到单文件运行时旁,命名为 `<runtime>-rg`;`deepseek_harness_runtime.bundled_runtime_path()` 会在启动前拒绝不完整的 wheel。两种载体均不要求宿主安装 `rg`。返回路径会相对于解析后的工作目录显示(调用方 agent(智能体)有会话 cwd 时使用该 cwd,否则使用 `process.cwd()`);只有该工作目录与文件系统根目录是同一工作区时,才能用 `read` 继续读取。这项共置要求不附带运行时跨服务校验;远程或虚拟文件系统搜索需等待共享工作区约定或特定提供方的搜索后端。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -129,6 +129,6 @@ glob 描述声明了配置的超过上限排序方式。生成的 [`glob` 和 `g
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **搜索与文件访问没有共享工作区证明**——只有当工作目录与文件系统根目录指向同一工作区时,返回路径才可继续读取;本包不执行运行时跨服务校验。
|
||||
- **打包二进制固定在依赖版本上**——`@vscode/ripgrep` 覆盖其随附的平台(macOS/Linux/Windows,x64/arm64);不支持的平台或损坏的安装会以 `SEARCH_FAILED` 使调用失败。远程或虚拟文件系统需要共置的工作区或另一个搜索消费方。
|
||||
- **打包二进制固定在依赖版本上**——Node 部署使用 `@vscode/ripgrep` 选择的版本;Python 单文件运行时将对应目标的原生版本复制为必需的 `-rg` 伴随文件。不支持的平台或损坏的安装会以 `SEARCH_FAILED` 使调用失败,Python 运行时包则会在启动前拒绝缺少伴随文件的安装。远程或虚拟文件系统需要共置的工作区或另一个搜索消费方。
|
||||
- **schema 只暴露一个有界页面**——偏移分页、大小写开关、替代输出模式与提供方支撑的发现仍不在本包范围内;达到上限的完整输出需要 spill 后端。
|
||||
- **启用采样时仅按搜索根正下方的第一段路径分组**——超过上限的 `glob` 页面在这些顶层条目之间平衡,因此集中在更深处的结果(一棵均匀树里某个繁忙目录)在该层级之下仍会呈现不均;递归平衡被延期。
|
||||
@@ -19,6 +19,7 @@
|
||||
* @module @deepseek-ai/dsh-tool-fs-search/search-core
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs'
|
||||
import { isAbsolute, relative, sep } from 'node:path'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
@@ -158,18 +159,21 @@ let rgPathPromise: Promise<string> | undefined
|
||||
/**
|
||||
* The packaged ripgrep binary path, resolved lazily once per process.
|
||||
*
|
||||
* `@vscode/ripgrep` resolves its platform package (`@vscode/ripgrep-<platform>
|
||||
* -<arch>`) at module evaluation, so a static import would turn a missing or
|
||||
* corrupt platform package (`pnpm install --omit=optional`, partial install)
|
||||
* into a failure of the whole Loader composition. Resolving at the call
|
||||
* boundary keeps that failure at the first search call as `SEARCH_FAILED` —
|
||||
* the package's documented no-load-time-probe contract.
|
||||
* A single-file runtime uses the executable's `-rg` sidecar because a native
|
||||
* helper cannot be spawned from pkg's virtual filesystem. Node-mode builds
|
||||
* fall back to the platform package selected by `@vscode/ripgrep`. Resolving
|
||||
* at the call boundary keeps a missing or corrupt binary at the first search
|
||||
* call as `SEARCH_FAILED`, rather than failing the Loader composition.
|
||||
*
|
||||
* @returns the packaged binary's absolute path; the memoized promise rejects
|
||||
* when the platform package cannot be resolved.
|
||||
*/
|
||||
export function resolveRgPath(): Promise<string> {
|
||||
rgPathPromise ??= import('@vscode/ripgrep').then(module => module.rgPath)
|
||||
rgPathPromise ??= Promise.resolve().then(async () => {
|
||||
const executableSidecar = `${process.execPath}-rg`
|
||||
if ('pkg' in process && existsSync(executableSidecar)) return executableSidecar
|
||||
return (await import('@vscode/ripgrep')).rgPath
|
||||
})
|
||||
return rgPathPromise
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { dependencyRgPath, existsSync } = vi.hoisted(() => ({
|
||||
dependencyRgPath: '/node_modules/@vscode/ripgrep/bin/rg',
|
||||
existsSync: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs')>()
|
||||
return { ...actual, existsSync }
|
||||
})
|
||||
|
||||
vi.mock('@vscode/ripgrep', () => ({ rgPath: dependencyRgPath }))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
existsSync.mockReset()
|
||||
Reflect.deleteProperty(process, 'pkg')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Reflect.deleteProperty(process, 'pkg')
|
||||
})
|
||||
|
||||
describe('ripgrep resolution', () => {
|
||||
it('uses the native sidecar beside the current executable', async () => {
|
||||
Reflect.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
existsSync.mockReturnValue(true)
|
||||
const sidecar = `${process.execPath}-rg`
|
||||
const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
|
||||
|
||||
await expect(resolveRgPath()).resolves.toBe(sidecar)
|
||||
expect(existsSync).toHaveBeenCalledWith(sidecar)
|
||||
})
|
||||
|
||||
it('uses the dependency binary in an ordinary Node process', async () => {
|
||||
existsSync.mockReturnValue(true)
|
||||
const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
|
||||
|
||||
await expect(resolveRgPath()).resolves.toBe(dependencyRgPath)
|
||||
expect(existsSync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses the dependency binary when a packaged runtime has no sidecar', async () => {
|
||||
Reflect.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
existsSync.mockReturnValue(false)
|
||||
const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
|
||||
|
||||
await expect(resolveRgPath()).resolves.toBe(dependencyRgPath)
|
||||
expect(existsSync).toHaveBeenCalledWith(`${process.execPath}-rg`)
|
||||
})
|
||||
})
|
||||
@@ -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/sdk/server/README.md
|
||||
README.md: 5377b4fcf425cc5e10497e9d40fdddc075d52a10
|
||||
README.zh.md: dcca65a7175bb2774460bf265d98e41439df6a01
|
||||
README.md: 29ad5840b9d70c9c22ecd387730ba21ce89cbe07
|
||||
README.zh.md: f5afb255c83fa62fa4c2891cf725ffbf4c93e77d
|
||||
@@ -22,7 +22,7 @@ The plugin answers `shutdown`, flushes the response, disposes the root context s
|
||||
|
||||
## Wire notes
|
||||
|
||||
`initialize.serverInfo.name` is the wire-stable `deepseek-harness-sdk-runtime`. An optional positive `initialize.maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants; invalid values reject initialization, while omission sends no SDK cap and allows the selected adapter or provider route default to apply. `session/prompt` queues one identified user message and immediately returns `{ messageId }`. The server streams every durable fact as `session.event` and every whole-agent lifecycle transition as `session.status`; it does not assign an assistant message or `turn/end` to that prompt. Independent requests may enqueue more work on the same session. Persistence roots and persona come from `cordis.yml`.
|
||||
`initialize` is the runtime-readiness boundary: when the server is mounted by a Loader composition, it waits for the current plugin tree to settle before replying, so async sibling capabilities such as initial MCP tool discovery are visible to the first prompt. Hand-built contexts without Loader remain immediately usable. `initialize.serverInfo.name` is the wire-stable `deepseek-harness-sdk-runtime`. An optional positive `initialize.maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants; invalid values reject initialization, while omission sends no SDK cap and allows the selected adapter or provider route default to apply. `session/prompt` queues one identified user message and immediately returns `{ messageId }`. The server streams every durable fact as `session.event` and every whole-agent lifecycle transition as `session.status`; it does not assign an assistant message or `turn/end` to that prompt. Independent requests may enqueue more work on the same session. Persistence roots and persona come from `cordis.yml`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写
|
||||
|
||||
## 协议说明
|
||||
|
||||
`initialize.serverInfo.name` 的协议稳定值为 `deepseek-harness-sdk-runtime`。可选的正整数 `initialize.maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限;非法值会使初始化失败,省略时则不发送 SDK 上限,并应用所选适配器或提供方路由的默认值。`session/prompt` 将一条带标识的用户消息排入队列,并立即返回 `{ messageId }`。服务器将每个持久事实作为 `session.event` 流式发出,并将整个 agent 生命周期的每次状态转换作为 `session.status` 发出;它不会把某条助手消息或 `turn/end` 归属于该提示词。同一会话上的独立请求可以继续排入更多工作。持久化根目录和 persona 由 `cordis.yml` 提供。
|
||||
`initialize` 是运行时就绪边界:服务器由 Loader 组合挂载时,会等待当前插件树完成所有加载任务后再响应,因此首次提示词能够看到 MCP 初始工具发现等异步同级能力。没有 Loader 的手工组装上下文仍可立即使用。`initialize.serverInfo.name` 的协议稳定值为 `deepseek-harness-sdk-runtime`。可选的正整数 `initialize.maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限;非法值会使初始化失败,省略时则不发送 SDK 上限,并应用所选适配器或提供方路由的默认值。`session/prompt` 将一条带标识的用户消息排入队列,并立即返回 `{ messageId }`。服务器将每个持久事实作为 `session.event` 流式发出,并将整个 agent 生命周期的每次状态转换作为 `session.status` 发出;它不会把某条助手消息或 `turn/end` 归属于该提示词。同一会话上的独立请求可以继续排入更多工作。持久化根目录和 persona 由 `cordis.yml` 提供。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -74,6 +74,12 @@ export function apply(ctx: Context, config: JsonRpcConfig): void {
|
||||
}
|
||||
|
||||
transport.onRequest(async (method, params) => {
|
||||
// `initialize` is the SDK's readiness boundary. This plugin can activate
|
||||
// before async sibling Loader entries (for example an MCP client's initial
|
||||
// tool discovery), so do not advertise a ready runtime until the complete
|
||||
// current tree has settled. A hand-built context without Loader remains
|
||||
// immediately usable.
|
||||
if (method === 'initialize') await ctx.get('loader')?.await()
|
||||
const result = await server.handleRequest(method, params)
|
||||
if (method === 'shutdown') {
|
||||
// Run after the handler result is written; the task then flushes, disposes, and exits.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { tmpdir } from 'node:os'
|
||||
import { PassThrough, Writable } from 'node:stream'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo'
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import * as jsonrpc from '../src/index.ts'
|
||||
@@ -57,12 +58,17 @@ async function settle(): Promise<void> {
|
||||
/** Mount the real plugin on a minimal harness with in-memory stdio and exit. */
|
||||
async function mountPlugin(
|
||||
storageDir: string,
|
||||
options: { writeDelayMs?: number; failFlush?: boolean } = {},
|
||||
options: {
|
||||
writeDelayMs?: number
|
||||
failFlush?: boolean
|
||||
beforeServer?: (ctx: Context) => Promise<void> | void
|
||||
} = {},
|
||||
): Promise<ApplyHarness> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(agentCore, { workspaceContext: false })
|
||||
await ctx.plugin(JsonlSessionPersistence, { root: storageDir })
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
await options.beforeServer?.(ctx)
|
||||
|
||||
const input = new PassThrough()
|
||||
const events: WireEvent[] = []
|
||||
@@ -170,6 +176,58 @@ describe('dsh-sdk-jsonrpc-server plugin apply', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not answer initialize until async sibling Loader entries settle', async () => {
|
||||
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-readiness-'))
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
|
||||
let markStarted!: () => void
|
||||
let release!: () => void
|
||||
const started = new Promise<void>((resolve) => { markStarted = resolve })
|
||||
const ready = new Promise<void>((resolve) => { release = resolve })
|
||||
let delayedEntry: Promise<string> | undefined
|
||||
const harness = await mountPlugin(storageDir, {
|
||||
beforeServer: async (ctx) => {
|
||||
await ctx.plugin(Loader)
|
||||
ctx.loader.builtins['delayed-readiness'] = {
|
||||
async apply() {
|
||||
markStarted()
|
||||
await ready
|
||||
},
|
||||
}
|
||||
delayedEntry = ctx.loader.create({ name: 'cordis:delayed-readiness' })
|
||||
await started
|
||||
},
|
||||
})
|
||||
try {
|
||||
const initialize = {
|
||||
jsonrpc: '2.0',
|
||||
id: 'init-delayed',
|
||||
method: 'initialize',
|
||||
params: { cwd: storageDir, provider: 'deepseek-official', model: 'apply-model' },
|
||||
}
|
||||
const probe = { jsonrpc: '2.0', id: 'probe-during-delay', method: 'nope/unknown' }
|
||||
harness.sendRaw(`${JSON.stringify(initialize)}\n${JSON.stringify(probe)}\n`)
|
||||
|
||||
// The transport processes independent requests concurrently. Receiving
|
||||
// this later probe proves the preceding initialize handler has reached
|
||||
// its Loader wait, without relying on a scheduler delay.
|
||||
await harness.waitForFrame(frame => frame.id === 'probe-during-delay', 'probe while initialize waits')
|
||||
expect(harness.frames().some(frame => frame.id === 'init-delayed')).toBe(false)
|
||||
|
||||
release()
|
||||
await delayedEntry
|
||||
const response = await harness.waitForFrame(frame => frame.id === 'init-delayed', 'initialize response after Loader settlement')
|
||||
expect(response).toMatchObject({
|
||||
id: 'init-delayed',
|
||||
result: { serverInfo: { name: 'deepseek-harness-sdk-runtime' } },
|
||||
})
|
||||
} finally {
|
||||
release()
|
||||
await Promise.allSettled(delayedEntry === undefined ? [] : [delayedEntry])
|
||||
await harness.dispose()
|
||||
await rm(storageDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('drives a session/prompt turn end-to-end and forwards session notifications as output frames', async () => {
|
||||
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-apply-prompt-'))
|
||||
const llmServer = await mockCompletionServer()
|
||||
|
||||
Generated
+21
@@ -8514,6 +8514,9 @@ importers:
|
||||
'@deepseek-ai/dsh-agent-spine-demo':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/examples/agent-spine-demo
|
||||
'@deepseek-ai/dsh-agent-tool-presentation':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/core/agent-tool-presentation
|
||||
'@deepseek-ai/dsh-anonymous-user-id':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/identity/anonymous-user-id
|
||||
@@ -8535,6 +8538,9 @@ importers:
|
||||
'@deepseek-ai/dsh-code-runtime-worker-thread':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/code-runtime/code-runtime-worker-thread
|
||||
'@deepseek-ai/dsh-command-compact':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/compaction/command-compact
|
||||
'@deepseek-ai/dsh-command-goal':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/goal/command-goal
|
||||
@@ -8610,12 +8616,18 @@ importers:
|
||||
'@deepseek-ai/dsh-llm-retry':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/llm/llm-retry
|
||||
'@deepseek-ai/dsh-mcp-client':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/mcp/mcp-client
|
||||
'@deepseek-ai/dsh-output-retention':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/util/output-retention
|
||||
'@deepseek-ai/dsh-permission-presets':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/interaction/permission-presets
|
||||
'@deepseek-ai/dsh-persona':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/preset/persona
|
||||
'@deepseek-ai/dsh-plan-mode':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/plan/plan-mode
|
||||
@@ -8691,6 +8703,9 @@ importers:
|
||||
'@deepseek-ai/dsh-skill-filesystem':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/skill/skill-filesystem
|
||||
'@deepseek-ai/dsh-spill':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/spill/spill
|
||||
'@deepseek-ai/dsh-subagent':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/subagent/subagent
|
||||
@@ -8745,12 +8760,18 @@ importers:
|
||||
'@deepseek-ai/dsh-tool-fs':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/fs/tool-fs
|
||||
'@deepseek-ai/dsh-tool-fs-search':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/fs/tool-fs-search
|
||||
'@deepseek-ai/dsh-tool-goal':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/goal/tool-goal
|
||||
'@deepseek-ai/dsh-tool-jobs':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/jobs/tool-jobs
|
||||
'@deepseek-ai/dsh-tool-ralph':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/workflow/tool-ralph
|
||||
'@deepseek-ai/dsh-tool-skill':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/skill/tool-skill
|
||||
|
||||
@@ -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/development.md
|
||||
development.md: fe62a109f2643afe0b9be1ed51b86be0b9fa731f
|
||||
development.zh.md: d4ab9850d6c83dc17a740240f97cef89d61faaa0
|
||||
development.md: 617d030294dafa51aea513adb811bb5f377431c9
|
||||
development.zh.md: 2049ad12856c4107788aec168e3724965cebd5af
|
||||
+10
-2
@@ -61,10 +61,18 @@ The root `package.json` version is authoritative for both Python distributions.
|
||||
Build the pure SDK wheel once and one runtime wheel on each native platform:
|
||||
|
||||
```sh
|
||||
version="$(node -p "require('./package.json').version")"
|
||||
version="$(python - <<'PY'
|
||||
import runpy
|
||||
|
||||
release = runpy.run_path("scripts/build-python-release.py")
|
||||
print(release["pep440_version"](release["repository_version"]()))
|
||||
PY
|
||||
)"
|
||||
python scripts/build-python-release.py --package sdk --output-dir dist-python
|
||||
python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python
|
||||
pip install --find-links dist-python deepseek-harness-sdk=="$version"
|
||||
pip install \
|
||||
"dist-python/deepseek_harness_sdk-$version-py3-none-any.whl" \
|
||||
"dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl"
|
||||
```
|
||||
|
||||
The runtime distribution is wheel-only. The release pipeline publishes three platform wheels with the pure SDK wheel: Linux x64, Linux arm64, and macOS 14 or newer on arm64. A `python-v<repository-version>` tag is accepted only when it matches the repository version; prerelease repository versions such as `0.0.1-rc.1` use their normalized PEP 440 spelling, such as `0.0.1rc1`, inside wheel filenames and metadata.
|
||||
|
||||
@@ -61,10 +61,18 @@ with DeepSeekHarness() as harness:
|
||||
纯 SDK wheel 包只需构建一次;每个原生平台分别构建一个运行时 wheel 包:
|
||||
|
||||
```sh
|
||||
version="$(node -p "require('./package.json').version")"
|
||||
version="$(python - <<'PY'
|
||||
import runpy
|
||||
|
||||
release = runpy.run_path("scripts/build-python-release.py")
|
||||
print(release["pep440_version"](release["repository_version"]()))
|
||||
PY
|
||||
)"
|
||||
python scripts/build-python-release.py --package sdk --output-dir dist-python
|
||||
python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python
|
||||
pip install --find-links dist-python deepseek-harness-sdk=="$version"
|
||||
pip install \
|
||||
"dist-python/deepseek_harness_sdk-$version-py3-none-any.whl" \
|
||||
"dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl"
|
||||
```
|
||||
|
||||
运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布三个平台 wheel 包:Linux x64、Linux arm64 和 macOS 14 或更高版本的 arm64。只有与仓库版本匹配时,才接受 `python-v<repository-version>` 标签;`0.0.1-rc.1` 之类的仓库预发布版本在 wheel 包文件名和元数据中使用规范化的 PEP 440 写法,例如 `0.0.1rc1`。
|
||||
|
||||
@@ -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: 592ce091f3b6c4bc151dfdee9d313b7c970c95de
|
||||
README.zh.md: 0a29281a39c885bc3c40e017bdc9475e0065605f
|
||||
README.md: 597d69a803a7cd1204fd48456f8e1ba18d9786e5
|
||||
README.zh.md: 30dc6b2a34709ac76d16133be34f3d0c87f93b8a
|
||||
@@ -8,19 +8,21 @@ 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>-<arch>` (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.
|
||||
- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (platform: `linux`/`macos`; arch: `x64`/`arm64`) with a target-native ripgrep `-rg` sidecar. 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-sdk-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](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/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.
|
||||
|
||||
The bundled plugin set includes `@deepseek-ai/dsh-mcp-client`, so an external Cordis config can connect to stdio or Streamable HTTP MCP servers and expose their tools to the model. The wheel does not bundle MCP server programs or credentials: a stdio config supplies its executable and arguments, while a Streamable HTTP config supplies its URL and headers. The bridge supports MCP tools; MCP Resources and Prompts remain unsupported.
|
||||
|
||||
A missing exe raises `FileNotFoundError` naming both acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or install the matching platform runtime wheel produced by the `build-exe-for-python-sdk` CI workflow. A missing dev-only node carrier names its sole route, the build script. The workflow retains wheels rather than standalone executable archives. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers.
|
||||
|
||||
Each wheel contains exactly one runtime executable. The macOS wheel also contains its matching native spawn helper; a missing sidecar makes that installation incomplete and is a hard startup error, even for a selected Cordis composition that does not use PTY tools. Linux wheels contain no spawn helper because `node-pty` uses the staged `pty.node` addon directly. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_14_0_arm64`; the macOS tag conservatively matches the bundled Node 24 executable's macOS 13.5 deployment target. This package's `platforms.json` owns the fixed tag and executable-name pairs used by both the repository release builder and the isolated build hook. The build hook rejects `py3-none-any`, absent or multiple runtime files, non-executable files, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-v<repository-version>` release tag must match it.
|
||||
Each wheel contains exactly one runtime executable and its matching ripgrep `-rg` sidecar. The macOS wheel also contains its matching native spawn helper; any missing sidecar makes that installation incomplete and is a hard startup error, even for a selected Cordis composition that does not use filesystem-search or PTY tools. Linux wheels contain no spawn helper because `node-pty` uses the staged `pty.node` addon directly. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_14_0_arm64`; the macOS tag conservatively matches the bundled Node 24 executable's macOS 13.5 deployment target. This package's `platforms.json` owns the fixed tag and executable-name pairs used by both the repository release builder and the isolated build hook. The build hook rejects `py3-none-any`, absent or multiple runtime executables, missing or extra sidecars, non-executable files, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-v<repository-version>` release tag must match it.
|
||||
|
||||
## Resolution API
|
||||
|
||||
- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]` — the argv tuple that launches the bundled runtime: `(exe_path,)` in exe mode, `(node_path, bin_js_path)` in node mode. Mode selection: explicit argument > `DSH_RUNTIME_MODE` env var (`exe` | `node`) > automatic. Automatic resolution finds the production exe ONLY — the dev-only node carrier must be opted into explicitly so a production deployment can never silently ride on a source build.
|
||||
- `bundled_runtime_path() -> Path` — the platform exe path (exe carrier only; on macOS it validates that the required sibling `-spawn-helper` is also installed). The node carrier has no single-path equivalent and launches via the argv tuple above.
|
||||
- `bundled_runtime_path() -> Path` — the platform exe path (exe carrier only); it validates the required sibling `-rg` sidecar on every platform and the `-spawn-helper` sidecar on macOS. The node carrier has no single-path equivalent and launches via the argv tuple above.
|
||||
- `bundled_default_config_path() -> Path` — the checked-in default config (see below).
|
||||
- `bundled_package_dir() -> Path` — the installed package data root.
|
||||
|
||||
|
||||
@@ -8,19 +8,21 @@ 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>-<arch>`(platform:`linux`/`macos`;arch:`x64`/`arm64`)。macOS 构建还会随附 `node-pty` 在该平台使用的原生 `-spawn-helper` 伴随文件。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。
|
||||
- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<arch>`(platform:`linux`/`macos`;arch:`x64`/`arm64`),以及匹配目标平台的 ripgrep `-rg` 伴随文件。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-sdk-jsonrpc-demo/lib/packaged-bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。
|
||||
|
||||
两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
|
||||
|
||||
内置插件集合包含 `@deepseek-ai/dsh-mcp-client`,因此外部 Cordis 配置可以连接 stdio 或 Streamable HTTP MCP server,并向模型提供这些 server 的工具。wheel 包不包含 MCP server 程序或凭据:stdio 配置需要提供可执行程序及其参数,Streamable HTTP 配置需要提供 URL 和请求头。该桥接仅支持 MCP 工具,尚不支持 MCP Resources 与 Prompts。
|
||||
|
||||
exe 缺失时抛出 `FileNotFoundError`,并写明两种获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或安装 `build-exe-for-python-sdk` CI 工作流生成的对应平台运行时 wheel 包。仅限开发的 node 载体缺失时只提示构建脚本这一条途径。该工作流只保留 wheel 包,不保留独立 exe 归档。获取策略与查找接口刻意分离,之后可以换成按需下载而不改动任何调用方。
|
||||
|
||||
每个 wheel 包只包含一个运行时可执行文件。macOS wheel 包还包含与其匹配的原生 spawn helper;缺少伴随文件意味着该安装不完整,并会在启动时硬失败,即使所选 Cordis 组合不使用 PTY 工具也是如此。Linux wheel 包不包含 spawn helper,因为 `node-pty` 直接使用暂存的 `pty.node` 原生插件。固定标签为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_14_0_arm64`;macOS 标签保守匹配内置 Node 24 可执行文件的 macOS 13.5 部署目标。本包的 `platforms.json` 统一定义仓库发行构建器与隔离构建钩子使用的固定标签和可执行文件名。构建钩子会拒绝 `py3-none-any`、不存在运行时文件、存在多个运行时文件、文件不可执行以及不支持的平台标签。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-v<repository-version>` 发布标签必须与其匹配。
|
||||
每个 wheel 包只包含一个运行时可执行文件及其匹配的 ripgrep `-rg` 伴随文件。macOS wheel 包还包含与其匹配的原生 spawn helper;缺少任一伴随文件都意味着该安装不完整,并会在启动时硬失败,即使所选 Cordis 组合不使用文件系统搜索或 PTY 工具也是如此。Linux wheel 包不包含 spawn helper,因为 `node-pty` 直接使用暂存的 `pty.node` 原生插件。固定标签为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_14_0_arm64`;macOS 标签保守匹配内置 Node 24 可执行文件的 macOS 13.5 部署目标。本包的 `platforms.json` 统一定义仓库发行构建器与隔离构建钩子使用的固定标签和可执行文件名。构建钩子会拒绝 `py3-none-any`、不存在或存在多个运行时可执行文件、缺失或多余的伴随文件、文件不可执行以及不支持的平台标签。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-v<repository-version>` 发布标签必须与其匹配。
|
||||
|
||||
## 解析 API
|
||||
|
||||
- `resolve_bundled_launch_args(mode=None) -> tuple[str, ...]`——启动内置运行时的 argv 元组:exe 模式下为 `(exe_path,)`,node 模式下为 `(node_path, bin_js_path)`。模式选择:显式参数 > `DSH_RUNTIME_MODE` 环境变量(`exe` | `node`)> 自动。自动解析只找生产 exe——仅限开发的 node 载体必须显式选用,从而生产部署绝不会悄悄跑在源码构建上。
|
||||
- `bundled_runtime_path() -> Path`——平台 exe 路径(仅 exe 载体,并会在 macOS 上校验必要的 `-spawn-helper` 伴随文件也已安装)。node 载体没有单一路径的等价物,经由上面的 argv 元组启动。
|
||||
- `bundled_runtime_path() -> Path`——平台 exe 路径(仅 exe 载体);它会在所有平台校验必要的 `-rg` 伴随文件,并在 macOS 上额外校验 `-spawn-helper` 伴随文件。node 载体没有单一路径的等价物,经由上面的 argv 元组启动。
|
||||
- `bundled_default_config_path() -> Path`——检入的默认配置(见下文)。
|
||||
- `bundled_package_dir() -> Path`——已安装包的数据根目录。
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class RuntimeBuildHook(BuildHookInterface):
|
||||
expected_executable = matches[0][1]
|
||||
runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
|
||||
runtime_files = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else [])
|
||||
expected_files = [expected_executable]
|
||||
expected_files = [expected_executable, f"{expected_executable}-rg"]
|
||||
if "-macos-" in expected_executable:
|
||||
expected_files.append(f"{expected_executable}-spawn-helper")
|
||||
found_files = [path.name for path in runtime_files]
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-tool-presentation": "workspace:^",
|
||||
"@deepseek-ai/dsh-app-boot": "workspace:^",
|
||||
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||
"@deepseek-ai/dsh-shell": "workspace:^",
|
||||
@@ -22,6 +23,7 @@
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-code-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-code-runtime-worker-thread": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-compact": "workspace:^",
|
||||
"@deepseek-ai/dsh-command-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-compaction": "workspace:^",
|
||||
@@ -46,9 +48,11 @@
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-retry": "workspace:^",
|
||||
"@deepseek-ai/dsh-mcp-client": "workspace:^",
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-permission-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||
"@deepseek-ai/dsh-persona": "workspace:^",
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-terminal": "workspace:^",
|
||||
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
|
||||
@@ -72,6 +76,7 @@
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
|
||||
"@deepseek-ai/dsh-spill": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent-acp": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent-fork-in-process": "workspace:^",
|
||||
@@ -90,7 +95,9 @@
|
||||
"@deepseek-ai/dsh-tool-bash-persistent": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-fs-search": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
|
||||
|
||||
@@ -5,8 +5,8 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's
|
||||
|
||||
- **exe (production)**: single-file Node executables named
|
||||
``dsh-jsonrpc-agent-pkg-<platform>-<arch>`` (platform in {linux, macos}, arch in
|
||||
{x64, arm64}); macOS also uses a sibling ``-spawn-helper``. The target machine
|
||||
needs no Node installation.
|
||||
{x64, arm64}) with a sibling ``-rg`` executable; macOS also uses a sibling
|
||||
``-spawn-helper``. The target machine 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-sdk-jsonrpc-demo/lib/packaged-bin.js`` on a
|
||||
@@ -71,10 +71,11 @@ def bundled_runtime_path() -> Path:
|
||||
"""Absolute path of the bundled single-file runtime executable for the current platform.
|
||||
|
||||
Raises FileNotFoundError when the platform is unsupported, the executable
|
||||
has not been placed into this package, or the required macOS spawn helper is
|
||||
missing; the message names the acquisition routes (acquisition strategy is
|
||||
deliberately separate from this lookup interface, so an on-demand download
|
||||
can replace it without touching callers).
|
||||
has not been placed into this package, the required ripgrep sidecar is
|
||||
missing, or the required macOS spawn helper is missing; the message names
|
||||
the acquisition routes (acquisition strategy is deliberately separate from
|
||||
this lookup interface, so an on-demand download can replace it without
|
||||
touching callers).
|
||||
"""
|
||||
tag = _current_platform_tag()
|
||||
path = bundled_package_dir() / "runtime" / f"dsh-jsonrpc-agent-pkg-{tag}"
|
||||
@@ -83,6 +84,12 @@ def bundled_runtime_path() -> Path:
|
||||
f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
|
||||
+ _EXE_ACQUISITION_HINT
|
||||
)
|
||||
ripgrep = Path(f"{path}-rg")
|
||||
if not ripgrep.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"deepseek-harness-runtime-bin is missing the ripgrep sidecar at {ripgrep}. "
|
||||
+ _EXE_ACQUISITION_HINT
|
||||
)
|
||||
if tag.startswith("macos-"):
|
||||
helper = Path(f"{path}-spawn-helper")
|
||||
if not helper.is_file():
|
||||
|
||||
@@ -92,6 +92,10 @@ def test_stage_runtime_copies_platform_payload(
|
||||
executable.write_bytes(b"runtime")
|
||||
executable.chmod(0o755)
|
||||
expected = {executable.name: b"runtime"}
|
||||
ripgrep = Path(f"{executable}-rg")
|
||||
ripgrep.write_bytes(b"ripgrep")
|
||||
ripgrep.chmod(0o755)
|
||||
expected[ripgrep.name] = b"ripgrep"
|
||||
if with_helper:
|
||||
spawn_helper = Path(f"{executable}-spawn-helper")
|
||||
spawn_helper.write_bytes(b"helper")
|
||||
|
||||
@@ -51,7 +51,10 @@ def test_runtime_requires_spawn_helper_only_on_macos(
|
||||
runtime_dir.mkdir()
|
||||
linux = runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64"
|
||||
linux.touch()
|
||||
(runtime_dir / "dsh-jsonrpc-agent-pkg-macos-arm64").touch()
|
||||
Path(f"{linux}-rg").touch()
|
||||
macos = runtime_dir / "dsh-jsonrpc-agent-pkg-macos-arm64"
|
||||
macos.touch()
|
||||
Path(f"{macos}-rg").touch()
|
||||
monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
|
||||
|
||||
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "macos-arm64")
|
||||
@@ -59,3 +62,16 @@ def test_runtime_requires_spawn_helper_only_on_macos(
|
||||
runtime.bundled_runtime_path()
|
||||
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
|
||||
assert runtime.bundled_runtime_path() == linux
|
||||
|
||||
|
||||
def test_runtime_requires_ripgrep_sidecar(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
runtime_dir = tmp_path / "runtime"
|
||||
runtime_dir.mkdir()
|
||||
(runtime_dir / "dsh-jsonrpc-agent-pkg-linux-x64").touch()
|
||||
monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
|
||||
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
|
||||
|
||||
with pytest.raises(FileNotFoundError, match="ripgrep sidecar"):
|
||||
runtime.bundled_runtime_path()
|
||||
@@ -30,3 +30,44 @@ def test_child_prompt_precedes_runtime_context(prompt_name: str, expected: str)
|
||||
for chunk in chunks
|
||||
for choice in chunk.get("choices", [])
|
||||
)
|
||||
|
||||
|
||||
def test_mcp_smoke_requests_the_discovered_tool() -> None:
|
||||
chunks = SMOKE["completion_chunks"]({
|
||||
"messages": [{"role": "user", "content": SMOKE["MCP_PROMPT"]}],
|
||||
"tools": [{"type": "function", "function": {"name": "mcp__fixture__add"}}],
|
||||
})
|
||||
|
||||
calls = [
|
||||
call
|
||||
for chunk in chunks
|
||||
for choice in chunk.get("choices", [])
|
||||
for call in choice.get("delta", {}).get("tool_calls", [])
|
||||
]
|
||||
assert calls[0]["function"] == {
|
||||
"name": "mcp__fixture__add",
|
||||
"arguments": '{"a": 19, "b": 23}',
|
||||
}
|
||||
|
||||
|
||||
def test_mcp_smoke_accepts_the_external_server_result() -> None:
|
||||
chunks = SMOKE["completion_chunks"]({
|
||||
"messages": [
|
||||
{"role": "user", "content": SMOKE["MCP_PROMPT"]},
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [{
|
||||
"id": "mcp-add",
|
||||
"type": "function",
|
||||
"function": {"name": "mcp__fixture__add", "arguments": '{}'},
|
||||
}],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "mcp-add", "content": "42"},
|
||||
],
|
||||
})
|
||||
|
||||
assert any(
|
||||
choice.get("delta", {}).get("content") == SMOKE["MCP_TEXT"]
|
||||
for chunk in chunks
|
||||
for choice in chunk.get("choices", [])
|
||||
)
|
||||
@@ -378,7 +378,7 @@ class SingleExeBuild {
|
||||
/**
|
||||
* Package one target; SEA mode accepts one target per invocation.
|
||||
* @param target - the pkg target triple to build.
|
||||
* @returns the executable path and, on macOS, its helper path.
|
||||
* @returns the executable and ripgrep sidecar paths, plus the macOS spawn helper path when required.
|
||||
*/
|
||||
async pack(target: Target): Promise<string[]> {
|
||||
const product = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
|
||||
@@ -397,7 +397,8 @@ class SingleExeBuild {
|
||||
if (!this.cli.dryRun && !existsSync(product)) {
|
||||
throw new Error(`build-exe-for-python-sdk: product ${product} is missing after the pkg run; inspect ${this.outDir}.`)
|
||||
}
|
||||
if (target.platform !== 'macos') return [product]
|
||||
const ripgrep = await this.copyRipgrepSidecar(target, product)
|
||||
if (target.platform !== 'macos') return [product, ripgrep]
|
||||
const spawnHelper = `${product}-spawn-helper`
|
||||
const source = join(this.staging, 'node_modules', 'node-pty', 'prebuilds', `darwin-${target.arch}`, 'spawn-helper')
|
||||
if (this.cli.dryRun) {
|
||||
@@ -406,7 +407,31 @@ class SingleExeBuild {
|
||||
await copyFile(source, spawnHelper)
|
||||
await chmod(spawnHelper, 0o755)
|
||||
}
|
||||
return [product, spawnHelper]
|
||||
return [product, ripgrep, spawnHelper]
|
||||
}
|
||||
|
||||
/** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
|
||||
private async copyRipgrepSidecar(target: Target, product: string): Promise<string> {
|
||||
const platform = target.platform === 'macos' ? 'darwin' : target.platform
|
||||
const source = join(
|
||||
this.staging,
|
||||
'node_modules',
|
||||
'@vscode',
|
||||
`ripgrep-${platform}-${target.arch}`,
|
||||
'bin',
|
||||
'rg',
|
||||
)
|
||||
const destination = `${product}-rg`
|
||||
if (this.cli.dryRun) {
|
||||
console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
|
||||
return destination
|
||||
}
|
||||
if (!existsSync(source)) {
|
||||
throw new Error(`build-exe-for-python-sdk: target ripgrep binary is missing at ${source}.`)
|
||||
}
|
||||
await copyFile(source, destination)
|
||||
await chmod(destination, 0o755)
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,8 @@ PLATFORMS = load_platforms()
|
||||
|
||||
|
||||
def runtime_suffixes(executable_name: str) -> tuple[str, ...]:
|
||||
return ("", "-spawn-helper") if "-macos-" in executable_name else ("",)
|
||||
suffixes = ("", "-rg")
|
||||
return (*suffixes, "-spawn-helper") if "-macos-" in executable_name else suffixes
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -268,7 +268,10 @@ describe('Python release workflows', () => {
|
||||
},
|
||||
})
|
||||
expect(pythonCompat.strategy).toMatchObject({ matrix: { python: ['3.10', '3.14'] } })
|
||||
expect(JSON.stringify(pythonCompat.steps)).toContain('deepseek-harness-sdk==${{ steps.compatibility-version.outputs.version }}')
|
||||
const pythonCompatSteps = JSON.stringify(pythonCompat.steps)
|
||||
expect(pythonCompatSteps).toContain('dist/deepseek_harness_sdk-$VERSION-py3-none-any.whl')
|
||||
expect(pythonCompatSteps).toContain('dist/deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl')
|
||||
expect(pythonCompatSteps).not.toContain('--find-links')
|
||||
const validateSteps = JSON.stringify(validate.steps)
|
||||
const authorize = validate.steps.filter(isRecord).find(step => step.name === 'Authorize publication request')
|
||||
if (!isRecord(authorize) || typeof authorize.run !== 'string') {
|
||||
@@ -341,7 +344,14 @@ describe('Python release workflows', () => {
|
||||
expect(plan.if).toContain('inputs.ci')
|
||||
expect(plan.if).toContain('inputs.release')
|
||||
expect(JSON.stringify(plan.steps)).toContain('pep440_version')
|
||||
expect(JSON.stringify(workflow)).toContain('macosx_14_0_arm64')
|
||||
const workflowJson = JSON.stringify(workflow)
|
||||
expect(workflowJson).toContain('macosx_14_0_arm64')
|
||||
expect(workflowJson).toContain('dist-python/$SDK_WHEEL')
|
||||
expect(workflowJson).toContain('dist-python/$RUNTIME_WHEEL')
|
||||
expect(workflowJson).toContain('/work/dist-python/$SDK_WHEEL')
|
||||
expect(workflowJson).toContain('/work/dist-python/$RUNTIME_WHEEL')
|
||||
expect(workflowJson).not.toContain('--find-links dist-python')
|
||||
expect(workflowJson).not.toContain('--find-links /work/dist-python')
|
||||
expect(manylinuxAddon).toMatchObject({ if: "runner.os == 'Linux'" })
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('manylinux_2_28_x86_64')
|
||||
expect(JSON.stringify(manylinuxAddon)).toContain('manylinux_2_28_aarch64')
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Cordis YAML parsing and Loader-entry classification shared by repository checks.
|
||||
* @module scripts/cordis-yaml
|
||||
*/
|
||||
|
||||
import * as yaml from 'js-yaml'
|
||||
|
||||
/** A Loader `!!js` expression preserved as data instead of executed. */
|
||||
export interface JsExpr {
|
||||
__jsExpr: string
|
||||
}
|
||||
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
resolve: data => typeof data === 'string',
|
||||
construct: (data: unknown): JsExpr => {
|
||||
if (typeof data !== 'string') throw new TypeError('!!js requires a scalar string')
|
||||
return { __jsExpr: data }
|
||||
},
|
||||
})
|
||||
const schema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
|
||||
/**
|
||||
* Parse a Cordis config while preserving Loader `!!js` expressions as data.
|
||||
* @param source - Cordis YAML source text.
|
||||
* @returns the parsed YAML value.
|
||||
*/
|
||||
export function loadCordisYaml(source: string): unknown {
|
||||
return yaml.load(source, { schema })
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a value is a preserved Loader `!!js` expression.
|
||||
* @param value - parsed YAML value.
|
||||
* @returns whether the value contains one preserved expression.
|
||||
*/
|
||||
export function isJsExpr(value: unknown): value is JsExpr {
|
||||
return typeof value === 'object'
|
||||
&& value !== null
|
||||
&& typeof (value as Record<string, unknown>).__jsExpr === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a Loader entry owns nested entries in its `config` array.
|
||||
* @param value - parsed Loader entry.
|
||||
* @returns whether the entry is an explicit or package-named Cordis group.
|
||||
*/
|
||||
export function isCordisGroupEntry(value: unknown): value is Record<string, unknown> & { config: unknown[] } {
|
||||
return typeof value === 'object'
|
||||
&& value !== null
|
||||
&& Array.isArray((value as Record<string, unknown>).config)
|
||||
&& ((value as Record<string, unknown>).group === true
|
||||
|| (value as Record<string, unknown>).name === '@deepseek-ai/cordis-plugin-group')
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
@@ -28,6 +29,11 @@ WORKFLOW_WORKER_TEXT = "workflow worker smoke ok"
|
||||
MINIMAL_PROMPT = "Exercise the packaged minimal agent's persistent Bash and string-replacement editor."
|
||||
MINIMAL_TEXT = "minimal agent smoke ok"
|
||||
MINIMAL_EDITOR_PATH_PREFIX = "Editor path: "
|
||||
FS_SEARCH_PROMPT = "Exercise the packaged filesystem search tools."
|
||||
FS_SEARCH_TEXT = "filesystem search smoke ok"
|
||||
FS_SEARCH_MARKER = "PACKAGED_FS_SEARCH_OK"
|
||||
MCP_PROMPT = "Exercise the packaged MCP client with one external stdio server."
|
||||
MCP_TEXT = "MCP client smoke ok"
|
||||
MINIMAL_CORDIS = (
|
||||
Path(__file__).resolve().parent.parent / "examples" / "jsonrpc-agent" / "minimal.cordis.yml"
|
||||
)
|
||||
@@ -116,6 +122,144 @@ CUSTOM_CORDIS = """\
|
||||
- id: cordis-tool
|
||||
name: '@deepseek-ai/dsh-tool-cordis'
|
||||
"""
|
||||
FS_SEARCH_CORDIS = """\
|
||||
- id: sdk-jsonrpc-server
|
||||
name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
|
||||
- id: agent-core
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
workspaceContext: false
|
||||
skills:
|
||||
enabled: false
|
||||
toolBash: false
|
||||
toolJobs: false
|
||||
- id: sessions
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: !!js process.env.DSH_SESSION_ROOT
|
||||
compression: 'none'
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
- id: fs-search
|
||||
name: '@deepseek-ai/dsh-tool-fs-search'
|
||||
config:
|
||||
sampleOverCapGlobResults: false
|
||||
"""
|
||||
MCP_SERVER_SCRIPT = """\
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
log_path = os.environ.get("MCP_SMOKE_LOG")
|
||||
|
||||
|
||||
def send(message):
|
||||
sys.stdout.write(json.dumps(message, separators=(",", ":")) + "\\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
for line in sys.stdin:
|
||||
request = json.loads(line)
|
||||
if log_path is not None:
|
||||
with open(log_path, "a", encoding="utf-8") as log:
|
||||
log.write(str(request.get("method")) + "\\n")
|
||||
request_id = request.get("id")
|
||||
if request_id is None:
|
||||
continue
|
||||
method = request.get("method")
|
||||
if method == "initialize":
|
||||
send({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"protocolVersion": request["params"]["protocolVersion"],
|
||||
"capabilities": {"tools": {"listChanged": False}},
|
||||
"serverInfo": {"name": "python-wheel-fixture", "version": "1.0.0"},
|
||||
},
|
||||
})
|
||||
elif method == "tools/list":
|
||||
# Keep discovery pending longer than the old smoke's 100 ms grace
|
||||
# period. An SDK runtime that answers initialize too early will make
|
||||
# its first model request without this tool and fail deterministically.
|
||||
time.sleep(0.25)
|
||||
send({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"tools": [{
|
||||
"name": "add",
|
||||
"description": "Add two numbers.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"a": {"type": "number"}, "b": {"type": "number"}},
|
||||
"required": ["a", "b"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
}],
|
||||
},
|
||||
})
|
||||
elif method == "tools/call":
|
||||
params = request["params"]
|
||||
if params.get("name") != "add" or params.get("arguments") != {"a": 19, "b": 23}:
|
||||
send({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {"code": -32602, "message": "unexpected tool call"},
|
||||
})
|
||||
continue
|
||||
send({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {"content": [{"type": "text", "text": "42"}]},
|
||||
})
|
||||
else:
|
||||
send({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {"code": -32601, "message": f"unsupported method: {method}"},
|
||||
})
|
||||
"""
|
||||
|
||||
|
||||
def mcp_cordis(server_script: Path) -> str:
|
||||
"""Build an external config that mounts the packaged MCP client."""
|
||||
return json.dumps([
|
||||
{
|
||||
"id": "sdk-jsonrpc-server",
|
||||
"name": "@deepseek-ai/dsh-sdk-jsonrpc-server",
|
||||
},
|
||||
{
|
||||
"id": "agent-core",
|
||||
"name": "@deepseek-ai/dsh-agent-spine-demo",
|
||||
"config": {
|
||||
"workspaceContext": False,
|
||||
"skills": {"enabled": False},
|
||||
"toolBash": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "sessions",
|
||||
"name": "@deepseek-ai/dsh-session-persistence-jsonl",
|
||||
"config": {"root": "./sessions", "compression": "none"},
|
||||
},
|
||||
{
|
||||
"id": "mcp-fixture",
|
||||
"name": "@deepseek-ai/dsh-mcp-client",
|
||||
"config": {
|
||||
"serverName": "fixture",
|
||||
"transport": "stdio",
|
||||
"command": sys.executable,
|
||||
"args": [str(server_script)],
|
||||
"env": {"MCP_SMOKE_LOG": str(server_script.with_suffix(".log"))},
|
||||
"failOnStartupError": True,
|
||||
"reconnect": {"enabled": False},
|
||||
},
|
||||
},
|
||||
], indent=2)
|
||||
|
||||
|
||||
class MockModelHandler(BaseHTTPRequestHandler):
|
||||
"""Return deterministic text, worker, and orchestration completions."""
|
||||
|
||||
@@ -150,6 +294,12 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
if latest.get("role") == "tool":
|
||||
call_id, tool_name = latest_tool_call(messages)
|
||||
tool_text = message_text(latest.get("content"))
|
||||
mcp = mcp_tool_followup(call_id, tool_name, tool_text)
|
||||
if mcp is not None:
|
||||
return mcp
|
||||
fs_search = fs_search_tool_followup(call_id, tool_name, tool_text)
|
||||
if fs_search is not None:
|
||||
return fs_search
|
||||
minimal = minimal_tool_followup(body, call_id, tool_name, tool_text)
|
||||
if minimal is not None:
|
||||
return minimal
|
||||
@@ -191,6 +341,8 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
SNAPSHOT_PROMPT,
|
||||
CODE_PROMPT,
|
||||
WORKFLOW_PROMPT,
|
||||
FS_SEARCH_PROMPT,
|
||||
MCP_PROMPT,
|
||||
}
|
||||
prompt = next(
|
||||
(candidate for candidate in user_prompts if candidate in scenario_prompts),
|
||||
@@ -232,9 +384,60 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
},
|
||||
},
|
||||
)
|
||||
if prompt == FS_SEARCH_PROMPT:
|
||||
assert_advertised_tool(body, "grep")
|
||||
assert_advertised_tool(body, "glob")
|
||||
return tool_call_chunks(
|
||||
"fs-search-grep",
|
||||
"grep",
|
||||
{"pattern": FS_SEARCH_MARKER, "path": "."},
|
||||
)
|
||||
if prompt == MCP_PROMPT:
|
||||
assert_advertised_tool(body, "mcp__fixture__add")
|
||||
return tool_call_chunks(
|
||||
"mcp-add",
|
||||
"mcp__fixture__add",
|
||||
{"a": 19, "b": 23},
|
||||
)
|
||||
return text_chunks(EXPECTED_TEXT)
|
||||
|
||||
|
||||
def mcp_tool_followup(
|
||||
call_id: str,
|
||||
tool_name: str,
|
||||
tool_text: str,
|
||||
) -> list[dict[str, object]] | None:
|
||||
"""Verify one tool call through the packaged MCP client."""
|
||||
if call_id != "mcp-add":
|
||||
return None
|
||||
if tool_name != "mcp__fixture__add" or "42" not in tool_text:
|
||||
raise AssertionError(f"packaged MCP call returned an unexpected result: {tool_name}: {tool_text}")
|
||||
return text_chunks(MCP_TEXT)
|
||||
|
||||
|
||||
def fs_search_tool_followup(
|
||||
call_id: str,
|
||||
tool_name: str,
|
||||
tool_text: str,
|
||||
) -> list[dict[str, object]] | None:
|
||||
"""Exercise both ripgrep-backed tools through the packaged executable."""
|
||||
if not call_id.startswith("fs-search-"):
|
||||
return None
|
||||
if call_id == "fs-search-grep" and tool_name == "grep":
|
||||
if "needle.txt" not in tool_text or FS_SEARCH_MARKER not in tool_text:
|
||||
raise AssertionError(f"packaged grep returned no marker: {tool_text}")
|
||||
return tool_call_chunks(
|
||||
"fs-search-glob",
|
||||
"glob",
|
||||
{"pattern": "**/*.txt"},
|
||||
)
|
||||
if call_id == "fs-search-glob" and tool_name == "glob":
|
||||
if "needle.txt" not in tool_text:
|
||||
raise AssertionError(f"packaged glob returned no fixture path: {tool_text}")
|
||||
return text_chunks(FS_SEARCH_TEXT)
|
||||
raise AssertionError(f"unexpected filesystem-search follow-up: {call_id} {tool_name}: {tool_text}")
|
||||
|
||||
|
||||
def minimal_tool_followup(
|
||||
body: dict[str, object],
|
||||
call_id: str,
|
||||
@@ -476,13 +679,13 @@ def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--scenario",
|
||||
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-snapshot", "direct"),
|
||||
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-mcp", "sdk-snapshot", "direct"),
|
||||
default="all",
|
||||
)
|
||||
parser.add_argument("--exe", type=Path)
|
||||
parser.add_argument("--update-snapshots", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-snapshot", "direct"} and args.exe is None:
|
||||
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-snapshot", "direct"} and args.exe is None:
|
||||
parser.error("--exe is required for custom, minimal, snapshot, and direct scenarios")
|
||||
if args.update_snapshots and args.scenario not in {"all", "sdk-minimal", "sdk-snapshot"}:
|
||||
parser.error("--update-snapshots requires --scenario sdk-minimal, sdk-snapshot, or all")
|
||||
@@ -498,6 +701,11 @@ def main() -> None:
|
||||
if args.scenario in {"all", "sdk-minimal"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_minimal(model.url, args.exe.resolve(), args.update_snapshots)
|
||||
if args.scenario in {"all", "sdk-fs-search"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_fs_search(model.url, args.exe.resolve())
|
||||
if args.scenario in {"all", "sdk-mcp"}:
|
||||
smoke_sdk_mcp(model.url, None if args.exe is None else args.exe.resolve())
|
||||
if args.scenario in {"all", "sdk-snapshot"}:
|
||||
assert args.exe is not None
|
||||
smoke_sdk_snapshot(model.url, args.exe.resolve(), args.update_snapshots)
|
||||
@@ -594,6 +802,68 @@ def smoke_sdk_minimal(base_url: str, executable: Path, update_snapshots: bool) -
|
||||
)
|
||||
|
||||
|
||||
def smoke_sdk_fs_search(base_url: str, executable: Path) -> None:
|
||||
"""Exercise real grep and glob spawns through the packaged executable."""
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="dsh-sdk-fs-search-") as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
(root / "needle.txt").write_text(f"{FS_SEARCH_MARKER}\n")
|
||||
sessions = root / "sessions"
|
||||
cordis = root / "cordis.yml"
|
||||
cordis.write_text(FS_SEARCH_CORDIS)
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
cwd=str(root),
|
||||
session_root=str(sessions),
|
||||
cordis=str(cordis),
|
||||
runtime_bin=str(executable),
|
||||
api_key="sk-keyless-smoke",
|
||||
base_url=base_url,
|
||||
request_timeout_seconds=60,
|
||||
) as harness:
|
||||
result = harness.run(FS_SEARCH_PROMPT, session_id="fs-search-smoke")
|
||||
|
||||
assert result.final_response == FS_SEARCH_TEXT, result.final_response
|
||||
assert_session_log(sessions, root, FS_SEARCH_TEXT, FS_SEARCH_MARKER, "needle.txt")
|
||||
|
||||
|
||||
def smoke_sdk_mcp(base_url: str, executable: Path | None) -> None:
|
||||
"""Discover and call an external stdio MCP tool through the packaged client."""
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="dsh-sdk-mcp-") as temporary:
|
||||
root = Path(temporary).resolve()
|
||||
sessions = root / "sessions"
|
||||
server_script = root / "mcp_server.py"
|
||||
server_script.write_text(MCP_SERVER_SCRIPT)
|
||||
cordis = root / "cordis.yml"
|
||||
cordis.write_text(mcp_cordis(server_script))
|
||||
discovery_log = server_script.with_suffix(".log")
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
model="smoke-model",
|
||||
cwd=str(root),
|
||||
session_root=str(sessions),
|
||||
cordis=str(cordis),
|
||||
runtime_bin=None if executable is None else str(executable),
|
||||
api_key="sk-keyless-smoke",
|
||||
base_url=base_url,
|
||||
request_timeout_seconds=60,
|
||||
) as harness:
|
||||
result = harness.run(MCP_PROMPT, session_id="mcp-smoke")
|
||||
|
||||
assert result.final_response == MCP_TEXT, result.final_response
|
||||
assert discovery_log.read_text().splitlines() == [
|
||||
"initialize",
|
||||
"notifications/initialized",
|
||||
"tools/list",
|
||||
"tools/call",
|
||||
]
|
||||
assert_session_log(sessions, root, MCP_TEXT, "mcp__fixture__add", "42")
|
||||
|
||||
|
||||
def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) -> None:
|
||||
"""Drive and compare the advanced SDK/executable behavioral snapshot."""
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
@@ -12,13 +12,10 @@
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { dirname, relative, resolve } from 'node:path'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { Script } from 'node:vm'
|
||||
import ts from 'typescript'
|
||||
import { cordisConfigFiles } from './cordis-config-files.ts'
|
||||
|
||||
interface JsExpr {
|
||||
__jsExpr: string
|
||||
}
|
||||
import { isCordisGroupEntry, isJsExpr, loadCordisYaml } from './cordis-yaml.ts'
|
||||
|
||||
export interface PackageManifest {
|
||||
name?: string
|
||||
@@ -58,16 +55,6 @@ const CHOOSER_BACKEND_PACKAGES = [
|
||||
'@deepseek-ai/dsh-client-ui-directory-picker-browse',
|
||||
'@deepseek-ai/dsh-client-ui-directory-picker-native',
|
||||
]
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
resolve: data => typeof data === 'string',
|
||||
construct: (data: unknown): JsExpr => {
|
||||
if (typeof data !== 'string') throw new TypeError('!!js requires a scalar string')
|
||||
return { __jsExpr: data }
|
||||
},
|
||||
})
|
||||
const schema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
|
||||
const errors: string[] = []
|
||||
const pluginReferences: PluginReference[] = []
|
||||
|
||||
@@ -75,7 +62,7 @@ if (import.meta.main) {
|
||||
const files = cordisConfigFiles(root)
|
||||
|
||||
for (const file of files) {
|
||||
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
|
||||
const document = loadCordisYaml(readFileSync(resolve(root, file), 'utf8'))
|
||||
if (!isUnknownArray(document)) {
|
||||
errors.push(`${file}: root must be a Loader entry array`)
|
||||
continue
|
||||
@@ -174,7 +161,7 @@ function validatePresetPlaneSeparation(): string[] {
|
||||
|
||||
/** Every entry of one config file, or an empty list when it is not an entry array. */
|
||||
function loadEntries(file: string): unknown[] {
|
||||
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
|
||||
const document = loadCordisYaml(readFileSync(resolve(root, file), 'utf8'))
|
||||
return isUnknownArray(document) ? document : []
|
||||
}
|
||||
|
||||
@@ -206,7 +193,7 @@ function validateEntry(value: unknown, file: string, path: string): void {
|
||||
}
|
||||
recordPlugin(value, file)
|
||||
validateMetadata(value, file, path)
|
||||
if ((value.group === true || value.name === '@deepseek-ai/cordis-plugin-group') && isUnknownArray(value.config)) {
|
||||
if (isCordisGroupEntry(value)) {
|
||||
for (let index = 0; index < value.config.length; index++) {
|
||||
validateEntry(value.config[index], file, `${path}.config[${index}]`)
|
||||
}
|
||||
@@ -496,9 +483,8 @@ export function metadataExpressionErrors(entry: Record<string, unknown>, path: s
|
||||
*/
|
||||
function disabledExpressionProblem(expression: string): string | undefined {
|
||||
try {
|
||||
// Compilation only — the constructor never executes the body.
|
||||
// oxlint-disable-next-line typescript/no-implied-eval
|
||||
new Function(`return (${expression})`)
|
||||
// Compilation only — constructing a Script does not execute its source.
|
||||
new Script(`(${expression})`)
|
||||
return undefined
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
@@ -519,10 +505,6 @@ function collectExpressionPaths(value: unknown, path: string, output: string[]):
|
||||
for (const [key, child] of Object.entries(value)) collectExpressionPaths(child, `${path}.${key}`, output)
|
||||
}
|
||||
|
||||
function isJsExpr(value: unknown): value is JsExpr {
|
||||
return isRecord(value) && typeof value.__jsExpr === 'string'
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { verifyRuntimeClosure } from './verify-runtime-closure.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
function fixture(files: Record<string, string | Record<string, unknown>>): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-runtime-closure-'))
|
||||
roots.push(root)
|
||||
for (const [relative, value] of Object.entries(files)) {
|
||||
const path = join(root, relative)
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
const platforms = {
|
||||
'linux-x64': { tag: 'manylinux_2_28_x86_64', executable: 'runtime-linux-x64' },
|
||||
'linux-arm64': { tag: 'manylinux_2_28_aarch64', executable: 'runtime-linux-arm64' },
|
||||
'macos-arm64': { tag: 'macosx_14_0_arm64', executable: 'runtime-macos-arm64' },
|
||||
}
|
||||
|
||||
function workspace(root: string, name: string, manifest: Record<string, unknown>): void {
|
||||
const packageName = name.replace('@scope/', '')
|
||||
const path = join(root, 'packages/core', packageName, 'package.json')
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, `${JSON.stringify({ name, ...manifest }, null, 2)}\n`)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('verifyRuntimeClosure', () => {
|
||||
it('requires only plugins active for a Linux or macOS target', async () => {
|
||||
const root = fixture({
|
||||
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/shared': 'workspace:^' } },
|
||||
'python/sdk-runtime/platforms.json': platforms,
|
||||
'apps/cli/config/agent-presets/standard/agent.cordis.yml': `
|
||||
- id: tools
|
||||
name: cordis:group
|
||||
group: true
|
||||
config:
|
||||
- id: shared
|
||||
name: '@scope/shared'
|
||||
- id: linux
|
||||
name: '@scope/linux'
|
||||
disabled: !!js process.platform !== 'linux'
|
||||
- id: macos
|
||||
name: '@scope/macos'
|
||||
disabled: !!js process.platform !== 'darwin'
|
||||
`,
|
||||
})
|
||||
|
||||
const result = await verifyRuntimeClosure(root)
|
||||
|
||||
expect(result.presetCount).toBe(1)
|
||||
expect(result.failures).toEqual([
|
||||
'standard preset -> @scope/linux (linux-arm64, linux-x64)',
|
||||
'standard preset -> @scope/macos (macos-arm64)',
|
||||
])
|
||||
})
|
||||
|
||||
it('treats an unsupported disabled expression as active on every target', async () => {
|
||||
const root = fixture({
|
||||
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: {} },
|
||||
'python/sdk-runtime/platforms.json': platforms,
|
||||
'apps/cli/config/agent-presets/standard/agent.cordis.yml': `
|
||||
- id: conditional
|
||||
name: '@scope/conditional'
|
||||
disabled: !!js process.env.DSH_DISABLE_CONDITIONAL === '1'
|
||||
`,
|
||||
})
|
||||
|
||||
const result = await verifyRuntimeClosure(root)
|
||||
|
||||
expect(result.failures).toEqual([
|
||||
'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64)',
|
||||
])
|
||||
})
|
||||
|
||||
it('does not interpret an ordinary plugin array config as nested Loader entries', async () => {
|
||||
const root = fixture({
|
||||
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/plugin': 'workspace:^' } },
|
||||
'python/sdk-runtime/platforms.json': platforms,
|
||||
'apps/cli/config/agent-presets/standard/agent.cordis.yml': `
|
||||
- id: plugin
|
||||
name: '@scope/plugin'
|
||||
config:
|
||||
- name: '@scope/config-value'
|
||||
`,
|
||||
})
|
||||
|
||||
const result = await verifyRuntimeClosure(root)
|
||||
|
||||
expect(result.failures).toEqual([])
|
||||
})
|
||||
|
||||
it('requires preset plugins to be linked from the workspace', async () => {
|
||||
const root = fixture({
|
||||
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/plugin': '1.2.3' } },
|
||||
'python/sdk-runtime/platforms.json': platforms,
|
||||
'apps/cli/config/agent-presets/standard/agent.cordis.yml': `
|
||||
- id: plugin
|
||||
name: '@scope/plugin'
|
||||
`,
|
||||
})
|
||||
|
||||
const result = await verifyRuntimeClosure(root)
|
||||
|
||||
expect(result.failures).toEqual([
|
||||
'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64)',
|
||||
])
|
||||
})
|
||||
|
||||
it('fails when no shipped preset is discovered', async () => {
|
||||
const root = fixture({
|
||||
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: {} },
|
||||
'python/sdk-runtime/platforms.json': platforms,
|
||||
})
|
||||
|
||||
const result = await verifyRuntimeClosure(root)
|
||||
|
||||
expect(result.presetCount).toBe(0)
|
||||
expect(result.failures).toEqual([
|
||||
'no agent presets matched apps/cli/config/agent-presets/*/agent.cordis.yml',
|
||||
])
|
||||
})
|
||||
|
||||
it('fails when the runtime platform manifest has no targets', async () => {
|
||||
const root = fixture({
|
||||
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: {} },
|
||||
'python/sdk-runtime/platforms.json': {},
|
||||
'apps/cli/config/agent-presets/standard/agent.cordis.yml': '[]\n',
|
||||
})
|
||||
|
||||
const result = await verifyRuntimeClosure(root)
|
||||
|
||||
expect(result.failures).toEqual([
|
||||
'python/sdk-runtime/platforms.json defines no runtime targets',
|
||||
])
|
||||
})
|
||||
|
||||
it('retains the required workspace-peer closure check', async () => {
|
||||
const root = fixture({
|
||||
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/root': 'workspace:^' } },
|
||||
'python/sdk-runtime/platforms.json': platforms,
|
||||
'apps/cli/config/agent-presets/minimal/agent.cordis.yml': '[]\n',
|
||||
})
|
||||
workspace(root, '@scope/root', {
|
||||
peerDependencies: { '@scope/required': 'workspace:^', '@scope/optional': 'workspace:^' },
|
||||
peerDependenciesMeta: { '@scope/optional': { optional: true } },
|
||||
})
|
||||
workspace(root, '@scope/required', {})
|
||||
workspace(root, '@scope/optional', {})
|
||||
|
||||
const result = await verifyRuntimeClosure(root)
|
||||
|
||||
expect(result.workspacePackageCount).toBe(1)
|
||||
expect(result.failures).toEqual(['runtime -> @scope/root -> @scope/required'])
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,14 @@
|
||||
/**
|
||||
* Verify that the executable deploy manifest supplies every required workspace
|
||||
* peer in its dependency graph. With auto peer installation disabled, a missing
|
||||
* root peer can otherwise fail only when Cordis loads the packaged plugin.
|
||||
* Verify that the executable deploy manifest supplies every plugin referenced
|
||||
* by a shipped agent preset and every required workspace peer in its dependency
|
||||
* graph. With auto peer installation disabled, either omission can otherwise
|
||||
* fail only when Cordis loads the packaged plugin.
|
||||
*/
|
||||
import { globSync } from 'node:fs'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { basename, dirname, resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { isCordisGroupEntry, loadCordisYaml } from './cordis-yaml.ts'
|
||||
|
||||
interface PackageManifest {
|
||||
name?: string
|
||||
@@ -21,58 +23,181 @@ interface WorkspacePackage {
|
||||
manifest: PackageManifest
|
||||
}
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const { values } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: { manifest: { type: 'string' } },
|
||||
})
|
||||
const runtimeManifestPath = resolve(root, values.manifest ?? 'python/sdk-runtime/package.json')
|
||||
const runtimeManifest = await loadManifest(runtimeManifestPath)
|
||||
const runtimeName = runtimeManifest.name ?? 'python/sdk-runtime'
|
||||
const workspace = await loadWorkspacePackages()
|
||||
const runtimeDependencies = runtimeManifest.dependencies ?? {}
|
||||
const parents = new Map<string, string | undefined>()
|
||||
const queue: string[] = []
|
||||
|
||||
for (const dependency of Object.keys(runtimeDependencies).sort()) {
|
||||
if (!workspace.has(dependency)) continue
|
||||
parents.set(dependency, undefined)
|
||||
queue.push(dependency)
|
||||
interface RuntimePlatform {
|
||||
tag: string
|
||||
executable: string
|
||||
}
|
||||
|
||||
const failures: string[] = []
|
||||
for (let index = 0; index < queue.length; index += 1) {
|
||||
const packageName = queue[index]
|
||||
if (packageName === undefined) continue
|
||||
const current = workspace.get(packageName)
|
||||
if (current === undefined) continue
|
||||
const peers = current.manifest.peerDependencies ?? {}
|
||||
const peerMeta = current.manifest.peerDependenciesMeta ?? {}
|
||||
for (const peer of Object.keys(peers).sort()) {
|
||||
if (!workspace.has(peer) || peerMeta[peer]?.optional === true) continue
|
||||
if (runtimeDependencies[peer]?.startsWith('workspace:') === true) continue
|
||||
failures.push(`${formatChain(runtimeName, packageName, parents)} -> ${peer}`)
|
||||
}
|
||||
const dependencies = {
|
||||
...current.manifest.dependencies,
|
||||
...current.manifest.optionalDependencies,
|
||||
}
|
||||
for (const dependency of Object.keys(dependencies).sort()) {
|
||||
if (!workspace.has(dependency) || parents.has(dependency)) continue
|
||||
parents.set(dependency, packageName)
|
||||
type RuntimePlatformManifest = Record<string, RuntimePlatform>
|
||||
|
||||
const AGENT_PRESET_GLOB = 'apps/cli/config/agent-presets/*/agent.cordis.yml'
|
||||
|
||||
export interface RuntimeClosureResult {
|
||||
failures: string[]
|
||||
presetCount: number
|
||||
workspacePackageCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the runtime manifest contains every shipped-preset plugin and workspace peer.
|
||||
* @param root repository root containing the runtime manifest and shipped presets.
|
||||
* @param manifestPath runtime manifest path relative to {@link root}.
|
||||
* @returns the discovered preset count, reachable workspace package count, and violations.
|
||||
*/
|
||||
export async function verifyRuntimeClosure(
|
||||
root: string,
|
||||
manifestPath = 'python/sdk-runtime/package.json',
|
||||
): Promise<RuntimeClosureResult> {
|
||||
const runtimeManifest = await loadManifest(resolve(root, manifestPath))
|
||||
const runtimeName = runtimeManifest.name ?? manifestPath
|
||||
const workspace = await loadWorkspacePackages(root)
|
||||
const runtimeDependencies = runtimeManifest.dependencies ?? {}
|
||||
const platforms = await loadJson<RuntimePlatformManifest>(resolve(root, 'python/sdk-runtime/platforms.json'))
|
||||
const presetPaths = globSync(AGENT_PRESET_GLOB, { cwd: root }).sort()
|
||||
const targets = Object.keys(platforms).sort()
|
||||
const parents = new Map<string, string | undefined>()
|
||||
const queue: string[] = []
|
||||
|
||||
for (const dependency of Object.keys(runtimeDependencies).sort()) {
|
||||
if (!workspace.has(dependency)) continue
|
||||
parents.set(dependency, undefined)
|
||||
queue.push(dependency)
|
||||
}
|
||||
|
||||
const failures: string[] = []
|
||||
if (presetPaths.length === 0) failures.push(`no agent presets matched ${AGENT_PRESET_GLOB}`)
|
||||
if (targets.length === 0) failures.push('python/sdk-runtime/platforms.json defines no runtime targets')
|
||||
failures.push(...await missingPresetPlugins(root, runtimeDependencies, presetPaths, targets))
|
||||
for (let index = 0; index < queue.length; index += 1) {
|
||||
const packageName = queue[index]
|
||||
if (packageName === undefined) continue
|
||||
const current = workspace.get(packageName)
|
||||
if (current === undefined) continue
|
||||
const peers = current.manifest.peerDependencies ?? {}
|
||||
const peerMeta = current.manifest.peerDependenciesMeta ?? {}
|
||||
for (const peer of Object.keys(peers).sort()) {
|
||||
if (!workspace.has(peer) || peerMeta[peer]?.optional === true) continue
|
||||
if (runtimeDependencies[peer]?.startsWith('workspace:') === true) continue
|
||||
failures.push(`${formatChain(runtimeName, packageName, parents)} -> ${peer}`)
|
||||
}
|
||||
const dependencies = {
|
||||
...current.manifest.dependencies,
|
||||
...current.manifest.optionalDependencies,
|
||||
}
|
||||
for (const dependency of Object.keys(dependencies).sort()) {
|
||||
if (!workspace.has(dependency) || parents.has(dependency)) continue
|
||||
parents.set(dependency, packageName)
|
||||
queue.push(dependency)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
failures,
|
||||
presetCount: presetPaths.length,
|
||||
workspacePackageCount: queue.length,
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('verify-runtime-closure: required workspace peers are missing from python/sdk-runtime dependencies:')
|
||||
for (const failure of failures) console.error(` ${failure}`)
|
||||
process.exit(1)
|
||||
if (import.meta.main) {
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const { values } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: { manifest: { type: 'string' } },
|
||||
})
|
||||
const result = await verifyRuntimeClosure(root, values.manifest)
|
||||
if (result.failures.length > 0) {
|
||||
console.error('verify-runtime-closure: preset plugins or required workspace peers are missing from python/sdk-runtime dependencies:')
|
||||
for (const failure of result.failures) console.error(` ${failure}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(
|
||||
`verify-runtime-closure: ${result.presetCount} agent presets and ${result.workspacePackageCount} workspace packages form a closed runtime dependency graph.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`verify-runtime-closure: ${queue.length} workspace packages form a closed runtime dependency graph.`)
|
||||
async function missingPresetPlugins(
|
||||
root: string,
|
||||
runtimeDependencies: Readonly<Record<string, string>>,
|
||||
presetPaths: readonly string[],
|
||||
targets: readonly string[],
|
||||
): Promise<string[]> {
|
||||
const missing = new Map<string, Set<string>>()
|
||||
const failures: string[] = []
|
||||
for (const presetPath of presetPaths) {
|
||||
const document = loadCordisYaml(await readFile(resolve(root, presetPath), 'utf8'))
|
||||
if (!Array.isArray(document)) {
|
||||
failures.push(`${presetPath}: preset root must be a Loader entry array`)
|
||||
continue
|
||||
}
|
||||
for (const target of targets) {
|
||||
const processPlatform = processPlatformForTarget(target)
|
||||
for (const plugin of activeBarePluginPackages(document, processPlatform)) {
|
||||
const version = runtimeDependencies[plugin]
|
||||
if (version?.startsWith('workspace:') === true) continue
|
||||
const preset = basename(dirname(presetPath))
|
||||
const declaration = version === undefined
|
||||
? ''
|
||||
: ` [runtime dependency is ${JSON.stringify(version)}; expected workspace:]`
|
||||
const key = `${preset} preset -> ${plugin}${declaration}`
|
||||
const targets = missing.get(key) ?? new Set<string>()
|
||||
targets.add(target)
|
||||
missing.set(key, targets)
|
||||
}
|
||||
}
|
||||
}
|
||||
failures.push(...[...missing.entries()].map(([chain, targets]) =>
|
||||
`${chain} (${[...targets].sort().join(', ')})`))
|
||||
return failures
|
||||
}
|
||||
|
||||
async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
|
||||
function activeBarePluginPackages(entries: unknown[], processPlatform: string): Set<string> {
|
||||
const packages = new Set<string>()
|
||||
const visit = (value: unknown, parentDisabled: boolean): void => {
|
||||
if (!isRecord(value)) return
|
||||
const disabled = parentDisabled || disabledOnPlatform(value.disabled, processPlatform)
|
||||
if (disabled) return
|
||||
if (typeof value.name === 'string') {
|
||||
const packageName = barePackageName(value.name)
|
||||
if (packageName !== undefined) packages.add(packageName)
|
||||
}
|
||||
if (isCordisGroupEntry(value)) {
|
||||
for (const child of value.config) visit(child, disabled)
|
||||
}
|
||||
}
|
||||
for (const entry of entries) visit(entry, false)
|
||||
return packages
|
||||
}
|
||||
|
||||
function disabledOnPlatform(value: unknown, processPlatform: string): boolean {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (!isRecord(value) || typeof value.__jsExpr !== 'string') return false
|
||||
const match = /^process\.platform\s*(===|!==)\s*(['"])(win32|linux|darwin)\2$/.exec(value.__jsExpr.trim())
|
||||
if (match === null) return false
|
||||
const [, operator, , expected] = match
|
||||
return operator === '===' ? processPlatform === expected : processPlatform !== expected
|
||||
}
|
||||
|
||||
function processPlatformForTarget(target: string): string {
|
||||
if (target.startsWith('linux-')) return 'linux'
|
||||
if (target.startsWith('macos-')) return 'darwin'
|
||||
throw new Error(`verify-runtime-closure: unsupported runtime target ${JSON.stringify(target)}`)
|
||||
}
|
||||
|
||||
function barePackageName(specifier: string): string | undefined {
|
||||
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.includes(':')) return undefined
|
||||
const parts = specifier.split('/')
|
||||
if (specifier.startsWith('@')) {
|
||||
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined
|
||||
}
|
||||
return parts[0] || undefined
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
async function loadWorkspacePackages(root: string): Promise<Map<string, WorkspacePackage>> {
|
||||
const paths = globSync(['packages/*/*/package.json', 'vendor/*/package.json'], { cwd: root })
|
||||
.sort()
|
||||
.map(relative => resolve(root, relative))
|
||||
@@ -85,7 +210,11 @@ async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
|
||||
}
|
||||
|
||||
async function loadManifest(path: string): Promise<PackageManifest> {
|
||||
return JSON.parse(await readFile(path, 'utf8')) as PackageManifest
|
||||
return loadJson<PackageManifest>(path)
|
||||
}
|
||||
|
||||
async function loadJson<T>(path: string): Promise<T> {
|
||||
return JSON.parse(await readFile(path, 'utf8')) as T
|
||||
}
|
||||
|
||||
function formatChain(
|
||||
|
||||
Reference in new issue
Block a user