فهرست منبع

fix(document): select Office engines from declared native targets

yudshj 1 هفته پیش
والد
کامیت
fcf9f4cfa2
39فایلهای تغییر یافته به همراه187 افزوده شده و 90 حذف شده
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.i18n.yaml
  5. 5 5
      .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md
  6. 5 5
      .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md
  7. 2 2
      apps/desktop/README.i18n.yaml
  8. 1 1
      apps/desktop/README.md
  9. 1 1
      apps/desktop/README.zh.md
  10. 4 2
      apps/desktop/scripts/prepare-dsh.ts
  11. 3 3
      apps/desktop/scripts/runtime-file-policy.ts
  12. 21 8
      apps/desktop/tests/runtime-file-policy.spec.ts
  13. 2 2
      docs/capability-seams.i18n.yaml
  14. 1 1
      docs/capability-seams.md
  15. 1 1
      docs/capability-seams.zh.md
  16. 2 2
      docs/subsystems/document-convert.i18n.yaml
  17. 1 1
      docs/subsystems/document-convert.md
  18. 1 1
      docs/subsystems/document-convert.zh.md
  19. 2 2
      packages/document/README.i18n.yaml
  20. 1 1
      packages/document/README.md
  21. 1 1
      packages/document/README.zh.md
  22. 2 2
      packages/document/document-convert-libreoffice/README.i18n.yaml
  23. 2 2
      packages/document/document-convert-libreoffice/README.md
  24. 2 2
      packages/document/document-convert-libreoffice/README.zh.md
  25. 1 1
      packages/document/document-convert-libreoffice/package.json
  26. 2 2
      python/sdk-runtime/README.i18n.yaml
  27. 1 1
      python/sdk-runtime/README.md
  28. 1 1
      python/sdk-runtime/README.zh.md
  29. 9 4
      python/sdk-runtime/hatch_build.py
  30. 15 7
      python/sdk-runtime/src/deepseek_harness_runtime/__init__.py
  31. 4 1
      python/sdk/tests/test_release_version.py
  32. 18 6
      python/sdk/tests/test_runtime_resolution.py
  33. 13 9
      scripts/build-exe-for-python-sdk-office.spec.ts
  34. 7 4
      scripts/build-exe-for-python-sdk-office.ts
  35. 3 1
      scripts/build-python-release.py
  36. 1 1
      scripts/gen-doc-graphs.ts
  37. 24 0
      scripts/libreoffice-engine.spec.ts
  38. 17 0
      scripts/libreoffice-engine.ts
  39. 5 1
      scripts/smoke-python-runtime.py

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

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

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md

@@ -72,7 +72,7 @@ The verification surface has three tiers. Mechanism tier: the measured conclusio
 
 Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends.
 
-The installed-wheel `sdk-office` scenario copies the target's complete payload to a private temporary directory, requires exactly one target backend (WASM on Linux, native on macOS/Windows), and converts DOCX once through the shipped SDK profile with that engine. It checks that the kit module resolves inside the relocated directory, that conversion reports the expected backend, and that the output contains PDF bytes. Wheel validation checks the declared engine assets and native helper execute bit; CI uploads the completed wheel, so nested helper permissions stay inside the ZIP archive.
+The installed-wheel `sdk-office` scenario copies the target's complete payload to a private temporary directory, requires exactly one target backend (the declared native target, or WASM when no native target is declared), and converts DOCX once through the shipped SDK profile with that engine. It checks that the kit module resolves inside the relocated directory, that conversion reports the expected backend, and that the output contains PDF bytes. Wheel validation checks the declared engine assets and native helper execute bit; CI uploads the completed wheel, so nested helper permissions stay inside the ZIP archive.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md

@@ -72,7 +72,7 @@ Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和
 
 手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。
 
-安装后 wheel 的 `sdk-office` 场景会将目标的完整载荷复制到私有临时目录,要求仅存在一个目标后端(Linux 为 WASM,macOS/Windows 为原生),并通过随附 SDK profile 使用该引擎转换一次 DOCX。它检查 kit 模块确实解析到迁移后的目录、转换报告预期后端,且输出包含 PDF 字节。wheel 校验会检查声明的引擎资源与原生辅助程序执行位;CI 上传已完成的 wheel,因此嵌套辅助程序权限保留在 ZIP 归档中。
+安装后 wheel 的 `sdk-office` 场景会将目标的完整载荷复制到私有临时目录,要求仅存在一个目标后端(使用已声明的原生目标,未声明原生目标时使用 WASM),并通过随附 SDK profile 使用该引擎转换一次 DOCX。它检查 kit 模块确实解析到迁移后的目录、转换报告预期后端,且输出包含 PDF 字节。wheel 校验会检查声明的引擎资源与原生辅助程序执行位;CI 上传已完成的 wheel,因此嵌套辅助程序权限保留在 ZIP 归档中。
 
 ## 曾考虑的替代方案
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md
-2026-09-15-platform-office-engines.md: fa8656eb7505b599f76640077f9b957fd867269c
-2026-09-15-platform-office-engines.zh.md: eed1879b9a0bd5fe5056670c414c4db4b81d9d9b
+2026-09-15-platform-office-engines.md: a3d1365bf33006c31d53c54f1255f72c4663cb5d
+2026-09-15-platform-office-engines.zh.md: dfffd1b9de33a2b138e0450f8a60d940fadcefd4

+ 5 - 5
.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md

@@ -10,20 +10,20 @@ Installing WASM beside a usable native engine adds a second LibreOffice payload
 
 ## Decision
 
-Harness depends on the kit API and uses its platform-filtered optional engine dependencies. macOS and Windows install their matching ARM64 or x64 native package; Linux installs the shared WASM package. WASM declares Linux as its npm OS. The provider has no direct WASM dependency. Missing native engines on macOS/Windows reject converter creation instead of selecting WASM.
+Harness selects one engine from the installed kit API’s `optionalDependencies`. A declared `@deepseek-ai/libreoffice-kit-${platform}-${arch}` requires that native package; other targets require the shared WASM package. The supported native target set belongs to the kit release, not an operating-system branch in Harness. A missing declared native package is an incomplete installation and never selects WASM. The provider has no direct WASM dependency.
 
-Python sidecar assembly copies only the target engine and its dependency closure. Wheel packaging and runtime lookup require that engine; the relocated conversion smoke checks the selected backend. Desktop carries the ordinary npm installation in application resources. Engine compilation, qualification, and publication remain in the kit repository.
+Python sidecar assembly copies only the selected engine and its dependency closure. Wheel packaging and runtime lookup select from the same kit manifest; the relocated conversion smoke checks the selected backend. Desktop filters its npm installation to that engine before signing and integrity sealing. Engine compilation, resolver compatibility, npm platform metadata, qualification, and publication remain in the kit repository.
 
 ## Alternatives considered
 
-**Keep WASM beside every native engine.** This tolerates a missing optional native package but increases every macOS/Windows installation. Fixed-platform distributions require their native engine to be present and tested instead.
+**Keep WASM beside every native engine.** This tolerates a missing optional native package but increases every distribution with native support. Fixed-platform distributions require their declared native engine to be present and tested instead.
 
