mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
refactor(skill): flatten primary runtime version metadata
This commit is contained in:
1 parent
b16b116408
commit
b51e64d70e
19 files changed
+188
-97
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-shared-office-runtime.md
|
||||
2026-09-17-shared-office-runtime.md: 5925974ce96a91afcb97f89af58f87a3b2d8ff93
|
||||
2026-09-17-shared-office-runtime.zh.md: 61be67508f285cb3e06305c95eed617e0d5f4218
|
||||
2026-09-17-shared-office-runtime.md: c25ce3a66660f1dca9959ecbb155ae38f7046d00
|
||||
2026-09-17-shared-office-runtime.zh.md: 8889a6b13d55919d4b5e92b06d434186d70dbe6a
|
||||
@@ -16,6 +16,8 @@ The [shared build entry](../../../../scripts/primary-runtime/prepare.ts) owns th
|
||||
|
||||
The [Desktop primary-runtime decision](../feature/2026-09-14-desktop-primary-runtime.md) continues to own Desktop installation and platform signing. This decision replaces only its Desktop-only builder and query placement; both records remain active.
|
||||
|
||||
`runtime.json` stores Python, Node.js and pnpm versions at the top level and all Python distribution versions in `pythonPackages`. Build metadata does not single out numpy or pandas. The shared parser accepts legacy `components` files without rewriting them, retains their consistency checks, and returns only the canonical flat fields. Mixed formats are rejected. The build digest includes the assembly format so changed manifest bytes invalidate payload reuse.
|
||||
|
||||
## 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.
|
||||
@@ -24,6 +26,8 @@ The [Desktop primary-runtime decision](../feature/2026-09-14-desktop-primary-run
|
||||
|
||||
**Always copy to the Harness home.** Copying duplicates image-layer content, requires a writable destination and cannot serve the intended immutable carrier deployment.
|
||||
|
||||
**One component field per Python library.** This duplicates the distribution map and requires schema changes for each library. A complete map records the locked package set independently of the interpreter and package-manager fields.
|
||||
|
||||
## Consequences
|
||||
|
||||
Successful tool preparation is shared by concurrent calls for one plugin lifetime. Failed preparation is retryable; disposal removes registration and waits for outstanding filesystem work. Copy mode verifies a staged replacement and restores the preceding installation after a failed publication. In-place mode performs no payload writes. Paths are absolute, and runtime metadata and declared entries are checked before results are returned.
|
||||
|
||||
@@ -16,6 +16,8 @@ SDK 部署需要与 Desktop 相同的 Office 创作库,同时将解释器 payl
|
||||
|
||||
[Desktop 第一方运行时决策](../feature/2026-09-14-desktop-primary-runtime.zh.md)继续持有 Desktop 安装与平台签名规则。本决策仅替代其中由 Desktop 独占构建器和查询实现的部分;两份记录均保持有效。
|
||||
|
||||
`runtime.json` 在顶层保存 Python、Node.js 和 pnpm 版本,并在 `pythonPackages` 中保存全部 Python 分发包版本。构建元数据不单独列出 numpy 或 pandas。共享解析器接受旧 `components` 文件而不改写它们,保留旧格式的一致性校验,并仅返回统一的扁平字段。混合格式会被拒绝。构建摘要包含组装格式,因此清单字节变化会使旧产物身份失效。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**将实现保留在 Desktop。** SDK 和容器构建器只需要解释器、Office 资源和查询,却仍会依赖应用打包及签名代码。
|
||||
@@ -24,6 +26,8 @@ SDK 部署需要与 Desktop 相同的 Office 创作库,同时将解释器 payl
|
||||
|
||||
**总是复制到 Harness home。** 复制会重复镜像层内容、要求目标可写,也无法满足预期的不可变载体部署。
|
||||
|
||||
**为每个 Python 库增加组件字段。** 这会重复分发包版本表,并使新增库要求修改 schema。完整版本表将锁定包集合与解释器、包管理器字段分开记录。
|
||||
|
||||
## 影响
|
||||
|
||||
同一插件生命周期内的并发调用共享成功的工具准备结果。准备失败后可重试;卸载移除工具注册并等待未完成的文件系统工作。复制模式验证暂存替换内容,并在发布失败后恢复原安装。原位模式不写入 payload。路径为绝对路径,返回结果前会检查运行时元数据和所声明的条目。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md
|
||||
2026-09-14-desktop-primary-runtime.md: de535fd7125c08bf788ddf7680224c6e0df81a4a
|
||||
2026-09-14-desktop-primary-runtime.zh.md: 508c574e4970b4cc53ef3bdaee8524a867673d34
|
||||
2026-09-14-desktop-primary-runtime.md: a1491ca0c47e40c6d5b885c9f6a42ccf8180b78c
|
||||
2026-09-14-desktop-primary-runtime.zh.md: 6447086a6948ed262699b62061eb44af8d6071d7
|
||||
@@ -12,7 +12,7 @@ Desktop agents need predictable Python data-processing libraries and an independ
|
||||
|
||||
Desktop ships Python, Node.js, pnpm, data-processing libraries and Office authoring libraries as one release-bound payload. The path-query tool installs the payload from application resources into the fixed Harness-home directory and returns absolute paths and the bundled Python distribution versions. The version report excludes packages added by users. It does not change PATH, environment variables or package-manager configuration. pnpm uses its native global-install rules.
|
||||
|
||||
The application version, component versions, Python distribution versions and locked-input digest live in `runtime.json`, not the directory name. The digest covers the selected target's interpreter and wheel archives, shared wheel inputs, Python distribution versions, pnpm version and assembly format; other targets do not invalidate it. Key order within the selected target, wheel records and distribution map, plus wheel-entry order, participates in this identity; top-level lock key order does not. Extraction or assembly changes that alter payload bytes without changing locked inputs require an explicit format bump. Installation publishes a completed staged copy and retains the previous directory until replacement succeeds. Matching payloads reuse installed files; replacements also replace user-added Python dependencies inside the managed tree. Older manifests without a digest remain readable and differ from the current payload. Distribution names use PEP 503 normalization; duplicate normalized names are rejected, and present numpy/pandas distribution versions must agree with their component versions. The Desktop single-instance owner and the tool's shared installation promise serialize normal installation requests.
|
||||
The application version, top-level interpreter/package-manager versions, Python distribution map and locked-input digest live in `runtime.json`, not the directory name. The digest covers the selected target's interpreter and wheel archives, shared wheel inputs, Python distribution versions, pnpm version and assembly format; other targets do not invalidate it. Key order within the selected target, wheel records and distribution map, plus wheel-entry order, participates in this identity; top-level lock key order does not. Extraction or assembly changes that alter payload bytes without changing locked inputs require an explicit format bump. Installation publishes a completed staged copy and retains the previous directory until replacement succeeds. Matching payloads reuse installed files; replacements also replace user-added Python dependencies inside the managed tree. Older manifests without a digest remain readable and differ from the current payload. Distribution names use PEP 503 normalization; duplicate normalized names are rejected. The reader normalizes legacy `components` files and retains their numpy/pandas consistency checks; current manifests carry Python libraries only in the distribution map. The Desktop single-instance owner and the tool's shared installation promise serialize normal installation requests.
|
||||
|
||||
The shared Node build entry downloads and hash-verifies the complete locked wheel set and unpacks library files into site-packages. This avoids build-host Python and pip version selection without implementing dependency resolution or general wheel installation. Auxiliary scripts, including XlsxWriter's VBA extraction script, remain under the wheel's `.data/scripts` directory; command-line entry-point wrappers are outside this library payload. Other `.data` installation schemes are rejected. Native smoke executes the final payload after temporary files are removed, checking the exact locked distribution set plus bundled pip, Python and pinned wheel versions, dependency completeness and editable Office document round trips, so interpreter links must survive relocation. Smoke checks disable bytecode writes to keep validation artifacts out of the shipped payload.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Desktop 代理需要在没有开发环境的机器上获得确定的 Python 数
|
||||
|
||||
Desktop 将 Python、Node.js、pnpm、数据处理库和 Office 创作库作为绑定应用版本的产物交付。路径查询工具从应用资源将产物安装到 Harness home 下的固定目录,并返回绝对路径与内置 Python 分发包版本。版本报告不包含用户自行添加的包。它不修改 PATH、环境变量或包管理器配置。pnpm 使用原生全局安装规则。
|
||||
|
||||
应用版本、组件版本、Python 分发包版本和锁定输入摘要记录在 `runtime.json` 中,不放在目录名里。摘要涵盖所选目标的解释器与 wheel 压缩包、共享 wheel 输入、Python 分发包版本、pnpm 版本和组装格式;其他目标不会使其失效。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序参与该身份计算;锁文件顶层键的顺序不参与。解压或组装逻辑在锁定输入不变时改变产物字节,必须显式提升格式版本。安装发布完整的暂存副本,并在替换成功前保留之前的目录。相同产物复用已安装文件;替换也会移除受管目录内用户添加的 Python 依赖。不含摘要的旧清单仍可读取,并与当前产物区分。分发包名称按 PEP 503 归一化,归一化后重复的名称会被拒绝;清单包含 numpy/pandas 分发包版本时,必须与相应组件版本一致。Desktop 单实例所有者和工具共享的安装 Promise 串行处理正常安装请求。
|
||||
应用版本、顶层解释器和包管理器版本、Python 分发包版本表和锁定输入摘要记录在 `runtime.json` 中,不放在目录名里。摘要涵盖所选目标的解释器与 wheel 压缩包、共享 wheel 输入、Python 分发包版本、pnpm 版本和组装格式;其他目标不会使其失效。所选目标、wheel 记录及分发包映射内部的键顺序,以及 wheel 条目顺序参与该身份计算;锁文件顶层键的顺序不参与。解压或组装逻辑在锁定输入不变时改变产物字节,必须显式提升格式版本。安装发布完整的暂存副本,并在替换成功前保留之前的目录。相同产物复用已安装文件;替换也会移除受管目录内用户添加的 Python 依赖。不含摘要的旧清单仍可读取,并与当前产物区分。分发包名称按 PEP 503 归一化,归一化后重复的名称会被拒绝。读取器归一化旧 `components` 文件并保留其 numpy/pandas 一致性校验;当前清单仅在分发包版本表中记录 Python 库。Desktop 单实例所有者和工具共享的安装 Promise 串行处理正常安装请求。
|
||||
|
||||
共享 Node 构建入口下载并校验完整锁定 wheel 集的哈希,将库文件解压到 site-packages。这避免选择构建主机的 Python 和 pip 版本,也无需实现依赖解析或通用 wheel 安装。XlsxWriter 的 VBA 提取脚本等辅助脚本保留在 wheel 的 `.data/scripts` 目录中;命令行入口包装器不属于该库产物。其他 `.data` 安装方案会被拒绝。本机 smoke 在临时文件删除后执行最终产物,检查精确锁定的分发包集合与内置 pip、Python 与固定 wheel 版本、依赖完整性及可编辑 Office 文档的写入和读取,因此解释器链接必须在迁移后仍有效。Smoke 检查禁用字节码写入,避免把验证产物纳入分发内容。
|
||||
|
||||
|
||||
@@ -415,8 +415,8 @@ 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' }) },
|
||||
python: '3.12.14',
|
||||
...(pythonOnly ? {} : { node: '24.21.0', pnpm: '11.7.0' }),
|
||||
pythonPackages: { 'python-docx': '1.2.0', 'python-pptx': '1.0.2', openpyxl: '3.1.5' },
|
||||
}
|
||||
const paths = workspaceDependencyPaths(source, manifest)
|
||||
|
||||
@@ -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: 91e093e17fe7b748beda76a39c2002732b892902
|
||||
README.zh.md: 484788eaaa5c7ce604f784c0f8b89688ecc5c815
|
||||
README.md: a798368dc5c94b0110251bd333951b6a221028dd
|
||||
README.zh.md: b4d14727e16ddccb4b96c41fa8158bfc9f66d531
|
||||
@@ -24,7 +24,7 @@ Desktop carries independent Python, Node.js and pnpm distributions. Python inclu
|
||||
|
||||
Desktop registers `office-docx`, `office-pptx`, and `office-xlsx` by default. The skills use the bundled Python libraries for creation and focused edits, then reopen the files and run a shared structural checker before delivery. PowerPoint creation and editing use python-pptx. Skill resources are copied to `runtime/office-skills` outside ASAR so Python can read the checker. An available `render_document` tool can add visual inspection; its absence does not prevent authoring or delivery. See the [Office skill package](../../packages/skill/skill-office/README.md) for checks and limitations.
|
||||
|
||||
The payload follows the Desktop release. `runtime.json` records the Desktop version, target, component and Python distribution versions, and a digest of the selected target’s locked payload inputs and assembly format. Distribution names use PEP 503 normalization; duplicate normalized names and conflicting numpy/pandas component and distribution versions reject the manifest. Matching installations are reused; a dependency or archive change replaces the directory after a complete staged copy even when the Desktop version stays unchanged. Older manifests without a digest are replaced on their next installation. User-added Python packages remain only while the payload identity matches. A failed directory replacement retains the previous installation; Windows may refuse replacement while an interpreter is still running.
|
||||
The payload follows the Desktop release. `runtime.json` records the Desktop version, target, top-level interpreter/package-manager versions and the Python distribution map, and a digest of the selected target’s locked payload inputs and assembly format. Distribution names use PEP 503 normalization; duplicate normalized names reject the manifest. Legacy `components` manifests remain readable through normalization, with their existing library-version consistency checks. Matching installations are reused; a dependency or archive change replaces the directory after a complete staged copy even when the Desktop version stays unchanged. Older manifests without a digest are replaced on their next installation. User-added Python packages remain only while the payload identity matches. A failed directory replacement retains the previous installation; Windows may refuse replacement while an interpreter is still running.
|
||||
|
||||
The private Desktop `runtime/bin` directory is added only to package-installation processes, not the Host PATH inherited by PTC and agent shells. This tool does not change PATH, environment variables or user package-manager configuration. pnpm retains its own defaults and user settings for global packages, executable entries and its store, including native errors when the environment does not support global installation. There is no separate dependency updater. [The primary-runtime decision](../../.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.md) records these choices.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Desktop 携带独立的 Python、Node.js 和 pnpm 分发包。Python 包含 nump
|
||||
|
||||
Desktop 默认注册 `office-docx`、`office-pptx` 和 `office-xlsx`。这些技能使用内置 Python 库创建文件和进行定点编辑,随后重新打开文件,并在交付前运行共享结构检查器。PowerPoint 的创建和编辑使用 python-pptx。技能资源复制到 ASAR 外的 `runtime/office-skills`,让 Python 可以读取检查器。可用的 `render_document` 工具可以补充视觉检查;缺少该工具不妨碍创作或交付。检查范围与限制见 [Office 技能包](../../packages/skill/skill-office/README.zh.md)。
|
||||
|
||||
该产物随 Desktop 版本发布。`runtime.json` 记录 Desktop 版本、目标平台、组件与 Python 分发包版本,以及所选目标的锁定产物输入与组装格式的摘要。分发包名称按 PEP 503 归一化;名称归一化后重复,或 numpy/pandas 的组件版本与分发包版本冲突时,清单会被拒绝。匹配的安装会被复用;依赖或压缩包变化后,即使 Desktop 版本不变,也会在完整暂存副本完成后替换目录。不含摘要的旧清单会在下次安装时被替换。用户自行添加的 Python 包仅在产物身份一致时保留。目录替换失败时保留之前的安装;解释器仍在运行时,Windows 可能拒绝替换。
|
||||
该产物随 Desktop 版本发布。`runtime.json` 记录 Desktop 版本、目标平台、顶层解释器和包管理器版本及 Python 分发包版本表,以及所选目标的锁定产物输入与组装格式的摘要。分发包名称按 PEP 503 归一化;名称归一化后重复时,清单会被拒绝。旧 `components` 清单通过归一化继续可读,并保留其原有库版本一致性校验。匹配的安装会被复用;依赖或压缩包变化后,即使 Desktop 版本不变,也会在完整暂存副本完成后替换目录。不含摘要的旧清单会在下次安装时被替换。用户自行添加的 Python 包仅在产物身份一致时保留。目录替换失败时保留之前的安装;解释器仍在运行时,Windows 可能拒绝替换。
|
||||
|
||||
Desktop 私有的 `runtime/bin` 目录仅添加到包安装进程,不进入 PTC 和 agent shell 从 Host 继承的 PATH。该工具不修改 PATH、环境变量或用户包管理器配置。pnpm 的全局包、命令入口和 store 保留自身默认值及用户设置,包括环境不支持全局安装时的原生错误。不提供独立依赖更新器。[第一方 Runtime 决策](../../.agents/notes/implemented/feature/2026-09-14-desktop-primary-runtime.zh.md)记录这些选择。
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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 { parsePrimaryRuntime, workspaceDependencyPaths } from '../../../packages/skill/tool-workspace-dependencies/src/index.ts'
|
||||
import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
|
||||
import { scrubWindowsSigningEnvironment } from './windows-sign.mjs'
|
||||
|
||||
@@ -24,9 +24,9 @@ export async function preparePrimaryRuntime(options: { deferSmoke?: boolean } =
|
||||
* @param root - Final payload directory, including any platform signatures.
|
||||
*/
|
||||
export function smokePrimaryRuntime(root: string): void {
|
||||
const manifest = JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')) as PrimaryRuntimeManifest
|
||||
const manifest = parsePrimaryRuntime(JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')))
|
||||
if (manifest.platform !== process.platform || manifest.arch !== process.arch) return
|
||||
if (manifest.pythonPackages === undefined) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
|
||||
if (Object.keys(manifest.pythonPackages).length === 0) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
|
||||
const entries = workspaceDependencyPaths(root, manifest)
|
||||
if (entries.node === undefined || entries.pnpm === undefined) throw new Error('primary runtime: the Desktop payload must declare node and pnpm components.')
|
||||
smokePayload(root, scrubWindowsSigningEnvironment(process.env))
|
||||
|
||||
@@ -10,8 +10,8 @@ it('rejects a Python-only native Desktop payload before executing interpreters',
|
||||
const root = await mkdtemp(join(tmpdir(), 'desktop-python-only-'))
|
||||
try {
|
||||
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' },
|
||||
desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
|
||||
python: '3.12.14', pythonPackages: { numpy: '2.3.5', pandas: '3.0.1' },
|
||||
}))
|
||||
expect(() => { smokePrimaryRuntime(root) }).toThrow('Desktop payload must declare node and pnpm')
|
||||
} finally { await rm(root, { recursive: true, force: true }) }
|
||||
|
||||
@@ -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: 77020b8ca21224b9c2aeed440c7837a1fc610e7c
|
||||
README.zh.md: 62ca9c088a3c6b69343285699864b2cd3418d683
|
||||
README.md: 092bc9084ad2807d830520ac540e661d9fe17bcb
|
||||
README.zh.md: 533a4f8c931a501e5f725cd099dbee72b7d9e436
|
||||
@@ -42,7 +42,7 @@ Mount the plugin beside the tool registry with the payload directory. Configurat
|
||||
|
||||
### 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.
|
||||
`runtime.json` records `desktopVersion`, `platform` (`win32`, `darwin`, or `linux`), `arch`, optional `payloadDigest`, top-level `python`, optional `node`/`pnpm` versions, and the complete `pythonPackages` distribution-version map. A pnpm entry requires Node.js. Python libraries, including numpy and pandas, appear only in `pythonPackages`. Entries live under `dependencies/`: `python/bin/python3` (`python/python.exe` on Windows) with `site-packages` beneath it, and, when declared, `node/bin/node` with `node/node_modules` and `pnpm/bin/pnpm.mjs`. A manifest whose platform or architecture differs from the running process is rejected.
|
||||
|
||||
The `sdk` profile enables this tool and the Office skills when `DSH_PRIMARY_RUNTIME` is nonempty. The profile resolves that path from the launch directory and loads sibling `office-skills/` resources. Missing skill resources produce a startup warning and leave Office skills unavailable; invalid or incomplete runtime payloads fail the first tool call. Unset or empty configuration disables both rows. Profile patches can override either row, and changes require restarting the SDK process.
|
||||
|
||||
@@ -60,7 +60,7 @@ A container can copy both directories into an immutable image layer and set `DSH
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
`readPrimaryRuntime` validates the manifest and rejects duplicate normalized distribution names and numpy/pandas versions that disagree with the components table. `workspaceDependencyPaths` derives the platform-specific entries. `installPrimaryRuntime` copies into a staging directory, verifies every declared entry, and swaps it into place while retaining the previous tree on failure; `resolvePrimaryRuntime` verifies the same entries without copying. The tool memoizes the first successful preparation for the plugin's lifetime.
|
||||
`readPrimaryRuntime` and build smoke checks share `parsePrimaryRuntime`. It validates the flat manifest and rejects duplicate normalized distribution names. Legacy `components` metadata is normalized in memory, retaining its consistency checks; a missing legacy distribution map becomes empty. Mixed flat and legacy version fields are rejected. Reads do not rewrite metadata, and equivalent normalized manifests can reuse an installed payload. `workspaceDependencyPaths` derives the platform-specific entries. `installPrimaryRuntime` copies into a staging directory, verifies every declared entry, and swaps it into place while retaining the previous tree on failure; `resolvePrimaryRuntime` verifies the same entries without copying. The tool memoizes the first successful preparation for the plugin's lifetime.
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
|
||||
@@ -42,7 +42,7 @@ kind: "package-reference"
|
||||
|
||||
### 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`。平台或架构与当前进程不符的清单被拒绝。
|
||||
`runtime.json` 记录 `desktopVersion`、`platform`(`win32`、`darwin` 或 `linux`)、`arch`、可选的 `payloadDigest`、顶层 `python` 与可选的 `node`/`pnpm` 版本,以及完整的 `pythonPackages` 分发包版本表。声明 pnpm 时必须同时声明 Node.js。numpy、pandas 等 Python 库只出现在 `pythonPackages` 中。条目位于 `dependencies/`:`python/bin/python3`(Windows 为 `python/python.exe`)及其下的 `site-packages`;声明了才有的 `node/bin/node`、`node/node_modules` 与 `pnpm/bin/pnpm.mjs`。平台或架构与当前进程不符的清单被拒绝。
|
||||
|
||||
`DSH_PRIMARY_RUNTIME` 非空时,`sdk` profile 启用本工具和 Office skills。profile 从启动目录解析该路径,并加载同级 `office-skills/` 资源。缺少 skill 资源会产生启动警告并使 Office skills 不可用;无效或不完整的运行时 payload 会在首次工具调用时失败。未设置或为空时,两行都禁用。profile patch 可以覆盖任意一行,配置变更需要重启 SDK 进程。
|
||||
|
||||
@@ -60,7 +60,7 @@ kind: "package-reference"
|
||||
<details>
|
||||
<summary>实现细节——点击展开</summary>
|
||||
|
||||
`readPrimaryRuntime` 校验清单,拒绝归一化后重名的发行版以及与 components 表不一致的 numpy/pandas 版本。`workspaceDependencyPaths` 推导各平台的条目路径。`installPrimaryRuntime` 复制到暂存目录、校验每个声明的条目、再换入正式位置,失败时保留旧树;`resolvePrimaryRuntime` 做同样的校验但不复制。工具在插件生命周期内记住首次成功的结果。
|
||||
`readPrimaryRuntime` 与构建冒烟检查共用 `parsePrimaryRuntime`。它校验扁平清单,拒绝归一化后重名的分发包。旧 `components` 元数据在内存中归一化,并保留旧格式的一致性校验;旧分发包版本表缺失时转换为空表。混用顶层与旧组件版本字段会被拒绝。读取不改写元数据,归一化后等价的清单可复用已安装产物。`workspaceDependencyPaths` 推导各平台的条目路径。`installPrimaryRuntime` 复制到暂存目录、校验每个声明的条目、再换入正式位置,失败时保留旧树;`resolvePrimaryRuntime` 做同样的校验但不复制。工具在插件生命周期内记住首次成功的结果。
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
|
||||
@@ -29,24 +29,21 @@ export const Config: z<Config> = z.object({
|
||||
root: z.string().min(1),
|
||||
})
|
||||
|
||||
/** Versions recorded by the payload build, independent of user-installed packages. */
|
||||
/** Canonical build metadata, independent of user-installed packages; legacy files are normalized on read. */
|
||||
export interface PrimaryRuntimeManifest {
|
||||
readonly desktopVersion: string
|
||||
readonly platform: string
|
||||
readonly arch: string
|
||||
/** Locked payload identity; absent only in installations made before payload hashing. */
|
||||
readonly payloadDigest?: string
|
||||
/** Installed wheel distribution versions; absent in older release manifests. */
|
||||
readonly pythonPackages?: Readonly<Record<string, string>>
|
||||
readonly components: {
|
||||
readonly python: string
|
||||
/** Absent when the payload ships no Node.js. */
|
||||
readonly node?: string
|
||||
/** Absent when the payload ships no pnpm. */
|
||||
readonly pnpm?: string
|
||||
readonly numpy: string
|
||||
readonly pandas: string
|
||||
}
|
||||
/** Bundled Python interpreter version. */
|
||||
readonly python: string
|
||||
/** Absent when the payload ships no Node.js. */
|
||||
readonly node?: string
|
||||
/** Bundled pnpm version; requires a bundled Node.js executable. */
|
||||
readonly pnpm?: string
|
||||
/** All recorded Python distribution versions; empty for legacy files without a distribution map. */
|
||||
readonly pythonPackages: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
/** Absolute entry points and bundled versions; pnpm runs through the returned Node executable. */
|
||||
@@ -63,43 +60,76 @@ export interface WorkspaceDependencies {
|
||||
const VERSION = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/u
|
||||
const PLATFORMS = ['win32', 'darwin', 'linux']
|
||||
|
||||
function isVersion(value: unknown): boolean {
|
||||
function isVersion(value: unknown): value is string {
|
||||
return typeof value === 'string' && VERSION.test(value)
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function isDistributionMap(value: unknown): value is Readonly<Record<string, string>> {
|
||||
return isRecord(value) && Object.entries(value).every(([name, version]) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)
|
||||
&& typeof version === 'string' && /^\d[\w.!+-]*$/u.test(version))
|
||||
}
|
||||
|
||||
/**
|
||||
* Read build metadata, rejecting duplicate normalized names and conflicting component/distribution versions.
|
||||
* Validate payload JSON and normalize legacy component fields to top-level versions.
|
||||
* @param value - Untrusted decoded runtime.json contents.
|
||||
* @returns Canonical metadata with one Python distribution map and no components field.
|
||||
* @throws For malformed or mixed formats, duplicate distribution names, or inconsistent legacy metadata.
|
||||
*/
|
||||
export function parsePrimaryRuntime(value: unknown): PrimaryRuntimeManifest {
|
||||
if (!isRecord(value)) throw new Error('primary runtime: invalid metadata')
|
||||
const legacy = value.components !== undefined
|
||||
const versions = legacy ? value.components : value
|
||||
if (!isRecord(versions) || (legacy && ['python', 'node', 'pnpm'].some(key => value[key] !== undefined))) {
|
||||
throw new Error('primary runtime: invalid metadata')
|
||||
}
|
||||
const { desktopVersion, platform, arch, payloadDigest } = value
|
||||
const { python, node, pnpm } = versions
|
||||
const pythonPackages = value.pythonPackages === undefined && legacy ? {} : value.pythonPackages
|
||||
if (typeof desktopVersion !== 'string' || desktopVersion.length === 0
|
||||
|| typeof platform !== 'string' || !PLATFORMS.includes(platform)
|
||||
|| typeof arch !== 'string' || !['x64', 'arm64'].includes(arch)
|
||||
|| !isVersion(python)
|
||||
|| (node !== undefined && !isVersion(node))
|
||||
|| (pnpm !== undefined && (!isVersion(pnpm) || node === undefined))
|
||||
|| (payloadDigest !== undefined && (typeof payloadDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(payloadDigest)))
|
||||
|| !isDistributionMap(pythonPackages)) {
|
||||
throw new Error('primary runtime: invalid metadata')
|
||||
}
|
||||
const entries = Object.entries(pythonPackages)
|
||||
const distributions = new Map(entries.map(([name, version]) => [name.toLowerCase().replace(/[-_.]+/gu, '-'), version]))
|
||||
if (distributions.size !== entries.length) throw new Error('primary runtime: invalid metadata')
|
||||
// Only the old on-disk format duplicates these versions. New manifests use the distribution map alone.
|
||||
if (legacy) {
|
||||
for (const name of ['numpy', 'pandas'] as const) {
|
||||
if (!isVersion(versions[name])) throw new Error('primary runtime: invalid metadata')
|
||||
const version = distributions.get(name)
|
||||
if (version !== undefined && version !== versions[name]) {
|
||||
throw new Error(`primary runtime: conflicting ${name} distribution version`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
desktopVersion, platform, arch,
|
||||
...(payloadDigest === undefined ? {} : { payloadDigest }),
|
||||
python,
|
||||
...(node === undefined ? {} : { node }),
|
||||
...(pnpm === undefined ? {} : { pnpm }),
|
||||
pythonPackages,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and normalize build metadata without rewriting the source file.
|
||||
* @param root - Installed or bundled primary runtime directory.
|
||||
* @returns Validated component versions and target identifiers.
|
||||
* @returns Validated top-level versions and target identifiers, including for legacy components manifests.
|
||||
*/
|
||||
export async function readPrimaryRuntime(root: string): Promise<PrimaryRuntimeManifest> {
|
||||
const value: unknown = JSON.parse(await readFile(join(root, 'runtime.json'), 'utf8'))
|
||||
if (typeof value !== 'object' || value === null) throw new Error('primary runtime: invalid metadata')
|
||||
const record = value as Record<string, unknown>
|
||||
const components = record.components as Record<string, unknown> | null | undefined
|
||||
const packages = record.pythonPackages
|
||||
if (typeof record.desktopVersion !== 'string' || record.desktopVersion.length === 0
|
||||
|| !PLATFORMS.includes(String(record.platform)) || !['x64', 'arm64'].includes(String(record.arch))
|
||||
|| typeof components !== 'object' || components === null
|
||||
|| !['python', 'numpy', 'pandas'].every(key => isVersion(components[key]))
|
||||
|| !['node', 'pnpm'].every(key => components[key] === undefined || isVersion(components[key]))
|
||||
|| (record.payloadDigest !== undefined && (typeof record.payloadDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.payloadDigest)))
|
||||
|| (packages !== undefined && (typeof packages !== 'object' || packages === null || Array.isArray(packages)
|
||||
|| !Object.entries(packages).every(([name, version]) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)
|
||||
&& typeof version === 'string' && /^\d[\w.!+-]*$/u.test(version))))) {
|
||||
throw new Error('primary runtime: invalid metadata')
|
||||
}
|
||||
const manifest = value as PrimaryRuntimeManifest
|
||||
const entries = Object.entries(manifest.pythonPackages ?? {})
|
||||
const distributions = new Map(entries.map(([name, version]) => [name.toLowerCase().replace(/[-_.]+/gu, '-'), version]))
|
||||
if (distributions.size !== entries.length) throw new Error('primary runtime: invalid metadata')
|
||||
for (const name of ['numpy', 'pandas'] as const) {
|
||||
const version = distributions.get(name)
|
||||
if (version !== undefined && version !== manifest.components[name]) {
|
||||
throw new Error(`primary runtime: conflicting ${name} distribution version`)
|
||||
}
|
||||
}
|
||||
return manifest
|
||||
return parsePrimaryRuntime(value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,17 +141,17 @@ export async function readPrimaryRuntime(root: string): Promise<PrimaryRuntimeMa
|
||||
export function workspaceDependencyPaths(root: string, manifest: PrimaryRuntimeManifest): WorkspaceDependencies {
|
||||
const dependencies = join(root, 'dependencies')
|
||||
const windows = manifest.platform === 'win32'
|
||||
const node = manifest.components.node === undefined ? {} : {
|
||||
const node = manifest.node === undefined ? {} : {
|
||||
node: join(dependencies, 'node', 'bin', windows ? 'node.exe' : 'node'),
|
||||
nodePackages: join(dependencies, 'node', 'node_modules'),
|
||||
}
|
||||
const pnpm = manifest.components.pnpm === undefined ? {} : { pnpm: join(dependencies, 'pnpm', 'bin', 'pnpm.mjs') }
|
||||
const pnpm = manifest.pnpm === undefined ? {} : { pnpm: join(dependencies, 'pnpm', 'bin', 'pnpm.mjs') }
|
||||
return {
|
||||
python: join(dependencies, 'python', ...(windows ? ['python.exe'] : ['bin', 'python3'])),
|
||||
...node,
|
||||
...pnpm,
|
||||
pythonPackages: join(dependencies, 'python', ...(windows ? ['Lib'] : ['lib', `python${manifest.components.python.split('.').slice(0, 2).join('.')}`]), 'site-packages'),
|
||||
pythonDistributions: manifest.pythonPackages ?? {},
|
||||
pythonPackages: join(dependencies, 'python', ...(windows ? ['Lib'] : ['lib', `python${manifest.python.split('.').slice(0, 2).join('.')}`]), 'site-packages'),
|
||||
pythonDistributions: manifest.pythonPackages,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+72
-17
@@ -12,7 +12,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import * as workspaceDependencies from '../src/index.ts'
|
||||
import { installPrimaryRuntime, readPrimaryRuntime, resolvePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../src/index.ts'
|
||||
import { installPrimaryRuntime, parsePrimaryRuntime, readPrimaryRuntime, resolvePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../src/index.ts'
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof fs>()
|
||||
@@ -32,7 +32,7 @@ async function fixture() {
|
||||
const root = join(directory, 'home', 'dsh-runtimes', 'dsh-primary-runtime')
|
||||
const manifest: PrimaryRuntimeManifest = {
|
||||
desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
|
||||
components: { python: '3.12.14', node: '24.21.0', pnpm: '11.7.0', numpy: '2.3.5', pandas: '3.0.1' },
|
||||
python: '3.12.14', node: '24.21.0', pnpm: '11.7.0',
|
||||
pythonPackages: { 'python-docx': '1.2.0', 'python-pptx': '1.0.2', openpyxl: '3.1.5' },
|
||||
}
|
||||
const paths = workspaceDependencyPaths(source, manifest)
|
||||
@@ -48,7 +48,7 @@ async function fixture() {
|
||||
}
|
||||
|
||||
it.each(['win32', 'darwin', 'linux'])('returns %s interpreter and package paths', (platform) => {
|
||||
const manifest: PrimaryRuntimeManifest = { desktopVersion: '1', platform, arch: 'x64', components: { python: '3.12.14', node: '24.21.0', pnpm: '11.7.0', numpy: '2.3.5', pandas: '3.0.1' } }
|
||||
const manifest: PrimaryRuntimeManifest = { desktopVersion: '1', platform, arch: 'x64', python: '3.12.14', node: '24.21.0', pnpm: '11.7.0', pythonPackages: {} }
|
||||
const paths = workspaceDependencyPaths('/runtime', manifest)
|
||||
expect(paths.pythonDistributions).toEqual({})
|
||||
expect(paths.python).toBe(join('/runtime', 'dependencies', 'python', ...(platform === 'win32' ? ['python.exe'] : ['bin', 'python3'])))
|
||||
@@ -136,18 +136,27 @@ it('rejects malformed metadata and incompatible targets', async () => {
|
||||
const { source, root, manifest } = await fixture()
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, arch: process.arch === 'x64' ? 'arm64' : 'x64' }))
|
||||
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('incompatible')
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, components: { ...manifest.components, python: '../escape' } }))
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, python: '../escape' }))
|
||||
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
|
||||
})
|
||||
|
||||
it.each([['numpy', 'numpy'], ['pandas', 'pandas'], ['Numpy', 'numpy'], ['PANDAS', 'pandas']] as const)('rejects conflicting %s component and distribution versions', async (distribution, name) => {
|
||||
const { source, manifest } = await fixture()
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, pythonPackages: { [distribution]: '0.0.1' } }))
|
||||
await expect(readPrimaryRuntime(source)).rejects.toThrow(`conflicting ${name} distribution version`)
|
||||
const consistent = { ...manifest, pythonPackages: { [distribution]: manifest.components[name] } }
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify(consistent))
|
||||
expect(await readPrimaryRuntime(source)).toEqual(consistent)
|
||||
})
|
||||
/** Previous Desktop files carry interpreter and selected library versions under components. */
|
||||
function legacyManifest(manifest: PrimaryRuntimeManifest) {
|
||||
const { python, node, pnpm, ...metadata } = manifest
|
||||
return { ...metadata, components: { python, node, pnpm, numpy: '2.3.5', pandas: '3.0.1' } }
|
||||
}
|
||||
|
||||
it.each([['numpy', 'numpy'], ['pandas', 'pandas'], ['Numpy', 'numpy'], ['PANDAS', 'pandas']] as const)(
|
||||
'rejects conflicting %s versions in a legacy manifest', async (distribution, name) => {
|
||||
const { source, manifest } = await fixture()
|
||||
const legacy = legacyManifest(manifest)
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...legacy, pythonPackages: { [distribution]: '0.0.1' } }))
|
||||
await expect(readPrimaryRuntime(source)).rejects.toThrow(`conflicting ${name} distribution version`)
|
||||
const pythonPackages = { [distribution]: legacy.components[name] }
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...legacy, pythonPackages }))
|
||||
expect(await readPrimaryRuntime(source)).toEqual({ ...manifest, pythonPackages })
|
||||
},
|
||||
)
|
||||
|
||||
it.each([
|
||||
{ payloadDigest: 'invalid' },
|
||||
@@ -206,7 +215,7 @@ it('uses a payload in place without copying and tolerates a payload without Node
|
||||
const { source, directory } = await fixture()
|
||||
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' },
|
||||
python: '3.12.14',
|
||||
pythonPackages: { 'python-docx': '1.2.0' },
|
||||
}
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
|
||||
@@ -223,7 +232,7 @@ it('uses a payload in place without copying and tolerates a payload without Node
|
||||
|
||||
it('rejects a payload whose Node.js or pnpm version is malformed', async () => {
|
||||
const { source, manifest } = await fixture()
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, components: { ...manifest.components, node: 'latest' } }))
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, node: 'latest' }))
|
||||
await expect(readPrimaryRuntime(source)).rejects.toThrow('invalid metadata')
|
||||
})
|
||||
|
||||
@@ -248,8 +257,9 @@ async function tool(config: workspaceDependencies.Config) {
|
||||
}
|
||||
|
||||
it.each([null, [], { desktopVersion: '' }, { platform: 'unsupported' }, { arch: 'ia32' },
|
||||
{ components: null }, { components: { python: 3 } }, { payloadDigest: 1 },
|
||||
{ pythonPackages: null }, { pythonPackages: { numpy: 3 } },
|
||||
{ python: undefined }, { python: 3 }, { node: 24 }, { pnpm: 'latest' }, { payloadDigest: 1 },
|
||||
{ pythonPackages: undefined }, { pythonPackages: null }, { pythonPackages: 3 }, { pythonPackages: { numpy: 3 } },
|
||||
{ desktopVersion: 1 }, { platform: ['linux'] }, { arch: ['x64'] },
|
||||
])('rejects malformed manifest fields: %j', async (invalid) => {
|
||||
const { source, manifest } = await fixture()
|
||||
const value = invalid === null || Array.isArray(invalid) ? invalid : { ...manifest, ...invalid }
|
||||
@@ -259,7 +269,7 @@ it.each([null, [], { desktopVersion: '' }, { platform: 'unsupported' }, { arch:
|
||||
|
||||
it('reads legacy metadata without a distribution map and rejects incompatible platforms', async () => {
|
||||
const { source, manifest } = await fixture()
|
||||
const { pythonPackages: _packages, ...legacy } = manifest
|
||||
const { pythonPackages: _packages, ...legacy } = legacyManifest(manifest)
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify(legacy))
|
||||
expect((await resolvePrimaryRuntime(source)).pythonDistributions).toEqual({})
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify({ ...manifest, platform: process.platform === 'linux' ? 'darwin' : 'linux' }))
|
||||
@@ -360,3 +370,48 @@ it('validates the previous directory even when a current installation exists', a
|
||||
await expect(installPrimaryRuntime(source, root)).rejects.toThrow('filesystem link')
|
||||
expect(await readFile(join(outside, 'keep'), 'utf8')).toBe('untouched')
|
||||
})
|
||||
|
||||
it('normalizes legacy metadata without rewriting the source or leaking components', async () => {
|
||||
const { source, manifest } = await fixture()
|
||||
const stored = JSON.stringify(legacyManifest(manifest))
|
||||
await writeFile(join(source, 'runtime.json'), stored)
|
||||
expect(await readPrimaryRuntime(source)).toEqual(manifest)
|
||||
expect(await readFile(join(source, 'runtime.json'), 'utf8')).toBe(stored)
|
||||
expect(await resolvePrimaryRuntime(source)).toEqual(workspaceDependencyPaths(source, manifest))
|
||||
})
|
||||
|
||||
it('reuses an equivalent legacy installation without replacing user-added packages', async () => {
|
||||
const { source, root, manifest } = await fixture()
|
||||
const stored = JSON.stringify(legacyManifest(manifest))
|
||||
await writeFile(join(source, 'runtime.json'), stored)
|
||||
const installed = await installPrimaryRuntime(source, root)
|
||||
await writeFile(join(installed.pythonPackages, 'user-package.py'), 'keep')
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify(manifest))
|
||||
expect(await installPrimaryRuntime(source, root)).toEqual(installed)
|
||||
expect(await readFile(join(root, 'runtime.json'), 'utf8')).toBe(stored)
|
||||
expect(await readFile(join(installed.pythonPackages, 'user-package.py'), 'utf8')).toBe('keep')
|
||||
})
|
||||
|
||||
it('accepts flat Python libraries without requiring or singling out numpy and pandas', async () => {
|
||||
const { source, manifest } = await fixture()
|
||||
const flat = { ...manifest, pythonPackages: { 'custom-library': '1.0.0' } }
|
||||
await writeFile(join(source, 'runtime.json'), JSON.stringify(flat))
|
||||
expect(await readPrimaryRuntime(source)).toEqual(flat)
|
||||
expect((await resolvePrimaryRuntime(source)).pythonDistributions).toEqual(flat.pythonPackages)
|
||||
})
|
||||
|
||||
it.each(['python', 'node', 'pnpm'])('rejects mixed legacy and flat %s fields', async (name) => {
|
||||
const { manifest } = await fixture()
|
||||
expect(() => parsePrimaryRuntime({ ...legacyManifest(manifest), [name]: '1.0.0' })).toThrow('invalid metadata')
|
||||
})
|
||||
|
||||
it.each([null, [], 'invalid', { python: '3.12.14' }])('rejects malformed legacy components: %j', async (components) => {
|
||||
const { manifest } = await fixture()
|
||||
expect(() => parsePrimaryRuntime({ ...legacyManifest(manifest), components })).toThrow('invalid metadata')
|
||||
})
|
||||
|
||||
it.each(['flat', 'legacy'])('rejects pnpm without a Node.js interpreter in %s metadata', async (format) => {
|
||||
const { manifest } = await fixture()
|
||||
const { node: _node, ...input } = manifest
|
||||
expect(() => parsePrimaryRuntime(format === 'legacy' ? legacyManifest(input) : input)).toThrow('invalid metadata')
|
||||
})
|
||||
@@ -44,7 +44,8 @@ it('invalidates payload identity for shared wheels, package versions and package
|
||||
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 }))
|
||||
await writeFile(join(root, 'runtime.json'), JSON.stringify({ desktopVersion: '1.0.0', platform: process.platform, arch: process.arch,
|
||||
components: { python: '3.12.14', numpy: '2.3.5', pandas: '3.0.1' } }))
|
||||
expect(() => { smokePrimaryRuntime(root) }).toThrow('missing Python distribution versions; prepare the payload')
|
||||
} finally { await rm(root, { recursive: true, force: true }) }
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ 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 { parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../packages/skill/tool-workspace-dependencies/src/index.ts'
|
||||
import lock from './lock.json' with { type: 'json' }
|
||||
|
||||
/**
|
||||
@@ -54,7 +54,7 @@ export function primaryRuntimePayloadDigest(
|
||||
// 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,
|
||||
format: 4, target, pythonVersion, pythonRelease, nodeVersion: pnpmVersion === undefined ? undefined : nodeVersion,
|
||||
artifact: runtimeLock.targets[target], wheels, pythonPackages, pnpm: pnpmVersion,
|
||||
})).digest('hex')
|
||||
}
|
||||
@@ -147,12 +147,9 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
|
||||
platform: target === 'win-x64' ? 'win32' : target === 'linux-x64' ? 'linux' : 'darwin',
|
||||
arch: target === 'mac-arm64' ? 'arm64' : 'x64',
|
||||
payloadDigest: primaryRuntimePayloadDigest(target, lock, pnpmVersion),
|
||||
python: lock.pythonVersion,
|
||||
...(pnpmVersion === undefined ? {} : { node: lock.nodeVersion, pnpm: 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]) {
|
||||
@@ -178,16 +175,16 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
|
||||
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
|
||||
const manifest = parsePrimaryRuntime(JSON.parse(readFileSync(join(root, 'runtime.json'), 'utf8')))
|
||||
if (manifest.platform !== process.platform || manifest.arch !== process.arch) return
|
||||
if (manifest.pythonPackages === undefined) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
|
||||
if (Object.keys(manifest.pythonPackages).length === 0) throw new Error('primary runtime: missing Python distribution versions; prepare the payload before running its smoke checks.')
|
||||
const entries = workspaceDependencyPaths(root, manifest)
|
||||
const options = { stdio: 'inherit', timeout: 120_000, env: 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)
|
||||
manifest.python, join(dirname(root), 'office-skills', 'scripts', 'check_office.py')], options)
|
||||
execFileSync(entries.python, ['-I', '-B', '-m', 'pip', 'check'], options)
|
||||
if (entries.node !== undefined) execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(manifest.components.node)}) process.exit(1)`], options)
|
||||
if (entries.node !== undefined) execFileSync(entries.node, ['-e', `if (process.versions.node !== ${JSON.stringify(manifest.node)}) process.exit(1)`], options)
|
||||
if (entries.pnpm !== undefined && entries.node !== undefined) execFileSync(entries.node, [entries.pnpm, '--version'], options)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user