Kaynağa Gözat

fix(python-sdk): harden packaged runtime behavior

fz 3 hafta önce
ebeveyn
işleme
ee5280bd6e

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

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

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md


+ 2 - 2
packages/fs/tool-fs-search/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/fs/tool-fs-search/README.md
-README.md: 83290df98260e977a8cd3ea808f491ea63e75857
-README.zh.md: b33fe002fc4ccc2bfe5fefec342f2044e3511078
+README.md: 84a3adc31f9c1580b90c038b0902e88b050f0340
+README.zh.md: 9f815e685eae93b7c184e529692c0c73fc8ba4bf

+ 3 - 3
packages/fs/tool-fs-search/README.md

@@ -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.

+ 3 - 3
packages/fs/tool-fs-search/README.zh.md

@@ -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` sidecar,sidecar 缺失时回退到依赖中的二进制。两种载体均打包 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` sidecar。不支持的平台或损坏的安装会以 `SEARCH_FAILED` 使调用失败,Python 运行时包则会在启动前拒绝缺失 sidecar 的安装。远程或虚拟文件系统需要共置的工作区或另一个搜索消费方。
 - **schema 只暴露一个有界页面**——偏移分页、大小写开关、替代输出模式与提供方支撑的发现仍不在本包范围内;达到上限的完整输出需要 spill 后端。
 - **启用采样时仅按搜索根正下方的第一段路径分组**——超过上限的 `glob` 页面在这些顶层条目之间平衡,因此集中在更深处的结果(一棵均匀树里某个繁忙目录)在该层级之下仍会呈现不均;递归平衡被延期。

+ 1 - 1
packages/fs/tool-fs-search/src/search-core.ts

@@ -171,7 +171,7 @@ let rgPathPromise: Promise<string> | undefined
 export function resolveRgPath(): Promise<string> {
   rgPathPromise ??= Promise.resolve().then(async () => {
     const executableSidecar = `${process.execPath}-rg`
-    if (existsSync(executableSidecar)) return executableSidecar
+    if ('pkg' in process && existsSync(executableSidecar)) return executableSidecar
     return (await import('@vscode/ripgrep')).rgPath
   })
   return rgPathPromise

+ 36 - 9
packages/fs/tool-fs-search/tests/rg-sidecar.spec.ts

@@ -1,25 +1,52 @@
-import { describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 
-const existsSync = vi.hoisted(() => vi.fn(() => true))
+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', () => new Proxy({}, {
-  get() {
-    throw new Error('the platform package must not load when the executable sidecar exists')
-  },
-}))
+vi.mock('@vscode/ripgrep', () => ({ rgPath: dependencyRgPath }))
+
+beforeEach(() => {
+  vi.resetModules()
+  existsSync.mockReset()
+  Reflect.deleteProperty(process, 'pkg')
+})
 
-import { resolveRgPath } from '@deepseek-ai/dsh-tool-fs-search'
+afterEach(() => {
+  Reflect.deleteProperty(process, 'pkg')
+})
 
