Merge pull request #5812 from deepseek-harness/worktree/local-bundle-registry

feat(scripts): serve locally built Official bundles from a loopback registry
This commit is contained in:
Tianyi Cui authored and GitHub committed 2026-10-07 15:28:19 +08:00
commit a06d137463
37 files changed
+2357 -372

No files matched your search

@@ -21,8 +21,8 @@
en: c35ea4b858448b49
zh: 1f8ff0a9f82ca881
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts:
en: 6356c325e36dc2a8
zh: 63da32299cb356c9
en: d3bbd24ead02af4f
zh: 64623d88bee4314b
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/python-sdk-distribution-two-carriers-exe-for-production-node-for-development:
en: 9bd60549d4a52ec0
zh: e2343b7210212ee0
@@ -48,7 +48,7 @@ The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed de
### Build pipeline and artifacts
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its target-specific Office directory into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its target-specific Office directory into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v<repository-version>` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64.
@@ -48,7 +48,7 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
### 构建流水线与产物
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与对应目标的 Office 目录一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与对应目标的 Office 目录一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。
@@ -232,7 +232,8 @@ jobs:
esac
addon_dir="$(realpath packages/subprocess/subprocess-local/node_modules/node-pty)"
pnpm_setup_root="$(realpath "$(dirname "$(dirname "$PNPM_HOME")")")"
(cd "$addon_dir" && npm_config_build_from_source=true pnpm run install)
# Verification installs would install node-gyp inside the package and package it into the exe.
(cd "$addon_dir" && npm_config_build_from_source=true pnpm --config.verifyDepsBeforeRun=false run install)
addon="$addon_dir/build/Release/pty.node"
[ -f "$addon_dir/build/Makefile" ] || {
echo "::error::node-pty install did not generate $addon_dir/build/Makefile"
@@ -1,13 +1,27 @@
/** Observe ordinary manager operations in an installed Web profile without starting native agents. */
import { existsSync } from 'node:fs'
import { readFile, readdir, stat } from 'node:fs/promises'
import { basename, join } from 'node:path'
export const name = 'official-installer-observer'
export const inject = ['pluginManager', 'agentPresets', 'subagents', 'sessions', 'appReady']
let lastStage = 'none'
/** Report one stage on the child's stdout, which the test forwards when it fails. */
function note(stage) {
lastStage = stage
process.stdout.write(`PACKED_OFFICIAL_STEP=${stage} ${String(Date.now())}\n`)
}
// A run that never finishes must name its stage in the parent's log even when the child is killed at its deadline.
setInterval(() => { note(`stalled-at ${lastStage}`) }, 30_000).unref()
export function apply(ctx, config) {
let active = true
note('applied')
const stop = ctx.appReady.onReady(() => {
note('ready')
void exercise(ctx, config).then(
value => { if (active) process.stdout.write(`PACKED_OFFICIAL_RESULT=${JSON.stringify({ value })}\n`) },
error => { if (active) process.stdout.write(`PACKED_OFFICIAL_RESULT=${JSON.stringify({ error: String(error) })}\n`) },
@@ -20,20 +34,59 @@ function applied(stage, result) {
if (result.application !== 'applied') throw new Error(`${stage}: ${JSON.stringify(result)}`)
}
/**
* Run one operation under a bound, so a stalled manager operation reports what it waits for instead of
* holding the whole child until its timeout. pnpm 11.7.0 can print its result and never exit
* (pnpm#12297, fixed in 11.23.0); the run record names the process that stayed alive.
* @param label - the operation being run.
* @param operation - the manager call to await.
* @param boundMs - the time the operation may take.
* @param profile - the isolated profile directory the operation writes.
* @returns The operation's own result.
*/
async function bounded(label, operation, boundMs, profile) {
let timer
const expiry = new Promise((_resolve, reject) => {
timer = setTimeout(() => { reject(new Error(`${label} did not settle within ${String(boundMs)}ms`)) }, boundMs)
})
try {
return await Promise.race([operation(), expiry])
} catch (error) {
if (!String(error).includes('did not settle')) throw error
const locks = await readdir(profile).then(files => files.filter(file => file.endsWith('.lock')).join(',') || 'none', () => 'none')
const record = await readFile(join(profile, '.plugin-manager', 'run.json'), 'utf8')
.then(text => JSON.parse(text), () => undefined)
const pid = record?.pid
const alive = pid === undefined ? false : (() => { try { process.kill(pid, 0); return true } catch { return false } })()
const state = alive && existsSync(`/proc/${String(pid)}/status`)
? await readFile(`/proc/${String(pid)}/status`, 'utf8').then(text => text.split('\n').filter(line => /^(State|Threads):/u.test(line)).join(' '), () => '')
: ''
const cmdline = alive && existsSync(`/proc/${String(pid)}/cmdline`)
? await readFile(`/proc/${String(pid)}/cmdline`, 'utf8').then(text => text.replaceAll('\0', ' ').trim().slice(0, 120), () => '')
: ''
throw new Error(`${String(error)}\nprofile-locks=${locks}\npnpm-run pid=${String(pid)} alive=${String(alive)} ${state} ${cmdline}`)
} finally { clearTimeout(timer) }
}
async function exercise(ctx, config) {
note('list-initial')
const initial = await ctx.pluginManager.listBundles()
await fetch(`${config.registry}catalog-observed`)
note('listed-initial')
const steps = []
for (const name of config.packages) {
const spec = initial.find(bundle => bundle.name === name)?.installTarget?.spec
if (spec === undefined) throw new Error(`No catalog installation target for ${name}`)
const options = { enabled: true, saveExact: true, registry: config.registry }
note(`install ${name}`)
let install = await ctx.pluginManager.installBundle(spec, options)
const approval = install.pendingBuilds
if (approval?.length) install = await ctx.pluginManager.installBundle(spec, { ...options, approvedBuilds: approval })
note(`installed ${name}`)
applied(`install ${name}`, install)
const manifest = JSON.parse(await readFile(join(config.profile, 'package.json'), 'utf8'))
const installed = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name)
// A transitive repository package the registry served, absent from npm at this version.
const localDependency = JSON.parse(await readFile(join(config.profile, 'node_modules', config.localDependency, 'package.json'), 'utf8')).version
const provider = name.endsWith('-codex') ? 'codex' : 'claude-code'
const providerEnabled = ctx.subagents.list().includes(provider)
const nativeDir = join(config.profile, 'node_modules', config.nativePackages[name])
@@ -42,7 +95,9 @@ async function exercise(ctx, config) {
const nativeArtifact = nativeBinary !== undefined && (await stat(join(nativeDir, nativeBinary))).size > 0
const presets = await ctx.agentPresets.list()
const beforeUnavailable = await readFile(join(config.profile, 'package.json'), 'utf8')
note(`unavailable ${name}`)
const unavailable = await ctx.pluginManager.installBundle(`${name}@9999.0.0`, options)
note(`unavailable-done ${name}`)
const unchangedAfterUnavailable = beforeUnavailable === await readFile(join(config.profile, 'package.json'), 'utf8')
const off = await ctx.pluginManager.setBundleEnabled(name, false)
applied(`disable ${name}`, off)
@@ -50,11 +105,13 @@ async function exercise(ctx, config) {
const on = await ctx.pluginManager.setBundleEnabled(name, true)
applied(`enable ${name}`, on)
const enabled = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name)
const removed = await ctx.pluginManager.removeBundle(name)
note(`remove ${name}`)
const removed = await bounded(`remove ${name}`, () => ctx.pluginManager.removeBundle(name), 90_000, config.profile)
note(`removed ${name}`)
applied(`remove ${name}`, removed)
const absent = (await ctx.pluginManager.listBundles()).find(bundle => bundle.name === name)
steps.push({ name, install, savedVersion: manifest.dependencies?.[name], installed, providerEnabled, nativeArtifact, presets,
unavailable, unchangedAfterUnavailable, off, disabled, on, enabled, removed, absent })
steps.push({ name, install, savedVersion: manifest.dependencies?.[name], installed, localDependency, providerEnabled, nativeArtifact,
presets, unavailable, unchangedAfterUnavailable, off, disabled, on, enabled, removed, absent })
}
return { initial, steps, sessionCount: ctx.sessions.list().length }
}
@@ -1,8 +1,6 @@
/** Install actual packed Official providers through the ordinary manager in a source-independent Web process. */
import { createHash, randomUUID } from 'node:crypto'
import { createReadStream, existsSync, globSync } from 'node:fs'
import { existsSync, globSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { join } from 'node:path'
import { createInterface } from 'node:readline'
import { fileURLToPath } from 'node:url'
@@ -10,7 +8,21 @@ import { execa } from 'execa'
import yaml from 'js-yaml'
import { expect, it, onTestFinished } from 'vitest'
import type { BundleInfo, ChangeResult } from '@deepseek-ai/dsh-plugin-manager'
import { packedManifest } from '../../../../scripts/release/tarball.ts'
import { getDshRuntimeVersion } from '../../../../packages/boot/app-boot/src/plugin-compatibility.ts'
import { generateOfficialBundleCatalog } from '../../../../scripts/gen-official-bundle-catalog.ts'
import {
REGISTRY_MANIFEST_FILE,
createRegistryManifest,
hostPlatform,
packBundleArtifacts,
readReleaseMembers,
readWorkspacePackages,
selectBundleDependencies,
sourceEvidence,
startBundleRegistry,
verifyBuiltBundles,
writeRegistryManifest,
} from '../../../../scripts/local-bundle-registry.ts'
import { assertInstalledTree, createPackedInstallation } from './packed-installation.ts'
const root = fileURLToPath(new URL('../../../..', import.meta.url))
@@ -28,6 +40,7 @@ interface Observation {
installed: BundleInfo
providerEnabled: boolean
nativeArtifact: boolean
localDependency: string
presets: Array<{ id: string; broken?: string }>
unavailable: ChangeResult
unchangedAfterUnavailable: boolean
@@ -40,111 +53,65 @@ interface Observation {
}>
}
it.skipIf(!built)('installs exact Official versions and switches/removes them through the generic package manager', {
// Real tarball packing and isolated dependency installation own this budget; no native model request runs.
timeout: 360_000,
/**
* Read the tail of the newest plugin-manager pnpm log, for a failure the child cannot report itself.
* @param profile - isolated profile directory.
* @returns The last log lines, or a marker when no run logged anything.
*/
function pnpmLogTail(profile: string): string {
const logs = join(profile, '.plugin-manager', 'logs')
const newest = existsSync(logs)
? readdirSync(logs, { withFileTypes: true }).filter(entry => entry.isDirectory())
.map(entry => join(logs, entry.name, 'pnpm.log'))
.filter(path => existsSync(path))
.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs)[0]
: undefined
if (newest === undefined) return '--- plugin-manager log: none ---'
return `--- ${newest} ---\n${readFileSync(newest, 'utf8').split('\n').slice(-40).join('\n')}`
}
it.skipIf(!built)('installs the exported registry\'s local closure and switches Official bundles through the generic manager', {
// Real packing of two dependency closures, an isolated product installation, and an isolated plugin installation
// own this budget; no native model request runs.
timeout: 900_000,
retry: 0,
}, async () => {
const fixture = await createPackedInstallation('dsh-official-packed-', packages)
const { temporary, installation, home, profile, environment, tarballs, members, externalVersions } = fixture
const manifests = new Map(packages.map((name) => {
const archive = tarballs.get(name)
if (archive === undefined) throw new Error(`missing provider archive ${name}`)
return [name, { archive, manifest: packedManifest(archive) }]
// The product under test is a real source-independent installation that ships neither provider, so both stay
// available to the Official catalog and only the local registry can supply them.
const fixture = await createPackedInstallation('dsh-official-packed-', [])
const { temporary, installation, home, profile, environment } = fixture
// The registry packs and serves the catalog's own local dependency closure; no checkout link or file: override
// stands in for a package the registry should answer.
const entries = generateOfficialBundleCatalog(root).map(entry => entry.packageName)
const workspace = readWorkspacePackages(root)
verifyBuiltBundles({ entries, packages: workspace })
const selection = selectBundleDependencies({ entries, packages: workspace, platform: hostPlatform() })
const runDirectory = join(temporary, 'registry')
const artifacts = await packBundleArtifacts({
root, destination: runDirectory, members: selection.members, release: readReleaseMembers(root),
})
const version = artifacts.find(artifact => artifact.name === packages[0])?.version
if (version === undefined) throw new Error('the local registry packed no claude-code provider')
for (const name of packages) expect(artifacts.find(artifact => artifact.name === name), name).toMatchObject({ version })
await writeRegistryManifest(join(runDirectory, REGISTRY_MANIFEST_FILE), createRegistryManifest({
buildId: 'packed-e2e',
source: sourceEvidence(root, getDshRuntimeVersion()),
entries,
workspaceNames: selection.workspaceNames,
artifacts,
}))
const version = manifests.get(packages[0]!)?.manifest.version
if (typeof version !== 'string') throw new Error('packed provider has no version')
const integrity = new Map(await Promise.all([...manifests].map(async ([name, entry]) =>
[name, `sha512-${createHash('sha512').update(await readFile(entry.archive)).digest('base64')}`] as const)))
const requests: string[] = []
let requestsBeforeCatalog: string[] | undefined
let registry = ''
const namespace = `/${randomUUID()}/`
const server = createServer((request, response) => {
const pathname = new URL(request.url ?? '/', 'http://registry.invalid').pathname
if (!pathname.startsWith(namespace)) { response.writeHead(404).end(); return }
const path = decodeURIComponent(pathname.slice(namespace.length))
if (path === 'catalog-observed') {
requestsBeforeCatalog = [...requests]
response.end('ok')
return
}
requests.push(path)
const archive = [...manifests].find(([name]) => path === `tarballs/${name}`)?.[1].archive
if (archive !== undefined) {
response.setHeader('content-type', 'application/octet-stream')
createReadStream(archive).on('error', error => response.destroy(error)).pipe(response)
return
}
const entry = manifests.get(path)
if (entry === undefined) {
// Only package publication is local: third-party metadata and payloads retain npm's real bytes.
response.writeHead(302, { location: `https://registry.npmjs.org/${request.url?.slice(namespace.length) ?? ''}` }).end()
return
}
response.setHeader('content-type', 'application/json')
response.end(JSON.stringify({ name: path, 'dist-tags': { latest: version }, time: { [version]: '2000-01-01T00:00:00.000Z' },
versions: { [version]: { ...entry.manifest, dist: { tarball: `${registry}tarballs/${encodeURIComponent(path)}`, integrity: integrity.get(path) } } },
}))
})
onTestFinished(async () => {
server.closeAllConnections()
if (!server.listening) return
await new Promise<void>((resolveClose, reject) => {
server.close((error) => { if (error) reject(error); else resolveClose() })
})
})
await new Promise<void>((resolveListen, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => { server.off('error', reject); resolveListen() })
const registry = await startBundleRegistry({
directory: runDirectory, artifacts, workspaceNames: selection.workspaceNames,
})
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('test registry has no TCP address')
registry = `http://127.0.0.1:${String(address.port)}${namespace}`
onTestFinished(() => registry.close())
const dependencies = new Set(packages)
for (const name of dependencies) {
const member = members.get(name)
if (member === undefined) throw new Error(`missing packed dependency ${name}`)
for (const [dependency, range] of Object.entries(member.manifest.dependencies as Record<string, string> | undefined ?? {})) {
if (range.startsWith('workspace:')) dependencies.add(dependency)
}
}
for (const name of packages) {
dependencies.delete(name)
expect(existsSync(join(installation, 'node_modules', name))).toBe(false)
}
await writeFile(join(profile, 'package.json'), JSON.stringify({ name: 'packed-official-profile', private: true, dependencies: {},
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'] } },
}))
const lock = yaml.load(await readFile(join(root, 'pnpm-lock.yaml'), 'utf8')) as {
packages: Record<string, { os?: string[]; cpu?: string[]; libc?: string[] }>
snapshots: Record<string, { optionalDependencies?: Record<string, string> }>
}
const report = process.report.getReport() as { header: { glibcVersionRuntime?: string } }
const libc = process.platform === 'linux' ? report.header.glibcVersionRuntime === undefined ? 'musl' : 'glibc' : undefined
// The fixture owns one host installation; foreign native artifacts are never executed or required here.
const foreignPackages = new Set(Object.entries(lock.packages).filter(([, constraints]) =>
([['os', process.platform], ['cpu', process.arch], ['libc', libc]] as const).some(([field, current]) => {
const allowed = constraints[field]
return current !== undefined && allowed !== undefined && !allowed.includes(current)
&& !allowed.includes('any') && allowed.every(value => !value.startsWith('!'))
})).map(([name]) => name))
const ignoredOptionalDependencies = [...new Set(Object.values(lock.snapshots).flatMap(snapshot =>
Object.entries(snapshot.optionalDependencies ?? {})
.filter(([name, version]) => foreignPackages.has(`${name}@${version}`) || foreignPackages.has(version)).map(([name]) => name)))]
const nativePackages = {
'@deepseek-ai/dsh-subagent-claude-code': `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}${libc === 'musl' ? '-musl' : ''}`,
'@deepseek-ai/dsh-subagent-codex': `@openai/codex-${process.platform}-${process.arch}`,
}
for (const name of Object.values(nativePackages)) expect(ignoredOptionalDependencies).not.toContain(name)
await writeFile(join(profile, 'pnpm-workspace.yaml'), yaml.dump({ packages: ['.'], nodeLinker: 'hoisted', autoInstallPeers: false,
ignoredOptionalDependencies,
overrides: { ...externalVersions, ...Object.fromEntries([...dependencies].map(name => [name, `file:${tarballs.get(name)!}`])) },
}))
const externalScopes = new Set(Object.keys(externalVersions)
.filter(name => name.startsWith('@') && !name.startsWith('@deepseek-ai/')).map(name => name.slice(0, name.indexOf('/'))))
await writeFile(join(profile, '.npmrc'), [...externalScopes].map(scope => `${scope}:registry=https://registry.npmjs.org/`).join('\n') + '\n')
// Only the settings a real profile gets. No override, ignore list, or scoped .npmrc may route a package around the
// registry, so every dependency of the packed bundles is answered by this run or by its fixed upstream redirect.
await writeFile(join(profile, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
const userConfig = join(temporary, 'empty.npmrc')
await writeFile(userConfig, '')
environment.npm_config_userconfig = userConfig
@@ -152,7 +119,17 @@ it.skipIf(!built)('installs exact Official versions and switches/removes them th
await writeFile(observer, await readFile(new URL('./fixtures/official-installer-observer.mjs', import.meta.url), 'utf8'))
const patch = join(temporary, 'official-installer.patch.yml')
await writeFile(patch, yaml.dump([{ insert: [{ id: 'official-installer-observer', name: observer,
config: { registry, profile, packages, nativePackages },
config: {
registry: registry.url,
profile,
packages,
localDependency: '@deepseek-ai/dsh-brand',
nativePackages: {
// The Agent SDK publishes a musl variant of its Linux artifact, which is what pnpm installs there.
'@deepseek-ai/dsh-subagent-claude-code': `@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}${hostPlatform().libc?.[0] === 'musl' ? '-musl' : ''}`,
'@deepseek-ai/dsh-subagent-codex': `@openai/codex-${process.platform}-${process.arch}`,
},
},
}] }]))
const child = execa(process.execPath, [join(installation, 'node_modules/@deepseek-ai/dsh/lib/bin.js'),
'web', '--patch', patch, '--no-open', '--port', '0'], {
@@ -160,6 +137,9 @@ it.skipIf(!built)('installs exact Official versions and switches/removes them th
})
if (child.stdout === null) throw new Error('installed Web process has no stdout')
const lines = createInterface({ input: child.stdout })
// A failure in the body must stay the reported one: teardown asserts only when the body
// passed, so a Web process that does not exit cannot replace the real error.
let failure: unknown
try {
const observed = await new Promise<Observation>((resolveObservation, reject) => {
lines.on('line', (line) => {
@@ -170,10 +150,10 @@ it.skipIf(!built)('installs exact Official versions and switches/removes them th
})
void child.then((result) => { reject(new Error(`Web process exited before observation:\n${result.stdout}\n${result.stderr}`)) })
})
expect(requestsBeforeCatalog).toEqual([])
for (const name of packages) expect(observed.initial.find(entry => entry.name === name)).toMatchObject({
official: true, availability: 'missing', installed: false, enabled: false, installTarget: { spec: `${name}@${version}`, version },
})
expect(observed.steps).toHaveLength(packages.length)
for (const step of observed.steps) {
expect(step.install, JSON.stringify(step.install)).toMatchObject({ application: 'applied', enabled: true, version })
expect(step.savedVersion).toBe(version)
@@ -182,6 +162,8 @@ it.skipIf(!built)('installs exact Official versions and switches/removes them th
.toEqual(['preset-cordis', 'preset-ptc', 'preset-standard'])
expect(step.providerEnabled).toBe(true)
expect(step.nativeArtifact).toBe(true)
// A transitive repository package the closure needs can only have come from this run's artifacts.
expect(step.localDependency).toBe(version)
expect(step.presets.every(preset => preset.broken === undefined)).toBe(true)
expect(step.unavailable).toMatchObject({ application: 'failed', stage: 'install' })
expect(step.unchangedAfterUnavailable).toBe(true)
@@ -192,15 +174,26 @@ it.skipIf(!built)('installs exact Official versions and switches/removes them th
expect(step.removed, JSON.stringify(step.removed)).toMatchObject({ application: 'applied' })
expect(step.absent).toMatchObject({ official: true, availability: 'missing', enabled: false, installed: false })
}
expect(observed.steps).toHaveLength(packages.length)
expect(observed.sessionCount).toBe(0)
expect(globSync('sessions/**/*.jsonl*', { cwd: home })).toEqual([])
await assertInstalledTree(profile)
// The completed run records every package it served, so a failure can be matched to the exact checkout.
const record = JSON.parse(await readFile(join(runDirectory, REGISTRY_MANIFEST_FILE), 'utf8')) as { buildId: string; packages: unknown[] }
expect(record.buildId).toBe('packed-e2e')
expect(record.packages).toHaveLength(selection.members.length)
expect(record.packages).toEqual(expect.arrayContaining([expect.objectContaining({ name: packages[0], version })]))
} catch (error) {
// The manager's own pnpm output names a stalled or refused operation, which the child's stdout cannot show.
failure = error instanceof Error ? new Error(`${error.message}\n\n${pnpmLogTail(profile)}`) : error
} finally {
lines.close()
child.kill('SIGTERM')
const result = await child
expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false)
expect(result.exitCode === 0 || result.signal === 'SIGTERM', result.stderr).toBe(true)
if (failure === undefined) {
// The installed Web process stops on SIGTERM within its own budget; a timed-out child is a teardown defect.
expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false)
expect(result.exitCode === 0 || result.signal === 'SIGTERM', result.stderr).toBe(true)
}
}
if (failure !== undefined) throw failure
})
+3
View File
@@ -47,6 +47,9 @@
/deepseek-harness-desktop/package/local-macos-dmg-without-release-signing:
en: 57869a5e3885f843
zh: 7cfc01b2dcbebd78
/deepseek-harness-desktop/package/local-official-bundles-from-a-loopback-registry:
en: 5f05072ec82cc62d
zh: a3d63b58d1ef2db7
/deepseek-harness-desktop/package/runtime-file-selection:
en: e7c1eb8e78fe46e3
zh: b7ea497dd75be524
+16
View File
@@ -250,6 +250,22 @@ Add `--check` to validate local configuration and tools without building, or `--
These commands ignore `.env.macos` and inherited release settings, use `com.deepseek.harness` as the app ID, and omit automatic-update and mandatory-update configuration. Build tools, standard network proxies, and `DSH_DESKTOP_NPM_REGISTRY` remain available from the shell. They skip Developer ID signing, Apple notarization, temporary signing keychains, and notarization proxy changes. Electron receives a local ad-hoc signature without certificates so the modified executable can run; the DMG is unsigned. Both preparation and the assembled application still run runtime integrity and smoke checks. Local builds omit update feeds, ZIPs, and release completion records, and the upload commands cannot publish them. macOS may require approval in System Settings → Privacy & Security to open an unnotarized application.
### Local Official bundles from a loopback registry
The test DMG excludes the provider runtimes the Official on-demand catalog advertises, and a release publishes matching bundle versions to npm. To install locally built bundles before that release, start a loopback registry that packs and serves the catalog's complete local dependency closure:
```sh
pnpm run dev:bundle-registry
```
The command validates the catalog and the workspace inventory, runs `pnpm run build:official`, packs the closure into a new `dist/test-bundles/<run-id>/` directory, writes `bundle-registry.json` last as the completion marker, and only then prints the artifact path, the registry URL, and the exact `name@version` specs. It serves `127.0.0.1` on an allocated port under a fresh namespace until Ctrl+C or SIGTERM, and stopping it leaves both the artifacts and the installed packages on disk. It needs no `.env.macos`, Apple credentials, or npm credentials, and it does not change `pnpm run package:desktop:unsigned`.
In the packaged application, open Plugins → Add plugin, paste one printed spec, choose **Custom address**, paste the printed registry URL, and install. A custom registry is asked alone, so this process must answer every dependency: repository-owned packages come from the run's own archives, and third-party packages keep npm's real metadata and bytes through a fixed redirect to `registry.npmjs.org`, which still requires network access. The installed entry stays Official because its package name is in the catalog. The registry choice is remembered in this browser; choose an offered registry again to switch back.
A run's artifacts are immutable, so a rebuild serves a new namespace and a new URL, and the Official **Update** control does not appear while the installed and running versions match. The profile records the previous run's tarball URL in `pnpm-lock.yaml`, and pnpm verifies recorded entries, so installing or reinstalling that bundle against a new URL is refused with `ERR_PNPM_TARBALL_URL_MISMATCH` and can leave the earlier files in place. To test changed code at the same version, clear the stale resolution first — `pnpm clean --lockfile` in `$DSH_HOME/profiles/<profile>`, or install into a fresh profile — then install again from the new URL. `bundle-registry.json` records each archive's version, filename, size, and SHA-512 integrity together with the packing checkout's commit and dirty flag.
The served `time` is the real packing time, so pnpm 11's default 24-hour `minimumReleaseAge` records the exemption it granted in the profile's `pnpm-workspace.yaml` (`minimumReleaseAgeExclude`); an explicitly stricter policy there fails the installation with pnpm's own message instead of being overridden. A `@scope:registry` route in `~/.npmrc` or the profile's `.npmrc` overrides `--registry` for that scope, so it must not route the served scope.
### Runtime file selection
Desktop packs workspace packages locally and installs external dependencies through the target's bundled Node and pnpm. [Desktop's file policy](scripts/runtime-file-policy.ts) then filters the immutable `resources/app.asar/dsh/node_modules` copy before signing and integrity sealing. It omits TypeScript declarations, recognized JavaScript/CSS/TypeScript source maps, TypeScript build caches, Domino's test directory, selected native compiler outputs, and node-pty prebuilds for other platforms. It preserves runtime JavaScript, native modules and their DLL/EXE helpers, WASM, unknown assets, licenses, and notices. Dependency manifests pass through electron-builder's metadata cleanup before integrity sealing, so archiving preserves their recorded bytes. The policy does not alter npm tarballs, the bundled package manager, or user-installed plugin files.
+16
View File
@@ -252,6 +252,22 @@ pnpm run package:desktop:mac:x64:unsigned
这些命令忽略 `.env.macos` 和继承的发布配置,使用 `com.deepseek.harness` 作为应用 ID,并省略自动更新和强制更新配置。构建工具、标准网络代理与 `DSH_DESKTOP_NPM_REGISTRY` 仍可从 shell 提供。它们跳过 Developer ID 签名、Apple 公证、临时签名钥匙串和公证代理修改。Electron 使用无需证书的本地 ad-hoc 签名,以便修改后的可执行文件能够运行;DMG 不签名。准备阶段和组装后的应用仍执行运行时完整性与冒烟检查。本地构建不生成更新源、ZIP 或发布完成记录,上传命令无法发布这些产物。macOS 可能要求在“系统设置 → 隐私与安全性”中批准打开未经公证的应用。
### 用回环 registry 安装本地 Official 包
测试 DMG 不包含 Official 按需目录所宣传的 provider 运行时,而正式发布才会把对应版本的 bundle 发到 npm。要在发布前安装本地构建的 bundle,请启动一个回环 registry,它会打包并提供该目录的完整本地依赖闭包:
```sh
pnpm run dev:bundle-registry
```
该命令先校验目录与 workspace 清单,运行 `pnpm run build:official`,把闭包打包进新的 `dist/test-bundles/<run-id>/` 目录,最后写入 `bundle-registry.json` 作为完成标记,然后才打印产物路径、registry URL 和精确的 `name@version` 规格。它在分配到的端口上只监听 `127.0.0.1`,使用一次性的命名空间提供服务,直到 Ctrl+C 或 SIGTERM;停止后产物和已安装的包都保留在磁盘上。它不需要 `.env.macos`、Apple 凭据或 npm 凭据,也不改变 `pnpm run package:desktop:unsigned`。
在打包应用中打开“插件 → 添加插件”,粘贴一条打印出的规格,选择**自定义地址**,粘贴打印出的 registry URL 后安装。自定义 registry 会被单独询问,因此必须由本进程回答每一个依赖:仓库自有的包取自本次运行的产物,第三方包则通过固定重定向到 `registry.npmjs.org` 保留 npm 的真实元数据与字节,因此仍需要网络访问。安装后的条目仍归类为 Official,因为它的包名在目录中。registry 选择会在此浏览器中记住;再次选择某个提供的 registry 即可切回。
一次运行的产物是不可变的,所以重新构建会提供新的命名空间和新 URL;当已安装版本与运行版本一致时,Official 的**更新**控件不会出现。profile 会在 `pnpm-lock.yaml` 中记录上一次运行的 tarball URL,而 pnpm 会校验已记录的条目,因此用新 URL 再次安装或重装该 bundle 会被 `ERR_PNPM_TARBALL_URL_MISMATCH` 拒绝,并且可能继续保留旧文件。要在同一版本上验证改动后的代码,请先清除过期的解析结果——在 `$DSH_HOME/profiles/<profile>` 中运行 `pnpm clean --lockfile`,或改用全新的 profile——然后再从新 URL 安装。`bundle-registry.json` 记录每个归档的版本、文件名、大小和 SHA-512 完整性,以及打包所用 checkout 的 commit 和 dirty 标记。
所提供的 `time` 是真实的打包时间,因此 pnpm 11 默认的 24 小时 `minimumReleaseAge` 会在 profile 的 `pnpm-workspace.yaml` 中记录它授予的豁免(`minimumReleaseAgeExclude`);如果那里显式配置了更严格的策略,安装会以 pnpm 自己的报错失败,而不会被覆盖。`~/.npmrc` 或 profile 的 `.npmrc` 中的 `@scope:registry` 路由会覆盖该 scope 的 `--registry`,因此不能让它们指向正在提供服务的 scope。
### 运行时文件筛选
Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安装外部依赖。[Desktop 文件策略](scripts/runtime-file-policy.ts)随后在签名和完整性封装前过滤不可变的 `resources/app.asar/dsh/node_modules` 副本。它排除 TypeScript 声明、已识别的 JavaScript/CSS/TypeScript source map、TypeScript 构建缓存、Domino 测试目录、选定的原生编译器输出和其他平台的 node-pty 预构建文件。它保留运行时 JavaScript、原生模块及其 DLL/EXE 辅助文件、WASM、未知资源、许可证和 notices。依赖清单在完整性封装前经过 electron-builder 的元数据清理,确保归档保持已记录的字节。该策略不修改 npm tarball、捆绑的包管理器或用户安装的插件文件。
+1 -1
View File
@@ -67,7 +67,7 @@
"extract-zip": "^2.0.1",
"js-yaml": "^4.2.0",
"koffi": "3.1.1",
"pnpm": "11.7.0",
"pnpm": "11.28.5",
"sharp": "^0.35.3",
"typescript": "^6.0.3",
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
+2 -2
View File
@@ -9,8 +9,8 @@
en: 5207d17725c07a27
zh: 258884c4300ac63a
/development-guide/setup-tutorial/prerequisites:
en: a8543c278338a02d
zh: b5790c5fea7c0bc6
en: ae47d95cd48bbb1d
zh: 2378e6ed3305e28d
/development-guide/setup-tutorial/windows-and-wsl-2:
en: e86be579be6f1813
zh: 14f6a220337d0861
+1 -1
View File
@@ -10,7 +10,7 @@ The setup tutorial takes a new contributor from prerequisites to a checked check
- Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md).
- Node.js TypeScript type stripping enabled. The repository build scripts load `tsdown.config.ts` with tsdown's native config loader, so they fail when `--no-experimental-strip-types` is in `NODE_OPTIONS` or the Node.js build lacks TypeScript support; `pnpm run build` checks this first and names the cause.
- Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack.
- Corepack-enabled pnpm. The repo pins `pnpm@11.28.5` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack.
- Git 2.26 or newer; hook setup enables Git's worktree-specific configuration extension.
- Optional: a DeepSeek API key for the Web, headless, and ACP automation demos and real-API e2e tests.
+1 -1
View File
@@ -12,7 +12,7 @@
- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.zh.md)。
- 启用 Node.js TypeScript 类型剥离。仓库构建脚本用 tsdown 的 native 配置加载器加载 `tsdown.config.ts`,因此当 `NODE_OPTIONS` 含 `--no-experimental-strip-types` 或 Node.js 构建缺少 TypeScript 支持时会失败;`pnpm run build` 会先检查这一条件并指出原因。
- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。
- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.28.5`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。
- Git 2.26 或更高版本;钩子设置会启用 Git 的 worktree 专属配置扩展。
- 可选:一个 DeepSeek API key,用于 Web、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。
+1 -1
View File
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"license": "BSD-3-Clause",
"packageManager": "pnpm@11.7.0",
"packageManager": "pnpm@11.28.5",
"scripts": {
"build": "pnpm build:ts",
"build:ts": "tsc -b",
+2 -6
View File
@@ -95,12 +95,8 @@ try {
const publishOrder = [];
for (const dir of dirs) {
const manifest = readJson(path.join(root, dir, 'package.json'));
// Platform packages are packed with npm: pnpm pack (observed on 11.7.0)
// normalizes file modes and STRIPS the executable bit, which ships a
// launcher no consumer can spawn; npm pack preserves it. Platform packages
// have no dependencies by construction, so they need none of pnpm's
// workspace-protocol conversion — the entry packages do, and carry no
// executables, so they keep pnpm pack.
// Platform packages have no workspace dependencies; npm preserves their
// executable modes. Entry packages need pnpm's workspace version conversion.
if (platformSet.has(dir)) {
run('npm', ['pack', `./${dir}`, '--pack-destination', destination], packRoot);
} else {
+2 -2
View File
@@ -15,7 +15,7 @@ function fixture(t) {
t.after(() => fs.rmSync(workspace, { recursive: true, force: true }));
const dir = path.join(workspace, 'native/system');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(workspace, 'package.json'), `${JSON.stringify({ private: true, packageManager: 'pnpm@11.7.0' })}\n`);
fs.writeFileSync(path.join(workspace, 'package.json'), `${JSON.stringify({ private: true, packageManager: 'pnpm@11.28.5' })}\n`);
fs.writeFileSync(path.join(workspace, 'pnpm-workspace.yaml'), 'packages:\n - native/system\n - native/system/packages/*\n');
fs.writeFileSync(path.join(workspace, 'LICENSE'), workspaceLicense);
const scratch = path.join(workspace, 'scratch');
@@ -26,7 +26,7 @@ function fixture(t) {
fs.writeFileSync(path.join(dir, file), `${JSON.stringify(value, null, 2)}\n`);
};
const repository = (name) => ({ type: 'git', url: publicRepository, directory: `native/system/packages/${name}` });
writeJson('package.json', { name: 'native-packing-fixture', private: true, version: '1.2.3', packageManager: 'pnpm@11.7.0' });
writeJson('package.json', { name: 'native-packing-fixture', private: true, version: '1.2.3', packageManager: 'pnpm@11.28.5' });
writeJson('packages/linux-x64/package.json', {
name: '@fixture/native-linux-x64', version: '1.2.3', repository: repository('linux-x64'),
os: ['linux'], cpu: ['x64'], files: ['bin/', 'prebuilds.json'],
+3 -2
View File
@@ -4,7 +4,7 @@
"license": "MIT",
"private": true,
"type": "module",
"packageManager": "pnpm@11.7.0",
"packageManager": "pnpm@11.28.5",
"engines": {
"node": "^22.19.0 || >=24.0.0"
},
@@ -206,6 +206,7 @@
"mock:llm": "node --import tsx packages/test-support/llm-mock-server/src/bin.ts",
"start:web": "node --import tsx/esm apps/cli/src/bin.ts web",
"dev:web": "tsx scripts/dev-web.ts --poll",
"dev:bundle-registry": "tsx scripts/dev-bundle-registry.ts",
"postinstall": "node scripts/install-lefthook.mjs",
"gen-official-bundle-catalog": "tsx scripts/gen-official-bundle-catalog.ts",
"verify-official-bundle-catalog": "tsx scripts/gen-official-bundle-catalog.ts --check",
@@ -259,7 +260,7 @@
"micromark-extension-gfm": "^3.0.0",
"oxlint": "1.76.0",
"oxlint-tsgolint": "7.0.2001",
"pnpm": "11.7.0",
"pnpm": "11.28.5",
"publint": "^0.3.21",
"semver": "^7.8.5",
"smol-toml": "^1.7.1",
@@ -15,8 +15,8 @@
en: 87679c51aa3693f5
zh: 11684a48a5806afb
/deepseek-ai-dsh-plugin-manager/use-this-package:
en: ebf6cb77f7abfd51
zh: 8a246ab021c99265
en: af5bf0a205b5a2a1
zh: 0114039816454a1a
/deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions:
en: 920c0acb3b9feaf3
zh: ba6e832a256880a7
+1 -1
View File
@@ -63,7 +63,7 @@ The browser-safe `@deepseek-ai/dsh-plugin-manager/registry` entry exports `OFFIC
`waitForInstall(requestId)` lets a client recover a lost response by waiting for the active installation, including its non-cancellable application phase. It returns the same outcome as the original call, or `null` if the request is not active. Completed results are not retained; `null` establishes neither success nor cancellation.
When pnpm 11 blocks dependency scripts, the failed installation reports every pending package name in the profile under `pendingBuilds`, including names left by earlier attempts; a failed run restores `package.json` and `pnpm-lock.yaml` but deliberately not `pnpm-workspace.yaml`, where pnpm records them. The Web plugin page offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Retry preserves the original activation choice.
When pnpm 11 blocks dependency scripts, the failed installation reports every pending package name in the profile under `pendingBuilds`, including names left by earlier attempts and names the run reported as ignored. pnpm writes those entries into `pnpm-workspace.yaml` only when it can prompt, so the manager records the reported names itself; a failed run restores `package.json` and `pnpm-lock.yaml` but deliberately not `pnpm-workspace.yaml`. The Web plugin page offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Retry preserves the original activation choice.
<a id="version-compatibility-and-exemptions"></a>
### Version compatibility and exemptions
+1 -1
View File
@@ -63,7 +63,7 @@ kind: "package-reference"
`waitForInstall(requestId)` 让客户端在响应丢失后等待活动安装的结果,包括不可取消的应用阶段。它返回与原调用相同的结果,请求不在活动中时返回 `null`。已完成的结果不保留;`null` 不表示成功或已取消。
pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 profile 中所有待决定的包名,包括先前尝试留下的;失败的运行会恢复 `package.json` 与 `pnpm-lock.yaml`,但有意不恢复 pnpm 记录这些名字的 `pnpm-workspace.yaml`。Web 插件页提供**允许这些脚本并重试**;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代为授权。服务只校验待决定的名字,不核实对话中的批准。授权按包名保存在当前 profile,允许以宿主用户的权限执行命令,并在再次安装失败后保留。只能批准当前未决定的名字;已有的拒绝与通配规则不能通过此操作覆盖。`allowBuilds` 里出现 YAML 锚点或别名时拒绝授权。重试保留原来的启用选择。
pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 profile 中所有待决定的包名,包括先前尝试留下的,以及本次运行报告为已忽略的。pnpm 只在能够交互提示时才把这些条目写入 `pnpm-workspace.yaml`,因此由管理器自行记录本次运行报告的包名;失败的运行会恢复 `package.json` 与 `pnpm-lock.yaml`,但有意不恢复 `pnpm-workspace.yaml`。Web 插件页提供**允许这些脚本并重试**;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代为授权。服务只校验待决定的名字,不核实对话中的批准。授权按包名保存在当前 profile,允许以宿主用户的权限执行命令,并在再次安装失败后保留。只能批准当前未决定的名字;已有的拒绝与通配规则不能通过此操作覆盖。`allowBuilds` 里出现 YAML 锚点或别名时拒绝授权。重试保留原来的启用选择。
<a id="version-compatibility-and-exemptions"></a>
### 版本兼容性与豁免
@@ -5,6 +5,12 @@ import { isAlias, isMap, isNode, isScalar, parseDocument, visit } from 'yaml'
import { ManagementFailure } from './failure.ts'
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
/** Value pnpm leaves for a build script that still needs a decision. */
const UNDECIDED_BUILD = 'set this to true or false'
/** Where a non-interactive run names the build scripts it skipped. */
const IGNORED_BUILDS = /\bIgnored build scripts: (.+)/
async function readPolicy(dir: string) {
let text: string
try { text = await readFile(join(dir, 'pnpm-workspace.yaml'), 'utf8') }
@@ -22,18 +28,43 @@ async function readPolicy(dir: string) {
throw new Error('allowBuilds must not contain YAML anchors or aliases')
}
})
const keys = isMap(builds) ? builds.items.flatMap(({ key }) =>
isScalar(key) && typeof key.value === 'string' ? [key.value] : []) : []
const pending = isMap(builds) ? builds.items.flatMap(({ key, value }) =>
isScalar(key) && typeof key.value === 'string' && !/[*?]/.test(key.value)
&& isScalar(value) && value.value === 'set this to true or false' ? [key.value] : []) : []
return { document, pending }
&& isScalar(value) && value.value === UNDECIDED_BUILD ? [key.value] : []) : []
return { document, pending, keys }
}
/** Read package names left undecided by pnpm 11, including after installation cleanup.
/**
* Record the build scripts a failed run reported as ignored, then list every name awaiting a decision.
*
* pnpm writes those policy entries itself only when it can prompt, and the
* manager always runs it non-interactively, so the run's own report is the
* record that survives.
* @param dir Current profile directory.
* @param output Captured output of the failed run.
* @returns Exact package names awaiting a build decision; wildcard rules are excluded.
*/
export async function readPendingBuilds(dir: string): Promise<string[]> {
return (await readPolicy(dir)).pending
export async function recordPendingBuilds(dir: string, output: string): Promise<string[]> {
const { document, pending, keys } = await readPolicy(dir)
const known = new Set(keys)
const added = ignoredBuildNames(output).filter(name => !known.has(name))
if (added.length === 0) return pending
for (const name of added) document.setIn(['allowBuilds', name], UNDECIDED_BUILD)
await writeFileAtomic(join(dir, 'pnpm-workspace.yaml'), String(document), { mode: 0o600 })
return [...pending, ...added]
}
/**
* Read the package names a pnpm run reported as ignored build scripts.
* @param output Captured output of the run.
* @returns Exact names in report order; wildcard patterns are excluded.
*/
export function ignoredBuildNames(output: string): string[] {
const reported = IGNORED_BUILDS.exec(output)?.[1]
if (reported === undefined) return []
return reported.split(',').map(name => name.trim()).filter(name => name !== '' && !/[*?]/.test(name))
}
/** Persist approval without running scripts; the caller holds the profile manifest lock.
+3 -3
View File
@@ -28,7 +28,7 @@ import { dependencySpec, InvalidInstallSpecError, parseInstallSpec, type ParsedI
import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from './registry.ts'
import { writePluginEnabled } from './patch.ts'
import { incompatiblePlugin, ManagementFailure } from './failure.ts'
import { approveBuilds, readPendingBuilds } from './build-approval.ts'
import { approveBuilds, recordPendingBuilds } from './build-approval.ts'
import { bundleDeclarations, bundleModuleOwner, type BundleDeclaration } from './bundle-rows.ts'
import { checkGithubConnection } from './github-connection.ts'
import type {
@@ -568,8 +568,8 @@ export class PluginManager extends TypertRemoteService {
const succeeded = run.exitCode === 0 && run.timedOut !== true
if (succeeded) delete result.failedAt
if (!succeeded) {
// pnpm-workspace.yaml is not restored, so the names pnpm left undecided there can be offered for approval.
try { result.pendingBuilds = await readPendingBuilds(this.profile.dir) }
// pnpm-workspace.yaml is not restored, so the names it left undecided, or reported as ignored, are offered for approval.
try { result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output) }
catch (error) {
this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error)
}
@@ -4,7 +4,7 @@ import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { expect, it, onTestFinished } from 'vitest'
import { parse } from 'yaml'
import { approveBuilds, readPendingBuilds } from '../src/build-approval.ts'
import { approveBuilds, recordPendingBuilds } from '../src/build-approval.ts'
function fixture(text?: string) {
const dir = mkdtempSync(join(tmpdir(), 'build-approval-'))
@@ -16,12 +16,12 @@ function fixture(text?: string) {
it('approves only named pending packages and preserves comments, decisions and settings', async () => {
const { dir, filename } = fixture('# profile settings\nother: &unrelated value\ncopy: *unrelated\nnodeLinker: hoisted\nallowBuilds:\n native: set this to true or false\n "@scope/other": set this to true or false\n trusted: true\n denied: false\n "@scope/*": set this to true or false\n')
expect(await readPendingBuilds(dir)).toEqual(['native', '@scope/other'])
expect(await recordPendingBuilds(dir, '')).toEqual(['native', '@scope/other'])
await approveBuilds(dir, ['native'])
const text = readFileSync(filename, 'utf8')
expect(text).toContain('# profile settings')
expect(parse(text)).toMatchObject({ nodeLinker: 'hoisted', allowBuilds: { native: true, trusted: true, denied: false } })
expect(await readPendingBuilds(dir)).toEqual(['@scope/other'])
expect(await recordPendingBuilds(dir, '')).toEqual(['@scope/other'])
})
it.each(['missing', 'denied', '*', '--all'])('rejects an unlisted approval atomically: %s', async (name) => {
@@ -34,20 +34,20 @@ it.each(['missing', 'denied', '*', '--all'])('rejects an unlisted approval atomi
it('preserves pnpm file dependency selectors verbatim', async () => {
const name = '@scope/addon@file:../local addon'
const { dir, filename } = fixture(`allowBuilds:\n '${name}': set this to true or false\n`)
expect(await readPendingBuilds(dir)).toEqual([name])
expect(await recordPendingBuilds(dir, '')).toEqual([name])
await approveBuilds(dir, [name])
expect(parse(readFileSync(filename, 'utf8'))).toEqual({ allowBuilds: { [name]: true } })
})
it.each([undefined, '{}\n', 'nodeLinker: hoisted\n', 'allowBuilds: {}\n'])('has no pending approval without pnpm placeholders: %s', async (text) => {
const { dir } = fixture(text)
expect(await readPendingBuilds(dir)).toEqual([])
expect(await recordPendingBuilds(dir, '')).toEqual([])
await approveBuilds(dir, [])
})
it.each(['[', '[]\n', 'allowBuilds: false\n'])('rejects malformed workspace settings without rewriting them: %s', async (text) => {
const { dir, filename } = fixture(text)
await expect(readPendingBuilds(dir)).rejects.toThrow()
await expect(recordPendingBuilds(dir, '')).rejects.toThrow()
await expect(approveBuilds(dir, ['native'])).rejects.toThrow()
expect(readFileSync(filename, 'utf8')).toBe(text)
})
@@ -55,7 +55,7 @@ it.each(['[', '[]\n', 'allowBuilds: false\n'])('rejects malformed workspace sett
it('reports unreadable workspace settings', async () => {
const { dir, filename } = fixture()
mkdirSync(filename)
await expect(readPendingBuilds(dir)).rejects.toThrow()
await expect(recordPendingBuilds(dir, '')).rejects.toThrow()
})
it.each([
@@ -67,3 +67,44 @@ it.each([
await expect(approveBuilds(dir, ['native'])).rejects.toThrow()
expect(readFileSync(filename, 'utf8')).toBe(original)
})
it('records the ignored build scripts a non-interactive run reported', async () => {
const { dir, filename } = fixture('# profile settings\nnodeLinker: hoisted\n')
const output = [
'Progress: resolved 2, reused 2, downloaded 0, added 2, done',
'[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: [email protected], @scope/other@file:../other, @scope/*',
'',
'Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.',
].join('\n')
expect(await recordPendingBuilds(dir, output)).toEqual(['[email protected]', '@scope/other@file:../other'])
const text = readFileSync(filename, 'utf8')
expect(text).toContain('# profile settings')
expect(parse(text)).toMatchObject({
nodeLinker: 'hoisted',
allowBuilds: { '[email protected]': 'set this to true or false', '@scope/other@file:../other': 'set this to true or false' },
})
expect(await recordPendingBuilds(dir, output)).toEqual(['[email protected]', '@scope/other@file:../other'])
await approveBuilds(dir, ['[email protected]'])
expect(parse(readFileSync(filename, 'utf8'))).toMatchObject({ allowBuilds: { '[email protected]': true } })
})
it.each([
['no report', 'Progress: resolved 1, reused 1, downloaded 0, added 1, done\n'],
['an empty report', '[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: \n'],
['only wildcards', '[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: @scope/*, native@?\n'],
])('leaves the profile settings untouched for %s', async (_label, output) => {
const original = 'allowBuilds:\n decided: true\n'
const { dir, filename } = fixture(original)
expect(await recordPendingBuilds(dir, output)).toEqual([])
expect(readFileSync(filename, 'utf8')).toBe(original)
})
it('preserves approval keys that are not package names', async () => {
const { dir, filename } = fixture('allowBuilds:\n 1: true\n')
expect(await recordPendingBuilds(dir, '[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: [email protected]\n'))
.toEqual(['[email protected]'])
expect(parse(readFileSync(filename, 'utf8'))).toMatchObject({
allowBuilds: { 1: true, '[email protected]': 'set this to true or false' },
})
})
+147 -180
View File
@@ -96,7 +96,7 @@ importers:
version: link:packages/session/working-directory
'@stylistic/eslint-plugin':
specifier: ^5.10.0
version: 5.10.0([email protected]([email protected]))
version: 5.10.0([email protected]([email protected])([email protected]))
'@testing-library/dom':
specifier: ^10.4.1
version: 10.4.2
@@ -132,7 +132,7 @@ importers:
version: 4.17.1
eslint-plugin-sonarjs:
specifier: ^4.1.0
version: 4.1.0([email protected]([email protected]))
version: 4.1.0([email protected]([email protected])([email protected]))
execa:
specifier: ^10.0.0
version: 10.0.0
@@ -168,7 +168,7 @@ importers:
version: 2.0.3([email protected])
mdast-util-gfm:
specifier: ^3.1.0
version: 3.1.0
version: 3.1.0([email protected])
mermaid:
specifier: 11.16.0
version: 11.16.0
@@ -182,8 +182,8 @@ importers:
specifier: 7.0.2001
version: 7.0.2001
pnpm:
specifier: 11.7.0
version: 11.7.0
specifier: 11.28.5
version: 11.28.5
publint:
specifier: ^0.3.21
version: 0.3.21
@@ -646,7 +646,7 @@ importers:
version: link:../../packages/api/gateway
electron-updater:
specifier: ^6.8.9
version: 6.8.9
version: 6.8.9([email protected])
semver:
specifier: ^7.8.5
version: 7.8.5
@@ -692,7 +692,7 @@ importers:
version: 5.1.0([email protected])
'@electron/notarize':
specifier: 2.5.0
version: 2.5.0
version: 2.5.0([email protected])
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
@@ -713,16 +713,16 @@ importers:
version: 8.18.1
app-builder-lib:
specifier: 26.15.3
version: 26.15.3([email protected])([email protected])
version: 26.15.3([email protected])([email protected])([email protected])
cos-nodejs-sdk-v5:
specifier: 3.0.0
version: 3.0.0
electron:
specifier: ^44.0.0
version: 44.0.0
version: 44.0.0([email protected])
electron-builder:
specifier: ^26.15.3
version: 26.15.3([email protected])
version: 26.15.3([email protected])([email protected])
extract-zip:
specifier: ^2.0.1
version: 2.0.1([email protected])
@@ -733,8 +733,8 @@ importers:
specifier: 3.1.1
version: 3.1.1
pnpm:
specifier: 11.7.0
version: 11.7.0
specifier: 11.28.5
version: 11.28.5
react:
specifier: ^18.2.0
version: 18.3.1
@@ -909,7 +909,7 @@ importers:
version: 0.8.3
http-server:
specifier: ^14.1.1
version: 14.1.1([email protected])
version: 14.1.1([email protected]([email protected]))([email protected])
playwright:
specifier: ^1.49.0
version: 1.61.1
@@ -4572,10 +4572,10 @@ importers:
version: 2.0.3([email protected])
mdast-util-gfm:
specifier: ^3.1.0
version: 3.1.0
version: 3.1.0([email protected])
mdast-util-math:
specifier: ^3.0.0
version: 3.0.0
version: 3.0.0([email protected])
micromark-core-commonmark:
specifier: ^2.0.3
version: 2.0.3
@@ -10751,7 +10751,7 @@ importers:
version: link:../../../vendor/schemastery
'@earendil-works/pi-ai':
specifier: ^1.0.2
version: 1.0.2(patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])
version: 1.0.2(patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:~
@@ -11088,10 +11088,10 @@ importers:
version: 2.0.0
'@modelcontextprotocol/server-everything':
specifier: ^2026.7.4
version: 2026.7.4
version: 2026.7.4([email protected])
'@modelcontextprotocol/server-filesystem':
specifier: ^2026.7.4
version: 2026.7.10([email protected])
version: 2026.7.10([email protected])([email protected])
zod:
specifier: ^4.4.3
version: 4.4.3
@@ -13453,7 +13453,7 @@ importers:
dependencies:
'@anthropic-ai/claude-agent-sdk':
specifier: 0.3.263
version: 0.3.263(@anthropic-ai/[email protected]([email protected]))(@modelcontextprotocol/[email protected]([email protected]))([email protected])
version: 0.3.263(@anthropic-ai/[email protected]([email protected]))(@modelcontextprotocol/[email protected]([email protected])([email protected]))([email protected])
'@anthropic-ai/sdk':
specifier: 0.93.0
version: 0.93.0([email protected])
@@ -13468,7 +13468,7 @@ importers:
version: link:../../../vendor/schemastery
'@modelcontextprotocol/sdk':
specifier: ^1.29.0
version: 1.29.0([email protected])
version: 1.29.0([email protected])([email protected])
zod:
specifier: ^4.4.3
version: 4.4.3
@@ -14055,7 +14055,7 @@ importers:
version: 4.3.1
vitest:
specifier: ^4.1.8
version: 4.1.8(@opentelemetry/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected](@noble/[email protected]))([email protected](@types/node@26.1.2)([email protected])([email protected])([email protected])([email protected]))
version: 4.1.8(@opentelemetry/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected](@noble/[email protected]))([email protected](@types/node@22.20.0)([email protected])([email protected])([email protected])([email protected]))
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:~
@@ -14260,7 +14260,7 @@ importers:
version: 4.2.0
vitest:
specifier: ^4.1.8
version: 4.1.8(@opentelemetry/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected](@noble/[email protected]))([email protected](@types/node@26.1.2)([email protected])([email protected])([email protected])([email protected]))
version: 4.1.8(@opentelemetry/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected](@noble/[email protected]))([email protected](@types/node@22.20.0)([email protected])([email protected])([email protected])([email protected]))
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:~
@@ -18738,11 +18738,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
[email protected]:
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
@@ -21493,8 +21488,8 @@ packages:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'}
pnpm@11.7.0:
resolution: {integrity: sha512-GcyFLBIMcSV2DyRD7mvgyltA+fUFmN4aCaHxd1A+AQ5Xwjx3ZG4B52HeWb+HT7IqM5jDOrlpH8E+uUa28PTWIA==}
pnpm@11.28.5:
resolution: {integrity: sha512-SEVj0wtMKtJ8tIqpT/KzAtvOvI/2vyiBbCWJiG4UbOf3bzVPO8GkOOb16XaMMCqgpvet2CMpA81M+BQmy8FNng==}
engines: {node: '>=22.13'}
hasBin: true
@@ -21874,11 +21869,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
engines: {node: '>=10'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
@@ -23037,10 +23027,10 @@ snapshots:
'@anthropic-ai/[email protected]':
optional: true
'@anthropic-ai/[email protected](@anthropic-ai/[email protected]([email protected]))(@modelcontextprotocol/[email protected]([email protected]))([email protected])':
'@anthropic-ai/[email protected](@anthropic-ai/[email protected]([email protected]))(@modelcontextprotocol/[email protected]([email protected])([email protected]))([email protected])':
dependencies:
'@anthropic-ai/sdk': 0.93.0([email protected])
'@modelcontextprotocol/sdk': 1.29.0([email protected])
'@modelcontextprotocol/sdk': 1.29.0([email protected])([email protected])
zod: 4.4.3
optionalDependencies:
'@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.263
@@ -23280,7 +23270,7 @@ snapshots:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/[email protected]([email protected]))
'@babel/helper-module-transforms': 7.29.7(@babel/[email protected]([email protected]))([email protected])
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
@@ -23322,17 +23312,17 @@ snapshots:
'@babel/[email protected]': {}
'@babel/[email protected]':
'@babel/[email protected]([email protected])':
dependencies:
'@babel/traverse': 7.29.7([email protected])
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/[email protected](@babel/[email protected]([email protected]))':
'@babel/[email protected](@babel/[email protected]([email protected]))([email protected])':
dependencies:
'@babel/core': 7.29.7([email protected])
'@babel/helper-module-imports': 7.29.7
'@babel/helper-module-imports': 7.29.7([email protected])
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.7([email protected])
transitivePeerDependencies:
@@ -23517,12 +23507,12 @@ snapshots:
transitivePeerDependencies:
- '@algolia/client-search'
'@earendil-works/[email protected](patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])':
'@earendil-works/[email protected](patch_hash=1cc211bc358d30cb58bd975f39c03e296f8a277c3de845514d12f9ae77ebab5f)(@aws-sdk/[email protected])(@modelcontextprotocol/[email protected]([email protected])([email protected]))(@smithy/[email protected])([email protected])([email protected])([email protected])([email protected])':
dependencies:
'@anthropic-ai/sdk': 0.129.0([email protected])
'@aws-sdk/client-bedrock-runtime': 3.1127.0
'@earendil-works/pi-telemetry': 1.0.2
'@google/genai': 2.21.0(@modelcontextprotocol/[email protected]([email protected]))
'@google/genai': 2.21.0(@modelcontextprotocol/[email protected]([email protected])([email protected]))([email protected])
'@smithy/node-http-handler': 4.12.1
http-proxy-agent: 9.1.0([email protected])
https-proxy-agent: 9.1.0([email protected])
@@ -23558,7 +23548,7 @@ snapshots:
fs-extra: 9.1.0
minimist: 1.2.8
'@electron/[email protected]':
'@electron/[email protected]([email protected])':
dependencies:
debug: 4.4.3([email protected])
env-paths: 2.2.1
@@ -23566,7 +23556,7 @@ snapshots:
got: 11.8.6
progress: 2.0.3
semver: 6.3.1
sumchecker: 3.0.1
sumchecker: 3.0.1([email protected])
optionalDependencies:
global-agent: 3.0.0
transitivePeerDependencies:
@@ -23579,13 +23569,13 @@ snapshots:
graceful-fs: 4.2.11
progress: 2.0.3
semver: 7.8.5
sumchecker: 3.0.1
sumchecker: 3.0.1([email protected])
optionalDependencies:
undici: 7.28.0
transitivePeerDependencies:
- supports-color
'@electron/[email protected]':
'@electron/[email protected]([email protected])':
dependencies:
debug: 4.4.3([email protected])
fs-extra: 9.1.0
@@ -23593,7 +23583,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@electron/[email protected](patch_hash=280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861)':
'@electron/[email protected](patch_hash=280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861)([email protected])':
dependencies:
compare-version: 0.1.2
debug: 4.4.3([email protected])
@@ -23604,18 +23594,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@electron/[email protected]':
'@electron/[email protected]([email protected])':
dependencies:
'@malept/cross-spawn-promise': 2.0.0
debug: 4.4.3([email protected])
node-abi: 4.34.0
node-api-version: 0.2.1
node-gyp: 12.4.0
read-binary-file-arch: 1.0.6
read-binary-file-arch: 1.0.6([email protected])
transitivePeerDependencies:
- supports-color
'@electron/[email protected]':
'@electron/[email protected]([email protected])':
dependencies:
'@electron/asar': 3.4.1
'@malept/cross-spawn-promise': 2.0.0
@@ -23627,7 +23617,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@electron/[email protected]':
'@electron/[email protected]([email protected])':
dependencies:
cross-dirname: 0.1.0
debug: 4.4.3([email protected])
@@ -23900,14 +23890,14 @@ snapshots:
'@esbuild/[email protected]':
optional: true
'@eslint-community/[email protected]([email protected]([email protected]))':
'@eslint-community/[email protected]([email protected]([email protected])([email protected]))':
dependencies:
eslint: 10.5.0([email protected])
eslint: 10.5.0([email protected])([email protected])
eslint-visitor-keys: 3.4.3
'@eslint-community/[email protected]': {}
'@eslint/[email protected]':
'@eslint/[email protected]([email protected])':
dependencies:
'@eslint/object-schema': 3.0.5
debug: 4.4.3([email protected])
@@ -23982,14 +23972,14 @@ snapshots:
react-dom: 18.3.1([email protected])
regenerator-runtime: 0.14.1
'@google/[email protected](@modelcontextprotocol/[email protected]([email protected]))':
'@google/[email protected](@modelcontextprotocol/[email protected]([email protected])([email protected]))([email protected])':
dependencies:
google-auth-library: 10.7.0
google-auth-library: 10.7.0([email protected])
p-retry: 4.6.2
protobufjs: 7.6.4
ws: 8.21.0
optionalDependencies:
'@modelcontextprotocol/sdk': 1.29.0([email protected])
'@modelcontextprotocol/sdk': 1.29.0([email protected])([email protected])
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -24327,7 +24317,7 @@ snapshots:
dependencies:
cross-spawn: 7.0.6
'@malept/[email protected]':
'@malept/[email protected]([email protected])':
dependencies:
debug: 4.4.3([email protected])
fs-extra: 9.1.0
@@ -24374,18 +24364,18 @@ snapshots:
optionalDependencies:
hono: 4.12.29
'@modelcontextprotocol/[email protected]([email protected])':
'@modelcontextprotocol/[email protected]([email protected])([email protected])':
dependencies:
'@hono/node-server': 1.19.14([email protected])
ajv: 8.20.0
ajv-formats: 3.0.1([email protected])
ajv-formats: 3.0.1
content-type: 1.0.5
cors: 2.8.6
cross-spawn: 7.0.6
eventsource: 3.0.7
eventsource-parser: 3.1.0
express: 5.2.1
express-rate-limit: 8.5.2([email protected])
express: 5.2.1([email protected])
express-rate-limit: 8.5.2([email protected]([email protected]))
hono: 4.12.29
jose: 6.2.3
json-schema-typed: 8.0.2
@@ -24396,20 +24386,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@modelcontextprotocol/[email protected]':
'@modelcontextprotocol/[email protected]([email protected])':
dependencies:
'@modelcontextprotocol/sdk': 1.29.0([email protected])
'@modelcontextprotocol/sdk': 1.29.0([email protected])([email protected])
cors: 2.8.6
express: 5.2.1
express: 5.2.1([email protected])
jszip: 3.10.1
zod: 4.4.3
transitivePeerDependencies:
- '@cfworker/json-schema'
- supports-color
'@modelcontextprotocol/[email protected]([email protected])':
'@modelcontextprotocol/[email protected]([email protected])([email protected])':
dependencies:
'@modelcontextprotocol/sdk': 1.29.0([email protected])
'@modelcontextprotocol/sdk': 1.29.0([email protected])([email protected])
diff: 8.0.4
glob: 10.5.0
minimatch: 10.2.5
@@ -25114,11 +25104,11 @@ snapshots:
'@standard-schema/[email protected]': {}
'@stylistic/[email protected]([email protected]([email protected]))':
'@stylistic/[email protected]([email protected]([email protected])([email protected]))':
dependencies:
'@eslint-community/eslint-utils': 4.9.1([email protected]([email protected]))
'@eslint-community/eslint-utils': 4.9.1([email protected]([email protected])([email protected]))
'@typescript-eslint/types': 8.61.0
eslint: 10.5.0([email protected])
eslint: 10.5.0([email protected])([email protected])
eslint-visitor-keys: 4.2.1
espree: 10.4.0
estraverse: 5.3.0
@@ -25641,14 +25631,6 @@ snapshots:
optionalDependencies:
vite: 8.0.16(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))':
dependencies:
'@vitest/spy': 4.1.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.16(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
'@vitest/[email protected]':
dependencies:
tinyrainbow: 3.1.0
@@ -25905,8 +25887,8 @@ snapshots:
dependencies:
humanize-ms: 1.2.1
[email protected]([email protected]):
optionalDependencies:
[email protected]:
dependencies:
ajv: 8.20.0
[email protected]:
@@ -25958,33 +25940,33 @@ snapshots:
[email protected]: {}
[email protected]([email protected])([email protected]):
[email protected]([email protected])([email protected])([email protected]):
dependencies:
'@electron/asar': 3.4.1
'@electron/fuses': 1.8.0
'@electron/get': 3.1.0
'@electron/notarize': 2.5.0
'@electron/osx-sign': 1.3.3(patch_hash=280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861)
'@electron/rebuild': 4.2.0
'@electron/universal': 2.0.3
'@malept/flatpak-bundler': 0.4.0
'@electron/get': 3.1.0([email protected])
'@electron/notarize': 2.5.0([email protected])
'@electron/osx-sign': 1.3.3(patch_hash=280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861)([email protected])
'@electron/rebuild': 4.2.0([email protected])
'@electron/universal': 2.0.3([email protected])
'@malept/flatpak-bundler': 0.4.0([email protected])
'@noble/hashes': 2.3.0
'@peculiar/webcrypto': 1.7.1
'@types/fs-extra': 9.0.13
ajv: 8.20.0
asn1js: 3.0.10
async-exit-hook: 2.0.1
builder-util: 26.15.3
builder-util-runtime: 9.7.0
builder-util: 26.15.3([email protected])
builder-util-runtime: 9.7.0([email protected])
chromium-pickle-js: 0.2.0
ci-info: 4.3.1
debug: 4.4.3([email protected])
dmg-builder: 26.15.3([email protected])
dmg-builder: 26.15.3([email protected])([email protected])
dotenv: 16.6.1
dotenv-expand: 11.0.7
ejs: 3.1.10
electron-builder-squirrel-windows: 26.15.3([email protected])
electron-publish: 26.15.3
electron-builder-squirrel-windows: 26.15.3([email protected])([email protected])
electron-publish: 26.15.3([email protected])
fs-extra: 10.1.0
hosted-git-info: 4.1.0
isbinaryfile: 5.0.7
@@ -26164,7 +26146,7 @@ snapshots:
[email protected]: {}
[email protected]:
[email protected]([email protected]):
dependencies:
bytes: 3.1.2
content-type: 2.1.0
@@ -26232,23 +26214,23 @@ snapshots:
[email protected]: {}
[email protected]:
[email protected]([email protected]):
dependencies:
debug: 4.4.3([email protected])
sax: 1.6.1
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
'@types/debug': 4.1.13
builder-util-runtime: 9.7.0
builder-util-runtime: 9.7.0([email protected])
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3([email protected])
fs-extra: 10.1.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
http-proxy-agent: 7.0.2([email protected])
https-proxy-agent: 7.0.6([email protected])
js-yaml: 4.3.1
sanitize-filename: 1.6.4
source-map-support: 0.5.21
@@ -26804,10 +26786,10 @@ snapshots:
minimatch: 3.1.5
p-limit: 3.1.0
[email protected]([email protected]):
[email protected]([email protected])([email protected]):
dependencies:
app-builder-lib: 26.15.3([email protected])([email protected])
builder-util: 26.15.3
app-builder-lib: 26.15.3([email protected])([email protected])([email protected])
builder-util: 26.15.3([email protected])
fs-extra: 10.1.0
js-yaml: 4.3.1
transitivePeerDependencies:
@@ -26857,23 +26839,23 @@ snapshots:
dependencies:
jake: 10.9.4
[email protected]([email protected]):
[email protected]([email protected])([email protected]):
dependencies:
app-builder-lib: 26.15.3([email protected])([email protected])
builder-util: 26.15.3
electron-winstaller: 5.4.0
app-builder-lib: 26.15.3([email protected])([email protected])([email protected])
builder-util: 26.15.3([email protected])
electron-winstaller: 5.4.0([email protected])
transitivePeerDependencies:
- dmg-builder
- supports-color
[email protected]([email protected]):
[email protected]([email protected])([email protected]):
dependencies:
app-builder-lib: 26.15.3([email protected])([email protected])
builder-util: 26.15.3
builder-util-runtime: 9.7.0
app-builder-lib: 26.15.3([email protected])([email protected])([email protected])
builder-util: 26.15.3([email protected])
builder-util-runtime: 9.7.0([email protected])
chalk: 4.1.2
ci-info: 4.4.0
dmg-builder: 26.15.3([email protected])
dmg-builder: 26.15.3([email protected])([email protected])
fs-extra: 10.1.0
lazy-val: 1.0.5
simple-update-notifier: 2.0.0
@@ -26882,12 +26864,12 @@ snapshots:
- electron-builder-squirrel-windows
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
'@types/fs-extra': 9.0.13
aws4: 1.13.2
builder-util: 26.15.3
builder-util-runtime: 9.7.0
builder-util: 26.15.3([email protected])
builder-util-runtime: 9.7.0([email protected])
chalk: 4.1.2
form-data: 4.0.6
fs-extra: 10.1.0
@@ -26898,9 +26880,9 @@ snapshots:
[email protected]: {}
[email protected]:
[email protected]([email protected]):
dependencies:
builder-util-runtime: 9.7.0
builder-util-runtime: 9.7.0([email protected])
fs-extra: 10.1.0
js-yaml: 4.3.1
lazy-val: 1.0.5
@@ -26911,7 +26893,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
'@electron/asar': 3.4.1
debug: 4.4.3([email protected])
@@ -26919,11 +26901,11 @@ snapshots:
lodash: 4.18.1
temp: 0.9.4
optionalDependencies:
'@electron/windows-sign': 1.2.2
'@electron/windows-sign': 1.2.2([email protected])
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
'@electron-internal/extract-zip': 1.0.5
'@electron/get': 5.1.0([email protected])
@@ -27071,19 +27053,19 @@ snapshots:
[email protected]: {}
[email protected]([email protected]([email protected])):
[email protected]([email protected]([email protected])([email protected])):
dependencies:
'@eslint-community/regexpp': 4.12.2
builtin-modules: 3.3.0
bytes: 3.1.2
eslint: 10.5.0([email protected])
eslint: 10.5.0([email protected])([email protected])
functional-red-black-tree: 1.0.1
globals: 17.7.0
jsx-ast-utils-x: 0.1.0
lodash.merge: 4.6.2
minimatch: 10.2.5
scslre: 0.3.0
semver: 7.8.4
semver: 7.8.5
ts-api-utils: 2.5.0([email protected])
typescript: 6.0.3
yaml: 2.9.0
@@ -27101,11 +27083,11 @@ snapshots:
[email protected]: {}
[email protected]([email protected]):
[email protected]([email protected])([email protected]):
dependencies:
'@eslint-community/eslint-utils': 4.9.1([email protected]([email protected]))
'@eslint-community/eslint-utils': 4.9.1([email protected]([email protected])([email protected]))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.5
'@eslint/config-array': 0.23.5([email protected])
'@eslint/config-helpers': 0.6.0
'@eslint/core': 1.2.1
'@eslint/plugin-kit': 0.7.2
@@ -27222,15 +27204,15 @@ snapshots:
[email protected]: {}
[email protected]([email protected]):
[email protected]([email protected]([email protected])):
dependencies:
express: 5.2.1
express: 5.2.1([email protected])
ip-address: 10.2.0
[email protected]:
[email protected]([email protected]):
dependencies:
accepts: 2.0.0
body-parser: 2.3.0
body-parser: 2.3.0([email protected])
content-disposition: 1.1.0
content-type: 1.0.5
cookie: 0.7.2
@@ -27240,7 +27222,7 @@ snapshots:
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 2.1.1
finalhandler: 2.1.1([email protected])
fresh: 2.0.0
http-errors: 2.0.1
merge-descriptors: 2.0.0
@@ -27251,7 +27233,7 @@ snapshots:
proxy-addr: 2.0.8
qs: 6.15.3
range-parser: 1.3.0
router: 2.2.0
router: 2.2.0([email protected])
send: 1.2.1([email protected])
serve-static: 2.2.1([email protected])
statuses: 2.0.2
@@ -27334,7 +27316,7 @@ snapshots:
dependencies:
minimatch: 5.1.9
[email protected]:
[email protected]([email protected]):
dependencies:
debug: 4.4.3([email protected])
encodeurl: 2.0.0
@@ -27361,7 +27343,9 @@ snapshots:
dependencies:
tabbable: 6.5.0
[email protected]: {}
[email protected]([email protected]([email protected])):
optionalDependencies:
debug: 4.4.3([email protected])
[email protected]:
dependencies:
@@ -27474,17 +27458,17 @@ snapshots:
[email protected]: {}
[email protected]:
[email protected]([email protected]):
dependencies:
extend: 3.0.2
https-proxy-agent: 7.0.6
https-proxy-agent: 7.0.6([email protected])
node-fetch: 3.3.2
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
gaxios: 7.1.5
gaxios: 7.1.5([email protected])
google-logging-utils: 1.1.3
json-bigint: 1.0.0
transitivePeerDependencies:
@@ -27575,12 +27559,12 @@ snapshots:
[email protected]: {}
[email protected]:
[email protected]([email protected]):
dependencies:
base64-js: 1.5.1
ecdsa-sig-formatter: 1.0.11
gaxios: 7.1.5
gcp-metadata: 8.1.2
gaxios: 7.1.5([email protected])
gcp-metadata: 8.1.2([email protected])
google-logging-utils: 1.1.3
jws: 4.0.1
transitivePeerDependencies:
@@ -27707,7 +27691,7 @@ snapshots:
statuses: 2.0.2
toidentifier: 1.0.1
[email protected]:
[email protected]([email protected]):
dependencies:
agent-base: 7.1.4
debug: 4.4.3([email protected])
@@ -27723,22 +27707,22 @@ snapshots:
- kerberos
- supports-color
[email protected]:
[email protected]([email protected]([email protected])):
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.16.0
follow-redirects: 1.16.0([email protected]([email protected]))
requires-port: 1.0.0
transitivePeerDependencies:
- debug
[email protected]([email protected]):
[email protected]([email protected]([email protected]))([email protected]):
dependencies:
basic-auth: 2.0.1
chalk: 4.1.2
corser: 2.0.1
he: 1.2.0
html-encoding-sniffer: 3.0.0
http-proxy: 1.18.1
http-proxy: 1.18.1([email protected]([email protected]))
mime: 1.6.0
minimist: 1.2.8
opener: 1.5.2
@@ -27766,7 +27750,7 @@ snapshots:
quick-lru: 5.1.1
resolve-alpn: 1.2.1
[email protected]:
[email protected]([email protected]):
dependencies:
agent-base: 7.1.4
debug: 4.4.3([email protected])
@@ -28323,7 +28307,7 @@ snapshots:
mdast-util-find-and-replace: 3.0.2
micromark-util-character: 2.1.1
[email protected]:
[email protected]([email protected]):
dependencies:
'@types/mdast': 4.0.4
devlop: 1.1.0
@@ -28333,7 +28317,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
'@types/mdast': 4.0.4
mdast-util-from-markdown: 2.0.3([email protected])
@@ -28341,7 +28325,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
'@types/mdast': 4.0.4
devlop: 1.1.0
@@ -28351,7 +28335,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
'@types/mdast': 4.0.4
devlop: 1.1.0
@@ -28360,19 +28344,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
mdast-util-from-markdown: 2.0.3([email protected])
mdast-util-gfm-autolink-literal: 2.0.1
mdast-util-gfm-footnote: 2.1.0
mdast-util-gfm-strikethrough: 2.0.0
mdast-util-gfm-table: 2.0.0
mdast-util-gfm-task-list-item: 2.0.0
mdast-util-gfm-footnote: 2.1.0([email protected])
mdast-util-gfm-strikethrough: 2.0.0([email protected])
mdast-util-gfm-table: 2.0.0([email protected])
mdast-util-gfm-task-list-item: 2.0.0([email protected])
mdast-util-to-markdown: 2.1.2
transitivePeerDependencies:
- supports-color
[email protected]:
[email protected]([email protected]):
dependencies:
'@types/hast': 3.0.5
'@types/mdast': 4.0.4
@@ -29072,7 +29056,7 @@ snapshots:
base64-js: 1.5.1
xmlbuilder: 15.1.1
pnpm@11.7.0: {}
pnpm@11.28.5: {}
[email protected]: {}
@@ -29240,7 +29224,7 @@ snapshots:
dependencies:
loose-envify: 1.4.0
[email protected]:
[email protected]([email protected]):
dependencies:
debug: 4.4.3([email protected])
transitivePeerDependencies:
@@ -29450,7 +29434,7 @@ snapshots:
points-on-curve: 0.2.0
points-on-path: 0.2.1
[email protected]:
[email protected]([email protected]):
dependencies:
debug: 4.4.3([email protected])
depd: 2.0.0
@@ -29511,8 +29495,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]([email protected]):
@@ -29806,7 +29788,7 @@ snapshots:
[email protected]: {}
[email protected]:
[email protected]([email protected]):
dependencies:
debug: 4.4.3([email protected])
transitivePeerDependencies:
@@ -29983,7 +29965,7 @@ snapshots:
picomatch: 4.0.4
rolldown: 1.1.1
rolldown-plugin-dts: 0.25.2([email protected])([email protected])([email protected])
semver: 7.8.4
semver: 7.8.5
tinyexec: 1.2.4
tinyglobby: 0.2.17
tree-kill: 1.2.2
@@ -30232,21 +30214,6 @@ snapshots:
tsx: 4.22.4
yaml: 2.9.0
[email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.15
rolldown: 1.0.3
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 26.1.2
esbuild: 0.28.1
fsevents: 2.3.3
jiti: 2.7.0
tsx: 4.22.4
yaml: 2.9.0
[email protected]([email protected])([email protected](@algolia/[email protected])(@types/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])([email protected])):
dependencies:
mermaid: 11.16.0
@@ -30366,10 +30333,10 @@ snapshots:
transitivePeerDependencies:
- msw
[email protected](@opentelemetry/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected](@noble/[email protected]))([email protected](@types/node@26.1.2)([email protected])([email protected])([email protected])([email protected])):
[email protected](@opentelemetry/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected](@noble/[email protected]))([email protected](@types/node@22.20.0)([email protected])([email protected])([email protected])([email protected])):
dependencies:
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8([email protected](@types/node@26.1.2)([email protected])([email protected])([email protected])([email protected]))
'@vitest/mocker': 4.1.8([email protected](@types/node@22.20.0)([email protected])([email protected])([email protected])([email protected]))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
@@ -30386,7 +30353,7 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.0.16(@types/node@26.1.2)([email protected])([email protected])([email protected])([email protected])
vite: 8.0.16(@types/node@22.20.0)([email protected])([email protected])([email protected])([email protected])
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.1
+3
View File
@@ -97,6 +97,9 @@ minimumReleaseAgeExclude:
- '@trycua/[email protected]'
- '@trycua/[email protected]'
- '@trycua/[email protected]'
# The package manager is a workspace devDependency, so its own exact pin
# must be exempt from the release-age window that guards other packages.
- [email protected]
patchedDependencies:
'@earendil-works/[email protected]': patches/@[email protected]
'@electron/[email protected]': patches/@[email protected]
+2 -2
View File
@@ -15,5 +15,5 @@
en: 641c647efc49c568
zh: a29371de9fbbebfd
/deepseek-harness-runtime-bin/build-and-distribution:
en: 9a4ba11944fbfc54
zh: 2ae046803108df2b
en: 437ab8ba3bbe7ed6
zh: 3f321a73e472220b
+1 -1
View File
@@ -50,6 +50,6 @@ External profile management uses `dsh plugin --profile <name> ...`. That command
Production deployment permits unused workspace patches for packages outside the runtime closure; patches for included packages must still apply successfully. This exception is confined to the deploy command; repository installation still rejects unused patches.
From the repository root, `pnpm exec tsx scripts/build-exe-for-python-sdk.ts` verifies the closure, builds packages, deploys a symlink-free tree, packages the selected target, and syncs the executable and sidecars into this module. `scripts/build-python-release.py` stages release-shaped wheels at the root repository version and pins `deepseek-harness-sdk` to the exact runtime version.
From the repository root, `pnpm exec tsx scripts/build-exe-for-python-sdk.ts` verifies the closure, builds packages, deploys a symlink-free tree, packages the selected target, and syncs the executable and sidecars into this module. Deployment disables workspace hoisting so unrelated source packages stay out of the payload. Staging shares root workspace packages with nested consumers of the same pnpm dependency identity, preserving singleton services while retaining different peer resolutions. `scripts/build-python-release.py` stages release-shaped wheels at the root repository version and pins `deepseek-harness-sdk` to the exact runtime version.
The installed-wheel smoke creates a clean virtual environment outside the checkout, proves the installed distribution and executable identities, then exercises default and customized SDK profiles, external plugins, MCP, native tools, direct JSON-RPC, committed snapshots, and the real provider on trusted runs. Its Office scenario relocates the complete target payload and converts DOCX with the required platform engine: the target’s declared native engine, or WASM when no native engine is declared. See the [Python contributor workflow](../development.md) and [installed-wheel testing decision](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.md).
+1 -1
View File
@@ -50,6 +50,6 @@ Python bootstrap 从相邻目录解析 Office kit,让原生辅助程序与 URL
生产部署允许工作区中不属于运行时闭包的补丁保持未使用;闭包内包的补丁仍必须成功应用。此例外仅用于部署命令,仓库安装仍拒绝未使用的补丁。
在仓库根目录运行 `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`,会校验闭包、构建包、部署无符号链接的文件树、打包所选目标,并把可执行程序及伴随文件同步到本模块。`scripts/build-python-release.py` 按仓库根版本暂存发布形态的 wheel 包,并将 `deepseek-harness-sdk` 固定到完全相同的运行时版本。
在仓库根目录运行 `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`,会校验闭包、构建包、部署无符号链接的文件树、打包所选目标,并把可执行程序及伴随文件同步到本模块。部署会关闭工作区提升,避免无关源码包进入载荷。暂存步骤让使用相同 pnpm 依赖标识的嵌套消费者共享根目录的工作区包,保留服务单例,同时保留不同的对等依赖解析。`scripts/build-python-release.py` 按仓库根版本暂存发布形态的 wheel 包,并将 `deepseek-harness-sdk` 固定到完全相同的运行时版本。
已安装 wheel 包冒烟测试会在检出目录外创建干净的虚拟环境,验证分发物与可执行程序的来源,然后覆盖默认及自定义 SDK profile、外部插件、MCP、原生工具、直接 JSON-RPC、检入快照,以及可信运行中的真实提供方。Office 场景会迁移完整的目标载荷目录,并使用所需的平台引擎转换 DOCX:使用目标已声明的原生引擎,未声明原生引擎时使用 WASM。另见 [Python 贡献者工作流](../development.zh.md) 与 [installed-wheel 测试决策](../../.agents/notes/implemented/testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)。
@@ -0,0 +1,121 @@
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, relative } from 'node:path'
import { createRequire } from 'node:module'
import { afterEach, expect, it } from 'vitest'
import { deduplicateStagedWorkspacePackages, materializeStagedLinks } from './build-exe-for-python-sdk-staging.ts'
const temporaryDirectories: string[] = []
const directoryLinkType = process.platform === 'win32' ? 'junction' : 'dir'
afterEach(async () => {
await Promise.all(temporaryDirectories.splice(0).map(directory => rm(directory, { recursive: true, force: true })))
})
async function fixture(prefix: string) {
const workspace = await mkdtemp(join(tmpdir(), prefix))
temporaryDirectories.push(workspace)
return workspace
}
/** List every symbolic link below a directory. */
async function listSymlinks(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true })
const nested = await Promise.all(entries.map(async (entry) => {
const path = join(directory, entry.name)
if (entry.isSymbolicLink()) return [path]
if (entry.isDirectory()) return listSymlinks(path)
return []
}))
return nested.flat()
}
it('drops the deploy root link that points back into the payload', async () => {
const workspace = await fixture('dsh-python-staging-root-')
const project = join(workspace, 'python', 'sdk-runtime')
const staging = join(project, 'src', 'runtime', 'node')
await mkdir(join(staging, 'node_modules'), { recursive: true })
await writeFile(join(project, 'package.json'), '{"name":"dsh-python-runtime-closure"}\n')
await symlink(project, join(staging, 'node_modules', 'dsh-python-runtime-closure'), directoryLinkType)
await materializeStagedLinks(staging)
expect(existsSync(join(staging, 'node_modules', 'dsh-python-runtime-closure'))).toBe(false)
await expect(readFile(join(project, 'package.json'), 'utf8')).resolves.toContain('dsh-python-runtime-closure')
})
it('copies workspace package links as files and removes command shims', async () => {
const workspace = await fixture('dsh-python-staging-packages-')
const staging = join(workspace, 'runtime', 'node')
const packageDirectory = join(workspace, 'packages', 'dsh-base')
await mkdir(join(packageDirectory, 'lib'), { recursive: true })
await writeFile(join(packageDirectory, 'package.json'), '{"name":"@deepseek-ai/dsh-base"}\n')
await writeFile(join(packageDirectory, 'lib', 'index.js'), 'export {}\n')
await mkdir(join(staging, 'node_modules', '@deepseek-ai'), { recursive: true })
await mkdir(join(staging, 'node_modules', '.bin'), { recursive: true })
await writeFile(join(staging, 'package.json'), '{"name":"dsh-python-runtime-closure"}\n')
await symlink(packageDirectory, join(staging, 'node_modules', '@deepseek-ai', 'dsh-base'), directoryLinkType)
await symlink(packageDirectory, join(staging, 'node_modules', '.bin', 'dsh'), directoryLinkType)
await materializeStagedLinks(staging)
expect(existsSync(join(staging, 'node_modules', '.bin'))).toBe(false)
await expect(readFile(join(staging, 'node_modules', '@deepseek-ai', 'dsh-base', 'lib', 'index.js'), 'utf8'))
.resolves.toBe('export {}\n')
expect(await listSymlinks(staging)).toEqual([])
})
it('shares an identical workspace package while retaining different peers and third-party copies', async () => {
const workspace = await fixture('dsh-python-staging-dedupe-')
const staging = join(workspace, 'payload')
const modules = join(staging, 'node_modules')
const root = join(modules, '@fixture', 'tools')
const duplicate = join(modules, 'app', 'node_modules', '@fixture', 'tools')
const differentPeers = join(modules, 'other', 'node_modules', '@fixture', 'tools')
const external = join(modules, 'external')
const externalCopy = join(modules, 'app', 'node_modules', 'external')
for (const directory of [root, duplicate, differentPeers, external, externalCopy]) {
await mkdir(directory, { recursive: true })
await writeFile(join(directory, 'index.js'), 'exports.scheduler = Symbol("scheduler")\n')
}
const locations = (paths: string[]) => paths.map(path => relative(workspace, path))
await writeFile(join(modules, '.modules.yaml'), JSON.stringify({ hoistedLocations: {
'@fixture/tools@file:packages/tools(peer@1)': locations([root, duplicate]),
'@fixture/tools@file:packages/tools(peer@2)': locations([differentPeers]),
'[email protected](@fixture/tools@file:packages/tools)': locations([external, externalCopy]),
} }))
await deduplicateStagedWorkspacePackages(staging, workspace)
expect(existsSync(duplicate)).toBe(false)
for (const directory of [root, differentPeers, external, externalCopy]) {
expect(existsSync(join(directory, 'index.js'))).toBe(true)
}
const fromRoot = createRequire(join(staging, 'entry.cjs'))
const fromApp = createRequire(join(modules, 'app', 'entry.cjs'))
const fromOther = createRequire(join(modules, 'other', 'entry.cjs'))
expect(fromApp('@fixture/tools')).toBe(fromRoot('@fixture/tools'))
expect(fromOther('@fixture/tools')).not.toBe(fromRoot('@fixture/tools'))
})
it('rejects invalid deployment metadata before removing any package', async () => {
const workspace = await fixture('dsh-python-staging-metadata-')
const staging = join(workspace, 'payload')
const modules = join(staging, 'node_modules')
const root = join(modules, 'tools')
const duplicate = join(modules, 'app', 'node_modules', 'tools')
const outside = join(workspace, 'source')
for (const directory of [root, duplicate, outside]) await mkdir(directory, { recursive: true })
const valid = { 'tools@file:packages/tools': [root, duplicate].map(path => relative(workspace, path)) }
for (const invalid of [
{},
{ hoistedLocations: { invalid: [42] } },
{ hoistedLocations: { ...valid, 'other@file:packages/other': [relative(workspace, outside)] } },
]) {
await writeFile(join(modules, '.modules.yaml'), JSON.stringify(invalid))
await expect(deduplicateStagedWorkspacePackages(staging, workspace)).rejects.toThrow('hoistedLocations')
expect(existsSync(duplicate)).toBe(true)
expect(existsSync(outside)).toBe(true)
}
})
+101
View File
@@ -0,0 +1,101 @@
/** Prepare a symlink-free Python runtime payload with shared workspace module instances. */
import { cp, lstat, readFile, readdir, realpath, rm, unlink } from 'node:fs/promises'
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
import { load } from 'js-yaml'
function isHoistedLocations(value: unknown): value is Record<string, string[]> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
&& Object.values(value).every(paths => Array.isArray(paths)
&& paths.every((path: unknown) => typeof path === 'string'))
}
/**
* Share root workspace packages with nested consumers of the same pnpm dependency identity.
* Different peer resolutions and third-party packages keep their installed locations.
* All metadata paths are checked before deletion; the caller first materializes links.
* @param staging - Symlink-free deployed payload root.
* @param workspace - Workspace root relative to which pnpm records hoisted locations.
*/
export async function deduplicateStagedWorkspacePackages(staging: string, workspace: string): Promise<void> {
const modules = resolve(staging, 'node_modules')
const file = join(modules, '.modules.yaml')
const manifest: unknown = load(await readFile(file, 'utf8'))
if (typeof manifest !== 'object' || manifest === null || !('hoistedLocations' in manifest)
|| !isHoistedLocations(manifest.hoistedLocations)) {
throw new Error(`${file}: hoistedLocations must map dependency identities to path lists`)
}
const duplicates: string[] = []
for (const [identity, locations] of Object.entries(manifest.hoistedLocations)) {
const paths = locations.map((location) => {
const path = resolve(workspace, location)
const local = relative(modules, path)
if (local === '' || local === '..' || local.startsWith(`..${sep}`) || isAbsolute(local)) {
throw new Error(`${file}: hoistedLocations path is outside deployed node_modules: ${location}`)
}
return { path, parts: local.split(sep) }
})
if (!/^(?:@[^/]+\/)?[^@()]+@file:/.test(identity)) continue
const root = paths.find(({ parts }) => parts.length === (parts[0]?.startsWith('@') === true ? 2 : 1))
if (root === undefined) continue
if (!(await lstat(root.path)).isDirectory()) {
throw new Error(`${file}: hoistedLocations root package is not a directory: ${root.path}`)
}
duplicates.push(...paths.filter(({ path }) => path !== root.path).map(({ path }) => path))
}
for (const path of duplicates) await rm(path, { recursive: true, force: true })
}
/** Return the first symbolic link below a directory, if one exists. */
async function findSymlink(directory: string): Promise<string | undefined> {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name)
const metadata = await lstat(path)
if (metadata.isSymbolicLink()) return path
if (metadata.isDirectory()) {
const nested = await findSymlink(path)
if (nested !== undefined) return nested
}
}
return undefined
}
/**
* Replace every deploy-time package link below the staged payload with real files.
*
* Legacy deploy links workspace packages into the target's `node_modules`, and
* those links must survive packaging as files. One link is not a package: when
* the deploy target sits inside the deployed project, pnpm links that project
* into its own `node_modules`. Dereferencing it would copy the payload into
* itself, and the packaged payload never carries the project source, so the
* entry is dropped instead.
* @param staging - Deployed payload root whose `node_modules` becomes symlink-free.
*/
export async function materializeStagedLinks(staging: string): Promise<void> {
const payload = await realpath(staging)
const nodeModules = join(staging, 'node_modules')
let remaining = await findSymlink(nodeModules)
while (remaining !== undefined) {
const segments = remaining.slice(nodeModules.length + 1).split(sep)
const binIndex = segments.lastIndexOf('.bin')
if (binIndex >= 0) {
await rm(join(nodeModules, ...segments.slice(0, binIndex + 1)), { recursive: true, force: true })
remaining = await findSymlink(nodeModules)
continue
}
const destination = remaining
const source = await realpath(destination)
if (payload === source || payload.startsWith(source + sep)) {
await unlink(destination)
remaining = await findSymlink(nodeModules)
continue
}
const nestedNodeModules = join(source, 'node_modules')
await unlink(destination)
await cp(source, destination, {
recursive: true,
dereference: true,
filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
})
remaining = await findSymlink(nodeModules)
}
}
+1
View File
@@ -135,6 +135,7 @@ describe('Python runtime executable builder CLI', () => {
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs --filter dsh-python-runtime-closure deploy`)
const deploy = result.stdout.split('\n').find(line => line.includes(' --filter dsh-python-runtime-closure deploy'))
expect(deploy).toContain('--prod --config.allow-unused-patches=true')
expect(deploy).toContain('--config.hoist-workspace-packages=false')
expect(result.stdout.split('--config.allow-unused-patches=true')).toHaveLength(2)
expect(result.stdout).not.toContain(resolve(root, 'python/sdk-runtime/runtime-bootstrap.mjs'))
expect(result.stdout).toContain('"bin":"runtime-bootstrap.mjs"')
+5 -36
View File
@@ -8,12 +8,13 @@
import { spawn } from 'node:child_process'
import { existsSync, statSync } from 'node:fs'
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
import { chmod, copyFile, cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, extname, join, resolve, sep } from 'node:path'
import { parseArgs } from 'node:util'
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
import { copyOfficeSidecar, OFFICE_ASSET_IGNORES } from './build-exe-for-python-sdk-office.ts'
import { deduplicateStagedWorkspacePackages, materializeStagedLinks } from './build-exe-for-python-sdk-staging.ts'
import { preparePrimaryRuntime, smokePrimaryRuntime, type PrimaryRuntimeTarget } from './primary-runtime/prepare.ts'
const root = resolve(import.meta.dirname, '..')
@@ -310,6 +311,7 @@ class SingleExeBuild {
'--config.node-linker=hoisted',
'--config.auto-install-peers=false',
'--config.link-workspace-packages=true',
'--config.hoist-workspace-packages=false',
this.staging,
])
await this.restoreLegacyHoists()
@@ -372,41 +374,8 @@ class SingleExeBuild {
console.log('build-exe-for-python-sdk: [dry-run] materialize staged package links')
return
}
const nodeModules = join(this.staging, 'node_modules')
let remaining = await this.findSymlink(nodeModules)
while (remaining !== undefined) {
const segments = remaining.slice(nodeModules.length + 1).split(sep)
const binIndex = segments.lastIndexOf('.bin')
if (binIndex >= 0) {
await rm(join(nodeModules, ...segments.slice(0, binIndex + 1)), { recursive: true, force: true })
remaining = await this.findSymlink(nodeModules)
continue
}
const destination = remaining
const source = await realpath(destination)
const nestedNodeModules = join(source, 'node_modules')
await rm(destination, { recursive: true, force: true })
await cp(source, destination, {
recursive: true,
dereference: true,
filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
})
remaining = await this.findSymlink(nodeModules)
}
}
/** Return the first symbolic link below a directory, if one exists. */
private async findSymlink(directory: string): Promise<string | undefined> {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name)
const metadata = await lstat(path)
if (metadata.isSymbolicLink()) return path
if (metadata.isDirectory()) {
const nested = await this.findSymlink(path)
if (nested !== undefined) return nested
}
}
return undefined
await materializeStagedLinks(this.staging)
await deduplicateStagedWorkspacePackages(this.staging, root)
}
/** Add the executable entry and pkg assets to the staged manifest. */
+1 -1
View File
@@ -1019,7 +1019,7 @@ describe('Python release workflows', () => {
expect(manylinuxAddon).toMatchObject({ if: "runner.os == 'Linux'" })
expect(JSON.stringify(manylinuxAddon)).toContain('manylinux_2_28_x86_64')
expect(JSON.stringify(manylinuxAddon)).toContain('manylinux_2_28_aarch64')
expect(JSON.stringify(manylinuxAddon)).toContain('npm_config_build_from_source=true pnpm run install')
expect(JSON.stringify(manylinuxAddon)).toContain('npm_config_build_from_source=true pnpm --config.verifyDepsBeforeRun=false run install')
expect(JSON.stringify(manylinuxAddon)).toContain('pnpm_setup_root')
expect(JSON.stringify(manylinuxAddon)).toContain('$pnpm_setup_root:$pnpm_setup_root:ro')
expect(JSON.stringify(manylinuxAddon)).toContain('node-pty-glibc-versions.txt')
+170
View File
@@ -0,0 +1,170 @@
/**
* Build, pack, and serve the Official on-demand bundles' local dependency
* closure as a loopback registry, so a packaged test app can install them
* before their release reaches npm.
*
* The run is a build/test utility, not a Harness application launcher: it
* starts no agent and needs no credentials. It shares only the artifact
* transport with the release path; packing, manifests, and validation stay with
* the owners that already perform them.
*/
import { mkdirSync } from 'node:fs'
import { randomUUID } from 'node:crypto'
import { join, resolve } from 'node:path'
import { execa } from 'execa'
import { getDshRuntimeVersion } from '../packages/boot/app-boot/src/plugin-compatibility.ts'
import { generateOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
import {
REGISTRY_MANIFEST_FILE,
REGISTRY_OUTPUT_ROOT,
createRegistryManifest,
hostPlatform,
packBundleArtifacts,
pnpmInvocationFor,
readReleaseMembers,
readWorkspacePackages,
selectBundleDependencies,
sourceEvidence,
startBundleRegistry,
verifyBuiltBundles,
verifyReleaseVersions,
writeRegistryManifest,
type BundleArtifact,
} from './local-bundle-registry.ts'
/** The preparation step a signal stopped, so the failure is not reported as a command's. */
class Interrupted extends Error {
/**
* @param stage - the step that was running when the signal arrived.
*/
constructor(readonly stage: string) {
super(`interrupted during ${stage}`)
}
}
/**
* Run one package script in the repository root, propagating its exit status.
* @param root - repository root the command runs in.
* @param args - pnpm arguments.
* @param signal - abort signal that stops the child on SIGINT/SIGTERM.
* @param label - step name used in the failure message.
*/
async function runStep(root: string, args: readonly string[], signal: AbortSignal, label: string): Promise<void> {
const invocation = pnpmInvocationFor(args)
let result
try {
result = await execa(invocation.command, invocation.args, {
cwd: root, stdio: 'inherit', reject: false, killDescendants: true, cancelSignal: signal,
})
} catch (error) {
if (signal.aborted) throw new Interrupted(label)
throw error
}
if (result.isCanceled || signal.aborted) throw new Interrupted(label)
if (result.exitCode !== 0) throw new Error(`${label} exited with ${String(result.exitCode ?? result.signal)}`)
}
/**
* Print the installation summary the developer copies from.
* Only the catalog roots are installable specs; the rest of the closure is a
* dependency the installer resolves itself and stays in the run manifest.
* @param options - run directory, registry URL, catalog roots, packed artifacts, source evidence.
*/
function printSummary(options: {
readonly directory: string
readonly url: string
readonly entries: readonly string[]
readonly artifacts: readonly BundleArtifact[]
readonly source: ReturnType<typeof sourceEvidence>
}): void {
const roots = options.entries.flatMap(name => options.artifacts.filter(artifact => artifact.name === name))
process.stdout.write(`Source: ${options.source.commit}${options.source.dirty ? ' (dirty worktree)' : ''}, DSH ${options.source.dshVersion}\n`)
process.stdout.write(`Artifacts: ${options.directory}\n`)
process.stdout.write(`Registry: ${options.url}\n`)
process.stdout.write(`Packages (${String(options.artifacts.length)} archives served):\n`)
for (const artifact of roots) process.stdout.write(` ${artifact.name}@${artifact.version}\n`)
process.stdout.write('In Plugins > Add plugin, use an exact package above and this custom registry URL.\n')
process.stdout.write('Keep this process running during package operations. Ctrl+C stops the registry.\n')
}
/** Build one run directory name: sortable by time and unique per invocation. */
function runId(): string {
return `${new Date().toISOString().replace(/[:.]/gu, '-')}-${randomUUID().slice(0, 8)}`
}
/** Build, pack, and serve the local bundle closure until a signal stops the run. */
async function main(): Promise<void> {
const root = resolve(import.meta.dirname, '..')
const abort = new AbortController()
// A persistent listener, so a second signal during shutdown cannot fall back to the default kill;
// the second signal is an explicit forced exit instead.
let stopping = false
const onSignal = (signal: NodeJS.Signals): void => {
if (stopping) {
process.exit(130)
return
}
stopping = true
abort.abort(signal)
}
process.on('SIGINT', onSignal)
process.on('SIGTERM', onSignal)
let registry
try {
// 1. The catalog is the only root list; the inventory is how a local package is distinguished from an external one.
const entries = generateOfficialBundleCatalog(root).map(entry => entry.packageName)
const packages = readWorkspacePackages(root)
const selection = selectBundleDependencies({ entries, packages, platform: hostPlatform() })
const release = readReleaseMembers(root)
verifyReleaseVersions(root)
// 2. Current official artifacts, so the run never serves an earlier build.
await runStep(root, ['run', 'build:official'], abort.signal, 'build:official')
verifyBuiltBundles({ entries, packages })
// 3. One immutable, run-owned directory; a later run never overwrites these bytes.
const buildId = runId()
const directory = join(root, REGISTRY_OUTPUT_ROOT, buildId)
mkdirSync(directory, { recursive: true })
const artifacts = await packBundleArtifacts({
root, destination: directory, members: selection.members, release, signal: abort.signal,
})
// 4. The manifest is the completion marker: only a fully packed run has one.
const source = sourceEvidence(root, getDshRuntimeVersion())
const manifest = createRegistryManifest({
buildId, source, entries, workspaceNames: selection.workspaceNames, artifacts,
})
await writeRegistryManifest(join(directory, REGISTRY_MANIFEST_FILE), manifest)
// 5. Serve, then announce: the summary is never printed for a registry that did not listen.
registry = await startBundleRegistry({
directory, artifacts, workspaceNames: selection.workspaceNames,
})
printSummary({ directory, url: registry.url, entries, artifacts, source })
await new Promise<void>((resolveStop) => {
if (abort.signal.aborted) resolveStop()
else abort.signal.addEventListener('abort', () => { resolveStop() }, { once: true })
})
await registry.close()
registry = undefined
process.stdout.write('Registry stopped. Artifacts and installed packages stay on disk.\n')
} catch (error) {
if (registry !== undefined) await registry.close()
// A signal that stopped packing or packing's child is the same stop as a stopped build step.
if (error instanceof Interrupted || abort.signal.aborted) {
const stage = error instanceof Interrupted ? error.message : 'interrupted during preparation'
process.stderr.write(`dev:bundle-registry: ${stage}\n`)
process.exitCode = 130
return
}
throw error
} finally {
process.off('SIGINT', onSignal)
process.off('SIGTERM', onSignal)
}
}
await main()
+559
View File
@@ -0,0 +1,559 @@
/** Behavior of the local Official-bundle registry: closure selection, archive validation, HTTP, and shutdown. */
import { createHash } from 'node:crypto'
import { connect } from 'node:net'
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execa } from 'execa'
import { describe, expect, it, onTestFinished } from 'vitest'
import { ON_DEMAND_BUNDLES } from '../packages/boot/app-boot/src/official-bundle-packages.ts'
import { generateOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
import {
createRegistryManifest,
hostPlatform,
pnpmInvocationFor,
readPackedArtifacts,
readReleaseMembers,
readWorkspacePackages,
selectBundleDependencies,
packBundleArtifacts,
startBundleRegistry,
writeRegistryManifest,
type BundleArtifact,
type ReleaseEntry,
type WorkspacePackage,
} from './local-bundle-registry.ts'
import { releaseFamily, type ReleaseMember } from './release/families.ts'
import { capture } from './release/process.ts'
const root = fileURLToPath(new URL('..', import.meta.url))
/** A private temporary root this spec owns, removed with the test. */
function temporaryDirectory(prefix: string): string {
const directory = realpathSync(mkdtempSync(join(tmpdir(), prefix)))
onTestFinished(() => { rmSync(directory, { recursive: true, force: true }) })
return directory
}
/** One fabricated workspace package with a `1.0.0` version line. */
function workspacePackage(name: string, manifest: Record<string, unknown>): WorkspacePackage {
return { name, directory: `/workspace/${name.replace('@', '').replace('/', '-')}`, manifest: { name, version: '1.0.0', ...manifest } }
}
/** An inventory of fabricated packages, keyed the way discovery keys them. */
function inventory(...members: WorkspacePackage[]): Map<string, WorkspacePackage> {
return new Map(members.map(member => [member.name, member]))
}
/** One fabricated package's archive together with the identities needed to read it back. */
interface Fixture {
readonly member: WorkspacePackage
readonly entry: ReleaseEntry
}
/**
* Pack one fabricated package archive into a run directory.
* @param options - destination, identity, source text, dependency ranges, and an optional declared bundle patch.
* @returns The workspace identity and the release entry its tarball must satisfy.
*/
function packFixture(options: {
readonly destination: string
readonly name: string
readonly version: string
readonly source: string
readonly dependencies?: Record<string, string>
readonly patch?: string
}): Fixture {
const staging = join(options.destination, `${options.name.replace('@', '').replace('/', '-')}-staging`)
const manifest: Record<string, unknown> = {
name: options.name,
version: options.version,
main: 'lib/index.js',
...options.dependencies === undefined ? {} : { dependencies: options.dependencies },
...options.patch === undefined ? {} : { dsh: { bundle: { patch: options.patch } } },
}
mkdirSync(join(staging, 'package', 'lib'), { recursive: true })
writeFileSync(join(staging, 'package', 'package.json'), JSON.stringify(manifest))
writeFileSync(join(staging, 'package', 'lib', 'index.js'), options.source)
if (options.patch !== undefined) writeFileSync(join(staging, 'package', options.patch), '[]\n')
const file = `${options.name.replace('@', '').replace('/', '-')}-${options.version}.tgz`
capture('tar', ['-czf', join(options.destination, file), '-C', staging, 'package'])
const releaseMember: ReleaseMember = { directory: staging, name: options.name, version: options.version, manifest }
return {
member: workspacePackage(options.name, { version: options.version, ...manifest }),
entry: { family: releaseFamily('vendor'), member: releaseMember },
}
}
/**
* Pack a fixture archive and read it back through the real archive validation.
* @param options - the fixture to pack.
* @returns The servable artifact and the identities it was packed from.
*/
function fabricate(options: Parameters<typeof packFixture>[0]): Fixture & { readonly artifact: BundleArtifact } {
const fixture = packFixture(options)
const [artifact] = readPackedArtifacts({
destination: options.destination,
members: [fixture.member],
release: new Map([[fixture.member.name, fixture.entry]]),
})
if (artifact === undefined) throw new Error('fabricated artifact was not read back')
return { ...fixture, artifact }
}
/** Start a registry for one fabricated run, closed with the test. */
async function servingRun(options: {
readonly destination: string
readonly artifacts: readonly BundleArtifact[]
readonly workspaceNames: readonly string[]
}): Promise<string> {
const registry = await startBundleRegistry({
directory: options.destination, artifacts: options.artifacts, workspaceNames: options.workspaceNames,
})
onTestFinished(() => registry.close())
return registry.url
}
describe('catalog and local closure', () => {
it('selects the real catalog closure from this checkout', () => {
const entries = generateOfficialBundleCatalog(root).map(entry => entry.packageName)
expect(entries).toEqual([...ON_DEMAND_BUNDLES])
const packages = readWorkspacePackages(root)
const release = readReleaseMembers(root)
const selection = selectBundleDependencies({ entries, packages, platform: hostPlatform() })
expect(selection.members.map(member => member.name)).toEqual(expect.arrayContaining([...entries]))
for (const member of selection.members) {
expect(member.manifest.private, member.name).not.toBe(true)
expect(release.has(member.name), member.name).toBe(true)
}
expect(selection.workspaceNames).toEqual(expect.arrayContaining(selection.members.map(member => member.name)))
// A vendor package keeps its own version line, so a run must not assume one shared version.
expect(packages.get('@deepseek-ai/cordis')?.manifest.version)
.not.toBe(packages.get('@deepseek-ai/dsh-brand')?.manifest.version)
// Nothing in this closure is built for another platform, on any host.
expect(selection.foreignTargets).toEqual([])
})
it('follows install, optional, and required peer edges but not optional peers', () => {
const selection = selectBundleDependencies({
entries: ['@scope/root'],
packages: inventory(
workspacePackage('@scope/root', {
dependencies: { '@scope/required': 'workspace:*' },
optionalDependencies: { '@scope/optional': 'workspace:*' },
peerDependencies: { '@scope/peer': 'workspace:*', '@scope/unused-peer': 'workspace:*' },
peerDependenciesMeta: { '@scope/unused-peer': { optional: true } },
}),
workspacePackage('@scope/required', {}),
workspacePackage('@scope/optional', { dependencies: { '@scope/transitive': 'workspace:*' } }),
workspacePackage('@scope/peer', {}),
workspacePackage('@scope/transitive', {}),
workspacePackage('@scope/unused-peer', {}),
workspacePackage('@scope/unreferenced', {}),
),
platform: { os: ['darwin'] },
})
expect(selection.members.map(member => member.name)).toEqual([
'@scope/optional', '@scope/peer', '@scope/required', '@scope/root', '@scope/transitive',
])
})
it('leaves a dependency that is not a workspace package to its own registry', () => {
const selection = selectBundleDependencies({
entries: ['@scope/root'],
packages: inventory(workspacePackage('@scope/root', {
dependencies: { '@other/external': '^1.0.0', 'plain-external': '^2.0.0' },
})),
platform: {},
})
expect(selection.members.map(member => member.name)).toEqual(['@scope/root'])
})
it('fails when a catalog root is not a workspace package', () => {
expect(() => selectBundleDependencies({ entries: ['@scope/missing'], packages: inventory(), platform: {} }))
.toThrow(/cannot resolve workspace package @scope\/missing \(catalog entry @scope\/missing\)/u)
})
it('rejects an empty or duplicate catalog', () => {
expect(() => selectBundleDependencies({ entries: [], packages: inventory(), platform: {} }))
.toThrow(/no on-demand entries/u)
expect(() => selectBundleDependencies({ entries: ['@scope/a', '@scope/a'], packages: inventory(), platform: {} }))
.toThrow(/must be distinct/u)
})
it('fails on a required edge that targets another platform, naming the path', () => {
expect(() => selectBundleDependencies({
entries: ['@scope/root'],
packages: inventory(
workspacePackage('@scope/root', { dependencies: { '@scope/native-win': 'workspace:*' } }),
workspacePackage('@scope/native-win', { os: ['win32'] }),
),
platform: { os: ['darwin'], cpu: ['arm64'] },
})).toThrow(/@scope\/root -> @scope\/native-win requires @scope\/native-win, which targets os=win32; this host is os=darwin cpu=arm64/u)
})
it('keeps an optional-only foreign-target member and reports it', () => {
const selection = selectBundleDependencies({
entries: ['@scope/root'],
packages: inventory(
workspacePackage('@scope/root', { optionalDependencies: { '@scope/native-win': 'workspace:*' } }),
workspacePackage('@scope/native-win', { os: ['win32'] }),
),
platform: { os: ['darwin'] },
})
expect(selection.members.map(member => member.name)).toEqual(['@scope/native-win', '@scope/root'])
expect(selection.foreignTargets).toEqual(['@scope/native-win'])
})
})
describe('packed archives', () => {
it('describes exactly the bytes it will serve', () => {
const destination = temporaryDirectory('dsh-local-registry-archive-')
const { artifact } = fabricate({ destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n' })
const bytes = readFileSync(artifact.archive)
expect(artifact.bytes).toBe(bytes.byteLength)
expect(artifact.integrity).toBe(`sha512-${createHash('sha512').update(bytes).digest('base64')}`)
expect(artifact.shasum).toBe(createHash('sha1').update(bytes).digest('hex'))
expect(artifact.file).toBe('scope-pkg-1.0.0.tgz')
expect(artifact.platform).toEqual({})
expect(artifact.manifest.version).toBe('1.0.0')
})
it('rejects an archive that lacks the main it declares', () => {
const destination = temporaryDirectory('dsh-local-registry-payload-')
const staging = join(destination, 'staging')
mkdirSync(join(staging, 'package'), { recursive: true })
writeFileSync(join(staging, 'package', 'package.json'), JSON.stringify({
name: '@scope/pkg', version: '1.0.0', main: 'lib/index.js', dsh: { bundle: { patch: 'cordis.patch.yml' } },
}))
capture('tar', ['-czf', join(destination, 'scope-pkg-1.0.0.tgz'), '-C', staging, 'package'])
const member = workspacePackage('@scope/pkg', { version: '1.0.0' })
const entry: ReleaseEntry = {
family: releaseFamily('vendor'), member: { directory: staging, name: '@scope/pkg', version: '1.0.0', manifest: member.manifest },
}
expect(() => readPackedArtifacts({ destination, members: [member], release: new Map([['@scope/pkg', entry]]) }))
.toThrow(/@scope\/pkg: packed archive lacks its declared main lib\/index\.js/u)
})
it('rejects a bundle whose patch is missing from its archive', () => {
const destination = temporaryDirectory('dsh-local-registry-patch-')
const { member, entry } = packFixture({
destination, name: '@scope/bundle', version: '1.0.0', source: 'export default 1\n', patch: 'cordis.patch.yml',
})
const archive = join(destination, 'scope-bundle-1.0.0.tgz')
const stripped = join(destination, 'stripped')
mkdirSync(join(stripped, 'package'), { recursive: true })
writeFileSync(join(stripped, 'package', 'package.json'), JSON.stringify({
name: '@scope/bundle', version: '1.0.0', main: 'lib/index.js', dsh: { bundle: { patch: 'cordis.patch.yml' } },
}))
mkdirSync(join(stripped, 'package', 'lib'), { recursive: true })
writeFileSync(join(stripped, 'package', 'lib', 'index.js'), 'export default 1\n')
capture('tar', ['-czf', archive, '-C', stripped, 'package'])
expect(() => readPackedArtifacts({ destination, members: [member], release: new Map([['@scope/bundle', entry]]) }))
.toThrow(/@scope\/bundle: packed archive lacks its declared bundle patch cordis\.patch\.yml/u)
})
it('reports a preparation stopped by a signal as a cancellation', async () => {
const destination = temporaryDirectory('dsh-local-registry-cancel-')
const { member, entry } = packFixture({ destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n' })
const controller = new AbortController()
controller.abort()
await expect(packBundleArtifacts({
root, destination, members: [member], release: new Map([[member.name, entry]]), signal: controller.signal,
})).rejects.toThrow(/packing was cancelled/u)
})
it('rejects a missing archive and a package that is not a public release member', () => {
const destination = temporaryDirectory('dsh-local-registry-missing-')
const missing = workspacePackage('@scope/pkg', { version: '1.0.0' })
const entry: ReleaseEntry = {
family: releaseFamily('vendor'), member: { directory: destination, name: '@scope/pkg', version: '1.0.0', manifest: missing.manifest },
}
expect(() => readPackedArtifacts({ destination, members: [missing], release: new Map([['@scope/pkg', entry]]) }))
.toThrow(/@scope\/pkg produced no tarball/u)
fabricate({ destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n' })
expect(() => readPackedArtifacts({ destination, members: [missing], release: new Map() }))
.toThrow(/not a public release member/u)
})
it('rejects a packed manifest that keeps a workspace protocol', () => {
const destination = temporaryDirectory('dsh-local-registry-protocol-')
const { member, entry } = packFixture({
destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n',
dependencies: { '@scope/dependency': 'workspace:*' },
})
// The fixture writes the manifest verbatim, so a protocol a real pack rewrites is still observable here.
expect(() => readPackedArtifacts({ destination, members: [member], release: new Map([['@scope/pkg', entry]]) }))
.toThrow(/packed dependencies\.@scope\/dependency still declares workspace:\*/u)
})
it('rejects a packed local dependency this run does not serve at the required version', () => {
const destination = temporaryDirectory('dsh-local-registry-version-')
const consumer = packFixture({
destination, name: '@scope/pkg', version: '1.0.0', source: 'export default 1\n',
dependencies: { '@scope/dependency': '^2.0.0' },
})
const dependency = packFixture({ destination, name: '@scope/dependency', version: '1.0.0', source: 'export default 2\n' })
const release = new Map([[consumer.member.name, consumer.entry], [dependency.member.name, dependency.entry]])
expect(() => readPackedArtifacts({ destination, members: [consumer.member, dependency.member], release }))
.toThrow(/@scope\/pkg: packed dependencies\.@scope\/dependency requires \^2\.0\.0, but this run serves 1\.0\.0/u)
})
})
describe('registry HTTP', () => {
it('answers scoped packuments, version documents, and dist tags for its own artifacts', async () => {
const destination = temporaryDirectory('dsh-local-registry-http-')
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.2.3-alpha.1', source: 'export default 1\n' })
const url = await servingRun({ destination, artifacts: [artifact], workspaceNames: ['@scope/local', '@scope/absent'] })
// pnpm encodes the scope separator; an unencoded path names the same package.
for (const path of ['@scope%2flocal', '@scope/local']) {
const response = await fetch(`${url}${path}`)
expect(response.status, path).toBe(200)
const packument = await response.json() as {
name: string
'dist-tags': Record<string, string>
versions: Record<string, { dist: { tarball: string; integrity: string } }>
time: Record<string, string>
}
expect(packument.name).toBe('@scope/local')
expect(packument['dist-tags']).toEqual({ latest: '1.2.3-alpha.1', next: '1.2.3-alpha.1' })
expect(packument.versions['1.2.3-alpha.1']?.dist.tarball).toBe(`${url}tarballs/scope-local-1.2.3-alpha.1.tgz`)
expect(packument.versions['1.2.3-alpha.1']?.dist.integrity).toBe(artifact.integrity)
// The only publication time a run can claim is when its own archives were packed.
expect(packument.time['1.2.3-alpha.1']).toBe(artifact.packedAt)
}
for (const path of ['@scope%2flocal/1.2.3-alpha.1', '@scope/local/latest', '@scope/local/next']) {
const response = await fetch(`${url}${path}`)
expect(response.status, path).toBe(200)
expect(await response.json()).toMatchObject({ name: '@scope/local', version: '1.2.3-alpha.1' })
}
const unknown = await fetch(`${url}@scope%2flocal/9.9.9`)
expect(unknown.status).toBe(404)
expect(await unknown.json()).toMatchObject({ reason: 'this run serves @scope/[email protected] only' })
})
it('serves exactly the archived bytes, and no body for HEAD', async () => {
const destination = temporaryDirectory('dsh-local-registry-tarball-')
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
const url = await servingRun({ destination, artifacts: [artifact], workspaceNames: ['@scope/local'] })
const response = await fetch(`${url}tarballs/${artifact.file}`)
expect(response.status).toBe(200)
expect(Buffer.from(await response.arrayBuffer())).toEqual(readFileSync(artifact.archive))
const head = await fetch(`${url}tarballs/${artifact.file}`, { method: 'HEAD' })
expect(head.status).toBe(200)
expect(head.headers.get('content-length')).toBe(String(artifact.bytes))
expect(await head.text()).toBe('')
expect((await fetch(`${url}tarballs/no-such-package-1.0.0.tgz`)).status).toBe(404)
})
it('keeps a repository-owned miss local and redirects only external packages', async () => {
const destination = temporaryDirectory('dsh-local-registry-miss-')
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
const url = await servingRun({
destination, artifacts: [artifact], workspaceNames: ['@scope/local', '@deepseek-ai/dsh-private-thing'],
})
const owned = await fetch(`${url}@deepseek-ai%2fdsh-private-thing`)
expect(owned.status).toBe(404)
const refusal = await owned.json() as { reason?: string }
expect(refusal.reason ?? '').toContain('repository-owned')
// A repository-owned name that is not even in the closure must never reach npm.
expect((await fetch(`${url}@deepseek-ai%2fdsh-private-thing/1.0.0`)).status).toBe(404)
const external = await fetch(`${url}@anthropic-ai%2fclaude-agent-sdk`, { redirect: 'manual' })
expect(external.status).toBe(302)
expect(external.headers.get('location')).toBe('https://registry.npmjs.org/@anthropic-ai%2fclaude-agent-sdk')
})
it('keeps serving after a client abandons a tarball request', async () => {
const destination = temporaryDirectory('dsh-local-registry-disconnect-')
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
const url = await servingRun({ destination, artifacts: [artifact], workspaceNames: ['@scope/local'] })
const { port, pathname } = new URL(url)
const socket = connect(Number(port), '127.0.0.1')
await new Promise<void>((resolveConnect, rejectConnect) => {
socket.once('connect', resolveConnect)
socket.once('error', rejectConnect)
})
socket.write(`GET ${pathname}tarballs/${artifact.file} HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\n\r\n`)
await new Promise<void>((resolveData) => { socket.once('data', () => { resolveData() }) })
socket.destroy()
const response = await fetch(`${url}@scope%2flocal`)
expect(response.status).toBe(200)
expect(await response.json()).toMatchObject({ name: '@scope/local' })
})
it('rejects out-of-namespace, malformed, and non-GET requests, then stops listening', async () => {
const destination = temporaryDirectory('dsh-local-registry-reject-')
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
const registry = await startBundleRegistry({ directory: destination, artifacts: [artifact], workspaceNames: ['@scope/local'] })
onTestFinished(() => registry.close())
const { origin, pathname } = new URL(registry.url)
expect((await fetch(`${origin}/@scope%2flocal`)).status).toBe(404)
expect((await fetch(`${origin}${pathname}%zz`)).status).toBe(404)
expect((await fetch(`${registry.url}@scope%2flocal/%2e%2e%2f%2e%2e%2fetc%2fpasswd`)).status).toBe(404)
expect((await fetch(`${registry.url}@scope%2flocal/1.0.0/extra`)).status).toBe(404)
expect((await fetch(`${registry.url}tarballs/%2e%2e%2f%2e%2e%2fetc%2fpasswd`)).status).toBe(404)
expect((await fetch(`${registry.url}-/ping`)).status).toBe(404)
expect((await fetch(registry.url, { method: 'POST', body: 'x' })).status).toBe(405)
const other = await startBundleRegistry({ directory: destination, artifacts: [artifact], workspaceNames: ['@scope/local'] })
onTestFinished(() => other.close())
expect(other.url).not.toBe(registry.url)
await registry.close()
await expect(fetch(`${registry.url}@scope%2flocal`)).rejects.toThrow()
})
})
describe('run record', () => {
it('names the run, its membership, and the integrity of every served archive', async () => {
const destination = temporaryDirectory('dsh-local-registry-manifest-')
const { artifact } = fabricate({ destination, name: '@scope/local', version: '1.0.0', source: 'export default 1\n' })
const manifest = createRegistryManifest({
buildId: 'run-1',
source: { commit: 'a'.repeat(40), dirty: true, dshVersion: '0.0.0-test' },
entries: ['@scope/local'],
workspaceNames: ['@scope/local', '@scope/absent'],
artifacts: [artifact],
})
expect(manifest).toEqual({
schemaVersion: 1,
buildId: 'run-1',
source: { commit: 'a'.repeat(40), dirty: true, dshVersion: '0.0.0-test' },
entries: ['@scope/local'],
workspaceNames: ['@scope/local', '@scope/absent'],
packages: [{ name: '@scope/local', version: '1.0.0', file: artifact.file, bytes: artifact.bytes, integrity: artifact.integrity }],
})
const path = join(destination, 'bundle-registry.json')
await writeRegistryManifest(path, manifest)
expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual(manifest)
expect(readFileSync(path, 'utf8').endsWith('}\n')).toBe(true)
})
})
/**
* Run one pnpm command in a profile-shaped directory, terminating and awaiting it with the test.
* @param options - consumer directory, environment, the store and cache directories an installing
* command shares, and the arguments to append.
* @returns The settled child result.
*/
async function pnpmIn(options: {
readonly consumer: string
readonly environment: NodeJS.ProcessEnv
readonly store?: string
readonly cache?: string
readonly args: readonly string[]
}): Promise<{ exitCode?: number | null; stdout: string; stderr: string; timedOut: boolean }> {
const invocation = pnpmInvocationFor([
...options.args,
...options.store === undefined ? [] : ['--store-dir', options.store],
...options.cache === undefined ? [] : ['--cache-dir', options.cache],
'--config.fetch-retries=0',
])
const child = execa(invocation.command, invocation.args, {
cwd: options.consumer, env: options.environment, extendEnv: false, reject: false, timeout: 240_000,
})
onTestFinished(async () => {
child.kill('SIGKILL')
await child.catch(() => undefined)
})
return child
}
/** Prepare one isolated profile-shaped consumer directory. */
function consumerDirectory(parent: string, name: string): string {
const consumer = join(parent, name)
mkdirSync(consumer, { recursive: true })
writeFileSync(join(consumer, 'package.json'), JSON.stringify({ name, private: true, dependencies: {} }))
writeFileSync(join(consumer, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
return consumer
}
/** The payload a consumer's installed fixture carries, or a marker when nothing is installed. */
function installedPayload(consumer: string): string {
try { return readFileSync(join(consumer, 'node_modules', '@dsh-local', 'fixture', 'lib', 'index.js'), 'utf8').trim() }
catch { return '(absent)' }
}
describe('package cache freshness', () => {
it('installs a rebuilt archive at the same version through a retained pnpm store', { timeout: 300_000 }, async () => {
const temporary = temporaryDirectory('dsh-local-registry-cache-')
const store = join(temporary, 'store')
const cache = join(temporary, 'cache')
const userConfig = join(temporary, 'empty.npmrc')
writeFileSync(userConfig, '')
// A retained store, metadata cache, and npm configuration, so the second installation cannot pass by starting empty.
const environment = { ...process.env, npm_config_userconfig: userConfig }
const install = async (run: string, source: string): Promise<{ payload: string; settings: string }> => {
const destination = join(temporary, run, 'artifacts')
mkdirSync(destination, { recursive: true })
const { artifact } = fabricate({ destination, name: '@dsh-local/fixture', version: '1.0.0', source })
const registry = await startBundleRegistry({
directory: destination, artifacts: [artifact], workspaceNames: ['@dsh-local/fixture'],
})
onTestFinished(() => registry.close())
const consumer = consumerDirectory(temporary, `consumer-${run}`)
const result = await pnpmIn({
consumer, environment, store, cache,
args: ['add', '@dsh-local/[email protected]', `--registry=${registry.url}`],
})
expect(result.timedOut, `${result.stdout}\n${result.stderr}`).toBe(false)
expect(result.exitCode, `${result.stdout}\n${result.stderr}`).toBe(0)
await registry.close()
return { payload: installedPayload(consumer), settings: readFileSync(join(consumer, 'pnpm-workspace.yaml'), 'utf8') }
}
const first = await install('first', 'export default "first build"\n')
expect(first.payload).toContain('first build')
// pnpm 11's default minimumReleaseAge is 24 hours, and its non-strict mode records the exemption it granted
// instead of faking an older publication time for a locally packed artifact.
expect(first.settings).toContain('minimumReleaseAgeExclude')
expect(first.settings).toContain('@dsh-local/[email protected]')
const second = await install('second', 'export default "second build"\n')
expect(second.payload).toContain('second build')
})
})
describe('same-version iteration', () => {
it('needs the earlier run\'s stale resolution cleared before a new run at the same version installs', { timeout: 300_000 }, async () => {
const temporary = temporaryDirectory('dsh-local-registry-iteration-')
const store = join(temporary, 'store')
const userConfig = join(temporary, 'empty.npmrc')
writeFileSync(userConfig, '')
const environment = { ...process.env, npm_config_userconfig: userConfig }
const consumer = consumerDirectory(temporary, 'profile')
const serve = async (tag: string, payload: string) => {
const destination = join(temporary, tag)
mkdirSync(destination, { recursive: true })
const { artifact } = fabricate({ destination, name: '@dsh-local/fixture', version: '1.0.0', source: `export default ${payload}\n` })
return startBundleRegistry({ directory: destination, artifacts: [artifact], workspaceNames: ['@dsh-local/fixture'] })
}
const add = (url: string) => pnpmIn({ consumer, environment, store, args: ['add', '@dsh-local/[email protected]', `--registry=${url}`] })
const first = await serve('run-a', '"A"')
expect((await add(first.url)).exitCode).toBe(0)
expect(installedPayload(consumer)).toContain('"A"')
await first.close()
// A new run answers at a new URL, so the recorded tarball no longer verifies against it.
const second = await serve('run-b', '"B"')
onTestFinished(() => second.close())
const refused = await add(second.url)
expect(refused.exitCode).not.toBe(0)
expect(`${refused.stdout}${refused.stderr}`).toContain('ERR_PNPM_TARBALL_URL_MISMATCH')
expect(installedPayload(consumer)).toContain('"A"')
// pnpm's own remedy for that error: drop the stale resolution, then resolve again from the new run.
const cleaned = await pnpmIn({ consumer, environment, args: ['clean', '--lockfile'] })
expect(cleaned.exitCode, `${cleaned.stdout}\n${cleaned.stderr}`).toBe(0)
const reinstalled = await add(second.url)
expect(reinstalled.exitCode, `${reinstalled.stdout}\n${reinstalled.stderr}`).toBe(0)
expect(installedPayload(consumer)).toContain('"B"')
})
})
+929
View File
@@ -0,0 +1,929 @@
/**
* Pack the local dependency closure of the Official on-demand bundles and serve
* it as a loopback npm registry.
*
* A packaged application installs an Official bundle as `name@<running DSH
* version>`, and the plugin manager asks a custom registry *alone*
* ([registry plan](../packages/boot/plugin-manager/src/registry.ts)). Every
* transitive dependency of the packed bundles must therefore be answerable by
* this process: repository-owned packages come from the run's own artifacts,
* and genuinely external packages keep npm's real metadata and bytes through a
* fixed redirect. Publication is untouched; only one development run's
* transport is local.
*/
import { createHash, randomUUID } from 'node:crypto'
import { createReadStream, existsSync, globSync, readFileSync, statSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { createServer, type Server, type ServerResponse } from 'node:http'
import { join, resolve } from 'node:path'
import { execa } from 'execa'
import yaml from 'js-yaml'
import semver from 'semver'
import { pnpmInvocation } from './pnpm-invocation.ts'
import { capture, pnpmCommand } from './release/process.ts'
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './release/families.ts'
import { packedManifest, tarballFiles } from './release/tarball.ts'
/** Run directories a registry invocation owns, relative to the repository root. */
export const REGISTRY_OUTPUT_ROOT = 'dist/test-bundles'
/** Manifest filename whose presence marks a run directory as complete. */
export const REGISTRY_MANIFEST_FILE = 'bundle-registry.json'
/** Registry every external package is redirected to, with the request path preserved. */
export const UPSTREAM_REGISTRY = 'https://registry.npmjs.org/'
/** Installation edges a consumer cannot load without. */
const REQUIRED_SECTIONS = ['dependencies'] as const
/** Edges pnpm may skip on this host; their packages still need local metadata. */
const OPTIONAL_SECTIONS = ['optionalDependencies'] as const
/** Edges that constrain an installation without installing on the package's behalf. */
const PEER_SECTIONS = ['peerDependencies'] as const
/** One workspace package available to pack and serve. */
export interface WorkspacePackage {
/** Package name exactly as it is declared. */
readonly name: string
/** Absolute package directory. */
readonly directory: string
/** Parsed `package.json`. */
readonly manifest: Record<string, unknown>
}
/** Platform constraints a package declares for its own artifacts. */
export interface PlatformConstraint {
/** Accepted `process.platform` values, which may be `!value` negations. */
readonly os?: readonly string[]
/** Accepted `process.arch` values, which may be `!value` negations. */
readonly cpu?: readonly string[]
/** Accepted libc values on Linux, which may be `!value` negations. */
readonly libc?: readonly string[]
}
/** One packed package archive the registry answers with. */
export interface BundleArtifact {
/** Package name from the packed manifest. */
readonly name: string
/** Packed version, served exactly. */
readonly version: string
/** Absolute archive path in the run directory. */
readonly archive: string
/** Tarball filename inside the run directory. */
readonly file: string
/** Archive size in bytes, as served. */
readonly bytes: number
/** SHA-512 subresource integrity of the served bytes. */
readonly integrity: string
/** SHA-1 hex digest of the served bytes. */
readonly shasum: string
/** Manifest contained in the archive, which the registry serves as the version metadata. */
readonly manifest: Record<string, unknown>
/** Platform constraints the packed manifest declares for itself. */
readonly platform: PlatformConstraint
/** Dist tag this version publishes under, from its release family. */
readonly distTag?: string
/** When this run packed the archive; the only publication time the run can honestly claim. */
readonly packedAt: string
}
/** The catalog roots together with the local packages an ordinary installation needs. */
export interface BundleSelection {
/** Catalog roots, in admission order. */
readonly entries: readonly string[]
/** Local closure, sorted by package name. */
readonly members: readonly WorkspacePackage[]
/** Every repository-owned name, so a request for one is a local miss rather than an upstream redirect. */
readonly workspaceNames: readonly string[]
/** Members built for another platform, kept so pnpm reads their metadata and applies its own skip. */
readonly foreignTargets: readonly string[]
}
/** What one run records beside its artifacts; the file is written only once the run is complete. */
export interface RegistryManifest {
/** Manifest layout version. */
readonly schemaVersion: 1
/** Unique run directory name. */
readonly buildId: string
/** Which bytes these artifacts were packed from. */
readonly source: {
/** Full Git object id of the packed checkout. */
readonly commit: string
/** Whether the checkout had uncommitted changes, so the commit alone is not the snapshot. */
readonly dirty: boolean
/** Running DSH version the catalog installation target names. */
readonly dshVersion: string
}
/** Catalog roots in admission order. */
readonly entries: readonly string[]
/** Every repository-owned name. */
readonly workspaceNames: readonly string[]
/** Packed packages, sorted by name. */
readonly packages: readonly {
readonly name: string
readonly version: string
readonly file: string
readonly bytes: number
readonly integrity: string
}[]
}
/** A running loopback registry serving one immutable run directory. */
export interface BundleRegistry {
/** Registry URL ending in the run's fresh namespace and a slash, as `--registry` takes it. */
readonly url: string
/** Stop dispatching, drop open connections, and resolve once the listener is gone. */
close(): Promise<void>
}
/** One public release member and the family that owns its payload rules. */
export interface ReleaseEntry {
/** Family owning this member's payload and version rules. */
readonly family: ReleaseFamily
/** The member as its family publishes it. */
readonly member: ReleaseMember
}
/**
* Resolve pnpm for a shell-free child process.
*
* A package script's own pnpm is authoritative, because it owns the workspace
* lockfile and store; a direct invocation (`pnpm exec`, a test runner) has no
* lifecycle environment, so the workspace's own pnpm dependency runs instead.
* @param args - pnpm arguments.
* @returns A command and argument array suitable for `spawn` without a shell.
*/
export function pnpmInvocationFor(args: readonly string[]): { command: string; args: string[] } {
const entrypoint = process.env.npm_execpath
// `npm run` and `yarn run` set the same variable; handing them pnpm's arguments would write a different lockfile.
if (entrypoint !== undefined && /[\\/]pnpm[\\/]/u.test(entrypoint) && /\.[cm]?js$/u.test(entrypoint)) {
return pnpmInvocation(args)
}
const [command, ...prefix] = pnpmCommand()
return { command, args: [...prefix, ...args] }
}
/**
* Read the pnpm workspace inventory.
* @param root - repository root containing `pnpm-workspace.yaml`.
* @returns Workspace packages indexed by name.
*/
export function readWorkspacePackages(root: string): Map<string, WorkspacePackage> {
const settings: unknown = yaml.load(readFileSync(join(root, 'pnpm-workspace.yaml'), 'utf8'))
if (!isRecord(settings)) throw new Error('pnpm-workspace.yaml must be a mapping')
const patterns = settings.packages
if (!Array.isArray(patterns) || patterns.length === 0 || !patterns.every(value => typeof value === 'string')) {
throw new Error('pnpm-workspace.yaml must list package patterns')
}
const packages = new Map<string, WorkspacePackage>()
const paths = globSync(patterns.map(pattern => `${pattern}/package.json`), { cwd: root })
.map(path => path.replaceAll('\\', '/'))
.sort()
for (const path of paths) {
const manifest = manifestAt(join(root, path))
const name = manifest.name
if (typeof name !== 'string' || name === '') throw new Error(`${path} must declare a string name`)
if (packages.has(name)) throw new Error(`duplicate workspace package ${name}`)
packages.set(name, { name, directory: resolve(root, path, '..'), manifest })
}
if (packages.size === 0) throw new Error('the pnpm workspace inventory is empty')
return packages
}
/**
* Index the release families' public members by package name.
* @param root - repository root containing the release families.
* @returns Public release members keyed by package name.
*/
export function readReleaseMembers(root: string): Map<string, ReleaseEntry> {
const members = new Map<string, ReleaseEntry>()
for (const id of ['dsh', 'vendor']) {
const family = releaseFamily(id)
for (const member of family.members(root)) members.set(member.name, { family, member })
}
return members
}
/**
* Assert each family's own version baseline, which is what one tag publishes.
* @param root - repository root containing the release families.
*/
export function verifyReleaseVersions(root: string): void {
for (const id of ['dsh', 'vendor']) {
const family = releaseFamily(id)
family.verifyVersions(family.members(root))
}
}
/**
* Require every catalog root to be a built, patch-carrying bundle in this checkout.
*
* The catalog generator already checks admission metadata; this checks the
* artifacts a packed consumer would actually load, so a stale or partial build
* fails before any archive is served.
* @param options - catalog roots in admission order and the workspace inventory.
*/
export function verifyBuiltBundles(options: {
readonly entries: readonly string[]
readonly packages: ReadonlyMap<string, WorkspacePackage>
}): void {
for (const name of options.entries) {
const member = options.packages.get(name)
if (member === undefined) throw new Error(`catalog entry ${name} is not a workspace package`)
const main = member.manifest.main
if (typeof main !== 'string' || main === '') throw new Error(`${name} declares no package main to load`)
const entry = resolve(member.directory, main)
if (!existsSync(entry) || !statSync(entry).isFile()) {
throw new Error(`${name}: built runtime is missing at ${entry}; run pnpm run build:official first`)
}
const dsh = member.manifest.dsh
if (!isRecord(dsh) || !isRecord(dsh.bundle)) throw new Error(`${name} declares no dsh.bundle`)
const patches = declaredBundlePatches(member.manifest)
if (patches.length === 0) throw new Error(`${name}: dsh.bundle.patch must be a file path or a list of file paths`)
for (const patch of patches) {
const path = join(member.directory, patch)
if (!existsSync(path) || !statSync(path).isFile()) throw new Error(`${name}: bundle patch ${path} is missing`)
}
}
}
/**
* Read the platform constraints a manifest declares for its own artifacts.
* @param manifest - package manifest.
* @returns The declared `os`, `cpu`, and `libc` lists.
*/
export function platformConstraint(manifest: Record<string, unknown>): PlatformConstraint {
const lists: { os?: readonly string[]; cpu?: readonly string[]; libc?: readonly string[] } = {}
for (const field of ['os', 'cpu', 'libc'] as const) {
const value = manifest[field]
if (Array.isArray(value) && value.every(entry => typeof entry === 'string')) lists[field] = value
}
return lists
}
/**
* This host's platform values, for comparing against a package's own constraints.
* @returns Host `os`, `cpu`, and libc.
*/
export function hostPlatform(): PlatformConstraint {
if (process.platform !== 'linux') return { os: [process.platform], cpu: [process.arch] }
const report = process.report.getReport() as { header?: { glibcVersionRuntime?: string } }
return { os: [process.platform], cpu: [process.arch], libc: [report.header?.glibcVersionRuntime === undefined ? 'musl' : 'glibc'] }
}
/**
* Whether a package's declared platform accepts this host.
* @param constraint - declared constraints.
* @param host - host `os`, `cpu`, and libc values.
* @returns True when the package may run here.
*/
export function matchesPlatform(constraint: PlatformConstraint, host: PlatformConstraint): boolean {
return allowsValue(constraint.os, host.os?.[0])
&& allowsValue(constraint.cpu, host.cpu?.[0])
&& allowsValue(constraint.libc, host.libc?.[0])
}
/**
* Select the catalog roots and the local packages an ordinary installation needs.
*
* Every dependency naming a workspace package is followed, whatever its section
* or protocol, so a locally owned requirement is never left to npm. A required
* edge naming a package built for another platform fails here, because the run
* cannot supply the artifact that installer would need; an optional edge is
* kept so pnpm still reads its metadata and applies its own platform skip.
* @param options - catalog roots, workspace inventory, and the host to judge platform edges against.
* @returns The closure, the repository-owned names, and the foreign-target members it kept.
*/
export function selectBundleDependencies(options: {
readonly entries: readonly string[]
readonly packages: ReadonlyMap<string, WorkspacePackage>
readonly platform: PlatformConstraint
}): BundleSelection {
const { entries, packages, platform } = options
if (entries.length === 0) throw new Error('the Official catalog has no on-demand entries')
if (new Set(entries).size !== entries.length) throw new Error('Official catalog entries must be distinct')
const selected = new Map<string, WorkspacePackage>()
const parents = new Map<string, string>()
const queued = new Set<string>()
const queue: string[] = []
const enqueue = (name: string, parent: string | undefined): void => {
if (selected.has(name) || queued.has(name)) return
queued.add(name)
if (parent !== undefined) parents.set(name, parent)
queue.push(name)
}
for (const entry of entries) enqueue(entry, undefined)
for (let index = 0; index < queue.length; index += 1) {
const name = queue[index] as string
const member = packages.get(name)
if (member === undefined) {
throw new Error(`local bundle closure cannot resolve workspace package ${name} (${dependencyPath(name, parents, entries)})`)
}
selected.set(name, member)
for (const name of installedEdges(member.manifest)) {
if (packages.has(name)) enqueue(name, member.name)
}
}
const foreign: string[] = []
for (const [name, member] of selected) {
if (matchesPlatform(platformConstraint(member.manifest), platform)) continue
if (reachedByRequiredEdge(name, entries, selected)) {
throw new Error(
`${dependencyPath(name, parents, entries)} requires ${name}, which targets`
+ ` ${describePlatform(platformConstraint(member.manifest))}; this host is ${describePlatform(platform)}.`
+ ' This run cannot supply a matching artifact for that package.',
)
}
foreign.push(name)
}
return {
entries: [...entries],
members: [...selected.values()].sort((left, right) => left.name.localeCompare(right.name)),
workspaceNames: [...packages.keys()].sort(),
foreignTargets: foreign.sort(),
}
}
/**
* Pack the selected packages into one run directory and validate every archive
* against the release family that owns it.
* @param options - repository root, destination, selected members, and the public release index.
* @returns Packed artifacts sorted by package name.
*/
export async function packBundleArtifacts(options: {
readonly root: string
readonly destination: string
readonly members: readonly WorkspacePackage[]
readonly release: ReadonlyMap<string, ReleaseEntry>
readonly signal?: AbortSignal
}): Promise<BundleArtifact[]> {
const { root, destination, members, release } = options
if (members.length === 0) throw new Error('no local packages were selected to pack')
await mkdir(destination, { recursive: true })
const invocation = pnpmInvocationFor([
'pack', '--recursive', `--workspace-concurrency=${String(Math.min(4, members.length))}`,
...members.map(member => `--filter=${member.name}`), '--pack-destination', destination,
])
let result
try {
result = await execa(invocation.command, invocation.args, {
cwd: root, reject: false, killDescendants: true,
...options.signal === undefined ? {} : { cancelSignal: options.signal },
})
} catch (error) {
if (isCanceled(error)) throw new Error('packing was cancelled')
throw error
}
if (result.isCanceled) throw new Error('packing was cancelled')
if (result.exitCode !== 0) {
throw new Error(`pnpm pack exited with ${String(result.exitCode ?? result.signal)}:\n${result.stdout}\n${result.stderr}`)
}
return readPackedArtifacts({ destination, members, release })
}
/**
* Whether a settled or thrown child-process outcome was a cancellation.
* @param value - the settled execa result or the error it rejected with.
* @returns True when an abort signal ended the child.
*/
function isCanceled(value: unknown): boolean {
return typeof value === 'object' && value !== null && 'isCanceled' in value && value.isCanceled === true
}
/**
* Validate already packed archives and describe them as servable artifacts.
* @param options - run directory, the members that were packed into it, and the public release index.
* @returns Packed artifacts sorted by package name.
*/
export function readPackedArtifacts(options: {
readonly destination: string
readonly members: readonly WorkspacePackage[]
readonly release: ReadonlyMap<string, ReleaseEntry>
}): BundleArtifact[] {
const { destination, members, release } = options
const packed = members.map((member) => {
const entry = release.get(member.name)
if (entry === undefined) {
throw new Error(`${member.name}: not a public release member; the local registry does not republish a private or unreleased package`)
}
const file = tarballName(entry.member)
const archive = join(destination, file)
if (!existsSync(archive)) throw new Error(`${member.name} produced no tarball at ${archive}`)
const contained = tarballFiles(archive)
entry.family.validatePayload(entry.member, contained)
const manifest = packedManifest(archive)
entry.family.validatePackedManifest(entry.member, manifest)
const main = manifest.main
if (typeof main === 'string' && main !== '' && !contained.includes(`package/${payloadPath(main)}`)) {
throw new Error(`${entry.member.name}: packed archive lacks its declared main ${main}; rebuild before serving`)
}
// A bundle whose patch is missing installs and then never loads its rows.
for (const patch of declaredBundlePatches(manifest)) {
if (!contained.includes(`package/${payloadPath(patch)}`)) {
throw new Error(`${entry.member.name}: packed archive lacks its declared bundle patch ${patch}`)
}
}
const bytes = readFileSync(archive)
const distTag = entry.family.distTagForVersion(entry.member.version)
return {
name: entry.member.name,
version: entry.member.version,
archive,
file,
bytes: bytes.byteLength,
integrity: `sha512-${createHash('sha512').update(bytes).digest('base64')}`,
shasum: createHash('sha1').update(bytes).digest('hex'),
manifest,
platform: platformConstraint(manifest),
...distTag === undefined ? {} : { distTag },
packedAt: new Date().toISOString(),
}
})
verifyPackedClosure(packed)
return packed.sort((left, right) => left.name.localeCompare(right.name))
}
/**
* Compose the record a completed run directory carries.
* @param options - run identity, selection, artifacts, and source evidence.
* @returns The manifest to write last.
*/
export function createRegistryManifest(options: {
readonly buildId: string
readonly source: RegistryManifest['source']
readonly entries: readonly string[]
readonly workspaceNames: readonly string[]
readonly artifacts: readonly BundleArtifact[]
}): RegistryManifest {
return {
schemaVersion: 1,
buildId: options.buildId,
source: options.source,
entries: [...options.entries],
workspaceNames: [...options.workspaceNames],
packages: [...options.artifacts]
.sort((left, right) => left.name.localeCompare(right.name))
.map(artifact => ({
name: artifact.name,
version: artifact.version,
file: artifact.file,
bytes: artifact.bytes,
integrity: artifact.integrity,
})),
}
}
/**
* Write the completion marker into a run directory.
* @param path - absolute manifest path inside the run directory.
* @param manifest - the record to serialize.
*/
export async function writeRegistryManifest(path: string, manifest: RegistryManifest): Promise<void> {
await writeFile(path, `${JSON.stringify(manifest, undefined, 2)}\n`, { mode: 0o600 })
}
/**
* Record which checkout produced a run.
* @param root - repository root.
* @param dshVersion - running DSH version the catalog installation target names.
* @returns The commit, whether the tree was dirty, and the version served.
*/
export function sourceEvidence(root: string, dshVersion: string): RegistryManifest['source'] {
return {
commit: capture('git', ['rev-parse', 'HEAD'], { cwd: root }),
dirty: capture('git', ['status', '--porcelain'], { cwd: root }) !== '',
dshVersion,
}
}
/**
* Start the loopback registry for one run's artifacts.
*
* The listener binds `127.0.0.1` on an allocated port and answers under a fresh
* namespace, so two runs never share a URL even when the port is reused.
* @param options - the run's artifacts and directory, repository-owned names, and upstream registry.
* @returns The registry URL and its close operation.
*/
export async function startBundleRegistry(options: {
readonly directory: string
readonly artifacts: readonly BundleArtifact[]
readonly workspaceNames: readonly string[]
readonly upstream?: string
}): Promise<BundleRegistry> {
if (options.artifacts.length === 0) throw new Error('the local registry was started without artifacts')
const upstream = options.upstream ?? UPSTREAM_REGISTRY
if (!upstream.endsWith('/')) throw new Error(`upstream registry must end with a slash: ${upstream}`)
const index = new Map(options.artifacts.map(artifact => [artifact.name, artifact]))
const owned = new Set(options.workspaceNames)
const namespace = randomUUID()
// Assigned once the listener reports its allocated port; requests cannot arrive before then.
let origin = ''
const server = createServer((request, response) => {
const method = request.method ?? 'GET'
const head = method === 'HEAD'
if (method !== 'GET' && method !== 'HEAD') {
respond(response, 405, { error: 'Method Not Allowed', reason: `${method} is not served` }, false)
return
}
const raw = request.url ?? '/'
const query = raw.indexOf('?')
const path = query === -1 ? raw : raw.slice(0, query)
const prefix = `/${namespace}/`
if (!path.startsWith(prefix)) {
respond(response, 404, { error: 'Not found', reason: 'request is outside this run\'s registry namespace' }, head)
return
}
const suffix = path.slice(prefix.length)
if (suffix.startsWith('tarballs/')) {
serveTarball(suffix.slice('tarballs/'.length), response, head, options.directory, index)
return
}
const requested = selection(suffix)
if (requested === undefined) {
respond(response, 404, { error: 'Not found', reason: 'malformed package path' }, head)
return
}
const artifact = index.get(requested.name)
if (artifact !== undefined) {
serveMetadata(artifact, requested.version, response, head, origin)
return
}
if (owned.has(requested.name)) {
respond(response, 404, {
error: 'Not found',
reason: `${requested.name} is repository-owned but not part of this run's local closure; rebuild the registry after changing the catalog`,
}, head)
return
}
// Only package publication is local: third-party metadata and payloads keep npm's real bytes.
response.writeHead(302, { location: `${upstream}${suffix}`, 'cache-control': 'no-store' })
response.end()
})
const listening = new Promise<void>((resolveListen, rejectListen) => {
server.once('error', rejectListen)
server.once('listening', () => {
server.off('error', rejectListen)
resolveListen()
})
})
server.listen({ port: 0, host: '127.0.0.1' })
await listening
const address = server.address()
if (address === null || typeof address === 'string') {
await closeServer(server)
throw new Error('the local registry has no TCP address')
}
origin = `http://127.0.0.1:${String(address.port)}/${namespace}/`
let closing: Promise<void> | undefined
return {
url: origin,
close: () => {
closing ??= closeServer(server)
return closing
},
}
}
/**
* Read a JSON manifest from disk.
* @param path - absolute `package.json` path.
* @returns The parsed object.
*/
function manifestAt(path: string): Record<string, unknown> {
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
if (!isRecord(parsed)) throw new Error(`${path} is not a JSON object`)
return parsed
}
/**
* Whether a parsed value is a JSON object.
* @param value - value to inspect.
* @returns True for a non-null, non-array object.
*/
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/**
* Read one dependency section as declared ranges.
* @param manifest - package manifest.
* @param section - section name.
* @returns Name and range pairs, with malformed entries rejected.
*/
function dependencyRanges(manifest: Record<string, unknown>, section: string): [string, string][] {
const value = manifest[section]
if (value === undefined) return []
if (!isRecord(value)) throw new Error(`${section} must be a mapping`)
return Object.entries(value).map(([name, range]) => {
if (typeof range !== 'string') throw new Error(`${section}.${name} must be a string range`)
return [name, range]
})
}
/**
* Every package this manifest installs on a consumer: required, optional, and
* required peers.
* @param manifest - package manifest.
* @returns Dependency names in section order.
*/
function installedEdges(manifest: Record<string, unknown>): string[] {
const names = [...REQUIRED_SECTIONS, ...OPTIONAL_SECTIONS].flatMap(section => dependencyRanges(manifest, section).map(([name]) => name))
for (const section of PEER_SECTIONS) {
for (const [name] of dependencyRanges(manifest, section)) {
if (!optionalPeer(manifest, name)) names.push(name)
}
}
return names
}
/**
* Whether a package opts out of a peer it declares.
* @param manifest - package manifest.
* @param name - peer package name.
* @returns True when the peer is optional.
*/
function optionalPeer(manifest: Record<string, unknown>, name: string): boolean {
const metadata = manifest.peerDependenciesMeta
if (!isRecord(metadata)) return false
const entry = metadata[name]
return isRecord(entry) && entry.optional === true
}
/**
* Whether one constrained value accepts the host's own value.
* @param constraint - accepted values, which may be only `!value` negations.
* @param current - the host's value, or undefined when the field does not apply here.
* @returns True when the host value is allowed.
*/
function allowsValue(constraint: readonly string[] | undefined, current: string | undefined): boolean {
if (constraint === undefined || constraint.length === 0) return true
// A field the host does not have (libc off Linux) constrains nothing here.
if (current === undefined) return true
const positives = constraint.filter(value => !value.startsWith('!'))
if (positives.length > 0 && !positives.includes('any') && !positives.includes(current)) return false
return !constraint.includes(`!${current}`)
}
/**
* Describe a platform constraint for a failure message.
* @param constraint - declared or host constraints.
* @returns A compact description.
*/
function describePlatform(constraint: PlatformConstraint): string {
const parts = (['os', 'cpu', 'libc'] as const).flatMap(field => (constraint[field] ?? []).map(value => `${field}=${value}`))
return parts.length === 0 ? 'any platform' : parts.join(' ')
}
/**
* Whether any required edge reaches a package, ignoring optional and peer edges.
* @param target - package to look for.
* @param entries - catalog roots.
* @param selected - the whole closure.
* @returns True when a required dependency edge reaches the target.
*/
function reachedByRequiredEdge(target: string, entries: readonly string[], selected: ReadonlyMap<string, WorkspacePackage>): boolean {
if (entries.includes(target)) return true
const visited = new Set<string>()
const pending = [...entries]
for (let index = 0; index < pending.length; index += 1) {
const name = pending[index] as string
if (visited.has(name)) continue
visited.add(name)
const member = selected.get(name)
if (member === undefined) continue
for (const section of REQUIRED_SECTIONS) {
for (const [dependency] of dependencyRanges(member.manifest, section)) {
if (dependency === target) return true
if (selected.has(dependency)) pending.push(dependency)
}
}
}
return false
}
/**
* Render the dependency path that reached a package, for a failure message.
* @param name - the package being explained.
* @param parents - the first edge that reached each package.
* @param entries - catalog roots, which have no parent.
* @returns A `root -> ... -> name` chain.
*/
function dependencyPath(name: string, parents: ReadonlyMap<string, string>, entries: readonly string[]): string {
const chain = [name]
let current = parents.get(name)
while (current !== undefined) {
chain.unshift(current)
current = parents.get(current)
}
return chain.length === 1 && entries.includes(name) ? `catalog entry ${name}` : chain.join(' -> ')
}
/**
* Normalize a manifest path or tarball member to its package-relative form, the
* same rule the publication payload check applies.
* @param file - manifest path or archive member.
* @returns The package-relative path.
*/
function payloadPath(file: string): string {
return file.replaceAll('\\', '/').replace(/^\.\/+/u, '')
}
/**
* The bundle patch files a manifest declares, as written.
* @param manifest - packed or declared package manifest.
* @returns Package-relative patch paths, or nothing when the package is not a bundle.
*/
function declaredBundlePatches(manifest: Record<string, unknown>): string[] {
const dsh = manifest.dsh
if (!isRecord(dsh) || !isRecord(dsh.bundle)) return []
const declared = dsh.bundle.patch
if (typeof declared === 'string') return [declared]
return Array.isArray(declared) && declared.every(entry => typeof entry === 'string') ? declared : []
}
/**
* Reject packed manifests that would send installation back to the checkout or
* to a package this run does not serve.
* @param artifacts - packed artifacts of one run.
*/
function verifyPackedClosure(artifacts: readonly BundleArtifact[]): void {
const served = new Map(artifacts.map(artifact => [artifact.name, artifact]))
for (const artifact of artifacts) {
for (const section of [...REQUIRED_SECTIONS, ...OPTIONAL_SECTIONS, ...PEER_SECTIONS]) {
for (const [name, range] of dependencyRanges(artifact.manifest, section)) {
if (/^(?:workspace|file|link|portal):/u.test(range)) {
throw new Error(`${artifact.name}: packed ${section}.${name} still declares ${range}`)
}
const local = served.get(name)
if (local !== undefined && !semver.satisfies(local.version, range, { includePrerelease: true })) {
throw new Error(`${artifact.name}: packed ${section}.${name} requires ${range}, but this run serves ${local.version}`)
}
}
}
}
}
/** The package a registry request path selects. */
interface PackageRequest {
/** Package name, with its scope when it has one. */
readonly name: string
/** Exact version or dist tag the path asks for, absent for a packument request. */
readonly version?: string
}
/**
* Split a request path into the package name and version it selects.
* @param suffix - the raw request path after the namespace.
* @returns The selected package, or undefined when the path is not a package path.
*/
function selection(suffix: string): PackageRequest | undefined {
const decoded = decodeSegment(suffix)
if (decoded === undefined) return undefined
const segments = decoded.split('/')
// Reject traversal, registry administration paths, and empty segments before any name is considered.
if (segments.some(segment => segment === '' || segment === '.' || segment === '..') || segments[0]?.startsWith('-') === true) {
return undefined
}
if (segments.length === 1 && segments[0] !== undefined) return { name: segments[0] }
if (!decoded.startsWith('@')) {
const [name, version] = segments
return name !== undefined && version !== undefined && segments.length === 2 ? { name, version } : undefined
}
if (segments.length === 2) return { name: decoded }
if (segments.length === 3) {
const version = segments[2]
return version === undefined ? undefined : { name: segments.slice(0, 2).join('/'), version }
}
return undefined
}
/**
* Decode one percent-encoded path.
* @param value - raw path text.
* @returns The decoded text, or undefined when the encoding is malformed.
*/
function decodeSegment(value: string): string | undefined {
try {
return decodeURIComponent(value)
} catch {
return undefined
}
}
/**
* Serve one archive's bytes from the artifact index.
* @param file - raw requested tarball filename.
* @param response - the response to write.
* @param head - whether this is a HEAD request, whose body is omitted.
* @param directory - the run directory holding the archives.
* @param index - the run's artifact index.
*/
function serveTarball(
file: string,
response: ServerResponse,
head: boolean,
directory: string,
index: ReadonlyMap<string, BundleArtifact>,
): void {
const decoded = decodeSegment(file)
const artifact = decoded === undefined ? undefined : [...index.values()].find(entry => entry.file === decoded)
if (artifact === undefined) {
respond(response, 404, { error: 'Not found', reason: 'no archive with that name in this run' }, head)
return
}
response.writeHead(200, {
'content-type': 'application/octet-stream',
'content-length': String(artifact.bytes),
'cache-control': 'no-store',
})
if (head) {
response.end()
return
}
const stream = createReadStream(join(directory, artifact.file))
// A client that disconnects mid-archive must not fail an unrelated request.
response.on('close', () => stream.destroy())
stream.on('error', (error) => { response.destroy(error) })
stream.pipe(response)
}
/**
* Serve the packument, a version document, or a dist-tag document for one artifact.
* @param artifact - the artifact being described.
* @param version - requested version or dist tag, or undefined for the whole packument.
* @param response - the response to write.
* @param head - whether this is a HEAD request, whose body is omitted.
* @param origin - the run's registry URL.
*/
function serveMetadata(
artifact: BundleArtifact,
version: string | undefined,
response: ServerResponse,
head: boolean,
origin: string,
): void {
const tags: Record<string, string> = { latest: artifact.version }
if (artifact.distTag !== undefined && artifact.distTag !== 'latest') tags[artifact.distTag] = artifact.version
const document = {
...artifact.manifest,
dist: {
tarball: `${origin}tarballs/${encodeURIComponent(artifact.file)}`,
integrity: artifact.integrity,
shasum: artifact.shasum,
},
}
if (version === undefined) {
respond(response, 200, {
name: artifact.name,
'dist-tags': tags,
versions: { [artifact.version]: document },
time: { created: artifact.packedAt, modified: artifact.packedAt, [artifact.version]: artifact.packedAt },
}, head)
return
}
if (version !== artifact.version && !Object.hasOwn(tags, version)) {
respond(response, 404, {
error: 'Not found',
reason: `this run serves ${artifact.name}@${artifact.version} only`,
}, head)
return
}
respond(response, 200, document, head)
}
/**
* Write one JSON response.
* @param response - the response to write.
* @param status - HTTP status.
* @param body - JSON body.
* @param head - whether this is a HEAD request, whose body is omitted.
*/
function respond(response: ServerResponse, status: number, body: unknown, head: boolean): void {
const text = `${JSON.stringify(body)}\n`
response.writeHead(status, {
'content-type': 'application/json',
'content-length': String(Buffer.byteLength(text)),
'cache-control': 'no-store',
})
response.end(head ? undefined : text)
}
/**
* Close a server and everything it is still serving.
* @param server - the listener to close.
* @returns Resolves once the listener is gone.
*/
function closeServer(server: Server): Promise<void> {
return new Promise<void>((resolveClose, rejectClose) => {
server.closeAllConnections()
server.close((error) => {
if (error === undefined) resolveClose()
else rejectClose(error)
})
})
}
+11 -1
View File
@@ -1298,7 +1298,11 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b
with tempfile.TemporaryDirectory(prefix="dsh-sdk-dynamic-tools-") as temporary:
root = Path(temporary).resolve()
dsh_home = root / "home"
user_home = root / "user-home"
user_home.mkdir()
sessions = dsh_home / "sessions"
# Own the baseline instruction message instead of inheriting host instructions.
(root / "AGENTS.md").write_text("Use the tools requested by the user.\n", encoding="utf-8")
task = root / "dynamic-tool-task.txt"
task.write_text("Call snapshot_ping once.\n", encoding="utf-8")
patch = write_profile_patch(root, "dynamic-tools.patch.yml", sessions, [
@@ -1308,6 +1312,8 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b
{"id": "tool-bash", "disabled": True},
{"id": "tool-pwsh", "disabled": True},
{"id": "session-title-llm", "disabled": True},
# Export acknowledgements can change the request header sequence numbers.
{"id": "session-log-deepseek", "config": {"enabled": False}},
{"insert": [{
"id": "dynamic-tools",
"name": (Path(__file__).resolve().parent / "fixtures/python-sdk-dynamic-tools.mjs").as_uri(),
@@ -1316,7 +1322,11 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b
with DeepSeekHarness(
provider="deepseek-official", model="smoke-model", cwd=str(root),
dsh_bin=str(executable), dsh_home=str(dsh_home), patches=(str(patch),),
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"},
env={
"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1",
"HOME": str(user_home), "USERPROFILE": str(user_home),
"DSH_AGENTS_HOME": str(user_home / ".agents"),
},
api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60,
) as harness:
result = harness.run(DYNAMIC_TOOLS_PROMPT, session_id="dynamic-tools-smoke")