-**Remove WASM on every platform.** Linux has no released native engine in this kit family. Keeping the shared WASM package on Linux preserves Office conversion without introducing a new native release target.
+**Remove WASM on every platform.** Targets without a released native engine still need conversion. The shared WASM package serves those targets without introducing a new native release target.
 
 **Make the Python Office sidecar optional.** The runtime carries the shared `dsh` CLI and its Web profile as well as the default SDK profile. Requiring the target engine gives the installed wheel a complete shipped profile set and reports an incomplete payload before launch. SDK and headless users also pay the engine download and installed-size cost.
 
 ## Consequences
 
-macOS/Windows distributions omit the WASM download and expanded assets. A missing native package becomes an installation defect that must be repaired; conversion does not download an engine. Linux retains its WASM resource and font requirements. The kit’s engine-selection tests and actual npm installation tests cover platform selection; Harness sidecar, wheel, and runtime-resolution tests cover packaging requirements. New package bytes require kit qualification and matching dependency integrity records before publication.
+Distributions with a declared native target omit WASM assets. Other targets retain WASM resource and font requirements. The pinned kit declares macOS/Windows ARM64 and x64 native packages, so current Linux distributions select WASM; a kit release can add a native Linux target without changing Harness’s selection rule. This does not expand Harness’s supported release platforms. Harness sidecar, wheel, and runtime-resolution tests cover both declared native targets and WASM selection, including missing native packages. New package bytes require kit qualification and matching dependency integrity records before publication.
 
 The [public Python release workflow](../../../../.github/workflows/python-release.yml) rejects any wheel at or above 100,000,000 bytes. Selecting one engine reduces payload size but does not establish that a runtime wheel meets this limit; npm engine publication and local conversion are separate from wheel upload eligibility.

+ 5 - 5
.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md

@@ -10,20 +10,20 @@ Status: implemented
 
 ## Decision
 
-Harness 依赖 kit API,并使用它按平台筛选的可选引擎依赖。macOS 和 Windows 安装匹配的 ARM64 或 x64 原生包;Linux 安装共享 WASM 包。WASM 的 npm OS 声明为 Linux。provider 不直接依赖 WASM。macOS/Windows 缺少原生引擎时拒绝创建转换器,不会选择 WASM。
+Harness 从已安装 kit API 的 `optionalDependencies` 中选择一个引擎。声明了 `@deepseek-ai/libreoffice-kit-${platform}-${arch}` 的目标要求该原生包,其余目标要求共享 WASM 包。受支持的原生目标集合由 kit 发布版本决定,不由 Harness 中的操作系统分支决定。已声明的原生包缺失表示安装不完整,不会改选 WASM。provider 不直接依赖 WASM。
 
-Python sidecar 组装仅复制目标引擎及其依赖闭包。wheel 打包和运行时查找要求该引擎存在;迁移后的转换冒烟检查所选 backend。Desktop 在应用资源中携带常规 npm 安装结果。引擎编译、资格验证和发布仍由 kit 仓库负责。
+Python sidecar 组装仅复制所选引擎及其依赖闭包。wheel 打包和运行时查找依据同一份 kit 清单选择;迁移目录后的转换冒烟检查所选后端。Desktop 在签名和完整性封装前将 npm 安装结果过滤为该引擎。引擎编译、解析器兼容性、npm 平台元数据、资格验证与发布仍由 kit 仓库负责。
 
 ## Alternatives considered
 
-**在每个原生引擎旁保留 WASM。** 这能容忍可选原生包缺失,却会增大每个 macOS/Windows 安装。固定平台分发物改为要求其原生引擎存在且经过测试。
+**在每个原生引擎旁保留 WASM。** 这能容忍可选原生包缺失,却会增大每个支持原生引擎的分发物。固定平台分发物要求其已声明的原生引擎存在且经过测试。
 
-**在所有平台移除 WASM。** 当前 kit 家族没有已发布的 Linux 原生引擎。在 Linux 保留共享 WASM 包,既能继续转换 Office 文档,也无需引入新的原生发布目标。
+**在所有平台移除 WASM。** 没有已发布原生引擎的目标仍需转换能力。共享 WASM 包为这些目标提供转换,无需引入新的原生发布目标。
 
 **将 Python Office 伴随目录设为可选。** 运行时承载共享的 `dsh` CLI 及其 Web profile,而不只有默认 SDK profile。强制携带目标引擎使已安装 wheel 具备完整的内置 profile 集合,并在启动前报告载荷不完整。SDK 和 headless 用户也承担引擎的下载与安装体积。
 
 ## Consequences
 
-macOS/Windows 分发物省去 WASM 下载和展开资源。原生包缺失成为必须修复的安装缺陷;转换不会下载引擎。Linux 保留 WASM 的资源和字体要求。kit 的引擎选择测试与真实 npm 安装测试覆盖平台选择;Harness 的 sidecar、wheel 和运行时解析测试覆盖打包要求。新包字节在发布前需要 kit 资格验证和匹配的依赖完整性记录。
+声明了原生目标的分发物省去 WASM 资源,其余目标保留 WASM 的资源和字体要求。锁定的 kit 声明了 macOS/Windows ARM64 和 x64 原生包,因此当前 Linux 分发物选择 WASM;kit 发布版本可以新增 Linux 原生目标,无需修改 Harness 的选择规则。这不扩展 Harness 支持的发布平台。Harness 的 sidecar、wheel 和运行时解析测试覆盖已声明原生目标与 WASM 选择,包括原生包缺失。新包字节在发布前需要 kit 资格验证和匹配的依赖完整性记录。
 
 [公开 Python 发布工作流](../../../../.github/workflows/python-release.yml)拒绝任何大于等于 100,000,000 字节的 wheel。只选择一个引擎会减少载荷,但不能据此认定运行时 wheel 已满足此限制;npm 引擎发布、本地转换与 wheel 上传资格是不同的验证。

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: f08fab5911450a615fc9b5cf750268b029a24f5d
-README.zh.md: d82a4f21e1bfcd9fbc56376033e4811bf4bc644d
+README.md: df2e01dd70b07cfe9c8982874fb337ec976f7b9f
+README.zh.md: 470d21abd4f70b718fac1fd305b38a5448598c54

+ 1 - 1
apps/desktop/README.md

