Merge master after validating subagent cleanup

This commit is contained in:
Dudu-0223 committed 2026-09-20 17:59:25 +08:00
commit 703bf4d504
107 files changed
+2618 -834

No files matched your search

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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-09-17-shared-office-runtime.md
2026-09-17-shared-office-runtime.md: 25b6ddccbb5b17b93bec2c0b024dfcb63e94abc3
2026-09-17-shared-office-runtime.zh.md: 99dffe8d0233e60480689ff983b6261cbb83bb7b
@@ -0,0 +1,37 @@
# Agent Note: Shared Office runtime for Desktop and SDK
Status: implemented
English | [中文](2026-09-17-shared-office-runtime.zh.md)
## Problem
SDK deployments need the same Office authoring libraries as Desktop while keeping interpreter payloads in read-only container image layers. A Desktop-owned query and builder require downstream carriers to duplicate dependency locks and path conventions.
## Decision
[`tool-workspace-dependencies`](../../../../packages/skill/tool-workspace-dependencies/README.md) owns manifest validation, interpreter paths, installation and the model query. It stays in the skill group beside its Office workflow consumer: the payload supplies those workflows, while the package exposes no workspace entity service. Desktop imports this package and retains copy-on-first-use installation. The packaged SDK profile enables the query and Office skills from its carrier default; `DSH_PRIMARY_RUNTIME` overrides that path, including an empty-string opt-out. It reads the payload in place. The common base and `sdk-minimal` do not enable Office implicitly.
The [shared build entry](../../../../scripts/primary-runtime/prepare.ts) owns the download lock, extraction and native smoke checks for Desktop targets and GNU/Linux x64/ARM64. Desktop supplies output paths and its release version, requires Node.js and pnpm, and applies its signing-specific subprocess environment. Other carriers choose their output directory and may build a Python-only payload. The existing `desktopVersion` manifest field retains its name and records the carrier release; `payloadDigest` distinguishes locked inputs and component selection.
The [Desktop primary-runtime decision](../feature/2026-09-14-desktop-primary-runtime.md) continues to own Desktop installation and platform signing. This decision replaces only its Desktop-only builder and query placement; both records remain active.
`runtime.json` stores Python, Node.js and pnpm versions at the top level and all Python distribution versions in `pythonPackages`. Build metadata does not single out numpy or pandas. The shared parser accepts legacy `components` files without rewriting them, retains their consistency checks, and returns only the canonical flat fields. Mixed formats are rejected. The build digest includes the assembly format so changed manifest bytes invalidate payload reuse.
The Python runtime wheel carries target-native CPython and Office libraries plus external skills in a sibling `<platform>-<arch>/` resource directory. Short platform directory names leave room for Python extension and dependency DLL paths after installation on Windows. Bundling supplies defaults; the skill registry still lets project, custom and user skills override same-name bundled entries, and profile patches can disable or replace Office skills without replacing Python. This retains configurable workflows while eliminating a separate environment installation step. Real filesystem resources let Python load native extensions and skills invoke their shared checker without startup extraction. The wheel grows by the compressed payload size; public-index size limits remain enforced by the release workflow.
## Alternatives considered
**Keep implementation in Desktop.** This leaves SDK and container builders depending on application packaging and signing code despite needing only interpreters, Office resources and a query.
**Enable Office in the common base.** A base deployment has no guaranteed bundled interpreter or resources. Explicit SDK carrier configuration makes that requirement observable without changing unrelated profiles.
**Always copy to the Harness home.** Copying duplicates image-layer content, requires a writable destination and cannot serve the intended immutable carrier deployment.
**One component field per Python library.** This duplicates the distribution map and requires schema changes for each library. A complete map records the locked package set independently of the interpreter and package-manager fields.
## Consequences
Successful tool preparation is shared by concurrent calls for one plugin lifetime. Failed preparation is retryable; disposal removes registration and waits for outstanding filesystem work. Copy mode verifies a staged replacement and restores the preceding installation after a failed publication. In-place mode performs no payload writes. Paths are absolute, and runtime metadata and declared entries are checked before results are returned.
SDK configuration changes require restart. Missing Office resources produce a startup warning and leave the optional skill provider inactive; runtime metadata and interpreter availability are validated on the first tool call. musl builds remain outside the lock. Cross-target assembly verifies archive hashes but requires execution on the target host; the Linux artifact CI lane owns native Linux checks. Unit coverage pins manifest, installation and lifecycle behavior; SDK profile tests observe optional tools, skill discovery and results, and the recorded workspace-dependencies scenario pins model-visible validation errors.
@@ -0,0 +1,37 @@
# Agent Note: Desktop 与 SDK 共享 Office 运行时
Status: implemented
[English](2026-09-17-shared-office-runtime.md) | 中文
## 问题
SDK 部署需要与 Desktop 相同的 Office 创作库,同时将解释器 payload 保存在只读容器镜像层中。由 Desktop 持有查询工具和构建器,会要求下游载体重复维护依赖锁与路径约定。
## 决策
[`tool-workspace-dependencies`](../../../../packages/skill/tool-workspace-dependencies/README.zh.md) 持有清单校验、解释器路径、安装逻辑和模型查询。它保留在 skill 组中,与 Office 工作流消费者相邻:payload 服务于这些工作流,本包不提供 workspace 实体服务。Desktop 引用本包并保留首次使用时复制安装。打包的 SDK profile 通过载体默认路径启用查询和 Office skills;`DSH_PRIMARY_RUNTIME` 覆盖该路径,空字符串表示禁用。它原位读取 payload。公共 base 与 `sdk-minimal` 不会隐式启用 Office。
[共享构建入口](../../../../scripts/primary-runtime/prepare.ts) 持有 Desktop 目标与 GNU/Linux x64/ARM64 的下载锁、解压和本机冒烟检查。Desktop 提供输出路径和发布版本,要求包含 Node.js 与 pnpm,并应用签名专用子进程环境。其他载体选择自己的输出目录,也可构建 Python-only payload。现有 `desktopVersion` 清单字段保留名字并记录载体发布版本;`payloadDigest` 区分锁定输入与组件选择。
[Desktop 第一方运行时决策](../feature/2026-09-14-desktop-primary-runtime.zh.md)继续持有 Desktop 安装与平台签名规则。本决策仅替代其中由 Desktop 独占构建器和查询实现的部分;两份记录均保持有效。
`runtime.json` 在顶层保存 Python、Node.js 和 pnpm 版本,并在 `pythonPackages` 中保存全部 Python 分发包版本。构建元数据不单独列出 numpy 或 pandas。共享解析器接受旧 `components` 文件而不改写它们,保留旧格式的一致性校验,并仅返回统一的扁平字段。混合格式会被拒绝。构建摘要包含组装格式,因此清单字节变化会使旧产物身份失效。
Python runtime wheel 将目标平台的 CPython、Office 库和外部 skills 放在可执行文件同级的 `<platform>-<arch>/` 资源目录中。较短的平台目录名为 Windows 安装后的 Python 扩展与依赖 DLL 路径保留长度空间。打包提供默认资源;skill 注册表仍允许项目、自定义目录和用户 skills 覆盖同名随包条目,profile patch 可在保留 Python 的同时禁用或替换 Office skills。这样既保留工作流配置能力,又省去单独安装环境的步骤。真实文件系统资源允许 Python 加载原生扩展,并让 skills 调用共用检查脚本,无需启动时解包。wheel 体积增加一个压缩 payload;发布流程仍执行公开索引的大小限制检查。
## 考虑过的替代方案
**将实现保留在 Desktop。** SDK 和容器构建器只需要解释器、Office 资源和查询,却仍会依赖应用打包及签名代码。
**在公共 base 中启用 Office。** base 部署不保证提供内置解释器或资源。显式 SDK 载体配置可以让该要求可见,并保持其他 profile 的行为。
**总是复制到 Harness home。** 复制会重复镜像层内容、要求目标可写,也无法满足预期的不可变载体部署。
**为每个 Python 库增加组件字段。** 这会重复分发包版本表,并使新增库要求修改 schema。完整版本表将锁定包集合与解释器、包管理器字段分开记录。
## 影响
同一插件生命周期内的并发调用共享成功的工具准备结果。准备失败后可重试;卸载移除工具注册并等待未完成的文件系统工作。复制模式验证暂存替换内容,并在发布失败后恢复原安装。原位模式不写入 payload。路径为绝对路径,返回结果前会检查运行时元数据和所声明的条目。
SDK 配置变更需要重启。缺少 Office 资源会产生启动警告,并使可选的 skill 提供方保持未激活;运行时元数据和解释器是否存在在首次工具调用时校验。musl 构建仍不在锁文件范围内。跨目标组装校验归档哈希,但需要在目标主机执行;Linux 产物 CI 负责本机 Linux 检查。单元覆盖固定清单、安装与生命周期行为;SDK profile 测试观察可选工具、skill 发现与结果,workspace-dependencies 录制场景固定模型可见的校验错误。
@@ -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/feature/2026-09-14-desktop-primary-runtime.md
2026-09-14-desktop-primary-runtime.md: 51bbaac89c5c2ec9a3a016030e7b320d2b5566b8
2026-09-14-desktop-primary-runtime.zh.md: 4f27d61b5e713f66e2f478894dbd547cd7ecfff4
2026-09-14-desktop-primary-runtime.md: a1491ca0c47e40c6d5b885c9f6a42ccf8180b78c
2026-09-14-desktop-primary-runtime.zh.md: 6447086a6948ed262699b62061eb44af8d6071d7
@@ -12,9 +12,9 @@ Desktop agents need predictable Python data-processing libraries and an independ
Desktop ships Python, Node.js, pnpm, data-processing libraries and Office authoring libraries as one release-bound payload. The path-query tool installs the payload from application resources into the fixed Harness-home directory and returns absolute paths and the bundled Python distribution versions. The version report excludes packages added by users. It does not change PATH, environment variables or package-manager configuration. pnpm uses its native global-install rules.
The application version, component versions, Python distribution versions and locked-input digest live in `runtime.json`, not the directory name. The digest covers the selected target's interpreter and wheel archives, shared wheel inputs, Python distribution versions, pnpm version and assembly format; other targets do not invalidate it. Key order within the selected target, wheel records and distribution map, plus wheel-entry order, participates in this identity; top-level lock key order does not. Extraction or assembly changes that alter payload bytes without changing locked inputs require an explicit format bump. Installation publishes a completed staged copy and retains the previous directory until replacement succeeds. Matching payloads reuse installed files; replacements also replace user-added Python dependencies inside the managed tree. Older manifests without a digest remain readable and differ from the current payload. Distribution names use PEP 503 normalization; duplicate normalized names are rejected, and present numpy/pandas distribution versions must agree with their component versions. The Desktop single-instance owner and the tool's shared installation promise serialize normal installation requests.
The application version, top-level interpreter/package-manager versions, Python distribution map and locked-input digest live in `runtime.json`, not the directory name. The digest covers the selected target's interpreter and wheel archives, shared wheel inputs, Python distribution versions, pnpm version and assembly format; other targets do not invalidate it. Key order within the selected target, wheel records and distribution map, plus wheel-entry order, participates in this identity; top-level lock key order does not. Extraction or assembly changes that alter payload bytes without changing locked inputs require an explicit format bump. Installation publishes a completed staged copy and retains the previous directory until replacement succeeds. Matching payloads reuse installed files; replacements also replace user-added Python dependencies inside the managed tree. Older manifests without a digest remain readable and differ from the current payload. Distribution names use PEP 503 normalization; duplicate normalized names are rejected. The reader normalizes legacy `components` files and retains their numpy/pandas consistency checks; current manifests carry Python libraries only in the distribution map. The Desktop single-instance owner and the tool's shared installation promise serialize normal installation requests.
Node downloads and hash-verifies the complete locked wheel set and unpacks library files into site-packages. This avoids build-host Python and pip version selection without implementing dependency resolution or general wheel installation. Auxiliary scripts, including XlsxWriter's VBA extraction script, remain under the wheel's `.data/scripts` directory; command-line entry-point wrappers are outside this library payload. Other `.data` installation schemes are rejected. Native smoke executes the final payload after temporary files are removed, checking the exact locked distribution set plus bundled pip, Python and pinned wheel versions, dependency completeness and editable Office document round trips, so interpreter links must survive relocation. Smoke checks disable bytecode writes to keep validation artifacts out of the shipped payload.
The shared Node build entry downloads and hash-verifies the complete locked wheel set and unpacks library files into site-packages. This avoids build-host Python and pip version selection without implementing dependency resolution or general wheel installation. Auxiliary scripts, including XlsxWriter's VBA extraction script, remain under the wheel's `.data/scripts` directory; command-line entry-point wrappers are outside this library payload. Other `.data` installation schemes are rejected. Native smoke executes the final payload after temporary files are removed, checking the exact locked distribution set plus bundled pip, Python and pinned wheel versions, dependency completeness and editable Office document round trips, so interpreter links must survive relocation. Smoke checks disable bytecode writes to keep validation artifacts out of the shipped payload.
macOS grants `com.apple.security.cs.allow-jit` only to the standalone Node executable. Hardened-runtime signing without that entitlement prevents V8 from allocating its code region. Interpreter and library smoke checks run after signing as well as after staging cleanup; a valid signature alone does not establish executable behavior.
@@ -22,6 +22,8 @@ Windows signed packaging separates materialization from execution with supervise
Desktop ZIP extraction pins `extract-zip` to `yauzl` 3.4.0 through a scoped dependency override. The 2.x reader can leave large deflate entries unfinished on Node 26 ([upstream issue](https://github.com/thejoshwolfe/yauzl/issues/176)); retaining the existing extractor preserves its path validation and wheel-entry checks. The development launcher uses top-level await so unfinished preparation cannot exit successfully. A large compressed wheel regression checks the complete extracted bytes.
The [shared-runtime decision](../architecture/2026-09-17-shared-office-runtime.md) owns builder, lock and query-package placement for Desktop and SDK carriers, including GNU/Linux x64 and Python-only payloads. Desktop retains the installation and signing behavior described here and requires the full Python, Node.js and pnpm payload.
## Alternatives considered
**System interpreters only.** They do not provide predictable availability or preinstalled numpy and pandas.
@@ -12,9 +12,9 @@ Desktop 代理需要在没有开发环境的机器上获得确定的 Python 数
Desktop 将 Python、Node.js、pnpm、数据处理库和 Office 创作库作为绑定应用版本的产物交付。路径查询工具从应用资源将产物安装到 Harness home 下的固定目录,并返回绝对路径与内置 Python 分发包版本。版本报告不包含用户自行添加的包。它不修改 PATH、环境变量或包管理器配置。pnpm 使用原生全局安装规则。
应用版本、组件版本、Python 分发包版本和锁定输入摘要记录在 `runtime.json` 中,不放在目录名里。摘要涵盖所选目标的解释器与 wheel 压缩包、共享 wheel 输入、Python 分发包版本、pnpm 版本和组装格式;其他目标不会使其失效。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序参与该身份计算;锁文件顶层键的顺序不参与。解压或组装逻辑在锁定输入不变时改变产物字节,必须显式提升格式版本。安装发布完整的暂存副本,并在替换成功前保留之前的目录。相同产物复用已安装文件;替换也会移除受管目录内用户添加的 Python 依赖。不含摘要的旧清单仍可读取,并与当前产物区分。分发包名称按 PEP 503 归一化,归一化后重复的名称会被拒绝;清单包含 numpy/pandas 分发包版本时,必须与相应组件版本一致。Desktop 单实例所有者和工具共享的安装 Promise 串行处理正常安装请求。
应用版本、顶层解释器和包管理器版本、Python 分发包版本表和锁定输入摘要记录在 `runtime.json` 中,不放在目录名里。摘要涵盖所选目标的解释器与 wheel 压缩包、共享 wheel 输入、Python 分发包版本、pnpm 版本和组装格式;其他目标不会使其失效。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序参与该身份计算;锁文件顶层键的顺序不参与。解压或组装逻辑在锁定输入不变时改变产物字节,必须显式提升格式版本。安装发布完整的暂存副本,并在替换成功前保留之前的目录。相同产物复用已安装文件;替换也会移除受管目录内用户添加的 Python 依赖。不含摘要的旧清单仍可读取,并与当前产物区分。分发包名称按 PEP 503 归一化,归一化后重复的名称会被拒绝。读取器归一化旧 `components` 文件并保留其 numpy/pandas 一致性校验;当前清单仅在分发包版本表中记录 Python 库。Desktop 单实例所有者和工具共享的安装 Promise 串行处理正常安装请求。
Node 下载并校验完整锁定 wheel 集的哈希,将库文件解压到 site-packages。这避免选择构建主机的 Python 和 pip 版本,也无需实现依赖解析或通用 wheel 安装。XlsxWriter 的 VBA 提取脚本等辅助脚本保留在 wheel 的 `.data/scripts` 目录中;命令行入口包装器不属于该库产物。其他 `.data` 安装方案会被拒绝。本机 smoke 在临时文件删除后执行最终产物,检查精确锁定的分发包集合与内置 pip、Python 与固定 wheel 版本、依赖完整性及可编辑 Office 文档的写入和读取,因此解释器链接必须在迁移后仍有效。Smoke 检查禁用字节码写入,避免把验证产物纳入分发内容。
共享 Node 构建入口下载并校验完整锁定 wheel 集的哈希,将库文件解压到 site-packages。这避免选择构建主机的 Python 和 pip 版本,也无需实现依赖解析或通用 wheel 安装。XlsxWriter 的 VBA 提取脚本等辅助脚本保留在 wheel 的 `.data/scripts` 目录中;命令行入口包装器不属于该库产物。其他 `.data` 安装方案会被拒绝。本机 smoke 在临时文件删除后执行最终产物,检查精确锁定的分发包集合与内置 pip、Python 与固定 wheel 版本、依赖完整性及可编辑 Office 文档的写入和读取,因此解释器链接必须在迁移后仍有效。Smoke 检查禁用字节码写入,避免把验证产物纳入分发内容。
macOS 仅向独立 Node 可执行文件授予 `com.apple.security.cs.allow-jit`。缺少此权限的强化运行时签名会阻止 V8 分配代码区域。解释器和库的 smoke 检查在签名后以及暂存清理后执行;签名有效本身不能证明程序可运行。
@@ -22,6 +22,8 @@ Windows 签名打包通过受监督的签名阶段覆盖第一方运行时和应
Desktop ZIP 解压通过定向依赖覆盖为 `extract-zip` 固定 `yauzl` 3.4.0。2.x 读取器在 Node 26 上可能无法完成较大 deflate 条目的读取([上游问题](https://github.com/thejoshwolfe/yauzl/issues/176));保留现有解压器可保留其路径校验和 wheel 条目检查。开发启动器使用顶层 await,避免准备未完成却成功退出。大压缩 wheel 回归测试检查完整的解压字节。
[共享运行时决策](../architecture/2026-09-17-shared-office-runtime.zh.md)持有 Desktop 与 SDK 载体的构建器、锁文件和查询包归属,覆盖 GNU/Linux x64 与 Python-only payload。Desktop 保留本文的安装与签名行为,并要求完整的 Python、Node.js 和 pnpm payload。
## Alternatives considered
**只使用系统解释器。** 无法保证可用性或预装 numpy 和 pandas。
@@ -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/process/2026-07-30-generated-third-party-notices.md
2026-07-30-generated-third-party-notices.md: 2b0bd012c23c873b3003916491daad467cf757b3
2026-07-30-generated-third-party-notices.zh.md: 0c7b4df8edb60f2f70781e179edc7377da930128
2026-07-30-generated-third-party-notices.md: 71f1fd0a5b650d53c5ef21438e17e0a6e162bdf5
2026-07-30-generated-third-party-notices.zh.md: e3adb43e4b407945a8cca88105de689407b5908b
@@ -12,13 +12,13 @@ A hand-written inventory answers none of those durably. Roughly a hundred rows o
## Decision
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) is generated by [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) from the workspace manifests, `vendor/README.md`, the `pyproject.toml` files, `pnpm-workspace.yaml`, and the [Desktop runtime lock](../../../../apps/desktop/scripts/primary-runtime-lock.json). The root README pair links the file from its License section.
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) is generated by [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) from the workspace manifests, `vendor/README.md`, the `pyproject.toml` files, `pnpm-workspace.yaml`, and the [shared runtime lock](../../../../scripts/primary-runtime/lock.json). The root README pair links the file from its License section.
**Freshness is maintained, not merely enforced.** A pre-commit job regenerates the file and stages it whenever a generator input is staged — any manifest, a workspace declaration, the root or Desktop runtime lock file, `vendor/README.md`, a `pyproject.toml`, the generator itself, or the script holding the build-time pin — so an unrelated dependency edit never has to come back and rerun a generator. The committed bytes are then asserted inside [`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts), which the test lane already runs — the check adds no gate process, no scheduler slot, and no separate CI step. `pnpm run verify-third-party-notices` remains available for a standalone check.
**Freshness is maintained, not merely enforced.** A pre-commit job regenerates the file and stages it whenever a generator input is staged — any manifest, a workspace declaration, the root or shared runtime lock file, `vendor/README.md`, a `pyproject.toml`, the generator itself, or the script holding the build-time pin — so an unrelated dependency edit never has to come back and rerun a generator. The committed bytes are then asserted inside [`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts), which the test lane already runs — the check adds no gate process, no scheduler slot, and no separate CI step. `pnpm run verify-third-party-notices` remains available for a standalone check.
One trigger gap is accepted rather than worked around: lefthook inspects only files present on disk, so **deleting** a manifest runs no job, and removing a package reaches the assertion in the test lane instead. Reconstructing the staged file list to include deletions does not work — lefthook filters the list against the working tree either way. The assertion is the backstop for exactly this case.
The file discloses **direct** dependencies by default; complete npm and Python SDK closures remain in their lock files. Two separately distributed payload sets receive explicit entries: the official Claude executable packages declared by `@anthropic-ai/claude-agent-sdk`, and every distribution in the Desktop runtime lock's `pythonPackages`. Desktop entries include normalized names, exact versions, and recorded license metadata; missing metadata, duplicate normalized names and conflicting normalized versions fail generation. The Desktop wheel set is independent of the `python/` manifests, so scanning only those manifests would omit the packaged Office libraries. Desktop distributions also pass the runtime license check, including the permissive `MIT-CMU` and `PSF-2.0` identifiers declared by Pillow and typing-extensions.
The file discloses **direct** dependencies by default; complete npm and Python SDK closures remain in their lock files. Two separately distributed payload sets receive explicit entries: the official Claude executable packages declared by `@anthropic-ai/claude-agent-sdk`, and every distribution in the shared runtime lock's `pythonPackages`. Bundled entries include normalized names, exact versions, and recorded license metadata; missing metadata, duplicate normalized names and conflicting normalized versions fail generation. The shared wheel set is independent of the `python/` manifests, so scanning only those manifests would omit the packaged Office libraries. Bundled distributions also pass the runtime license check, including the permissive `MIT-CMU` and `PSF-2.0` identifiers declared by Pillow and typing-extensions.
**Tiering follows distribution, not manifest section.** Installed runtime libraries are identified by `dependencies` or `optionalDependencies` outside `DEV_ONLY_AREAS` — the root manifest, `packages/test-support/`, `packages/test-support/client-runtime/`, `website/`, `native/`. Browser inputs resolved by the shipping tsdown and Vite configurations also count as runtime, even in `devDependencies`; [browser third-party build inputs](2026-09-08-browser-third-party-build-inputs.md) owns that classification. Test-support dependencies do not ship merely because their manifest says `dependencies`, and the generator explicitly discloses `tsx` because source launches execute through its ESM hook.
@@ -12,13 +12,13 @@ Status: implemented
## 决策
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) 由 [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) 依据各工作区 manifest、`vendor/README.md`、`pyproject.toml`、`pnpm-workspace.yaml` 与 [Desktop 运行时锁文件](../../../../apps/desktop/scripts/primary-runtime-lock.json)生成。根 README 双语两侧都从「许可证」一节链到该文件。
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) 由 [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) 依据各工作区 manifest、`vendor/README.md`、`pyproject.toml`、`pnpm-workspace.yaml` 与 [共享运行时锁文件](../../../../scripts/primary-runtime/lock.json)生成。根 README 双语两侧都从「许可证」一节链到该文件。
**新鲜度会得到维护,而非仅靠校验。** 只要暂存了生成器的任一输入——任何 manifest、工作区声明、根锁文件或 Desktop 运行时锁文件、`vendor/README.md`、某个 `pyproject.toml`、生成器自身,或持有构建期 pin 的脚本——pre-commit 任务就会重新生成该文件并将其暂存,改依赖的人不必事后再折返跑一次生成器。已提交的字节随后由 [`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts) 断言,而测试 lane 本就会跑这个文件——这项校验不增加门禁进程、不占调度位、也不新增 CI 步骤。需要单独校验时,`pnpm run verify-third-party-notices` 仍然可用。
**新鲜度会得到维护,而非仅靠校验。** 只要暂存了生成器的任一输入——任何 manifest、工作区声明、根锁文件或共享运行时锁文件、`vendor/README.md`、某个 `pyproject.toml`、生成器自身,或持有构建期 pin 的脚本——pre-commit 任务就会重新生成该文件并将其暂存,改依赖的人不必事后再折返跑一次生成器。已提交的字节随后由 [`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts) 断言,而测试 lane 本就会跑这个文件——这项校验不增加门禁进程、不占调度位、也不新增 CI 步骤。需要单独校验时,`pnpm run verify-third-party-notices` 仍然可用。
有一处触发缺口是接受而非绕过的:lefthook 只检视磁盘上存在的文件,因此**删除** manifest 不会触发任何任务,移除一个包会落到测试 lane 的断言上。重构暂存文件列表以纳入删除的做法不成立——无论怎么给列表,lefthook 都会拿工作树过滤一遍。这个场景正由断言兜底。
文件默认只披露**直接**依赖;完整的 npm 与 Python SDK 闭包保留在各自锁文件中。两组独立分发的载荷会明确列出:`@anthropic-ai/claude-agent-sdk` 声明的官方 Claude 可执行包,以及 Desktop 运行时锁文件 `pythonPackages` 中的每个分发包。Desktop 条目包含归一化名称、精确版本和已记录的许可证元数据;元数据缺失、归一化后名称重复或版本冲突都会使生成失败。Desktop wheel 集合独立于 `python/` 下的 manifest,仅扫描后者会遗漏已打包的 Office 库。Desktop 分发包同样通过运行时许可证检查,其中包括 Pillow 与 typing-extensions 声明的宽松许可证标识 `MIT-CMU` 和 `PSF-2.0`。
文件默认只披露**直接**依赖;完整的 npm 与 Python SDK 闭包保留在各自锁文件中。两组独立分发的载荷会明确列出:`@anthropic-ai/claude-agent-sdk` 声明的官方 Claude 可执行包,以及共享运行时锁文件 `pythonPackages` 中的每个分发包。内置条目包含归一化名称、精确版本和已记录的许可证元数据;元数据缺失、归一化后名称重复或版本冲突都会使生成失败。共享 wheel 集合独立于 `python/` 下的 manifest,仅扫描后者会遗漏已打包的 Office 库。内置分发包同样通过运行时许可证检查,其中包括 Pillow 与 typing-extensions 声明的宽松许可证标识 `MIT-CMU` 和 `PSF-2.0`。
**分层依据是分发内容,而非 manifest 字段名。** 安装的运行时库由 `DEV_ONLY_AREAS` 之外的 `dependencies` 或 `optionalDependencies` 识别;排除区域为根 manifest、`packages/test-support/`、`packages/test-support/client-runtime/`、`website/`、`native/`。发布所用的 tsdown 与 Vite 配置解析到的浏览器输入也属于运行时,即使它们位于 `devDependencies`;[浏览器第三方构建输入](2026-09-08-browser-third-party-build-inputs.zh.md)拥有这项分类。测试支撑依赖不会仅因字段写成 `dependencies` 就被交付,而生成器显式披露 `tsx`,因为源码启动通过其 ESM 钩子执行。
+3
View File
@@ -357,6 +357,9 @@ jobs:
if: vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]'
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium
- name: Build and execute the Linux Office runtime
run: CI=true pnpm run prepare:primary-runtime --target linux-x64 --output "$RUNNER_TEMP/office-runtime" --cache "$RUNNER_TEMP/office-runtime-downloads"
- name: Run compatibility, snapshot, and artifact gates
run: pnpm run check:ci:consumers
+1 -2
View File
@@ -32,8 +32,7 @@ mise.toml
dist-exe/
/dist/
apps/desktop/.desktop-build/
python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*
python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/
python/sdk-runtime/src/deepseek_harness_runtime/runtime/
python/**/__pycache__/
python/**/.pytest_cache/
packages/**/__pycache__/
+3 -3
View File
@@ -5,7 +5,7 @@
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
This file lists **direct** dependencies declared by the workspace, the explicitly disclosed official Claude Code platform payload closure, and the Desktop bundled Python distributions. It is generated by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
This file lists **direct** dependencies declared by the workspace, the explicitly disclosed official Claude Code platform payload closure, and the Bundled Python distributions. It is generated by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [`pnpm-lock.yaml`](pnpm-lock.yaml) — inspect it with `pnpm licenses list`. The Python SDK closure is recorded separately in [`python/sdk/uv.lock`](python/sdk/uv.lock).
@@ -245,9 +245,9 @@ Direct dependencies of the `pyproject.toml` manifests, plus `uv` as the developm
| [`pytest`](https://github.com/pytest-dev/pytest) | MIT | test-only |
| [`uv`](https://github.com/astral-sh/uv) | MIT / Apache-2.0 | development workflow tool |
## Desktop bundled Python distributions
## Bundled Python distributions
The [Desktop runtime lock](apps/desktop/scripts/primary-runtime-lock.json) records each distribution version and the wheel download hashes. The table includes every entry in `pythonPackages`, including transitive dependencies. Wheel extraction preserves distribution metadata and the license and notice files supplied by each archive. Project licenses below do not enumerate the separate licenses of native libraries bundled inside wheels.
The [shared runtime lock](scripts/primary-runtime/lock.json) records each distribution version and the wheel download hashes. The table includes every entry in `pythonPackages`, including transitive dependencies. Wheel extraction preserves distribution metadata and the license and notice files supplied by each archive. Project licenses below do not enumerate the separate licenses of native libraries bundled inside wheels.
| Distribution | Locked version | Project license |
| --- | --- | --- |
+2
View File
@@ -69,6 +69,8 @@
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
"@deepseek-ai/dsh-skill-office": "workspace:^",
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:^",
"@deepseek-ai/dsh-terminal": "workspace:^",
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
"@deepseek-ai/dsh-time-context": "workspace:^",
+1 -2
View File
@@ -13,7 +13,7 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
const modules = join(root, 'node_modules', '@deepseek-ai')
const hostDirectory = fileURLToPath(new URL('../../desktop-host/', import.meta.url))
const manifest = JSON.parse(readFileSync(join(hostDirectory, 'package.json'), 'utf8')) as { dependencies: Record<string, string> }
const stubbed = new Set(['@deepseek-ai/dsh-app-boot', '@deepseek-ai/dsh', '@deepseek-ai/dsh-home-paths', '@deepseek-ai/dsh-tools'])
const stubbed = new Set(['@deepseek-ai/dsh-app-boot', '@deepseek-ai/dsh', '@deepseek-ai/dsh-home-paths'])
for (const name of Object.keys(manifest.dependencies)) {
const destination = join(root, 'node_modules', name)
mkdirSync(dirname(destination), { recursive: true })
@@ -22,7 +22,6 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
}
for (const [name, source] of [
['dsh-home-paths', `export const resolveDshHome = () => ${JSON.stringify(root)}`],
['dsh-tools', 'export const defineTool = value => value'],
] as const) {
writeFileSync(join(modules, name, 'package.json'), '{"type":"module","exports":"./index.js"}')
writeFileSync(join(modules, name, 'index.js'), source)
@@ -1,13 +1,14 @@
import { createServer } from 'node:http'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { zstdDecompress } from 'node:zlib'
import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
import { execa } from 'execa'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, onTestFinished } from 'vitest'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
const launch = resolveExampleLaunch({
@@ -408,3 +409,123 @@ describe('Python SDK dsh profile keyless smoke', () => {
}
}, 30_000)
})
/** Materialize a native-layout payload whose paths the query can validate without executing binaries. */
async function officeFixture(root: string, pythonOnly: boolean) {
const source = join(root, 'resources', 'primary-runtime')
const manifest: PrimaryRuntimeManifest = {
desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
python: '3.12.14',
...(pythonOnly ? {} : { node: '24.21.0', pnpm: '11.7.0' }),
pythonPackages: { 'python-docx': '1.2.0', 'python-pptx': '1.0.2', openpyxl: '3.1.5' },
}
const paths = workspaceDependencyPaths(source, manifest)
for (const file of [paths.python, paths.node, paths.pnpm]) {
if (file === undefined) continue
await mkdir(dirname(file), { recursive: true })
await writeFile(file, 'fixture interpreter')
}
await mkdir(paths.pythonPackages, { recursive: true })
if (paths.nodePackages !== undefined) await mkdir(paths.nodePackages, { recursive: true })
await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
await cp(join(repoRoot, 'packages/skill/skill-office/assets'), join(root, 'resources', 'office-skills'), { recursive: true })
return { source, paths }
}
it.each(['unset', 'empty', 'bundled', 'full', 'python-only', 'missing-assets', 'wrong-type'] as const)('composes Office resources through the SDK profile (%s)', async (mode) => {
const root = await mkdtemp(join(tmpdir(), 'sdk-office-'))
onTestFinished(() => rm(root, { recursive: true, force: true }))
const enabled = mode !== 'unset' && mode !== 'empty'
const { source, paths } = await officeFixture(root, mode === 'python-only')
if (mode === 'missing-assets') await rm(join(root, 'resources', 'office-skills'), { recursive: true })
if (mode === 'wrong-type') {
await rm(paths.python)
await mkdir(paths.python)
}
const requests: Record<string, unknown>[] = []
const server = createServer((request, response) => {
let body = ''
request.setEncoding('utf8').on('data', (chunk: string) => { body += chunk })
request.on('end', () => {
requests.push(JSON.parse(body) as Record<string, unknown>)
const query = requests.length === 1 && enabled
response.writeHead(200, { 'content-type': 'text/event-stream' })
response.end(messagesResponse(query
? { type: 'tool_use', id: 'workspace-dependencies', name: 'load_workspace_dependencies', input: {} }
: { type: 'text', text: 'done' }, query ? 'tool_use' : 'end_turn'))
})
})
onTestFinished(() => new Promise<void>((resolve) => { server.close(() => { resolve() }) }))
await new Promise<void>((resolve) => { server.listen(0, '127.0.0.1', resolve) })
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('model fixture did not bind')
const home = join(root, 'home')
const officeLaunch = resolveExampleLaunch({
srcBin: fileURLToPath(new URL('../../../src/bin.ts', import.meta.url)), mode: 'lib',
configArgs: ['--profile', 'sdk'],
env: { DSH_HOME: home, DSH_PRIMARY_RUNTIME: mode === 'unset' || mode === 'bundled' ? undefined : mode === 'empty' ? '' : source + '/',
DSH_BUNDLED_PRIMARY_RUNTIME: mode === 'unset' ? undefined : mode === 'bundled' || mode === 'empty' ? source : join(root, 'unused-default'),
DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'local-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}` },
})
const child = execa(officeLaunch.command, officeLaunch.args, { cwd: repoRoot, env: officeLaunch.env, timeout: 60_000, reject: false })
onTestFinished(async () => { child.kill('SIGKILL'); await child })
let buffer = '', stderr = ''
const lines: string[] = []
child.stdout.on('data', (chunk: Buffer) => {
buffer += chunk.toString()
const parts = buffer.split('\n')
buffer = parts.pop() ?? ''
lines.push(...parts)
})
child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString() })
const send = (id: number, method: string, params?: object) => {
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`)
return waitForLine(lines, value => value.id === id, () => stderr)
}
expect(await send(1, 'initialize', { cwd: root, provider: 'deepseek-official', model: 'deepseek-v4-pro' })).toHaveProperty('result')
await send(2, 'session/prompt', { sessionId: 'office', contentBlocks: [{ type: 'text', text: 'Query workspace dependencies.' }] })
await waitForLine(lines, (value) => {
const params = value.params as { sessionId?: string; event?: { type?: string } } | undefined
return value.method === 'session.event' && params?.sessionId === 'office' && params.event?.type === 'turn/end'
}, () => stderr)
const names = (requests[0]!.tools as { name: string }[]).map(value => value.name)
expect(names.includes('load_workspace_dependencies')).toBe(enabled)
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
expect(JSON.stringify(requests[0]!.messages).includes(name)).toBe(enabled && mode !== 'missing-assets')
}
if (mode === 'wrong-type') {
expect(requests).toHaveLength(2)
const messages = requests[1]!.messages as { content: { type: string; tool_use_id?: string; content?: unknown }[] }[]
const result = messages.flatMap(message => message.content).find(block => block.tool_use_id === 'workspace-dependencies')
expect(JSON.parse(JSON.stringify(result).replaceAll(JSON.stringify(paths.python).slice(1, -1), '<python>'))).toMatchInlineSnapshot(`
{
"content": [
{
"text": "Error: primary runtime: expected file at <python>",
"type": "text",
},
],
"is_error": true,
"tool_use_id": "workspace-dependencies",
"type": "tool_result",
}
`)
} else if (enabled) {
expect(requests).toHaveLength(2)
const content: unknown = expect.arrayContaining([
expect.objectContaining({ type: 'tool_result', tool_use_id: 'workspace-dependencies',
content: [{ type: 'text', text: JSON.stringify(paths, undefined, 2) }] }),
])
expect(requests[1]!.messages).toEqual(expect.arrayContaining([
expect.objectContaining({ role: 'user', content }),
]))
}
if (mode === 'missing-assets') expect(stderr).toContain('check_office.py')
await send(3, 'shutdown')
const exit = await child
expect(exit.timedOut).toBe(false)
expect(exit.signal).toBeUndefined()
expect(exit.exitCode, stderr).toBe(0)
await expect(readFile(join(home, 'dsh-runtimes', 'dsh-primary-runtime', 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
})
+1 -1
View File
@@ -18,7 +18,7 @@
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:^",
"@deepseek-ai/dsh-skill-office": "workspace:^"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
import { dirname, join } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import * as officeSkills from '@deepseek-ai/dsh-skill-office'
import * as workspaceDependencies from './workspace-dependencies.ts'
import * as workspaceDependencies from '@deepseek-ai/dsh-tool-workspace-dependencies'
/** Loader identity for the application-owned Office composition. */
export const name = 'desktop-office'
-134
View File
@@ -1,134 +0,0 @@
/** Offline installation and absolute paths for Desktop's bundled script dependencies. */
import { cp, lstat, mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises'
import { dirname, join } from 'node:path'
/** Versions recorded by the Desktop build, independent of user-installed packages. */
export interface PrimaryRuntimeManifest {
readonly desktopVersion: string
readonly platform: string
readonly arch: string
/** Locked payload identity; absent only in installations made before payload hashing. */
readonly payloadDigest?: string
/** Installed wheel distribution versions; absent in older release manifests. */
readonly pythonPackages?: Readonly<Record<string, string>>
readonly components: {
readonly python: string
readonly node: string
readonly pnpm: string
readonly numpy: string
readonly pandas: string
}
}
/** Absolute entry points and bundled versions; pnpm runs through the returned Node executable. */
export interface WorkspaceDependencies {
readonly python: string
readonly node: string
readonly pnpm: string
readonly pythonPackages: string
readonly nodePackages: string
/** Locked distribution versions; excludes packages users add to the installed environment. */
readonly pythonDistributions: Readonly<Record<string, string>>
}
/**
* Read build metadata, rejecting duplicate normalized names and conflicting component/distribution versions.
* @param root - Installed or bundled primary runtime directory.
* @returns Validated component versions and target identifiers.
*/
export async function readPrimaryRuntime(root: string): Promise<PrimaryRuntimeManifest> {
const value: unknown = JSON.parse(await readFile(join(root, 'runtime.json'), 'utf8'))
if (typeof value !== 'object' || value === null) throw new Error('primary runtime: invalid metadata')
const record = value as Record<string, unknown>
const components = record.components
const packages = record.pythonPackages
if (typeof record.desktopVersion !== 'string' || record.desktopVersion.length === 0
|| !['win32', 'darwin'].includes(String(record.platform)) || !['x64', 'arm64'].includes(String(record.arch))
|| typeof components !== 'object' || components === null
|| !['python', 'node', 'pnpm', 'numpy', 'pandas'].every(key => /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/u.test(String((components as Record<string, unknown>)[key])))
|| (record.payloadDigest !== undefined && (typeof record.payloadDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.payloadDigest)))
|| (packages !== undefined && (typeof packages !== 'object' || packages === null || Array.isArray(packages)
|| !Object.entries(packages).every(([name, version]) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)
&& typeof version === 'string' && /^\d[\w.!+-]*$/u.test(version))))) {
throw new Error('primary runtime: invalid metadata')
}
const manifest = value as PrimaryRuntimeManifest
const entries = Object.entries(manifest.pythonPackages ?? {})
const distributions = new Map(entries.map(([name, version]) => [name.toLowerCase().replace(/[-_.]+/gu, '-'), version]))
if (distributions.size !== entries.length) throw new Error('primary runtime: invalid metadata')
for (const name of ['numpy', 'pandas'] as const) {
const version = distributions.get(name)
if (version !== undefined && version !== manifest.components[name]) {
throw new Error(`primary runtime: conflicting ${name} distribution version`)
}
}
return manifest
}
/**
* Resolve platform-specific interpreter and library locations without changing the environment.
* @param root - Absolute installation directory.
* @param manifest - Validated runtime metadata.
* @returns Absolute paths for explicit script execution and recorded bundled Python versions.
*/
export function workspaceDependencyPaths(root: string, manifest: PrimaryRuntimeManifest): WorkspaceDependencies {
const dependencies = join(root, 'dependencies')
const windows = manifest.platform === 'win32'
return {
python: join(dependencies, 'python', ...(windows ? ['python.exe'] : ['bin', 'python3'])),
node: join(dependencies, 'node', 'bin', windows ? 'node.exe' : 'node'),
pnpm: join(dependencies, 'pnpm', 'bin', 'pnpm.mjs'),
pythonPackages: join(dependencies, 'python', ...(windows ? ['Lib'] : ['lib', `python${manifest.components.python.split('.').slice(0, 2).join('.')}`]), 'site-packages'),
nodePackages: join(dependencies, 'node', 'node_modules'),
pythonDistributions: manifest.pythonPackages ?? {},
}
}
async function exists(path: string): Promise<boolean> {
try {
if ((await lstat(path)).isSymbolicLink()) throw new Error(`primary runtime: installation path is a filesystem link: ${path}`)
return true
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
throw error
}
}
/**
* Install the application-owned payload locally, retaining a complete previous tree on copy failure.
* @param source - Payload carried by the current Desktop installation.
* @param root - Fixed primary runtime directory under the Harness home.
* @returns Paths into the installed payload; no PATH or package-manager configuration is changed.
*/
export async function installPrimaryRuntime(source: string, root: string): Promise<WorkspaceDependencies> {
const manifest = await readPrimaryRuntime(source)
if (manifest.platform !== process.platform || manifest.arch !== process.arch) throw new Error('primary runtime: incompatible platform or architecture')
await mkdir(dirname(root), { recursive: true })
const previous = `${root}.previous`
await exists(previous)
await exists(root)
if (!await exists(root) && await exists(previous)) await rename(previous, root)
if (await exists(join(root, 'runtime.json')) && JSON.stringify(await readPrimaryRuntime(root)) === JSON.stringify(manifest)) {
const paths = workspaceDependencyPaths(root, manifest)
for (const path of [paths.python, paths.node, paths.pnpm, paths.pythonPackages, paths.nodePackages]) await stat(path)
return paths
}
const staging = await mkdtemp(join(dirname(root), '.primary-runtime-'))
try {
await cp(source, staging, { recursive: true, dereference: true })
const paths = workspaceDependencyPaths(staging, manifest)
for (const path of [paths.python, paths.node, paths.pnpm, paths.pythonPackages, paths.nodePackages]) await stat(path)
await rm(previous, { recursive: true, force: true })
const replacing = await exists(root)
if (replacing) await rename(root, previous)
try { await rename(staging, root) } catch (error) {
if (replacing) await rename(previous, root)
throw error
}
await rm(previous, { recursive: true, force: true })
} finally {
await rm(staging, { recursive: true, force: true })
}
return workspaceDependencyPaths(root, manifest)
}
@@ -1,54 +0,0 @@
/** Desktop tool exposing bundled interpreters without modifying command resolution. */
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { installPrimaryRuntime, type WorkspaceDependencies } from './primary-runtime.ts'
export const name = 'desktop-workspace-dependencies'
export const inject = ['tools']
/** Application-selected payload and installation directories. */
export interface Config {
readonly source: string
readonly root: string
}
/**
* Register the read-only path query, preparing bundled files on its first invocation.
* @param ctx - Desktop tool registry owner.
* @param config - Application payload and fixed installation paths.
*/
export function apply(ctx: Context, config: Config): void {
let installation: Promise<WorkspaceDependencies> | undefined
ctx.effect(() => async () => {
// Tool execution reports installation failures; disposal only waits for filesystem work to settle.
await installation?.catch(() => undefined)
})
ctx.tools.register(defineTool({
name: 'load_workspace_dependencies',
description: 'Get absolute paths to bundled Python, Node.js, pnpm, and library directories, plus bundled Python distribution versions. Python includes numpy, pandas, python-docx, python-pptx, openpyxl, Pillow, lxml, and XlsxWriter. Use these libraries for Office files unless the user or workspace instructions select another environment. Run pnpm with the returned Node executable and pnpm script path. This does not change PATH or package-manager settings.',
parameters: {},
output: {
schema: {
type: 'object', additionalProperties: false,
properties: {
python: { type: 'string', required: true },
node: { type: 'string', required: true },
pnpm: { type: 'string', required: true },
pythonPackages: { type: 'string', required: true },
nodePackages: { type: 'string', required: true },
pythonDistributions: { type: 'object', additionalProperties: true, required: true, description: 'Bundled distribution names and versions recorded in runtime.json; excludes user-installed additions.' },
},
},
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, undefined, 2) }],
},
execute: () => {
installation ??= installPrimaryRuntime(config.source, config.root).catch((error: unknown) => {
installation = undefined
throw error
})
return installation
},
presentCall: () => ({ card: 'generic', title: 'Load workspace dependencies', kind: 'read' }),
}))
}
@@ -1,193 +0,0 @@
import { mkdir, mkdtemp, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { installPrimaryRuntime, readPrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../src/primary-runtime.ts'
import * as workspaceDependencies from '../src/workspace-dependencies.ts'
const roots: string[] = []
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))) })
async function fixture() {
const directory = await mkdtemp(join(tmpdir(), 'dsh-primary-runtime-'))
roots.push(directory)
const source = join(directory, 'resources')
const root = join(directory, 'home', 'dsh-runtimes', 'dsh-primary-runtime')
const manifest: PrimaryRuntimeManifest = {
desktopVersion: '1.0.0', platform: process.platform === 'win32' ? 'win32' : 'darwin', arch: process.arch,
components: { python: '3.12.14', node: '24.21.0', pnpm: '11.7.0', numpy: '2.3.5', pandas: '3.0.1' },
pythonPackages: { 'python-docx': '1.2.0', 'python-pptx': '1.0.2', openpyxl: '3.1.5' },
}
const paths = workspaceDependencyPaths(source, manifest)
for (const path of [paths.python, paths.node, paths.pnpm]) {
await mkdir(dirname(path), { recursive: true })
await writeFile(path, 'interpreter')
}
await mkdir(paths.pythonPackages, { recursive: true })
await mkdir(paths.nodePackages, { recursive: true })
await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
return { source, root, manifest, directory }
}
it.each(['win32', 'darwin'])('returns %s interpreter and package paths', (platform) => {
const manifest: PrimaryRuntimeManifest = { desktopVersion: '1', platform, arch: 'x64', components: { python: '3.12.14', node: '24.21.0', pnpm: '11.7.0', numpy: '2.3.5', pandas: '3.0.1' } }
const paths = workspaceDependencyPaths('/runtime', manifest)
expect(paths.pythonDistributions).toEqual({})
expect(paths.python).toBe(join('/runtime', 'dependencies', 'python', ...(platform === 'win32' ? ['python.exe'] : ['bin', 'python3'])))
expect(paths.pythonPackages).toBe(join('/runtime', 'dependencies', 'python', ...(platform === 'win32' ? ['Lib'] : ['lib', 'python3.12']), 'site-packages'))
})
it.skipIf(process.platform === 'linux')('installs offline, reuses the same release, and leaves environment and user packages unchanged', async () => {
const { source, root, manifest } = await fixture()
const environment = { ...process.env }
const installed = await installPrimaryRuntime(source, root)
await writeFile(join(installed.pythonPackages, 'user-package.py'), 'user content')
expect(await installPrimaryRuntime(source, root)).toEqual(installed)
expect(installed.pythonDistributions).toEqual(manifest.pythonPackages)
expect(await readFile(join(installed.pythonPackages, 'user-package.py'), 'utf8')).toBe('user content')
expect(process.env).toEqual(environment)
})
it.skipIf(process.platform === 'linux')('replaces release components and recovers an interrupted directory swap', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await rename(root, `${root}.previous`)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
await installPrimaryRuntime(source, root)
expect((await readPrimaryRuntime(root)).desktopVersion).toBe('2.0.0')
})
it.skipIf(process.platform === 'linux')('replaces dependencies when the locked payload changes without a Desktop version change', async () => {
const { source, root, manifest } = await fixture()
const first = { ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.1.2' } }
await writeFile(join(source, 'runtime.json'), JSON.stringify(first))
const installed = await installPrimaryRuntime(source, root)
await writeFile(join(installed.pythonPackages, 'old-package.py'), 'old dependency')
const next = { ...first, payloadDigest: 'b'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }
await writeFile(join(source, 'runtime.json'), JSON.stringify(next))
await writeFile(join(workspaceDependencyPaths(source, next).pythonPackages, 'new-package.py'), 'new dependency')
await installPrimaryRuntime(source, root)
expect(await readPrimaryRuntime(root)).toEqual(next)
expect(await readFile(join(installed.pythonPackages, 'new-package.py'), 'utf8')).toBe('new dependency')
await expect(readFile(join(installed.pythonPackages, 'old-package.py'))).rejects.toMatchObject({ code: 'ENOENT' })
})
it.skipIf(process.platform === 'linux')('upgrades a release manifest without a payload digest', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }))
await installPrimaryRuntime(source, root)
expect((await readPrimaryRuntime(root)).payloadDigest).toBe('a'.repeat(64))
})
it.skipIf(process.platform === 'linux')('replaces changed payload bytes when only the digest changes', async () => {
const { source, root, manifest } = await fixture()
const first = { ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }
const sourceFile = join(workspaceDependencyPaths(source, first).pythonPackages, 'library.py')
await writeFile(join(source, 'runtime.json'), JSON.stringify(first))
await writeFile(sourceFile, 'first wheel bytes')
const installed = await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...first, payloadDigest: 'b'.repeat(64) }))
await writeFile(sourceFile, 'repacked wheel bytes')
await installPrimaryRuntime(source, root)
expect(await readFile(join(installed.pythonPackages, 'library.py'), 'utf8')).toBe('repacked wheel bytes')
expect((await readPrimaryRuntime(root)).pythonPackages).toEqual(first.pythonPackages)
})
it.skipIf(process.platform === 'linux')('keeps the installed release when the replacement payload is incomplete', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
await rm(workspaceDependencyPaths(source, manifest).python)
await expect(installPrimaryRuntime(source, root)).rejects.toThrow()
expect((await readPrimaryRuntime(root)).desktopVersion).toBe('1.0.0')
})
it.skipIf(process.platform === 'linux')('refuses linked installation directories without modifying their targets', async () => {
const { source, root, directory } = await fixture()
const outside = join(directory, 'outside')
await mkdir(outside)
await writeFile(join(outside, 'keep'), 'untouched')
await mkdir(dirname(root), { recursive: true })
await symlink(outside, root, process.platform === 'win32' ? 'junction' : 'dir')
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('filesystem link')
expect(await readFile(join(outside, 'keep'), 'utf8')).toBe('untouched')
})
it('rejects malformed metadata and incompatible targets', async () => {
const { source, root, manifest } = await fixture()
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, arch: process.arch === 'x64' ? 'arm64' : 'x64' }))
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('incompatible')
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, components: { ...manifest.components, python: '../escape' } }))
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
})
it.each([['numpy', 'numpy'], ['pandas', 'pandas'], ['Numpy', 'numpy'], ['PANDAS', 'pandas']] as const)('rejects conflicting %s component and distribution versions', async (distribution, name) => {
const { source, manifest } = await fixture()
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, pythonPackages: { [distribution]: '0.0.1' } }))
await expect(readPrimaryRuntime(source)).rejects.toThrow(`conflicting ${name} distribution version`)
const consistent = { ...manifest, pythonPackages: { [distribution]: manifest.components[name] } }
await writeFile(join(source, 'runtime.json'), JSON.stringify(consistent))
expect(await readPrimaryRuntime(source)).toEqual(consistent)
})
it.each([
{ payloadDigest: 'invalid' },
{ pythonPackages: ['python-docx'] },
{ pythonPackages: { 'python-docx': '../escape' } },
{ pythonPackages: { '../escape': '1.2.0' } },
{ pythonPackages: { numpy: '2.3.5', Numpy: '2.3.5' } },
{ pythonPackages: { Pillow: '12.3.0', pillow: '12.3.0' } },
{ pythonPackages: { typing_extensions: '4.16.0', 'typing.extensions': '4.16.0' } },
])('rejects invalid locked payload metadata: %j', async (invalid) => {
const { source, manifest } = await fixture()
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, ...invalid }))
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
})
it.skipIf(process.platform === 'linux')('loads the real tool through Cordis, exposes installed paths, and unregisters on disposal', async () => {
const { source, root, manifest, directory } = await fixture()
const ctx = new Context()
try {
ctx.baseUrl = pathToFileURL(directory).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
const modules = new Map<string, unknown>([
['agents', AgentRegistry], ['systemPrompt', SystemPrompt], ['tools', ToolRuntime], ['dependencies', workspaceDependencies],
])
ctx.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (!modules.has(specifier)) throw new Error(`unexpected plugin ${specifier}`)
return modules.get(specifier)
},
} as unknown as NonNullable<typeof ctx.loader.internal>
const config = join(directory, 'cordis.yml')
await writeFile(config, `- name: agents\n- name: systemPrompt\n- name: tools\n- name: dependencies\n config:\n source: ${JSON.stringify(source)}\n root: ${JSON.stringify(root)}\n`)
await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(config).href } })
await ctx.loader.await()
for (const entry of ctx.loader.entries()) await entry.fiber?.await()
const environment = { ...process.env }
const request = { signal: new AbortController().signal, name: 'load_workspace_dependencies', arguments: {} }
const results = await Promise.all(['first', 'second'].map(id => ctx.tools.execute({ ...request, callId: ToolCallId(id) })))
for (const result of results) {
expect(result.isError).toBe(false)
expect(result.content).toEqual([{ type: 'text', text: JSON.stringify(workspaceDependencyPaths(root, manifest), undefined, 2) }])
}
expect(process.env).toEqual(environment)
const entry = [...ctx.loader.entries()].find(entry => entry.options.name === 'dependencies')
expect(entry).toBeDefined()
await entry?.fiber?.dispose()
expect(ctx.tools.schemas().some(tool => tool.name === 'load_workspace_dependencies')).toBe(false)
} finally {
await ctx.fiber.dispose()
}
})
+1 -1
View File
@@ -13,7 +13,7 @@
{ "path": "../../packages/jobs/jobs" },
{ "path": "../../packages/skill/skill-office" },
{ "path": "../../packages/util/home-paths" },
{ "path": "../../packages/core/tools" },
{ "path": "../../packages/skill/tool-workspace-dependencies" },
{
"path": "../cli"
},
+2 -2
View File
@@ -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 apps/desktop/README.md
README.md: aa2d9163067ea48b398e200b644fbeda6ea370a9
README.zh.md: a27b551b4fc2ea37d5a2da45edbcf4b2901e5dde
README.md: 94d09c628d6bb40933f95cd3874d412ab18521df
README.zh.md: bfb156bfaa0400799498eabb6034f32c2d50e7f3
+2 -2
View File
@@ -24,11 +24,11 @@ Desktop carries independent Python, Node.js and pnpm distributions. Python inclu
Desktop registers `office-docx`, `office-pptx`, and `office-xlsx` by default. The skills use the bundled Python libraries for creation and focused edits, then reopen the files and run a shared structural checker before delivery. PowerPoint creation and editing use python-pptx. Skill resources are copied to `runtime/office-skills` outside ASAR so Python can read the checker. An available `render_document` tool can add visual inspection; its absence does not prevent authoring or delivery. See the [Office skill package](../../packages/skill/skill-office/README.md) for checks and limitations.
The payload follows the Desktop release. `runtime.json` records the Desktop version, target, component and Python distribution versions, and a digest of the selected target’s locked payload inputs and assembly format. Distribution names use PEP 503 normalization; duplicate normalized names and conflicting numpy/pandas component and distribution versions reject the manifest. Matching installations are reused; a dependency or archive change replaces the directory after a complete staged copy even when the Desktop version stays unchanged. Older manifests without a digest are replaced on their next installation. User-added Python packages remain only while the payload identity matches. A failed directory replacement retains the previous installation; Windows may refuse replacement while an interpreter is still running.
The payload follows the Desktop release. `runtime.json` records the Desktop version, target, top-level interpreter/package-manager versions and the Python distribution map, and a digest of the selected target’s locked payload inputs and assembly format. Distribution names use PEP 503 normalization; duplicate normalized names reject the manifest. Legacy `components` manifests remain readable through normalization, with their existing library-version consistency checks. Matching installations are reused; a dependency or archive change replaces the directory after a complete staged copy even when the Desktop version stays unchanged. Older manifests without a digest are replaced on their next installation. User-added Python packages remain only while the payload identity matches. A failed directory replacement retains the previous installation; Windows may refuse replacement while an interpreter is still running.
The private Desktop `runtime/bin` directory is added only to package-installation processes, not the Host PATH inherited by PTC and agent shells. This tool does not change PATH, environment variables or user package-manager configuration. pnpm retains its own defaults and user settings for global packages, executable entries and its store, including native errors when the environment does not support global installation. There is no separate dependency updater. [The primary-runtime decision](../../.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md) records these choices.
Node prepares the bundled interpreters and Python libraries without a system Python or pip. [The download lock](scripts/primary-runtime-lock.json) pins interpreter archives, Python distribution versions and target-specific wheel URLs and hashes; pnpm follows the Desktop build dependency lock. Wheel filenames and distribution versions must agree for every target. Preserve key order within the selected target, wheel records and distribution map, plus wheel-entry order; these affect payload identity, while top-level lock key order does not. Library wheels unpack into site-packages, retaining auxiliary files under each wheel's `.data/scripts` directory without generating command wrappers. Other installation schemes are rejected. Native-target checks verify the locked wheel set and versions, permit the interpreter's bundled pip, and check the Python version, Office document read/write operations and dependency completeness without writing bytecode after staging cleanup and again after macOS signing. The standalone Node executable receives the JIT entitlement required by V8. Cross-target execution and signed installation require the target release host. Both `dev:desktop` and `start:desktop` prepare `.desktop-build/targets/<target>/runtime/primary-runtime` before launching Electron; first use may download locked dependencies. An unfinished preparation cannot report a successful launcher exit.
Node prepares the bundled interpreters and Python libraries without a system Python or pip. [The download lock](../../scripts/primary-runtime/lock.json) pins interpreter archives, Python distribution versions and target-specific wheel URLs and hashes; the shared builder resolves pnpm from the root development dependency pin. The root package-manager version and the Desktop pin are checked for agreement. Wheel filenames and distribution versions must agree for every target. Preserve key order within the selected target, wheel records and distribution map, plus wheel-entry order; these affect payload identity, while top-level lock key order does not. Library wheels unpack into site-packages, retaining auxiliary files under each wheel's `.data/scripts` directory without generating command wrappers. Other installation schemes are rejected. Native-target checks verify the locked wheel set and versions, permit the interpreter's bundled pip, and check the Python version, Office document read/write operations and dependency completeness without writing bytecode after staging cleanup and again after macOS signing. The standalone Node executable receives the JIT entitlement required by V8. Cross-target execution and signed installation require the target release host. Both `dev:desktop` and `start:desktop` prepare `.desktop-build/targets/<target>/runtime/primary-runtime` before launching Electron; first use may download locked dependencies. An unfinished preparation cannot report a successful launcher exit.
| Decision | Why | Direct consequence |
|---|---|---|
+2 -2
View File
@@ -24,11 +24,11 @@ Desktop 携带独立的 Python、Node.js 和 pnpm 分发包。Python 包含 nump
Desktop 默认注册 `office-docx`、`office-pptx` 和 `office-xlsx`。这些技能使用内置 Python 库创建文件和进行定点编辑,随后重新打开文件,并在交付前运行共享结构检查器。PowerPoint 的创建和编辑使用 python-pptx。技能资源复制到 ASAR 外的 `runtime/office-skills`,让 Python 可以读取检查器。可用的 `render_document` 工具可以补充视觉检查;缺少该工具不妨碍创作或交付。检查范围与限制见 [Office 技能包](../../packages/skill/skill-office/README.zh.md)。
该产物随 Desktop 版本发布。`runtime.json` 记录 Desktop 版本、目标平台、组件与 Python 分发包版本,以及所选目标的锁定产物输入与组装格式的摘要。分发包名称按 PEP 503 归一化;名称归一化后重复,或 numpy/pandas 的组件版本与分发包版本冲突时,清单会被拒绝。匹配的安装会被复用;依赖或压缩包变化后,即使 Desktop 版本不变,也会在完整暂存副本完成后替换目录。不含摘要的旧清单会在下次安装时被替换。用户自行添加的 Python 包仅在产物身份一致时保留。目录替换失败时保留之前的安装;解释器仍在运行时,Windows 可能拒绝替换。
该产物随 Desktop 版本发布。`runtime.json` 记录 Desktop 版本、目标平台、顶层解释器和包管理器版本及 Python 分发包版本表,以及所选目标的锁定产物输入与组装格式的摘要。分发包名称按 PEP 503 归一化;名称归一化后重复时,清单会被拒绝。旧 `components` 清单通过归一化继续可读,并保留其原有库版本一致性校验。匹配的安装会被复用;依赖或压缩包变化后,即使 Desktop 版本不变,也会在完整暂存副本完成后替换目录。不含摘要的旧清单会在下次安装时被替换。用户自行添加的 Python 包仅在产物身份一致时保留。目录替换失败时保留之前的安装;解释器仍在运行时,Windows 可能拒绝替换。
Desktop 私有的 `runtime/bin` 目录仅添加到包安装进程,不进入 PTC 和 agent shell 从 Host 继承的 PATH。该工具不修改 PATH、环境变量或用户包管理器配置。pnpm 的全局包、命令入口和 store 保留自身默认值及用户设置,包括环境不支持全局安装时的原生错误。不提供独立依赖更新器。[第一方 Runtime 决策](../../.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.zh.md)记录这些选择。
Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载锁](scripts/primary-runtime-lock.json)固定解释器压缩包、Python 分发包版本及目标平台 wheel 的 URL 和哈希;pnpm 使用 Desktop 构建依赖锁。每个目标的 wheel 文件名必须与分发包版本一致。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序都会影响产物身份,编辑时须保留;锁文件顶层键的顺序不影响该身份。库 wheel 解压到 site-packages,各 wheel 的 `.data/scripts` 目录保留辅助文件,不生成命令行包装器。其他安装方案会被拒绝。本机目标检查在清理暂存目录后以及 macOS 签名后验证锁定 wheel 的集合与版本,允许解释器自带的 pip,并检查 Python 版本、Office 文档读写和依赖完整性,不写入字节码。独立 Node 可执行文件获得 V8 所需的 JIT 权限。跨目标执行和签名安装需要对应的发布主机。`dev:desktop` 和 `start:desktop` 都会在启动 Electron 前准备 `.desktop-build/targets/<target>/runtime/primary-runtime`;首次准备可能需要下载锁定的依赖。准备未完成时,启动命令不能报告成功退出。
Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载锁](../../scripts/primary-runtime/lock.json)固定解释器压缩包、Python 分发包版本及目标平台 wheel 的 URL 和哈希;共享构建器从根开发依赖中解析 pnpm 固定版本。测试检查根 package-manager 版本和 Desktop 固定版本保持一致。每个目标的 wheel 文件名必须与分发包版本一致。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序都会影响产物身份,编辑时须保留;锁文件顶层键的顺序不影响该身份。库 wheel 解压到 site-packages,各 wheel 的 `.data/scripts` 目录保留辅助文件,不生成命令行包装器。其他安装方案会被拒绝。本机目标检查在清理暂存目录后以及 macOS 签名后验证锁定 wheel 的集合与版本,允许解释器自带的 pip,并检查 Python 版本、Office 文档读写和依赖完整性,不写入字节码。独立 Node 可执行文件获得 V8 所需的 JIT 权限。跨目标执行和签名安装需要对应的发布主机。`dev:desktop` 和 `start:desktop` 都会在启动 Electron 前准备 `.desktop-build/targets/<target>/runtime/primary-runtime`;首次准备可能需要下载锁定的依赖。准备未完成时,启动命令不能报告成功退出。
| 决策 | 原因 | 直接结果 |
|---|---|---|
-2
View File
@@ -53,11 +53,9 @@
"electron": "^44.0.0",
"electron-builder": "^26.15.3",
"extract-zip": "^2.0.1",
"fflate": "^0.8.2",
"js-yaml": "^4.2.0",
"koffi": "^3.1.0",
"pnpm": "11.7.0",
"tar": "^7.5.0",
"typescript": "^6.0.3"
}
}
+14 -150
View File
@@ -1,171 +1,35 @@
/** Prepare pinned, relocatable script interpreters without installing into the build host. */
/** Desktop resource locations and signing-aware verification for the shared runtime builder. */
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { cp } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import extractZip from 'extract-zip'
import { x as extractTar } from 'tar'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../desktop-host/src/primary-runtime.ts'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { preparePrimaryRuntime as preparePayload, smokePrimaryRuntime as smokePayload } from '../../../scripts/primary-runtime/prepare.ts'
import { parsePrimaryRuntime, workspaceDependencyPaths } from '../../../packages/skill/tool-workspace-dependencies/src/index.ts'
import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
import { scrubWindowsSigningEnvironment } from './windows-sign.mjs'
import lock from './primary-runtime-lock.json' with { type: 'json' }
/**
* Download or reuse an archive only when its bytes match the release lock.
* @param url - Locked archive URL.
* @param sha256 - Expected SHA-256 digest.
* @param cache - Download cache directory.
* @returns Verified local archive path.
*/
export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string): Promise<string> {
const destination = join(cache, sha256)
let bytes: Buffer
try { bytes = readFileSync(destination) } catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
const response = await fetch(url)
if (!response.ok) throw new Error(`primary runtime download: ${String(response.status)} ${url}`)
bytes = Buffer.from(await response.arrayBuffer())
}
if (createHash('sha256').update(bytes).digest('hex') !== sha256) throw new Error(`primary runtime download: checksum mismatch for ${url}`)
writeFileSync(destination, bytes)
return destination
}
async function pythonArchive(target: keyof typeof lock.targets, cache: string): Promise<string> {
const artifact = lock.targets[target]
const filename = `cpython-${lock.pythonVersion}+${lock.pythonRelease}-${artifact.pythonTarget}-install_only_stripped.tar.gz`
return downloadPrimaryRuntimeAsset(`https://github.com/astral-sh/python-build-standalone/releases/download/${lock.pythonRelease}/${encodeURIComponent(filename)}`, artifact.pythonSha256, cache)
}
/**
* Identify the inputs that assemble one target's payload, excluding unrelated target locks.
* @param target - Desktop target whose archives are installed.
* @param runtimeLock - Locked interpreter and wheel inputs.
* @param pnpmVersion - Package-manager version copied into the payload.
* @returns SHA-256 payload identity for installation reuse.
*/
export function primaryRuntimePayloadDigest(target: keyof typeof lock.targets, runtimeLock: typeof lock, pnpmVersion: string): string {
const { pythonVersion, pythonRelease, nodeVersion, wheels, pythonPackages } = runtimeLock
// Identity preserves key order within the selected target, wheel records and distribution map, plus wheel-entry order.
// Bump format when extraction or assembly changes payload bytes without changing locked inputs.
return createHash('sha256').update(JSON.stringify({
format: 2, target, pythonVersion, pythonRelease, nodeVersion,
artifact: runtimeLock.targets[target], wheels, pythonPackages, pnpm: pnpmVersion,
})).digest('hex')
}
/**
* Unpack a locked library wheel, retaining auxiliary scripts in its distribution data directory.
* @param archive - Hash-verified wheel archive.
* @param destination - Absolute site-packages directory.
* @returns Resolves after extraction without command wrappers; rejects other wheel installation schemes.
*/
export async function unpackPrimaryRuntimeWheel(archive: string, destination: string): Promise<void> {
await extractZip(archive, {
dir: destination,
onEntry: (entry) => {
const [directory, scheme] = entry.fileName.split('/')
if (directory?.endsWith('.data') && scheme !== '' && scheme !== 'scripts') {
throw new Error(`primary runtime: wheel requires unsupported installation paths: ${entry.fileName}`)
}
},
})
}
/**
* Copy the skill package's complete asset tree to ordinary filesystem resources.
* @param source - The package's assets directory.
* @param destination - Desktop runtime resource directory outside ASAR.
* @returns Resolves after replacing the external assets with the complete package tree.
*/
export async function prepareOfficeSkillAssets(source: string, destination: string): Promise<void> {
rmSync(destination, { recursive: true, force: true })
await cp(source, destination, { recursive: true, dereference: true })
}
/**
* Materialize the selected Desktop target's primary runtime in its build resources.
* Prepare Desktop resources for its selected packaging target.
* @param options - Signed Windows packaging defers execution until its supervised signing stage.
* @returns Resolves after materialization and, unless deferred, native-target execution checks.
* @returns Resolves after preparation and, unless deferred, native-target execution checks.
*/
export async function preparePrimaryRuntime(options: { deferSmoke?: boolean } = {}): Promise<void> {
const target = resolveDesktopBuildTarget()
const paths = resolveDesktopTargetBuildPaths()
const artifact = lock.targets[target]
mkdirSync(paths.runtime, { recursive: true })
mkdirSync(paths.downloads, { recursive: true })
const staging = mkdtempSync(join(tmpdir(), 'dsh-primary-'))
try {
const output = join(staging, 'payload')
const dependencies = join(output, 'dependencies')
mkdirSync(dependencies, { recursive: true })
const nodeFilename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}`
const nodeArchive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${nodeFilename}`, artifact.nodeSha256, paths.downloads)
const unpackedNode = join(staging, 'node')
mkdirSync(unpackedNode)
if (target === 'win-x64') await extractZip(nodeArchive, { dir: unpackedNode })
else await extractTar({ file: nodeArchive, cwd: unpackedNode })
const nodeSource = join(unpackedNode, nodeFilename.replace(/\.(?:zip|tar\.gz)$/u, ''))
mkdirSync(join(dependencies, 'node', 'bin'), { recursive: true })
mkdirSync(join(dependencies, 'node', 'node_modules'))
writeFileSync(join(dependencies, 'node', 'node_modules', 'README.txt'), 'Reserved for bundled Node packages. pnpm uses its default installation directories.\n')
cpSync(join(nodeSource, ...(target === 'win-x64' ? ['node.exe'] : ['bin', 'node'])),
join(dependencies, 'node', 'bin', target === 'win-x64' ? 'node.exe' : 'node'))
cpSync(join(nodeSource, 'LICENSE'), join(dependencies, 'node', 'LICENSE'))
await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies })
const require = createRequire(import.meta.url)
const pnpmManifest = require.resolve('pnpm')
const pnpm = JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
const desktop = JSON.parse(readFileSync(join(import.meta.dirname, '..', 'package.json'), 'utf8')) as { version: string }
const manifest: PrimaryRuntimeManifest = {
desktopVersion: desktop.version,
platform: target === 'win-x64' ? 'win32' : 'darwin',
arch: target === 'mac-arm64' ? 'arm64' : 'x64',
payloadDigest: primaryRuntimePayloadDigest(target, lock, pnpm.version),
pythonPackages: lock.pythonPackages,
components: {
python: lock.pythonVersion, node: lock.nodeVersion, pnpm: pnpm.version,
numpy: lock.pythonPackages.numpy, pandas: lock.pythonPackages.pandas,
},
}
const entries = workspaceDependencyPaths(output, manifest)
for (const wheel of [...artifact.wheels, ...lock.wheels]) {
await unpackPrimaryRuntimeWheel(await downloadPrimaryRuntimeAsset(wheel.url, wheel.sha256, paths.downloads), entries.pythonPackages)
}
writeFileSync(join(output, 'runtime.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
const destination = join(paths.runtime, 'primary-runtime')
rmSync(destination, { recursive: true, force: true })
await cp(output, destination, { recursive: true, dereference: true })
} finally {
rmSync(staging, { recursive: true, force: true })
}
const hostRequire = createRequire(resolve(import.meta.dirname, '..', '..', 'desktop-host', 'package.json'))
await prepareOfficeSkillAssets(join(dirname(hostRequire.resolve('@deepseek-ai/dsh-skill-office/package.json')), 'assets'),
join(paths.runtime, 'office-skills'))
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }
await preparePayload({ target: resolveDesktopBuildTarget(), output: paths.runtime, cache: paths.downloads, version })
if (!options.deferSmoke) smokePrimaryRuntime(join(paths.runtime, 'primary-runtime'))
}
/**
* Execute the native payload's interpreters, package manager and Python libraries.
* Verify Desktop's complete payload after preparation or platform signing.
* @param root - Final payload directory, including any platform signatures.
*/
export function smokePrimaryRuntime(root: string): void {
const manifest = JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')) as PrimaryRuntimeManifest
const manifest = parsePrimaryRuntime(JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')))
if (manifest.platform !== process.platform || manifest.arch !== process.arch) return
if (manifest.pythonPackages === undefined) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
if (Object.keys(manifest.pythonPackages).length === 0) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
const entries = workspaceDependencyPaths(root, manifest)
const options = { stdio: 'inherit', timeout: 120_000, env: scrubWindowsSigningEnvironment(process.env) } as const
execFileSync(entries.python, ['-I', '-c', 'import decimal, xml.parsers.expat, lzma, uuid, numpy, pandas; assert numpy.arange(4).sum() == 6; assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3'], options)
execFileSync(entries.python, ['-I', '-B', join(import.meta.dirname, 'smoke-primary-runtime.py'), JSON.stringify(manifest.pythonPackages),
manifest.components.python, join(dirname(root), 'office-skills', 'scripts', 'check_office.py')], options)
execFileSync(entries.python, ['-I', '-B', '-m', 'pip', 'check'], options)
execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(manifest.components.node)}) process.exit(1)`], options)
execFileSync(entries.node, [entries.pnpm, '--version'], options)
if (entries.node === undefined || entries.pnpm === undefined) throw new Error('primary runtime: the Desktop payload must declare node and pnpm components.')
smokePayload(root, scrubWindowsSigningEnvironment(process.env))
}
if (import.meta.main) await preparePrimaryRuntime()
@@ -1,131 +1,18 @@
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
/** Desktop carriers keep a complete interpreter payload even when SDK carriers choose Python-only. */
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { zipSync } from 'fflate'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import { downloadPrimaryRuntimeAsset, prepareOfficeSkillAssets, primaryRuntimePayloadDigest, smokePrimaryRuntime, unpackPrimaryRuntimeWheel } from '../scripts/prepare-primary-runtime.ts'
import lock from '../scripts/primary-runtime-lock.json' with { type: 'json' }
import { smokePrimaryRuntime } from '../scripts/prepare-primary-runtime.ts'
const libraryWheel = Buffer.from('UEsDBAoAAAAAAASeLl0sYMPjDAAAAAwAAAAJAAAAc2FtcGxlLnB5c2FtcGxlID0gNDIKUEsBAh4DCgAAAAAABJ4uXSxgw+MMAAAADAAAAAkAAAAAAAAAAQAAAKSBAAAAAHNhbXBsZS5weVBLBQYAAAAAAQABADcAAAAzAAAAAAA=', 'base64')
const relocatedWheel = Buffer.from('UEsDBAoAAAAAAASeLl3x0Nj9FAAAABQAAAAeAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlcmVxdWlyZXMgcmVsb2NhdGlvbgpQSwECHgMKAAAAAAAEni5d8dDY/RQAAAAUAAAAHgAAAAAAAAABAAAApIEAAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlUEsFBgAAAAABAAEATAAAAFAAAAAAAA==', 'base64')
const externalLibraryWheel = Buffer.from('UEsDBBQAAAAAAAAAIVyBOE8OHAAAABwAAAAhAAAAc2FtcGxlLTEuMC5kYXRhL3B1cmVsaWIvc2FtcGxlLnB5cmVxdWlyZXMgbGlicmFyeSByZWxvY2F0aW9uClBLAQIUAxQAAAAAAAAAIVyBOE8OHAAAABwAAAAhAAAAAAAAAAAAAACAAQAAAABzYW1wbGUtMS4wLmRhdGEvcHVyZWxpYi9zYW1wbGUucHlQSwUGAAAAAAEAAQBPAAAAWwAAAAAA', 'base64')
it.each(Object.entries(lock.targets))('records every locked wheel distribution and version for %s', (_target, artifact) => {
const normalize = (name: string): string => name.toLowerCase().replace(/[-_.]+/gu, '-')
const distributions = [...artifact.wheels, ...lock.wheels].map(({ url }) => {
const [name, version] = basename(new URL(url).pathname).split('-')
return [normalize(name!), version] as const
})
const declared = Object.entries(lock.pythonPackages).map(([name, version]) => [normalize(name), version] as const)
expect(new Set(distributions.map(([name]) => name)).size).toBe(distributions.length)
expect(new Set(declared.map(([name]) => name)).size).toBe(declared.length)
expect(Object.fromEntries(distributions)).toEqual(Object.fromEntries(declared))
})
it('keeps a target payload identity independent of other target archives', () => {
const changed = structuredClone(lock)
changed.targets['win-x64'].wheels[0]!.sha256 = 'a'.repeat(64)
expect(primaryRuntimePayloadDigest('mac-arm64', changed, '11.7.0')).toBe(primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.0'))
expect(primaryRuntimePayloadDigest('win-x64', changed, '11.7.0')).not.toBe(primaryRuntimePayloadDigest('win-x64', lock, '11.7.0'))
})
it('invalidates payload identity for shared wheels, package versions and package-manager changes', () => {
const wheel = structuredClone(lock), distribution = structuredClone(lock)
wheel.wheels[0]!.sha256 = 'a'.repeat(64)
distribution.pythonPackages['python-docx'] = '1.2.1'
const original = primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.0')
expect(primaryRuntimePayloadDigest('mac-arm64', wheel, '11.7.0')).not.toBe(original)
expect(primaryRuntimePayloadDigest('mac-arm64', distribution, '11.7.0')).not.toBe(original)
expect(primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.1')).not.toBe(original)
})
it('reports missing distribution metadata before trying to execute a stale native payload', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-stale-runtime-'))
it('rejects a Python-only native Desktop payload before executing interpreters', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-python-only-'))
try {
await writeFile(join(root, 'runtime.json'), JSON.stringify({ platform: process.platform, arch: process.arch }))
expect(() => { smokePrimaryRuntime(root) }).toThrow('missing Python distribution versions; prepare the payload')
} finally { await rm(root, { recursive: true, force: true }) }
})
it('extracts a hash-verified cached library without a Python installer or network request', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const hash = createHash('sha256').update(libraryWheel).digest('hex')
const archive = join(root, hash)
await writeFile(archive, libraryWheel)
expect(await downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).toBe(archive)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample.py'), 'utf8')).toBe('sample = 42\n')
await writeFile(archive, 'corrupt archive')
await expect(downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).rejects.toThrow('checksum mismatch')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('fully extracts a large deflate-compressed wheel entry', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-large-wheel-'))
try {
const expected = Buffer.alloc(1024 * 1024)
// Seeded xorshift bytes keep the compressed entry larger than the stream buffers.
let state = 26
for (let index = 0; index < expected.length; index++) {
state ^= state << 13
state ^= state >>> 17
state ^= state << 5
expected[index] = state & 255
}
const archive = join(root, 'large.whl')
await writeFile(archive, zipSync({ 'large.bin': expected }, { level: 6 }))
await unpackPrimaryRuntimeWheel(archive, root)
expect(await readFile(join(root, 'large.bin'))).toEqual(expected)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('retains auxiliary wheel scripts without generating command wrappers', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const archive = join(root, 'relocated.whl')
await writeFile(archive, relocatedWheel)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample-1.0.data/scripts/sample'), 'utf8')).toBe('requires relocation\n')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('rejects library files requiring an unsupported installation scheme', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const archive = join(root, 'relocated.whl')
await writeFile(archive, externalLibraryWheel)
await expect(unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))).rejects.toThrow('unsupported installation paths')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('copies complete Office resources outside the application archive and removes obsolete assets', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-office-assets-'))
try {
const source = join(root, 'package', 'assets')
const destination = join(root, 'Contents', 'Resources', 'runtime', 'office-skills')
await mkdir(join(source, 'scripts'), { recursive: true })
await writeFile(join(source, 'scripts', 'check_office.py'), 'print("checker")\n')
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
await mkdir(join(source, name))
await writeFile(join(source, name, 'SKILL.md'), `# ${name}\n`)
}
await prepareOfficeSkillAssets(source, destination)
await writeFile(join(destination, 'obsolete.py'), 'old helper')
await prepareOfficeSkillAssets(source, destination)
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
expect(await readFile(join(destination, name, 'SKILL.md'), 'utf8')).toBe(`# ${name}\n`)
}
expect(await readFile(join(destination, 'scripts', 'check_office.py'), 'utf8')).toBe('print("checker")\n')
await expect(readFile(join(destination, 'obsolete.py'))).rejects.toMatchObject({ code: 'ENOENT' })
await writeFile(join(root, 'runtime.json'), JSON.stringify({
desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
python: '3.12.14', pythonPackages: { numpy: '2.3.5', pandas: '3.0.1' },
}))
expect(() => { smokePrimaryRuntime(root) }).toThrow('Desktop payload must declare node and pnpm')
} finally { await rm(root, { recursive: true, force: true }) }
})
+10 -1
View File
@@ -227,14 +227,19 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
document.body.append(probe)
const actual = getComputedStyle(element)
const reference = getComputedStyle(probe)
// The sibling access-mode trigger: the chip shares its corner
// curvature (the theme's superellipse, not a circular capsule).
const accessMode = document.querySelector('button[aria-label^="Access mode"]')
const result = {
color: actual.color,
backgroundColor: actual.backgroundColor,
height: actual.height,
borderRadius: actual.borderRadius,
cornerShape: actual.getPropertyValue('corner-shape'),
fontSize: actual.fontSize,
referenceColor: reference.color,
referenceBackgroundColor: reference.backgroundColor,
siblingCornerShape: accessMode === null ? null : getComputedStyle(accessMode).getPropertyValue('corner-shape'),
}
probe.remove()
return result
@@ -242,7 +247,11 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
expect(planStyle.color).toBe(planStyle.referenceColor)
expect(planStyle.backgroundColor).toBe(planStyle.referenceBackgroundColor)
expect(planStyle.height).toBe('28px')
expect(planStyle.borderRadius).toBe('999px')
// Half the 28px height, the compact Button geometry, under the theme's
// corner curvature; a 999px pill would need the circular opt-out.
expect(planStyle.borderRadius).toBe('14px')
expect(planStyle.siblingCornerShape).not.toBeNull()
expect(planStyle.cornerShape).toBe(planStyle.siblingCornerShape)
expect(planStyle.fontSize).toBe('13px')
await planButton.click()
await expect.poll(() => planButton.count()).toBe(0)
+46 -1
View File
@@ -10,7 +10,7 @@
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import type { Browser, ConsoleMessage, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
@@ -211,6 +211,51 @@ describe('web e2e: plan review takeover round trip', () => {
}
}, 60_000)
it.skipIf(MODE === 'record')('opens a review that arrives while a global panel is active', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-plan-review-panel-return'))
const agent = scaffold.ctx.agents.get(reviewedSession)
if (agent === undefined) throw new Error('The reviewed Session has no active agent')
// The Plugins panel replaces the Conversation and its right Sidebar. The
// review arrives while neither is mounted; returning mounts both in one
// commit, and the review's automatic open runs before the Sidebar's own
// effects. A crash there retires the opener for the rest of the page.
await page.getByRole('button', { name: 'Plugins', exact: true }).click()
await expect.poll(() => page.locator('[data-composer-input]').count()).toBe(0)
const crashes: string[] = []
const onConsole = (message: ConsoleMessage): void => {
if (message.type() === 'error' && /slot entry crashed/i.test(message.text())) crashes.push(message.text())
}
page.on('console', onConsole)
const controller = new AbortController()
const asked = scaffold.ctx.userQuestions.ask({
agent, signal: controller.signal,
questions: [{ id: 'off-screen', question: 'Approve this plan?',
detail: '# Off-screen review\n\nSubmitted while the Plugins panel was open.',
options: [{ label: 'Approve' }, { label: 'Keep planning' }], intent: { kind: 'plan-review', approve: 'Approve' },
}],
})
const outcome = asked.then(value => value, (error: unknown) => ({ error }))
try {
const row = page.locator('[role="treeitem"]').filter({ has: page.locator('[data-state="warning"]') }).first()
await row.waitFor({ timeout: 10_000 })
await row.click()
const card = page.locator('[data-plan-review-key]')
await card.waitFor({ timeout: 10_000 })
const preview = page.locator('[data-plan-preview^="dsh-resource://plan-review/"]')
await preview.waitFor({ state: 'visible', timeout: 10_000 })
expect(await preview.getByText('Submitted while the Plugins panel was open.').isVisible()).toBe(true)
expect(await card.getByRole('button', { name: 'Open plan in sidebar' }).count()).toBe(1)
expect(await page.locator('[data-slot-error]').count()).toBe(0)
expect(crashes).toEqual([])
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
} finally {
page.off('console', onConsole)
controller.abort()
await outcome
}
}, 60_000)
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
'session.v3.jsonl', 'review.expected.md', 'sidebar.expected.md', 'preview.expected.md',
+2 -2
View File
@@ -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 docs/config-catalog.md
config-catalog.md: 4c89900a886e06f58d581af305bfa080b2d92d6a
config-catalog.zh.md: ff41610c361e1e1265f3201f8d62842195e7bf98
config-catalog.md: 862bb8a03e140ce61c904537b6d0b2d809ce8ae5
config-catalog.zh.md: 3fec8404bd853dae26b389f87376770b81ec6e6a
+22
View File
@@ -3455,6 +3455,28 @@ export interface Config {
Source: [`packages/workflow/tool-workflow/src/index.ts:32`](../packages/workflow/tool-workflow/src/index.ts)
<a id="deepseek-aidsh-tool-workspace-dependencies"></a>
## `@deepseek-ai/dsh-tool-workspace-dependencies`
Requires: `tools`
```ts config-catalog
/** Payload location and optional installation directory. */
export interface Config {
/** Payload directory carrying `runtime.json` and `dependencies/`. */
readonly source: string
/**
* Installation directory under the Harness home. When set, the payload is copied there on the
* first call (the Desktop behavior); when omitted, the payload is used in place without copying,
* which suits read-only carriers such as container image layers.
*/
readonly root?: string
}
```
Source: [`packages/skill/tool-workspace-dependencies/src/index.ts:15`](../packages/skill/tool-workspace-dependencies/src/index.ts)
<a id="deepseek-aidsh-tools"></a>
## `@deepseek-ai/dsh-tools`
+22
View File
@@ -3457,6 +3457,28 @@ export interface Config {
来源:[`packages/workflow/tool-workflow/src/index.ts:32`](../packages/workflow/tool-workflow/src/index.ts)
<a id="deepseek-aidsh-tool-workspace-dependencies"></a>
## `@deepseek-ai/dsh-tool-workspace-dependencies`
依赖:`tools`
```ts config-catalog
/** Payload location and optional installation directory. */
export interface Config {
/** Payload directory carrying `runtime.json` and `dependencies/`. */
readonly source: string
/**
* Installation directory under the Harness home. When set, the payload is copied there on the
* first call (the Desktop behavior); when omitted, the payload is used in place without copying,
* which suits read-only carriers such as container image layers.
*/
readonly root?: string
}
```
来源:[`packages/skill/tool-workspace-dependencies/src/index.ts:15`](../packages/skill/tool-workspace-dependencies/src/index.ts)
<a id="deepseek-aidsh-tools"></a>
## `@deepseek-ai/dsh-tools`
+2 -2
View File
@@ -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 docs/module-graph.md
module-graph.md: 1b013cd5d96c133ed3ffcd419d2cdebe653187e9
module-graph.zh.md: d8b981f500fce55c29a5ab99df1cb493064f3365
module-graph.md: e4ac3c5b8769b9ccbd7b7ebe0d2addf597f8ef1d
module-graph.zh.md: 0d9ee982c4ecf07b4f5d4ec70591c19b93d37184
+3
View File
@@ -65,6 +65,7 @@ flowchart TD
pkg_skill_filesystem["skill-filesystem"]
pkg_skill_office["skill-office"]
pkg_tool_skill["tool-skill"]
pkg_tool_workspace_dependencies["tool-workspace-dependencies"]
end
subgraph group_subagent["packages/subagent"]
pkg_subagent["subagent"]
@@ -775,6 +776,7 @@ flowchart TD
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
pkg_tool_skill --> pkg_tools
pkg_tool_workspace_dependencies --> pkg_tools
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
pkg_tool_web --> pkg_tools
@@ -1499,6 +1501,7 @@ flowchart TD
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
+3
View File
@@ -67,6 +67,7 @@ flowchart TD
pkg_skill_filesystem["skill-filesystem"]
pkg_skill_office["skill-office"]
pkg_tool_skill["tool-skill"]
pkg_tool_workspace_dependencies["tool-workspace-dependencies"]
end
subgraph group_subagent["packages/subagent"]
pkg_subagent["subagent"]
@@ -777,6 +778,7 @@ flowchart TD
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
pkg_tool_skill --> pkg_tools
pkg_tool_workspace_dependencies --> pkg_tools
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
pkg_tool_web --> pkg_tools
@@ -1501,6 +1503,7 @@ flowchart TD
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
+2 -2
View File
@@ -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 docs/tool-catalog.md
tool-catalog.md: 70d32d5818690fbaa3717ece8314ac8a5a8ffcbf
tool-catalog.zh.md: 72493ae3b858b40e46caa5f4962c2132f2bb55c6
tool-catalog.md: f8ea8e3c41b74011c6e4dbf3dcc3c9d63d1635b2
tool-catalog.zh.md: a91470bc81f90bc61273f196100564eef171c040
+18
View File
@@ -43,6 +43,7 @@ This table connects model-visible tool names to the plugin package and service s
| `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/message/queued`, `team/message/delivered`, `team/task`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. |
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflowEngine`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-workspace-dependencies` | `load_workspace_dependencies` | `ctx.tools` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. |
<a id="deepseek-aidsh-plugin-manager"></a>
@@ -2438,6 +2439,23 @@ Constraints: concurrency and total-agent caps apply; no filesystem, network, tim
Source: [`packages/workflow/tool-workflow/src/index.ts`](../packages/workflow/tool-workflow/src/index.ts)
<a id="deepseek-aidsh-tool-workspace-dependencies"></a>
## `@deepseek-ai/dsh-tool-workspace-dependencies`
### `load_workspace_dependencies`
Get absolute paths to bundled Python and library directories, plus bundled Python distribution versions. Node.js and pnpm paths are included when the payload provides them. Python includes numpy, pandas, python-docx, python-pptx, openpyxl, Pillow, lxml, and XlsxWriter. Use these libraries for Office files unless the user or workspace instructions select another environment. When Node.js and pnpm paths are returned, run pnpm with that Node executable and pnpm script path. This does not change PATH or package-manager settings.
```json
{
"type": "object",
"properties": {}
}
```
Source: [`packages/skill/tool-workspace-dependencies/src/index.ts`](../packages/skill/tool-workspace-dependencies/src/index.ts)
<a id="deepseek-aidsh-tool-web"></a>
## `@deepseek-ai/dsh-tool-web`
+18
View File
@@ -47,6 +47,7 @@
| `@deepseek-ai/dsh-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/message/queued`、`team/message/delivered`、`team/task`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 |
| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 |
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`、`ctx.workflowEngine`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents the script children)` | `tool/call`、`tool/result` | - | - |
| `@deepseek-ai/dsh-tool-workspace-dependencies` | `load_workspace_dependencies` | `ctx.tools` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-web` | `web_fetch`、`web_search` | `ctx.tools`、`ctx.web`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | web_search 和 web_fetch 将提供方选择置于 ctx.web 之后,使模型可见 schema 在更换后端时保持稳定。 |
<a id="deepseek-aidsh-plugin-manager"></a>
@@ -2446,6 +2447,23 @@ todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为
来源:[`packages/workflow/tool-workflow/src/index.ts`](../packages/workflow/tool-workflow/src/index.ts)
<a id="deepseek-aidsh-tool-workspace-dependencies"></a>
## `@deepseek-ai/dsh-tool-workspace-dependencies`
### `load_workspace_dependencies`
获取随包附带的 Python 和库目录的绝对路径,以及随包 Python 发行版的版本。payload 提供 Node.js 和 pnpm 时才返回对应路径。Python 含 numpy、pandas、python-docx、python-pptx、openpyxl、Pillow、lxml 与 XlsxWriter。除非用户或工作区指令选择了别的环境,Office 文件请使用这些库。返回 Node.js 和 pnpm 路径时,用该 Node 可执行文件和 pnpm 脚本路径运行 pnpm。本工具不改 PATH,也不改包管理器设置。
```json
{
"type": "object",
"properties": {}
}
```
来源:[`packages/skill/tool-workspace-dependencies/src/index.ts`](../packages/skill/tool-workspace-dependencies/src/index.ts)
<a id="deepseek-aidsh-tool-web"></a>
## `@deepseek-ai/dsh-tool-web`
+1 -1
View File
@@ -28,7 +28,7 @@ pre-commit:
# lefthook only inspects files present on disk — so that one case still
# falls through to the freshness assertion in the test lane.
- name: third-party notices (staged)
glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,apps/desktop/scripts/primary-runtime-lock.json,scripts/gen-third-party-notices.ts,scripts/browser-bundled-externals.ts,scripts/build-exe-for-python-sdk.ts,tsconfig.base*.json,packages/*/*/src/**/*,packages/*/*/tsdown.config.ts,packages/client/tsdown.client.ts,apps/*/src/**/*,apps/*/vite.config.ts}'
glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/primary-runtime/lock.json,scripts/gen-third-party-notices.ts,scripts/browser-bundled-externals.ts,scripts/build-exe-for-python-sdk.ts,tsconfig.base*.json,packages/*/*/src/**/*,packages/*/*/tsdown.config.ts,packages/client/tsdown.client.ts,apps/*/src/**/*,apps/*/vite.config.ts}'
run: node_modules/.bin/tsx scripts/gen-third-party-notices.ts && git add THIRD_PARTY_NOTICES.md
- name: whitespace (staged)
+7 -1
View File
@@ -17,6 +17,7 @@
"website"
],
"scripts": {
"prepare:primary-runtime": "tsx scripts/primary-runtime/prepare.ts",
"build": "tsx scripts/build.ts",
"build:bench": "npm run build:native-system && npm run build:lib && tsdown --config benchmarks/tsdown.config.ts",
"build:official": "tsx scripts/build.ts --profile official",
@@ -226,7 +227,9 @@
"@yarnpkg/cli-dist": "4.17.1",
"eslint-plugin-sonarjs": "^4.1.0",
"execa": "^10.0.0",
"extract-zip": "^2.0.1",
"fast-check": "^4.8.0",
"fflate": "^0.8.2",
"istanbul-lib-report": "^3.0.1",
"js-yaml": "^4.2.0",
"jscpd": "^5.0.12",
@@ -239,14 +242,17 @@
"micromark-extension-gfm": "^3.0.0",
"oxlint": "1.76.0",
"oxlint-tsgolint": "7.0.2001",
"pnpm": "11.7.0",
"publint": "^0.3.21",
"smol-toml": "^1.7.1",
"spdx-expression-parse": "^5.0.0",
"tar": "^7.5.0",
"tsdown": "^0.22.2",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.8",
"@deepseek-ai/dsh-skill-office": "workspace:^"
"@deepseek-ai/dsh-skill-office": "workspace:^",
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:^"
}
}
+2 -2
View File
@@ -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/bundle/sdk-app/README.md
README.md: 0bac09b5a7e355434e35b04f4e9abd3e816505af
README.zh.md: e0e0d7e1e147c0a2dfaed7014ea0b3a0a9028f2b
README.md: 6d6106e6dd803cee5c51cee0a38f80705986ddfa
README.zh.md: 9de1fb6e17f3f83b7d8c2acbf87154b1cdfea38f
+2
View File
@@ -33,6 +33,8 @@ The startup provider binds stdin EOF to the launcher's bounded successful shutdo
The SDK uses the base `read`, `write`, and `edit` defaults. To add `str_replace_editor`, use the explicit insertion patch in the [base configuration guide](../base/README.md#use-this-package). The standalone `sdk-minimal` profile owns its separate tool selection.
The packaged Python runtime enables the [Office runtime query and skills](../../skill/tool-workspace-dependencies/README.md#use-this-package) by default. `DSH_PRIMARY_RUNTIME` overrides its bundled payload; an empty value disables both rows. Source launches without a carrier default remain opt-in. The tool reads the payload in place. Profile patches can disable `skill-office` or replace its `assetRoot` independently of `workspace-dependencies`; filesystem skills with the same name take precedence over bundled skills. See the [runtime package](../../../python/sdk-runtime/README.md) for packaging and configuration.
-----
<a id="model-experience"></a>
+2
View File
@@ -33,6 +33,8 @@ kind: "package-bundle"
SDK 使用 base 默认提供的 `read`、`write` 和 `edit`。要添加 `str_replace_editor`,请使用 [base 配置指南](../base/README.zh.md#use-this-package)中的显式插入 patch。独立的 `sdk-minimal` profile 自行决定其工具选择。
打包的 Python runtime 默认启用 [Office 运行时查询与 skills](../../skill/tool-workspace-dependencies/README.zh.md#use-this-package)。`DSH_PRIMARY_RUNTIME` 覆盖随包 payload;空值禁用两行。没有载体默认路径的源码启动仍需显式启用。工具原位读取 payload。profile patch 可独立于 `workspace-dependencies` 禁用 `skill-office` 或替换其 `assetRoot`;同名文件系统 skills 优先于随包 skills。打包与配置见 [runtime 包](../../../python/sdk-runtime/README.zh.md)。
-----
<a id="model-experience"></a>
+15
View File
@@ -23,3 +23,18 @@
- id: hmr
disabled: true
# The packaged carrier supplies a default; DSH_PRIMARY_RUNTIME overrides it,
# including an empty-string opt-out. Each row can be patched independently.
- insert:
- id: workspace-dependencies
name: '@deepseek-ai/dsh-tool-workspace-dependencies'
disabled: !!js "!(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME)"
config:
source: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '')"
- id: skill-office
name: '@deepseek-ai/dsh-skill-office'
disabled: !!js "!(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME)"
config:
assetRoot: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')"
+3 -1
View File
@@ -37,7 +37,9 @@
"@deepseek-ai/dsh-cmdline": "workspace:^",
"@deepseek-ai/dsh-sdk-jsonrpc-server": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^",
"commander": "^15.0.0"
"commander": "^15.0.0",
"@deepseek-ai/dsh-skill-office": "workspace:^",
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
+2 -2
View File
@@ -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/client/ui-plan/README.md
README.md: 96ecd3777ef0ce4ce6babd977eef055ad5c683ec
README.zh.md: fb6eb850b868913bdccec175500ff4e26d7a2157
README.md: d87d288ace572bcaf97778f771b1ecc9b4400356
README.zh.md: 14b98d14376bcf7b27a07a249b14627714b31282
+2 -2
View File
@@ -29,7 +29,7 @@ Mount this plugin alongside `ui-conversation` and `dsh-plan-mode`; the chip then
### What the chip shows
While the effective target is plan mode, the seat renders the blue "Plan ×" status button, which executes `/plan off`. Otherwise the seat stays empty: a host without plan mode, or a Draft with no session, shows nothing. While plan mode is the effective target, the composer textarea's placeholder switches to the plan-task hint — "describe your task to generate plan" — unless the owning surface supplies its own placeholder.
While the effective target is plan mode, the seat renders the blue "Plan" status button — the plan glyph ahead of the label, swapped for a circled cross while the enabled button is hovered or keyboard-focused — which executes `/plan off`. Otherwise the seat stays empty: a host without plan mode, or a Draft with no session, shows nothing. While plan mode is the effective target, the composer textarea's placeholder switches to the plan-task hint — "describe your task to generate plan" — unless the owning surface supplies its own placeholder.
### Reading submitted plans
@@ -51,7 +51,7 @@ Admission failures (`matched: false`, business errors, transport faults) surface
The chip occupies the conversation-declared `conversation.input.plan` single seat; the node half is an empty apply (the roster row). Reads ride the generic projection pair through the standard-kit `useProjection`: the effective target is `pending ? !active : active` — a folded host value, not client optimism, so an arriving frame corrects the chip either way. The seat's injected face carries one verb, `exitPlanMode`, which executes `/plan off` through `ctx.remote.commands.execute` and maps admission failures to an inline error line. The placeholder and hint text live in ui-conversation's `conversation` locale namespace and are shared verbatim with the claimed `/plan` command hint.
Plan cards derive from native `tool/call` or PTC dispatch arguments through a Conversation Definition, with each invocation’s resolved Turn location. They contribute to the additive `conversation.chat.turnTail` list alongside file deliveries. The plan resource address identifies the invocation and its complete ordinary or direct-parent subagent Session address; its provider reads existing Session history, including older pages, without storing document text in sidebar layout. The question plugin owns the review action slot and supplies its request key, complete text, and optional invocation identity. The [decision](../../../.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md) explains why review lifetime and document lifetime remain separate. Subagent plan addresses also preserve unknown mode so history reads can resolve the child descriptor.
Plan cards derive from native `tool/call` or PTC dispatch arguments through a Conversation Definition, with each invocation’s resolved Turn location. They contribute to the additive `conversation.chat.turnTail` list alongside file deliveries. The plan resource address identifies the invocation and its complete ordinary or direct-parent subagent Session address; its provider reads existing Session history, including older pages, without storing document text in sidebar layout. The question plugin owns the review action slot and supplies its request key, complete text, and optional invocation identity. The automatic open reads `ctx.sidebarRight.mounted` through a bound hook and runs once a seat is on screen: a review arriving while a global panel is active mounts ahead of the returning seat in the same commit, before that seat binds. The [decision](../../../.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md) explains why review lifetime and document lifetime remain separate. Subagent plan addresses also preserve unknown mode so history reads can resolve the child descriptor.
</details>
+2 -2
View File
@@ -29,7 +29,7 @@ kind: "package-reference"
### 徽章显示什么
当有效目标为 plan 模式时,该座位渲染 蓝色「计划 ×」状态按钮,执行 `/plan off`。否则座位保持为空:未组合 plan-mode 的宿主,或尚无会话的 Draft,都不显示任何内容。plan 模式为有效目标期间,composer 文本框的 placeholder 切换为 plan 任务提示——「describe your task to generate plan」——除非所属 surface 提供自己的 placeholder。
当有效目标为 plan 模式时,该座位渲染蓝色「计划」状态按钮——标签前是计划图标,按钮可用且被悬停或键盘聚焦时换成圆形 ×——点击执行 `/plan off`。否则座位保持为空:未组合 plan-mode 的宿主,或尚无会话的 Draft,都不显示任何内容。plan 模式为有效目标期间,composer 文本框的 placeholder 切换为 plan 任务提示——「describe your task to generate plan」——除非所属 surface 提供自己的 placeholder。
### 查看已提交的计划
@@ -51,7 +51,7 @@ kind: "package-reference"
徽章占据 conversation 声明的 `conversation.input.plan` 单实例座位;node 半部是空 apply(roster 行)。读取经 standard-kit 的 `useProjection` 走通用投影对:有效目标是 `pending ? !active : active`——折叠的宿主值而非客户端乐观态,因此到达的帧无论哪个方向都会纠正徽章。座位注入面携带一个动词 `exitPlanMode`,经 `ctx.remote.commands.execute` 执行 `/plan off`,并把准入失败映射为一行内联错误。placeholder 与提示文案位于 ui-conversation 的 `conversation` locale 命名空间,与已认领 `/plan` 命令的提示逐字共用。
计划卡片通过 Conversation Definition 从原生 `tool/call` 或 PTC dispatch 参数派生,并使用每次调用已解析的回合位置。它们与文件产物一起贡献到可追加的 `conversation.chat.turnTail` 列表。计划资源地址标识调用及完整的普通会话地址或子会话直接父级地址;provider 读取已有会话历史及较早分页,不把正文存入侧边栏布局。提问插件拥有审批操作插槽,并提供请求键、完整正文和可选的调用标识。[决策记录](../../../.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.zh.md)说明审批与文档为何保持独立生命周期。 子代理计划地址也保留未知模式,以便历史读取解析子 descriptor。
计划卡片通过 Conversation Definition 从原生 `tool/call` 或 PTC dispatch 参数派生,并使用每次调用已解析的回合位置。它们与文件产物一起贡献到可追加的 `conversation.chat.turnTail` 列表。计划资源地址标识调用及完整的普通会话地址或子会话直接父级地址;provider 读取已有会话历史及较早分页,不把正文存入侧边栏布局。提问插件拥有审批操作插槽,并提供请求键、完整正文和可选的调用标识。自动打开通过绑定的 hook 读取 `ctx.sidebarRight.mounted`,在座位出现在屏幕上后才执行:全局面板打开期间到达的审批,会在返回时与座位在同一次提交里先挂载、早于座位完成绑定。[决策记录](../../../.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.zh.md)说明审批与文档为何保持独立生命周期。 子代理计划地址也保留未知模式,以便历史读取解析子 descriptor。
</details>
@@ -4,8 +4,9 @@ import { FileTypeIcon, IconChevronRightOutlineRegular } from '@deepseek-ai/dsh-c
import type { ChatNode } from '@deepseek-ai/dsh-client-ui-chat/client'
import { shallowEqual } from '@deepseek-ai/dsh-client-store'
import type {} from './plan-definition.ts'
import type { InjectFace, PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
import type { HostObservable, InjectFace, PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type {} from '@deepseek-ai/dsh-client-ui-user-questions/client'
import type { createPlanReviewStore } from './review-store.ts'
import css from './PlanPreview.module.css'
@@ -20,6 +21,10 @@ export interface PlanOpenInjected {
export interface PlanReviewOpenInjected {
/** Open the logged plan, or the request's temporary document when no invocation exists. */
openReview: (review: PropsRuntime<'conversation.plan-review.actions'>['review'], requestKey: string) => void
readonly hooks: {
/** The session whose right Sidebar seat is mounted; `undefined` while none is on screen. */
readonly sidebarMounted: HostObservable<SessionId | undefined>
}
}
/**
@@ -52,17 +57,23 @@ export function PlanCards({ turn, useChat, openPlan, t }: PropsRuntime<'conversa
/**
* Open each pending plan automatically and retain a manual opener without answering it.
*
* The automatic open waits for a mounted Sidebar seat: a review that arrives
* while the Conversation is off screen mounts in the same commit as the seat,
* ahead of it, and the seat binds from its own effect. Reading the bound
* session through the hook opens once that binding exists.
* @param props - Review identity, Session store, localized copy, and navigation.
* @returns an opener for either logged or temporary plan text.
*/
export function PlanReviewOpen({ review, requestKey, openReview, t, useStore, actions }: PropsRuntime<'conversation.plan-review.actions'> & InjectFace<PlanReviewOpenInjected> & PropsLocale<'plan'> & PropsStore<ReturnType<typeof createPlanReviewStore>>) {
export function PlanReviewOpen({ review, requestKey, openReview, useSidebarMounted, t, useStore, actions }: PropsRuntime<'conversation.plan-review.actions'> & InjectFace<PlanReviewOpenInjected> & PropsLocale<'plan'> & PropsStore<ReturnType<typeof createPlanReviewStore>>) {
const identity = review.callId === undefined ? `review:${requestKey}` : `call:${review.callId}`
const opened = useStore(state => state.opened[identity] === true)
const mounted = useSidebarMounted(session => session !== undefined)
useEffect(() => {
if (opened) return
if (opened || !mounted) return
openReview(review, requestKey)
actions.markOpened(identity)
}, [identity, opened, openReview, review, requestKey, actions])
}, [identity, opened, mounted, openReview, review, requestKey, actions])
return <button type="button" className={css.reviewLink} title={t('preview.open')} aria-label={t('preview.open')}
onClick={() => { openReview(review, requestKey) }}>{t('preview.full')}<IconChevronRightOutlineRegular size={14} /></button>
}
@@ -1,5 +1,9 @@
/* Active plan status: a business accent pill at the control row's own height
(28px, like the + control and the sibling selects). */
(28px, like the + control and the sibling selects). The radius is half that
height — the compact Button geometry — under the theme's corner curvature,
the same corners as the sibling access-mode trigger; a 999px pill would
need the circular `corner-shape: round` opt-out and read rounder than
every neighbouring control. */
.wrap {
display: inline-flex;
@@ -15,8 +19,7 @@
min-width: 34px;
padding: 0 8px;
border: none;
border-radius: 999px;
corner-shape: round;
border-radius: 14px;
background: var(--dsw-alias-state-business-tertiary);
color: var(--dsw-alias-state-business-primary);
font-size: 13px;
@@ -43,12 +46,28 @@
cursor: default;
}
.close {
/* One 14px glyph slot ahead of the label (the access-mode trigger's layout):
the plan glyph at rest, the circled close while the enabled chip is hovered
or keyboard-focused, marking the pending click as turning plan mode off. */
.glyph {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
width: 14px;
height: 14px;
color: currentColor;
}
.hoverGlyph,
.chip:hover:not(:disabled) .restGlyph,
.chip:focus-visible .restGlyph {
display: none;
}
.chip:hover:not(:disabled) .hoverGlyph,
.chip:focus-visible .hoverGlyph {
display: block;
}
.error {
color: var(--dsw-alias-state-error-primary);
font-size: 12px;
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { IconCloseFillRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import { IconCloseCircleFillRegular, IconPlanOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
// Type-only: pulls the ui-conversation SlotMap merge (the input.plan seat and
// its {locked} owner share).
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
@@ -58,10 +58,11 @@ export function PlanChip({ useProjection, locked, exitPlanMode, t }: PlanChipPro
disabled={locked || leaving}
onClick={off}
>
{t('chip.label')}
<span className={css.close} aria-hidden>
<IconCloseFillRegular size={12} />
<span className={css.glyph} aria-hidden>
<IconPlanOutlineRegular className={css.restGlyph} size={14} />
<IconCloseCircleFillRegular className={css.hoverGlyph} size={14} />
</span>
{t('chip.label')}
</button>
{error !== null && <span className={css.error} role="status" title={error}>{t('chip.exitFailed')}</span>}
</span>
@@ -88,6 +88,7 @@ export function apply(ctx: ClientContext): void {
params: { planReview: { markdown: review.plan, title: extractMarkdownPlainText(review.plan, { mode: 'first-line' }) } },
})
},
hooks: { sidebarMounted: ctx.sidebarRight.mounted },
}),
}, PlanReviewOpen))
ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
@@ -11,6 +11,7 @@ import { describe, expect, it, vi } from 'vitest'
import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
import { PlanChip } from '../src/client/PlanModeControl.tsx'
import { PlanCards, PlanReviewOpen, type PlanOpenInjected, type PlanReviewOpenInjected } from '../src/client/PlanCard.tsx'
@@ -31,13 +32,14 @@ function providePreview(ctx: Context) {
const registerType = vi.fn<Context['sidebarRightTabs']['register']>(() => removeType)
const openResourceIn = vi.fn<Context['sidebarRight']['openResourceIn']>()
const openResource = vi.fn<Context['sidebarRight']['openResource']>()
const mounted = createSnapshotStore<SessionId | undefined>(undefined)
const subagentAddress = vi.fn<Context['sessions']['subagentAddress']>(() => undefined)
ctx.provide('sessions', { subagentAddress })
ctx.provide('resources', { register: vi.fn(() => removeResources) })
ctx.provide('sidebarRightTabs', { register: registerType })
ctx.provide('sidebarRight', { openResourceIn, openResource })
ctx.provide('sidebarRight', { openResourceIn, openResource, mounted })
ctx.provide('remote.session', {})
return { events, removeResources, removeType, registerType, openResourceIn, openResource, subagentAddress }
return { events, removeResources, removeType, registerType, openResourceIn, openResource, mounted, subagentAddress }
}
const SID = 's-plan' as SessionId
@@ -183,6 +185,8 @@ describe('ui-plan browser apply', () => {
const review = b.slots.entries('conversation.plan-review.actions')[0]!
expect(review.component).toBe(PlanReviewOpen)
const reviewInjected = (review.inject as unknown as (sessionId: SessionId) => PlanReviewOpenInjected)(SID)
// The automatic open reads the service's mounted-seat source, not a copy.
expect(reviewInjected.hooks.sidebarMounted).toBe(b.mounted)
const pending = { id: 'review', question: 'Approve?', plan: plan.markdown, callId: plan.callId, approve: { label: 'Approve' } }
reviewInjected.openReview(pending, 'question:1')
expect(b.openResource).toHaveBeenLastCalledWith(address)
@@ -11,6 +11,7 @@ import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import type { PlanProjection } from '@deepseek-ai/dsh-plan-mode/client'
import { PlanChip, type PlanChipProps } from '../src/client/PlanModeControl.tsx'
import css from '../src/client/PlanModeControl.module.css'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
import { zh } from '../src/client/locales.ts'
@@ -54,6 +55,21 @@ describe('PlanChip', () => {
expect(chip().textContent).toBe('计划')
})
it('leads with the plan glyph and swaps in the circled close only while hovered', () => {
setup({ active: true, pending: false })
// Glyph-then-label like the sibling access-mode trigger; the two glyphs
// share one slot and CSS shows exactly one of them, so no trailing cross.
const glyphs = chip().querySelectorAll('svg')
expect(glyphs).toHaveLength(2)
const slot = glyphs[0]!.parentElement!
expect(slot).toBe(chip().firstElementChild)
expect(slot.getAttribute('aria-hidden')).toBe('true')
expect(glyphs[0]!.classList.contains(css.restGlyph!)).toBe(true)
expect(glyphs[1]!.classList.contains(css.hoverGlyph!)).toBe(true)
expect(glyphs[1]!.parentElement).toBe(slot)
expect(chip().lastChild?.textContent).toBe('计划')
})
it('executes /plan off once and follows the projection down', async () => {
let resolve!: (value: string | null) => void
const exitPlanMode = vi.fn(() => new Promise<string | null>((done) => { resolve = done }))
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi, type Mock } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { ConversationNodeAssembler } from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { SessionLiveEventEntry } from '@deepseek-ai/dsh-api-session-controller/client'
@@ -7,6 +7,7 @@ import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client'
import { chatViewDefinition } from '../../ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
import { en, zh } from '../src/client/locales.ts'
import { PlanCards, PlanReviewOpen } from '../src/client/PlanCard.tsx'
@@ -29,6 +30,16 @@ function planNode(data: typeof plan) {
function planHook(nodes: readonly unknown[]): Parameters<typeof PlanCards>[0]['useChat'] {
return ((select: (snapshot: unknown) => unknown) => select({ nodes: { values: () => nodes } })) as Parameters<typeof PlanCards>[0]['useChat']
}
/** The opener's props share; each test supplies the seat hook beside it. */
function reviewProps(review: { plan: string; callId?: typeof plan.callId }, openReview: Mock, store: ReturnType<ReturnType<typeof createPlanReviewStore>['create']>) {
return { review, requestKey: 'question:1', t, openReview, actions: store.actions,
useStore: (select: (state: ReturnType<typeof store.getSnapshot>) => unknown) => select(store.getSnapshot()),
} as unknown as Parameters<typeof PlanReviewOpen>[0]
}
/** A bound `useSidebarMounted` reading one observable seat value. */
function seatHook(read: () => SessionId | undefined) {
return <S,>(select: (session: SessionId | undefined) => S): S => select(read())
}
describe('submitted plan identity', () => {
it('reads native, running PTC, and settled PTC calls', () => {
@@ -164,9 +175,7 @@ describe('plan entry points and document', () => {
const openReview = vi.fn()
const store = createPlanReviewStore().create()
const review = { plan: markdown, ...(logged ? { callId: plan.callId } : {}) }
const props = { review, requestKey: 'question:1', t, openReview, actions: store.actions,
useStore: (select: (state: ReturnType<typeof store.getSnapshot>) => unknown) => select(store.getSnapshot()),
} as unknown as Parameters<typeof PlanReviewOpen>[0]
const props = { ...reviewProps(review, openReview, store), useSidebarMounted: seatHook(() => target.session.sessionId) }
const first = render(<PlanReviewOpen {...props} />)
expect(openReview).toHaveBeenCalledExactlyOnceWith(review, 'question:1')
first.rerender(<PlanReviewOpen {...props} />)
@@ -186,6 +195,31 @@ describe('plan entry points and document', () => {
} as Parameters<typeof PlanReviewOpen>[0]} />)
expect(openReview).toHaveBeenCalledTimes(4)
})
it('waits for a mounted sidebar seat before opening automatically, then opens once', () => {
// The review and the seat mount in one commit, the review first: its effect
// runs while no seat is bound, and the seat's own effect binds afterwards.
const openReview = vi.fn()
const store = createPlanReviewStore().create()
const mounted = createSnapshotStore<SessionId | undefined>(undefined)
const review = { plan: markdown, callId: plan.callId }
const props = { ...reviewProps(review, openReview, store), useSidebarMounted: seatHook(() => mounted.getSnapshot()) }
const view = render(<PlanReviewOpen {...props} />)
expect(openReview).not.toHaveBeenCalled()
expect(store.getSnapshot().opened).toEqual({})
// The manual opener stays available without a seat; the service decides what to do.
fireEvent.click(screen.getByRole('button', { name: 'Open plan in sidebar' }))
expect(openReview).toHaveBeenCalledTimes(1)
mounted.set(target.session.sessionId)
view.rerender(<PlanReviewOpen {...props} />)
expect(openReview).toHaveBeenCalledTimes(2)
expect(store.getSnapshot().opened).toEqual({ [`call:${plan.callId}`]: true })
view.rerender(<PlanReviewOpen {...props} />)
mounted.set(undefined)
view.rerender(<PlanReviewOpen {...props} />)
mounted.set(target.session.sessionId)
view.rerender(<PlanReviewOpen {...props} />)
expect(openReview).toHaveBeenCalledTimes(2)
})
it('renders temporary Markdown and reports expired navigation after reload', () => {
const address = reviewPreviewAddress(target.session.sessionId, 'window:question:1')
expect(isReviewPreviewAddress(address)).toBe(true)
@@ -278,6 +278,24 @@ export const IconCloseFillMedium = (props: IconProps) => (
<IconCloseFillArtwork {...props} strokeWidth={ICON_MEDIUM_STROKE} />
)
// A 7-unit disc with the cross knocked out of it (even-odd), so the cross
// takes the color of whatever sits behind the glyph.
const IconCloseCircleFillArtwork = ({ size = 16, className, strokeWidth }: WeightedIconProps) => (
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" strokeWidth={strokeWidth}>
<path fillRule="evenodd" clipRule="evenodd" d="M15 8A7 7 0 1 1 1 8A7 7 0 1 1 15 8ZM6.409 10.652L5.348 9.591L6.939 8L5.348 6.409L6.409 5.348L8 6.939L9.591 5.348L10.652 6.409L9.061 8L10.652 9.591L9.591 10.652L8 9.061Z" fill="currentColor" />
</svg>
)
/** Regular IconCloseCircleFill artwork (cross knocked out of a filled disc); its fill-only geometry is weight-independent. */
export const IconCloseCircleFillRegular = (props: IconProps) => (
<IconCloseCircleFillArtwork {...props} strokeWidth={ICON_REGULAR_STROKE} />
)
/** Medium IconCloseCircleFill artwork; it matches Regular because the geometry is fill-only. */
export const IconCloseCircleFillMedium = (props: IconProps) => (
<IconCloseCircleFillArtwork {...props} strokeWidth={ICON_MEDIUM_STROKE} />
)
const IconCopyOutlineArtwork = ({ size = 16, className, strokeWidth }: WeightedIconProps) => (
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" strokeWidth={strokeWidth}>
<rect x="1.52075" y="4.07373" width="10.3932" height="10.3932" rx="2" stroke="currentColor" />
@@ -17,18 +17,33 @@ const icons = Object.fromEntries(
const iconNames = Object.keys(icons)
describe('product icon set', () => {
it('exports regular and medium variants for all 90 public glyphs', () => {
expect(iconNames.length).toBe(180)
it('exports regular and medium variants for all 91 public glyphs', () => {
expect(iconNames.length).toBe(182)
expect(iconNames.some(name => /\d+$/.test(name))).toBe(false)
const regular = iconNames.filter(name => name.endsWith('Regular')).map(name => name.slice(0, -'Regular'.length))
const medium = iconNames.filter(name => name.endsWith('Medium')).map(name => name.slice(0, -'Medium'.length))
expect(medium.sort()).toEqual(regular.sort())
expect(iconNames).toEqual(expect.arrayContaining([
'IconPlanOutlineRegular', 'IconCompactOutlineRegular', 'IconShieldOutlineRegular', 'IconDeliverDocRegular',
'IconWarningTriangleOutlineRegular', 'IconCompareSplitOutlineRegular',
'IconWarningTriangleOutlineRegular', 'IconCompareSplitOutlineRegular', 'IconCloseCircleFillRegular',
]))
})
it('draws the circled close as one currentColor knockout path in both weights', () => {
// A filled disc with the cross cut out of it (even-odd), so the cross shows
// the surface behind the glyph on any background instead of a second color.
for (const Icon of [primitives.IconCloseCircleFillRegular, primitives.IconCloseCircleFillMedium]) {
const { container, unmount } = render(<Icon />)
const paths = container.querySelectorAll('path')
expect(paths).toHaveLength(1)
expect(paths[0]!.getAttribute('fill')).toBe('currentColor')
expect(paths[0]!.getAttribute('fill-rule')).toBe('evenodd')
expect(paths[0]!.getAttribute('stroke')).toBeNull()
expect(container.querySelector('svg')!.getAttribute('width')).toBe('16')
unmount()
}
})
it('exports the shield contour and regular stroke for composite glyphs', () => {
const { container } = render(<primitives.IconShieldOutlineRegular />)
expect(container.querySelector('path')?.getAttribute('d')).toBe(primitives.SHIELD_OUTLINE_PATH)
@@ -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/client/ui-sidebar-right/README.md
README.md: e9c143f34076c308f3afdb348cc09506f3983a35
README.zh.md: 72d0c20817c690c1dcca5a97b67adbdfa1f11261
README.md: 9318723b99adb003b25cf69bf07a8419eea6d1ae
README.zh.md: dcd6dcd59ac19c18c0a7d2e9a72e7a92a6d3bec4
+1 -1
View File
@@ -92,7 +92,7 @@ Two more seats extend what is already there: `sidebar.right.tab.guide` (chain) r
`preferNewPane: true` first asks the target docked pane to split under the normal two-pane and room rules, then falls back to that pane when no split is available. `replaceTab` takes precedence and disables this preference.
`close(tabId)` closes a tab; `active()` reads the active tab. `isExpanded()` and `toggleExpanded()` read and drive the column's expansion; the presentation switch is the panel's own control and not part of this face. Layout operations, for callers that arrange the column programmatically, each recorded like the gesture it stands in for: `focus(tabId)` focuses a tab and its pane; `split(paneId?)` splits a docked pane (the active one by default) under the same pane budget and room rule as the strip's control and returns the new pane's id, or `undefined` — recording nothing — when it cannot; `float(tabId, rect?)` takes a docked tab out into a panel; `dock(paneId)` returns a floating panel to the active docked pane. A tab or pane that does not exist, or already is where the call would put it, is left alone. The face exposes operations only: no layout snapshot, no operation log, no lookup by address. `_undo()` / `_redo()` step the mounted surface's history; they are `@internal` — the sequence has no user-facing control, and these exist for tests. Commands need a mounted session surface; with none, they throw rather than write into a surface nobody draws.
`close(tabId)` closes a tab; `active()` reads the active tab. `isExpanded()` and `toggleExpanded()` read and drive the column's expansion; the presentation switch is the panel's own control and not part of this face. Layout operations, for callers that arrange the column programmatically, each recorded like the gesture it stands in for: `focus(tabId)` focuses a tab and its pane; `split(paneId?)` splits a docked pane (the active one by default) under the same pane budget and room rule as the strip's control and returns the new pane's id, or `undefined` — recording nothing — when it cannot; `float(tabId, rect?)` takes a docked tab out into a panel; `dock(paneId)` returns a floating panel to the active docked pane. A tab or pane that does not exist, or already is where the call would put it, is left alone. The face exposes operations only: no layout snapshot, no operation log, no lookup by address. `_undo()` / `_redo()` step the mounted surface's history; they are `@internal` — the sequence has no user-facing control, and these exist for tests. Commands need a mounted session surface; with none, they throw rather than write into a surface nobody draws. `mounted` is the observable of the session whose seat is on screen, `undefined` while a global panel replaces the Conversation or no session is selected. A component that opens content from its own mount effect reads it through a bound hook and acts once it is defined: the frame mounts the Conversation column ahead of this seat, and the seat publishes its binding from a passive effect of the same commit, so an effect that assumed a bound seat would run first and throw. The plan review's automatic open reads it this way.
<a id="the-tab-domain"></a>
## The Tab domain
@@ -92,7 +92,7 @@ tab 类型分两阶段注册,随包发布的引导类型走的正是别的包
`preferNewPane: true` 会先按普通的两格上限与空间规则尝试分割目标停靠格,无法分栏时回退到该格。`replaceTab` 优先并会禁用这项偏好。
`close(tabId)` 关闭一个 tab;`active()` 读取活动 tab。`isExpanded()` 与 `toggleExpanded()` 读取并驱动该列的展开;形态切换是面板自己的控件,不属于这个接口。布局操作供以编程方式安排该列的调用方使用,每个都像它替代的手势一样被记录:`focus(tabId)` 聚焦一个 tab 及其格;`split(paneId?)` 在与 tab 条控件相同的格预算与空间规则下分栏一个停靠格(默认活跃格),返回新格的 id,做不到时返回 `undefined`——且不记录任何东西;`float(tabId, rect?)` 把停靠 tab 浮出为浮窗;`dock(paneId)` 把浮窗放回活跃停靠格。不存在的 tab 或格、或已处于调用目标状态的,都原样不动。该接口只暴露操作:没有布局快照、没有操作日志、没有按地址查找。`_undo()` / `_redo()` 步进已挂载停靠面的历史;它们是 `@internal`——序列没有面向用户的控件,这两个只为测试存在。命令需要一个已挂载的会话停靠面;没有时它们 throw,而不是写进一个没人绘制的面里。
`close(tabId)` 关闭一个 tab;`active()` 读取活动 tab。`isExpanded()` 与 `toggleExpanded()` 读取并驱动该列的展开;形态切换是面板自己的控件,不属于这个接口。布局操作供以编程方式安排该列的调用方使用,每个都像它替代的手势一样被记录:`focus(tabId)` 聚焦一个 tab 及其格;`split(paneId?)` 在与 tab 条控件相同的格预算与空间规则下分栏一个停靠格(默认活跃格),返回新格的 id,做不到时返回 `undefined`——且不记录任何东西;`float(tabId, rect?)` 把停靠 tab 浮出为浮窗;`dock(paneId)` 把浮窗放回活跃停靠格。不存在的 tab 或格、或已处于调用目标状态的,都原样不动。该接口只暴露操作:没有布局快照、没有操作日志、没有按地址查找。`_undo()` / `_redo()` 步进已挂载停靠面的历史;它们是 `@internal`——序列没有面向用户的控件,这两个只为测试存在。命令需要一个已挂载的会话停靠面;没有时它们 throw,而不是写进一个没人绘制的面里。`mounted` 是「席位正在屏幕上的那个会话」的可观察值,全局面板替代 Conversation 或没有选中会话时为 `undefined`。要在自己的挂载 effect 里打开内容的组件通过绑定的 hook 读取它,并在它有值后再行动:框架把 Conversation 列挂载在这个席位之前,而席位在同一次提交的 passive effect 里才发布绑定,一个假定席位已绑定的 effect 会先运行并 throw。计划审阅的自动打开就是这样读取它的。
<a id="the-tab-domain"></a>
## Tab 域
@@ -7,7 +7,10 @@
* session id, bound actions, its surface — for exactly as long as it is mounted,
* and every command on the public face goes through that binding; a command
* arriving with no seat mounted has no session to act on and fails loudly rather
* than writing into a surface nobody is drawing. And the plugin adopts each
* than writing into a surface nobody is drawing. `mounted` publishes that
* binding's session as an observable, so a consumer that wants to open content
* as soon as a seat is on screen subscribes to it instead of assuming one is
* bound when its own effect runs. And the plugin adopts each
* session's store instance as the runtime mints it, so the controller reaches
* any session's store by id and syncs the Tab domain from that store's commits.
*
@@ -28,6 +31,7 @@
* callers use the service's navigation methods.
*/
import { randomUUID } from '@deepseek-ai/dsh-util-crypto'
import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { FloatRect, PaneId, TabId, TabRecord } from '@deepseek-ai/dsh-client-ui-dockkit'
import { activeDockPaneId, canSplit, findContentTab, dockPaneIds, findTabPane, getPane } from '@deepseek-ai/dsh-client-ui-dockkit'
import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
@@ -148,6 +152,15 @@ export type SidebarRightCloseHandler = (sessionId: SessionId, tab: TabRecord) =>
/** The outward right-Sidebar face (`ctx.sidebarRight`). */
export interface ISidebarRight {
/**
* The session whose seat is mounted, or `undefined` while no seat is on
* screen (a global panel is active, or no session is selected). Moves when a
* seat binds or releases, so a component that opens content from its own
* mount effect reads it through a bound hook and acts once it is defined:
* the frame mounts the Conversation column ahead of the seat, and the seat
* publishes its binding from a passive effect of the same commit.
*/
readonly mounted: ObservableSnapshot<SessionId | undefined>
/**
* Open a resource: claim it, place it, reveal the column, record the navigation.
*
@@ -216,6 +229,9 @@ export interface ISidebarRight {
export class SidebarRightController implements ISidebarRight {
/** Open tab metadata across saved and adopted Sessions, independent of visible seats. */
readonly openTabs: SidebarTabInventory['source']
private readonly mountedSession = createSnapshotStore<SessionId | undefined>(undefined)
/** The mounted seat's session; see {@link ISidebarRight.mounted}. */
readonly mounted: ObservableSnapshot<SessionId | undefined> = this.mountedSession
private binding: SidebarRightBinding | undefined
private readonly closeHandlers = new Map<string, SidebarRightCloseHandler>()
@@ -271,13 +287,22 @@ export class SidebarRightController implements ISidebarRight {
*/
bind(binding: SidebarRightBinding): () => void {
this.binding = binding
this.publishMounted()
return () => {
// A newer seat may already have taken over; only the binding that is
// still ours may be cleared.
if (this.binding === binding) this.binding = undefined
if (this.binding !== binding) return
this.binding = undefined
this.publishMounted()
}
}
/** Publish the mounted session only when it changes; a republished binding for the same session is silent. */
private publishMounted(): void {
const next = this.binding?.sessionId
if (this.mountedSession.getSnapshot() !== next) this.mountedSession.set(next)
}
/**
* Open a resource: claim it, place it, reveal the column, record the navigation.
* @param address - a `dsh-resource://<type>/…` address.
@@ -426,7 +451,7 @@ export class SidebarRightController implements ISidebarRight {
* @returns the record, or `undefined` with no mounted surface.
*/
active(): TabRecord | undefined {
const layout = this.mounted()?.layout
const layout = this.mountedSurface()?.layout
if (layout === undefined) return undefined
const { activeTabId } = getPane(layout, layout.activePaneId)
return Object.values(layout.tabs).find(tab => tab.id === activeTabId)
@@ -437,7 +462,7 @@ export class SidebarRightController implements ISidebarRight {
* @returns `true` while expanded; `false` while collapsed or with no mounted surface.
*/
isExpanded(): boolean {
return this.mounted()?.layout.expanded ?? false
return this.mountedSurface()?.layout.expanded ?? false
}
/** Collapse an expanded column, or expand a collapsed one. */
@@ -452,7 +477,7 @@ export class SidebarRightController implements ISidebarRight {
*/
focus(tabId: TabId): void {
const { sessionId, actions } = this.require()
if (this.mounted()?.layout.tabs[tabId] === undefined) return
if (this.mountedSurface()?.layout.tabs[tabId] === undefined) return
actions.focusTab(sessionId, tabId)
}
@@ -463,7 +488,7 @@ export class SidebarRightController implements ISidebarRight {
*/
split(paneId?: PaneId): PaneId | undefined {
const { sessionId, actions, canSplitPane } = this.require()
const layout = this.mounted()?.layout
const layout = this.mountedSurface()?.layout
if (layout === undefined) return undefined
const target = paneId ?? activeDockPaneId(layout)
const node = layout.nodes[target]
@@ -481,7 +506,7 @@ export class SidebarRightController implements ISidebarRight {
*/
float(tabId: TabId, rect?: FloatRect): void {
const { sessionId, actions } = this.require()
const layout = this.mounted()?.layout
const layout = this.mountedSurface()?.layout
if (layout === undefined || layout.tabs[tabId] === undefined) return
if (findTabPane(layout, tabId).host !== 'dock') return
actions.floatTab(sessionId, tabId, rect)
@@ -493,7 +518,7 @@ export class SidebarRightController implements ISidebarRight {
*/
dock(paneId: PaneId): void {
const { sessionId, actions } = this.require()
const node = this.mounted()?.layout.nodes[paneId]
const node = this.mountedSurface()?.layout.nodes[paneId]
if (node === undefined || node.kind !== 'pane' || node.host !== 'float') return
actions.unfloatPane(sessionId, paneId)
}
@@ -520,7 +545,7 @@ export class SidebarRightController implements ISidebarRight {
}
/** The mounted session's surface; `undefined` without a seat or before its first open. */
private mounted(): SurfaceState | undefined {
private mountedSurface(): SurfaceState | undefined {
const { binding } = this
return binding === undefined ? undefined : binding.surfaces[binding.sessionId]
}
@@ -80,7 +80,8 @@ export interface SidebarRightInjected {
* Publish this seat's session, actions, and the store's surfaces to `ctx.sidebarRight`.
*
* The service is root-scoped and cannot read a per-entry store, so the only
* honest source is the mounted seat. Held for as long as the seat is mounted.
* honest source is the mounted seat. Held for as long as the seat is mounted;
* the service publishes the bound session through `ctx.sidebarRight.mounted`.
* @param binding - what a command needs to act on this session, and what a tab's own action needs to act on its.
* @returns a release callback.
*/
@@ -2,7 +2,7 @@
/** Sidebar presentation and tab subscriptions through the production slot renderer. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, fireEvent } from '@testing-library/react'
import { useState } from 'react'
import { useEffect, useState, useSyncExternalStore } from 'react'
import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
@@ -21,6 +21,13 @@ declare module '../src/client/contract/params.ts' {
}
}
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
/** A Conversation-column stand-in rendered before the seat, opening a resource as soon as a seat is mounted. */
'sidebar-right.test.opener': { kind: 'single'; scope: 'session'; owner: { armed: boolean } }
}
}
const SESSION = 's-test' as SessionId
const OTHER = 's-other' as SessionId
const runtimes: SlotTestRuntime[] = []
@@ -57,7 +64,7 @@ function transition(property = 'transform') {
}
}
async function mountSeat(viewportWidth = 1440, canShow = true, entryCount = 0) {
async function mountSeat(viewportWidth = 1440, canShow = true, entryCount = 0, opener = false) {
const runtime = await SlotTestRuntime.create()
runtimes.push(runtime)
const frame = { openRightbar: vi.fn(), closeRightbar: vi.fn() }
@@ -68,12 +75,40 @@ async function mountSeat(viewportWidth = 1440, canShow = true, entryCount = 0) {
runtime.ctx.provide('locale', locale)
runtime.slots.installLocale(locale)
await runtime.declare({
'sidebar-right.test.opener': { kind: 'single', scope: 'session' },
'rightbar': { kind: 'single', scope: 'root' },
'conversation.session.header.corner': { kind: 'single', scope: 'session' },
})
await runtime.sessions.add({ id: SESSION })
let reference = runtime.sessions.retainFor(runtime.ctx, SESSION, { source: 'mainView' })
const feature = await runtime.mount({ inject: [...inject], apply })
// The frame mounts the Conversation column before the right column, so a
// Conversation component's mount effect runs before the seat's. The opener
// stands in for one that opens a resource as soon as a seat is mounted — it
// reads `mounted` the way a slot component reads its bound hook — and shows
// only while the Conversation is selected, as the main panel does.
const opened: string[] = []
function OpenerMount() {
const { mounted } = runtime.ctx.sidebarRight
const seat = useSyncExternalStore(listener => mounted.subscribe(listener), () => mounted.getSnapshot())
useEffect(() => {
if (seat === undefined) return
const address = `dsh-resource://file/session/s-test/arrival-${opened.length + 1}.txt`
runtime.ctx.sidebarRight.openResource(address)
opened.push(address)
}, [seat])
return null
}
function Opener({ usePanelInfo, armed }: PropsRuntime<'sidebar-right.test.opener'>) {
const visible = usePanelInfo(info => info.activePanelId === null)
return armed && visible ? <OpenerMount /> : null
}
let arm = (_armed: boolean): void => { throw new Error('mountSeat(opener = true) renders the opener') }
if (opener) {
await act(async () => { runtime.slots.register({ name: 'sidebar-right.test.opener' }, Opener) })
const openerView = runtime.renderSlot('sidebar-right.test.opener', { armed: false }, { session: reference })
arm = (armed) => { openerView.update({ armed }) }
}
const bodies = new Map<string, SidebarRightTabInfo>()
const titles = new Map<string, SidebarRightTabInfo>()
const hooks = new Map<string, PropsRuntime<'sidebar.right.pane.tab'>['useTabInfo']>()
@@ -115,7 +150,7 @@ async function mountSeat(viewportWidth = 1440, canShow = true, entryCount = 0) {
}
return {
runtime, feature, controller, instance, actions: instance.actions, layout,
open, selectSession, frame, pin, bodies, titles, hooks, view,
open, selectSession, frame, pin, bodies, titles, hooks, view, arm, opened,
}
}
@@ -139,6 +174,29 @@ describe('RightbarSeat presentation', () => {
expect(h.layout()).toBe(retained)
})
it('publishes the mounted seat so a Conversation opener acts in the commit that returns from a global panel', async () => {
const h = await mountSeat(1440, true, 0, true)
const errors = vi.spyOn(console, 'error').mockImplementation(() => {})
expect(h.controller.mounted.getSnapshot()).toBe(SESSION)
// Armed while the seat is already bound: the ordinary case.
act(() => { h.arm(true) })
expect(h.opened).toHaveLength(1)
expect(Object.values(h.layout().tabs).map(tab => tab.contentId)).toEqual(h.opened)
// A global panel unmounts both columns and the mounted seat with them.
act(() => { h.runtime.panelInfo.set({ activePanelId: 'other-panel' as MainPanelId }) })
expect(h.view.container.querySelector('[data-sidebar-right-panel]')).toBeNull()
expect(h.controller.mounted.getSnapshot()).toBeUndefined()
// Returning mounts both in one commit, the Conversation's effects first; the
// opener sees no seat, then the seat's own effect binds and it opens.
act(() => { h.runtime.panelInfo.set({ activePanelId: null }) })
expect(h.controller.mounted.getSnapshot()).toBe(SESSION)
expect(h.opened).toHaveLength(2)
expect(Object.values(h.layout().tabs).map(tab => tab.contentId)).toEqual(h.opened)
expect(h.controller.active()?.contentId).toBe(h.opened[1])
expect(errors.mock.calls.map(call => String(call[0])).filter(text => text.includes('slot entry crashed'))).toEqual([])
expect(document.querySelector('[data-slot-error]')).toBeNull()
})
it.each([0, 1, 2])('selects the default from %i guide entries and protects only a sole guide', async (entryCount) => {
const h = await mountSeat(1440, true, entryCount)
act(() => { h.controller.toggleExpanded() })
@@ -111,6 +111,26 @@ describe('SidebarRightController — opening', () => {
expect(() => { controller.dock('pane1' as PaneId) }).toThrow('no session surface is mounted')
})
it('publishes the mounted session only on real transitions of the seat binding', () => {
const h = harness()
const seen: (string | undefined)[] = []
const unsubscribe = h.controller.mounted.subscribe(() => { seen.push(h.controller.mounted.getSnapshot()) })
try {
expect(h.controller.mounted.getSnapshot()).toBeUndefined()
const first = h.publish()
expect(h.controller.mounted.getSnapshot()).toBe(SESSION)
// The seat republishes on every store commit; the same session is silent.
const second = h.publish()
expect(seen).toEqual([SESSION])
// A stale release — the first seat's, after a newer one took over — changes nothing.
first()
expect(h.controller.mounted.getSnapshot()).toBe(SESSION)
second()
expect(h.controller.mounted.getSnapshot()).toBeUndefined()
expect(seen).toEqual([SESSION, undefined])
} finally { unsubscribe() }
})
it('refuses an address no registered type claims, before touching the surface', () => {
const { controller, publish, titles } = harness()
publish()
@@ -31,7 +31,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep',
'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output',
'list_agents', 'list_agents', 'list_mcp_resource_templates', 'list_mcp_resources',
'list_subagent_models', 'lsp', 'plugin_manager', 'present', 'pwsh', 'pwsh', 'ralph',
'list_subagent_models', 'load_workspace_dependencies', 'lsp', 'plugin_manager', 'present', 'pwsh', 'pwsh', 'ralph',
'read', 'read_image', 'read_mcp_resource', 'run_code', 'schedule_create', 'schedule_delete',
'schedule_list', 'send_message', 'send_message', 'session_event_read', 'session_event_search',
'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate',
+2 -2
View File
@@ -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/skill/README.md
README.md: 9546dfbfb2081480a1af8078d65481d0fc080ae3
README.zh.md: eb1231b6fc7e110afdcc22fb25db3dd55b830092
README.md: 75261bdca06b658f04fac17a3eab4306dbe5cecd
README.zh.md: 7d1c38a4fb456f26bf038c8ddfe6f8182d0058f8
+1
View File
@@ -29,6 +29,7 @@ The skill family lets agents and users discover and load reusable task instructi
| [`skill-badge/`](skill-badge/README.md) | Bundles the official "powered by dsh" badge skill, disabled by default | registers on `ctx.skills` |
| [`skill-office/`](skill-office/README.md) | Bundles Word, PowerPoint, and Excel workflows with structural file checks | registers on `ctx.skills` |
| [`tool-skill/`](tool-skill/README.md) | Publishes the session skill catalog and the model-facing `skill` loader tool | registers on `ctx.tools` |
| [`tool-workspace-dependencies/`](tool-workspace-dependencies/README.md) | Reports bundled Office interpreter paths and versions for Desktop and SDK carriers | registers on `ctx.tools` |
-----
+1
View File
@@ -29,6 +29,7 @@ skill 家族让 agent 和用户仅在需要时发现并加载可复用的任务
| [`skill-badge/`](skill-badge/README.zh.md) | 随包附带官方「powered by dsh」徽章 skill,默认禁用 | 注册到 `ctx.skills` |
| [`skill-office/`](skill-office/README.zh.md) | 随包提供 Word、PowerPoint 和 Excel 工作流及文件结构检查 | 注册到 `ctx.skills` |
| [`tool-skill/`](tool-skill/README.zh.md) | 发布会话 skill 目录与面向模型的 `skill` 加载工具 | 注册到 `ctx.tools` |
| [`tool-workspace-dependencies/`](tool-workspace-dependencies/README.zh.md) | 为 Desktop 与 SDK 载体报告内置 Office 解释器路径和版本 | 注册到 `ctx.tools` |
-----
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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/skill/tool-workspace-dependencies/README.md
README.md: 1f7147a1febeb0210befe64dc462e2527013dab3
README.zh.md: f2a1934256776ef97d414be4709a6b918989ae35
@@ -0,0 +1,128 @@
---
description: "The load_workspace_dependencies tool: absolute paths into a bundled Python, Node.js, and pnpm payload, used in place or installed under the Harness home."
kind: "package-reference"
---
# @deepseek-ai/dsh-tool-workspace-dependencies
English | [中文](README.zh.md)
## Summary
Deployments that ship their own script runtimes (Desktop's primary runtime, or a container image layer) mount this tool so the agent can ask where the bundled Python, Node.js, and pnpm live instead of discovering a system interpreter. The tool returns absolute paths and recorded distribution versions; it changes neither `PATH` nor package-manager settings. The payload is either copied under the Harness home on first use (Desktop) or used where it lies (read-only carriers).
## Table of Contents
- [Use this package](#use-this-package)
- [Understand the implementation](#understand-the-implementation)
- [Further Exploration](#further-exploration)
- [Model Experience](#model-experience)
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
- [Dev Note](#dev-note)
-----
<a id="use-this-package"></a>
## Use this package
Mount the plugin beside the tool registry with the payload directory. Configuration validation requires a nonempty `source` and rejects empty `root` values before activation; both paths must be absolute. The bundled Office skills (`@deepseek-ai/dsh-skill-office`) reference this tool by name for their default interpreter.
### Minimal configuration
```yaml
- name: '@deepseek-ai/dsh-tool-workspace-dependencies'
config:
source: /path/to/primary-runtime
```
| Field | Default | Meaning |
|---|---|---|
| `source` | required | Absolute payload directory carrying `runtime.json` and `dependencies/`. |
| `root` | unset | Absolute installation directory under the Harness home. Set: the payload is copied there on the first call and reused while `runtime.json` is unchanged. Unset: the payload is validated and used in place; nothing is copied. |
### Payload layout
`runtime.json` records `desktopVersion`, `platform` (`win32`, `darwin`, or `linux`), `arch`, optional `payloadDigest`, top-level `python`, optional `node`/`pnpm` versions, and the complete `pythonPackages` distribution-version map. A pnpm entry requires Node.js. Python libraries, including numpy and pandas, appear only in `pythonPackages`. Entries live under `dependencies/`: `python/bin/python3` (`python/python.exe` on Windows) with `site-packages` beneath it, and, when declared, `node/bin/node` with `node/node_modules` and `pnpm/bin/pnpm.mjs`. A manifest whose platform or architecture differs from the running process is rejected.
The packaged `sdk` profile uses its bundled Python and Office skills by default; `DSH_PRIMARY_RUNTIME` overrides the resource location, and an empty value opts out. Source launches without a carrier default remain opt-in. See [runtime configuration](../../../python/sdk-runtime/README.md) for independent skill selection. Missing skill resources produce a startup warning; invalid or incomplete external runtime payloads fail the first tool call. Profile configuration changes require restarting the SDK process.
### Build a carrier payload
From a repository checkout with dependencies installed, `CI=true pnpm run prepare:primary-runtime --target linux-x64 --output /tmp/dsh-office` writes `primary-runtime/` and `office-skills/`. The shared [download lock](../../../scripts/primary-runtime/lock.json) also covers `linux-arm64`, `mac-arm64`, `mac-x64`, and `win-x64`. `--python-only` omits Node.js and pnpm; `--cache` selects the hash-verified archive cache. The entry executes interpreter and Office read/write checks only for a native target. Cross-target builds require those checks on the target host before deployment.
A container can copy both directories into an immutable image layer and set `DSH_PRIMARY_RUNTIME` to the absolute `primary-runtime/` path. The SDK queries that payload in place. Desktop uses the same builder and retains its Harness-home installation and signing checks.
-----
<a id="understand-the-implementation"></a>
## Understand the implementation
<details>
<summary>Implementation internals — click to expand</summary>
`readPrimaryRuntime` and build smoke checks share `parsePrimaryRuntime`. It validates the flat manifest and rejects duplicate normalized distribution names. Legacy `components` metadata is normalized in memory, retaining its consistency checks; a missing legacy distribution map becomes empty. Mixed flat and legacy version fields are rejected. Reads do not rewrite metadata, and equivalent normalized manifests can reuse an installed payload. `workspaceDependencyPaths` derives the platform-specific entries. `installPrimaryRuntime` copies into a staging directory, requires declared interpreters and scripts to be files and package roots to be directories, and swaps it into place while retaining the previous tree on failure; `resolvePrimaryRuntime` verifies the same entries without copying. The tool memoizes the first successful preparation for the plugin's lifetime.
| File | Responsibility |
|---|---|
| [`src/index.ts`](src/index.ts) | Manifest validation, path derivation, in-place and installed preparation, tool registration. |
| — | No runtime invariant companion is published: the payload manifest is validated on every preparation, and the tool registry owns registration lifecycle. |
</details>
-----
<a id="further-exploration"></a>
## Further Exploration
- [Office skills](../skill-office/README.md) — the workflows that call this tool for their interpreter.
- [Tool registry](../../core/tools/README.md) — registration and schemas.
-----
<a id="model-experience"></a>
## Model Experience
### Tool schema
#### What the model sees
The model sees the generated [`load_workspace_dependencies` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-workspace-dependencies).
#### Token effect
Fixed schema cost per request where the tool is visible; the description names the bundled Office libraries so the model can choose the interpreter without loading a skill first.
#### KV Cache effect
Prefix-stable while the tool definition and visibility are unchanged.
### Tool result
#### What the model sees
One JSON object with absolute `python` and `pythonPackages` paths, `pythonDistributions` from `runtime.json`, and `node`, `nodePackages`, and `pnpm` when the payload declares them. Repeated calls return the same object.
#### Token effect
A few hundred characters per call; paths dominate.
#### KV Cache effect
Append-only tool result in the turn history; no prompt section is added.
## Known Limitations and Deferred Work
<a id="known-limitations-and-deferred-work"></a>
- Linux targets require glibc; musl payloads are not locked.
- Windows payloads used in place must already be executable from their carrier; the in-place mode performs no permission repair.
<a id="dev-note"></a>
### Dev Note
<details>
<summary>Working context for maintainers — click to expand</summary>
Placement and carrier choices are recorded in the [shared-runtime Agent Note](../../../.agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.md).
</details>
@@ -0,0 +1,128 @@
---
description: "load_workspace_dependencies 工具:返回随包附带的 Python、Node.js 与 pnpm 的绝对路径,payload 可原地使用或安装到 Harness home。"
kind: "package-reference"
---
# @deepseek-ai/dsh-tool-workspace-dependencies
[English](README.md) | 中文
## 概述
自带脚本运行时的部署(Desktop 的 primary runtime,或容器镜像层)挂载本工具,让 agent 询问随包的 Python、Node.js 与 pnpm 在哪里,而不是自行寻找系统解释器。工具返回绝对路径与记录的发行版版本,不改 `PATH`,不改包管理器设置。payload 或在首次调用时复制到 Harness home 下(Desktop),或原地使用(只读载体)。
## 目录
- [使用本包](#use-this-package)
- [了解实现](#understand-the-implementation)
- [延伸阅读](#further-exploration)
- [模型体验](#model-experience)
- [已知限制与暂缓事项](#known-limitations-and-deferred-work)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用本包
与工具注册表一起挂载,给出 payload 目录。配置校验在激活前要求非空 `source` 并拒绝空 `root`;两条路径都必须为绝对路径。内置 Office skills(`@deepseek-ai/dsh-skill-office`)按名字引用本工具取默认解释器。
### 最小配置
```yaml
- name: '@deepseek-ai/dsh-tool-workspace-dependencies'
config:
source: /path/to/primary-runtime
```
| 字段 | 默认值 | 含义 |
|---|---|---|
| `source` | 必填 | 含 `runtime.json` 与 `dependencies/` 的 payload 绝对目录。 |
| `root` | 未设置 | Harness home 下的绝对安装目录。设置时首次调用把 payload 复制过去,`runtime.json` 不变则复用;未设置时校验后原地使用,不复制。 |
### payload 布局
`runtime.json` 记录 `desktopVersion`、`platform`(`win32`、`darwin` 或 `linux`)、`arch`、可选的 `payloadDigest`、顶层 `python` 与可选的 `node`/`pnpm` 版本,以及完整的 `pythonPackages` 分发包版本表。声明 pnpm 时必须同时声明 Node.js。numpy、pandas 等 Python 库只出现在 `pythonPackages` 中。条目位于 `dependencies/`:`python/bin/python3`(Windows 为 `python/python.exe`)及其下的 `site-packages`;声明了才有的 `node/bin/node`、`node/node_modules` 与 `pnpm/bin/pnpm.mjs`。平台或架构与当前进程不符的清单被拒绝。
打包的 `sdk` profile 默认使用随包 Python 和 Office skills;`DSH_PRIMARY_RUNTIME` 覆盖资源位置,空值表示禁用。没有载体默认路径的源码启动仍需显式启用。独立选择 skills 的方式见[运行时配置](../../../python/sdk-runtime/README.zh.md)。缺少 skill 资源会产生启动警告;无效或不完整的外部运行时 payload 在首次工具调用时失败。profile 配置变更需要重启 SDK 进程。
### 构建载体 payload
在已安装依赖的仓库 checkout 中运行 `CI=true pnpm run prepare:primary-runtime --target linux-x64 --output /tmp/dsh-office`,会生成 `primary-runtime/` 和 `office-skills/`。[共享下载锁](../../../scripts/primary-runtime/lock.json)还覆盖 `linux-arm64`、`mac-arm64`、`mac-x64` 和 `win-x64`。`--python-only` 省略 Node.js 和 pnpm;`--cache` 选择经过哈希校验的归档缓存。入口仅对本机目标执行解释器与 Office 读写检查。跨目标构建必须在部署前到目标主机执行这些检查。
容器可将这两个目录复制到不可变镜像层,并将 `DSH_PRIMARY_RUNTIME` 设为 `primary-runtime/` 的绝对路径。SDK 原位查询该 payload。Desktop 使用同一构建器,并保留 Harness-home 安装与签名检查。
-----
<a id="understand-the-implementation"></a>
## 了解实现
<details>
<summary>实现细节——点击展开</summary>
`readPrimaryRuntime` 与构建冒烟检查共用 `parsePrimaryRuntime`。它校验扁平清单,拒绝归一化后重名的分发包。旧 `components` 元数据在内存中归一化,并保留旧格式的一致性校验;旧分发包版本表缺失时转换为空表。混用顶层与旧组件版本字段会被拒绝。读取不改写元数据,归一化后等价的清单可复用已安装产物。`workspaceDependencyPaths` 推导各平台的条目路径。`installPrimaryRuntime` 复制到暂存目录、要求声明的解释器和脚本为文件、库路径为目录、再换入正式位置,失败时保留旧树;`resolvePrimaryRuntime` 做同样的校验但不复制。工具在插件生命周期内记住首次成功的结果。
| 文件 | 职责 |
|---|---|
| [`src/index.ts`](src/index.ts) | 清单校验、路径推导、原地与安装两种准备方式、工具注册。 |
| — | 不发布运行时不变量伴随入口:每次准备都校验 payload 清单,工具注册表负责注册生命周期。 |
</details>
-----
<a id="further-exploration"></a>
## 延伸阅读
- [Office skills](../skill-office/README.zh.md)——调用本工具取解释器的工作流。
- [工具注册表](../../core/tools/README.zh.md)——注册与 schema。
-----
<a id="model-experience"></a>
## 模型体验
### 工具 schema
#### 模型看到什么
模型看到生成的 [`load_workspace_dependencies` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-workspace-dependencies)。
#### Token 影响
工具可见的每个请求固定一份 schema 开销;描述里列出了随包的 Office 库,模型不必先加载 skill 就能选择解释器。
#### KV 缓存影响
工具定义与可见性不变时前缀稳定。
### 工具结果
#### 模型看到什么
一个 JSON 对象:绝对路径 `python` 与 `pythonPackages`、来自 `runtime.json` 的 `pythonDistributions`,以及 payload 声明了才有的 `node`、`nodePackages` 与 `pnpm`。重复调用返回同一对象。
#### Token 影响
每次调用几百个字符,以路径为主。
#### KV 缓存影响
作为工具结果追加进轮次历史;不增加提示词分区。
## 已知限制与暂缓事项
<a id="known-limitations-and-deferred-work"></a>
- Linux 目标需要 glibc;尚未锁定 musl payload。
- Windows 上原地使用的 payload 须在载体里已可执行;原地模式不修复权限。
<a id="dev-note"></a>
### 开发备注
<details>
<summary>维护者的工作上下文——点击展开</summary>
包归属与载体选择记录在[共享运行时 Agent Note](../../../.agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.zh.md) 中。
</details>
@@ -0,0 +1,44 @@
{
"name": "@deepseek-ai/dsh-tool-workspace-dependencies",
"description": "The load_workspace_dependencies tool: absolute paths into a bundled Python, Node.js, and pnpm payload",
"version": "0.1.6-alpha.2",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/skill/tool-workspace-dependencies"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts"
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/cordis-plugin-include": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
}
}
@@ -0,0 +1,277 @@
/** Model-facing query for a bundled Python, Node.js, and pnpm payload, in place or installed under the Harness home. */
import { cp, lstat, mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises'
import { dirname, isAbsolute, join } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
/** Cordis plugin identity. */
export const name = 'tool-workspace-dependencies'
/** Registry the tool registers into. */
export const inject = ['tools']
/** Payload location and optional installation directory. */
export interface Config {
/** Payload directory carrying `runtime.json` and `dependencies/`. */
readonly source: string
/**
* Installation directory under the Harness home. When set, the payload is copied there on the
* first call (the Desktop behavior); when omitted, the payload is used in place without copying,
* which suits read-only carriers such as container image layers.
*/
readonly root?: string
}
/** Require a named payload source before the Loader activates the tool. */
export const Config: z<Config> = z.object({
source: z.string().min(1).required(),
root: z.string().min(1),
})
/** Canonical build metadata, independent of user-installed packages; legacy files are normalized on read. */
export interface PrimaryRuntimeManifest {
readonly desktopVersion: string
readonly platform: string
readonly arch: string
/** Locked payload identity; absent only in installations made before payload hashing. */
readonly payloadDigest?: string
/** Bundled Python interpreter version. */
readonly python: string
/** Absent when the payload ships no Node.js. */
readonly node?: string
/** Bundled pnpm version; requires a bundled Node.js executable. */
readonly pnpm?: string
/** All recorded Python distribution versions; empty for legacy files without a distribution map. */
readonly pythonPackages: Readonly<Record<string, string>>
}
/** Absolute entry points and bundled versions; pnpm runs through the returned Node executable. */
export interface WorkspaceDependencies {
readonly python: string
readonly node?: string
readonly pnpm?: string
readonly pythonPackages: string
readonly nodePackages?: string
/** Locked distribution versions; excludes packages users add to the installed environment. */
readonly pythonDistributions: Readonly<Record<string, string>>
}
const VERSION = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/u
const PLATFORMS = ['win32', 'darwin', 'linux']
function isVersion(value: unknown): value is string {
return typeof value === 'string' && VERSION.test(value)
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isDistributionMap(value: unknown): value is Readonly<Record<string, string>> {
return isRecord(value) && Object.entries(value).every(([name, version]) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)
&& typeof version === 'string' && /^\d[\w.!+-]*$/u.test(version))
}
/**
* Validate payload JSON and normalize legacy component fields to top-level versions.
* @param value - Untrusted decoded runtime.json contents.
* @returns Canonical metadata with one Python distribution map and no components field.
* @throws For malformed or mixed formats, duplicate distribution names, or inconsistent legacy metadata.
*/
export function parsePrimaryRuntime(value: unknown): PrimaryRuntimeManifest {
if (!isRecord(value)) throw new Error('primary runtime: invalid metadata')
const legacy = value.components !== undefined
const versions = legacy ? value.components : value
if (!isRecord(versions) || (legacy && ['python', 'node', 'pnpm'].some(key => value[key] !== undefined))) {
throw new Error('primary runtime: invalid metadata')
}
const { desktopVersion, platform, arch, payloadDigest } = value
const { python, node, pnpm } = versions
const pythonPackages = value.pythonPackages === undefined && legacy ? {} : value.pythonPackages
if (typeof desktopVersion !== 'string' || desktopVersion.length === 0
|| typeof platform !== 'string' || !PLATFORMS.includes(platform)
|| typeof arch !== 'string' || !['x64', 'arm64'].includes(arch)
|| !isVersion(python)
|| (node !== undefined && !isVersion(node))
|| (pnpm !== undefined && (!isVersion(pnpm) || node === undefined))
|| (payloadDigest !== undefined && (typeof payloadDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(payloadDigest)))
|| !isDistributionMap(pythonPackages)) {
throw new Error('primary runtime: invalid metadata')
}
const entries = Object.entries(pythonPackages)
const distributions = new Map(entries.map(([name, version]) => [name.toLowerCase().replace(/[-_.]+/gu, '-'), version]))
if (distributions.size !== entries.length) throw new Error('primary runtime: invalid metadata')
// Only the old on-disk format duplicates these versions. New manifests use the distribution map alone.
if (legacy) {
for (const name of ['numpy', 'pandas'] as const) {
if (!isVersion(versions[name])) throw new Error('primary runtime: invalid metadata')
const version = distributions.get(name)
if (version !== undefined && version !== versions[name]) {
throw new Error(`primary runtime: conflicting ${name} distribution version`)
}
}
}
return {
desktopVersion, platform, arch,
...(payloadDigest === undefined ? {} : { payloadDigest }),
python,
...(node === undefined ? {} : { node }),
...(pnpm === undefined ? {} : { pnpm }),
pythonPackages,
}
}
/**
* Read and normalize build metadata without rewriting the source file.
* @param root - Installed or bundled primary runtime directory.
* @returns Validated top-level versions and target identifiers, including for legacy components manifests.
*/
export async function readPrimaryRuntime(root: string): Promise<PrimaryRuntimeManifest> {
const value: unknown = JSON.parse(await readFile(join(root, 'runtime.json'), 'utf8'))
return parsePrimaryRuntime(value)
}
/**
* Resolve platform-specific interpreter and library locations without changing the environment.
* @param root - Absolute payload or installation directory.
* @param manifest - Validated runtime metadata.
* @returns Absolute paths for explicit script execution and recorded bundled Python versions.
*/
export function workspaceDependencyPaths(root: string, manifest: PrimaryRuntimeManifest): WorkspaceDependencies {
const dependencies = join(root, 'dependencies')
const windows = manifest.platform === 'win32'
const node = manifest.node === undefined ? {} : {
node: join(dependencies, 'node', 'bin', windows ? 'node.exe' : 'node'),
nodePackages: join(dependencies, 'node', 'node_modules'),
}
const pnpm = manifest.pnpm === undefined ? {} : { pnpm: join(dependencies, 'pnpm', 'bin', 'pnpm.mjs') }
return {
python: join(dependencies, 'python', ...(windows ? ['python.exe'] : ['bin', 'python3'])),
...node,
...pnpm,
pythonPackages: join(dependencies, 'python', ...(windows ? ['Lib'] : ['lib', `python${manifest.python.split('.').slice(0, 2).join('.')}`]), 'site-packages'),
pythonDistributions: manifest.pythonPackages,
}
}
async function validatePayloadEntries(paths: WorkspaceDependencies): Promise<void> {
for (const [path, kind] of [
[paths.python, 'file'], [paths.node, 'file'], [paths.pnpm, 'file'],
[paths.pythonPackages, 'directory'], [paths.nodePackages, 'directory'],
] as const) {
if (path === undefined) continue
const entry = await stat(path)
if (!(kind === 'file' ? entry.isFile() : entry.isDirectory())) {
throw new Error(`primary runtime: expected ${kind} at ${path}`)
}
}
}
async function exists(path: string): Promise<boolean> {
try {
if ((await lstat(path)).isSymbolicLink()) throw new Error(`primary runtime: installation path is a filesystem link: ${path}`)
return true
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
throw error
}
}
async function compatibleManifest(source: string): Promise<PrimaryRuntimeManifest> {
const manifest = await readPrimaryRuntime(source)
if (manifest.platform !== process.platform || manifest.arch !== process.arch) throw new Error('primary runtime: incompatible platform or architecture')
return manifest
}
/**
* Use a payload where it lies: validate its metadata and entries, copying nothing.
* @param source - Payload directory, typically a read-only carrier.
* @returns Paths into the payload itself.
*/
export async function resolvePrimaryRuntime(source: string): Promise<WorkspaceDependencies> {
const paths = workspaceDependencyPaths(source, await compatibleManifest(source))
await validatePayloadEntries(paths)
return paths
}
/**
* Install the application-owned payload locally, retaining a complete previous tree on copy failure.
* @param source - Payload carried by the current installation.
* @param root - Fixed primary runtime directory under the Harness home.
* @returns Paths into the installed payload; no PATH or package-manager configuration is changed.
*/
export async function installPrimaryRuntime(source: string, root: string): Promise<WorkspaceDependencies> {
const manifest = await compatibleManifest(source)
await mkdir(dirname(root), { recursive: true })
const previous = `${root}.previous`
const previousExists = await exists(previous)
const rootExists = await exists(root)
if (!rootExists && previousExists) await rename(previous, root)
if (await exists(join(root, 'runtime.json')) && JSON.stringify(await readPrimaryRuntime(root)) === JSON.stringify(manifest)) {
const paths = workspaceDependencyPaths(root, manifest)
await validatePayloadEntries(paths)
return paths
}
const staging = await mkdtemp(join(dirname(root), '.primary-runtime-'))
try {
await cp(source, staging, { recursive: true, dereference: true })
const paths = workspaceDependencyPaths(staging, manifest)
await validatePayloadEntries(paths)
await rm(previous, { recursive: true, force: true })
const replacing = await exists(root)
if (replacing) await rename(root, previous)
try { await rename(staging, root) } catch (error) {
if (replacing) await rename(previous, root)
throw error
}
await rm(previous, { recursive: true, force: true })
} finally {
await rm(staging, { recursive: true, force: true })
}
return workspaceDependencyPaths(root, manifest)
}
/**
* Register the read-only path query; the first invocation prepares (or merely validates) the payload.
* @param ctx - Tool registry owner.
* @param config - Payload location and optional installation directory.
*/
export function apply(ctx: Context, config: Config): void {
if (!isAbsolute(config.source) || (config.root !== undefined && !isAbsolute(config.root))) {
throw new Error('workspace dependencies: source and root must be absolute paths')
}
let preparation: Promise<WorkspaceDependencies> | undefined
ctx.effect(() => async () => {
// Tool execution reports preparation failures; disposal only waits for filesystem work to settle.
await preparation?.catch(() => undefined)
})
ctx.tools.register(defineTool({
name: 'load_workspace_dependencies',
description: 'Get absolute paths to bundled Python and library directories, plus bundled Python distribution versions. Node.js and pnpm paths are included when the payload provides them. Python includes numpy, pandas, python-docx, python-pptx, openpyxl, Pillow, lxml, and XlsxWriter. Use these libraries for Office files unless the user or workspace instructions select another environment. When Node.js and pnpm paths are returned, run pnpm with that Node executable and pnpm script path. This does not change PATH or package-manager settings.',
parameters: {},
output: {
schema: {
type: 'object', additionalProperties: false,
properties: {
python: { type: 'string', required: true },
node: { type: 'string', description: 'Absent when the payload ships no Node.js.' },
pnpm: { type: 'string', description: 'Absent when the payload ships no pnpm.' },
pythonPackages: { type: 'string', required: true },
nodePackages: { type: 'string', description: 'Absent when the payload ships no Node.js.' },
pythonDistributions: { type: 'object', additionalProperties: true, required: true, description: 'Bundled distribution names and versions recorded in runtime.json; excludes user-installed additions.' },
},
},
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, undefined, 2) }],
},
execute: () => {
preparation ??= (config.root === undefined ? resolvePrimaryRuntime(config.source) : installPrimaryRuntime(config.source, config.root))
.catch((error: unknown) => {
preparation = undefined
throw error
})
return preparation
},
presentCall: () => ({ card: 'generic', title: 'Load workspace dependencies', kind: 'read' }),
}))
}
@@ -0,0 +1,427 @@
import * as fs from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import z from '@deepseek-ai/schemastery'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import * as workspaceDependencies from '../src/index.ts'
import { installPrimaryRuntime, parsePrimaryRuntime, readPrimaryRuntime, resolvePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../src/index.ts'
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof fs>()
return { ...actual, rename: vi.fn(actual.rename), lstat: vi.fn(actual.lstat), cp: vi.fn(actual.cp) }
})
const roots: string[] = []
afterEach(async () => {
vi.resetAllMocks()
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })))
})
async function fixture() {
const directory = await mkdtemp(join(tmpdir(), 'dsh-primary-runtime-'))
roots.push(directory)
const source = join(directory, 'resources')
const root = join(directory, 'home', 'dsh-runtimes', 'dsh-primary-runtime')
const manifest: PrimaryRuntimeManifest = {
desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
python: '3.12.14', node: '24.21.0', pnpm: '11.7.0',
pythonPackages: { 'python-docx': '1.2.0', 'python-pptx': '1.0.2', openpyxl: '3.1.5' },
}
const paths = workspaceDependencyPaths(source, manifest)
for (const path of [paths.python, paths.node, paths.pnpm]) {
if (path === undefined) continue
await mkdir(dirname(path), { recursive: true })
await writeFile(path, 'interpreter')
}
await mkdir(paths.pythonPackages, { recursive: true })
if (paths.nodePackages !== undefined) await mkdir(paths.nodePackages, { recursive: true })
await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
return { source, root, manifest, directory }
}
it.each(['win32', 'darwin', 'linux'])('returns %s interpreter and package paths', (platform) => {
const manifest: PrimaryRuntimeManifest = { desktopVersion: '1', platform, arch: 'x64', python: '3.12.14', node: '24.21.0', pnpm: '11.7.0', pythonPackages: {} }
const paths = workspaceDependencyPaths('/runtime', manifest)
expect(paths.pythonDistributions).toEqual({})
expect(paths.python).toBe(join('/runtime', 'dependencies', 'python', ...(platform === 'win32' ? ['python.exe'] : ['bin', 'python3'])))
expect(paths.pythonPackages).toBe(join('/runtime', 'dependencies', 'python', ...(platform === 'win32' ? ['Lib'] : ['lib', 'python3.12']), 'site-packages'))
})
it('installs offline, reuses the same release, and leaves environment and user packages unchanged', async () => {
const { source, root, manifest } = await fixture()
const environment = { ...process.env }
const installed = await installPrimaryRuntime(source, root)
await writeFile(join(installed.pythonPackages, 'user-package.py'), 'user content')
expect(await installPrimaryRuntime(source, root)).toEqual(installed)
expect(installed.pythonDistributions).toEqual(manifest.pythonPackages)
expect(await readFile(join(installed.pythonPackages, 'user-package.py'), 'utf8')).toBe('user content')
expect(process.env).toEqual(environment)
})
it('replaces release components and recovers an interrupted directory swap', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await rename(root, `${root}.previous`)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
await installPrimaryRuntime(source, root)
expect((await readPrimaryRuntime(root)).desktopVersion).toBe('2.0.0')
})
it('replaces dependencies when the locked payload changes without a Desktop version change', async () => {
const { source, root, manifest } = await fixture()
const first = { ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.1.2' } }
await writeFile(join(source, 'runtime.json'), JSON.stringify(first))
const installed = await installPrimaryRuntime(source, root)
await writeFile(join(installed.pythonPackages, 'old-package.py'), 'old dependency')
const next = { ...first, payloadDigest: 'b'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }
await writeFile(join(source, 'runtime.json'), JSON.stringify(next))
await writeFile(join(workspaceDependencyPaths(source, next).pythonPackages, 'new-package.py'), 'new dependency')
await installPrimaryRuntime(source, root)
expect(await readPrimaryRuntime(root)).toEqual(next)
expect(await readFile(join(installed.pythonPackages, 'new-package.py'), 'utf8')).toBe('new dependency')
await expect(readFile(join(installed.pythonPackages, 'old-package.py'))).rejects.toMatchObject({ code: 'ENOENT' })
})
it('upgrades a release manifest without a payload digest', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }))
await installPrimaryRuntime(source, root)
expect((await readPrimaryRuntime(root)).payloadDigest).toBe('a'.repeat(64))
})
it('replaces changed payload bytes when only the digest changes', async () => {
const { source, root, manifest } = await fixture()
const first = { ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }
const sourceFile = join(workspaceDependencyPaths(source, first).pythonPackages, 'library.py')
await writeFile(join(source, 'runtime.json'), JSON.stringify(first))
await writeFile(sourceFile, 'first wheel bytes')
const installed = await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...first, payloadDigest: 'b'.repeat(64) }))
await writeFile(sourceFile, 'repacked wheel bytes')
await installPrimaryRuntime(source, root)
expect(await readFile(join(installed.pythonPackages, 'library.py'), 'utf8')).toBe('repacked wheel bytes')
expect((await readPrimaryRuntime(root)).pythonPackages).toEqual(first.pythonPackages)
})
it('keeps the installed release when the replacement payload is incomplete', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
await rm(workspaceDependencyPaths(source, manifest).python)
await expect(installPrimaryRuntime(source, root)).rejects.toThrow()
expect((await readPrimaryRuntime(root)).desktopVersion).toBe('1.0.0')
})
it('refuses linked installation directories without modifying their targets', async () => {
const { source, root, directory } = await fixture()
const outside = join(directory, 'outside')
await mkdir(outside)
await writeFile(join(outside, 'keep'), 'untouched')
await mkdir(dirname(root), { recursive: true })
await symlink(outside, root, process.platform === 'win32' ? 'junction' : 'dir')
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('filesystem link')
expect(await readFile(join(outside, 'keep'), 'utf8')).toBe('untouched')
})
it.each(['in-place', 'copy', 'reuse'] as const)('rejects wrong payload entry types during %s preparation', async (mode) => {
for (const field of ['python', 'node', 'pnpm', 'pythonPackages', 'nodePackages'] as const) {
const { source, root, manifest } = await fixture()
if (mode === 'reuse') await installPrimaryRuntime(source, root)
const paths = workspaceDependencyPaths(mode === 'reuse' ? root : source, manifest)
const path = paths[field]!
const directory = field.endsWith('Packages')
await rm(path, { recursive: true })
if (directory) await writeFile(path, 'not a library directory')
else await mkdir(path)
const operation = mode === 'in-place' ? resolvePrimaryRuntime(source) : installPrimaryRuntime(source, root)
await expect(operation).rejects.toThrow(`expected ${directory ? 'directory' : 'file'} at`)
if (mode === 'copy') await expect(readFile(join(root, 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
}
})
it('rejects malformed metadata and incompatible targets', async () => {
const { source, root, manifest } = await fixture()
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, arch: process.arch === 'x64' ? 'arm64' : 'x64' }))
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('incompatible')
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, python: '../escape' }))
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
})
/** Previous Desktop files carry interpreter and selected library versions under components. */
function legacyManifest(manifest: PrimaryRuntimeManifest) {
const { python, node, pnpm, ...metadata } = manifest
return { ...metadata, components: { python, node, pnpm, numpy: '2.3.5', pandas: '3.0.1' } }
}
it.each([['numpy', 'numpy'], ['pandas', 'pandas'], ['Numpy', 'numpy'], ['PANDAS', 'pandas']] as const)(
'rejects conflicting %s versions in a legacy manifest', async (distribution, name) => {
const { source, manifest } = await fixture()
const legacy = legacyManifest(manifest)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...legacy, pythonPackages: { [distribution]: '0.0.1' } }))
await expect(readPrimaryRuntime(source)).rejects.toThrow(`conflicting ${name} distribution version`)
const pythonPackages = { [distribution]: legacy.components[name] }
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...legacy, pythonPackages }))
expect(await readPrimaryRuntime(source)).toEqual({ ...manifest, pythonPackages })
},
)
it.each([
{ payloadDigest: 'invalid' },
{ pythonPackages: ['python-docx'] },
{ pythonPackages: { 'python-docx': '../escape' } },
{ pythonPackages: { '../escape': '1.2.0' } },
{ pythonPackages: { numpy: '2.3.5', Numpy: '2.3.5' } },
{ pythonPackages: { Pillow: '12.3.0', pillow: '12.3.0' } },
{ pythonPackages: { typing_extensions: '4.16.0', 'typing.extensions': '4.16.0' } },
])('rejects invalid locked payload metadata: %j', async (invalid) => {
const { source, manifest } = await fixture()
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, ...invalid }))
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
})
it('loads the real tool through Cordis, exposes payload paths in place, and unregisters on disposal', async () => {
const { source, manifest, directory } = await fixture()
const ctx = new Context()
try {
ctx.baseUrl = pathToFileURL(directory).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
Object.assign(ctx.loader.builtins, {
agents: AgentRegistry, systemPrompt: SystemPrompt, tools: ToolRuntime, dependencies: workspaceDependencies,
})
const config = join(directory, 'cordis.yml')
await writeFile(config, `- name: cordis:agents\n- name: cordis:systemPrompt\n- name: cordis:tools\n- name: cordis:dependencies\n config:\n source: ${JSON.stringify(source)}\n`)
await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(config).href } })
await ctx.loader.await()
for (const entry of ctx.loader.entries()) await entry.fiber?.await()
const environment = { ...process.env }
const request = { signal: new AbortController().signal, name: 'load_workspace_dependencies', arguments: {} }
const results = await Promise.all(['first', 'second'].map(id => ctx.tools.execute({ ...request, callId: ToolCallId(id) })))
for (const result of results) {
expect(result.isError).toBe(false)
expect(result.content).toEqual([{ type: 'text', text: JSON.stringify(workspaceDependencyPaths(source, manifest), undefined, 2) }])
}
expect(process.env).toEqual(environment)
const entry = [...ctx.loader.entries()].find(entry => entry.options.name === 'cordis:dependencies')
expect(entry).toBeDefined()
await entry?.fiber?.dispose()
expect(ctx.tools.schemas().some(tool => tool.name === 'load_workspace_dependencies')).toBe(false)
} finally {
await ctx.fiber.dispose()
}
})
it('uses a payload in place without copying and tolerates a payload without Node.js or pnpm', async () => {
const { source, directory } = await fixture()
const manifest: PrimaryRuntimeManifest = {
desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
python: '3.12.14',
pythonPackages: { 'python-docx': '1.2.0' },
}
await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
const paths = await resolvePrimaryRuntime(source)
expect(paths).toEqual({
python: join(source, 'dependencies', 'python', ...(process.platform === 'win32' ? ['python.exe'] : ['bin', 'python3'])),
pythonPackages: join(source, 'dependencies', 'python', ...(process.platform === 'win32' ? ['Lib'] : ['lib', 'python3.12']), 'site-packages'),
pythonDistributions: { 'python-docx': '1.2.0' },
})
await expect(readFile(join(directory, 'home', 'dsh-runtimes', 'dsh-primary-runtime', 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
await rm(paths.python)
await expect(resolvePrimaryRuntime(source)).rejects.toMatchObject({ code: 'ENOENT' })
})
it('rejects a payload whose Node.js or pnpm version is malformed', async () => {
const { source, manifest } = await fixture()
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, node: 'latest' }))
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
})
/** Create the real scoped tool registry for lifecycle and failure-path assertions. */
async function tool(config: workspaceDependencies.Config) {
const ctx = new Context()
try {
await ctx.plugin(AgentRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
const fiber = ctx.plugin(workspaceDependencies, config)
await fiber
const execute = () => ctx.tools.execute({
signal: new AbortController().signal, name: 'load_workspace_dependencies',
arguments: {}, callId: ToolCallId('dependencies'),
})
return { ctx, fiber, execute }
} catch (error) {
await ctx.fiber.dispose()
throw error
}
}
it.each([null, [], { desktopVersion: '' }, { platform: 'unsupported' }, { arch: 'ia32' },
{ python: undefined }, { python: 3 }, { node: 24 }, { pnpm: 'latest' }, { payloadDigest: 1 },
{ pythonPackages: undefined }, { pythonPackages: null }, { pythonPackages: 3 }, { pythonPackages: { numpy: 3 } },
{ desktopVersion: 1 }, { platform: ['linux'] }, { arch: ['x64'] },
])('rejects malformed manifest fields: %j', async (invalid) => {
const { source, manifest } = await fixture()
const value = invalid === null || Array.isArray(invalid) ? invalid : { ...manifest, ...invalid }
await writeFile(join(source, 'runtime.json'), JSON.stringify(value))
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
})
it('reads legacy metadata without a distribution map and rejects incompatible platforms', async () => {
const { source, manifest } = await fixture()
const { pythonPackages: _packages, ...legacy } = legacyManifest(manifest)
await writeFile(join(source, 'runtime.json'), JSON.stringify(legacy))
expect((await resolvePrimaryRuntime(source)).pythonDistributions).toEqual({})
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, platform: process.platform === 'linux' ? 'darwin' : 'linux' }))
await expect(resolvePrimaryRuntime(source)).rejects.toThrow('incompatible')
})
it('propagates filesystem access failures without touching the installation', async () => {
const { source, root } = await fixture()
vi.mocked(fs.lstat).mockRejectedValueOnce(Object.assign(new Error('denied'), { code: 'EACCES' }))
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('denied')
await expect(readFile(join(root, 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
})
it.each([false, true])('preserves the previous installation if publication fails (previous: %s)', async (previous) => {
const { source, root, manifest } = await fixture()
if (previous) {
await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
}
const actual = await vi.importActual<typeof fs>('node:fs/promises')
vi.mocked(fs.rename).mockImplementationOnce(previous ? actual.rename : async () => { throw new Error('publication failed') })
if (previous) vi.mocked(fs.rename).mockRejectedValueOnce(new Error('publication failed'))
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('publication failed')
if (previous) expect((await readPrimaryRuntime(root)).desktopVersion).toBe('1.0.0')
else await expect(readFile(join(root, 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
expect((await fs.readdir(dirname(root))).filter(name => name.startsWith('.primary-runtime-'))).toEqual([])
})
it('retries a failed tool preparation and renders its call presentation', async () => {
const { source, root } = await fixture()
const metadata = await readFile(join(source, 'runtime.json'))
await rm(join(source, 'runtime.json'))
const { ctx, execute } = await tool({ source, root })
try {
expect((await execute()).isError).toBe(true)
await writeFile(join(source, 'runtime.json'), metadata)
expect((await execute()).isError).toBe(false)
expect(await readFile(join(root, 'runtime.json'))).toEqual(metadata)
const descriptor = ctx.tools.get('load_workspace_dependencies')!
expect(descriptor.presentCall?.({})).toEqual({ card: 'generic', title: 'Load workspace dependencies', kind: 'read' })
} finally { await ctx.fiber.dispose() }
})
it.each([false, true])('disposal waits for pending filesystem preparation (failure: %s)', async (failure) => {
const { source, root } = await fixture()
const actual = await vi.importActual<typeof fs>('node:fs/promises')
let release!: () => void
let copying!: () => void
const entered = new Promise<void>((resolve) => { copying = resolve })
const proceed = new Promise<void>((resolve) => { release = resolve })
vi.mocked(fs.cp).mockImplementationOnce(async (...args) => {
copying()
await proceed
if (failure) throw new Error('copy failed')
await actual.cp(...args)
})
const { ctx, fiber, execute } = await tool({ source, root })
const pending = execute()
try {
await entered
let disposed = false
const disposal = fiber.dispose().then(() => { disposed = true })
await Promise.resolve()
expect(disposed).toBe(false)
release()
await disposal
expect((await pending).isError).toBe(failure)
expect(ctx.tools.schemas().some(value => value.name === 'load_workspace_dependencies')).toBe(false)
} finally {
release()
await pending
await ctx.fiber.dispose()
}
})
it('disposes an unused tool without preparing the payload', async () => {
const { source, root } = await fixture()
const { ctx } = await tool({ source, root })
await ctx.fiber.dispose()
await expect(readFile(join(root, 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
})
it.each([{ source: 'relative' }, { source: process.cwd(), root: 'relative' }])('rejects relative runtime configuration at plugin load: %j', async (config) => {
await expect(tool(config)).rejects.toThrow('must be absolute paths')
})
it.each([{}, { source: '' }, { source: 1 }])('rejects invalid payload configuration with a named source error: %j', (config) => {
expect(() => z.resolve(config, workspaceDependencies.Config, {})).toThrow('source')
})
it('validates the previous directory even when a current installation exists', async () => {
const { source, root, directory } = await fixture()
await installPrimaryRuntime(source, root)
const outside = join(directory, 'outside-previous')
await mkdir(outside)
await writeFile(join(outside, 'keep'), 'untouched')
await symlink(outside, `${root}.previous`, process.platform === 'win32' ? 'junction' : 'dir')
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('filesystem link')
expect(await readFile(join(outside, 'keep'), 'utf8')).toBe('untouched')
})
it('normalizes legacy metadata without rewriting the source or leaking components', async () => {
const { source, manifest } = await fixture()
const stored = JSON.stringify(legacyManifest(manifest))
await writeFile(join(source, 'runtime.json'), stored)
expect(await readPrimaryRuntime(source)).toEqual(manifest)
expect(await readFile(join(source, 'runtime.json'), 'utf8')).toBe(stored)
expect(await resolvePrimaryRuntime(source)).toEqual(workspaceDependencyPaths(source, manifest))
})
it('reuses an equivalent legacy installation without replacing user-added packages', async () => {
const { source, root, manifest } = await fixture()
const stored = JSON.stringify(legacyManifest(manifest))
await writeFile(join(source, 'runtime.json'), stored)
const installed = await installPrimaryRuntime(source, root)
await writeFile(join(installed.pythonPackages, 'user-package.py'), 'keep')
await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
expect(await installPrimaryRuntime(source, root)).toEqual(installed)
expect(await readFile(join(root, 'runtime.json'), 'utf8')).toBe(stored)
expect(await readFile(join(installed.pythonPackages, 'user-package.py'), 'utf8')).toBe('keep')
})
it('accepts flat Python libraries without requiring or singling out numpy and pandas', async () => {
const { source, manifest } = await fixture()
const flat = { ...manifest, pythonPackages: { 'custom-library': '1.0.0' } }
await writeFile(join(source, 'runtime.json'), JSON.stringify(flat))
expect(await readPrimaryRuntime(source)).toEqual(flat)
expect((await resolvePrimaryRuntime(source)).pythonDistributions).toEqual(flat.pythonPackages)
})
it.each(['python', 'node', 'pnpm'])('rejects mixed legacy and flat %s fields', async (name) => {
const { manifest } = await fixture()
expect(() => parsePrimaryRuntime({ ...legacyManifest(manifest), [name]: '1.0.0' })).toThrow('invalid metadata')
})
it.each([null, [], 'invalid', { python: '3.12.14' }])('rejects malformed legacy components: %j', async (components) => {
const { manifest } = await fixture()
expect(() => parsePrimaryRuntime({ ...legacyManifest(manifest), components })).toThrow('invalid metadata')
})
it.each(['flat', 'legacy'])('rejects pnpm without a Node.js interpreter in %s metadata', async (format) => {
const { manifest } = await fixture()
const { node: _node, ...input } = manifest
expect(() => parsePrimaryRuntime(format === 'legacy' ? legacyManifest(input) : input)).toThrow('invalid metadata')
})
@@ -0,0 +1,24 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../core/tools"
}
]
}
+63 -8
View File
@@ -54,6 +54,9 @@ importers:
'@deepseek-ai/dsh-tool-session-query':
specifier: workspace:^
version: link:packages/session-query/tool-session-query
'@deepseek-ai/dsh-tool-workspace-dependencies':
specifier: workspace:^
version: link:packages/skill/tool-workspace-dependencies
'@deepseek-ai/dsh-web-fetch-http':
specifier: workspace:^
version: link:packages/web/web-fetch-http
@@ -96,9 +99,15 @@ importers:
execa:
specifier: ^10.0.0
version: 10.0.0
extract-zip:
specifier: ^2.0.1
version: 2.0.1([email protected])
fast-check:
specifier: ^4.8.0
version: 4.8.0
fflate:
specifier: ^0.8.2
version: 0.8.3
istanbul-lib-report:
specifier: ^3.0.1
version: 3.0.1
@@ -135,6 +144,9 @@ importers:
oxlint-tsgolint:
specifier: 7.0.2001
version: 7.0.2001
pnpm:
specifier: 11.7.0
version: 11.7.0
publint:
specifier: ^0.3.21
version: 0.3.21
@@ -144,6 +156,9 @@ importers:
spdx-expression-parse:
specifier: ^5.0.0
version: 5.0.0
tar:
specifier: ^7.5.0
version: 7.5.22
tsdown:
specifier: ^0.22.2
version: 0.22.2([email protected])([email protected])([email protected])([email protected])
@@ -294,6 +309,9 @@ importers:
'@deepseek-ai/dsh-skill-filesystem':
specifier: workspace:^
version: link:../../packages/skill/skill-filesystem
'@deepseek-ai/dsh-skill-office':
specifier: workspace:^
version: link:../../packages/skill/skill-office
'@deepseek-ai/dsh-terminal':
specifier: workspace:^
version: link:../../packages/terminal/terminal
@@ -366,6 +384,9 @@ importers:
'@deepseek-ai/dsh-tool-workflow':
specifier: workspace:^
version: link:../../packages/workflow/tool-workflow
'@deepseek-ai/dsh-tool-workspace-dependencies':
specifier: workspace:^
version: link:../../packages/skill/tool-workspace-dependencies
'@deepseek-ai/dsh-web-app':
specifier: workspace:^
version: link:../../packages/bundle/web-app
@@ -572,9 +593,6 @@ importers:
extract-zip:
specifier: ^2.0.1
version: 2.0.1([email protected])
fflate:
specifier: ^0.8.2
version: 0.8.3
js-yaml:
specifier: ^4.2.0
version: 4.3.1
@@ -584,9 +602,6 @@ importers:
pnpm:
specifier: 11.7.0
version: 11.7.0
tar:
specifier: ^7.5.0
version: 7.5.22
typescript:
specifier: ^6.0.3
version: 6.0.3
@@ -620,9 +635,9 @@ importers:
'@deepseek-ai/dsh-skill-office':
specifier: workspace:^
version: link:../../packages/skill/skill-office
'@deepseek-ai/dsh-tools':
'@deepseek-ai/dsh-tool-workspace-dependencies':
specifier: workspace:^
version: link:../../packages/core/tools
version: link:../../packages/skill/tool-workspace-dependencies
apps/web:
devDependencies:
@@ -1948,6 +1963,12 @@ importers:
'@deepseek-ai/dsh-sdk-jsonrpc-server':
specifier: workspace:^
version: link:../../sdk/server
'@deepseek-ai/dsh-skill-office':
specifier: workspace:^
version: link:../../skill/skill-office
'@deepseek-ai/dsh-tool-workspace-dependencies':
specifier: workspace:^
version: link:../../skill/tool-workspace-dependencies
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
@@ -10375,6 +10396,34 @@ importers:
specifier: workspace:^
version: link:../../core/tools
packages/skill/tool-workspace-dependencies:
dependencies:
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/cordis-plugin-include':
specifier: workspace:^
version: link:../../../vendor/include
'@deepseek-ai/cordis-plugin-loader':
specifier: workspace:^
version: link:../../../vendor/loader
'@deepseek-ai/dsh-agent':
specifier: workspace:^
version: link:../../core/agent
'@deepseek-ai/dsh-llm':
specifier: workspace:^
version: link:../../llm/llm
'@deepseek-ai/dsh-system-prompt':
specifier: workspace:^
version: link:../../core/system-prompt
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../core/tools
packages/spill/spill:
devDependencies:
'@deepseek-ai/cordis':
@@ -12639,6 +12688,9 @@ importers:
'@deepseek-ai/dsh-skill-filesystem':
specifier: workspace:^
version: link:../../packages/skill/skill-filesystem
'@deepseek-ai/dsh-skill-office':
specifier: workspace:^
version: link:../../packages/skill/skill-office
'@deepseek-ai/dsh-spill':
specifier: workspace:^
version: link:../../packages/spill/spill
@@ -12735,6 +12787,9 @@ importers:
'@deepseek-ai/dsh-tool-workflow':
specifier: workspace:^
version: link:../../packages/workflow/tool-workflow
'@deepseek-ai/dsh-tool-workspace-dependencies':
specifier: workspace:^
version: link:../../packages/skill/tool-workspace-dependencies
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../packages/core/tools
+2 -2
View File
@@ -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: b2fb0b29c44db0e9e251ce0227e334e08a14e0f1
README.zh.md: 0f13c0a60c7378cb897ccfa7b7082bb86d722a07
README.md: 3a27ef586cce3290b13b9cd4517ff5a491f312c2
README.zh.md: 85bf35f62aa60bdb84e612518e2af740f1e8c708
+13
View File
@@ -12,6 +12,19 @@ Production executables are named `deepseek-harness-sdk-runtime-<platform>-<arch>
Each target also requires `<executable-stem>-office/`, where the stem excludes `.exe`. This directory contains the complete installed Office packages and their dependencies, preserving engine resources, manifests, licenses, source inventories, and helper permissions. Copy this directory together with the executable. A missing target engine fails the sidecar build with its npm package name and target platform/architecture.
Each wheel also includes `<platform>-<arch>/primary-runtime/` (CPython and the locked Office Python libraries) and sibling `office-skills/` (three default workflows and their shared checker). These are ordinary relocatable files, not bytes embedded in the executable. The shared builder selects target-native archives for all five wheel targets and executes its smoke on the native build host. Packaging and installed-runtime lookup reject missing resources, wrong-platform metadata, and lost Python executable permissions. The short platform directory avoids repeating the executable name in Python DLL paths on Windows.
The packaged bootstrap supplies `DSH_BUNDLED_PRIMARY_RUNTIME` as a carrier default. The `sdk` profile uses it when `DSH_PRIMARY_RUNTIME` is unset; an explicit path overrides it, and an empty string disables the query and Office provider. External payloads retain the `primary-runtime/` plus sibling `office-skills/` layout. The SDK reads Python in place without copying it into `DSH_HOME`. Source and dev-only Node carriers have no bundled default; they use an explicit `DSH_PRIMARY_RUNTIME`.
Skill selection is independent of delivery: project, custom-directory, and user filesystem skills override same-name bundled skills. An SDK patch can disable only the Office provider while retaining the Python query:
```yaml
- id: skill-office
disabled: true
```
To replace the three Office workflows and shared checker as a set, patch `skill-office.config.assetRoot` to another absolute resource directory. Use the filesystem skill provider for arbitrary skill collections. Configuration patches apply at process startup; changing skills does not require rebuilding the runtime wheel or Python environment.
Repository builds also materialize a dev-only `runtime/node/` carrier. It runs `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. It is never selected automatically and is excluded from wheels and sdists.
Both carriers execute the same `dsh` grammar and shipped profiles, including the standalone `sdk-minimal` tree and the full `web` profile with its frontend assets. The private `dsh-python-runtime-closure` manifest defines the packaged dependency closure; there is no Python-specific Node application or checked-in default `cordis.yml`.
+13
View File
@@ -12,6 +12,19 @@ wheel 包会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python
每个目标还要求 `<executable-stem>-office/`,其中 stem 不含 `.exe`。该目录包含完整的已安装 Office 包及其依赖,保留引擎资源、清单、许可证、源码清单与辅助程序权限。复制可执行文件时必须一并复制此目录。缺少目标引擎会使 sidecar 构建失败,错误会指出其 npm 包名与目标平台/架构。
每个 wheel 还包含 `<platform>-<arch>/primary-runtime/`(CPython 与锁定版本的 Office Python 库)及同级 `office-skills/`(三个默认工作流与共用检查脚本)。它们是可重定位的普通文件,不嵌入可执行文件。共享构建器为全部五个 wheel 目标选择原生归档,并在对应构建主机上执行冒烟检查。打包与已安装运行时定位会拒绝缺失资源、平台不符的元数据及 Python 执行权限丢失。 较短的平台目录避免在 Windows Python DLL 路径中重复可执行文件名称。
打包启动器通过 `DSH_BUNDLED_PRIMARY_RUNTIME` 提供载体默认路径。`sdk` profile 在未设置 `DSH_PRIMARY_RUNTIME` 时使用该路径;显式路径覆盖它,空字符串禁用查询与 Office 提供方。外部 payload 保持 `primary-runtime/` 与同级 `office-skills/` 布局。SDK 原位读取 Python,不复制到 `DSH_HOME`。源码与仅供开发的 Node 载体没有随包默认路径,需显式设置 `DSH_PRIMARY_RUNTIME`。
选择 skills 与交付资源相互独立:项目、自定义目录和用户文件系统 skills 优先于同名随包 skills。SDK patch 可以仅禁用 Office 提供方,同时保留 Python 查询:
```yaml
- id: skill-office
disabled: true
```
要成套替换三个 Office 工作流与共用检查脚本,可将 `skill-office.config.assetRoot` 配置为另一个绝对资源目录。任意 skill 集合通过文件系统 skill 提供方加载。配置 patch 在进程启动时生效;切换 skills 无需重建 runtime wheel 或 Python 环境。
仓库构建还会物化仅限开发的 `runtime/node/` 载体。它在系统 Node 22.19 或更高版本上运行 `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。系统不会自动选择它,而且 wheel 包与 sdist 均不包含它。
两种载体执行相同的 `dsh` 语法与随附 profile,包括独立的 `sdk-minimal` 配置树,以及包含前端产物的完整 `web` profile。私有 `dsh-python-runtime-closure` manifest(元数据清单)定义打包依赖闭包;不存在 Python 专用 Node 应用或检入的默认 `cordis.yml`。
+8 -1
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
import os
import platform
import runpy
import stat
from pathlib import Path
@@ -76,7 +77,7 @@ class RuntimeBuildHook(BuildHookInterface):
target = next(name for name, value in _PLATFORMS.items() if value[0] == platform_tag)
runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
runtime_files = sorted(
runtime_dir.glob("deepseek-harness-sdk-runtime-*") if runtime_dir.is_dir() else []
(path for path in runtime_dir.iterdir() if path.name != "node") if runtime_dir.is_dir() else []
)
expected_files = (
[expected_executable, f"{expected_executable.removesuffix('.exe')}-rg.exe"]
@@ -87,6 +88,8 @@ class RuntimeBuildHook(BuildHookInterface):
expected_files.append(f"{expected_executable}-spawn-helper")
office = runtime_dir / f"{expected_executable.removesuffix('.exe')}-office"
expected_files.append(office.name)
resources = runtime_dir / target
expected_files.append(resources.name)
expected_files.sort()
found_files = [path.name for path in runtime_files]
if found_files != expected_files:
@@ -94,6 +97,10 @@ class RuntimeBuildHook(BuildHookInterface):
f"runtime wheel {platform_tag} payload must be {expected_files}; found {found_files}"
)
for executable in runtime_files:
if executable == resources:
validate = runpy.run_path(str(runtime_dir.parent / "_resources.py"))["validate_resources"]
validate(resources, target)
continue
if executable == office:
adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
if not adapter.is_file():
+2
View File
@@ -86,6 +86,8 @@
"@deepseek-ai/dsh-shell-env": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
"@deepseek-ai/dsh-skill-office": "workspace:^",
"@deepseek-ai/dsh-tool-workspace-dependencies": "workspace:^",
"@deepseek-ai/dsh-spill": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-acp": "workspace:^",
+7 -1
View File
@@ -22,7 +22,13 @@ dsh = "deepseek_harness_runtime:main"
# Include the injected dsh executable and sidecars; exclude the dev-only node closure.
[tool.hatch.build]
artifacts = ["src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*", "src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*/**"]
artifacts = [
"src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*",
"src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*/**",
"src/deepseek_harness_runtime/runtime/linux-*/**",
"src/deepseek_harness_runtime/runtime/macos-*/**",
"src/deepseek_harness_runtime/runtime/win-*/**",
]
exclude = ["src/deepseek_harness_runtime/runtime/node"]
[tool.hatch.build.targets.wheel]
+6
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
/** Private entry owned by the Python single-file runtime packaging. */
import { registerHooks } from 'node:module'
import { dirname, join } from 'node:path'
import { isSea } from 'node:sea'
import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -28,6 +29,11 @@ if (aclRunner !== undefined && process.argv[2] === aclRunner) {
Reflect.deleteProperty(process.env, 'DSH_PTC_RUNTIME_NODE')
await import('@deepseek-ai/dsh-ptc-runtime-node/process')
} else if (selection === undefined) {
if (isSea()) {
// Carrier default stays separate so process/home environment and profile patches can override it.
const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win' : process.platform
process.env.DSH_BUNDLED_PRIMARY_RUNTIME = join(dirname(process.execPath), `${platform}-${process.arch}`, 'primary-runtime')
}
const { runCli } = await import('@deepseek-ai/dsh/lib/bin.js')
await runCli()
} else {
@@ -28,6 +28,7 @@ import shutil
import subprocess
import sys
from pathlib import Path
from ._resources import validate_resources
PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json"
@@ -59,7 +60,9 @@ def bundled_runtime_path() -> Path:
Raises FileNotFoundError when the platform is unsupported, the executable
has not been placed into this package, the ripgrep or Office sidecar is
missing, or the required macOS spawn helper is missing; the message names
missing, the authoring resources are incomplete, or the required macOS
spawn helper is missing. Invalid authoring metadata or Python permissions
raise ValueError. Missing executable/Office messages name
the acquisition routes (acquisition strategy is deliberately separate from
this lookup interface, so an on-demand download can replace it without
touching callers).
@@ -104,6 +107,7 @@ def bundled_runtime_path() -> Path:
f"deepseek-harness-runtime-bin is missing the Office sidecar engine {engine} at {office}. "
+ _EXE_ACQUISITION_HINT
)
validate_resources(path.with_name(tag), tag)
return path
@@ -0,0 +1,41 @@
"""Validate the authoring resources in staged, installed, or archived runtime wheels."""
from __future__ import annotations
import json
import re
import stat
import zipfile
from pathlib import Path
def validate_resources(root: Path | zipfile.Path, target: str) -> None:
"""Reject a missing or wrong-target Python environment or bundled Office skill tree."""
manifest_path = root / "primary-runtime/runtime.json"
if not manifest_path.is_file():
raise FileNotFoundError(f"runtime authoring resources are missing: {manifest_path}")
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
platform, arch = target.rsplit("-", 1)
platform = {"macos": "darwin", "win": "win32"}.get(platform, platform)
if not isinstance(manifest, dict) or (manifest.get("platform"), manifest.get("arch")) != (platform, arch):
raise ValueError(f"runtime authoring resources do not match target {target}")
version = manifest.get("python")
if not isinstance(version, str) or not re.fullmatch(r"\d+\.\d+\.\d+", version):
raise ValueError("runtime authoring resources have invalid Python version metadata")
if not isinstance(manifest.get("pythonPackages"), dict) or not manifest["pythonPackages"]:
raise ValueError("runtime authoring resources have no Python distributions")
python_root = root / "primary-runtime/dependencies/python"
executable = python_root / ("python.exe" if platform == "win32" else "bin/python3")
packages = python_root / ("Lib/site-packages" if platform == "win32" else f"lib/python{version.rsplit('.', 1)[0]}/site-packages")
required = [executable, root / "office-skills/scripts/check_office.py"]
required.extend(root / f"office-skills/office-{kind}/SKILL.md" for kind in ("docx", "pptx", "xlsx"))
for path in required:
if not path.is_file():
raise FileNotFoundError(f"runtime authoring resource is missing: {path}")
if not packages.is_dir():
raise FileNotFoundError(f"runtime Python site-packages is missing: {packages}")
if platform != "win32":
mode = (executable.root.getinfo(executable.at).external_attr >> 16
if isinstance(executable, zipfile.Path) else executable.stat().st_mode)
if mode & stat.S_IXUSR == 0:
raise ValueError(f"runtime Python lost its executable bit: {executable}")
+30
View File
@@ -26,6 +26,18 @@ def test_release_tag_is_optional_for_non_release_builds() -> None:
build_python_release.validate_release_tag(None, "1.2.3")
def test_wheel_verification_uses_distribution_metadata_not_nested_libraries(tmp_path: Path) -> None:
wheel = tmp_path / "sdk.whl"
with zipfile.ZipFile(wheel, "w") as archive:
archive.writestr("nested/library.dist-info/WHEEL", "Tag: cp312-cp312-linux_x86_64\n")
archive.writestr("nested/library.dist-info/METADATA", "Name: library\nVersion: 0.0.1\n")
archive.writestr("deepseek_harness_sdk-1.2.3.dist-info/WHEEL", "Tag: py3-none-any\n")
archive.writestr("deepseek_harness_sdk-1.2.3.dist-info/METADATA",
"Name: deepseek-harness-sdk\nVersion: 1.2.3\nLicense-Expression: MIT\n"
"License-File: LICENSE\nRequires-Dist: deepseek-harness-runtime-bin==1.2.3\n")
build_python_release.verify_wheel(wheel, "sdk", "1.2.3", None)
def test_release_tag_must_match_repository_version() -> None:
build_python_release.validate_release_tag("python-v1.2.3", "1.2.3")
@@ -96,6 +108,19 @@ def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path:
assert (destination / "src" / "deepseek_harness" / "__init__.py").is_file()
def test_copy_package_omits_generated_carriers_before_staging_one_target(tmp_path: Path) -> None:
source = tmp_path / "source"
module = source / "src/deepseek_harness_runtime"
for path in ("runtime/macos-arm64/primary-runtime/runtime.json", "runtime/node/package.json",
"runtime/deepseek-harness-sdk-runtime-win-x64.exe", "__init__.py", "_resources.py"):
file = module / path
file.parent.mkdir(parents=True, exist_ok=True)
file.touch()
destination = tmp_path / "staged"
build_python_release.copy_package(source, destination)
assert sorted(path.name for path in (destination / "src/deepseek_harness_runtime").iterdir()) == ["__init__.py", "_resources.py"]
@pytest.mark.parametrize(
("target", "with_helper"),
[("linux-x64", False), ("macos-arm64", True), ("macos-x64", True), ("win-x64.exe", False)],
@@ -124,6 +149,10 @@ def test_stage_runtime_copies_platform_payload(
office_asset = office / "node_modules" / "@deepseek-ai" / "libreoffice-kit-wasm" / "assets" / "soffice.data"
office_asset.parent.mkdir(parents=True)
office_asset.write_bytes(b"office data")
resources = executable.with_name(executable.name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe"))
resource = resources / "office-skills/scripts/check_office.py"
resource.parent.mkdir(parents=True)
resource.write_text("checker")
destination = tmp_path / "staging"
build_python_release.stage_runtime(destination, "1.2.3", executable, executable.name)
@@ -135,6 +164,7 @@ def test_stage_runtime_copies_platform_payload(
if path.is_file()
} == expected
assert (runtime_dir / office.name / office_asset.relative_to(office)).read_bytes() == b"office data"
assert (runtime_dir / resources.name / resource.relative_to(resources)).read_text() == "checker"
pyproject = (destination / "pyproject.toml").read_text()
assert 'license = "MIT"' in pyproject
assert 'license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]' in pyproject
+60 -6
View File
@@ -6,11 +6,13 @@ import json
import os
import subprocess
import sys
import zipfile
from pathlib import Path
from types import SimpleNamespace
import deepseek_harness_runtime as runtime
import pytest
from deepseek_harness_runtime._resources import validate_resources
from deepseek_harness_runtime import (
RUNTIME_MODE_ENV_VAR,
@@ -20,7 +22,7 @@ from deepseek_harness_runtime import (
)
def _office_sidecar(executable: Path, native_targets: tuple[str, ...] = ("darwin-arm64", "darwin-x64", "win32-x64")) -> Path:
def _resource_sidecars(executable: Path, native_targets: tuple[str, ...] = ("darwin-arm64", "darwin-x64", "win32-x64")) -> Path:
office = executable.with_name(f"{executable.name.removesuffix('.exe')}-office")
tag = executable.name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe")
native = tag.replace("win-", "win32-").replace("macos-", "darwin-")
@@ -33,6 +35,21 @@ def _office_sidecar(executable: Path, native_targets: tuple[str, ...] = ("darwin
adapter.write_text(json.dumps({"optionalDependencies": {
f"@deepseek-ai/libreoffice-kit-{target}": "0.0.1" for target in (*native_targets, "wasm")
}}), encoding="utf-8")
resources = executable.with_name(tag)
manifest = resources / "primary-runtime/runtime.json"
manifest.parent.mkdir(parents=True)
manifest.write_text(json.dumps({"platform": native.rsplit("-", 1)[0], "arch": tag.rsplit("-", 1)[1],
"python": "3.12.14", "pythonPackages": {"numpy": "2.3.5"}}))
python = resources / "primary-runtime/dependencies/python" / ("python.exe" if tag.startswith("win-") else "bin/python3")
python.parent.mkdir(parents=True)
python.touch()
python.chmod(0o755)
packages = python.parent if tag.startswith("win-") else python.parent.parent
(packages / ("Lib/site-packages" if tag.startswith("win-") else "lib/python3.12/site-packages")).mkdir(parents=True)
for file in ["scripts/check_office.py", *(f"office-{kind}/SKILL.md" for kind in ("docx", "pptx", "xlsx"))]:
path = resources / "office-skills" / file
path.parent.mkdir(parents=True, exist_ok=True)
path.touch()
return office
@@ -41,6 +58,43 @@ def test_unknown_explicit_mode_fails_loud() -> None:
resolve_bundled_launch_args("bogus")
@pytest.mark.parametrize("target", ["linux-x64", "linux-arm64", "macos-arm64", "macos-x64", "win-x64"])
@pytest.mark.parametrize("invalid", [None, "manifest", "platform", "python", "skills", "packages", "mode"])
def test_authoring_resources_validate_installed_and_wheel_payloads(tmp_path: Path, target: str, invalid: str | None) -> None:
executable = tmp_path / f"deepseek-harness-sdk-runtime-{target}"
_resource_sidecars(executable)
root = tmp_path / target
python = root / "primary-runtime/dependencies/python" / ("python.exe" if target == "win-x64" else "bin/python3")
if invalid == "mode" and target == "win-x64":
pytest.skip("Windows executables do not require a POSIX executable bit")
if invalid == "manifest":
(root / "primary-runtime/runtime.json").unlink()
elif invalid == "platform":
manifest_path = root / "primary-runtime/runtime.json"
manifest = json.loads(manifest_path.read_text())
manifest["arch"] = "wrong"
manifest_path.write_text(json.dumps(manifest))
elif invalid == "python":
python.unlink()
elif invalid == "skills":
(root / "office-skills/office-docx/SKILL.md").unlink()
elif invalid == "packages":
next(root.rglob("site-packages")).rmdir()
elif invalid == "mode":
python.chmod(0o644)
archive_path = tmp_path / "payload.zip"
with zipfile.ZipFile(archive_path, "w") as archive:
for path in root.rglob("*"):
archive.write(path, str(path.relative_to(root)).replace("\\", "/"))
with zipfile.ZipFile(archive_path) as archive:
for candidate in (root, zipfile.Path(archive)):
if invalid is None:
validate_resources(candidate, target)
else:
with pytest.raises((ValueError, FileNotFoundError)):
validate_resources(candidate, target)
def test_unknown_env_mode_fails_loud(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(RUNTIME_MODE_ENV_VAR, "bogus")
with pytest.raises(ValueError, match="expected 'exe' or 'node'"):
@@ -64,7 +118,7 @@ def test_runtime_requires_spawn_helper_only_on_macos(
linux = runtime_dir / "deepseek-harness-sdk-runtime-linux-x64"
linux.touch()
Path(f"{linux}-rg").touch()
_office_sidecar(linux)
_resource_sidecars(linux)
macos = runtime_dir / "deepseek-harness-sdk-runtime-macos-arm64"
macos.touch()
Path(f"{macos}-rg").touch()
@@ -85,7 +139,7 @@ def test_windows_runtime_uses_exe_payload_and_exe_sidecar(
executable = runtime_dir / "deepseek-harness-sdk-runtime-win-x64.exe"
executable.touch()
(runtime_dir / "deepseek-harness-sdk-runtime-win-x64-rg.exe").touch()
_office_sidecar(executable)
_resource_sidecars(executable)
monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "win-x64")
@@ -122,7 +176,7 @@ def test_runtime_requires_ripgrep_sidecar(
runtime.bundled_runtime_path()
def test_runtime_requires_complete_office_sidecar(
def test_runtime_requires_complete_resource_sidecars(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
executable = tmp_path / "runtime" / "deepseek-harness-sdk-runtime-linux-x64"
@@ -134,7 +188,7 @@ def test_runtime_requires_complete_office_sidecar(
with pytest.raises(FileNotFoundError, match="Office sidecar"):
runtime.bundled_runtime_path()
office = _office_sidecar(executable)
office = _resource_sidecars(executable)
assert runtime.bundled_runtime_path() == executable
(office / "node_modules/@deepseek-ai/libreoffice-kit-wasm/prebuilds.json").unlink()
with pytest.raises(FileNotFoundError, match="Office sidecar"):
@@ -250,7 +304,7 @@ def test_runtime_requires_its_platform_office_engine(
executable.with_name(f"{executable.stem}-rg{extension}").touch()
if target.startswith("macos-"):
Path(f"{executable}-spawn-helper").touch()
office = _office_sidecar(executable, native_targets)
office = _resource_sidecars(executable, native_targets)
monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: target)
assert runtime.bundled_runtime_path() == executable
+2
View File
@@ -101,6 +101,8 @@ describe('Python runtime executable builder CLI', () => {
expect(result.status).toBe(0)
expect(result.stdout).toContain('exec pkg')
expect(result.stdout).toContain('--sea --targets node24-macos-x64')
expect(result.stdout).toContain('prepare Python and Office skills for mac-x64')
expect(result.stdout).toContain(join(root, 'dist-exe', 'macos-x64'))
})
it('rejects a Windows arm64 product before any build step', () => {
+16 -4
View File
@@ -9,10 +9,12 @@
import { spawn } from 'node:child_process'
import { existsSync, statSync } from 'node:fs'
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, extname, join, resolve, sep } from 'node:path'
import { parseArgs } from 'node:util'
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
import { copyOfficeSidecar, OFFICE_ASSET_IGNORES } from './build-exe-for-python-sdk-office.ts'
import { preparePrimaryRuntime, smokePrimaryRuntime, type PrimaryRuntimeTarget } from './primary-runtime/prepare.ts'
const root = resolve(import.meta.dirname, '..')
@@ -419,7 +421,7 @@ class SingleExeBuild {
/**
* Package one target; SEA mode accepts one target per invocation.
* @param target - the pkg target triple to build.
* @returns the executable, Office directory, ripgrep, and required macOS spawn helper paths.
* @returns the executable, resource directories, ripgrep, and required macOS spawn helper paths.
*/
async pack(target: Target): Promise<string[]> {
const productBase = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
@@ -448,7 +450,17 @@ class SingleExeBuild {
console.log(`build-exe-for-python-sdk: copied ${packages.length} Office packages to ${office}`)
}
const ripgrep = await this.copyRipgrepSidecar(target, product)
if (target.platform !== 'macos') return [product, ripgrep, office]
const resources = join(this.outDir, `${target.platform}-${target.arch}`)
const runtimeTarget = `${target.platform === 'macos' ? 'mac' : target.platform}-${target.arch}` as PrimaryRuntimeTarget
if (this.cli.dryRun) {
console.log(`build-exe-for-python-sdk: [dry-run] prepare Python and Office skills for ${runtimeTarget} in ${resources}`)
} else {
const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { version: string }
await preparePrimaryRuntime({ target: runtimeTarget, output: resources,
cache: join(tmpdir(), 'dsh-primary-runtime-downloads'), version, pythonOnly: true })
smokePrimaryRuntime(join(resources, 'primary-runtime'))
}
if (target.platform !== 'macos') return [product, ripgrep, office, resources]
const spawnHelper = `${product}-spawn-helper`
const source = join(this.staging, 'node_modules', 'node-pty', 'prebuilds', `darwin-${target.arch}`, 'spawn-helper')
if (this.cli.dryRun) {
@@ -457,7 +469,7 @@ class SingleExeBuild {
await copyFile(source, spawnHelper)
await chmod(spawnHelper, 0o755)
}
return [product, ripgrep, spawnHelper, office]
return [product, ripgrep, spawnHelper, office, resources]
}
/** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
@@ -548,7 +560,7 @@ class SingleExeBuild {
continue
}
if (statSync(path).isDirectory()) {
console.log(` ${path} (Office dependency directory)`)
console.log(` ${path} (resource directory)`)
continue
}
const megabytes = statSync(path).size / (1024 * 1024)
+25 -14
View File
@@ -8,6 +8,7 @@ import email
import json
import os
import re
import runpy
import shutil
import stat
import subprocess
@@ -148,18 +149,21 @@ def validate_release_tag(tag: str | None, version: str) -> None:
def copy_package(source: Path, destination: Path) -> None:
generated_parent = source / "src" / "deepseek_harness_runtime"
ignore_files = shutil.ignore_patterns(
".venv", ".pytest_cache", "__pycache__", "*.pyc", "dist", "node_modules",
)
def ignore(directory: str, names: list[str]) -> set[str]:
ignored = ignore_files(directory, names)
if Path(directory) == generated_parent:
ignored.add("runtime")
return ignored
shutil.copytree(
source,
destination,
ignore=shutil.ignore_patterns(
".venv",
".pytest_cache",
"__pycache__",
"*.pyc",
"dist",
"node_modules",
"deepseek-harness-sdk-runtime-*",
),
ignore=ignore,
)
@@ -227,6 +231,8 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_
shutil.copy2(source_directory / filename, runtime_dir / filename)
office = office_sidecar_name(executable_name)
shutil.copytree(source_directory / office, runtime_dir / office)
resources = executable_name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe")
shutil.copytree(source_directory / resources, runtime_dir / resources)
def verify_office_payload(archive: zipfile.ZipFile, office_modules: str, platform_tag: str) -> None:
@@ -272,16 +278,17 @@ def verify_wheel(
platform: tuple[str, str] | None,
) -> None:
expected_tag = "py3-none-any" if platform is None else f"py3-none-{platform[0]}"
expected_distribution = SDK_DISTRIBUTION if package == "sdk" else RUNTIME_DISTRIBUTION
dist_info = f"{expected_distribution.replace('-', '_')}-{version}.dist-info"
with zipfile.ZipFile(wheel) as archive:
wheel_metadata_path = next(name for name in archive.namelist() if name.endswith(".dist-info/WHEEL"))
metadata_path = next(name for name in archive.namelist() if name.endswith(".dist-info/METADATA"))
wheel_metadata_path = f"{dist_info}/WHEEL"
metadata_path = f"{dist_info}/METADATA"
wheel_metadata = email.message_from_bytes(archive.read(wheel_metadata_path))
metadata = email.message_from_bytes(archive.read(metadata_path))
if wheel_metadata.get_all("Tag") != [expected_tag]:
raise RuntimeError(f"{wheel} has wrong WHEEL tags: {wheel_metadata.get_all('Tag')}")
if metadata.get("Version") != version:
raise RuntimeError(f"{wheel} has version {metadata.get('Version')}, expected {version}")
expected_distribution = SDK_DISTRIBUTION if package == "sdk" else RUNTIME_DISTRIBUTION
if metadata.get("Name") != expected_distribution:
raise RuntimeError(
f"{wheel} has distribution name {metadata.get('Name')}, expected {expected_distribution}"
@@ -297,17 +304,21 @@ def verify_wheel(
f"{wheel} has license files {license_files}, expected {expected_license_files}"
)
runtime_payload = [
name for name in archive.namelist() if "/runtime/deepseek-harness-sdk-runtime-" in name
name for name in archive.namelist() if name.startswith("deepseek_harness_runtime/runtime/")
]
if package == "runtime":
assert platform is not None
office = office_sidecar_name(platform[1])
expected_files = sorted((*runtime_filenames(platform[1]), office))
resources = platform[1].removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe")
expected_files = sorted((*runtime_filenames(platform[1]), office, resources))
found_files = sorted({name.split("/runtime/", 1)[1].split("/", 1)[0] for name in runtime_payload})
if found_files != expected_files:
raise RuntimeError(f"{wheel} runtime payload must be {expected_files}, found {found_files}")
office_modules = f"deepseek_harness_runtime/runtime/{office}/node_modules"
verify_office_payload(archive, office_modules, platform[0])
validate = runpy.run_path(str(ROOT / "python/sdk-runtime/src/deepseek_harness_runtime/_resources.py"))["validate_resources"]
target = next(name for name, value in PLATFORMS.items() if value == platform)
validate(zipfile.Path(archive, f"deepseek_harness_runtime/runtime/{resources}/"), target)
for runtime_file in runtime_payload:
if "/" in runtime_file.split("/runtime/", 1)[1]:
continue
+15 -15
View File
@@ -2,13 +2,13 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSyn
import { join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { describe, expect, it } from 'vitest'
import desktopRuntimeLock from '../apps/desktop/scripts/primary-runtime-lock.json' with { type: 'json' }
import primaryRuntimeLock from './primary-runtime/lock.json' with { type: 'json' }
import {
CLAUDE_AGENT_SDK_PACKAGE,
assertRuntimeLicenses,
claudeDistributionFromManifest,
collectPythonDependencies,
collectDesktopPythonDependencies,
collectBundledPythonDependencies,
isOwnerAuthorizedRuntime,
isPermissive,
type Manifest,
@@ -33,7 +33,7 @@ describe('THIRD_PARTY_NOTICES.md', () => {
}, async () => {
const generated = await render()
expect(generated).toContain('It depends on the third-party software listed below.')
expect(generated).toContain(`| [\`numpy\`](https://github.com/numpy/numpy) | ${desktopRuntimeLock.pythonPackages.numpy} | BSD-3-Clause |`)
expect(generated).toContain(`| [\`numpy\`](https://github.com/numpy/numpy) | ${primaryRuntimeLock.pythonPackages.numpy} | BSD-3-Clause |`)
expect(generated).toContain('## LibreOffice conversion kit')
expect(generated).toContain('Recipients must have access to those corresponding sources and notices.')
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(generated)
@@ -323,22 +323,22 @@ describe('collectPythonDependencies', () => {
})
})
describe('collectDesktopPythonDependencies', () => {
it('discloses the committed Desktop closure with its exact locked versions', () => {
const dependencies = collectDesktopPythonDependencies(desktopRuntimeLock.pythonPackages)
expect(dependencies).toHaveLength(Object.keys(desktopRuntimeLock.pythonPackages).length)
describe('collectBundledPythonDependencies', () => {
it('discloses the committed bundled Python closure with its exact locked versions', () => {
const dependencies = collectBundledPythonDependencies(primaryRuntimeLock.pythonPackages)
expect(dependencies).toHaveLength(Object.keys(primaryRuntimeLock.pythonPackages).length)
expect(dependencies).toContainEqual({
name: 'pillow', version: desktopRuntimeLock.pythonPackages.Pillow,
name: 'pillow', version: primaryRuntimeLock.pythonPackages.Pillow,
license: 'MIT-CMU', repo: 'https://github.com/python-pillow/Pillow',
})
expect(dependencies).toContainEqual({
name: 'typing-extensions', version: desktopRuntimeLock.pythonPackages.typing_extensions,
name: 'typing-extensions', version: primaryRuntimeLock.pythonPackages.typing_extensions,
license: 'PSF-2.0', repo: 'https://github.com/python/typing_extensions',
})
})
it('normalizes names while preserving pinned version strings', () => {
expect(collectDesktopPythonDependencies({ 'typing_extensions': '4.16.0', 'Pillow': '12.3.0' }))
expect(collectBundledPythonDependencies({ 'typing_extensions': '4.16.0', 'Pillow': '12.3.0' }))
.toEqual([
{ name: 'pillow', version: '12.3.0', license: 'MIT-CMU', repo: 'https://github.com/python-pillow/Pillow' },
{ name: 'typing-extensions', version: '4.16.0', license: 'PSF-2.0', repo: 'https://github.com/python/typing_extensions' },
@@ -346,18 +346,18 @@ describe('collectDesktopPythonDependencies', () => {
})
it('rejects duplicate normalized distribution names with the same locked version', () => {
expect(() => collectDesktopPythonDependencies({ typing_extensions: '4.16.0', 'typing.extensions': '4.16.0' }))
expect(() => collectBundledPythonDependencies({ typing_extensions: '4.16.0', 'typing.extensions': '4.16.0' }))
.toThrow('duplicate normalized names')
})
it('rejects missing distribution metadata and conflicting normalized versions', () => {
expect(() => collectDesktopPythonDependencies({ missing: '1.0' })).toThrow('missing from PYTHON_METADATA')
expect(() => collectDesktopPythonDependencies({ Pillow: '12.3.0', pillow: '12.4.0' })).toThrow('conflicting locked versions')
expect(() => collectBundledPythonDependencies({ missing: '1.0' })).toThrow('missing from PYTHON_METADATA')
expect(() => collectBundledPythonDependencies({ Pillow: '12.3.0', pillow: '12.4.0' })).toThrow('conflicting locked versions')
})
it('applies the runtime license check to bundled Python distributions', () => {
expect(() => { assertRuntimeLicenses(collectDesktopPythonDependencies(desktopRuntimeLock.pythonPackages)) }).not.toThrow()
const dependencies = collectDesktopPythonDependencies({ 'copyleft-wheel': '1.0' }, {
expect(() => { assertRuntimeLicenses(collectBundledPythonDependencies(primaryRuntimeLock.pythonPackages)) }).not.toThrow()
const dependencies = collectBundledPythonDependencies({ 'copyleft-wheel': '1.0' }, {
'copyleft-wheel': { license: 'GPL-3.0-only', repo: 'https://example.com/project' },
})
expect(() => { assertRuntimeLicenses(dependencies) }).toThrow('copyleft-wheel (GPL-3.0-only)')
+15 -15
View File
@@ -1,7 +1,7 @@
/**
* Generate `THIRD_PARTY_NOTICES.md` from the workspace manifests: every
* external dependency named by a workspace `package.json`, the vendored-package
* manifest in `vendor/README.md`, the Python `pyproject.toml` files, the Desktop
* manifest in `vendor/README.md`, the Python `pyproject.toml` files, the shared
* Python distribution lock, and the pnpm patch list. npm metadata comes from the installed
* store, so the tree must be installed. `--check` verifies the committed
* artifact. Tier policy and ownership live in
@@ -13,7 +13,7 @@ import { dirname, resolve } from 'node:path'
import * as yaml from 'js-yaml'
import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
import parseSpdx from 'spdx-expression-parse'
import desktopRuntimeLock from '../apps/desktop/scripts/primary-runtime-lock.json' with { type: 'json' }
import primaryRuntimeLock from './primary-runtime/lock.json' with { type: 'json' }
import { browserBundledExternals } from './browser-bundled-externals.ts'
const root = resolve(import.meta.dirname, '..')
@@ -93,7 +93,7 @@ const OVERRIDES: Record<string, { license?: string; repo?: string }> = {
/**
* Python metadata is recorded from the distributions' license and project
* fields; generation does not require installing their wheels. Both Python
* manifests and the Desktop lock reject names absent from this map.
* manifests and the shared runtime lock reject names absent from this map.
*/
const PYTHON_METADATA: Record<string, { license: string; repo: string; role?: string }> = {
pydantic: { license: 'MIT', repo: 'https://github.com/pydantic/pydantic', role: 'runtime dependency of `deepseek-harness-sdk`' },
@@ -616,12 +616,12 @@ function collectPython(): { name: string; license: string; repo: string; role: s
}
/**
* Disclose every Desktop wheel distribution using its locked version, rejecting duplicate normalized names.
* @param packages - Distribution names and exact versions from the Desktop runtime lock.
* Disclose every bundled wheel distribution using its locked version, rejecting duplicate normalized names.
* @param packages - Distribution names and exact versions from the shared runtime lock.
* @param metadata - License and source metadata for every locked distribution.
* @returns Normalized, sorted distribution identities with versions and licenses.
*/
export function collectDesktopPythonDependencies(
export function collectBundledPythonDependencies(
packages: Readonly<Record<string, string>>,
metadata: PythonMetadata = PYTHON_METADATA,
): { name: string; version: string; license: string; repo: string }[] {
@@ -630,14 +630,14 @@ export function collectDesktopPythonDependencies(
const name = normalizePythonDistributionName(distribution)
const previous = normalized.get(name)
if (previous !== undefined && previous !== version) {
throw new Error(`gen-third-party-notices: Desktop python distribution ${name} has conflicting locked versions.`)
throw new Error(`gen-third-party-notices: bundled Python distribution ${name} has conflicting locked versions.`)
}
if (previous !== undefined) throw new Error(`gen-third-party-notices: Desktop python distribution ${name} has duplicate normalized names.`)
if (previous !== undefined) throw new Error(`gen-third-party-notices: bundled Python distribution ${name} has duplicate normalized names.`)
normalized.set(name, version)
}
return [...normalized].sort(([a], [b]) => a.localeCompare(b)).map(([name, version]) => {
const entry = metadata[name]
if (entry === undefined) throw new Error(`gen-third-party-notices: Desktop python distribution ${name} is missing from PYTHON_METADATA.`)
if (entry === undefined) throw new Error(`gen-third-party-notices: bundled Python distribution ${name} is missing from PYTHON_METADATA.`)
return { name, version, license: entry.license, repo: entry.repo }
})
}
@@ -753,7 +753,7 @@ export async function render(): Promise<string> {
const kitRuntime = runtimeDeps.some(dep => dep.name === LIBREOFFICE_KIT_PACKAGE)
const vendored = collectVendored()
const python = collectPython()
const desktopPython = collectDesktopPythonDependencies(desktopRuntimeLock.pythonPackages)
const bundledPython = collectBundledPythonDependencies(primaryRuntimeLock.pythonPackages)
const patched = collectPatched()
const claudeDistribution = runtimeDeps.some(
dep => dep.name === CLAUDE_AGENT_SDK_PACKAGE,
@@ -762,7 +762,7 @@ export async function render(): Promise<string> {
: undefined
const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
assertRuntimeLicenses(runtimeDeps)
assertRuntimeLicenses(desktopPython)
assertRuntimeLicenses(bundledPython)
const patchedLines = patched.map(({ spec, patch }) => `- \`${spec}\` — [\`${patch}\`](${patch})`)
return `<!-- Generated by scripts/gen-third-party-notices.ts — do not edit by hand.
@@ -772,7 +772,7 @@ export async function render(): Promise<string> {
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
This file lists **direct** dependencies declared by the workspace, the explicitly disclosed official Claude Code platform payload closure, and the Desktop bundled Python distributions. It is generated by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
This file lists **direct** dependencies declared by the workspace, the explicitly disclosed official Claude Code platform payload closure, and the Bundled Python distributions. It is generated by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python SDK closure is recorded separately in [\`python/sdk/uv.lock\`](python/sdk/uv.lock).
@@ -817,13 +817,13 @@ Direct dependencies of the \`pyproject.toml\` manifests, plus \`uv\` as the deve
${python.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.license} | ${dep.role} |`).join('\n')}
| [\`uv\`](https://github.com/astral-sh/uv) | MIT / Apache-2.0 | development workflow tool |
## Desktop bundled Python distributions
## Bundled Python distributions
The [Desktop runtime lock](apps/desktop/scripts/primary-runtime-lock.json) records each distribution version and the wheel download hashes. The table includes every entry in \`pythonPackages\`, including transitive dependencies. Wheel extraction preserves distribution metadata and the license and notice files supplied by each archive. Project licenses below do not enumerate the separate licenses of native libraries bundled inside wheels.
The [shared runtime lock](scripts/primary-runtime/lock.json) records each distribution version and the wheel download hashes. The table includes every entry in \`pythonPackages\`, including transitive dependencies. Wheel extraction preserves distribution metadata and the license and notice files supplied by each archive. Project licenses below do not enumerate the separate licenses of native libraries bundled inside wheels.
| Distribution | Locked version | Project license |
| --- | --- | --- |
${desktopPython.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.version} | ${dep.license} |`).join('\n')}
${bundledPython.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.version} | ${dep.license} |`).join('\n')}
## First-party native packages
+12
View File
@@ -75,6 +75,7 @@ import WorkflowEngine from '@deepseek-ai/dsh-workflow'
import type { WorkflowRun, WorkflowStartRequest } from '@deepseek-ai/dsh-workflow'
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
import * as ToolWorkspaceDependencies from '@deepseek-ai/dsh-tool-workspace-dependencies'
import { githubSlug } from './verify-md-links.ts'
/** Attachment seam marker that makes the attachments-conditional `read_image` schema harvestable. */
@@ -637,6 +638,17 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolWorkflow)
},
},
{
pkg: '@deepseek-ai/dsh-tool-workspace-dependencies',
dir: 'tool-workspace-dependencies',
source: 'packages/skill/tool-workspace-dependencies/src/index.ts',
requires: ['ctx.tools'],
writes: ['tool/call', 'tool/result'],
async mount(ctx) {
// Schema harvest never prepares a payload; the directory need not exist.
await ctx.plugin(ToolWorkspaceDependencies, { source: resolve(root, '.tmp/tool-catalog/primary-runtime') })
},
},
{
pkg: '@deepseek-ai/dsh-tool-web',
dir: 'tool-web',
-3
View File
@@ -26,9 +26,6 @@
"apps/desktop-host/tests/office.spec.ts": {
"3f8653cd87f99ebd70765379dae51ab308ed9c280e831d39e1abd469bfcdc6e3": 1
},
"apps/desktop-host/tests/primary-runtime.spec.ts": {
"3f8653cd87f99ebd70765379dae51ab308ed9c280e831d39e1abd469bfcdc6e3": 1
},
"apps/desktop/scripts/installed-update-qualification.ts": {
"5a8270cf9b1e554ae0cff6eaf7086e44c22f61a08e5f91dc7e1397eccaa726a5": 2
},
@@ -74,6 +74,54 @@
"sha256": "623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d"
}
]
},
"linux-x64": {
"nodeArchive": "linux-x64.tar.gz",
"nodeSha256": "6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff",
"pythonTarget": "x86_64-unknown-linux-gnu",
"pythonSha256": "72748da13197c1fb161e3afeef20a6a385ff24f2165e6e2758e47008e7faba4c",
"wheels": [
{
"url": "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28"
},
{
"url": "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4"
},
{
"url": "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91"
},
{
"url": "https://files.pythonhosted.org/packages/db/36/aa413bc214dc4f785ad2b2ddd8cc99aae7062d49ab155e91e6011af00daf/lxml-6.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
"sha256": "379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11"
}
]
},
"linux-arm64": {
"nodeArchive": "linux-arm64.tar.gz",
"nodeSha256": "724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5",
"pythonTarget": "aarch64-unknown-linux-gnu",
"pythonSha256": "577b4bec0793ad1ff0cbff9adbd0df078eddde38a4c41bf5d83ad381a85ee39d",
"wheels": [
{
"url": "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",
"sha256": "8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d"
},
{
"url": "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl",
"sha256": "b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b"
},
{
"url": "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl",
"sha256": "d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9"
},
{
"url": "https://files.pythonhosted.org/packages/1e/1c/722e88883173097a1a375153e3c2447eba3060d0231522cf6596e99f4195/lxml-6.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl",
"sha256": "f683dc6300317700025e41d89a43e0276692ded16113a3c43eab704d605c58e5"
}
]
}
},
"wheels": [
+154
View File
@@ -0,0 +1,154 @@
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { zipSync } from 'fflate'
import { expect, it } from 'vitest'
import { downloadPrimaryRuntimeAsset, prepareOfficeSkillAssets, primaryRuntimePayloadDigest, smokePrimaryRuntime, unpackPrimaryRuntimeWheel } from './prepare.ts'
import lock from './lock.json' with { type: 'json' }
it('covers every SDK wheel target with the shared interpreter lock', () => {
const platforms = JSON.parse(readFileSync(new URL('../../python/sdk-runtime/platforms.json', import.meta.url), 'utf8')) as Record<string, unknown>
expect(Object.keys(lock.targets).sort()).toEqual(Object.keys(platforms).map(target => target.replace('macos-', 'mac-')).sort())
})
const libraryWheel = Buffer.from('UEsDBAoAAAAAAASeLl0sYMPjDAAAAAwAAAAJAAAAc2FtcGxlLnB5c2FtcGxlID0gNDIKUEsBAh4DCgAAAAAABJ4uXSxgw+MMAAAADAAAAAkAAAAAAAAAAQAAAKSBAAAAAHNhbXBsZS5weVBLBQYAAAAAAQABADcAAAAzAAAAAAA=', 'base64')
const relocatedWheel = Buffer.from('UEsDBAoAAAAAAASeLl3x0Nj9FAAAABQAAAAeAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlcmVxdWlyZXMgcmVsb2NhdGlvbgpQSwECHgMKAAAAAAAEni5d8dDY/RQAAAAUAAAAHgAAAAAAAAABAAAApIEAAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlUEsFBgAAAAABAAEATAAAAFAAAAAAAA==', 'base64')
const externalLibraryWheel = Buffer.from('UEsDBBQAAAAAAAAAIVyBOE8OHAAAABwAAAAhAAAAc2FtcGxlLTEuMC5kYXRhL3B1cmVsaWIvc2FtcGxlLnB5cmVxdWlyZXMgbGlicmFyeSByZWxvY2F0aW9uClBLAQIUAxQAAAAAAAAAIVyBOE8OHAAAABwAAAAhAAAAAAAAAAAAAACAAQAAAABzYW1wbGUtMS4wLmRhdGEvcHVyZWxpYi9zYW1wbGUucHlQSwUGAAAAAAEAAQBPAAAAWwAAAAAA', 'base64')
it.each(Object.entries(lock.targets))('records every locked wheel distribution and version for %s', (_target, artifact) => {
const normalize = (name: string): string => name.toLowerCase().replace(/[-_.]+/gu, '-')
const distributions = [...artifact.wheels, ...lock.wheels].map(({ url }) => {
const [name, version] = basename(new URL(url).pathname).split('-')
return [normalize(name!), version] as const
})
const declared = Object.entries(lock.pythonPackages).map(([name, version]) => [normalize(name), version] as const)
expect(new Set(distributions.map(([name]) => name)).size).toBe(distributions.length)
expect(new Set(declared.map(([name]) => name)).size).toBe(declared.length)
expect(Object.fromEntries(distributions)).toEqual(Object.fromEntries(declared))
})
it('keeps a target payload identity independent of other target archives', () => {
const changed = structuredClone(lock)
changed.targets['win-x64'].wheels[0]!.sha256 = 'a'.repeat(64)
expect(primaryRuntimePayloadDigest('mac-arm64', changed, '11.7.0')).toBe(primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.0'))
expect(primaryRuntimePayloadDigest('win-x64', changed, '11.7.0')).not.toBe(primaryRuntimePayloadDigest('win-x64', lock, '11.7.0'))
})
it('invalidates payload identity for shared wheels, package versions and package-manager changes', () => {
const wheel = structuredClone(lock), distribution = structuredClone(lock)
wheel.wheels[0]!.sha256 = 'a'.repeat(64)
distribution.pythonPackages['python-docx'] = '1.2.1'
const original = primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.0')
expect(primaryRuntimePayloadDigest('mac-arm64', wheel, '11.7.0')).not.toBe(original)
expect(primaryRuntimePayloadDigest('mac-arm64', distribution, '11.7.0')).not.toBe(original)
expect(primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.1')).not.toBe(original)
})
it('reports missing distribution metadata before trying to execute a stale native payload', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-stale-runtime-'))
try {
await writeFile(join(root, 'runtime.json'), JSON.stringify({ desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
components: { python: '3.12.14', numpy: '2.3.5', pandas: '3.0.1' } }))
expect(() => { smokePrimaryRuntime(root) }).toThrow('missing Python distribution versions; prepare the payload')
} finally { await rm(root, { recursive: true, force: true }) }
})
it('extracts a hash-verified cached library without a Python installer or network request', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const hash = createHash('sha256').update(libraryWheel).digest('hex')
const archive = join(root, hash)
await writeFile(archive, libraryWheel)
expect(await downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).toBe(archive)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample.py'), 'utf8')).toBe('sample = 42\n')
await writeFile(archive, 'corrupt archive')
await expect(downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).rejects.toThrow('checksum mismatch')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('fully extracts a large deflate-compressed wheel entry', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-large-wheel-'))
try {
const expected = Buffer.alloc(1024 * 1024)
// Seeded xorshift bytes keep the compressed entry larger than the stream buffers.
let state = 26
for (let index = 0; index < expected.length; index++) {
state ^= state << 13
state ^= state >>> 17
state ^= state << 5
expected[index] = state & 255
}
const archive = join(root, 'large.whl')
await writeFile(archive, zipSync({ 'large.bin': expected }, { level: 6 }))
await unpackPrimaryRuntimeWheel(archive, root)
expect(await readFile(join(root, 'large.bin'))).toEqual(expected)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('retains auxiliary wheel scripts without generating command wrappers', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const archive = join(root, 'relocated.whl')
await writeFile(archive, relocatedWheel)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample-1.0.data/scripts/sample'), 'utf8')).toBe('requires relocation\n')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('rejects library files requiring an unsupported installation scheme', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const archive = join(root, 'relocated.whl')
await writeFile(archive, externalLibraryWheel)
await expect(unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))).rejects.toThrow('unsupported installation paths')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('copies complete Office resources outside the application archive and removes obsolete assets', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-office-assets-'))
try {
const source = join(root, 'package', 'assets')
const destination = join(root, 'Contents', 'Resources', 'runtime', 'office-skills')
await mkdir(join(source, 'scripts'), { recursive: true })
await writeFile(join(source, 'scripts', 'check_office.py'), 'print("checker")\n')
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
await mkdir(join(source, name))
await writeFile(join(source, name, 'SKILL.md'), `# ${name}\n`)
}
await prepareOfficeSkillAssets(source, destination)
await writeFile(join(destination, 'obsolete.py'), 'old helper')
await prepareOfficeSkillAssets(source, destination)
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
expect(await readFile(join(destination, name, 'SKILL.md'), 'utf8')).toBe(`# ${name}\n`)
}
expect(await readFile(join(destination, 'scripts', 'check_office.py'), 'utf8')).toBe('print("checker")\n')
await expect(readFile(join(destination, 'obsolete.py'))).rejects.toMatchObject({ code: 'ENOENT' })
} finally { await rm(root, { recursive: true, force: true }) }
})
it('gives Python-only payloads a distinct identity', () => {
expect(primaryRuntimePayloadDigest('linux-x64', lock, undefined)).not.toBe(primaryRuntimePayloadDigest('linux-x64', lock, '11.7.0'))
})
it('keeps carrier pnpm versions aligned with the shared payload build', () => {
const root = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')) as {
packageManager: string
devDependencies: { pnpm: string }
}
const desktop = JSON.parse(readFileSync(new URL('../../apps/desktop/package.json', import.meta.url), 'utf8')) as {
devDependencies: { pnpm: string }
}
expect(desktop.devDependencies.pnpm).toBe(root.devDependencies.pnpm)
expect(root.packageManager).toBe(`pnpm@${root.devDependencies.pnpm}`)
})
+207
View File
@@ -0,0 +1,207 @@
/** Prepare pinned, relocatable script interpreters without installing into the build host. */
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { cp } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import extractZip from 'extract-zip'
import { x as extractTar } from 'tar'
import { parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../packages/skill/tool-workspace-dependencies/src/index.ts'
import lock from './lock.json' with { type: 'json' }
/**
* Download or reuse an archive only when its bytes match the release lock.
* @param url - Locked archive URL.
* @param sha256 - Expected SHA-256 digest.
* @param cache - Download cache directory.
* @returns Verified local archive path.
*/
export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string): Promise<string> {
const destination = join(cache, sha256)
let bytes: Buffer
try { bytes = readFileSync(destination) } catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
const response = await fetch(url)
if (!response.ok) throw new Error(`primary runtime download: ${String(response.status)} ${url}`)
bytes = Buffer.from(await response.arrayBuffer())
}
if (createHash('sha256').update(bytes).digest('hex') !== sha256) throw new Error(`primary runtime download: checksum mismatch for ${url}`)
writeFileSync(destination, bytes)
return destination
}
async function pythonArchive(target: keyof typeof lock.targets, cache: string): Promise<string> {
const artifact = lock.targets[target]
const filename = `cpython-${lock.pythonVersion}+${lock.pythonRelease}-${artifact.pythonTarget}-install_only_stripped.tar.gz`
return downloadPrimaryRuntimeAsset(`https://github.com/astral-sh/python-build-standalone/releases/download/${lock.pythonRelease}/${encodeURIComponent(filename)}`, artifact.pythonSha256, cache)
}
/**
* Identify the inputs that assemble one target's payload, excluding unrelated target locks.
* @param target - Runtime target whose archives are installed.
* @param runtimeLock - Locked interpreter and wheel inputs.
* @param pnpmVersion - Package-manager version copied into the payload.
* @returns SHA-256 payload identity for installation reuse.
*/
export function primaryRuntimePayloadDigest(
target: keyof typeof lock.targets, runtimeLock: typeof lock, pnpmVersion: string | undefined,
): string {
const { pythonVersion, pythonRelease, nodeVersion, wheels, pythonPackages } = runtimeLock
// Identity preserves key order within the selected target, wheel records and distribution map, plus wheel-entry order.
// Bump format when extraction or assembly changes payload bytes without changing locked inputs.
return createHash('sha256').update(JSON.stringify({
format: 4, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion,
artifact: runtimeLock.targets[target], wheels, pythonPackages, pnpm: pnpmVersion,
})).digest('hex')
}
/**
* Unpack a locked library wheel, retaining auxiliary scripts in its distribution data directory.
* @param archive - Hash-verified wheel archive.
* @param destination - Absolute site-packages directory.
* @returns Resolves after extraction without command wrappers; rejects other wheel installation schemes.
*/
export async function unpackPrimaryRuntimeWheel(archive: string, destination: string): Promise<void> {
await extractZip(archive, {
dir: destination,
onEntry: (entry) => {
const [directory, scheme] = entry.fileName.split('/')
if (directory?.endsWith('.data') && scheme !== '' && scheme !== 'scripts') {
throw new Error(`primary runtime: wheel requires unsupported installation paths: ${entry.fileName}`)
}
},
})
}
/**
* Copy the skill package's complete asset tree to ordinary filesystem resources.
* @param source - The package's assets directory.
* @param destination - External Office skill resource directory.
* @returns Resolves after replacing the external assets with the complete package tree.
*/
export async function prepareOfficeSkillAssets(source: string, destination: string): Promise<void> {
rmSync(destination, { recursive: true, force: true })
await cp(source, destination, { recursive: true, dereference: true })
}
/** A locked interpreter and wheel target. */
export type PrimaryRuntimeTarget = keyof typeof lock.targets
/** Build-only inputs shared by Desktop and SDK carriers. */
export interface PreparePrimaryRuntimeOptions {
/** Target whose archives and wheels are downloaded. */
readonly target: PrimaryRuntimeTarget
/** Resource directory receiving primary-runtime/ and office-skills/. */
readonly output: string
/** SHA-256-addressed archive cache. */
readonly cache: string
/** Carrier release recorded in the legacy desktopVersion manifest field. */
readonly version: string
/** Omit Node.js and pnpm for carriers providing only Python. */
readonly pythonOnly?: boolean
}
/**
* Materialize locked interpreters, libraries and Office resources without executing target code.
* @param options - Explicit target and carrier-owned output locations.
* @returns Resolves after the complete payload and skills have been copied to the output directory.
*/
export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOptions): Promise<void> {
const { target } = options
const paths = { runtime: resolve(options.output), downloads: resolve(options.cache) }
const artifact = lock.targets[target]
mkdirSync(paths.runtime, { recursive: true })
mkdirSync(paths.downloads, { recursive: true })
const staging = mkdtempSync(join(tmpdir(), 'dsh-primary-'))
try {
const output = join(staging, 'payload')
const dependencies = join(output, 'dependencies')
mkdirSync(dependencies, { recursive: true })
let pnpmVersion: string | undefined
if (!options.pythonOnly) {
const nodeFilename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}`
const nodeArchive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${nodeFilename}`, artifact.nodeSha256, paths.downloads)
const unpackedNode = join(staging, 'node')
mkdirSync(unpackedNode)
if (target === 'win-x64') await extractZip(nodeArchive, { dir: unpackedNode })
else await extractTar({ file: nodeArchive, cwd: unpackedNode })
const nodeSource = join(unpackedNode, nodeFilename.replace(/\.(?:zip|tar\.gz)$/u, ''))
mkdirSync(join(dependencies, 'node', 'bin'), { recursive: true })
mkdirSync(join(dependencies, 'node', 'node_modules'))
writeFileSync(join(dependencies, 'node', 'node_modules', 'README.txt'), 'Reserved for bundled Node packages. pnpm uses its default installation directories.\n')
cpSync(join(nodeSource, ...(target === 'win-x64' ? ['node.exe'] : ['bin', 'node'])),
join(dependencies, 'node', 'bin', target === 'win-x64' ? 'node.exe' : 'node'))
cpSync(join(nodeSource, 'LICENSE'), join(dependencies, 'node', 'LICENSE'))
const require = createRequire(import.meta.url)
const pnpmManifest = require.resolve('pnpm')
pnpmVersion = (JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }).version
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
}
await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies })
const manifest: PrimaryRuntimeManifest = {
desktopVersion: options.version,
platform: target === 'win-x64' ? 'win32' : target.startsWith('linux-') ? 'linux' : 'darwin',
arch: target.endsWith('-arm64') ? 'arm64' : 'x64',
payloadDigest: primaryRuntimePayloadDigest(target, lock, pnpmVersion),
python: lock.pythonVersion,
...(pnpmVersion === undefined ? {} : { node: lock.nodeVersion, pnpm: pnpmVersion }),
pythonPackages: lock.pythonPackages,
}
const entries = workspaceDependencyPaths(output, manifest)
for (const wheel of [...artifact.wheels, ...lock.wheels]) {
await unpackPrimaryRuntimeWheel(await downloadPrimaryRuntimeAsset(wheel.url, wheel.sha256, paths.downloads), entries.pythonPackages)
}
writeFileSync(join(output, 'runtime.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
const destination = join(paths.runtime, 'primary-runtime')
rmSync(destination, { recursive: true, force: true })
await cp(output, destination, { recursive: true, dereference: true })
} finally {
rmSync(staging, { recursive: true, force: true })
}
const require = createRequire(import.meta.url)
await prepareOfficeSkillAssets(join(dirname(require.resolve('@deepseek-ai/dsh-skill-office/package.json')), 'assets'),
join(paths.runtime, 'office-skills'))
}
/**
* Execute the native payload's interpreters, package manager and Python libraries.
* @param root - Final payload directory, including any platform signatures.
* @param environment - Scrubbed subprocess environment; defaults to excluding credential-shaped names.
*/
export function smokePrimaryRuntime(root: string, environment: NodeJS.ProcessEnv = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !/(?:KEY|SECRET|TOKEN|PASSWORD)/iu.test(name)),
)): void {
const manifest = parsePrimaryRuntime(JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')))
if (manifest.platform !== process.platform || manifest.arch !== process.arch) return
if (Object.keys(manifest.pythonPackages).length === 0) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
const entries = workspaceDependencyPaths(root, manifest)
const options = { stdio: 'inherit', timeout: 120_000, env: environment } as const
execFileSync(entries.python, ['-I', '-B', '-c', 'import decimal, xml.parsers.expat, lzma, uuid, numpy, pandas; assert numpy.arange(4).sum() == 6; assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3'], options)
execFileSync(entries.python, ['-I', '-B', join(import.meta.dirname, 'smoke.py'), JSON.stringify(manifest.pythonPackages),
manifest.python, join(dirname(root), 'office-skills', 'scripts', 'check_office.py')], options)
execFileSync(entries.python, ['-I', '-B', '-m', 'pip', 'check'], options)
if (entries.node !== undefined) execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(manifest.node)}) process.exit(1)`], options)
if (entries.pnpm !== undefined && entries.node !== undefined) execFileSync(entries.node, [entries.pnpm, '--version'], options)
}
if (import.meta.main) {
const { values } = parseArgs({ options: {
target: { type: 'string' }, output: { type: 'string' }, cache: { type: 'string' },
'python-only': { type: 'boolean', default: false },
} })
if (!values.target || !Object.hasOwn(lock.targets, values.target) || !values.output) {
throw new Error(`Usage: pnpm run prepare:primary-runtime --target <${Object.keys(lock.targets).join('|')}> --output <directory> [--cache <directory>] [--python-only]`)
}
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')) as { version: string }
const output = resolve(values.output)
await preparePrimaryRuntime({
target: values.target as PrimaryRuntimeTarget, output,
cache: values.cache ?? join(tmpdir(), 'dsh-primary-runtime-downloads'), version,
pythonOnly: values['python-only'],
})
smokePrimaryRuntime(join(output, 'primary-runtime'))
}
+81 -5
View File
@@ -44,6 +44,7 @@ MCP_TEXT = "MCP client smoke ok"
PROFILE_PLUGIN_PROMPT = "Verify the Python-installed dsh profile plugin."
PROFILE_PLUGIN_TEXT = "profile plugin smoke ok"
PROFILE_PLUGIN_MARKER = "PYTHON_INSTALLED_DSH_PROFILE_PLUGIN"
AUTHORING_PROMPT = "Query the packaged Python environment for Office authoring."
IS_WINDOWS = sys.platform == "win32"
MINIMAL_SHELL_TOOL = "pwsh" if IS_WINDOWS else "bash"
MINIMAL_SHELL_COMMAND = (
@@ -77,6 +78,8 @@ LEGACY_CUSTOM_DISABLED_ROWS = (
"plan-mode",
"skill",
"skill-filesystem",
"skill-office",
"workspace-dependencies",
"tool-fs",
"tool-fs-search",
"tool-goal",
@@ -320,6 +323,10 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
result = tool_results[-1]
call_id, tool_name = latest_tool_call(messages, result.get("tool_use_id"))
tool_text = message_text(result.get("content"))
if tool_name == "load_workspace_dependencies":
if result.get("is_error") or "python" not in json.loads(tool_text):
raise AssertionError(f"packaged Python query failed: {tool_text}")
return text_chunks(EXPECTED_TEXT)
mcp = mcp_tool_followup(call_id, tool_name, tool_text)
if mcp is not None:
return mcp
@@ -371,6 +378,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
RESTART_FIRST_PROMPT,
RESTART_SECOND_PROMPT,
PROFILE_PLUGIN_PROMPT,
AUTHORING_PROMPT,
}
prompt = next(
(candidate for candidate in user_prompts if candidate in scenario_prompts),
@@ -378,6 +386,9 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
)
if prompt == SNAPSHOT_DIRECT_CHILD_PROMPT:
return text_chunks("DIRECT_CHILD_OK")
if prompt == AUTHORING_PROMPT:
assert_advertised_tool(body, "load_workspace_dependencies")
return tool_call_chunks("authoring-runtime", "load_workspace_dependencies", {})
if prompt == SNAPSHOT_WORKFLOW_CHILD_PROMPT:
return text_chunks("WORKFLOW_CHILD_OK")
if prompt == SNAPSHOT_PROMPT:
@@ -716,7 +727,7 @@ def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--scenario",
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-office", "sdk-live", "runner", "direct"),
choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-office", "sdk-authoring", "sdk-live", "runner", "direct"),
default="all",
)
parser.add_argument("--exe", type=Path)
@@ -735,10 +746,10 @@ def main() -> None:
parser.error("--scenario sdk-profile-plugin requires --installed-wheel")
if args.installed_wheel:
args.exe = assert_installed_wheel_environment()
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-restart", "sdk-office", "runner", "direct"} and args.exe is None:
if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-minimal-in-history", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-restart", "sdk-office", "sdk-authoring", "runner", "direct"} and args.exe is None:
parser.error("--exe is required for custom, minimal, fs-search, spawn-node, snapshot, restart, office, runner, and direct scenarios")
if args.update_snapshots and args.scenario not in {"all", "sdk-minimal", "sdk-minimal-in-history", "sdk-snapshot", "sdk-restart"}:
parser.error("--update-snapshots requires --scenario sdk-minimal, sdk-minimal-in-history, sdk-snapshot, sdk-restart, or all")
if args.update_snapshots and args.scenario not in {"all", "sdk-minimal", "sdk-minimal-in-history", "sdk-snapshot", "sdk-restart", "sdk-authoring"}:
parser.error("--update-snapshots requires --scenario sdk-minimal, sdk-minimal-in-history, sdk-snapshot, sdk-restart, sdk-authoring, or all")
if args.exe is not None and not args.exe.is_file():
parser.error(f"runtime executable does not exist: {args.exe}")
@@ -762,6 +773,9 @@ def main() -> None:
return
with MockModel() as model:
if args.scenario in {"all", "sdk-authoring"}:
assert args.exe is not None
smoke_sdk_authoring(model.url, args.exe.resolve(), args.update_snapshots)
if args.scenario in {"all", "sdk-default"}:
smoke_sdk_default(model.url)
if args.scenario in {"all", "sdk-custom"}:
@@ -797,6 +811,65 @@ def main() -> None:
print(f"smoke-python-runtime: {args.scenario} passed")
def smoke_sdk_authoring(base_url: str, executable: Path, update_snapshots: bool) -> None:
"""Query the bundled Python and switch skills without replacing that environment."""
from deepseek_harness import DeepSeekHarness
resources = executable.with_name(executable.name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe"))
manifest = json.loads((resources / "primary-runtime/runtime.json").read_text())
for mode in ("default", "replacement", "disabled"):
with tempfile.TemporaryDirectory(prefix="dsh-sdk-authoring-") as temporary:
root = Path(temporary).resolve()
home = root / "home"
if mode == "replacement":
skill = home / "skills/office-docx/SKILL.md"
skill.parent.mkdir(parents=True)
skill.write_text("---\nname: office-docx\ndescription: CUSTOM_OFFICE_DOCX\n---\nUse the bundled Python for custom document work.\n")
patch = root / "skills.patch.yml"
patch.write_text(json.dumps([{"id": "skill-office", "disabled": True}] if mode == "disabled" else []))
first = len(MockModelHandler.requests)
with DeepSeekHarness(
provider="deepseek-official", model="smoke-model", cwd=str(root),
dsh_bin=str(executable), dsh_home=str(home), patches=(str(patch),),
api_key="sk-keyless-smoke", base_url=base_url,
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"},
request_timeout_seconds=60,
) as harness:
result = harness.run(AUTHORING_PROMPT, session_id="authoring")
assert result.final_response == EXPECTED_TEXT, result.final_response
requests = MockModelHandler.requests[first:]
assert len(requests) == 2, requests
prompt = json.dumps(requests[0], ensure_ascii=False)
for name in ("office-docx", "office-pptx", "office-xlsx"):
assert (name in prompt) == (mode != "disabled"), (mode, name)
assert ("CUSTOM_OFFICE_DOCX" in prompt) == (mode == "replacement"), mode
tool_result = next(block for message in requests[1]["messages"]
for block in message.get("content", [])
if isinstance(block, dict) and block.get("tool_use_id") == "authoring-runtime")
dependencies = json.loads(message_text(tool_result["content"]))
python = Path(dependencies["python"])
assert python.is_relative_to(resources), dependencies
assert "node" not in dependencies and "pnpm" not in dependencies, dependencies
assert dependencies["pythonDistributions"] == manifest["pythonPackages"], dependencies
assert not (home / "dsh-runtimes").exists()
if mode == "default":
subprocess.run([
str(python), "-I", "-B", str(Path(__file__).parent / "primary-runtime/smoke.py"),
json.dumps(manifest["pythonPackages"]), manifest["python"],
str(resources / "office-skills/scripts/check_office.py"),
], check=True, timeout=120,
env={name: value for name, value in os.environ.items()
if not re.search(r"KEY|SECRET|TOKEN|PASSWORD", name, re.I)})
schema = next(tool for tool in requests[0]["tools"] if tool.get("name") == "load_workspace_dependencies")
visible = {"tool": schema, "result": {**dependencies, "python": "{{python}}", "pythonPackages": "{{site-packages}}"}}
compare_snapshot_files(
{"model-visible.json": json.dumps(visible, indent=2, ensure_ascii=False) + "\n"},
update_snapshots, Path(__file__).parent / "snapshots/python-sdk-single-exe/authoring",
("model-visible.json",),
)
print("smoke-python-runtime: bundled Python, default skills, replacement and disabled skills passed")
def smoke_sdk_office(executable: Path) -> None:
"""Relocate the wheel payload and convert a real DOCX with the target platform engine."""
from deepseek_harness import DeepSeekHarness
@@ -805,7 +878,8 @@ def smoke_sdk_office(executable: Path) -> None:
root = Path(temporary).resolve()
relocated = root / executable.name
stem = executable.name.removesuffix(".exe")
for source in executable.parent.glob(f"{stem}*"):
resources = executable.with_name(stem.removeprefix("deepseek-harness-sdk-runtime-"))
for source in [*executable.parent.glob(f"{stem}*"), resources]:
destination = root / source.name
if source.is_dir():
shutil.copytree(source, destination)
@@ -850,6 +924,8 @@ def smoke_sdk_office(executable: Path) -> None:
api_key="sk-keyless-smoke",
base_url="http://127.0.0.1:9",
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"},
# The startup plugin awaits a converter with a 120-second deadline before JSON-RPC is ready.
initialize_timeout_seconds=180,
request_timeout_seconds=180,
):
pass
@@ -0,0 +1,29 @@
{
"tool": {
"name": "load_workspace_dependencies",
"description": "Get absolute paths to bundled Python and library directories, plus bundled Python distribution versions. Node.js and pnpm paths are included when the payload provides them. Python includes numpy, pandas, python-docx, python-pptx, openpyxl, Pillow, lxml, and XlsxWriter. Use these libraries for Office files unless the user or workspace instructions select another environment. When Node.js and pnpm paths are returned, run pnpm with that Node executable and pnpm script path. This does not change PATH or package-manager settings.",
"input_schema": {
"type": "object",
"properties": {}
}
},
"result": {
"python": "{{python}}",
"pythonPackages": "{{site-packages}}",
"pythonDistributions": {
"numpy": "2.3.5",
"pandas": "3.0.1",
"python-dateutil": "2.9.0.post0",
"six": "1.17.0",
"tzdata": "2025.2",
"python-docx": "1.2.0",
"python-pptx": "1.0.2",
"openpyxl": "3.1.5",
"Pillow": "12.3.0",
"lxml": "6.1.3",
"XlsxWriter": "3.2.9",
"typing_extensions": "4.16.0",
"et_xmlfile": "2.0.0"
}
}
}
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more