feat(sdk): bundle shared Office authoring resources

This commit is contained in:
fz committed 2026-09-18 17:17:46 +08:00
1 parent 1e3afc63ad
commit 37983d9298
28 files changed
+367 -58

No files matched your search

@@ -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-17-shared-office-runtime.md
2026-09-17-shared-office-runtime.md: c25ce3a66660f1dca9959ecbb155ae38f7046d00
2026-09-17-shared-office-runtime.zh.md: 8889a6b13d55919d4b5e92b06d434186d70dbe6a
2026-09-17-shared-office-runtime.md: b8aca3e84c6b426f19d5db4149afef0592c73db7
2026-09-17-shared-office-runtime.zh.md: 792f4c8bc2dce532e57916c9e95d182161f99c16
@@ -10,14 +10,16 @@ SDK deployments need the same Office authoring libraries as Desktop while keepin
## 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 SDK profile enables the query and Office skills only when its carrier sets `DSH_PRIMARY_RUNTIME`; it resolves the payload path and reads the payload in place. The common base and `sdk-minimal` do not enable Office implicitly.
[`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. 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 [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 resource directory. 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.
@@ -32,4 +34,4 @@ The [Desktop primary-runtime decision](../feature/2026-09-14-desktop-primary-run
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. Linux ARM64 and 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.
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.
@@ -10,14 +10,16 @@ SDK 部署需要与 Desktop 相同的 Office 创作库,同时将解释器 payl
## 决策
[`tool-workspace-dependencies`](../../../../packages/skill/tool-workspace-dependencies/README.zh.md) 持有清单校验、解释器路径、安装逻辑和模型查询。它保留在 skill 组中,与 Office 工作流消费者相邻:payload 服务于这些工作流,本包不提供 workspace 实体服务。Desktop 引用本包并保留首次使用时复制安装。SDK profile 仅在载体设置 `DSH_PRIMARY_RUNTIME` 时启用查询和 Office skills;它解析 payload 路径并原位读取。公共 base 与 `sdk-minimal` 不会隐式启用 Office。
[`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 的下载锁、解压和本机冒烟检查。Desktop 提供输出路径和发布版本,要求包含 Node.js 与 pnpm,并应用签名专用子进程环境。其他载体选择自己的输出目录,也可构建 Python-only payload。现有 `desktopVersion` 清单字段保留名字并记录载体发布版本;`payloadDigest` 区分锁定输入与组件选择。
[共享构建入口](../../../../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 放在可执行文件同级的资源目录中。打包提供默认资源;skill 注册表仍允许项目、自定义目录和用户 skills 覆盖同名随包条目,profile patch 可在保留 Python 的同时禁用或替换 Office skills。这样既保留工作流配置能力,又省去单独安装环境的步骤。真实文件系统资源允许 Python 加载原生扩展,并让 skills 调用共用检查脚本,无需启动时解包。wheel 体积增加一个压缩 payload;发布流程仍执行公开索引的大小限制检查。
## 考虑过的替代方案
**将实现保留在 Desktop。** SDK 和容器构建器只需要解释器、Office 资源和查询,却仍会依赖应用打包及签名代码。
@@ -32,4 +34,4 @@ SDK 部署需要与 Desktop 相同的 Office 创作库,同时将解释器 payl
同一插件生命周期内的并发调用共享成功的工具准备结果。准备失败后可重试;卸载移除工具注册并等待未完成的文件系统工作。复制模式验证暂存替换内容,并在发布失败后恢复原安装。原位模式不写入 payload。路径为绝对路径,返回结果前会检查运行时元数据和所声明的条目。
SDK 配置变更需要重启。缺少 Office 资源会产生启动警告,并使可选的 skill 提供方保持未激活;运行时元数据和解释器是否存在在首次工具调用时校验。Linux ARM64 与 musl 构建仍不在锁文件范围内。跨目标组装校验归档哈希,但需要在目标主机执行;Linux 产物 CI 负责本机 Linux 检查。单元覆盖固定清单、安装与生命周期行为;SDK profile 测试观察可选工具、skill 发现与结果,workspace-dependencies 录制场景固定模型可见的校验错误。
SDK 配置变更需要重启。缺少 Office 资源会产生启动警告,并使可选的 skill 提供方保持未激活;运行时元数据和解释器是否存在在首次工具调用时校验。musl 构建仍不在锁文件范围内。跨目标组装校验归档哈希,但需要在目标主机执行;Linux 产物 CI 负责本机 Linux 检查。单元覆盖固定清单、安装与生命周期行为;SDK profile 测试观察可选工具、skill 发现与结果,workspace-dependencies 录制场景固定模型可见的校验错误。
@@ -432,9 +432,10 @@ async function officeFixture(root: string, pythonOnly: boolean) {
return { source, paths }
}
it.each(['unset', 'full', 'python-only', 'missing-assets'] as const)('composes Office resources through the SDK profile (%s)', async (mode) => {
it.each(['unset', 'empty', 'bundled', 'full', 'python-only', 'missing-assets'] 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 })
const requests: Record<string, unknown>[] = []
@@ -443,7 +444,7 @@ it.each(['unset', 'full', 'python-only', 'missing-assets'] as const)('composes O
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 && mode !== 'unset'
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: {} }
@@ -458,7 +459,8 @@ it.each(['unset', 'full', 'python-only', 'missing-assets'] as const)('composes O
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' ? undefined : source + '/',
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}` },
})
@@ -484,11 +486,11 @@ it.each(['unset', 'full', 'python-only', 'missing-assets'] as const)('composes O
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(mode !== 'unset')
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(mode === 'full' || mode === 'python-only')
expect(JSON.stringify(requests[0]!.messages).includes(name)).toBe(enabled && mode !== 'missing-assets')
}
if (mode !== 'unset') {
if (enabled) {
expect(requests).toHaveLength(2)
expect(requests[1]!.messages).toEqual(expect.arrayContaining([
expect.objectContaining({ role: 'user', content: expect.arrayContaining([
+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: acf038281ea56b435abfcaeb7e8afddcbfbfb7e4
README.zh.md: 8e5c3f994b3d435bcfe6997f6bddc9d917f7307a
README.md: 6d6106e6dd803cee5c51cee0a38f80705986ddfa
README.zh.md: 9de1fb6e17f3f83b7d8c2acbf87154b1cdfea38f
+1 -1
View File
@@ -33,7 +33,7 @@ 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.
A deployment that sets `DSH_PRIMARY_RUNTIME` enables the [bundled Office runtime query and skills](../../skill/tool-workspace-dependencies/README.md#use-this-package). Unset or empty configuration leaves both disabled. The carrier supplies a native payload and sibling `office-skills/`; the tool reads them in place, including from a read-only image layer. Missing skill resources produce a startup warning and leave Office skills unavailable. Invalid runtime metadata or missing interpreter entries fail the tool call.
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.
-----
+1 -1
View File
@@ -33,7 +33,7 @@ kind: "package-bundle"
SDK 使用 base 默认提供的 `read`、`write` 和 `edit`。要添加 `str_replace_editor`,请使用 [base 配置指南](../base/README.zh.md#use-this-package)中的显式插入 patch。独立的 `sdk-minimal` profile 自行决定其工具选择。
部署设置 `DSH_PRIMARY_RUNTIME` 后会启用[内置 Office 运行时查询与 skills](../../skill/tool-workspace-dependencies/README.zh.md#use-this-package)。未设置或为空时两者都禁用。载体提供本机 payload 与同级 `office-skills/`;工具原位读取,也支持只读镜像层。缺少 skill 资源会产生启动警告并使 Office skills 不可用。无效运行时元数据或缺失解释器条目会导致工具调用失败。
打包的 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)。
-----
+6 -9
View File
@@ -24,20 +24,17 @@
- id: hmr
disabled: true
# Bundled script runtime and Office skills, the composition Desktop mounts as
# `desktop-office`. A carrier that ships the primary-runtime payload (for
# example a container image layer) names it with DSH_PRIMARY_RUNTIME; its
# sibling `office-skills/` directory carries the skill resources. Missing resources
# produce a startup warning and leave the Office skill provider inactive. Unset, both rows stay disabled.
# 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"
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 ?? '')"
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"
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 ?? '', '..', 'office-skills')"
assetRoot: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? process.env.DSH_BUNDLED_PRIMARY_RUNTIME ?? '', '..', 'office-skills')"
@@ -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/tool-workspace-dependencies/README.md
README.md: 092bc9084ad2807d830520ac540e661d9fe17bcb
README.zh.md: 533a4f8c931a501e5f725cd099dbee72b7d9e436
README.md: 36d1f0080712d6ed71917ead5097beb6b52c3c66
README.zh.md: e491f92d329142498993ce7976045a95e7ea16f7
@@ -44,11 +44,11 @@ Mount the plugin beside the tool registry with the payload directory. Configurat
`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 `sdk` profile enables this tool and the Office skills when `DSH_PRIMARY_RUNTIME` is nonempty. The profile resolves that path from the launch directory and loads sibling `office-skills/` resources. Missing skill resources produce a startup warning and leave Office skills unavailable; invalid or incomplete runtime payloads fail the first tool call. Unset or empty configuration disables both rows. Profile patches can override either row, and changes require restarting the SDK process.
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 `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.
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.
@@ -114,7 +114,7 @@ Append-only tool result in the turn history; no prompt section is added.
<a id="known-limitations-and-deferred-work"></a>
- The Linux build target is GNU/Linux x64; Linux ARM64 and musl payloads are not locked.
- 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>
@@ -44,11 +44,11 @@ kind: "package-reference"
`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`。平台或架构与当前进程不符的清单被拒绝。
`DSH_PRIMARY_RUNTIME` 非空时,`sdk` profile 启用本工具和 Office skills。profile 从启动目录解析该路径,并加载同级 `office-skills/` 资源。缺少 skill 资源会产生启动警告并使 Office skills 不可用;无效或不完整的运行时 payload 会在首次工具调用时失败。未设置或为空时,两行都禁用。profile patch 可以覆盖任意一行,配置变更需要重启 SDK 进程。
打包的 `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)还覆盖 `mac-arm64`、`mac-x64` 和 `win-x64`。`--python-only` 省略 Node.js 和 pnpm;`--cache` 选择经过哈希校验的归档缓存。入口仅对本机目标执行解释器与 Office 读写检查。跨目标构建必须在部署前到目标主机执行这些检查。
在已安装依赖的仓库 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 安装与签名检查。
@@ -114,7 +114,7 @@ kind: "package-reference"
<a id="known-limitations-and-deferred-work"></a>
- Linux 构建目标为 GNU/Linux x64;尚未锁定 Linux ARM64 与 musl payload。
- Linux 目标需要 glibc;尚未锁定 musl payload。
- Windows 上原地使用的 payload 须在载体里已可执行;原地模式不修复权限。
<a id="dev-note"></a>
+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: adb2fb71c3893d9360d7593826fdca609f770b6e
README.zh.md: a4decd5040ada700e84b0f1b3bacba7bbec8a56f
README.md: a767bcea00a5bb116fdf25a4d1d8fd916f94c5f5
README.zh.md: 539fae9f4f71df394bde54814b5190ea73126474
+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 `<executable-stem>-resources/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 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 还包含 `<executable-stem>-resources/primary-runtime/`(CPython 与锁定版本的 Office Python 库)及同级 `office-skills/`(三个默认工作流与共用检查脚本)。它们是可重定位的普通文件,不嵌入可执行文件。共享构建器为全部五个 wheel 目标选择原生归档,并在对应构建主机上执行冒烟检查。打包与已安装运行时定位会拒绝缺失资源、平台不符的元数据及 Python 执行权限丢失。
打包启动器通过 `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`。
+7
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
import os
import platform
import runpy
import stat
from pathlib import Path
@@ -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 / f"{expected_executable.removesuffix('.exe')}-resources"
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():
+4
View File
@@ -28,6 +28,10 @@ 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.
process.env.DSH_BUNDLED_PRIMARY_RUNTIME = `${process.execPath.replace(/\.exe$/i, '')}-resources/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(f"{path.name.removesuffix('.exe')}-resources"), 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}")
+17
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")
@@ -124,6 +136,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(f"{executable.name.removesuffix('.exe')}-resources")
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 +151,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(f"{executable.name.removesuffix('.exe')}-resources")
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 = Path(f"{executable}-resources")
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('deepseek-harness-sdk-runtime-macos-x64-resources')
})
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 = `${productBase}-resources`
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)
+12 -4
View File
@@ -8,6 +8,7 @@ import email
import json
import os
import re
import runpy
import shutil
import stat
import subprocess
@@ -227,6 +228,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 = f"{executable_name.removesuffix('.exe')}-resources"
shutil.copytree(source_directory / resources, runtime_dir / resources)
def verify_office_payload(archive: zipfile.ZipFile, office_modules: str, platform_tag: str) -> None:
@@ -272,16 +275,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}"
@@ -302,12 +306,16 @@ def verify_wheel(
if package == "runtime":
assert platform is not None
office = office_sidecar_name(platform[1])
expected_files = sorted((*runtime_filenames(platform[1]), office))
resources = f"{platform[1].removesuffix('.exe')}-resources"
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
+24
View File
@@ -98,6 +98,30 @@
"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": [
+5
View File
@@ -8,6 +8,11 @@ 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')
+2 -2
View File
@@ -144,8 +144,8 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies })
const manifest: PrimaryRuntimeManifest = {
desktopVersion: options.version,
platform: target === 'win-x64' ? 'win32' : target === 'linux-x64' ? 'linux' : 'darwin',
arch: target === 'mac-arm64' ? 'arm64' : 'x64',
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 }),
+77 -4
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",
@@ -315,6 +318,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
@@ -366,6 +373,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),
@@ -373,6 +381,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:
@@ -709,7 +720,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)
@@ -728,10 +739,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}")
@@ -755,6 +766,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"}:
@@ -790,6 +804,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(f"{executable.name.removesuffix('.exe')}-resources")
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
@@ -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"
}
}
}