@@ -116,7 +116,7 @@ Each target owns its packed package inputs, prepared runtime, package set, dsh t
 
 Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. [Desktop's file policy](scripts/runtime-file-policy.ts) then filters the immutable `resources/app.asar/dsh/node_modules` copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. It preserves runtime JavaScript, native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files.
 
-The [Office conversion provider](../../packages/document/document-convert-libreoffice/README.md) carries one matching native engine on macOS/Windows or the WASM engine on Linux. Preparation rejects a missing target engine before packaging. Engine assets, licenses, and notices remain in the runtime dependency tree.
+The [Office conversion provider](../../packages/document/document-convert-libreoffice/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. Engine assets, licenses, and notices remain in the runtime dependency tree.
 
 The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. [Copy-policy tests](tests/runtime-file-policy.spec.ts) cover exclusions and retained assets; `prepare:dsh` runs the [payload smoke](tests/fixtures/runtime-payload-smoke.mjs) under Electron RunAsNode before the Host smoke and final inventory verification.
 

+ 1 - 1
apps/desktop/README.zh.md

@@ -116,7 +116,7 @@ macOS arm64 命令要求 Apple Silicon。macOS x64 命令可以在 Intel macOS 
 
 Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安装外部依赖。[Desktop 文件策略](scripts/runtime-file-policy.ts)随后在签名和完整性封装前过滤不可变的 `resources/app.asar/dsh/node_modules` 副本。它排除 TypeScript 声明、已识别的 JavaScript/CSS/TypeScript source map、TypeScript 构建缓存、Domino 测试目录、选定的原生编译器输出和其他平台的 node-pty 预构建文件。它保留运行时 JavaScript、原生模块及其 DLL/EXE 辅助文件、WASM、未知资源、许可证和 notices。该策略不修改 npm tarball、捆绑的包管理器或用户安装的插件文件。
 
-[Office 转换提供方](../../packages/document/document-convert-libreoffice/README.zh.md)在 macOS/Windows 携带一个匹配的原生引擎,在 Linux 携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。引擎资源、许可证和 notices 保留在运行时依赖树中。
+[Office 转换提供方](../../packages/document/document-convert-libreoffice/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。引擎资源、许可证和 notices 保留在运行时依赖树中。
 
 打包应用运行编译后的 JavaScript 和预生成的 Typert 元数据,不编译 TypeScript 插件。源码级调试导航和编辑器声明仍可从开发包中获取。[复制规则测试](tests/runtime-file-policy.spec.ts)覆盖排除项和保留资源;`prepare:dsh` 在 Host smoke 和最终清单验证之前,使用 Electron RunAsNode 执行[产物 smoke](tests/fixtures/runtime-payload-smoke.mjs)。
 

+ 4 - 2
apps/desktop/scripts/prepare-dsh.ts

@@ -28,6 +28,7 @@ import {
 } from './macos-runtime.ts'
 import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
 import { desktopRuntimeFileExclusion } from './runtime-file-policy.ts'
+import { selectOfficeEngine } from '../../../scripts/libreoffice-engine.ts'
 
 const APP_ROOT = resolve(import.meta.dirname, '..')
 const BUILD_PATHS = resolveDesktopTargetBuildPaths()
@@ -123,10 +124,12 @@ async function main(): Promise<void> {
     const targetName = resolveDesktopBuildTarget()
     const target = { platform: process.platform, arch: targetName.endsWith('arm64') ? 'arm64' : 'x64' }
     const modules = join(BUILD_ROOT, 'node_modules')
+    const officeManifest = JSON.parse(readFileSync(join(modules, '@deepseek-ai/libreoffice-kit/package.json'), 'utf8'))
+    const officeEngine = selectOfficeEngine(officeManifest, target)
     mkdirSync(DSH_OUTPUT_ROOT, { recursive: true })
     cpSync(modules, join(DSH_OUTPUT_ROOT, 'node_modules'), {
       recursive: true, dereference: true,
-      filter: source => desktopRuntimeFileExclusion(relative(modules, source), target) === undefined,
+      filter: source => desktopRuntimeFileExclusion(relative(modules, source), target, officeEngine) === undefined,
     })
     writeFileSync(join(DSH_OUTPUT_ROOT, 'package.json'), `${JSON.stringify({
       name: '@deepseek-ai/dsh-desktop-runtime', private: true, version: release.version, type: 'module',
@@ -137,7 +140,6 @@ async function main(): Promise<void> {
         throw new Error(`desktop runtime: missing private Host file ${file}`)
       }
     }
-    const officeEngine = target.platform === 'linux' ? 'wasm' : `${target.platform}-${target.arch}`
     if (!existsSync(join(DSH_OUTPUT_ROOT, 'node_modules', '@deepseek-ai', `libreoffice-kit-${officeEngine}`, 'prebuilds.json'))) {
       throw new Error(`desktop runtime: missing required LibreOffice engine ${officeEngine}`)
     }

+ 3 - 3
apps/desktop/scripts/runtime-file-policy.ts

@@ -6,10 +6,11 @@
  * node_modules tree, including nested package containers.
  * @param path - Path relative to the production node_modules directory.
  * @param target - Platform and architecture of the Electron Node runtime.
+ * @param officeEngine - Engine selected from the installed kit manifest.
  * @returns Omission reason, or undefined when the entry must be copied.
  */
 export function desktopRuntimeFileExclusion(
-  path: string, target: { platform: NodeJS.Platform; arch: string },
+  path: string, target: { platform: NodeJS.Platform; arch: string }, officeEngine: string,
 ): string | undefined {
   const parts = path.split(/[\\/]/u)
   if (parts.some(part => ['.bin', '.pnpm', '.modules.yaml', '.pnpm-workspace-state-v1.json'].includes(part))) {
@@ -24,8 +25,7 @@ export function desktopRuntimeFileExclusion(
   const name = packageParts.slice(0, nameParts).join('/')
   const entry = packageParts.slice(nameParts).join('/')
   if (name.startsWith('@deepseek-ai/libreoffice-kit-')) {
-    const engine = target.platform === 'linux' ? 'wasm' : `${target.platform}-${target.arch}`
-    if (name !== `@deepseek-ai/libreoffice-kit-${engine}`) return 'LibreOffice other platform'
+    if (name !== `@deepseek-ai/libreoffice-kit-${officeEngine}`) return 'LibreOffice other platform'
   }
   if (name === 'fs-ext' && /^build\/(?:Release|Debug)\/(?:obj(?:\/|$)|fs_ext\.(?:exp|lib|pdb|iobj|ipdb)$)/u.test(entry)) {
     return 'fs-ext compiler output'

+ 21 - 8
apps/desktop/tests/runtime-file-policy.spec.ts

@@ -59,7 +59,7 @@ it('omits development artifacts while preserving executable modules, assets and
     }
     cpSync(modules, join(output, 'node_modules'), {
       recursive: true, dereference: true,
-      filter: path => desktopRuntimeFileExclusion(relative(modules, path), windows) === undefined,
+      filter: path => desktopRuntimeFileExclusion(relative(modules, path), windows, 'win32-x64') === undefined,
     })
     for (const path of removed) expect(existsSync(join(output, 'node_modules', path)), path).toBe(false)
     for (const path of retained) expect(readFileSync(join(output, 'node_modules', path), 'utf8'), path).toBe(`payload:${path}`)
@@ -72,15 +72,28 @@ it('omits development artifacts while preserving executable modules, assets and
 })
 
 it('applies package-specific rules inside scoped and nested dependency containers', () => {
-  expect(desktopRuntimeFileExclusion('outer/node_modules/@mixmark-io/domino/test/data.html', windows)).toBeDefined()
-  expect(desktopRuntimeFileExclusion('outer\\node_modules\\node-pty\\prebuilds\\win32-arm64\\conpty.node', windows)).toBeDefined()
-  expect(desktopRuntimeFileExclusion('outer/node_modules/unrelated/test/data.html', windows)).toBeUndefined()
-  expect(desktopRuntimeFileExclusion('outer/fs-ext/build/Release/fs_ext.lib', windows)).toBeUndefined()
+  expect(desktopRuntimeFileExclusion('outer/node_modules/@mixmark-io/domino/test/data.html', windows, 'win32-x64')).toBeDefined()
+  expect(desktopRuntimeFileExclusion('outer\\node_modules\\node-pty\\prebuilds\\win32-arm64\\conpty.node', windows, 'win32-x64')).toBeDefined()
+  expect(desktopRuntimeFileExclusion('outer/node_modules/unrelated/test/data.html', windows, 'win32-x64')).toBeUndefined()
+  expect(desktopRuntimeFileExclusion('outer/fs-ext/build/Release/fs_ext.lib', windows, 'win32-x64')).toBeUndefined()
 })
 
 it('retains native prebuilds for the selected macOS architecture', () => {
   const mac = { platform: 'darwin' as const, arch: 'arm64' }
-  expect(desktopRuntimeFileExclusion('node-pty/prebuilds/darwin-arm64/pty.node', mac)).toBeUndefined()
-  expect(desktopRuntimeFileExclusion('node-pty/prebuilds/darwin-x64/pty.node', mac)).toBeDefined()
-  expect(desktopRuntimeFileExclusion('node-pty/prebuilds/win32-x64/conpty.node', mac)).toBeDefined()
+  expect(desktopRuntimeFileExclusion('node-pty/prebuilds/darwin-arm64/pty.node', mac, 'darwin-arm64')).toBeUndefined()
+  expect(desktopRuntimeFileExclusion('node-pty/prebuilds/darwin-x64/pty.node', mac, 'darwin-arm64')).toBeDefined()
+  expect(desktopRuntimeFileExclusion('node-pty/prebuilds/win32-x64/conpty.node', mac, 'darwin-arm64')).toBeDefined()
+})
+
+it.each([
+  ['linux', 'x64', 'linux-x64'],
+  ['linux', 'arm64', 'wasm'],
+  ['freebsd', 'x64', 'wasm'],
+] as const)('retains only the selected Office engine for %s/%s', (platform, arch, engine) => {
+  for (const candidate of ['linux-x64', 'darwin-arm64', 'wasm']) {
+    const omitted = desktopRuntimeFileExclusion(
+      `@deepseek-ai/libreoffice-kit-${candidate}/prebuilds.json`, { platform, arch }, engine,
+    )
+    expect(omitted === undefined).toBe(candidate === engine)
+  }
 })

+ 2 - 2
docs/capability-seams.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/capability-seams.md
-capability-seams.md: ebe0bb074ad2d0c3739b83ddb3f08d1165e85277
-capability-seams.zh.md: 16707057fcd3f654c635f950c5f7fb77f23a3248
+capability-seams.md: 929173ab44c9f4cb4dbff8871fb473d9abff0141
+capability-seams.zh.md: f0526445a86fb0b0875bbca1382607c28b9db867

+ 1 - 1
docs/capability-seams.md

@@ -533,7 +533,7 @@ flowchart LR
 | `ctx.mcpResources` | `seam` | [`mcp-resources`](../packages/mcp/mcp-resources) | [`mcp-client`](../packages/mcp/mcp-client) | [`mcp-resources`](../packages/mcp/mcp-resources) | - | Connection-owned providers serve shared resource tools in the calling agent scope. |
 | `ctx.browserUse` | `seam` | [`browser-use`](../packages/browser-use/browser-use) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | - | One provider-owned name per service instance. Providers own their tools and browser resources per live Session; the shared service has no browser operation API. |
 | `ctx.computerUse` | `seam` | [`computer-use`](../packages/computer-use/computer-use) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | - | One provider-owned name per service instance. Each provider also owns its model tools; the service has no common action API, runtime selection, or Session workflow lock. |
-| `ctx.documentConvert` | `seam` | [`document-convert`](../packages/document/document-convert) | [`document-convert-libreoffice`](../packages/document/document-convert-libreoffice) | - | - | Authorized Office bytes are converted on the Host using native engines on macOS/Windows and Node WASM on Linux. |
+| `ctx.documentConvert` | `seam` | [`document-convert`](../packages/document/document-convert) | [`document-convert-libreoffice`](../packages/document/document-convert-libreoffice) | - | - | Authorized Office bytes are converted on the Host using the declared native target engine, or Node WASM when no native target is declared. |
 | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. |
 | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | Owns streaming intake, durable storage, and staged receipt lifetime; the Session controller binds receipts to accepted submissions. |
 | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |

+ 1 - 1
docs/capability-seams.zh.md

@@ -535,7 +535,7 @@ flowchart LR
 | `ctx.mcpResources` | `seam` | [`mcp-resources`](../packages/mcp/mcp-resources) | [`mcp-client`](../packages/mcp/mcp-client) | [`mcp-resources`](../packages/mcp/mcp-resources) | - | 连接所有者提供的操作在调用 agent 的作用域内服务于共享资源工具。 |
 | `ctx.browserUse` | `seam` | [`browser-use`](../packages/browser-use/browser-use) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp), [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp), [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | - | 每个服务实例注册一个提供方拥有的名称。提供方按实时 Session 拥有自己的工具与浏览器资源;共享服务不提供浏览器操作 API。 |
 | `ctx.computerUse` | `seam` | [`computer-use`](../packages/computer-use/computer-use) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp), [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | - | 每个服务实例只注册一个提供方自定的名称。各提供方也拥有自己的模型工具;服务不提供通用操作 API、运行时选择或 Session 流程锁。 |
-| `ctx.documentConvert` | `seam` | [`document-convert`](../packages/document/document-convert) | [`document-convert-libreoffice`](../packages/document/document-convert-libreoffice) | - | - | 已授权的 Office 字节在宿主上转换;macOS/Windows 使用原生引擎,Linux 使用 Node WASM。 |
+| `ctx.documentConvert` | `seam` | [`document-convert`](../packages/document/document-convert) | [`document-convert-libreoffice`](../packages/document/document-convert-libreoffice) | - | - | 已授权的 Office 字节在宿主上使用已声明的原生目标引擎转换;未声明原生目标时使用 Node WASM。 |
 | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 |
 | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | 负责流式接收、持久存储和暂存回执生命周期;Session Controller 将回执绑定到已接受的提交。 |
 | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 |

+ 2 - 2
docs/subsystems/document-convert.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/document-convert.md
-document-convert.md: 2dea31918e92524606ad85826de276c2af05f06d
-document-convert.zh.md: 22f0ec54615ebbf86743de6320ec4e8cc3fc8113
+document-convert.md: 1e17edb6314ef7c99cbf29313f64faa6a4b918cf
+document-convert.zh.md: 478043bae519ecab2d3c2ccbc46426f14c9c4f6e

+ 1 - 1
docs/subsystems/document-convert.md

@@ -29,7 +29,7 @@ The provider admits the deferred read before allocating source bytes, shares con
 
 ## Engine selection and limits
 
-The external [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit) Node API selects its precompiled engines. The kit has an independent version and release workflow, defined by the [release ownership decision](../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md). Application builds install the published npm packages. macOS and Windows require their matching ARM64 or x64 native engine; Linux uses Node WASM. The [platform engine decision](../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md) defines installation and packaging. Invalid metadata, missing required assets, and conversion errors reject without switching engines. Conversion uses disk input and output paths on the Host, with no browser conversion engine or font RPC.
+The external [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit) Node API selects its precompiled engines. The kit has an independent version and release workflow, defined by the [release ownership decision](../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md). Application builds install the published npm packages. Application packaging requires the target’s declared native engine, or Node WASM when the kit declares no native engine for that target. The [platform engine decision](../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md) defines installation and packaging. Invalid metadata, missing required assets, and conversion errors reject without switching engines. Conversion uses disk input and output paths on the Host, with no browser conversion engine or font RPC.
 
 The [Host provider configuration](../../packages/document/document-convert-libreoffice/README.md#use-this-package) owns concurrency, deadlines, input/output limits, archive limits, image resolution, and font access. Native/WASM implementation and asset distribution belong to the kit workspace. System LibreOffice discovery, runtime engine downloads, persistent PDF caching, and model-facing rendering are outside this provider.
 

+ 1 - 1
docs/subsystems/document-convert.zh.md

@@ -29,7 +29,7 @@
 
 ## 引擎选择和限制
 
-外部 [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit) Node API 选择其预编译引擎。kit 独立维护版本和发布流程,具体归属由[发布归属决策](../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md)定义。应用构建时安装已发布的 npm 包。macOS 和 Windows 要求匹配的 ARM64 或 x64 原生引擎;Linux 使用 Node WASM。[平台引擎决策](../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md)定义安装和打包规则。元数据无效、必需资源缺失和转换错误都会拒绝请求,不切换引擎。转换在 Host 使用磁盘输入输出路径,不使用浏览器转换引擎或字体 RPC。
+外部 [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit) Node API 选择其预编译引擎。kit 独立维护版本和发布流程,具体归属由[发布归属决策](../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md)定义。应用构建时安装已发布的 npm 包。应用打包要求目标已声明的原生引擎;kit 未为该目标声明原生引擎时使用 Node WASM。[平台引擎决策](../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md)定义安装和打包规则。元数据无效、必需资源缺失和转换错误都会拒绝请求,不切换引擎。转换在 Host 使用磁盘输入输出路径,不使用浏览器转换引擎或字体 RPC。
 
 [Host 提供方配置](../../packages/document/document-convert-libreoffice/README.zh.md#use-this-package)负责并发、期限、输入输出上限、归档上限、图像分辨率和字体访问。原生/WASM 实现和资产分发属于 kit 工作区。系统 LibreOffice 探测、运行时引擎下载、持久 PDF 缓存和面向模型的渲染不属于此提供方。
 

+ 2 - 2
packages/document/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/document/README.md
-README.md: 0f9d3f1ae0cb154a445e34debe0bf49ef5400d1e
-README.zh.md: 0325634d6c435b6d8aeebf202d04f84fa4975e33
+README.md: 693a831fb7a501f8c0376271715298c06b33efd8
+README.zh.md: ac4719874761c9267c25eee763013b5f7f5d9290

+ 1 - 1
packages/document/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Convert authorized Office files to reusable PDFs on the Host. The conversion family provides a shared conversion operation and a LibreOffice kit provider. macOS and Windows use their matching native engine; Linux uses Node WASM.
+Convert authorized Office files to reusable PDFs on the Host. The conversion family provides a shared conversion operation and a LibreOffice kit provider. Targets with a declared native engine use it; other targets use Node WASM.
 
 ## Table of Contents
 

+ 1 - 1
packages/document/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-group"
 
 ## 摘要
 
-在宿主上将已授权的 Office 文件转换为可复用的 PDF。转换包族提供共享转换操作和 LibreOffice kit 提供方。macOS 和 Windows 使用匹配的原生引擎;Linux 使用 Node WASM。
+在宿主上将已授权的 Office 文件转换为可复用的 PDF。转换包族提供共享转换操作和 LibreOffice kit 提供方。声明了原生引擎的目标使用原生引擎,其余目标使用 Node WASM。
 
 ## 目录
 

+ 2 - 2
packages/document/document-convert-libreoffice/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/document/document-convert-libreoffice/README.md
-README.md: 1c287b714ace289fa03f8be6da722fbe78e64065
-README.zh.md: 796d14c3bd873a3a6a24d4691f0d406d0894a122
+README.md: c083215ea2e3fa067043dac045becd892e0ca406
+README.zh.md: d10c1a14792c80b3608b3302958de4546b817cda

+ 2 - 2
packages/document/document-convert-libreoffice/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Convert Office documents to PDFs on the Host computer. macOS and Windows use their matching native LibreOffice engine; Linux uses Node WASM. The provider accepts DOC, DOCX, XLS, XLSX, PPT, and PPTX. OOXML conversion returns missing-font names; binary Office conversion returns an empty list.
+Convert Office documents to PDFs on the Host computer. Targets with a declared native LibreOffice engine use it; other targets use Node WASM. The provider accepts DOC, DOCX, XLS, XLSX, PPT, and PPTX. OOXML conversion returns missing-font names; binary Office conversion returns an empty list.
 
 ## Table of Contents
 
@@ -27,7 +27,7 @@ Convert Office documents to PDFs on the Host computer. macOS and Windows use the
 
 The [Web bundle](../../bundle/web-app/README.md) mounts this provider as `document-convert`. Independent compositions mount `@deepseek-ai/dsh-document-convert-libreoffice` as a `cordis.yml` row.
 
-The provider depends on the independently published [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit/tree/main/packages/entry) npm API at kit version `0.0.1`. npm selects the matching macOS/Windows native package or the Linux-only WASM package. A missing native engine rejects conversion on macOS/Windows without selecting WASM. The [platform engine decision](../../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md) defines installation and packaging; the [release ownership decision](../../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md) defines the independent kit and Harness responsibilities.
+The provider depends on the independently published [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit/tree/main/packages/entry) npm API at kit version `0.0.1`. Application packaging selects the matching native package declared in the kit’s `optionalDependencies`, or WASM when no native package is declared for that target. A missing declared native engine rejects packaging without selecting WASM. The [platform engine decision](../../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.md) defines installation and packaging; the [release ownership decision](../../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.md) defines the independent kit and Harness responsibilities.
 
 | Field | Default | Meaning |
 |---|---|---|

+ 2 - 2
packages/document/document-convert-libreoffice/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-在宿主计算机上将 Office 文档转换为 PDF。macOS 和 Windows 使用匹配的原生 LibreOffice 引擎;Linux 使用 Node WASM。提供方支持 DOC、DOCX、XLS、XLSX、PPT 和 PPTX。OOXML 转换返回缺失字体名称;二进制 Office 转换返回空列表。
+在宿主计算机上将 Office 文档转换为 PDF。声明了原生 LibreOffice 引擎的目标使用原生引擎,其余目标使用 Node WASM。提供方支持 DOC、DOCX、XLS、XLSX、PPT 和 PPTX。OOXML 转换返回缺失字体名称;二进制 Office 转换返回空列表。
 
 ## 目录
 
@@ -27,7 +27,7 @@ kind: "package-reference"
 
 [Web bundle](../../bundle/web-app/README.zh.md)以 `document-convert` 条目挂载此提供方。独立组合通过 `cordis.yml` 条目挂载 `@deepseek-ai/dsh-document-convert-libreoffice`。
 
-此 provider 依赖独立发布的 [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit/tree/main/packages/entry) npm API,kit 版本为 `0.0.1`。npm 选择匹配的 macOS/Windows 原生包或仅限 Linux 的 WASM 包。macOS/Windows 缺少原生引擎时拒绝转换,不会选择 WASM。[平台引擎决策](../../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md)定义安装与打包策略;[发布归属决策](../../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md)定义独立 kit 与 Harness 各自的职责。
+此 provider 依赖独立发布的 [`@deepseek-ai/libreoffice-kit`](https://github.com/deepseek-harness/libreoffice-kit/tree/main/packages/entry) npm API,kit 版本为 `0.0.1`。应用打包选择 kit 的 `optionalDependencies` 中声明的匹配原生包;目标没有声明原生包时选择 WASM。已声明的原生引擎缺失时拒绝打包,不会选择 WASM。[平台引擎决策](../../../.agents/notes/implemented/architecture/2026-09-15-platform-office-engines.zh.md)定义安装与打包策略;[发布归属决策](../../../.agents/notes/implemented/architecture/2026-09-14-independent-libreoffice-kit.zh.md)定义独立 kit 与 Harness 各自的职责。
 
 | 字段 | 默认值 | 含义 |
 |---|---|---|

+ 1 - 1
packages/document/document-convert-libreoffice/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-document-convert-libreoffice",
-  "description": "Host Office conversion with native macOS/Windows engines and Linux Node WASM",
+  "description": "Host Office conversion with a declared native target engine or Node WASM",
   "version": "0.1.6-alpha.1",
   "publishConfig": {
     "access": "public"

+ 2 - 2
python/sdk-runtime/README.i18n.yaml

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

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

@@ -39,4 +39,4 @@ Production deployment permits unused workspace patches for packages outside the
 
 From the repository root, `pnpm exec tsx scripts/build-exe-for-python-sdk.ts` verifies the closure, builds packages, deploys a symlink-free tree, packages the selected target, and syncs the executable and sidecars into this module. `scripts/build-python-release.py` stages release-shaped wheels at the root repository version and pins `deepseek-harness-sdk` to the exact runtime version.
 
-The installed-wheel smoke creates a clean virtual environment outside the checkout, proves the installed distribution and executable identities, then exercises default and customized SDK profiles, external plugins, MCP, native tools, direct JSON-RPC, committed snapshots, and the real provider on trusted runs. Its Office scenario relocates the complete target payload and converts DOCX with the required platform engine: native on macOS/Windows and WASM on Linux. See the [Python contributor workflow](../development.md) and [installed-wheel testing decision](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md).
+The installed-wheel smoke creates a clean virtual environment outside the checkout, proves the installed distribution and executable identities, then exercises default and customized SDK profiles, external plugins, MCP, native tools, direct JSON-RPC, committed snapshots, and the real provider on trusted runs. Its Office scenario relocates the complete target payload and converts DOCX with the required platform engine: the target’s declared native engine, or WASM when no native engine is declared. See the [Python contributor workflow](../development.md) and [installed-wheel testing decision](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md).

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

@@ -39,4 +39,4 @@ Python bootstrap 从相邻目录解析 Office kit,让原生辅助程序与 URL
 
 在仓库根目录运行 `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`,会校验闭包、构建包、部署无符号链接的文件树、打包所选目标,并把可执行程序及伴随文件同步到本模块。`scripts/build-python-release.py` 按仓库根版本暂存发布形态的 wheel 包,并将 `deepseek-harness-sdk` 固定到完全相同的运行时版本。
 
-已安装 wheel 包冒烟测试会在检出目录外创建干净的虚拟环境,验证分发物与可执行程序的来源,然后覆盖默认及自定义 SDK profile、外部插件、MCP、原生工具、直接 JSON-RPC、检入快照,以及可信运行中的真实提供方。Office 场景会迁移完整的目标载荷目录,并使用所需的平台引擎转换 DOCX:macOS/Windows 使用原生引擎,Linux 使用 WASM。另见 [Python 贡献者工作流](../development.zh.md) 与 [installed-wheel 测试决策](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)。
+已安装 wheel 包冒烟测试会在检出目录外创建干净的虚拟环境,验证分发物与可执行程序的来源,然后覆盖默认及自定义 SDK profile、外部插件、MCP、原生工具、直接 JSON-RPC、检入快照,以及可信运行中的真实提供方。Office 场景会迁移完整的目标载荷目录,并使用所需的平台引擎转换 DOCX:使用目标已声明的原生引擎,未声明原生引擎时使用 WASM。另见 [Python 贡献者工作流](../development.zh.md) 与 [installed-wheel 测试决策](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)。

+ 9 - 4
python/sdk-runtime/hatch_build.py

@@ -74,7 +74,6 @@ class RuntimeBuildHook(BuildHookInterface):
             )
         expected_executable = matches[0][1]
         target = next(name for name, value in _PLATFORMS.items() if value[0] == platform_tag)
-        engine = "wasm" if target.startswith("linux-") else target.replace("win-", "win32-").replace("macos-", "darwin-")
         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 []
@@ -96,9 +95,15 @@ class RuntimeBuildHook(BuildHookInterface):
             )
         for executable in runtime_files:
             if executable == office:
-                for required in ("@deepseek-ai/libreoffice-kit/package.json", f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json"):
-                    if not (office / "node_modules" / required).is_file():
-                        raise RuntimeError(f"runtime Office dependency is missing: {office / required}")
+                adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
+                if not adapter.is_file():
+                    raise RuntimeError(f"runtime Office dependency is missing: {adapter}")
+                native = target.replace("win-", "win32-").replace("macos-", "darwin-")
+                declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {})
+                engine = native if f"@deepseek-ai/libreoffice-kit-{native}" in declared else "wasm"
+                required = office / "node_modules" / f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json"
+                if not required.is_file():
+                    raise RuntimeError(f"runtime Office dependency is missing: {required}")
                 continue
             if not executable.is_file():
                 raise RuntimeError(f"runtime executable is not a file: {executable}")

+ 15 - 7
python/sdk-runtime/src/deepseek_harness_runtime/__init__.py

@@ -21,6 +21,7 @@ console command requires ``DSH_HOME`` for the same reason.
 
 from __future__ import annotations
 
+import json
 import os
 import platform
 import shutil
@@ -89,13 +90,20 @@ def bundled_runtime_path() -> Path:
                 + _EXE_ACQUISITION_HINT
             )
     office = path.with_name(f"{path.name.removesuffix('.exe')}-office")
-    engine = "wasm" if tag.startswith("linux-") else tag.replace("win-", "win32-").replace("macos-", "darwin-")
-    for required in ("@deepseek-ai/libreoffice-kit/package.json", f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json"):
-        if not (office / "node_modules" / required).is_file():
-            raise FileNotFoundError(
-                f"deepseek-harness-runtime-bin is missing the Office sidecar at {office}. "
-                + _EXE_ACQUISITION_HINT
-            )
+    adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
+    if not adapter.is_file():
+        raise FileNotFoundError(
+            f"deepseek-harness-runtime-bin is missing the Office sidecar at {office}. "
+            + _EXE_ACQUISITION_HINT
+        )
+    native = tag.replace("win-", "win32-").replace("macos-", "darwin-")
+    declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {})
+    engine = native if f"@deepseek-ai/libreoffice-kit-{native}" in declared else "wasm"
+    if not (office / "node_modules" / f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json").is_file():
+        raise FileNotFoundError(
+            f"deepseek-harness-runtime-bin is missing the Office sidecar engine {engine} at {office}. "
+            + _EXE_ACQUISITION_HINT
+        )
     return path
 
 

+ 4 - 1
python/sdk/tests/test_release_version.py

@@ -164,6 +164,7 @@ def test_stage_runtime_rejects_a_noncanonical_executable_name(tmp_path: Path) ->
 @pytest.mark.parametrize("platform_tag,selected", [
     ("manylinux_2_28_x86_64", "wasm"), ("macosx_14_0_arm64", "darwin-arm64"),
     ("macosx_14_0_x86_64", "darwin-x64"), ("win_amd64", "win32-x64"),
+    ("manylinux_2_28_x86_64", "linux-x64"), ("macosx_14_0_arm64", "wasm"),
 ])
 @pytest.mark.parametrize("invalid", [None, "asset", "missing-engine", "foreign-engine", "helper-mode"])
 def test_office_wheel_requires_only_target_engine(
@@ -177,7 +178,9 @@ def test_office_wheel_requires_only_target_engine(
     engine = ({"kind": "native", "executable": "bin/helper"} if native else
               {"kind": "wasm", "loader": "loader.cjs", "wasm": "engine.wasm", "data": "engine.data", "metadata": "fonts.json"})
     with zipfile.ZipFile(wheel, "w") as archive:
-        archive.writestr(f"{root}/@deepseek-ai/libreoffice-kit/package.json", "{}")
+        archive.writestr(f"{root}/@deepseek-ai/libreoffice-kit/package.json", json.dumps({
+            "optionalDependencies": {f"@deepseek-ai/libreoffice-kit-{selected}": "0.0.1"},
+        }))
         base = f"{root}/@deepseek-ai/libreoffice-kit-{selected}"
         if invalid != "missing-engine":
             archive.writestr(f"{base}/prebuilds.json", json.dumps({"engine": engine}))

+ 18 - 6
python/sdk/tests/test_runtime_resolution.py

@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import json
 import os
 import subprocess
 import sys
@@ -19,14 +20,19 @@ from deepseek_harness_runtime import (
 )
 
 
-def _office_sidecar(executable: Path) -> Path:
+def _office_sidecar(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")
-    engine = "wasm" if tag.startswith("linux-") else tag.replace("win-", "win32-").replace("macos-", "darwin-")
+    native = tag.replace("win-", "win32-").replace("macos-", "darwin-")
+    engine = native if native in native_targets else "wasm"
     for required in ("@deepseek-ai/libreoffice-kit/package.json", f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json"):
         path = office / "node_modules" / required
         path.parent.mkdir(parents=True, exist_ok=True)
         path.write_text("{}")
+    adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
+    adapter.write_text(json.dumps({"optionalDependencies": {
+        f"@deepseek-ai/libreoffice-kit-{target}": "0.0.1" for target in (*native_targets, "wasm")
+    }}), encoding="utf-8")
     return office
 
 
@@ -229,8 +235,14 @@ def test_windows_console_branch_preserves_real_child_io_and_completion(tmp_path:
     assert sentinel.read_text() == "done"
 
 
-@pytest.mark.parametrize("target", ["linux-x64", "linux-arm64", "macos-arm64", "macos-x64", "win-x64"])
-def test_runtime_requires_its_platform_office_engine(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, target: str) -> None:
+@pytest.mark.parametrize("target,native_targets", [
+    ("linux-x64", ()), ("linux-arm64", ()),
+    ("macos-arm64", ("darwin-arm64",)), ("macos-x64", ("darwin-x64",)), ("win-x64", ("win32-x64",)),
+    ("linux-x64", ("linux-x64",)), ("macos-arm64", ()),
+])
+def test_runtime_requires_its_platform_office_engine(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch, target: str, native_targets: tuple[str, ...],
+) -> None:
     extension = ".exe" if target.startswith("win-") else ""
     executable = tmp_path / "runtime" / f"deepseek-harness-sdk-runtime-{target}{extension}"
     executable.parent.mkdir()
@@ -238,13 +250,13 @@ def test_runtime_requires_its_platform_office_engine(tmp_path: Path, monkeypatch
     executable.with_name(f"{executable.stem}-rg{extension}").touch()
     if target.startswith("macos-"):
         Path(f"{executable}-spawn-helper").touch()
-    office = _office_sidecar(executable)
+    office = _office_sidecar(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
     engine = next(office.glob("node_modules/@deepseek-ai/libreoffice-kit-*/prebuilds.json"))
     engine.unlink()
-    foreign = office / "node_modules/@deepseek-ai" / ("libreoffice-kit-darwin-arm64" if target.startswith("linux-") else "libreoffice-kit-wasm") / "prebuilds.json"
+    foreign = office / "node_modules/@deepseek-ai" / ("libreoffice-kit-darwin-arm64" if engine.parent.name == "libreoffice-kit-wasm" else "libreoffice-kit-wasm") / "prebuilds.json"
     foreign.parent.mkdir(parents=True, exist_ok=True)
     foreign.write_text("{}")
     with pytest.raises(FileNotFoundError, match="Office sidecar"):

+ 13 - 9
scripts/build-exe-for-python-sdk-office.spec.ts

@@ -94,33 +94,37 @@ it('rejects an ancestor dependency outside the deployed closure', async () => {
     .rejects.toThrow('outside the deployed closure')
 })
 
-it('rejects an optional WASM engine resolved outside the deployed closure', async () => {
+it('requires the declared WASM engine inside the deployed closure', async () => {
   const { root, staging, destination, packageAt } = await fixture()
   await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } })
-  const ambient = await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root)
+  await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root)
   await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
-    .rejects.toThrow(`Python Office dependency is outside the deployed closure: ${ambient}`)
+    .rejects.toThrow('Python Office engine @deepseek-ai/libreoffice-kit-wasm required for linux/x64 is missing.')
   await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
 })
 
 it.each([
-  ['darwin', 'arm64'], ['darwin', 'x64'], ['win32', 'arm64'], ['win32', 'x64'], ['linux', 'x64'],
-])('copies only the %s/%s engine even when other engines are staged', async (platform, arch) => {
+  ['darwin', 'arm64', 'darwin-arm64'], ['darwin', 'x64', 'darwin-x64'],
+  ['win32', 'arm64', 'win32-arm64'], ['win32', 'x64', 'win32-x64'],
+  ['linux', 'x64', 'linux-x64'], ['linux', 'arm64', 'wasm'], ['freebsd', 'x64', 'wasm'],
+])('copies only the %s/%s engine even when other engines are staged', async (platform, arch, selected) => {
   const { staging, destination, packageAt } = await fixture()
-  const targets = ['darwin-arm64', 'darwin-x64', 'win32-arm64', 'win32-x64', 'wasm']
+  const targets = ['darwin-arm64', 'darwin-x64', 'win32-arm64', 'win32-x64', 'linux-x64', 'wasm']
   const names = targets.map(target => `@deepseek-ai/libreoffice-kit-${target}`)
   await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: Object.fromEntries(names.map(name => [name, '0.0.1'])) })
   for (const name of names) await packageAt(name)
-  const expected = `@deepseek-ai/libreoffice-kit-${platform === 'linux' ? 'wasm' : `${platform}-${arch}`}`
+  const expected = `@deepseek-ai/libreoffice-kit-${selected}`
   const packages = await copyOfficeSidecar(staging, destination, { platform, arch })
   expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([
     'node_modules/@deepseek-ai/libreoffice-kit', `node_modules/${expected}`,
   ])
 })
 
-it.each(['darwin', 'win32'])('%s requires its native package even when WASM is staged', async (platform) => {
+it.each(['darwin', 'win32', 'linux'])('%s requires its native package even when WASM is staged', async (platform) => {
   const { staging, destination, packageAt } = await fixture()
-  await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } })
+  await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: {
+    '@deepseek-ai/libreoffice-kit-wasm': '0.0.1', [`@deepseek-ai/libreoffice-kit-${platform}-arm64`]: '0.0.1',
+  } })
   await packageAt('@deepseek-ai/libreoffice-kit-wasm')
   await expect(copyOfficeSidecar(staging, destination, { platform, arch: 'arm64' }))
     .rejects.toThrow(`Python Office engine @deepseek-ai/libreoffice-kit-${platform}-arm64 required for ${platform}/arm64 is missing.`)

+ 7 - 4
scripts/build-exe-for-python-sdk-office.ts

@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs'
 import { cp, mkdir, readFile, rm } from 'node:fs/promises'
 import { createRequire } from 'node:module'
 import { isAbsolute, join, relative, sep } from 'node:path'
+import { selectOfficeEngine } from './libreoffice-engine.ts'
 
 /** pkg applies these exclusions to dependency `files` as well as root asset globs. */
 export const OFFICE_ASSET_IGNORES = [
@@ -22,7 +23,7 @@ interface PackageManifest {
 
 /**
  * Copy the installed Office dependency tree without changing package contents or executable modes.
- * Harness sidecars require the target native engine on macOS/Windows and WASM on Linux.
+ * Harness sidecars require the kit's declared target native engine, or WASM for other targets.
  * Missing target engines fail with their package name; missing required dependencies or paths outside the deployed closure also fail.
  * @param staging - Symlink-free deployed Node closure.
  * @param destination - Target-specific Office directory beside the executable; replaced when present.
@@ -34,7 +35,9 @@ export async function copyOfficeSidecar(
   destination: string,
   target: { platform: string; arch: string },
 ): Promise<string[]> {
-  const engineName = `@deepseek-ai/libreoffice-kit-${target.platform === 'linux' ? 'wasm' : `${target.platform}-${target.arch}`}`
+  const entry = join(staging, 'node_modules', '@deepseek-ai', 'libreoffice-kit')
+  const manifest = JSON.parse(await readFile(join(entry, 'package.json'), 'utf8')) as PackageManifest
+  const engineName = `@deepseek-ai/libreoffice-kit-${selectOfficeEngine(manifest, target)}`
   const packages = new Set<string>()
 
   async function visit(packageDirectory: string): Promise<void> {
@@ -53,7 +56,7 @@ export async function copyOfficeSidecar(
       ...Object.keys(manifest.peerDependencies ?? {}),
     ])
     for (const name of dependencies) {
-      if (name.startsWith('@deepseek-ai/libreoffice-kit-') && name !== engineName) continue
+      if (name.startsWith('@deepseek-ai/libreoffice-kit-')) continue
       const optional = manifest.optionalDependencies?.[name] !== undefined
         || manifest.peerDependenciesMeta?.[name]?.optional === true
       const dependencyDirectory = (require.resolve.paths(name) ?? [])
@@ -71,7 +74,7 @@ export async function copyOfficeSidecar(
     }
   }
 
-  await visit(join(staging, 'node_modules', '@deepseek-ai', 'libreoffice-kit'))
+  await visit(entry)
   const engineDirectory = join(staging, 'node_modules', engineName)
   if (!existsSync(engineDirectory)) throw new Error(`Python Office engine ${engineName} required for ${target.platform}/${target.arch} is missing.`)
   await visit(engineDirectory)

+ 3 - 1
scripts/build-python-release.py

@@ -237,7 +237,9 @@ def verify_office_payload(archive: zipfile.ZipFile, office_modules: str, platfor
         raise RuntimeError("Office dependency is missing: libreoffice-kit")
     engines = f"{office_modules}/@deepseek-ai"
     target = next(name for name, value in PLATFORMS.items() if value[0] == platform_tag)
-    selected = "wasm" if target.startswith("linux-") else target.replace("win-", "win32-").replace("macos-", "darwin-")
+    native_target = target.replace("win-", "win32-").replace("macos-", "darwin-")
+    declared = json.loads(archive.read(adapter)).get("optionalDependencies", {})
+    selected = native_target if f"@deepseek-ai/libreoffice-kit-{native_target}" in declared else "wasm"
     for required in (f"libreoffice-kit-{selected}/prebuilds.json",):
         if f"{engines}/{required}" not in names:
             raise RuntimeError(f"Office dependency is missing: {required}")

+ 1 - 1
scripts/gen-doc-graphs.ts

@@ -153,7 +153,7 @@ const SERVICE_ROLES: ServiceRole[] = [
   {
     key: 'documentConvert', pkg: 'document-convert', title: 'Office to PDF conversion',
     mode: 'seam', implementations: ['document-convert-libreoffice'], consumers: [],
-    note: 'Authorized Office bytes are converted on the Host using native engines on macOS/Windows and Node WASM on Linux.',
+    note: 'Authorized Office bytes are converted on the Host using the declared native target engine, or Node WASM when no native target is declared.',
   },
   {
     key: 'attachments',

+ 24 - 0
scripts/libreoffice-engine.spec.ts

@@ -0,0 +1,24 @@
+import { expect, it } from 'vitest'
+import { selectOfficeEngine } from './libreoffice-engine.ts'
+
+it.each([
+  ['darwin', 'arm64', 'darwin-arm64'], ['darwin', 'x64', 'darwin-x64'],
+  ['win32', 'arm64', 'win32-arm64'], ['win32', 'x64', 'win32-x64'],
+  ['linux', 'x64', 'wasm'], ['linux', 'arm64', 'wasm'],
+  ['darwin', 'other', 'wasm'], ['freebsd', 'x64', 'wasm'],
+])('selects the declared engine for %s/%s', (platform, arch, expected) => {
+  const optionalDependencies = Object.fromEntries(
+    ['darwin-arm64', 'darwin-x64', 'win32-arm64', 'win32-x64', 'wasm']
+      .map(engine => [`@deepseek-ai/libreoffice-kit-${engine}`, '0.0.1']),
+  )
+  expect(selectOfficeEngine({ optionalDependencies }, { platform, arch })).toBe(expected)
+})
+
+it('selects a declared Linux native target without an OS-specific policy change', () => {
+  expect(selectOfficeEngine({ optionalDependencies: { '@deepseek-ai/libreoffice-kit-linux-x64': '1' } },
+    { platform: 'linux', arch: 'x64' })).toBe('linux-x64')
+})
+
+it('uses WASM when no native targets are declared', () => {
+  expect(selectOfficeEngine({}, { platform: 'darwin', arch: 'x64' })).toBe('wasm')
+})

+ 17 - 0
scripts/libreoffice-engine.ts

@@ -0,0 +1,17 @@
+/** Select the single LibreOffice payload from the kit's declared native targets. */
+
+/**
+ * Require a declared native target even when its optional npm package is absent.
+ * Targets without a declared native engine use WASM.
+ * @param manifest - Installed LibreOffice kit package manifest.
+ * @param target - Node platform and architecture of the distribution.
+ * @returns Engine suffix used by the kit's npm packages.
+ */
+export function selectOfficeEngine(
+  manifest: { optionalDependencies?: Record<string, string> },
+  target: { platform: string; arch: string },
+): string {
+  const native = `${target.platform}-${target.arch}`
+  return Object.hasOwn(manifest.optionalDependencies ?? {}, `@deepseek-ai/libreoffice-kit-${native}`)
+    ? native : 'wasm'
+}

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

@@ -873,7 +873,11 @@ def smoke_sdk_office(executable: Path) -> None:
             else:
                 shutil.copy2(source, destination)
         office = root / f"{stem}-office"
-        expected_backend = "wasm" if sys.platform == "linux" else "native"
+        adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
+        native = stem.removeprefix("deepseek-harness-sdk-runtime-").replace("win-", "win32-").replace("macos-", "darwin-")
+        declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {})
+        selected = native if f"@deepseek-ai/libreoffice-kit-{native}" in declared else "wasm"
+        expected_backend = "wasm" if selected == "wasm" else "native"
         engines = [
             json.loads(manifest.read_text())["engine"]["kind"]
             for manifest in (office / "node_modules/@deepseek-ai").glob("libreoffice-kit-*/prebuilds.json")