-describe('single-executable ripgrep resolution', () => {
+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 - 2
python/sdk-runtime/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write python/sdk-runtime/README.md
-README.md: c00357dbad74f8de705789bce8e4a55cc5fd67b1
-README.zh.md: a62edb4cd98338d22332f8e124c3ecb3f2356ace
+README.md: 597d69a803a7cd1204fd48456f8e1ba18d9786e5
+README.zh.md: 888ecdad437d001b84b1af71c04e2da016d3901e

+ 3 - 3
python/sdk-runtime/README.md

@@ -8,7 +8,7 @@ Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`,
 
 Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injected by the repo's `scripts/build-exe-for-python-sdk.ts` build and both gitignored:
 
-- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg-<platform>-<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.
@@ -17,12 +17,12 @@ The bundled plugin set includes `@deepseek-ai/dsh-mcp-client`, so an external Co
 
 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.
 

+ 3 - 3
python/sdk-runtime/README.zh.md

@@ -8,7 +8,7 @@ Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`,
 
 两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略:
 
-- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<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` sidecar。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/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
@@ -17,12 +17,12 @@ Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`,
 
 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` sidecar。macOS wheel 包还包含与其匹配的原生 spawn helper;缺少任一 sidecar 都意味着该安装不完整,并会在启动时硬失败,即使所选 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`、不存在或存在多个运行时可执行文件、缺失或多余的 sidecar、文件不可执行以及不支持的平台标签。仓库根目录的 `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` sidecar,并在 macOS 上额外校验 `-spawn-helper` sidecar。node 载体没有单一路径的等价物,经由上面的 argv 元组启动。
 - `bundled_default_config_path() -> Path`——检入的默认配置(见下文)。
 - `bundled_package_dir() -> Path`——已安装包的数据根目录。
 

+ 5 - 4
python/sdk-runtime/src/deepseek_harness_runtime/__init__.py

@@ -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}"

+ 1 - 1
scripts/build-exe-for-python-sdk.ts

@@ -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}`)

+ 29 - 1
scripts/cordis-yaml.ts

@@ -1,5 +1,11 @@
+/**
+ * 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
 }
@@ -14,13 +20,35 @@ const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
 })
 const schema = yaml.JSON_SCHEMA.extend(jsExprType)
 
-/** Parse a Cordis config while preserving Loader `!!js` expressions as data. */
+/**
+ * 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')
+}

+ 0 - 1
scripts/smoke-python-runtime.py

@@ -29,7 +29,6 @@ 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: "
-MINIMAL_SYSTEM_PROMPT = "You are a helpful software engineer assistant."
 FS_SEARCH_PROMPT = "Exercise the packaged filesystem search tools."
 FS_SEARCH_TEXT = "filesystem search smoke ok"
 FS_SEARCH_MARKER = "PACKAGED_FS_SEARCH_OK"

+ 2 - 2
scripts/verify-cordis-config.ts

@@ -15,7 +15,7 @@ import { dirname, relative, resolve } from 'node:path'
 import { Script } from 'node:vm'
 import ts from 'typescript'
 import { cordisConfigFiles } from './cordis-config-files.ts'
-import { isJsExpr, loadCordisYaml } from './cordis-yaml.ts'
+import { isCordisGroupEntry, isJsExpr, loadCordisYaml } from './cordis-yaml.ts'
 
 interface PackageManifest {
   name?: string
@@ -191,7 +191,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}]`)
     }

+ 45 - 0
scripts/verify-runtime-closure.spec.ts

@@ -82,6 +82,51 @@ describe('verifyRuntimeClosure', () => {
     ])
   })
 
+  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('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:^' } },

+ 14 - 7
scripts/verify-runtime-closure.ts

@@ -8,7 +8,7 @@ import { globSync } from 'node:fs'
 import { readFile } from 'node:fs/promises'
 import { basename, dirname, resolve } from 'node:path'
 import { parseArgs } from 'node:util'
-import { loadCordisYaml } from './cordis-yaml.ts'
+import { isCordisGroupEntry, loadCordisYaml } from './cordis-yaml.ts'
 
 interface PackageManifest {
   name?: string
@@ -30,6 +30,8 @@ interface RuntimePlatform {
 
 type RuntimePlatformManifest = Record<string, RuntimePlatform>
 
+const AGENT_PRESET_GLOB = 'apps/cli/config/agent-presets/*/agent.cordis.yml'
+
 export interface RuntimeClosureResult {
   failures: string[]
   presetCount: number
@@ -51,6 +53,8 @@ export async function verifyRuntimeClosure(
   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[] = []
 
@@ -60,7 +64,10 @@ export async function verifyRuntimeClosure(
     queue.push(dependency)
   }
 
-  const failures = await missingPresetPlugins(root, runtimeDependencies, platforms)
+  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
@@ -86,7 +93,7 @@ export async function verifyRuntimeClosure(
 
   return {
     failures,
-    presetCount: globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root }).length,
+    presetCount: presetPaths.length,
     workspacePackageCount: queue.length,
   }
 }
@@ -112,18 +119,18 @@ if (import.meta.main) {
 async function missingPresetPlugins(
   root: string,
   runtimeDependencies: Readonly<Record<string, string>>,
-  platforms: RuntimePlatformManifest,
+  presetPaths: readonly string[],
+  targets: readonly string[],
 ): Promise<string[]> {
   const missing = new Map<string, Set<string>>()
   const failures: string[] = []
-  const presetPaths = globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root }).sort()
   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 Object.keys(platforms).sort()) {
+    for (const target of targets) {
       const processPlatform = processPlatformForTarget(target)
       for (const plugin of activeBarePluginPackages(document, processPlatform)) {
         if (runtimeDependencies[plugin] !== undefined) continue
@@ -150,7 +157,7 @@ function activeBarePluginPackages(entries: unknown[], processPlatform: string):
       const packageName = barePackageName(value.name)
       if (packageName !== undefined) packages.add(packageName)
     }
-    if (Array.isArray(value.config)) {
+    if (isCordisGroupEntry(value)) {
       for (const child of value.config) visit(child, disabled)
     }
   }

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor