feat(skill): complete shared Office runtime builds for SDK carriers

This commit is contained in:
fz committed 2026-09-17 21:10:12 +08:00
1 parent 6e49ccad18
commit 8fad9ccd8c
40 files changed
+779 -325

No files matched your search

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.md
2026-09-17-shared-office-runtime.md: 28daa6d3809705098722c41847fab9e3ee255bfe
2026-09-17-shared-office-runtime.zh.md: 09cc54e254546aa2787c37ab695c922ee86f0d66
@@ -0,0 +1,29 @@
# Agent Note: Shared Office runtime for Desktop and SDK
Status: implemented
English | [中文](2026-09-17-shared-office-runtime.zh.md)
## Problem
SDK deployments need the same Office authoring libraries as Desktop while keeping interpreter payloads in read-only container image layers. A Desktop-owned query and builder require downstream carriers to duplicate dependency locks and path conventions.
## Decision
[`tool-workspace-dependencies`](../../../../packages/skill/tool-workspace-dependencies/README.md) owns manifest validation, interpreter paths, installation and the model query. It stays in the skill group beside its Office workflow consumer: the payload supplies those workflows, while the package exposes no workspace entity service. Desktop imports this package and retains copy-on-first-use installation. The 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.
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.
## Alternatives considered
**Keep implementation in Desktop.** This leaves SDK and container builders depending on application packaging and signing code despite needing only interpreters, Office resources and a query.
**Enable Office in the common base.** A base deployment has no guaranteed bundled interpreter or resources. Explicit SDK carrier configuration makes that requirement observable without changing unrelated profiles.
**Always copy to the Harness home.** Copying duplicates image-layer content, requires a writable destination and cannot serve the intended immutable carrier deployment.
## Consequences
Successful tool preparation is shared by concurrent calls for one plugin lifetime. Failed preparation is retryable; disposal removes registration and waits for outstanding filesystem work. Copy mode verifies a staged replacement and restores the preceding installation after a failed publication. In-place mode performs no payload writes. Paths are absolute, and runtime metadata and declared entries are checked before results are returned.
SDK configuration changes require restart. Missing Office resources produce a startup warning and leave the optional skill provider inactive; runtime metadata and interpreter availability are validated on the first tool call. 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.
@@ -0,0 +1,29 @@
# Agent Note: Desktop 与 SDK 共享 Office 运行时
Status: implemented
[English](2026-09-17-shared-office-runtime.md) | 中文
## 问题
SDK 部署需要与 Desktop 相同的 Office 创作库,同时将解释器 payload 保存在只读容器镜像层中。由 Desktop 持有查询工具和构建器,会要求下游载体重复维护依赖锁与路径约定。
## 决策
[`tool-workspace-dependencies`](../../../../packages/skill/tool-workspace-dependencies/README.zh.md) 持有清单校验、解释器路径、安装逻辑和模型查询。它保留在 skill 组中,与 Office 工作流消费者相邻:payload 服务于这些工作流,本包不提供 workspace 实体服务。Desktop 引用本包并保留首次使用时复制安装。SDK profile 仅在载体设置 `DSH_PRIMARY_RUNTIME` 时启用查询和 Office skills;它解析 payload 路径并原位读取。公共 base 与 `sdk-minimal` 不会隐式启用 Office。
[共享构建入口](../../../../scripts/primary-runtime/prepare.ts) 持有 Desktop 目标与 GNU/Linux x64 的下载锁、解压和本机冒烟检查。Desktop 提供输出路径和发布版本,要求包含 Node.js 与 pnpm,并应用签名专用子进程环境。其他载体选择自己的输出目录,也可构建 Python-only payload。现有 `desktopVersion` 清单字段保留名字并记录载体发布版本;`payloadDigest` 区分锁定输入与组件选择。
## 考虑过的替代方案
**将实现保留在 Desktop。** SDK 和容器构建器只需要解释器、Office 资源和查询,却仍会依赖应用打包及签名代码。
**在公共 base 中启用 Office。** base 部署不保证提供内置解释器或资源。显式 SDK 载体配置可以让该要求可见,并保持其他 profile 的行为。
**总是复制到 Harness home。** 复制会重复镜像层内容、要求目标可写,也无法满足预期的不可变载体部署。
## 影响
同一插件生命周期内的并发调用共享成功的工具准备结果。准备失败后可重试;卸载移除工具注册并等待未完成的文件系统工作。复制模式验证暂存替换内容,并在发布失败后恢复原安装。原位模式不写入 payload。路径为绝对路径,返回结果前会检查运行时元数据和所声明的条目。
SDK 配置变更需要重启。缺少 Office 资源会产生启动警告,并使可选的 skill 提供方保持未激活;运行时元数据和解释器是否存在在首次工具调用时校验。Linux ARM64 与 musl 构建仍不在锁文件范围内。跨目标组装校验归档哈希,但需要在目标主机执行;Linux 产物 CI 负责本机 Linux 检查。单元覆盖固定清单、安装与生命周期行为;SDK profile 测试观察可选工具、skill 发现与结果,workspace-dependencies 录制场景固定模型可见的校验错误。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md
2026-07-30-generated-third-party-notices.md: 2b0bd012c23c873b3003916491daad467cf757b3
2026-07-30-generated-third-party-notices.zh.md: 0c7b4df8edb60f2f70781e179edc7377da930128
2026-07-30-generated-third-party-notices.md: c4458fac7de4d9e9b63b23855f95aaf69eb89f64
2026-07-30-generated-third-party-notices.zh.md: f26d0d001ed06dd5bea00bbc5e3400b31b254081
@@ -12,7 +12,7 @@ A hand-written inventory answers none of those durably. Roughly a hundred rows o
## Decision
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) is generated by [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) from the workspace manifests, `vendor/README.md`, the `pyproject.toml` files, `pnpm-workspace.yaml`, and the [Desktop runtime lock](../../../../apps/desktop/scripts/primary-runtime-lock.json). The root README pair links the file from its License section.
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) is generated by [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) from the workspace manifests, `vendor/README.md`, the `pyproject.toml` files, `pnpm-workspace.yaml`, and the [Desktop runtime lock](../../../../scripts/primary-runtime/lock.json). The root README pair links the file from its License section.
**Freshness is maintained, not merely enforced.** A pre-commit job regenerates the file and stages it whenever a generator input is staged — any manifest, a workspace declaration, the root or Desktop runtime lock file, `vendor/README.md`, a `pyproject.toml`, the generator itself, or the script holding the build-time pin — so an unrelated dependency edit never has to come back and rerun a generator. The committed bytes are then asserted inside [`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts), which the test lane already runs — the check adds no gate process, no scheduler slot, and no separate CI step. `pnpm run verify-third-party-notices` remains available for a standalone check.
@@ -12,7 +12,7 @@ Status: implemented
## 决策
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) 由 [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) 依据各工作区 manifest、`vendor/README.md`、`pyproject.toml`、`pnpm-workspace.yaml` 与 [Desktop 运行时锁文件](../../../../apps/desktop/scripts/primary-runtime-lock.json)生成。根 README 双语两侧都从「许可证」一节链到该文件。
[`THIRD_PARTY_NOTICES.md`](../../../../THIRD_PARTY_NOTICES.md) 由 [`scripts/gen-third-party-notices.ts`](../../../../scripts/gen-third-party-notices.ts) 依据各工作区 manifest、`vendor/README.md`、`pyproject.toml`、`pnpm-workspace.yaml` 与 [Desktop 运行时锁文件](../../../../scripts/primary-runtime/lock.json)生成。根 README 双语两侧都从「许可证」一节链到该文件。
**新鲜度会得到维护,而非仅靠校验。** 只要暂存了生成器的任一输入——任何 manifest、工作区声明、根锁文件或 Desktop 运行时锁文件、`vendor/README.md`、某个 `pyproject.toml`、生成器自身,或持有构建期 pin 的脚本——pre-commit 任务就会重新生成该文件并将其暂存,改依赖的人不必事后再折返跑一次生成器。已提交的字节随后由 [`scripts/gen-third-party-notices.spec.ts`](../../../../scripts/gen-third-party-notices.spec.ts) 断言,而测试 lane 本就会跑这个文件——这项校验不增加门禁进程、不占调度位、也不新增 CI 步骤。需要单独校验时,`pnpm run verify-third-party-notices` 仍然可用。
+3
View File
@@ -357,6 +357,9 @@ jobs:
if: vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]'
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium
- name: Build and execute the Linux Office runtime
run: CI=true pnpm run prepare:primary-runtime --target linux-x64 --output "$RUNNER_TEMP/office-runtime" --cache "$RUNNER_TEMP/office-runtime-downloads"
- name: Run compatibility, snapshot, and artifact gates
run: pnpm run check:ci:consumers
+1 -1
View File
@@ -246,7 +246,7 @@ Direct dependencies of the `pyproject.toml` manifests, plus `uv` as the developm
## Desktop bundled Python distributions
The [Desktop runtime lock](apps/desktop/scripts/primary-runtime-lock.json) records each distribution version and the wheel download hashes. The table includes every entry in `pythonPackages`, including transitive dependencies. Wheel extraction preserves distribution metadata and the license and notice files supplied by each archive. Project licenses below do not enumerate the separate licenses of native libraries bundled inside wheels.
The [Desktop runtime lock](scripts/primary-runtime/lock.json) records each distribution version and the wheel download hashes. The table includes every entry in `pythonPackages`, including transitive dependencies. Wheel extraction preserves distribution metadata and the license and notice files supplied by each archive. Project licenses below do not enumerate the separate licenses of native libraries bundled inside wheels.
| Distribution | Locked version | Project license |
| --- | --- | --- |
+1 -2
View File
@@ -13,7 +13,7 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
const modules = join(root, 'node_modules', '@deepseek-ai')
const hostDirectory = fileURLToPath(new URL('../../desktop-host/', import.meta.url))
const manifest = JSON.parse(readFileSync(join(hostDirectory, 'package.json'), 'utf8')) as { dependencies: Record<string, string> }
const stubbed = new Set(['@deepseek-ai/dsh-app-boot', '@deepseek-ai/dsh', '@deepseek-ai/dsh-home-paths', '@deepseek-ai/dsh-tools'])
const stubbed = new Set(['@deepseek-ai/dsh-app-boot', '@deepseek-ai/dsh', '@deepseek-ai/dsh-home-paths'])
for (const name of Object.keys(manifest.dependencies)) {
const destination = join(root, 'node_modules', name)
mkdirSync(dirname(destination), { recursive: true })
@@ -22,7 +22,6 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
}
for (const [name, source] of [
['dsh-home-paths', `export const resolveDshHome = () => ${JSON.stringify(root)}`],
['dsh-tools', 'export const defineTool = value => value'],
] as const) {
writeFileSync(join(modules, name, 'package.json'), '{"type":"module","exports":"./index.js"}')
writeFileSync(join(modules, name, 'index.js'), source)
@@ -1,12 +1,14 @@
import { createServer } from 'node:http'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { zstdDecompress } from 'node:zlib'
import { execa } from 'execa'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, onTestFinished } from 'vitest'
import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
const binScript = fileURLToPath(new URL('../../../src/bin.ts', import.meta.url))
const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
@@ -407,3 +409,99 @@ describe('Python SDK dsh profile keyless smoke', () => {
}
}, 30_000)
})
/** Materialize a native-layout payload whose paths the query can validate without executing binaries. */
async function officeFixture(root: string, pythonOnly: boolean) {
const source = join(root, 'resources', 'primary-runtime')
const manifest: PrimaryRuntimeManifest = {
desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
components: { python: '3.12.14', numpy: '2.3.5', pandas: '3.0.1',
...(pythonOnly ? {} : { node: '24.21.0', pnpm: '11.7.0' }) },
pythonPackages: { 'python-docx': '1.2.0', 'python-pptx': '1.0.2', openpyxl: '3.1.5' },
}
const paths = workspaceDependencyPaths(source, manifest)
for (const file of [paths.python, paths.node, paths.pnpm]) {
if (file === undefined) continue
await mkdir(dirname(file), { recursive: true })
await writeFile(file, 'fixture interpreter')
}
await mkdir(paths.pythonPackages, { recursive: true })
if (paths.nodePackages !== undefined) await mkdir(paths.nodePackages, { recursive: true })
await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
await cp(join(repoRoot, 'packages/skill/skill-office/assets'), join(root, 'resources', 'office-skills'), { recursive: true })
return { source, paths }
}
it.each(['unset', '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 { 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>[] = []
const server = createServer((request, response) => {
let body = ''
request.setEncoding('utf8').on('data', (chunk: string) => { body += chunk })
request.on('end', () => {
requests.push(JSON.parse(body) as Record<string, unknown>)
const query = requests.length === 1 && mode !== 'unset'
response.writeHead(200, { 'content-type': 'text/event-stream' })
response.end(messagesResponse(query
? { type: 'tool_use', id: 'workspace-dependencies', name: 'load_workspace_dependencies', input: {} }
: { type: 'text', text: 'done' }, query ? 'tool_use' : 'end_turn'))
})
})
onTestFinished(() => new Promise<void>((resolve) => { server.close(() => { resolve() }) }))
await new Promise<void>((resolve) => { server.listen(0, '127.0.0.1', resolve) })
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('model fixture did not bind')
const home = join(root, 'home')
const launch = resolveExampleLaunch({
srcBin: binScript, tsconfigPath: join(repoRoot, 'tsconfig.json'), sourceImport: 'tsx/esm',
configArgs: ['--profile', 'sdk'],
env: { DSH_HOME: home, DSH_PRIMARY_RUNTIME: mode === 'unset' ? undefined : source + '/',
DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'local-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}` },
})
const child = execa(launch.command, launch.args, { cwd: repoRoot, env: launch.env, timeout: 60_000, reject: false })
onTestFinished(async () => { child.kill('SIGKILL'); await child })
let buffer = '', stderr = ''
const lines: string[] = []
child.stdout.on('data', (chunk: Buffer) => {
buffer += chunk.toString()
const parts = buffer.split('\n')
buffer = parts.pop() ?? ''
lines.push(...parts)
})
child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString() })
const send = (id: number, method: string, params?: object) => {
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`)
return waitForLine(lines, value => value.id === id, () => stderr)
}
expect(await send(1, 'initialize', { cwd: root, provider: 'deepseek-official', model: 'deepseek-v4-pro' })).toHaveProperty('result')
await send(2, 'session/prompt', { sessionId: 'office', contentBlocks: [{ type: 'text', text: 'Query workspace dependencies.' }] })
await waitForLine(lines, (value) => {
const params = value.params as { sessionId?: string; event?: { type?: string } } | undefined
return value.method === 'session.event' && params?.sessionId === 'office' && params.event?.type === 'turn/end'
}, () => stderr)
const names = (requests[0]!.tools as { name: string }[]).map(value => value.name)
expect(names.includes('load_workspace_dependencies')).toBe(mode !== 'unset')
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
expect(JSON.stringify(requests[0]!.messages).includes(name)).toBe(mode === 'full' || mode === 'python-only')
}
if (mode !== 'unset') {
expect(requests).toHaveLength(2)
expect(requests[1]!.messages).toEqual(expect.arrayContaining([
expect.objectContaining({ role: 'user', content: expect.arrayContaining([
expect.objectContaining({ type: 'tool_result', tool_use_id: 'workspace-dependencies',
content: [{ type: 'text', text: JSON.stringify(paths, undefined, 2) }] }),
]) as unknown }),
]))
}
if (mode === 'missing-assets') expect(stderr).toContain('check_office.py')
await send(3, 'shutdown')
const exit = await child
expect(exit.timedOut).toBe(false)
expect(exit.signal).toBeUndefined()
expect(exit.exitCode, stderr).toBe(0)
await expect(readFile(join(home, 'dsh-runtimes', 'dsh-primary-runtime', 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
})
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/desktop/README.md
README.md: 14bbdb7ec0fddd47821c12ffe46549dd29abe0de
README.zh.md: 1e704900743ec6f339efdf849e60767c961d07ee
README.md: d6980c890901ae16e21a586105f71f3214ca5e30
README.zh.md: da67581fe609115f03cf3bfb3a518c2db5e3b320
+1 -1
View File
@@ -28,7 +28,7 @@ The payload follows the Desktop release. `runtime.json` records the Desktop vers
The private Desktop `runtime/bin` directory is added only to package-installation processes, not the Host PATH inherited by PTC and agent shells. This tool does not change PATH, environment variables or user package-manager configuration. pnpm retains its own defaults and user settings for global packages, executable entries and its store, including native errors when the environment does not support global installation. There is no separate dependency updater. [The primary-runtime decision](../../.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md) records these choices.
Node prepares the bundled interpreters and Python libraries without a system Python or pip. [The download lock](scripts/primary-runtime-lock.json) pins interpreter archives, Python distribution versions and target-specific wheel URLs and hashes; pnpm follows the Desktop build dependency lock. Wheel filenames and distribution versions must agree for every target. Preserve key order within the selected target, wheel records and distribution map, plus wheel-entry order; these affect payload identity, while top-level lock key order does not. Library wheels unpack into site-packages, retaining auxiliary files under each wheel's `.data/scripts` directory without generating command wrappers. Other installation schemes are rejected. Native-target checks verify the locked wheel set and versions, permit the interpreter's bundled pip, and check the Python version, Office document read/write operations and dependency completeness without writing bytecode after staging cleanup and again after macOS signing. The standalone Node executable receives the JIT entitlement required by V8. Cross-target execution and signed installation require the target release host. Both `dev:desktop` and `start:desktop` prepare `.desktop-build/targets/<target>/runtime/primary-runtime` before launching Electron; first use may download locked dependencies. An unfinished preparation cannot report a successful launcher exit.
Node prepares the bundled interpreters and Python libraries without a system Python or pip. [The download lock](../../scripts/primary-runtime/lock.json) pins interpreter archives, Python distribution versions and target-specific wheel URLs and hashes; pnpm follows the Desktop build dependency lock. Wheel filenames and distribution versions must agree for every target. Preserve key order within the selected target, wheel records and distribution map, plus wheel-entry order; these affect payload identity, while top-level lock key order does not. Library wheels unpack into site-packages, retaining auxiliary files under each wheel's `.data/scripts` directory without generating command wrappers. Other installation schemes are rejected. Native-target checks verify the locked wheel set and versions, permit the interpreter's bundled pip, and check the Python version, Office document read/write operations and dependency completeness without writing bytecode after staging cleanup and again after macOS signing. The standalone Node executable receives the JIT entitlement required by V8. Cross-target execution and signed installation require the target release host. Both `dev:desktop` and `start:desktop` prepare `.desktop-build/targets/<target>/runtime/primary-runtime` before launching Electron; first use may download locked dependencies. An unfinished preparation cannot report a successful launcher exit.
| Decision | Why | Direct consequence |
|---|---|---|
+1 -1
View File
@@ -28,7 +28,7 @@ Desktop 默认注册 `office-docx`、`office-pptx` 和 `office-xlsx`。这些技
Desktop 私有的 `runtime/bin` 目录仅添加到包安装进程,不进入 PTC 和 agent shell 从 Host 继承的 PATH。该工具不修改 PATH、环境变量或用户包管理器配置。pnpm 的全局包、命令入口和 store 保留自身默认值及用户设置,包括环境不支持全局安装时的原生错误。不提供独立依赖更新器。[第一方 Runtime 决策](../../.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.zh.md)记录这些选择。
Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载锁](scripts/primary-runtime-lock.json)固定解释器压缩包、Python 分发包版本及目标平台 wheel 的 URL 和哈希;pnpm 使用 Desktop 构建依赖锁。每个目标的 wheel 文件名必须与分发包版本一致。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序都会影响产物身份,编辑时须保留;锁文件顶层键的顺序不影响该身份。库 wheel 解压到 site-packages,各 wheel 的 `.data/scripts` 目录保留辅助文件,不生成命令行包装器。其他安装方案会被拒绝。本机目标检查在清理暂存目录后以及 macOS 签名后验证锁定 wheel 的集合与版本,允许解释器自带的 pip,并检查 Python 版本、Office 文档读写和依赖完整性,不写入字节码。独立 Node 可执行文件获得 V8 所需的 JIT 权限。跨目标执行和签名安装需要对应的发布主机。`dev:desktop` 和 `start:desktop` 都会在启动 Electron 前准备 `.desktop-build/targets/<target>/runtime/primary-runtime`;首次准备可能需要下载锁定的依赖。准备未完成时,启动命令不能报告成功退出。
Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载锁](../../scripts/primary-runtime/lock.json)固定解释器压缩包、Python 分发包版本及目标平台 wheel 的 URL 和哈希;pnpm 使用 Desktop 构建依赖锁。每个目标的 wheel 文件名必须与分发包版本一致。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序都会影响产物身份,编辑时须保留;锁文件顶层键的顺序不影响该身份。库 wheel 解压到 site-packages,各 wheel 的 `.data/scripts` 目录保留辅助文件,不生成命令行包装器。其他安装方案会被拒绝。本机目标检查在清理暂存目录后以及 macOS 签名后验证锁定 wheel 的集合与版本,允许解释器自带的 pip,并检查 Python 版本、Office 文档读写和依赖完整性,不写入字节码。独立 Node 可执行文件获得 V8 所需的 JIT 权限。跨目标执行和签名安装需要对应的发布主机。`dev:desktop` 和 `start:desktop` 都会在启动 Electron 前准备 `.desktop-build/targets/<target>/runtime/primary-runtime`;首次准备可能需要下载锁定的依赖。准备未完成时,启动命令不能报告成功退出。
| 决策 | 原因 | 直接结果 |
|---|---|---|
+10 -148
View File
@@ -1,157 +1,26 @@
/** Prepare pinned, relocatable script interpreters without installing into the build host. */
/** Desktop resource locations and signing-aware verification for the shared runtime builder. */
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { cp } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import extractZip from 'extract-zip'
import { x as extractTar } from 'tar'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { preparePrimaryRuntime as preparePayload, smokePrimaryRuntime as smokePayload } from '../../../scripts/primary-runtime/prepare.ts'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../../packages/skill/tool-workspace-dependencies/src/index.ts'
import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
import { scrubWindowsSigningEnvironment } from './windows-sign.mjs'
import lock from './primary-runtime-lock.json' with { type: 'json' }
/**
* Download or reuse an archive only when its bytes match the release lock.
* @param url - Locked archive URL.
* @param sha256 - Expected SHA-256 digest.
* @param cache - Download cache directory.
* @returns Verified local archive path.
*/
export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string): Promise<string> {
const destination = join(cache, sha256)
let bytes: Buffer
try { bytes = readFileSync(destination) } catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
const response = await fetch(url)
if (!response.ok) throw new Error(`primary runtime download: ${String(response.status)} ${url}`)
bytes = Buffer.from(await response.arrayBuffer())
}
if (createHash('sha256').update(bytes).digest('hex') !== sha256) throw new Error(`primary runtime download: checksum mismatch for ${url}`)
writeFileSync(destination, bytes)
return destination
}
async function pythonArchive(target: keyof typeof lock.targets, cache: string): Promise<string> {
const artifact = lock.targets[target]
const filename = `cpython-${lock.pythonVersion}+${lock.pythonRelease}-${artifact.pythonTarget}-install_only_stripped.tar.gz`
return downloadPrimaryRuntimeAsset(`https://github.com/astral-sh/python-build-standalone/releases/download/${lock.pythonRelease}/${encodeURIComponent(filename)}`, artifact.pythonSha256, cache)
}
/**
* Identify the inputs that assemble one target's payload, excluding unrelated target locks.
* @param target - Desktop target whose archives are installed.
* @param runtimeLock - Locked interpreter and wheel inputs.
* @param pnpmVersion - Package-manager version copied into the payload.
* @returns SHA-256 payload identity for installation reuse.
*/
export function primaryRuntimePayloadDigest(target: keyof typeof lock.targets, runtimeLock: typeof lock, pnpmVersion: string): string {
const { pythonVersion, pythonRelease, nodeVersion, wheels, pythonPackages } = runtimeLock
// Identity preserves key order within the selected target, wheel records and distribution map, plus wheel-entry order.
// Bump format when extraction or assembly changes payload bytes without changing locked inputs.
return createHash('sha256').update(JSON.stringify({
format: 2, target, pythonVersion, pythonRelease, nodeVersion,
artifact: runtimeLock.targets[target], wheels, pythonPackages, pnpm: pnpmVersion,
})).digest('hex')
}
/**
* Unpack a locked library wheel, retaining auxiliary scripts in its distribution data directory.
* @param archive - Hash-verified wheel archive.
* @param destination - Absolute site-packages directory.
* @returns Resolves after extraction without command wrappers; rejects other wheel installation schemes.
*/
export async function unpackPrimaryRuntimeWheel(archive: string, destination: string): Promise<void> {
await extractZip(archive, {
dir: destination,
onEntry: (entry) => {
const [directory, scheme] = entry.fileName.split('/')
if (directory?.endsWith('.data') && scheme !== '' && scheme !== 'scripts') {
throw new Error(`primary runtime: wheel requires unsupported installation paths: ${entry.fileName}`)
}
},
})
}
/**
* Copy the skill package's complete asset tree to ordinary filesystem resources.
* @param source - The package's assets directory.
* @param destination - Desktop runtime resource directory outside ASAR.
* @returns Resolves after replacing the external assets with the complete package tree.
*/
export async function prepareOfficeSkillAssets(source: string, destination: string): Promise<void> {
rmSync(destination, { recursive: true, force: true })
await cp(source, destination, { recursive: true, dereference: true })
}
/**
* Materialize the selected Desktop target's primary runtime in its build resources.
* Prepare Desktop resources for its selected packaging target.
* @param options - Signed Windows packaging defers execution until its supervised signing stage.
* @returns Resolves after materialization and, unless deferred, native-target execution checks.
* @returns Resolves after preparation and, unless deferred, native-target execution checks.
*/
export async function preparePrimaryRuntime(options: { deferSmoke?: boolean } = {}): Promise<void> {
const target = resolveDesktopBuildTarget()
const paths = resolveDesktopTargetBuildPaths()
const artifact = lock.targets[target]
mkdirSync(paths.runtime, { recursive: true })
mkdirSync(paths.downloads, { recursive: true })
const staging = mkdtempSync(join(tmpdir(), 'dsh-primary-'))
try {
const output = join(staging, 'payload')
const dependencies = join(output, 'dependencies')
mkdirSync(dependencies, { recursive: true })
const nodeFilename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}`
const nodeArchive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${nodeFilename}`, artifact.nodeSha256, paths.downloads)
const unpackedNode = join(staging, 'node')
mkdirSync(unpackedNode)
if (target === 'win-x64') await extractZip(nodeArchive, { dir: unpackedNode })
else await extractTar({ file: nodeArchive, cwd: unpackedNode })
const nodeSource = join(unpackedNode, nodeFilename.replace(/\.(?:zip|tar\.gz)$/u, ''))
mkdirSync(join(dependencies, 'node', 'bin'), { recursive: true })
mkdirSync(join(dependencies, 'node', 'node_modules'))
writeFileSync(join(dependencies, 'node', 'node_modules', 'README.txt'), 'Reserved for bundled Node packages. pnpm uses its default installation directories.\n')
cpSync(join(nodeSource, ...(target === 'win-x64' ? ['node.exe'] : ['bin', 'node'])),
join(dependencies, 'node', 'bin', target === 'win-x64' ? 'node.exe' : 'node'))
cpSync(join(nodeSource, 'LICENSE'), join(dependencies, 'node', 'LICENSE'))
await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies })
const require = createRequire(import.meta.url)
const pnpmManifest = require.resolve('pnpm')
const pnpm = JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
const desktop = JSON.parse(readFileSync(join(import.meta.dirname, '..', 'package.json'), 'utf8')) as { version: string }
const manifest: PrimaryRuntimeManifest = {
desktopVersion: desktop.version,
platform: target === 'win-x64' ? 'win32' : 'darwin',
arch: target === 'mac-arm64' ? 'arm64' : 'x64',
payloadDigest: primaryRuntimePayloadDigest(target, lock, pnpm.version),
pythonPackages: lock.pythonPackages,
components: {
python: lock.pythonVersion, node: lock.nodeVersion, pnpm: pnpm.version,
numpy: lock.pythonPackages.numpy, pandas: lock.pythonPackages.pandas,
},
}
const entries = workspaceDependencyPaths(output, manifest)
for (const wheel of [...artifact.wheels, ...lock.wheels]) {
await unpackPrimaryRuntimeWheel(await downloadPrimaryRuntimeAsset(wheel.url, wheel.sha256, paths.downloads), entries.pythonPackages)
}
writeFileSync(join(output, 'runtime.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
const destination = join(paths.runtime, 'primary-runtime')
rmSync(destination, { recursive: true, force: true })
await cp(output, destination, { recursive: true, dereference: true })
} finally {
rmSync(staging, { recursive: true, force: true })
}
const hostRequire = createRequire(resolve(import.meta.dirname, '..', '..', 'desktop-host', 'package.json'))
await prepareOfficeSkillAssets(join(dirname(hostRequire.resolve('@deepseek-ai/dsh-skill-office/package.json')), 'assets'),
join(paths.runtime, 'office-skills'))
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }
await preparePayload({ target: resolveDesktopBuildTarget(), output: paths.runtime, cache: paths.downloads, version })
if (!options.deferSmoke) smokePrimaryRuntime(join(paths.runtime, 'primary-runtime'))
}
/**
* Execute the native payload's interpreters, package manager and Python libraries.
* Verify Desktop's complete payload after preparation or platform signing.
* @param root - Final payload directory, including any platform signatures.
*/
export function smokePrimaryRuntime(root: string): void {
@@ -159,15 +28,8 @@ export function smokePrimaryRuntime(root: string): void {
if (manifest.platform !== process.platform || manifest.arch !== process.arch) return
if (manifest.pythonPackages === undefined) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
const entries = workspaceDependencyPaths(root, manifest)
// The Desktop payload always ships Node.js and pnpm; only other carriers may omit them.
if (entries.node === undefined || entries.pnpm === undefined) throw new Error('primary runtime: the Desktop payload must declare node and pnpm components.')
const options = { stdio: 'inherit', timeout: 120_000, env: scrubWindowsSigningEnvironment(process.env) } as const
execFileSync(entries.python, ['-I', '-c', 'import decimal, xml.parsers.expat, lzma, uuid, numpy, pandas; assert numpy.arange(4).sum() == 6; assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3'], options)
execFileSync(entries.python, ['-I', '-B', join(import.meta.dirname, 'smoke-primary-runtime.py'), JSON.stringify(manifest.pythonPackages),
manifest.components.python, join(dirname(root), 'office-skills', 'scripts', 'check_office.py')], options)
execFileSync(entries.python, ['-I', '-B', '-m', 'pip', 'check'], options)
execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(manifest.components.node)}) process.exit(1)`], options)
execFileSync(entries.node, [entries.pnpm, '--version'], options)
smokePayload(root, scrubWindowsSigningEnvironment(process.env))
}
if (import.meta.main) await preparePrimaryRuntime()
@@ -1,131 +1,18 @@
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
/** Desktop carriers keep a complete interpreter payload even when SDK carriers choose Python-only. */
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { zipSync } from 'fflate'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import { downloadPrimaryRuntimeAsset, prepareOfficeSkillAssets, primaryRuntimePayloadDigest, smokePrimaryRuntime, unpackPrimaryRuntimeWheel } from '../scripts/prepare-primary-runtime.ts'
import lock from '../scripts/primary-runtime-lock.json' with { type: 'json' }
import { smokePrimaryRuntime } from '../scripts/prepare-primary-runtime.ts'
const libraryWheel = Buffer.from('UEsDBAoAAAAAAASeLl0sYMPjDAAAAAwAAAAJAAAAc2FtcGxlLnB5c2FtcGxlID0gNDIKUEsBAh4DCgAAAAAABJ4uXSxgw+MMAAAADAAAAAkAAAAAAAAAAQAAAKSBAAAAAHNhbXBsZS5weVBLBQYAAAAAAQABADcAAAAzAAAAAAA=', 'base64')
const relocatedWheel = Buffer.from('UEsDBAoAAAAAAASeLl3x0Nj9FAAAABQAAAAeAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlcmVxdWlyZXMgcmVsb2NhdGlvbgpQSwECHgMKAAAAAAAEni5d8dDY/RQAAAAUAAAAHgAAAAAAAAABAAAApIEAAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlUEsFBgAAAAABAAEATAAAAFAAAAAAAA==', 'base64')
const externalLibraryWheel = Buffer.from('UEsDBBQAAAAAAAAAIVyBOE8OHAAAABwAAAAhAAAAc2FtcGxlLTEuMC5kYXRhL3B1cmVsaWIvc2FtcGxlLnB5cmVxdWlyZXMgbGlicmFyeSByZWxvY2F0aW9uClBLAQIUAxQAAAAAAAAAIVyBOE8OHAAAABwAAAAhAAAAAAAAAAAAAACAAQAAAABzYW1wbGUtMS4wLmRhdGEvcHVyZWxpYi9zYW1wbGUucHlQSwUGAAAAAAEAAQBPAAAAWwAAAAAA', 'base64')
it.each(Object.entries(lock.targets))('records every locked wheel distribution and version for %s', (_target, artifact) => {
const normalize = (name: string): string => name.toLowerCase().replace(/[-_.]+/gu, '-')
const distributions = [...artifact.wheels, ...lock.wheels].map(({ url }) => {
const [name, version] = basename(new URL(url).pathname).split('-')
return [normalize(name!), version] as const
})
const declared = Object.entries(lock.pythonPackages).map(([name, version]) => [normalize(name), version] as const)
expect(new Set(distributions.map(([name]) => name)).size).toBe(distributions.length)
expect(new Set(declared.map(([name]) => name)).size).toBe(declared.length)
expect(Object.fromEntries(distributions)).toEqual(Object.fromEntries(declared))
})
it('keeps a target payload identity independent of other target archives', () => {
const changed = structuredClone(lock)
changed.targets['win-x64'].wheels[0]!.sha256 = 'a'.repeat(64)
expect(primaryRuntimePayloadDigest('mac-arm64', changed, '11.7.0')).toBe(primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.0'))
expect(primaryRuntimePayloadDigest('win-x64', changed, '11.7.0')).not.toBe(primaryRuntimePayloadDigest('win-x64', lock, '11.7.0'))
})
it('invalidates payload identity for shared wheels, package versions and package-manager changes', () => {
const wheel = structuredClone(lock), distribution = structuredClone(lock)
wheel.wheels[0]!.sha256 = 'a'.repeat(64)
distribution.pythonPackages['python-docx'] = '1.2.1'
const original = primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.0')
expect(primaryRuntimePayloadDigest('mac-arm64', wheel, '11.7.0')).not.toBe(original)
expect(primaryRuntimePayloadDigest('mac-arm64', distribution, '11.7.0')).not.toBe(original)
expect(primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.1')).not.toBe(original)
})
it('reports missing distribution metadata before trying to execute a stale native payload', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-stale-runtime-'))
it('rejects a Python-only native Desktop payload before executing interpreters', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-python-only-'))
try {
await writeFile(join(root, 'runtime.json'), JSON.stringify({ platform: process.platform, arch: process.arch }))
expect(() => { smokePrimaryRuntime(root) }).toThrow('missing Python distribution versions; prepare the payload')
} finally { await rm(root, { recursive: true, force: true }) }
})
it('extracts a hash-verified cached library without a Python installer or network request', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const hash = createHash('sha256').update(libraryWheel).digest('hex')
const archive = join(root, hash)
await writeFile(archive, libraryWheel)
expect(await downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).toBe(archive)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample.py'), 'utf8')).toBe('sample = 42\n')
await writeFile(archive, 'corrupt archive')
await expect(downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).rejects.toThrow('checksum mismatch')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('fully extracts a large deflate-compressed wheel entry', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-large-wheel-'))
try {
const expected = Buffer.alloc(1024 * 1024)
// Seeded xorshift bytes keep the compressed entry larger than the stream buffers.
let state = 26
for (let index = 0; index < expected.length; index++) {
state ^= state << 13
state ^= state >>> 17
state ^= state << 5
expected[index] = state & 255
}
const archive = join(root, 'large.whl')
await writeFile(archive, zipSync({ 'large.bin': expected }, { level: 6 }))
await unpackPrimaryRuntimeWheel(archive, root)
expect(await readFile(join(root, 'large.bin'))).toEqual(expected)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('retains auxiliary wheel scripts without generating command wrappers', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const archive = join(root, 'relocated.whl')
await writeFile(archive, relocatedWheel)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample-1.0.data/scripts/sample'), 'utf8')).toBe('requires relocation\n')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('rejects library files requiring an unsupported installation scheme', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const archive = join(root, 'relocated.whl')
await writeFile(archive, externalLibraryWheel)
await expect(unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))).rejects.toThrow('unsupported installation paths')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('copies complete Office resources outside the application archive and removes obsolete assets', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-office-assets-'))
try {
const source = join(root, 'package', 'assets')
const destination = join(root, 'Contents', 'Resources', 'runtime', 'office-skills')
await mkdir(join(source, 'scripts'), { recursive: true })
await writeFile(join(source, 'scripts', 'check_office.py'), 'print("checker")\n')
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
await mkdir(join(source, name))
await writeFile(join(source, name, 'SKILL.md'), `# ${name}\n`)
}
await prepareOfficeSkillAssets(source, destination)
await writeFile(join(destination, 'obsolete.py'), 'old helper')
await prepareOfficeSkillAssets(source, destination)
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
expect(await readFile(join(destination, name, 'SKILL.md'), 'utf8')).toBe(`# ${name}\n`)
}
expect(await readFile(join(destination, 'scripts', 'check_office.py'), 'utf8')).toBe('print("checker")\n')
await expect(readFile(join(destination, 'obsolete.py'))).rejects.toMatchObject({ code: 'ENOENT' })
await writeFile(join(root, 'runtime.json'), JSON.stringify({
platform: process.platform, arch: process.arch, pythonPackages: {},
components: { python: '3.12.14', numpy: '2.3.5', pandas: '3.0.1' },
}))
expect(() => { smokePrimaryRuntime(root) }).toThrow('Desktop payload must declare node and pnpm')
} finally { await rm(root, { recursive: true, force: true }) }
})
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
module-graph.md: 8ec3673173c299fdd69134af1002ba157d6db870
module-graph.zh.md: 1dd43514acff155c92c139dca7ad519e137e2a03
module-graph.md: 29caf0ba5f4b00e6ef58a8687445d2b22713cf88
module-graph.zh.md: 7a21ed9ba9a8893bb6e72317e04b15d84dd0ca03
+3
View File
@@ -65,6 +65,7 @@ flowchart TD
pkg_skill_filesystem["skill-filesystem"]
pkg_skill_office["skill-office"]
pkg_tool_skill["tool-skill"]
pkg_tool_workspace_dependencies["tool-workspace-dependencies"]
end
subgraph group_subagent["packages/subagent"]
pkg_subagent["subagent"]
@@ -777,6 +778,7 @@ flowchart TD
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
pkg_tool_skill --> pkg_tools
pkg_tool_workspace_dependencies --> pkg_tools
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
pkg_tool_web --> pkg_tools
@@ -1508,6 +1510,7 @@ flowchart TD
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
+3
View File
@@ -67,6 +67,7 @@ flowchart TD
pkg_skill_filesystem["skill-filesystem"]
pkg_skill_office["skill-office"]
pkg_tool_skill["tool-skill"]
pkg_tool_workspace_dependencies["tool-workspace-dependencies"]
end
subgraph group_subagent["packages/subagent"]
pkg_subagent["subagent"]
@@ -779,6 +780,7 @@ flowchart TD
pkg_tool_skill --> pkg_llm
pkg_tool_skill --> pkg_skill
pkg_tool_skill --> pkg_tools
pkg_tool_workspace_dependencies --> pkg_tools
pkg_tool_web --> pkg_llm
pkg_tool_web --> pkg_system_prompt
pkg_tool_web --> pkg_tools
@@ -1510,6 +1512,7 @@ flowchart TD
| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) |
| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
| [`tool-workspace-dependencies`](../packages/skill/tool-workspace-dependencies) | `skill` | [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |
| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) |
| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
+1 -1
View File
@@ -28,7 +28,7 @@ pre-commit:
# lefthook only inspects files present on disk — so that one case still
# falls through to the freshness assertion in the test lane.
- name: third-party notices (staged)
glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,apps/desktop/scripts/primary-runtime-lock.json,scripts/gen-third-party-notices.ts,scripts/browser-bundled-externals.ts,scripts/build-exe-for-python-sdk.ts,tsconfig.base*.json,packages/*/*/src/**/*,packages/*/*/tsdown.config.ts,packages/client/tsdown.client.ts,apps/*/src/**/*,apps/*/vite.config.ts}'
glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/primary-runtime/lock.json,scripts/gen-third-party-notices.ts,scripts/browser-bundled-externals.ts,scripts/build-exe-for-python-sdk.ts,tsconfig.base*.json,packages/*/*/src/**/*,packages/*/*/tsdown.config.ts,packages/client/tsdown.client.ts,apps/*/src/**/*,apps/*/vite.config.ts}'
run: node_modules/.bin/tsx scripts/gen-third-party-notices.ts && git add THIRD_PARTY_NOTICES.md
- name: whitespace (staged)
+6
View File
@@ -17,6 +17,7 @@
"website"
],
"scripts": {
"prepare:primary-runtime": "tsx scripts/primary-runtime/prepare.ts",
"build": "tsx scripts/build.ts",
"build:bench": "npm run build:native-system && npm run build:lib && tsdown --config benchmarks/tsdown.config.ts",
"build:official": "tsx scripts/build.ts --profile official",
@@ -89,6 +90,7 @@
"check:ci:windows-complete": "tsx scripts/run-gates.ts ci-windows-complete",
"check:ci:windows-observational": "tsx scripts/run-gates.ts ci-windows-observational",
"check:node-compat": "tsx scripts/run-gates.ts node-compat",
"pnpm": "11.7.0",
"publint": "tsx scripts/publint-all.ts",
"doc-typecheck": "npm run build:lib:host && npm run doc-typecheck:contracts-ready",
"doc-typecheck:contracts-ready": "tsx scripts/doc-typecheck.ts",
@@ -219,7 +221,9 @@
"@yarnpkg/cli-dist": "4.17.1",
"eslint-plugin-sonarjs": "^4.1.0",
"execa": "^10.0.0",
"extract-zip": "^2.0.1",
"fast-check": "^4.8.0",
"fflate": "^0.8.2",
"istanbul-lib-report": "^3.0.1",
"js-yaml": "^4.2.0",
"jscpd": "^5.0.12",
@@ -232,9 +236,11 @@
"micromark-extension-gfm": "^3.0.0",
"oxlint": "1.76.0",
"oxlint-tsgolint": "7.0.2001",
"pnpm": "11.7.0",
"publint": "^0.3.21",
"smol-toml": "^1.7.1",
"spdx-expression-parse": "^5.0.0",
"tar": "^7.5.0",
"tsdown": "^0.22.2",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/bundle/sdk-app/README.md
README.md: 0bac09b5a7e355434e35b04f4e9abd3e816505af
README.zh.md: e0e0d7e1e147c0a2dfaed7014ea0b3a0a9028f2b
README.md: acf038281ea56b435abfcaeb7e8afddcbfbfb7e4
README.zh.md: 8e5c3f994b3d435bcfe6997f6bddc9d917f7307a
+2
View File
@@ -33,6 +33,8 @@ The startup provider binds stdin EOF to the launcher's bounded successful shutdo
The SDK uses the base `read`, `write`, and `edit` defaults. To add `str_replace_editor`, use the explicit insertion patch in the [base configuration guide](../base/README.md#use-this-package). The standalone `sdk-minimal` profile owns its separate tool selection.
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.
-----
<a id="model-experience"></a>
+2
View File
@@ -33,6 +33,8 @@ kind: "package-bundle"
SDK 使用 base 默认提供的 `read`、`write` 和 `edit`。要添加 `str_replace_editor`,请使用 [base 配置指南](../base/README.zh.md#use-this-package)中的显式插入 patch。独立的 `sdk-minimal` profile 自行决定其工具选择。
部署设置 `DSH_PRIMARY_RUNTIME` 后会启用[内置 Office 运行时查询与 skills](../../skill/tool-workspace-dependencies/README.zh.md#use-this-package)。未设置或为空时两者都禁用。载体提供本机 payload 与同级 `office-skills/`;工具原位读取,也支持只读镜像层。缺少 skill 资源会产生启动警告并使 Office skills 不可用。无效运行时元数据或缺失解释器条目会导致工具调用失败。
-----
<a id="model-experience"></a>
+4 -5
View File
@@ -27,18 +27,17 @@
# 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. Unset, both
# rows stay disabled and the profile is unchanged.
# 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.
- insert:
- id: workspace-dependencies
name: '@deepseek-ai/dsh-tool-workspace-dependencies'
disabled: !!js "!process.env.DSH_PRIMARY_RUNTIME"
config:
source: !!js process.env.DSH_PRIMARY_RUNTIME ?? ''
source: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? '')"
- id: skill-office
name: '@deepseek-ai/dsh-skill-office'
disabled: !!js "!process.env.DSH_PRIMARY_RUNTIME"
config:
# The payload's parent directory plus office-skills, without a path module in the expression scope.
assetRoot: !!js "process.env.DSH_PRIMARY_RUNTIME ? process.env.DSH_PRIMARY_RUNTIME.replace(/[\\\\/]+[^\\\\/]+[\\\\/]*$/, '') + '/office-skills' : ''"
assetRoot: !!js "process.getBuiltinModule('node:path').resolve(process.env.DSH_PRIMARY_RUNTIME ?? '', '..', 'office-skills')"
@@ -31,7 +31,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep',
'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output',
'list_agents', 'list_agents', 'list_mcp_resource_templates', 'list_mcp_resources',
'list_subagent_models', 'lsp', 'plugin_manager', 'present', 'pwsh', 'pwsh', 'ralph',
'list_subagent_models', 'load_workspace_dependencies', 'lsp', 'plugin_manager', 'present', 'pwsh', 'pwsh', 'ralph',
'read', 'read_image', 'read_mcp_resource', 'run_code', 'schedule_create', 'schedule_delete',
'schedule_list', 'send_message', 'send_message', 'session_event_read', 'session_event_search',
'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate',
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/skill/README.md
README.md: 9546dfbfb2081480a1af8078d65481d0fc080ae3
README.zh.md: eb1231b6fc7e110afdcc22fb25db3dd55b830092
README.md: 75261bdca06b658f04fac17a3eab4306dbe5cecd
README.zh.md: 7d1c38a4fb456f26bf038c8ddfe6f8182d0058f8
+1
View File
@@ -29,6 +29,7 @@ The skill family lets agents and users discover and load reusable task instructi
| [`skill-badge/`](skill-badge/README.md) | Bundles the official "powered by dsh" badge skill, disabled by default | registers on `ctx.skills` |
| [`skill-office/`](skill-office/README.md) | Bundles Word, PowerPoint, and Excel workflows with structural file checks | registers on `ctx.skills` |
| [`tool-skill/`](tool-skill/README.md) | Publishes the session skill catalog and the model-facing `skill` loader tool | registers on `ctx.tools` |
| [`tool-workspace-dependencies/`](tool-workspace-dependencies/README.md) | Reports bundled Office interpreter paths and versions for Desktop and SDK carriers | registers on `ctx.tools` |
-----
+1
View File
@@ -29,6 +29,7 @@ skill 家族让 agent 和用户仅在需要时发现并加载可复用的任务
| [`skill-badge/`](skill-badge/README.zh.md) | 随包附带官方「powered by dsh」徽章 skill,默认禁用 | 注册到 `ctx.skills` |
| [`skill-office/`](skill-office/README.zh.md) | 随包提供 Word、PowerPoint 和 Excel 工作流及文件结构检查 | 注册到 `ctx.skills` |
| [`tool-skill/`](tool-skill/README.zh.md) | 发布会话 skill 目录与面向模型的 `skill` 加载工具 | 注册到 `ctx.tools` |
| [`tool-workspace-dependencies/`](tool-workspace-dependencies/README.zh.md) | 为 Desktop 与 SDK 载体报告内置 Office 解释器路径和版本 | 注册到 `ctx.tools` |
-----
@@ -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: d39437c039ff479900f8192e666ab67f2dbedc89
README.zh.md: c45cd21e810eb8a6a2c3e8bf22da4a5bdc8089ff
README.md: 1464e231980bc24559d5dfc2635d51e57bcfc116
README.zh.md: 48520369dde7cb0a5fcfb8d53af687ce651c47d5
@@ -37,14 +37,20 @@ Mount the plugin beside the tool registry with the payload directory. The bundle
| Field | Default | Meaning |
|---|---|---|
| `source` | required | Payload directory carrying `runtime.json` and `dependencies/`. |
| `root` | unset | Installation directory under the Harness home. Set: the payload is copied there on the first call and reused while `runtime.json` is unchanged. Unset: the payload is validated and used in place; nothing is copied. |
| `source` | required | Absolute payload directory carrying `runtime.json` and `dependencies/`. |
| `root` | unset | Absolute installation directory under the Harness home. Set: the payload is copied there on the first call and reused while `runtime.json` is unchanged. Unset: the payload is validated and used in place; nothing is copied. |
### Payload layout
`runtime.json` records `desktopVersion`, `platform` (`win32`, `darwin`, or `linux`), `arch`, the `components` versions (`python`, `numpy`, `pandas`; `node` and `pnpm` are optional), and optionally `payloadDigest` and `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 mounts this tool and the Office skills when `DSH_PRIMARY_RUNTIME` names a payload directory whose sibling `office-skills/` carries the skill resources; both rows stay disabled otherwise.
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.
### 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.
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.
-----
@@ -108,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 payload build (`apps/desktop/scripts/prepare-primary-runtime.ts`) still targets Desktop; other carriers assemble the same layout themselves.
- The Linux build target is GNU/Linux x64; Linux ARM64 and 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>
@@ -117,6 +123,6 @@ Append-only tool result in the turn history; no prompt section is added.
<details>
<summary>Working context for maintainers — click to expand</summary>
Extracted from `apps/desktop-host` so the SDK profile can mount the same tool from a container image layer. The in-place mode and the optional Node.js/pnpm components exist for that carrier.
Placement and carrier choices are recorded in the [shared-runtime Agent Note](../../../.agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.md).
</details>
@@ -37,14 +37,20 @@ kind: "package-reference"
| 字段 | 默认值 | 含义 |
|---|---|---|
| `source` | 必填 | 含 `runtime.json` 与 `dependencies/` 的 payload 目录。 |
| `root` | 未设置 | Harness home 下的安装目录。设置时首次调用把 payload 复制过去,`runtime.json` 不变则复用;未设置时校验后原地使用,不复制。 |
| `source` | 必填 | 含 `runtime.json` 与 `dependencies/` 的 payload 绝对目录。 |
| `root` | 未设置 | Harness home 下的绝对安装目录。设置时首次调用把 payload 复制过去,`runtime.json` 不变则复用;未设置时校验后原地使用,不复制。 |
### payload 布局
`runtime.json` 记录 `desktopVersion`、`platform`(`win32`、`darwin` 或 `linux`)、`arch`、`components` 各版本(`python`、`numpy`、`pandas`;`node` 与 `pnpm` 可选),以及可选的 `payloadDigest` 与 `pythonPackages`。条目位于 `dependencies/`:`python/bin/python3`(Windows 为 `python/python.exe`)及其下的 `site-packages`;声明了才有的 `node/bin/node`、`node/node_modules` 与 `pnpm/bin/pnpm.mjs`。平台或架构与当前进程不符的清单被拒绝。
`sdk` profile 在 `DSH_PRIMARY_RUNTIME` 指向一个 payload 目录、且其同级 `office-skills/` 带有 skill 资源时挂载本工具与 Office skills;否则两行都保持禁用。
`DSH_PRIMARY_RUNTIME` 非空时,`sdk` profile 启用本工具和 Office skills。profile 从启动目录解析该路径,并加载同级 `office-skills/` 资源。缺少 skill 资源会产生启动警告并使 Office skills 不可用;无效或不完整的运行时 payload 会在首次工具调用时失败。未设置或为空时,两行都禁用。profile patch 可以覆盖任意一行,配置变更需要重启 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 读写检查。跨目标构建必须在部署前到目标主机执行这些检查。
容器可将这两个目录复制到不可变镜像层,并将 `DSH_PRIMARY_RUNTIME` 设为 `primary-runtime/` 的绝对路径。SDK 原位查询该 payload。Desktop 使用同一构建器,并保留 Harness-home 安装与签名检查。
-----
@@ -108,7 +114,7 @@ kind: "package-reference"
<a id="known-limitations-and-deferred-work"></a>
- payload 的构建脚本(`apps/desktop/scripts/prepare-primary-runtime.ts`)仍只面向 Desktop;其它载体按同一布局自行组装。
- Linux 构建目标为 GNU/Linux x64;尚未锁定 Linux ARM64 与 musl payload。
- Windows 上原地使用的 payload 须在载体里已可执行;原地模式不修复权限。
<a id="dev-note"></a>
@@ -117,6 +123,6 @@ kind: "package-reference"
<details>
<summary>维护者的工作上下文——点击展开</summary>
从 `apps/desktop-host` 抽出,让 SDK profile 能从容器镜像层挂载同一个工具。原地模式与可选的 Node.js/pnpm 组件都是为这种载体加的。
包归属与载体选择记录在[共享运行时 Agent Note](../../../.agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.zh.md) 中。
</details>
@@ -1,7 +1,7 @@
/** Model-facing query for a bundled Python, Node.js, and pnpm payload, in place or installed under the Harness home. */
import { cp, lstat, mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { dirname, isAbsolute, join } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -193,6 +193,9 @@ export async function installPrimaryRuntime(source: string, root: string): Promi
* @param config - Payload location and optional installation directory.
*/
export function apply(ctx: Context, config: Config): void {
if (!isAbsolute(config.source) || (config.root !== undefined && !isAbsolute(config.root))) {
throw new Error('workspace dependencies: source and root must be absolute paths')
}
let preparation: Promise<WorkspaceDependencies> | undefined
ctx.effect(() => async () => {
// Tool execution reports preparation failures; disposal only waits for filesystem work to settle.
@@ -1,8 +1,9 @@
import * as fs from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, expect, it } from 'vitest'
import { afterEach, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
@@ -13,8 +14,16 @@ import { ToolCallId } from '@deepseek-ai/dsh-llm'
import * as workspaceDependencies from '../src/index.ts'
import { installPrimaryRuntime, readPrimaryRuntime, resolvePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../src/index.ts'
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof fs>()
return { ...actual, rename: vi.fn(actual.rename), lstat: vi.fn(actual.lstat), cp: vi.fn(actual.cp) }
})
const roots: string[] = []
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))) })
afterEach(async () => {
vi.resetAllMocks()
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })))
})
async function fixture() {
const directory = await mkdtemp(join(tmpdir(), 'dsh-primary-runtime-'))
@@ -46,7 +55,7 @@ it.each(['win32', 'darwin', 'linux'])('returns %s interpreter and package paths'
expect(paths.pythonPackages).toBe(join('/runtime', 'dependencies', 'python', ...(platform === 'win32' ? ['Lib'] : ['lib', 'python3.12']), 'site-packages'))
})
it.skipIf(process.platform === 'linux')('installs offline, reuses the same release, and leaves environment and user packages unchanged', async () => {
it('installs offline, reuses the same release, and leaves environment and user packages unchanged', async () => {
const { source, root, manifest } = await fixture()
const environment = { ...process.env }
const installed = await installPrimaryRuntime(source, root)
@@ -57,7 +66,7 @@ it.skipIf(process.platform === 'linux')('installs offline, reuses the same relea
expect(process.env).toEqual(environment)
})
it.skipIf(process.platform === 'linux')('replaces release components and recovers an interrupted directory swap', async () => {
it('replaces release components and recovers an interrupted directory swap', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await rename(root, `${root}.previous`)
@@ -66,7 +75,7 @@ it.skipIf(process.platform === 'linux')('replaces release components and recover
expect((await readPrimaryRuntime(root)).desktopVersion).toBe('2.0.0')
})
it.skipIf(process.platform === 'linux')('replaces dependencies when the locked payload changes without a Desktop version change', async () => {
it('replaces dependencies when the locked payload changes without a Desktop version change', async () => {
const { source, root, manifest } = await fixture()
const first = { ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.1.2' } }
await writeFile(join(source, 'runtime.json'), JSON.stringify(first))
@@ -81,7 +90,7 @@ it.skipIf(process.platform === 'linux')('replaces dependencies when the locked p
await expect(readFile(join(installed.pythonPackages, 'old-package.py'))).rejects.toMatchObject({ code: 'ENOENT' })
})
it.skipIf(process.platform === 'linux')('upgrades a release manifest without a payload digest', async () => {
it('upgrades a release manifest without a payload digest', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }))
@@ -89,7 +98,7 @@ it.skipIf(process.platform === 'linux')('upgrades a release manifest without a p
expect((await readPrimaryRuntime(root)).payloadDigest).toBe('a'.repeat(64))
})
it.skipIf(process.platform === 'linux')('replaces changed payload bytes when only the digest changes', async () => {
it('replaces changed payload bytes when only the digest changes', async () => {
const { source, root, manifest } = await fixture()
const first = { ...manifest, payloadDigest: 'a'.repeat(64), pythonPackages: { 'python-docx': '1.2.0' } }
const sourceFile = join(workspaceDependencyPaths(source, first).pythonPackages, 'library.py')
@@ -103,7 +112,7 @@ it.skipIf(process.platform === 'linux')('replaces changed payload bytes when onl
expect((await readPrimaryRuntime(root)).pythonPackages).toEqual(first.pythonPackages)
})
it.skipIf(process.platform === 'linux')('keeps the installed release when the replacement payload is incomplete', async () => {
it('keeps the installed release when the replacement payload is incomplete', async () => {
const { source, root, manifest } = await fixture()
await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
@@ -112,7 +121,7 @@ it.skipIf(process.platform === 'linux')('keeps the installed release when the re
expect((await readPrimaryRuntime(root)).desktopVersion).toBe('1.0.0')
})
it.skipIf(process.platform === 'linux')('refuses linked installation directories without modifying their targets', async () => {
it('refuses linked installation directories without modifying their targets', async () => {
const { source, root, directory } = await fixture()
const outside = join(directory, 'outside')
await mkdir(outside)
@@ -217,3 +226,122 @@ it('rejects a payload whose Node.js or pnpm version is malformed', async () => {
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, components: { ...manifest.components, node: 'latest' } }))
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
})
/** Create the real scoped tool registry for lifecycle and failure-path assertions. */
async function tool(config: workspaceDependencies.Config) {
const ctx = new Context()
try {
await ctx.plugin(AgentRegistry)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
const fiber = ctx.plugin(workspaceDependencies, config)
await fiber
const execute = () => ctx.tools.execute({
signal: new AbortController().signal, name: 'load_workspace_dependencies',
arguments: {}, callId: ToolCallId('dependencies'),
})
return { ctx, fiber, execute }
} catch (error) {
await ctx.fiber.dispose()
throw error
}
}
it.each([null, [], { desktopVersion: '' }, { platform: 'unsupported' }, { arch: 'ia32' },
{ components: null }, { components: { python: 3 } }, { payloadDigest: 1 },
{ pythonPackages: null }, { pythonPackages: { numpy: 3 } },
])('rejects malformed manifest fields: %j', async (invalid) => {
const { source, manifest } = await fixture()
const value = invalid === null || Array.isArray(invalid) ? invalid : { ...manifest, ...invalid }
await writeFile(join(source, 'runtime.json'), JSON.stringify(value))
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
})
it('reads legacy metadata without a distribution map and rejects incompatible platforms', async () => {
const { source, manifest } = await fixture()
const { pythonPackages: _packages, ...legacy } = manifest
await writeFile(join(source, 'runtime.json'), JSON.stringify(legacy))
expect((await resolvePrimaryRuntime(source)).pythonDistributions).toEqual({})
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, platform: process.platform === 'linux' ? 'darwin' : 'linux' }))
await expect(resolvePrimaryRuntime(source)).rejects.toThrow('incompatible')
})
it('propagates filesystem access failures without touching the installation', async () => {
const { source, root } = await fixture()
vi.mocked(fs.lstat).mockRejectedValueOnce(Object.assign(new Error('denied'), { code: 'EACCES' }))
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('denied')
await expect(readFile(join(root, 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
})
it.each([false, true])('preserves the previous installation if publication fails (previous: %s)', async (previous) => {
const { source, root, manifest } = await fixture()
if (previous) {
await installPrimaryRuntime(source, root)
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, desktopVersion: '2.0.0' }))
}
const actual = await vi.importActual<typeof fs>('node:fs/promises')
vi.mocked(fs.rename).mockImplementationOnce(previous ? actual.rename : async () => { throw new Error('publication failed') })
if (previous) vi.mocked(fs.rename).mockRejectedValueOnce(new Error('publication failed'))
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('publication failed')
if (previous) expect((await readPrimaryRuntime(root)).desktopVersion).toBe('1.0.0')
else await expect(readFile(join(root, 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
expect((await fs.readdir(dirname(root))).filter(name => name.startsWith('.primary-runtime-'))).toEqual([])
})
it('retries a failed tool preparation and renders its call presentation', async () => {
const { source, root } = await fixture()
const metadata = await readFile(join(source, 'runtime.json'))
await rm(join(source, 'runtime.json'))
const { ctx, execute } = await tool({ source, root })
try {
expect((await execute()).isError).toBe(true)
await writeFile(join(source, 'runtime.json'), metadata)
expect((await execute()).isError).toBe(false)
expect(await readFile(join(root, 'runtime.json'))).toEqual(metadata)
const descriptor = ctx.tools.get('load_workspace_dependencies')!
expect(descriptor.presentCall?.({})).toEqual({ card: 'generic', title: 'Load workspace dependencies', kind: 'read' })
} finally { await ctx.fiber.dispose() }
})
it.each([false, true])('disposal waits for pending filesystem preparation (failure: %s)', async (failure) => {
const { source, root } = await fixture()
const actual = await vi.importActual<typeof fs>('node:fs/promises')
let release!: () => void
let copying!: () => void
const entered = new Promise<void>((resolve) => { copying = resolve })
const proceed = new Promise<void>((resolve) => { release = resolve })
vi.mocked(fs.cp).mockImplementationOnce(async (...args) => {
copying()
await proceed
if (failure) throw new Error('copy failed')
await actual.cp(...args)
})
const { ctx, fiber, execute } = await tool({ source, root })
const pending = execute()
try {
await entered
let disposed = false
const disposal = fiber.dispose().then(() => { disposed = true })
await Promise.resolve()
expect(disposed).toBe(false)
release()
await disposal
expect((await pending).isError).toBe(failure)
expect(ctx.tools.schemas().some(value => value.name === 'load_workspace_dependencies')).toBe(false)
} finally {
release()
await pending
await ctx.fiber.dispose()
}
})
it('disposes an unused tool without preparing the payload', async () => {
const { source, root } = await fixture()
const { ctx } = await tool({ source, root })
await ctx.fiber.dispose()
await expect(readFile(join(root, 'runtime.json'))).rejects.toMatchObject({ code: 'ENOENT' })
})
it.each([{ source: 'relative' }, { source: process.cwd(), root: 'relative' }])('rejects relative runtime configuration at plugin load: %j', async (config) => {
await expect(tool(config)).rejects.toThrow('must be absolute paths')
})
+12
View File
@@ -99,9 +99,15 @@ importers:
execa:
specifier: ^10.0.0
version: 10.0.0
extract-zip:
specifier: ^2.0.1
version: 2.0.1(supports-color@9.4.0)
fast-check:
specifier: ^4.8.0
version: 4.8.0
fflate:
specifier: ^0.8.2
version: 0.8.3
istanbul-lib-report:
specifier: ^3.0.1
version: 3.0.1
@@ -138,6 +144,9 @@ importers:
oxlint-tsgolint:
specifier: 7.0.2001
version: 7.0.2001
pnpm:
specifier: 11.7.0
version: 11.7.0
publint:
specifier: ^0.3.21
version: 0.3.21
@@ -147,6 +156,9 @@ importers:
spdx-expression-parse:
specifier: ^5.0.0
version: 5.0.0
tar:
specifier: ^7.5.0
version: 7.5.22
tsdown:
specifier: ^0.22.2
version: 0.22.2(oxc-resolver@11.20.0)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.3)
+1 -1
View File
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSyn
import { join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { describe, expect, it } from 'vitest'
import desktopRuntimeLock from '../apps/desktop/scripts/primary-runtime-lock.json' with { type: 'json' }
import desktopRuntimeLock from './primary-runtime/lock.json' with { type: 'json' }
import {
CLAUDE_AGENT_SDK_PACKAGE,
assertRuntimeLicenses,
+2 -2
View File
@@ -13,7 +13,7 @@ import { dirname, resolve } from 'node:path'
import * as yaml from 'js-yaml'
import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
import parseSpdx from 'spdx-expression-parse'
import desktopRuntimeLock from '../apps/desktop/scripts/primary-runtime-lock.json' with { type: 'json' }
import desktopRuntimeLock from './primary-runtime/lock.json' with { type: 'json' }
import { browserBundledExternals } from './browser-bundled-externals.ts'
const root = resolve(import.meta.dirname, '..')
@@ -819,7 +819,7 @@ ${python.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.license} | ${dep.r
## Desktop bundled Python distributions
The [Desktop runtime lock](apps/desktop/scripts/primary-runtime-lock.json) records each distribution version and the wheel download hashes. The table includes every entry in \`pythonPackages\`, including transitive dependencies. Wheel extraction preserves distribution metadata and the license and notice files supplied by each archive. Project licenses below do not enumerate the separate licenses of native libraries bundled inside wheels.
The [Desktop runtime lock](scripts/primary-runtime/lock.json) records each distribution version and the wheel download hashes. The table includes every entry in \`pythonPackages\`, including transitive dependencies. Wheel extraction preserves distribution metadata and the license and notice files supplied by each archive. Project licenses below do not enumerate the separate licenses of native libraries bundled inside wheels.
| Distribution | Locked version | Project license |
| --- | --- | --- |
@@ -74,6 +74,30 @@
"sha256": "623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d"
}
]
},
"linux-x64": {
"nodeArchive": "linux-x64.tar.gz",
"nodeSha256": "6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff",
"pythonTarget": "x86_64-unknown-linux-gnu",
"pythonSha256": "72748da13197c1fb161e3afeef20a6a385ff24f2165e6e2758e47008e7faba4c",
"wheels": [
{
"url": "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28"
},
{
"url": "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4"
},
{
"url": "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"sha256": "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91"
},
{
"url": "https://files.pythonhosted.org/packages/db/36/aa413bc214dc4f785ad2b2ddd8cc99aae7062d49ab155e91e6011af00daf/lxml-6.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
"sha256": "379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11"
}
]
}
},
"wheels": [
+135
View File
@@ -0,0 +1,135 @@
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { zipSync } from 'fflate'
import { expect, it } from 'vitest'
import { downloadPrimaryRuntimeAsset, prepareOfficeSkillAssets, primaryRuntimePayloadDigest, smokePrimaryRuntime, unpackPrimaryRuntimeWheel } from './prepare.ts'
import lock from './lock.json' with { type: 'json' }
const libraryWheel = Buffer.from('UEsDBAoAAAAAAASeLl0sYMPjDAAAAAwAAAAJAAAAc2FtcGxlLnB5c2FtcGxlID0gNDIKUEsBAh4DCgAAAAAABJ4uXSxgw+MMAAAADAAAAAkAAAAAAAAAAQAAAKSBAAAAAHNhbXBsZS5weVBLBQYAAAAAAQABADcAAAAzAAAAAAA=', 'base64')
const relocatedWheel = Buffer.from('UEsDBAoAAAAAAASeLl3x0Nj9FAAAABQAAAAeAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlcmVxdWlyZXMgcmVsb2NhdGlvbgpQSwECHgMKAAAAAAAEni5d8dDY/RQAAAAUAAAAHgAAAAAAAAABAAAApIEAAAAAc2FtcGxlLTEuMC5kYXRhL3NjcmlwdHMvc2FtcGxlUEsFBgAAAAABAAEATAAAAFAAAAAAAA==', 'base64')
const externalLibraryWheel = Buffer.from('UEsDBBQAAAAAAAAAIVyBOE8OHAAAABwAAAAhAAAAc2FtcGxlLTEuMC5kYXRhL3B1cmVsaWIvc2FtcGxlLnB5cmVxdWlyZXMgbGlicmFyeSByZWxvY2F0aW9uClBLAQIUAxQAAAAAAAAAIVyBOE8OHAAAABwAAAAhAAAAAAAAAAAAAACAAQAAAABzYW1wbGUtMS4wLmRhdGEvcHVyZWxpYi9zYW1wbGUucHlQSwUGAAAAAAEAAQBPAAAAWwAAAAAA', 'base64')
it.each(Object.entries(lock.targets))('records every locked wheel distribution and version for %s', (_target, artifact) => {
const normalize = (name: string): string => name.toLowerCase().replace(/[-_.]+/gu, '-')
const distributions = [...artifact.wheels, ...lock.wheels].map(({ url }) => {
const [name, version] = basename(new URL(url).pathname).split('-')
return [normalize(name!), version] as const
})
const declared = Object.entries(lock.pythonPackages).map(([name, version]) => [normalize(name), version] as const)
expect(new Set(distributions.map(([name]) => name)).size).toBe(distributions.length)
expect(new Set(declared.map(([name]) => name)).size).toBe(declared.length)
expect(Object.fromEntries(distributions)).toEqual(Object.fromEntries(declared))
})
it('keeps a target payload identity independent of other target archives', () => {
const changed = structuredClone(lock)
changed.targets['win-x64'].wheels[0]!.sha256 = 'a'.repeat(64)
expect(primaryRuntimePayloadDigest('mac-arm64', changed, '11.7.0')).toBe(primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.0'))
expect(primaryRuntimePayloadDigest('win-x64', changed, '11.7.0')).not.toBe(primaryRuntimePayloadDigest('win-x64', lock, '11.7.0'))
})
it('invalidates payload identity for shared wheels, package versions and package-manager changes', () => {
const wheel = structuredClone(lock), distribution = structuredClone(lock)
wheel.wheels[0]!.sha256 = 'a'.repeat(64)
distribution.pythonPackages['python-docx'] = '1.2.1'
const original = primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.0')
expect(primaryRuntimePayloadDigest('mac-arm64', wheel, '11.7.0')).not.toBe(original)
expect(primaryRuntimePayloadDigest('mac-arm64', distribution, '11.7.0')).not.toBe(original)
expect(primaryRuntimePayloadDigest('mac-arm64', lock, '11.7.1')).not.toBe(original)
})
it('reports missing distribution metadata before trying to execute a stale native payload', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-stale-runtime-'))
try {
await writeFile(join(root, 'runtime.json'), JSON.stringify({ platform: process.platform, arch: process.arch }))
expect(() => { smokePrimaryRuntime(root) }).toThrow('missing Python distribution versions; prepare the payload')
} finally { await rm(root, { recursive: true, force: true }) }
})
it('extracts a hash-verified cached library without a Python installer or network request', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const hash = createHash('sha256').update(libraryWheel).digest('hex')
const archive = join(root, hash)
await writeFile(archive, libraryWheel)
expect(await downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).toBe(archive)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample.py'), 'utf8')).toBe('sample = 42\n')
await writeFile(archive, 'corrupt archive')
await expect(downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).rejects.toThrow('checksum mismatch')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('fully extracts a large deflate-compressed wheel entry', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-large-wheel-'))
try {
const expected = Buffer.alloc(1024 * 1024)
// Seeded xorshift bytes keep the compressed entry larger than the stream buffers.
let state = 26
for (let index = 0; index < expected.length; index++) {
state ^= state << 13
state ^= state >>> 17
state ^= state << 5
expected[index] = state & 255
}
const archive = join(root, 'large.whl')
await writeFile(archive, zipSync({ 'large.bin': expected }, { level: 6 }))
await unpackPrimaryRuntimeWheel(archive, root)
expect(await readFile(join(root, 'large.bin'))).toEqual(expected)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('retains auxiliary wheel scripts without generating command wrappers', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const archive = join(root, 'relocated.whl')
await writeFile(archive, relocatedWheel)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample-1.0.data/scripts/sample'), 'utf8')).toBe('requires relocation\n')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('rejects library files requiring an unsupported installation scheme', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-wheel-'))
try {
const archive = join(root, 'relocated.whl')
await writeFile(archive, externalLibraryWheel)
await expect(unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))).rejects.toThrow('unsupported installation paths')
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('copies complete Office resources outside the application archive and removes obsolete assets', async () => {
const root = await mkdtemp(join(tmpdir(), 'desktop-office-assets-'))
try {
const source = join(root, 'package', 'assets')
const destination = join(root, 'Contents', 'Resources', 'runtime', 'office-skills')
await mkdir(join(source, 'scripts'), { recursive: true })
await writeFile(join(source, 'scripts', 'check_office.py'), 'print("checker")\n')
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
await mkdir(join(source, name))
await writeFile(join(source, name, 'SKILL.md'), `# ${name}\n`)
}
await prepareOfficeSkillAssets(source, destination)
await writeFile(join(destination, 'obsolete.py'), 'old helper')
await prepareOfficeSkillAssets(source, destination)
for (const name of ['office-docx', 'office-pptx', 'office-xlsx']) {
expect(await readFile(join(destination, name, 'SKILL.md'), 'utf8')).toBe(`# ${name}\n`)
}
expect(await readFile(join(destination, 'scripts', 'check_office.py'), 'utf8')).toBe('print("checker")\n')
await expect(readFile(join(destination, 'obsolete.py'))).rejects.toMatchObject({ code: 'ENOENT' })
} finally { await rm(root, { recursive: true, force: true }) }
})
it('gives Python-only payloads a distinct identity', () => {
expect(primaryRuntimePayloadDigest('linux-x64', lock, undefined)).not.toBe(primaryRuntimePayloadDigest('linux-x64', lock, '11.7.0'))
})
+210
View File
@@ -0,0 +1,210 @@
/** Prepare pinned, relocatable script interpreters without installing into the build host. */
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { cp } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import extractZip from 'extract-zip'
import { x as extractTar } from 'tar'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../packages/skill/tool-workspace-dependencies/src/index.ts'
import lock from './lock.json' with { type: 'json' }
/**
* Download or reuse an archive only when its bytes match the release lock.
* @param url - Locked archive URL.
* @param sha256 - Expected SHA-256 digest.
* @param cache - Download cache directory.
* @returns Verified local archive path.
*/
export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string): Promise<string> {
const destination = join(cache, sha256)
let bytes: Buffer
try { bytes = readFileSync(destination) } catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
const response = await fetch(url)
if (!response.ok) throw new Error(`primary runtime download: ${String(response.status)} ${url}`)
bytes = Buffer.from(await response.arrayBuffer())
}
if (createHash('sha256').update(bytes).digest('hex') !== sha256) throw new Error(`primary runtime download: checksum mismatch for ${url}`)
writeFileSync(destination, bytes)
return destination
}
async function pythonArchive(target: keyof typeof lock.targets, cache: string): Promise<string> {
const artifact = lock.targets[target]
const filename = `cpython-${lock.pythonVersion}+${lock.pythonRelease}-${artifact.pythonTarget}-install_only_stripped.tar.gz`
return downloadPrimaryRuntimeAsset(`https://github.com/astral-sh/python-build-standalone/releases/download/${lock.pythonRelease}/${encodeURIComponent(filename)}`, artifact.pythonSha256, cache)
}
/**
* Identify the inputs that assemble one target's payload, excluding unrelated target locks.
* @param target - Runtime target whose archives are installed.
* @param runtimeLock - Locked interpreter and wheel inputs.
* @param pnpmVersion - Package-manager version copied into the payload.
* @returns SHA-256 payload identity for installation reuse.
*/
export function primaryRuntimePayloadDigest(
target: keyof typeof lock.targets, runtimeLock: typeof lock, pnpmVersion: string | undefined,
): string {
const { pythonVersion, pythonRelease, nodeVersion, wheels, pythonPackages } = runtimeLock
// Identity preserves key order within the selected target, wheel records and distribution map, plus wheel-entry order.
// Bump format when extraction or assembly changes payload bytes without changing locked inputs.
return createHash('sha256').update(JSON.stringify({
format: 3, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion,
artifact: runtimeLock.targets[target], wheels, pythonPackages, pnpm: pnpmVersion,
})).digest('hex')
}
/**
* Unpack a locked library wheel, retaining auxiliary scripts in its distribution data directory.
* @param archive - Hash-verified wheel archive.
* @param destination - Absolute site-packages directory.
* @returns Resolves after extraction without command wrappers; rejects other wheel installation schemes.
*/
export async function unpackPrimaryRuntimeWheel(archive: string, destination: string): Promise<void> {
await extractZip(archive, {
dir: destination,
onEntry: (entry) => {
const [directory, scheme] = entry.fileName.split('/')
if (directory?.endsWith('.data') && scheme !== '' && scheme !== 'scripts') {
throw new Error(`primary runtime: wheel requires unsupported installation paths: ${entry.fileName}`)
}
},
})
}
/**
* Copy the skill package's complete asset tree to ordinary filesystem resources.
* @param source - The package's assets directory.
* @param destination - External Office skill resource directory.
* @returns Resolves after replacing the external assets with the complete package tree.
*/
export async function prepareOfficeSkillAssets(source: string, destination: string): Promise<void> {
rmSync(destination, { recursive: true, force: true })
await cp(source, destination, { recursive: true, dereference: true })
}
/** A locked interpreter and wheel target. */
export type PrimaryRuntimeTarget = keyof typeof lock.targets
/** Build-only inputs shared by Desktop and SDK carriers. */
export interface PreparePrimaryRuntimeOptions {
/** Target whose archives and wheels are downloaded. */
readonly target: PrimaryRuntimeTarget
/** Resource directory receiving primary-runtime/ and office-skills/. */
readonly output: string
/** SHA-256-addressed archive cache. */
readonly cache: string
/** Carrier release recorded in the legacy desktopVersion manifest field. */
readonly version: string
/** Omit Node.js and pnpm for carriers providing only Python. */
readonly pythonOnly?: boolean
}
/**
* Materialize locked interpreters, libraries and Office resources without executing target code.
* @param options - Explicit target and carrier-owned output locations.
* @returns Resolves after the complete payload and skills have been copied to the output directory.
*/
export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOptions): Promise<void> {
const { target } = options
const paths = { runtime: resolve(options.output), downloads: resolve(options.cache) }
const artifact = lock.targets[target]
mkdirSync(paths.runtime, { recursive: true })
mkdirSync(paths.downloads, { recursive: true })
const staging = mkdtempSync(join(tmpdir(), 'dsh-primary-'))
try {
const output = join(staging, 'payload')
const dependencies = join(output, 'dependencies')
mkdirSync(dependencies, { recursive: true })
let pnpmVersion: string | undefined
if (!options.pythonOnly) {
const nodeFilename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}`
const nodeArchive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${nodeFilename}`, artifact.nodeSha256, paths.downloads)
const unpackedNode = join(staging, 'node')
mkdirSync(unpackedNode)
if (target === 'win-x64') await extractZip(nodeArchive, { dir: unpackedNode })
else await extractTar({ file: nodeArchive, cwd: unpackedNode })
const nodeSource = join(unpackedNode, nodeFilename.replace(/\.(?:zip|tar\.gz)$/u, ''))
mkdirSync(join(dependencies, 'node', 'bin'), { recursive: true })
mkdirSync(join(dependencies, 'node', 'node_modules'))
writeFileSync(join(dependencies, 'node', 'node_modules', 'README.txt'), 'Reserved for bundled Node packages. pnpm uses its default installation directories.\n')
cpSync(join(nodeSource, ...(target === 'win-x64' ? ['node.exe'] : ['bin', 'node'])),
join(dependencies, 'node', 'bin', target === 'win-x64' ? 'node.exe' : 'node'))
cpSync(join(nodeSource, 'LICENSE'), join(dependencies, 'node', 'LICENSE'))
const require = createRequire(import.meta.url)
const pnpmManifest = require.resolve('pnpm')
pnpmVersion = (JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }).version
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
}
await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies })
const manifest: PrimaryRuntimeManifest = {
desktopVersion: options.version,
platform: target === 'win-x64' ? 'win32' : target === 'linux-x64' ? 'linux' : 'darwin',
arch: target === 'mac-arm64' ? 'arm64' : 'x64',
payloadDigest: primaryRuntimePayloadDigest(target, lock, pnpmVersion),
pythonPackages: lock.pythonPackages,
components: {
python: lock.pythonVersion,
...(pnpmVersion === undefined ? {} : { node: lock.nodeVersion, pnpm: pnpmVersion }),
numpy: lock.pythonPackages.numpy, pandas: lock.pythonPackages.pandas,
},
}
const entries = workspaceDependencyPaths(output, manifest)
for (const wheel of [...artifact.wheels, ...lock.wheels]) {
await unpackPrimaryRuntimeWheel(await downloadPrimaryRuntimeAsset(wheel.url, wheel.sha256, paths.downloads), entries.pythonPackages)
}
writeFileSync(join(output, 'runtime.json'), `${JSON.stringify(manifest, undefined, 2)}\n`)
const destination = join(paths.runtime, 'primary-runtime')
rmSync(destination, { recursive: true, force: true })
await cp(output, destination, { recursive: true, dereference: true })
} finally {
rmSync(staging, { recursive: true, force: true })
}
const require = createRequire(import.meta.url)
await prepareOfficeSkillAssets(join(dirname(require.resolve('@deepseek-ai/dsh-skill-office/package.json')), 'assets'),
join(paths.runtime, 'office-skills'))
}
/**
* Execute the native payload's interpreters, package manager and Python libraries.
* @param root - Final payload directory, including any platform signatures.
* @param environment - Scrubbed subprocess environment; defaults to excluding credential-shaped names.
*/
export function smokePrimaryRuntime(root: string, environment: NodeJS.ProcessEnv = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !/(?:KEY|SECRET|TOKEN|PASSWORD)/iu.test(name)),
)): void {
const manifest = JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')) as PrimaryRuntimeManifest
if (manifest.platform !== process.platform || manifest.arch !== process.arch) return
if (manifest.pythonPackages === undefined) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
const entries = workspaceDependencyPaths(root, manifest)
const options = { stdio: 'inherit', timeout: 120_000, env: environment } as const
execFileSync(entries.python, ['-I', '-B', '-c', 'import decimal, xml.parsers.expat, lzma, uuid, numpy, pandas; assert numpy.arange(4).sum() == 6; assert pandas.DataFrame({"n": [1, 2]}).n.sum() == 3'], options)
execFileSync(entries.python, ['-I', '-B', join(import.meta.dirname, 'smoke.py'), JSON.stringify(manifest.pythonPackages),
manifest.components.python, join(dirname(root), 'office-skills', 'scripts', 'check_office.py')], options)
execFileSync(entries.python, ['-I', '-B', '-m', 'pip', 'check'], options)
if (entries.node !== undefined) execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(manifest.components.node)}) process.exit(1)`], options)
if (entries.pnpm !== undefined && entries.node !== undefined) execFileSync(entries.node, [entries.pnpm, '--version'], options)
}
if (import.meta.main) {
const { values } = parseArgs({ options: {
target: { type: 'string' }, output: { type: 'string' }, cache: { type: 'string' },
'python-only': { type: 'boolean', default: false },
} })
if (!values.target || !Object.hasOwn(lock.targets, values.target) || !values.output) {
throw new Error(`Usage: pnpm run prepare:primary-runtime --target <${Object.keys(lock.targets).join('|')}> --output <directory> [--cache <directory>] [--python-only]`)
}
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')) as { version: string }
const output = resolve(values.output)
await preparePrimaryRuntime({
target: values.target as PrimaryRuntimeTarget, output,
cache: values.cache ?? join(tmpdir(), 'dsh-primary-runtime-downloads'), version,
pythonOnly: values['python-only'],
})
smokePrimaryRuntime(join(output, 'primary-runtime'))
}