Merge master into fix/prune-python-runtime-tests

This commit is contained in:
07akioni committed 2026-10-09 16:37:22 +08:00
commit 78be1e5e46
1036 files changed
+25603 -8827

No files matched your search

@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './executable-native-pty.ts'
const roots: string[] = []
+66 -59
View File
@@ -1,8 +1,12 @@
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { join } from 'node:path'
import { afterEach, expect, it } from 'vitest'
import { copyOfficeSidecar } from './build-exe-for-python-sdk-office.ts'
import { createHash } from 'node:crypto'
import { c as tar } from 'tar'
import { downloadOfficeSidecar, runtimeNpmArchive } from './build-exe-for-python-sdk-office.ts'
import { officePackageDirectories } from './libreoffice-packages.mjs'
const temporaryDirectories: string[] = []
@@ -24,33 +28,8 @@ async function fixture() {
return { root, staging, destination, packageAt }
}
it('copies package-owned data, licenses, helper permissions, and nested dependencies', async () => {
const { staging, destination, packageAt } = await fixture()
const entry = await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' }, dependencies: { decoder: '1' } })
const engine = await packageAt('@deepseek-ai/libreoffice-kit-wasm')
const decoder = await packageAt('decoder', { dependencies: { codec: '1' } }, entry)
await packageAt('codec', {}, decoder)
await packageAt('codec', { version: '2.0.0' })
const assets = ['assets/soffice.data', 'prebuilds.json', 'licenses/LICENSE', 'bin/helper']
for (const name of assets) {
const path = join(engine, name)
await mkdir(dirname(path), { recursive: true })
await writeFile(path, name)
}
await chmod(join(engine, 'bin/helper'), 0o755)
const packages = await copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })
expect(packages).toHaveLength(4)
expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/assets/soffice.data'), 'utf8')).toBe('assets/soffice.data')
expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/licenses/LICENSE'), 'utf8')).toBe('licenses/LICENSE')
expect(await readFile(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit/node_modules/decoder/node_modules/codec/package.json'), 'utf8')).toContain('codec')
await expect(stat(join(destination, 'node_modules/codec'))).rejects.toMatchObject({ code: 'ENOENT' })
if (process.platform !== 'win32') expect((await stat(join(destination, 'node_modules/@deepseek-ai/libreoffice-kit-wasm/bin/helper'))).mode & 0o111).toBe(0o111)
})
it('copies installed target optionals and leaves other platforms and absent optionals out', async () => {
const { staging, destination, packageAt } = await fixture()
it('locates installed target optionals and leaves other platforms and absent optionals out', async () => {
const { staging, packageAt } = await fixture()
await packageAt('@deepseek-ai/libreoffice-kit', {
optionalDependencies: { native: '1', foreign: '1', absent: '1' },
})
@@ -58,84 +37,112 @@ it('copies installed target optionals and leaves other platforms and absent opti
await packageAt('native', { os: ['linux'], cpu: ['x64'] })
await packageAt('foreign', { os: ['darwin'], cpu: ['arm64'] })
const packages = await copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' })
const packages = await officePackageDirectories(staging, { platform: 'linux', arch: 'x64' })
expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([
expect(packages.map(path => path.slice(staging.length + 1).replaceAll('\\', '/'))).toEqual([
'node_modules/@deepseek-ai/libreoffice-kit',
'node_modules/@deepseek-ai/libreoffice-kit-wasm',
'node_modules/native',
])
})
it('rejects a missing required dependency before producing a sidecar', async () => {
const { staging, destination, packageAt } = await fixture()
it('rejects a missing required dependency in the deployed closure', async () => {
const { staging, packageAt } = await fixture()
await packageAt('@deepseek-ai/libreoffice-kit', { dependencies: { 'dsh-missing-office-fixture': '1' } })
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }))
.rejects.toThrow('dsh-missing-office-fixture required by @deepseek-ai/libreoffice-kit is missing')
await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
})
it('rejects an incomplete installed optional instead of omitting it', async () => {
const { staging, destination, packageAt } = await fixture()
const { staging, packageAt } = await fixture()
await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { broken: '1' } })
await mkdir(join(staging, 'node_modules/broken'), { recursive: true })
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }))
.rejects.toMatchObject({ code: 'ENOENT' })
})
it('rejects an ancestor dependency outside the deployed closure', async () => {
const { root, staging, destination, packageAt } = await fixture()
const { root, staging, packageAt } = await fixture()
await packageAt('@deepseek-ai/libreoffice-kit', { dependencies: { 'ancestor-office-fixture': '1' } })
await packageAt('ancestor-office-fixture', {}, root)
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }))
.rejects.toThrow('outside the deployed closure')
})
it('requires the declared WASM engine inside the deployed closure', async () => {
const { root, staging, destination, packageAt } = await fixture()
await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } })
it.each([true, false])('requires a staged WASM engine when declared=%s even when an ancestor has one', async (declared) => {
const { root, staging, packageAt } = await fixture()
await packageAt('@deepseek-ai/libreoffice-kit', declared ? { optionalDependencies: { '@deepseek-ai/libreoffice-kit-wasm': '0.0.1' } } : {})
await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root)
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
await expect(officePackageDirectories(staging, { platform: 'linux', arch: 'x64' }))
.rejects.toThrow('Office engine @deepseek-ai/libreoffice-kit-wasm required for linux/x64 is missing.')
await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
})
it.each([
['darwin', 'arm64', 'darwin-arm64'], ['darwin', 'x64', 'darwin-x64'],
['win32', 'arm64', 'win32-arm64'], ['win32', 'x64', 'win32-x64'],
['linux', 'x64', 'linux-x64'], ['linux', 'arm64', 'wasm'], ['freebsd', 'x64', 'wasm'],
])('copies only the %s/%s engine even when other engines are staged', async (platform, arch, selected) => {
const { staging, destination, packageAt } = await fixture()
])('locates only the %s/%s engine even when other engines are staged', async (platform, arch, selected) => {
const { staging, packageAt } = await fixture()
const targets = ['darwin-arm64', 'darwin-x64', 'win32-arm64', 'win32-x64', 'linux-x64', 'wasm']
const names = targets.map(target => `@deepseek-ai/libreoffice-kit-${target}`)
await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: Object.fromEntries(names.map(name => [name, '0.0.1'])) })
for (const name of names) await packageAt(name)
const expected = `@deepseek-ai/libreoffice-kit-${selected}`
const packages = await copyOfficeSidecar(staging, destination, { platform, arch })
expect(packages.map(path => path.replaceAll('\\', '/'))).toEqual([
const packages = await officePackageDirectories(staging, { platform, arch })
expect(packages.map(path => path.slice(staging.length + 1).replaceAll('\\', '/'))).toEqual([
'node_modules/@deepseek-ai/libreoffice-kit', `node_modules/${expected}`,
])
})
it.each(['darwin', 'win32', 'linux'])('%s requires its native package even when WASM is staged', async (platform) => {
const { staging, destination, packageAt } = await fixture()
const { staging, packageAt } = await fixture()
await packageAt('@deepseek-ai/libreoffice-kit', { optionalDependencies: {
'@deepseek-ai/libreoffice-kit-wasm': '0.0.1', [`@deepseek-ai/libreoffice-kit-${platform}-arm64`]: '0.0.1',
} })
await packageAt('@deepseek-ai/libreoffice-kit-wasm')
await expect(copyOfficeSidecar(staging, destination, { platform, arch: 'arm64' }))
await expect(officePackageDirectories(staging, { platform, arch: 'arm64' }))
.rejects.toThrow(`Office engine @deepseek-ai/libreoffice-kit-${platform}-arm64 required for ${platform}/arm64 is missing.`)
await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
})
it('requires a staged Linux WASM engine even when an ancestor has one', async () => {
const { root, staging, destination, packageAt } = await fixture()
await packageAt('@deepseek-ai/libreoffice-kit')
await packageAt('@deepseek-ai/libreoffice-kit-wasm', {}, root)
await expect(copyOfficeSidecar(staging, destination, { platform: 'linux', arch: 'x64' }))
.rejects.toThrow('Office engine @deepseek-ai/libreoffice-kit-wasm required for linux/x64 is missing.')
await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
it('downloads pinned npm packages into the sidecar without resolving or executing package scripts', async () => {
const { root, destination } = await fixture()
const source = join(root, 'package')
await mkdir(source)
await writeFile(join(source, 'package.json'), JSON.stringify({ name: 'fixture', version: '1.0.0', scripts: { install: 'must-not-run' } }))
await writeFile(join(source, 'worker.js'), 'worker')
const archive = join(root, 'package.tgz')
await tar({ file: archive, cwd: root, gzip: true }, ['package'])
const bytes = await readFile(archive)
const checksum = createHash('sha512').update(bytes).digest()
const cache = join(root, 'archives')
await mkdir(cache)
await writeFile(join(cache, checksum.toString('hex')), bytes)
const artifact = { name: 'fixture', version: '1.0.0', directory: 'node_modules/fixture',
url: 'https://unused.invalid/fixture.tgz', integrity: `sha512-${checksum.toString('base64')}` }
await downloadOfficeSidecar([artifact], destination, cache)
expect(await readFile(join(destination, 'node_modules/fixture/worker.js'), 'utf8')).toBe('worker')
await expect(downloadOfficeSidecar([{ ...artifact, directory: 'node_modules/../../outside' }], destination, cache))
.rejects.toThrow('invalid package directory')
await expect(downloadOfficeSidecar([{ ...artifact, version: '2.0.0' }], destination, cache))
.rejects.toThrow('package identity mismatch')
await writeFile(join(cache, checksum.toString('hex')), 'corrupted archive')
await expect(downloadOfficeSidecar([artifact], destination, cache)).rejects.toThrow('checksum mismatch')
})
it('takes npm integrity from the workspace lock without querying registry metadata', async () => {
const { root } = await fixture()
const lockfile = join(root, 'pnpm-lock.yaml')
const integrity = `sha512-${Buffer.alloc(64, 7).toString('base64')}`
await writeFile(lockfile, JSON.stringify({ packages: { '@scope/[email protected]': { resolution: { integrity } } } }))
expect(await runtimeNpmArchive('@scope/fixture', '1.2.3', lockfile)).toEqual({
name: '@scope/fixture', version: '1.2.3', integrity,
url: 'https://registry.npmjs.org/@scope/fixture/-/fixture-1.2.3.tgz',
})
await expect(runtimeNpmArchive('@scope/fixture', '1.2.4', lockfile)).rejects.toThrow('missing SHA-512 integrity')
await writeFile(lockfile, JSON.stringify({ packages: { '@scope/[email protected]': { resolution: { integrity: 'sha256-invalid' } } } }))
await expect(runtimeNpmArchive('@scope/fixture', '1.2.3', lockfile)).rejects.toThrow('missing SHA-512 integrity')
})
+49 -23
View File
@@ -1,6 +1,8 @@
/** Keep LibreOffice workers, prebuilt engines, and their dependencies on the real filesystem. */
import { cp, mkdir, rm } from 'node:fs/promises'
import { join, relative, sep } from 'node:path'
import { readFile } from 'node:fs/promises'
import { isAbsolute, join, relative, sep } from 'node:path'
import { downloadRuntimeNpmPackage, type RuntimeNpmArchive } from './primary-runtime/prepare.ts'
import { load as parseYaml } from 'js-yaml'
import { officePackageDirectories } from './libreoffice-packages.mjs'
/** pkg applies these exclusions to dependency `files` as well as root asset globs. */
@@ -9,29 +11,53 @@ export const OFFICE_ASSET_IGNORES = [
'**/node_modules/@deepseek-ai/libreoffice-kit-*/**',
]
/** One npm package at its existing sidecar dependency location. */
export interface OfficePackageArchive extends RuntimeNpmArchive {
/** Relative package directory inside the sidecar. */
readonly directory: string
}
/**
* Copy the installed Office dependency tree without changing package contents or executable modes.
* Harness sidecars require the kit's declared target native engine, or WASM for other targets.
* Missing target engines fail with their package name; missing required dependencies or paths outside the deployed closure also fail.
* @param staging - Symlink-free deployed Node closure.
* @param destination - Target-specific Office directory beside the executable; replaced when present.
* @param target - Node platform and CPU of the executable.
* @returns Relative package directories included in the sidecar.
* Resolve one installed npm version against its reviewed workspace lock integrity.
* @param name - Published package name.
* @param version - Exact installed version.
* @param lockfile - Workspace lock recording the exact published archive.
* @returns Locked npm tarball identity; rejects versions without SHA-512 integrity.
*/
export async function copyOfficeSidecar(
staging: string,
destination: string,
target: { platform: string; arch: string },
): Promise<string[]> {
export async function runtimeNpmArchive(name: string, version: string, lockfile = join(import.meta.dirname, '../pnpm-lock.yaml')): Promise<RuntimeNpmArchive> {
const lock = parseYaml(await readFile(lockfile, 'utf8')) as { packages?: Record<string, { resolution?: { integrity?: string } }> }
const integrity = lock.packages?.[`${name}@${version}`]?.resolution?.integrity
if (typeof integrity !== 'string' || !/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(integrity)) {
throw new Error(`runtime npm lock: missing SHA-512 integrity for ${name}@${version}`)
}
return { name, version, url: `https://registry.npmjs.org/${name}/-/${name.split('/').at(-1)}-${version}.tgz`, integrity }
}
/**
* Lock the installed Office closure to published npm tarballs, preserving nested dependency locations.
* @param staging - Complete installed Node closure.
* @param target - Sidecar platform and CPU.
* @returns Download records for the selected engine and all dependencies.
*/
export async function officeSidecarArchives(staging: string, target: { platform: string; arch: string }): Promise<OfficePackageArchive[]> {
const directories = await officePackageDirectories(staging, target)
await rm(destination, { recursive: true, force: true })
await mkdir(destination, { recursive: true })
for (const source of directories) {
const nestedModules = join(source, 'node_modules')
await cp(source, join(destination, relative(staging, source)), {
recursive: true,
filter: path => path !== nestedModules && !path.startsWith(nestedModules + sep),
})
return Promise.all(directories.map(async (directory) => {
const manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')) as { name: string; version: string }
return { ...await runtimeNpmArchive(manifest.name, manifest.version), directory: relative(staging, directory).split(sep).join('/') }
}))
}
/**
* Download the complete pinned Office sidecar without npm or package lifecycle scripts.
* @param packages - Build-locked npm packages and dependency locations.
* @param destination - Empty sidecar directory.
* @param cache - Verified archive cache.
* @returns Resolves after all packages are unpacked and checked.
*/
export async function downloadOfficeSidecar(packages: readonly OfficePackageArchive[], destination: string, cache: string): Promise<void> {
for (const artifact of packages) {
if (isAbsolute(artifact.directory) || artifact.directory.split(/[\\/]/u).some(part => part === '..')
|| !artifact.directory.startsWith('node_modules/')) throw new Error(`Office download: invalid package directory ${artifact.directory}`)
await downloadRuntimeNpmPackage(artifact, join(destination, artifact.directory), cache)
}
return directories.map(directory => relative(staging, directory))
}
+4 -2
View File
@@ -1,4 +1,4 @@
/** Prepare a symlink-free Python runtime payload with shared workspace module instances. */
/** Prepare a symlink-free executable 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'
@@ -78,7 +78,9 @@ export async function materializeStagedLinks(staging: string): Promise<void> {
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 })
const directory = join(nodeModules, ...segments.slice(0, binIndex + 1))
if ((await lstat(directory)).isSymbolicLink()) await unlink(directory)
else await rm(directory, { recursive: true, force: true })
remaining = await findSymlink(nodeModules)
continue
}
+2 -1
View File
@@ -175,7 +175,8 @@ describe('Python runtime executable builder CLI', () => {
expect(result.status).toBe(0)
expect(result.stdout).toContain('exec pkg')
expect(result.stdout).toContain('--sea --targets node24-macos-x64')
expect(result.stdout).toContain('prepare Python and Office skills for mac-x64')
expect(result.stdout).toContain('keep only node-pty prebuilds darwin-x64')
expect(result.stdout).toContain('lock optional resource downloads and copy Office skills')
expect(result.stdout).toContain(join(root, 'dist-exe', 'macos-x64'))
})
+73 -80
View File
@@ -6,16 +6,20 @@
* runtime imports that pkg cannot discover statically.
*/
import { createHash } from 'node:crypto'
import { build as bundle } from 'tsdown'
import { spawn } from 'node:child_process'
import { pnpmInvocation, restoreLegacyHoists } from './executable-packaging.ts'
import { existsSync, statSync } from 'node:fs'
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 { chmod, copyFile, cp, glob, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { basename, dirname, 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 { pathToFileURL } from 'node:url'
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './executable-native-pty.ts'
import { officeSidecarArchives, runtimeNpmArchive, 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'
import { prepareOfficeSkillAssets } from './primary-runtime/prepare.ts'
import primaryLock from './primary-runtime/lock.json' with { type: 'json' }
const root = resolve(import.meta.dirname, '..')
@@ -44,6 +48,7 @@ const DEPLOY_ONLY_DOCS = ['README.md', 'README.zh.md', 'README.i18n.yaml']
*/
const ASSET_GLOBS = [
'package.json',
'*.mjs',
'node_modules/**/*.js',
'node_modules/**/*.cjs',
'node_modules/**/*.mjs',
@@ -232,29 +237,6 @@ class BuildCli {
}
}
function pnpmInvocation(args: string[]): [command: string, args: string[]] {
const entrypoint = process.env.npm_execpath?.trim()
if (entrypoint !== undefined && entrypoint !== '') {
const extension = extname(entrypoint).toLowerCase()
if (extension === '.js' || extension === '.cjs' || extension === '.mjs') {
return [process.execPath, [entrypoint, ...args]]
}
if (extension !== '.cmd') return [entrypoint, args]
}
const home = process.env.PNPM_HOME?.trim()
if (home !== undefined && home !== '') {
const packageBin = resolve(home, '..', 'pnpm', 'bin')
for (const filename of ['pnpm.mjs', 'pnpm.cjs']) {
const candidate = resolve(packageBin, filename)
if (existsSync(candidate)) return [process.execPath, [candidate, ...args]]
}
}
if (process.platform === 'win32') {
throw new Error('build-exe-for-python-sdk: pnpm must expose a JavaScript entrypoint through npm_execpath or PNPM_HOME on Windows.')
}
return ['pnpm', args]
}
/**
* Render a command for logs and errors, quoting arguments with spaces.
* @param command - the executable.
@@ -300,7 +282,7 @@ class SingleExeBuild {
}
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${this.staging}`)
else await rm(this.staging, { recursive: true, force: true })
await this.runPnpm('deploy', [
try { await this.runPnpm('deploy', [
'--filter',
DEPLOY_ROOT_PACKAGE,
'deploy',
@@ -313,13 +295,19 @@ class SingleExeBuild {
'--config.link-workspace-packages=true',
'--config.hoist-workspace-packages=false',
this.staging,
])
]) } finally {
// Legacy deploy records production-only workspace state; restore the development installation before exec.
await this.runPnpm('restore development dependencies', ['install', '--offline', '--frozen-lockfile', '--prod=false', '--ignore-scripts'])
}
await this.restoreLegacyHoists()
await this.materializeStagedLinks()
if (this.cli.dryRun) {
for (const name of DEPLOY_ONLY_DOCS) console.log(`build-exe-for-python-sdk: [dry-run] rm -f ${join(this.staging, name)}`)
} else {
await Promise.all(DEPLOY_ONLY_DOCS.map(name => rm(join(this.staging, name), { force: true })))
for await (const file of glob('node_modules/**/{README*.md,CHANGELOG*.md,HISTORY*.md,*.map,*.d.ts,*.d.mts,*.d.cts}', {
cwd: this.staging, exclude: ['node_modules/**/assets/**'],
})) await rm(join(this.staging, file), { force: true })
}
}
@@ -334,38 +322,8 @@ class SingleExeBuild {
console.log('build-exe-for-python-sdk: [dry-run] restore direct dependencies omitted by legacy deploy')
return
}
const manifestPath = join(this.staging, 'package.json')
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as {
dependencies?: Record<string, string>
}
const sourceNodeModules = resolve(root, DEPLOY_SOURCE_NODE_MODULES)
const restored: string[] = []
for (const dependency of Object.keys(manifest.dependencies ?? {}).sort()) {
const destination = join(this.staging, 'node_modules', dependency)
if (existsSync(destination)) continue
const source = join(sourceNodeModules, dependency)
if (!existsSync(source)) {
throw new Error(
`build-exe-for-python-sdk: deployed dependency ${dependency} is absent from both ${destination} and ${source}.`,
)
}
await mkdir(dirname(destination), { recursive: true })
const nestedNodeModules = join(source, 'node_modules')
await cp(source, destination, {
recursive: true,
dereference: true,
filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
})
restored.push(dependency)
}
const stillMissing = Object.keys(manifest.dependencies ?? {})
.filter(dependency => !existsSync(join(this.staging, 'node_modules', dependency)))
if (stillMissing.length > 0) {
throw new Error(`build-exe-for-python-sdk: staged dependencies remain missing: ${stillMissing.join(', ')}.`)
}
if (restored.length > 0) {
console.log(`build-exe-for-python-sdk: restored legacy deploy hoists: ${restored.join(', ')}`)
}
const restored = await restoreLegacyHoists(this.staging, resolve(root, DEPLOY_SOURCE_NODE_MODULES))
if (restored.length > 0) console.log(`build-exe-for-python-sdk: restored legacy deploy hoists: ${restored.join(', ')}`)
}
/** Replace deploy-time package links with files and reject any remaining link. */
@@ -380,7 +338,14 @@ class SingleExeBuild {
/** Add the executable entry and pkg assets to the staged manifest. */
async injectPkgConfig(): Promise<void> {
const patch = { bin: ENTRY_BIN, pkg: { assets: ASSET_GLOBS, ignore: OFFICE_ASSET_IGNORES } }
const sourceModules = join(root, 'node_modules').replaceAll('\\', '/')
const patch = { bin: ENTRY_BIN, pkg: { assets: ASSET_GLOBS, ignore: [
...OFFICE_ASSET_IGNORES,
`${sourceModules}/**`, `${sourceModules}/.pnpm/**`,
...['packages', 'apps', 'vendor', 'native', 'scripts'].map(directory => `${join(root, directory).replaceAll('\\', '/')}/**`),
`${root.replaceAll('\\', '/')}/tsconfig*.json`,
'**/*.map', '**/*.d.ts', '**/*.d.mts', '**/*.d.cts',
] } }
const manifestPath = join(this.staging, 'package.json')
if (this.cli.dryRun) {
console.log(`build-exe-for-python-sdk: [dry-run] patch ${manifestPath} with ${JSON.stringify(patch)}`)
@@ -392,8 +357,28 @@ class SingleExeBuild {
if (!existsSync(join(this.staging, ENTRY_BIN))) {
throw new Error(`build-exe-for-python-sdk: staged bootstrap ${join(this.staging, ENTRY_BIN)} is missing.`)
}
const helpers = [
{ name: 'primary-runtime', source: 'scripts/primary-runtime/prepare.ts', exports: 'downloadNodeRuntime, preparePrimaryRuntime' },
{ name: 'office-sidecar', source: 'scripts/build-exe-for-python-sdk-office.ts', exports: 'downloadOfficeSidecar' },
]
const entries: Record<string, string> = {}
for (const helper of helpers) {
const entry = join(this.staging, `.${helper.name}-entry.ts`)
entries[helper.name] = entry
await writeFile(entry, `export { ${helper.exports} } from ${JSON.stringify(join(root, helper.source).replaceAll('\\', '/'))}\n`)
}
try {
await bundle({ config: false, tsconfig: false, inputOptions: { tsconfig: false }, target: 'es2024', entry: entries,
outDir: this.staging, clean: false, format: 'esm', platform: 'node', dts: false,
deps: { neverBundle: [/^@deepseek-ai\//u], alwaysBundle: [/.*/u] }, shims: true, outExtensions: () => ({ js: '.mjs' }),
define: { 'import.meta.main': 'false' } })
} finally {
for (const entry of Object.values(entries)) await rm(entry, { force: true })
}
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Record<string, unknown>
await writeFile(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`)
await this.run('resource helper imports', process.execPath, ['--input-type=module', '--eval',
['primary-runtime.mjs', 'office-sidecar.mjs'].map(file => `await import(${JSON.stringify(pathToFileURL(join(this.staging, file)).href)})`).join('\n')])
console.log(`build-exe-for-python-sdk: injected pkg config into ${manifestPath}`)
}
@@ -406,6 +391,7 @@ class SingleExeBuild {
const productBase = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
const product = target.platform === 'win' ? `${productBase}.exe` : productBase
await this.prepareNativePty(target)
const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform
if (!this.cli.dryRun) await mkdir(this.outDir, { recursive: true })
await this.runPnpm(`pkg ${target.spec}`, [
'exec',
@@ -420,26 +406,22 @@ class SingleExeBuild {
if (!this.cli.dryRun && !existsSync(product)) {
throw new Error(`build-exe-for-python-sdk: product ${product} is missing after the pkg run; inspect ${this.outDir}.`)
}
const office = `${productBase}-office`
if (this.cli.dryRun) {
console.log(`build-exe-for-python-sdk: [dry-run] copy Office dependency closure from ${this.staging} to ${office}`)
} else {
const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform
const packages = await copyOfficeSidecar(this.staging, office, { platform, arch: target.arch })
console.log(`build-exe-for-python-sdk: copied ${packages.length} Office packages to ${office}`)
}
const ripgrep = await this.copyRipgrepSidecar(target, product)
const resources = join(this.outDir, `${target.platform}-${target.arch}`)
const runtimeTarget = `${target.platform === 'macos' ? 'mac' : target.platform}-${target.arch}` as PrimaryRuntimeTarget
if (this.cli.dryRun) {
console.log(`build-exe-for-python-sdk: [dry-run] prepare Python and Office skills for ${runtimeTarget} in ${resources}`)
console.log(`build-exe-for-python-sdk: [dry-run] lock optional resource downloads and copy Office skills to ${resources}`)
} else {
const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { version: string }
await preparePrimaryRuntime({ target: runtimeTarget, output: resources,
cache: join(tmpdir(), 'dsh-primary-runtime-downloads'), version })
smokePrimaryRuntime(join(resources, 'primary-runtime'))
await rm(resources, { recursive: true, force: true })
await mkdir(resources, { recursive: true })
const { version, packageManager } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { version: string; packageManager: string }
const office = await officeSidecarArchives(this.staging, { platform, arch: target.arch })
const pnpm = await runtimeNpmArchive('pnpm', packageManager.replace('pnpm@', ''))
const inputs = { version, target: `${target.platform === 'macos' ? 'mac' : target.platform}-${target.arch}`, office, pnpm }
const identity = createHash('sha256').update(JSON.stringify({ ...inputs, primaryLock })).digest('hex')
await writeFile(join(resources, 'downloads.json'), JSON.stringify({ ...inputs, identity }, undefined, 2) + '\n')
await prepareOfficeSkillAssets(join(this.staging, 'node_modules/@deepseek-ai/dsh-skill-office/assets'), join(resources, 'office-skills'))
}
if (target.platform !== 'macos') return [product, ripgrep, office, resources]
if (target.platform !== 'macos') return [product, ripgrep, resources]
const spawnHelper = `${product}-spawn-helper`
const source = join(this.staging, 'node_modules', 'node-pty', 'prebuilds', `darwin-${target.arch}`, 'spawn-helper')
if (this.cli.dryRun) {
@@ -448,7 +430,7 @@ class SingleExeBuild {
await copyFile(source, spawnHelper)
await chmod(spawnHelper, 0o755)
}
return [product, ripgrep, spawnHelper, office, resources]
return [product, ripgrep, spawnHelper, resources]
}
/** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
@@ -495,6 +477,17 @@ class SingleExeBuild {
'node_modules',
'node-pty',
)
const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform
const selected = `${platform}-${target.arch}`
const prebuilds = join(this.staging, 'node_modules/node-pty/prebuilds')
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] keep only node-pty prebuilds ${selected}`)
else {
const source = join(packageDirectory, 'prebuilds', selected)
if (existsSync(source)) await cp(source, join(prebuilds, selected), { recursive: true })
for (const entry of await readdir(prebuilds, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name !== selected) await rm(join(prebuilds, entry.name), { recursive: true, force: true })
}
}
if (target.platform === 'win') {
if (target.arch !== 'x64') {
throw new Error('build-exe-for-python-sdk: Windows supports x64 only.')
+176
View File
@@ -0,0 +1,176 @@
/** Build a relocatable, native-host SSH helper archive with its private Node workers. */
import { spawn, execFileSync } from 'node:child_process'
import { existsSync, globSync } from 'node:fs'
import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { basename, dirname, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { build } from 'tsdown'
import { create as createTar } from 'tar'
import { verifyRuntimeClosure } from './verify-runtime-closure.ts'
import { pnpmInvocation, restoreLegacyHoists } from './executable-packaging.ts'
import { materializeStagedLinks, deduplicateStagedWorkspacePackages } from './build-exe-for-python-sdk-staging.ts'
import { resolveLinuxNodePtyAddon } from './executable-native-pty.ts'
import { artifactManifestSchema, fileDigest, payloadFiles, SSH_HELPER_TARGETS, verifyPayload, type SshHelperTarget } from './ssh-helper/artifact.ts'
import { probeHelper } from './ssh-helper/probe.ts'
import { verifyNativePayload } from './ssh-helper/native.ts'
const root = resolve(import.meta.dirname, '..')
const closureDirectory = 'packages/ssh/ssh-helper-runtime'
const closureName = '@deepseek-ai/dsh-ssh-helper-runtime'
const assets = ['package.json', 'lib/**/*.js', ...['js', 'cjs', 'mjs', 'json', 'node', 'so', 'so.*', 'dylib', 'wasm', 'sh', 'bash'].map(extension => `node_modules/**/*.${extension}`)]
interface Manifest { name: string; version: string; license?: string; dependencies?: Record<string, string> }
async function manifest(path: string): Promise<Manifest> { return JSON.parse(await readFile(path, 'utf8')) as Manifest }
async function run(command: string, args: string[]): Promise<void> {
console.log(`ssh-helper build: ${[command, ...args].join(' ')}`)
await new Promise<void>((resolve, reject) => {
const child = spawn(command, args, { cwd: root, stdio: 'inherit', env: { ...process.env, CI: 'true' } })
child.once('error', reject)
child.once('close', (code, signal) => {
if (code === 0 && signal === null) resolve()
else reject(new Error(`SSH helper build command failed (${code ?? signal}): ${command}`))
})
})
}
async function pnpm(...args: string[]): Promise<void> { const [command, argv] = pnpmInvocation(args); await run(command, argv) }
async function compileRuntime(dependencies: Readonly<Record<string, string>>): Promise<void> {
await pnpm('exec', 'tsx', 'native/system/scripts/build.ts')
await pnpm('exec', 'tsc', '-b', closureDirectory, 'native/system/packages/entry')
const names = new Set([closureName, ...Object.keys(dependencies)])
for (const path of globSync(['packages/*/*/package.json', 'vendor/*/package.json'], { cwd: root }).sort()) {
if (!names.has((await manifest(join(root, path))).name)) continue
const cwd = dirname(join(root, path))
const config = join(cwd, 'tsdown.config.ts')
if (existsSync(config)) await build({ cwd, config })
else await build({ cwd, config: false, entry: ['lib/types/{index,invariant,startup}.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false })
}
}
async function prepareNative(staging: string, product: string, platform: string, arch: 'x64' | 'arm64'): Promise<void> {
const pty = join(staging, 'node_modules', 'node-pty')
await rm(join(pty, 'build'), { recursive: true, force: true })
for (const entry of await readdir(join(pty, 'prebuilds'))) {
if (entry !== `${platform}-${arch}`) await rm(join(pty, 'prebuilds', entry), { recursive: true, force: true })
}
if (platform === 'linux') {
const addon = resolveLinuxNodePtyAddon(join(root, 'packages/subprocess/subprocess-local/node_modules/node-pty'), arch)
await mkdir(join(pty, 'build/Release'), { recursive: true })
await copyFile(addon, join(pty, 'build/Release/pty.node'))
} else {
const destination = join(product, 'dsh-ssh-helper-spawn-helper')
await copyFile(join(pty, 'prebuilds', `darwin-${arch}`, 'spawn-helper'), destination)
await chmod(destination, 0o755)
}
const source = join(root, 'native/system/packages', `${platform}-${arch}`)
const destination = join(product, 'native/system')
await mkdir(destination, { recursive: true })
await copyFile(join(source, 'package.json'), join(destination, 'package.json'))
await cp(join(source, 'bin'), join(destination, 'bin'), { recursive: true })
// The Linux carrier targets glibc; no unused musl addon travels in this archive.
if (platform === 'linux') await rm(join(destination, 'bin/musl'), { recursive: true, force: true })
const binaries = platform === 'linux' ? ['bin/landlock-run', 'bin/glibc/system.node'] : ['bin/system.node']
for (const binary of binaries) {
if (!(await lstat(join(destination, binary))).isFile()) throw new Error(`Missing SSH native resource: ${binary}`)
}
}
async function copyLicenses(staging: string, product: string): Promise<void> {
const destination = join(product, 'LICENSES')
await mkdir(destination, { recursive: true })
const inventory: { name: string; version: string; license?: string }[] = []
for (const path of globSync(['node_modules/*/package.json', 'node_modules/@*/*/package.json'], { cwd: staging }).sort()) {
const directory = dirname(join(staging, path))
const data = await manifest(join(staging, path))
inventory.push({ name: data.name, version: data.version, ...(data.license === undefined ? {} : { license: data.license }) })
const licenses = (await readdir(directory)).filter(name => /^(?:licen[cs]e|copying|notice)(?:[.-]|$)/i.test(name))
for (const name of licenses) {
const source = join(directory, name)
if (!(await lstat(source)).isFile()) continue
const output = join(destination, data.name.replaceAll('/', '__'), name)
await mkdir(dirname(output), { recursive: true })
await copyFile(source, output)
}
}
await copyFile(join(root, 'LICENSE'), join(destination, 'deepseek-harness-MIT.txt'))
await copyFile(join(root, 'native/system/LICENSE'), join(destination, 'node-addon-system-BSD.txt'))
await writeFile(join(destination, 'packages.json'), `${JSON.stringify(inventory, null, 2)}\n`)
}
/**
* Build the selected native target and retain only its complete release archive.
* @param argv - command-line arguments, excluding the interpreter and script.
*/
export async function buildSshHelper(argv: string[]): Promise<void> {
const { values } = parseArgs({ args: argv, options: {
target: { type: 'string' }, output: { type: 'string' },
'skip-build': { type: 'boolean', default: false }, 'dry-run': { type: 'boolean', default: false },
} })
const host = `node24-${process.platform === 'darwin' ? 'macos' : process.platform}-${process.arch}`
const selected = values.target ?? host
if (!SSH_HELPER_TARGETS.some(target => target === selected)) throw new Error(`Unsupported SSH helper target: ${selected}`)
if (!values['dry-run'] && selected !== host) throw new Error(`SSH helper target ${selected} requires its native host; current host is ${host}`)
const target = selected as SshHelperTarget
const definition = await manifest(join(root, closureDirectory, 'package.json'))
const closure = await verifyRuntimeClosure(root, `${closureDirectory}/package.json`, false)
if (closure.failures.length > 0) throw new Error(`SSH helper runtime dependencies are incomplete:\n${closure.failures.join('\n')}`)
const output = resolve(root, values.output ?? 'dist-exe/ssh-helper')
if (values['dry-run']) {
console.log(`SSH helper ${target}: ${values['skip-build'] ? 'reuse built modules' : 'compile runtime'} -> deploy ${closureName} -> pkg --sea -> handshake -> ${output}`)
return
}
if (!values['skip-build']) await compileRuntime(definition.dependencies ?? {})
await mkdir(join(root, '.dsh-build'), { recursive: true })
const temporary = await mkdtemp(join(root, '.dsh-build/ssh-helper-'))
try {
const staging = join(temporary, 'staging')
const product = join(temporary, 'dsh-ssh-helper')
await mkdir(product)
try {
await pnpm('--filter', closureName, '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', staging)
} finally {
// Legacy deploy records production-only workspace state; pnpm's next exec otherwise prunes build tools.
await pnpm('install', '--offline', '--frozen-lockfile', '--prod=false', '--ignore-scripts')
}
await restoreLegacyHoists(staging, join(root, closureDirectory, 'node_modules'))
await materializeStagedLinks(staging)
await deduplicateStagedWorkspacePackages(staging, root)
await prepareNative(staging, product, process.platform, process.arch as 'x64' | 'arm64')
await verifyNativePayload(join(staging, 'node_modules'), target)
const stagedManifest = await manifest(join(staging, 'package.json'))
await writeFile(join(staging, 'runtime-bootstrap.mjs'), 'import { runSshHelperRuntime } from "./lib/index.js";\nawait runSshHelperRuntime();\n')
await writeFile(join(staging, 'package.json'), `${JSON.stringify({ ...stagedManifest,
bin: 'runtime-bootstrap.mjs', pkg: { assets, ignore: ['node_modules/@deepseek-ai/node-addon-system-*/**/*'] },
}, null, 2)}\n`)
const executable = join(product, 'dsh-ssh-helper')
await pnpm('exec', 'pkg', staging, '--sea', '--targets', target, '--output', executable)
await chmod(executable, 0o755)
await verifyNativePayload(product, target)
await copyLicenses(staging, product)
const hello = await probeHelper(executable, product)
if (hello.kind !== 'executable' || hello.executable !== executable) throw new Error('Packaged SSH helper did not report its physical executable')
const metadata = artifactManifestSchema.parse({
format: 1, version: definition.version, target,
commit: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(),
dirty: execFileSync('git', ['status', '--porcelain'], { cwd: root, encoding: 'utf8' }).trim() !== '',
protocol: hello.protocol, nodeVersion: hello.nodeVersion,
files: await Promise.all((await payloadFiles(product)).map(async path => ({
path, sha256: await fileDigest(join(product, path)), mode: (await lstat(join(product, path))).mode & 0o777,
}))),
})
await writeFile(join(product, 'manifest.json'), `${JSON.stringify(metadata, null, 2)}\n`)
await verifyPayload(product)
await mkdir(output, { recursive: true })
const archive = join(output, `dsh-ssh-helper-${definition.version}-${target.slice('node24-'.length)}.tar.gz`)
await createTar({ cwd: temporary, file: archive, gzip: true, portable: true }, ['dsh-ssh-helper'])
await writeFile(`${archive}.sha256`, `${await fileDigest(archive)} ${basename(archive)}\n`)
console.log(`SSH helper archive: ${archive}`)
} finally {
await rm(temporary, { recursive: true, force: true })
}
}
if (import.meta.main) await buildSshHelper(process.argv.slice(2))
+4 -48
View File
@@ -56,11 +56,6 @@ def runtime_filenames(executable_name: str) -> tuple[str, ...]:
return (*names, f"{executable_name}-spawn-helper") if "-macos-" in executable_name else names
def office_sidecar_name(executable_name: str) -> str:
"""Return the complete Office dependency directory for one executable."""
return f"{executable_name.removesuffix('.exe')}-office"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--package", choices=("sdk", "runtime"), required=True)
@@ -229,54 +224,18 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_
source_directory = executable.parent
for filename in runtime_filenames(executable_name):
shutil.copy2(source_directory / filename, runtime_dir / filename)
office = office_sidecar_name(executable_name)
shutil.copytree(source_directory / office, runtime_dir / office)
resources = executable_name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe")
shutil.copytree(source_directory / resources, runtime_dir / resources)
def verify_office_payload(archive: zipfile.ZipFile, office_modules: str, platform_tag: str) -> None:
"""Check packaged engine assets and native helper executable permissions."""
names = set(archive.namelist())
adapter = f"{office_modules}/@deepseek-ai/libreoffice-kit/package.json"
if adapter not in names:
raise RuntimeError("Office dependency is missing: libreoffice-kit")
engines = f"{office_modules}/@deepseek-ai"
target = next(name for name, value in PLATFORMS.items() if value[0] == platform_tag)
native_target = target.replace("win-", "win32-").replace("macos-", "darwin-")
declared = json.loads(archive.read(adapter)).get("optionalDependencies", {})
selected = native_target if f"@deepseek-ai/libreoffice-kit-{native_target}" in declared else "wasm"
for required in (f"libreoffice-kit-{selected}/prebuilds.json",):
if f"{engines}/{required}" not in names:
raise RuntimeError(f"Office dependency is missing: {required}")
manifests = (
name for name in names
if name.startswith(f"{engines}/libreoffice-kit-")
and name.endswith("/prebuilds.json")
and name.count("/") == engines.count("/") + 2
)
for manifest_path in manifests:
if manifest_path != f"{engines}/libreoffice-kit-{selected}/prebuilds.json":
raise RuntimeError(f"Unexpected Office engine for {platform_tag}: {manifest_path}")
manifest = json.loads(archive.read(manifest_path))
engine = manifest["engine"]
native = engine["kind"] == "native"
fields = ("executable",) if native else ("loader", "wasm", "data", "metadata")
for field in fields:
asset = f"{manifest_path.rsplit('/', 1)[0]}/{engine[field]}"
if asset not in names:
raise RuntimeError(f"Office engine asset is missing: {asset}")
mode = archive.getinfo(asset).external_attr >> 16
if native and platform_tag != "win_amd64" and mode & stat.S_IXUSR == 0:
raise RuntimeError(f"Office helper lost its executable bit: {asset}")
def verify_wheel(
wheel: Path,
package: str,
version: str,
platform: tuple[str, str] | None,
) -> None:
if wheel.stat().st_size > 100_000_000:
raise RuntimeError(f"{wheel} is {wheel.stat().st_size} bytes and exceeds the PyPI single-file limit of 100000000 bytes")
expected_tag = "py3-none-any" if platform is None else f"py3-none-{platform[0]}"
expected_distribution = SDK_DISTRIBUTION if package == "sdk" else RUNTIME_DISTRIBUTION
dist_info = f"{expected_distribution.replace('-', '_')}-{version}.dist-info"
@@ -308,15 +267,12 @@ def verify_wheel(
]
if package == "runtime":
assert platform is not None
office = office_sidecar_name(platform[1])
resources = platform[1].removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe")
expected_files = sorted((*runtime_filenames(platform[1]), office, resources))
expected_files = sorted((*runtime_filenames(platform[1]), resources))
found_files = sorted({name.split("/runtime/", 1)[1].split("/", 1)[0] for name in runtime_payload})
if found_files != expected_files:
raise RuntimeError(f"{wheel} runtime payload must be {expected_files}, found {found_files}")
office_modules = f"deepseek_harness_runtime/runtime/{office}/node_modules"
verify_office_payload(archive, office_modules, platform[0])
validate = runpy.run_path(str(ROOT / "python/sdk-runtime/src/deepseek_harness_runtime/_resources.py"))["validate_resources"]
validate = runpy.run_path(str(ROOT / "python/sdk-runtime/src/deepseek_harness_runtime/_resources.py"))["validate_downloads"]
target = next(name for name, value in PLATFORMS.items() if value == platform)
validate(zipfile.Path(archive, f"deepseek_harness_runtime/runtime/{resources}/"), target)
for runtime_file in runtime_payload:
+2
View File
@@ -378,6 +378,8 @@ export function checkExperimentalNameExceptions(manifests: readonly WorkspaceMan
}
function isReleaseMemberDirectory(dir: string): boolean {
// The SSH carrier is distributed as executable archives, never as an npm application.
if (dir === 'packages/ssh/ssh-helper-runtime') return false
return standardReleaseMemberDirectory.test(dir) || isPublicExperimentalPackageDirectory(dir)
}
+130
View File
@@ -646,6 +646,28 @@ describe('CI workflow', () => {
})
})
it('publishes the scaled benchmark report even when a budget fails', () => {
const benchmark = workflowJob(loadWorkflow('.github/workflows/ci.yml'), 'node-24-bench')
if (!Array.isArray(benchmark.steps)) throw new TypeError('benchmark job must define steps')
expect(benchmark.steps).toContainEqual({
name: 'Upload scaled benchmark report',
if: '${{ !cancelled() }}',
uses: 'actions/[email protected]',
with: {
name: 'benchmark-report',
path: 'benchmarks/.dsh-report/',
'include-hidden-files': true,
'if-no-files-found': 'warn',
'retention-days': 7,
},
})
const uploadIndex = benchmark.steps.findIndex(step => isRecord(step) && step.name === 'Upload scaled benchmark report')
const benchmarkIndex = benchmark.steps.findIndex(step => isRecord(step) && step.name === 'Run performance benchmarks')
expect(uploadIndex).toBeGreaterThan(benchmarkIndex)
})
it('gives the Wine Host TypeScript compile the repository heap budget', () => {
const wineGates = readFileSync(resolve(root, 'scripts/wine-windows-gates.sh'), 'utf8')
@@ -693,6 +715,7 @@ describe('CI workflow', () => {
const NOT_PUSH_REACHABLE = new Set([
"github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark'",
"github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark'",
"github.event_name == 'workflow_dispatch' && inputs.suite == 'ssh-helper'",
])
const pushReachable = Object.entries(workflow.jobs)
.filter(([, job]) => {
@@ -778,6 +801,104 @@ describe('CI workflow', () => {
expect(aggregate.needs).toContain('python-runtime')
})
it('builds SSH executables only for explicit manual or release requests', () => {
const ci = loadWorkflow('.github/workflows/ci.yml')
if (!isRecord(ci.jobs)) throw new Error('PR CI must define jobs')
expect(Object.values(ci.jobs)).not.toContainEqual(expect.objectContaining({ uses: './.github/workflows/build-exe-for-ssh-helper.yml' }))
expect(workflowJob(ci, 'all-checks-passed').needs).not.toContain('ssh-helper')
const master = workflowJob(loadWorkflow('.github/workflows/ci-master.yml'), 'ssh-helper')
if (typeof master.if !== 'string') throw new Error('SSH manual caller must define its trigger condition')
for (const [event, suite, expected] of [
['push', 'ssh-helper', false], ['pull_request', 'ssh-helper', false],
['workflow_dispatch', 'larger-runner-benchmark', false], ['workflow_dispatch', 'ssh-helper', true],
] as const) {
expect(runInNewContext(master.if, { github: { event_name: event, ref: 'refs/heads/master' }, inputs: { suite } }, { timeout: 1000 })).toBe(expected)
}
expect(master.with).toEqual({ targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64' })
const workflow = loadWorkflow('.github/workflows/build-exe-for-ssh-helper.yml')
if (!isRecord(workflow.on)) throw new Error('SSH builder must define triggers')
expect(Object.keys(workflow.on).sort()).toEqual(['workflow_call', 'workflow_dispatch'])
})
it('requires SSH helper acceptance before uploading artifacts or publishing', () => {
const workflow = loadWorkflow('.github/workflows/build-exe-for-ssh-helper.yml')
const build = workflowJob(workflow, 'build')
if (!Array.isArray(build.steps)) throw new Error('SSH build must define its verification steps')
const steps = build.steps.filter(isRecord)
const artifact = steps.findIndex(step => String(step.run).includes('scripts/verify-ssh-helper-artifact.ts'))
const transport = steps.findIndex(step => String(step.run).includes('scripts/verify-ssh-helper-ssh.ts'))
const landlock = steps.findIndex(step => String(step.run).includes('--backend=landlock-run'))
const upload = steps.findIndex(step => String(step.uses).startsWith('actions/upload-artifact@'))
expect(artifact).toBeGreaterThan(-1)
expect(landlock).toBeGreaterThan(artifact)
expect(transport).toBeGreaterThan(artifact)
expect(upload).toBeGreaterThan(transport)
for (const index of [artifact, transport, landlock, upload]) expect(steps[index]).not.toHaveProperty('continue-on-error', true)
const publish = loadWorkflow('.github/workflows/publish-ssh-helper.yml')
expect(publish.permissions).toEqual({ contents: 'read' })
expect(workflowJob(publish, 'build')).toMatchObject({ needs: 'validate', with: { release: true } })
const verify = workflowJob(publish, 'verify')
expect(verify.needs).toEqual(['validate', 'build'])
expect(verify).not.toHaveProperty('if')
expect(verify).not.toHaveProperty('permissions')
if (!Array.isArray(verify.steps)) throw new Error('Release verification must define steps')
const verification = verify.steps.findIndex(step => isRecord(step) && String(step.run).includes('scripts/ssh-helper/release.ts'))
const checksums = verify.steps.findIndex(step => isRecord(step) && String(step.uses).startsWith('actions/upload-artifact@'))
expect(verification).toBeGreaterThan(-1)
expect(checksums).toBeGreaterThan(verification)
expect(verify.steps[verification]).not.toHaveProperty('continue-on-error', true)
const publication = workflowJob(publish, 'publish')
expect(publication).toMatchObject({ if: 'inputs.publish', needs: ['validate', 'verify'], permissions: { contents: 'write' } })
for (const requested of [false, true]) {
expect(runInNewContext(String(publication.if), { inputs: { publish: requested } }, { timeout: 1000 })).toBe(requested)
}
})
it('validates any ref by default but refuses publication without the matching SSH release tag', () => {
const workflow = loadWorkflow('.github/workflows/publish-ssh-helper.yml')
if (!isRecord(workflow.on)) throw new Error('SSH release must define triggers')
expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
expect(workflowEvent(workflow, 'workflow_dispatch').inputs).toMatchObject({ publish: { type: 'boolean', default: false } })
const validate = workflowJob(workflow, 'validate')
expect(validate.steps).toContainEqual(expect.objectContaining({ id: 'source', env: { PUBLISH: '${{ inputs.publish }}' } }))
const source = nodeStepSource(validate, 'source')
const evaluate = (publish: boolean, type: string, name: string): string => {
let output = ''
runInNewContext(source, {
process: { env: { PUBLISH: String(publish), GITHUB_REF_TYPE: type, GITHUB_REF_NAME: name, GITHUB_OUTPUT: 'output' } },
readFileSync: () => JSON.stringify({ version: '1.2.3' }),
execFileSync: () => `${'a'.repeat(40)}\n`,
appendFileSync: (_path: string, text: string) => { output += text },
}, { timeout: 1000 })
return output
}
const expected = `version=1.2.3\ncommit=${'a'.repeat(40)}\n`
expect(evaluate(false, 'branch', 'worktree/helper')).toBe(expected)
expect(evaluate(false, 'tag', 'arbitrary-tag')).toBe(expected)
expect(evaluate(true, 'tag', 'dsh-v1.2.3')).toBe(expected)
for (const [type, name] of [['branch', 'dsh-v1.2.3'], ['tag', 'dsh-v1.2.2'], ['tag', 'python-v1.2.3']] as const) {
expect(() => evaluate(true, type, name)).toThrow('Publication requires the matching dsh-v<version> tag')
}
})
it('validates the SSH native matrix before allocating runners', () => {
const plan = workflowJob(loadWorkflow('.github/workflows/build-exe-for-ssh-helper.yml'), 'plan')
const source = nodeStepSource(plan, 'targets')
const evaluate = (requested: string): unknown => {
let output = ''
runInNewContext(source, {
process: { env: { REQUESTED_TARGETS: requested, GITHUB_OUTPUT: 'output' } },
appendFileSync: (_path: string, text: string) => { output += text },
}, { timeout: 1000 })
return JSON.parse(output.slice('matrix='.length))
}
expect(evaluate('')).toHaveLength(4)
expect(evaluate('node24-linux-arm64')).toEqual([{ target: 'node24-linux-arm64', runner: 'ubuntu-24.04-arm' }])
for (const invalid of ['node24-linux-arm64,node24-linux-arm64', 'node24-win-x64', 'node24-linux-x64,', ' ']) {
expect(() => evaluate(invalid)).toThrow('Invalid or duplicate')
}
})
it('keeps every Vitest project process-isolated on native Windows', () => {
const config = readFileSync(resolve(root, 'vitest.config.ts'), 'utf8')
@@ -1416,6 +1537,15 @@ function loadWorkflow(path: string): Record<string, unknown> {
return workflow
}
function nodeStepSource(job: Record<string, unknown>, id: string): string {
if (!Array.isArray(job.steps)) throw new Error('Node workflow job must define steps')
const step: unknown = job.steps.find(value => isRecord(value) && value.id === id)
if (!isRecord(step) || typeof step.run !== 'string') throw new Error(`Missing Node workflow step: ${id}`)
const source = step.run.split("<<'JS'\n")[1]?.split('\nJS')[0]?.replace(/^import .*;\n/gm, '')
if (source === undefined) throw new Error(`Workflow step ${id} must contain an executable Node script`)
return source
}
function workflowEvent(workflow: Record<string, unknown>, event: string): Record<string, unknown> {
if (!isRecord(workflow.on) || !isRecord(workflow.on[event])) {
throw new TypeError(`workflow must define the ${event} event`)
+78 -25
View File
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { runGate } from './run-gates.ts'
import { PassThrough } from 'node:stream'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, type MockInstance, vi } from 'vitest'
import { ForksPoolWorker } from 'vitest/node'
import type { PoolOptions, PoolWorker, TestProject, WorkerRequest, WorkerResponse } from 'vitest/node'
import { coverageForkPool } from './coverage-fork-diagnostics.ts'
@@ -55,6 +55,43 @@ function poolOptions(root: string) {
return { options, error, outputStream, errorStream }
}
/** Observers a pool registers on its worker, once `stubPool` has started it. */
interface WorkerObservers {
off: MockInstance
exit?: (code: number | null, signal?: NodeJS.Signals | null) => void
error?: () => void
}
/**
* Start a pool whose worker prototypes are stubs, and capture the observers it registers.
* The test's cleanups own the pool and its streams.
*/
async function stubPool(): Promise<{
pool: PoolWorker
error: MockInstance<(message: string) => void>
observers: WorkerObservers
}> {
const observers: WorkerObservers = {
off: vi.spyOn(ForksPoolWorker.prototype, 'off').mockImplementation(() => undefined),
}
vi.spyOn(ForksPoolWorker.prototype, 'start').mockResolvedValue(undefined)
vi.spyOn(ForksPoolWorker.prototype, 'stop').mockResolvedValue(undefined)
vi.spyOn(ForksPoolWorker.prototype, 'send').mockImplementation(() => undefined)
vi.spyOn(ForksPoolWorker.prototype, 'on').mockImplementation((event, callback) => {
if (event === 'exit') observers.exit = callback
if (event === 'error') observers.error = callback as () => void
})
const { options, error, outputStream, errorStream } = poolOptions('/unused')
const pool = coverageForkPool.createPoolWorker(options)
cleanups.push(async () => {
await pool.stop()
outputStream.destroy()
errorStream.destroy()
})
await pool.start()
return { pool, error, observers }
}
async function start(root: string, diagnostic = true) {
const { options, error, outputStream, errorStream } = poolOptions(root)
const pool: PoolWorker = diagnostic ? coverageForkPool.createPoolWorker(options) : new ForksPoolWorker(options)
@@ -113,21 +150,33 @@ async function configuredPool(pool: 'stock' | 'diagnostic', outcome: 'exit' | 'p
}
}
/**
* Vitest reports one unexpected worker exit either as the clean exit path or, when
* the worker's channel closes under a send, as a runner-level worker error.
*/
function workerExitReport(output: string, pool: string): string | undefined {
return ['Worker exited unexpectedly', `Worker ${pool} emitted error`].find(message => output.includes(message))
}
describe('coverage fork configuration entry', () => {
it('preserves the real Vitest failure and adds metadata before its error report', async ({ signal }) => {
const [stock, diagnostic] = await Promise.all([
configuredPool('stock', 'exit', signal),
configuredPool('diagnostic', 'exit', signal),
])
for (const result of [stock, diagnostic]) {
const stockOutput = stock.output.map(chunk => chunk.text).join('')
const output = diagnostic.output.map(chunk => chunk.text).join('')
for (const [pool, result, text] of [
['forks', stock, stockOutput],
['coverage-forks', diagnostic, output],
] as const) {
expect(result.aborted).not.toBe(true)
expect(result.error).toBeUndefined()
expect(result.signalCode).toBeNull()
expect(result.exitCode).toBe(1)
expect(result.output.map(chunk => chunk.text).join('')).toContain('Worker exited unexpectedly')
expect(workerExitReport(text, pool)).toBeDefined()
}
const output = diagnostic.output.map(chunk => chunk.text).join('')
expect(stock.output.map(chunk => chunk.text).join('')).not.toContain('coverage-worker-exit:')
expect(stockOutput).not.toContain('coverage-worker-exit:')
const encoded = /coverage-worker-exit: (\{[^\r\n]*\})/u.exec(output)?.[1]
expect(encoded).toBeDefined()
const record = JSON.parse(encoded!) as {
@@ -147,7 +196,8 @@ describe('coverage fork configuration entry', () => {
})
expect(record.files.map(file => file.replaceAll('\\', '/')))
.toEqual([resolve('scripts/fixtures/coverage-fork.fixture.ts').replaceAll('\\', '/')])
expect(output.indexOf('coverage-worker-exit:')).toBeLessThan(output.indexOf('Worker exited unexpectedly'))
expect(output.indexOf('coverage-worker-exit:'))
.toBeLessThan(output.indexOf(workerExitReport(output, 'coverage-forks')!))
})
it('keeps a successful configured run silent and successful', async ({ signal }) => {
@@ -165,33 +215,36 @@ describe('coverage fork exit callback', () => {
['native receiver', { pid: 41 }, 41],
['unavailable receiver', undefined, null],
] as const)('retains a signal with %s and silences expected shutdown', async (_name, receiver, pid) => {
let exitListener: ((value: unknown) => void) | undefined
vi.spyOn(ForksPoolWorker.prototype, 'start').mockResolvedValue(undefined)
vi.spyOn(ForksPoolWorker.prototype, 'stop').mockResolvedValue(undefined)
vi.spyOn(ForksPoolWorker.prototype, 'send').mockImplementation(() => undefined)
vi.spyOn(ForksPoolWorker.prototype, 'off').mockImplementation(() => undefined)
vi.spyOn(ForksPoolWorker.prototype, 'on').mockImplementation((event, callback) => {
if (event === 'exit') exitListener = callback
})
const { options, error, outputStream, errorStream } = poolOptions('/unused')
const pool = coverageForkPool.createPoolWorker(options)
cleanups.push(async () => {
await pool.stop()
outputStream.destroy()
errorStream.destroy()
})
await pool.start()
const { pool, error, observers } = await stubPool()
pool.send(request('/signal.spec.ts', 9))
if (exitListener === undefined) throw new Error('exit observer was not registered')
Reflect.apply(exitListener, receiver, [null, 'SIGTERM'])
const exit = observers.exit
if (exit === undefined) throw new Error('exit observer was not registered')
Reflect.apply(exit, receiver, [null, 'SIGTERM'])
expect(error).toHaveBeenCalledWith(`coverage-worker-exit: ${JSON.stringify({
pid, project: 'fixture', workerId: 9, files: ['/signal.spec.ts'], exitCode: null, signalCode: 'SIGTERM',
})}`)
error.mockClear()
pool.send(stop)
expect(() => { Reflect.apply(exitListener!, undefined, [null, 'SIGTERM']) }).not.toThrow()
expect(() => { Reflect.apply(exit, undefined, [null, 'SIGTERM']) }).not.toThrow()
expect(error).not.toHaveBeenCalled()
})
it('reports an exit whose worker errored even when a teardown raced it', async () => {
const { pool, error, observers } = await stubPool()
pool.send(request('/raced.spec.ts', 11))
const failed = observers.error
if (failed === undefined) throw new Error('error observer was not registered')
// Node reports the failed reply before it reports the exit of the same child.
failed()
await pool.stop()
expect(observers.off).not.toHaveBeenCalled()
const exit = observers.exit
if (exit === undefined) throw new Error('exit observer was not registered')
Reflect.apply(exit, { pid: 42 }, [23, null])
expect(error).toHaveBeenCalledWith(`coverage-worker-exit: ${JSON.stringify({
pid: 42, project: 'fixture', workerId: 11, files: ['/raced.spec.ts'], exitCode: 23, signalCode: null,
})}`)
})
})
describe('coverage fork exit diagnostics', () => {
+22 -6
View File
@@ -5,12 +5,17 @@ import type { PoolOptions, PoolRunnerInitializer, WorkerRequest, WorkerResponse
const FINISHED_RESPONSE_TYPE: WorkerResponse['type'] = 'testfileFinished'
/** Attribute only outstanding requests; stop silences exits, while cancel does not. */
/**
* Attribute only outstanding requests; stop silences exits, while cancel and a
* worker error observed before that stop do not.
*/
class DiagnosticForkWorker extends ForksPoolWorker {
private expectedExit = false
private observing = false
private workerErrored = false
private request: Extract<WorkerRequest, { type: 'run' | 'collect' }> | undefined
private readonly reportExit: (this: ChildProcess | undefined, code: number | null, signal?: NodeJS.Signals | null) => void
private readonly observeError: () => void
constructor(options: PoolOptions) {
super(options)
@@ -27,17 +32,24 @@ class DiagnosticForkWorker extends ForksPoolWorker {
}
// ForksPoolWorker.on forwards Node ChildProcess events and their receiver.
this.reportExit = function (code, signal) { report(this, code, signal) }
this.observeError = () => {
// A channel that closed under a send means the worker is not shutting down
// orderly, so the exit that follows keeps its code and signal. A stop
// requested before this error still silences that exit.
if (!this.expectedExit) this.workerErrored = true
}
}
override async start(): Promise<void> {
await super.start()
this.on('exit', this.reportExit)
this.on('error', this.observeError)
this.observing = true
}
override send(message: WorkerRequest): void {
if (message.type === 'run' || message.type === 'collect') this.request = message
if (message.type === 'stop') this.expectedExit = true
if (message.type === 'stop' && !this.workerErrored) this.expectedExit = true
super.send(message)
}
@@ -53,10 +65,14 @@ class DiagnosticForkWorker extends ForksPoolWorker {
}
override async stop(): Promise<void> {
this.expectedExit = true
if (this.observing) {
this.off('exit', this.reportExit)
this.observing = false
// A worker that already errored is not shutting down orderly; keep the exit
// observer so its code and signal still reach the coverage log.
if (!this.workerErrored) {
this.expectedExit = true
if (this.observing) {
this.off('exit', this.reportExit)
this.observing = false
}
}
await super.stop()
}
+1
View File
@@ -70,6 +70,7 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
'packages/sandbox/sandbox-windows-acl': 'Windows ACL sandbox library consumed by sandbox-local.',
'packages/sdk/client': 'Client-process library; the spawned runtime owns plugin behavior.',
'packages/sdk/protocol': 'Wire-protocol library with type declarations only.',
'packages/ssh/ssh-helper-runtime': 'Private executable entry consumed by the packaging pipeline; no Cordis plugin export.',
'packages/session/session-format': 'Pure Session format planning, codec dispatch, and lossless JSON library.',
'packages/session/session-format-catalog': 'Generated build-static Session format inventory with no plugin registration.',
'packages/session/session-format-v0-to-v1': 'Pure released-v0 codec and adjacent migration library.',
@@ -1,4 +1,4 @@
/** Resolve the native node-pty input used by the Python SDK runtime builder. */
/** Resolve the native node-pty input used by the SDK and SSH executable builders. */
import { existsSync } from 'node:fs'
import { join } from 'node:path'
@@ -18,7 +18,7 @@ export function resolveLinuxNodePtyAddon(
const prebuilt = join(packageDirectory, 'prebuilds', `linux-${arch}`, 'pty.node')
if (existsSync(prebuilt)) return prebuilt
throw new Error(
`build-exe-for-python-sdk: node-pty addon is absent from both ${built} and ${prebuilt}.`,
`executable-native-pty: node-pty addon is absent from both ${built} and ${prebuilt}.`,
)
}
@@ -39,7 +39,7 @@ export function resolveWindowsNodePtyAddons(
]
const missing = addons.filter(path => !existsSync(path))
if (missing.length > 0) {
throw new Error(`build-exe-for-python-sdk: Windows node-pty addons are missing: ${missing.join(', ')}.`)
throw new Error(`executable-native-pty: Windows node-pty addons are missing: ${missing.join(', ')}.`)
}
return addons
}
+47
View File
@@ -0,0 +1,47 @@
import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it, onTestFinished } from 'vitest'
import { restoreLegacyHoists } from './executable-packaging.ts'
import { materializeStagedLinks } from './build-exe-for-python-sdk-staging.ts'
async function fixture() {
const root = await mkdtemp(join(tmpdir(), 'dsh-exe-deploy-'))
onTestFinished(() => rm(root, { recursive: true, force: true }))
const source = join(root, 'source')
const staging = join(root, 'staging')
await mkdir(source)
await mkdir(join(staging, 'node_modules'), { recursive: true })
return { root, source, staging, modules: join(staging, 'node_modules') }
}
it('restores omitted direct peers without copying their nested module instances', async () => {
const test = await fixture()
await writeFile(join(test.staging, 'package.json'), JSON.stringify({ dependencies: { peer: 'workspace:^' } }))
await mkdir(join(test.source, 'peer/node_modules/shadow'), { recursive: true })
await writeFile(join(test.source, 'peer/index.js'), 'export const identity = 1\n')
await writeFile(join(test.source, 'peer/node_modules/shadow/index.js'), 'shadow')
expect(await restoreLegacyHoists(test.staging, test.source)).toEqual(['peer'])
expect(await readFile(join(test.modules, 'peer/index.js'), 'utf8')).toBe('export const identity = 1\n')
await expect(lstat(join(test.modules, 'peer/node_modules'))).rejects.toThrow('ENOENT')
expect(await restoreLegacyHoists(test.staging, test.source)).toEqual([])
})
it('rejects a missing dependency instead of packaging an incomplete graph', async () => {
const test = await fixture()
await writeFile(join(test.staging, 'package.json'), JSON.stringify({ dependencies: { missing: 'workspace:^' } }))
await expect(restoreLegacyHoists(test.staging, test.source)).rejects.toThrow('absent from both')
})
it('materializes a package junction without deleting its source and drops linked command directories', async () => {
const test = await fixture()
await mkdir(join(test.source, 'peer'))
await writeFile(join(test.source, 'peer/index.js'), 'intact')
await symlink(join(test.source, 'peer'), join(test.modules, 'peer'), 'junction')
await symlink(join(test.source, 'peer'), join(test.modules, '.bin'), 'junction')
await materializeStagedLinks(test.staging)
expect((await lstat(join(test.modules, 'peer'))).isSymbolicLink()).toBe(false)
expect(await readFile(join(test.modules, 'peer/index.js'), 'utf8')).toBe('intact')
expect(await readFile(join(test.source, 'peer/index.js'), 'utf8')).toBe('intact')
await expect(lstat(join(test.modules, '.bin'))).rejects.toThrow('ENOENT')
})
+70
View File
@@ -0,0 +1,70 @@
/** Shared production-deploy preparation for the SDK and SSH executable carriers. */
import { existsSync } from 'node:fs'
import { cp, mkdir, readFile } from 'node:fs/promises'
import { dirname, extname, join, resolve, sep } from 'node:path'
/**
* Resolve pnpm without invoking a command shell, including Windows shims.
* @param args - pnpm arguments.
* @returns the executable and its argument vector.
*/
export function pnpmInvocation(args: string[]): [command: string, args: string[]] {
const entrypoint = process.env.npm_execpath?.trim()
if (entrypoint !== undefined && entrypoint !== '') {
const extension = extname(entrypoint).toLowerCase()
if (extension === '.js' || extension === '.cjs' || extension === '.mjs') {
return [process.execPath, [entrypoint, ...args]]
}
if (extension !== '.cmd') return [entrypoint, args]
}
const home = process.env.PNPM_HOME?.trim()
if (home !== undefined && home !== '') {
const packageBin = resolve(home, '..', 'pnpm', 'bin')
for (const filename of ['pnpm.mjs', 'pnpm.cjs']) {
const candidate = resolve(packageBin, filename)
if (existsSync(candidate)) return [process.execPath, [candidate, ...args]]
}
}
if (process.platform === 'win32') {
throw new Error('executable-packaging: pnpm must expose a JavaScript entrypoint through npm_execpath or PNPM_HOME on Windows.')
}
return ['pnpm', args]
}
/**
* Restore direct dependencies omitted by pnpm's legacy hoister, excluding nested module trees.
* @param staging - deployed runtime root.
* @param sourceNodeModules - deploy source's installed dependencies.
* @returns names restored from the source installation.
*/
export async function restoreLegacyHoists(staging: string, sourceNodeModules: string): Promise<string[]> {
const manifestPath = join(staging, 'package.json')
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as {
dependencies?: Record<string, string>
}
const restored: string[] = []
for (const dependency of Object.keys(manifest.dependencies ?? {}).sort()) {
const destination = join(staging, 'node_modules', dependency)
if (existsSync(destination)) continue
const source = join(sourceNodeModules, dependency)
if (!existsSync(source)) {
throw new Error(
`executable-packaging: deployed dependency ${dependency} is absent from both ${destination} and ${source}.`,
)
}
await mkdir(dirname(destination), { recursive: true })
const nestedNodeModules = join(source, 'node_modules')
await cp(source, destination, {
recursive: true,
dereference: true,
filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
})
restored.push(dependency)
}
const stillMissing = Object.keys(manifest.dependencies ?? {})
.filter(dependency => !existsSync(join(staging, 'node_modules', dependency)))
if (stillMissing.length > 0) {
throw new Error(`executable-packaging: staged dependencies remain missing: ${stillMissing.join(', ')}.`)
}
return restored
}
+1 -1
View File
@@ -15,7 +15,7 @@ export async function apply(ctx, config) {
if (json === undefined) throw new Error('Office skill did not supply CLI paths')
const { libreofficeKit: { node, cli } } = JSON.parse(json)
const options = { env: { ...process.env, PATH: '' }, timeout: 120_000 }
const capabilities = await promisify(execFile)(node, [cli, 'capabilities'], options)
const capabilities = await promisify(execFile)(node, [cli, 'capabilities', '--json'], options)
await promisify(execFile)(node, [cli, 'convert', '--input', config.input, '--output', config.output + '.cli.pdf'], options)
const pdf = await readFile(config.output + '.cli.pdf')
if (pdf.subarray(0, 5).toString() !== '%PDF-') throw new Error('Skill CLI did not create a PDF')
+1 -1
View File
@@ -772,7 +772,7 @@ const SERVICE_ROLES: ServiceRole[] = [
title: 'Agent Teams coordination domain',
mode: 'core',
consumers: ['experimental-tool-agent-team'],
note: 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes model controls.',
note: 'Owns the implicit-root roster, direct peer inbox delivery, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes model controls.',
},
{
key: 'claudeCodeMods',
+1 -1
View File
@@ -563,7 +563,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
dir: 'tool-agent-team',
source: 'packages/experimental/tool-agent-team/src/index.ts',
requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.agentTeams', 'an exact live Team member Agent'],
writes: ['tool/call', 'team/member', 'team/message/queued', 'team/message/delivered', 'team/task', 'tool/result'],
writes: ['tool/call', 'team/member', 'team/task', 'agent/inbox/spliced', 'user/message on inbox claim', 'tool/result'],
async mount(ctx) {
await ctx.plugin(AgentRegistry)
await ctx.plugin(SessionStore)
+2 -17
View File
@@ -387,9 +387,6 @@
"packages/bundle/web-app/tests/startup.spec.ts": {
"2e78263d16d54ccfd8d322bcdf1bf738bd8134e15c24063ece9a1d1b52f4ebd9": 1
},
"packages/bundle/web-app/tests/web-app.spec.ts": {
"603938cefaa75059bc3902dedc4cd5cbfb2d6d3f6e56bb3cd89788e21ece0389": 1
},
"packages/client/connection/src/browser-auth.ts": {
"98acffd14d7c77e3412ce5a7d8e9c555b66de76eb12c14d4b22b251684b60447": 1
},
@@ -462,9 +459,6 @@
"packages/client/ui-attachment/tests/composer-attachments.client.spec.tsx": {
"ab22ebe23f59b5aa76d42502a0421f3953d1d170e8a4f900a0c442daf6f1397d": 1
},
"packages/client/ui-chat/src/client/chat/ChatView.tsx": {
"50ce0a6085bdfd7032e5a391ed34cd078768ad8f46cc885eb54571cf8edd4bee": 1
},
"packages/client/ui-chat/src/client/conversation-nodes/turn-process.ts": {
"a7d8cc8d42e37ad5cc4882241492ac572ed8163c7fb8663180e893c7178e906f": 1
},
@@ -1050,10 +1044,6 @@
"packages/experimental/agent-team/src/journal.ts": {
"364930a7ddcc79316a9ffc126602f45ae56a3cecdb80484c5c8a79686f8f1057": 1
},
"packages/experimental/agent-team/tests/persistence.spec.ts": {
"3ae979a4364fe22772c89133e79ea216cdb4f3163c7cb02c47eb1a5d4788e661": 1,
"a90b2e60ce1efd280f1956999454dd0b57ea788864c0162cc6e201ba0c7a90a8": 1
},
"packages/experimental/agent-team/tests/projection-events.spec.ts": {
"71ffbcca0bb71d3b595143632491d4c5d580aaf6ac384c22116e038995b2f2b5": 1,
"a66c8967969859328890e6049a0bd9e90403861eb19ed5d6937da5505aa79c05": 1,
@@ -1061,8 +1051,8 @@
"d8076a64f8d940fb0a5c3df15e9e1c8fb7ac05e84762a6198dbbfcffcf379821": 1
},
"packages/experimental/agent-team/tests/team.spec.ts": {
"3ae979a4364fe22772c89133e79ea216cdb4f3163c7cb02c47eb1a5d4788e661": 4,
"ae5776af8c1e9965a4eca94bcbc07b65da8d2e9e24e4570d2141b34d60b4f90f": 3
"3ae979a4364fe22772c89133e79ea216cdb4f3163c7cb02c47eb1a5d4788e661": 3,
"ae5776af8c1e9965a4eca94bcbc07b65da8d2e9e24e4570d2141b34d60b4f90f": 1
},
"packages/experimental/auto-review/tests/auto-review.spec.ts": {
"5bb6193769cfbb9fc09abd705fed714917784de269546fde14ac1171fb00644b": 1
@@ -1454,11 +1444,6 @@
"aa5de2d4958ec64219565d53ec5a9d7f40f1f60037a2d52d1a9c3c65db034d73": 1,
"be027af1ef5cb3bfe41eeb19b54dbb176ec7c23ae367d709059839f9851833d5": 1
},
"packages/ptc-runtime/ptc-runtime-node/tests/launch.spec.ts": {
"57f598f462c77fae2b147b07c2e1228199c53dfd43652021af57712b4860732a": 1,
"9ddf7336f94d8ff4be8fe0ae7977c1846e6e92e7ea66180f8e2e6f8902b3c915": 1,
"e02136d8ab1f1fb84c6e772bbc863ceb3a0a3801f6eb158d5b9be074baa0885c": 1
},
"packages/ptc-runtime/ptc-runtime-node/tests/output-json.spec.ts": {
"8c4c486bbc27559714f4e0aaf376c72c49ac9157539b03c6c12853b8d3fc6e74": 1
},
+28 -58
View File
@@ -67,49 +67,26 @@ describe.each(['web', 'desktop'] as const)('%s optional bundle overrides', (surf
const warnings: string[] = []
const entries = composeEntries([...shipped, ...selectedNames.map(name => bundle(name).patches), [
{ id: 'skill-badge', disabled: true },
...['preset-standard', 'preset-cordis', 'preset-ptc'].map(preset => ({ preset, id: 'tool-ralph', config, disabled: true })),
{ id: 'tool-ralph', config, disabled: true },
]], warning => warnings.push(warning))
expect(warnings).toEqual([])
expect(entries.find(row => row.id === 'skill-badge')).toMatchObject({ name: '@deepseek-ai/dsh-skill-badge', disabled: true })
for (const preset of ['preset-standard', 'preset-cordis', 'preset-ptc']) {
const roots = presetRows(entries, preset)
expect(flattenRows(roots).find(row => row.id === 'tool-ralph'))
.toMatchObject({ name: '@deepseek-ai/dsh-tool-ralph', config, disabled: true })
expect(roots.find(row => row.id === 'optional-ralph')?.isolate).toEqual({ workflowEngine: true })
}
})
it('keeps a later complete preset replacement and its manually declared disabled tool', () => {
const shipped = profileLayers(surface)
const baseline = composeEntries(shipped)
const preset = baseline.find(row => row.id === 'preset-standard')!
const config = { ...preset.config as object, plugins: [...presetRows(baseline, 'preset-standard'), {
id: 'tool-ralph', name: '@deepseek-ai/dsh-tool-ralph', disabled: true,
config: { subagentProvider: 'spawn', maxRounds: 2 },
}] }
const warnings: string[] = []
const entries = composeEntries([...shipped, ...selectedNames.map(name => bundle(name).patches), [
{ id: 'preset-standard', config },
]], warning => warnings.push(warning))
expect(warnings).toEqual([])
expect(entries.find(row => row.id === 'preset-standard')?.config).toEqual(config)
expect(presetRows(entries, 'preset-standard').some(row => row.id === 'optional-ralph')).toBe(false)
const inherited = flattenRows(presetRows(entries, 'preset-cordis')).find(row => row.id === 'tool-ralph')
expect(inherited).toMatchObject({ name: '@deepseek-ai/dsh-tool-ralph' })
expect(inherited?.disabled).not.toBe(true)
expect(flattenRows(entries).find(row => row.id === 'tool-ralph'))
.toMatchObject({ name: '@deepseek-ai/dsh-tool-ralph', config, disabled: true })
expect(entries.find(row => row.id === 'optional-ralph')?.isolate).toEqual({ workflowEngine: true })
})
it('does not select absent bundles through enable-only row overrides', () => {
const warnings: string[] = []
const entries = composeEntries([...profileLayers(surface), [
{ id: 'skill-badge', disabled: false },
{ preset: 'preset-standard', id: 'tool-ralph', disabled: false },
{ id: 'tool-ralph', disabled: false },
]], warning => warnings.push(warning))
expect(warnings).toHaveLength(2)
expect(warnings.some(warning => warning.includes('skill-badge'))).toBe(true)
expect(warnings.some(warning => warning.includes('tool-ralph'))).toBe(true)
expect(entries.some(row => row.id === 'skill-badge')).toBe(false)
expect(flattenRows(presetRows(entries, 'preset-standard')).some(row => row.id === 'tool-ralph')).toBe(false)
expect(flattenRows(entries).some(row => row.id === 'tool-ralph')).toBe(false)
})
})
@@ -129,10 +106,9 @@ describe('optional bundles', () => {
const warnings: string[] = []
const composed = composeEntries([...shipped, patches], message => warnings.push(message))
expect(warnings).toEqual([])
// Inserted ids remain addressable inside their selected profile or preset tree.
// Inserted ids remain addressable in the composed profile tree.
const ids = new Set(flattenRows(composed).map(entry => entry.id))
for (const patch of patches) {
const rows = typeof patch.preset === 'string' ? presetRows(composed, patch.preset) : composed
const ids = new Set(flattenRows(rows).map(entry => entry.id))
for (const row of patch.insert ?? []) {
expect(typeof row.id).toBe('string')
expect(ids.has(row.id)).toBe(true)
@@ -146,7 +122,7 @@ describe('optional bundles', () => {
// row, and the override keeps the package the shipped layer declared on it.
const shippedComposed = composeEntries([...shipped])
for (const patch of patches) {
if (patch.insert !== undefined || typeof patch.id !== 'string' || patch.preset !== undefined) continue
if (patch.insert !== undefined || typeof patch.id !== 'string') continue
const matches = composed.filter(entry => entry.id === patch.id)
expect(matches).toHaveLength(1)
expect(matches[0]?.name).toBe(shippedComposed.find(entry => entry.id === patch.id)?.name)
@@ -172,7 +148,7 @@ describe('optional bundles', () => {
}
})
it('composes every lightweight selection without changing minimal or adding host tools', () => {
it('composes every lightweight selection as Host rows without changing any preset', () => {
const baseline = composeEntries(shipped)
const layers = lightweight.map(name => bundle(name).patches)
for (let mask = 0; mask < 2 ** lightweight.length; mask += 1) {
@@ -180,35 +156,29 @@ describe('optional bundles', () => {
const warnings: string[] = []
const composed = composeEntries([...shipped, ...selected], warning => warnings.push(warning))
expect(warnings).toEqual([])
expect(presetRows(composed, 'preset-minimal')).toEqual(presetRows(baseline, 'preset-minimal'))
const addedHost = composed.filter(row => !baseline.some(existing => existing.id === row.id))
expect(addedHost.every(row => ['skill-badge', 'optional-session-title-all-prompts'].includes(row.id ?? ''))).toBe(true)
for (const id of ['preset-standard', 'preset-cordis', 'preset-ptc']) {
const rows = flattenRows(presetRows(composed, id))
const ids = rows.map(row => row.id)
expect(new Set(ids).size).toBe(ids.length)
for (const id of ['preset-standard', 'preset-cordis', 'preset-ptc', 'preset-minimal']) {
expect(presetRows(composed, id)).toEqual(presetRows(baseline, id))
}
const ids = flattenRows(composed).map(row => row.id)
expect(new Set(ids).size).toBe(ids.length)
}
})
it('contributes tools inside all full presets with independently isolated services', () => {
it('contributes global tools from Host rows with independently isolated services', () => {
const composed = composeEntries([...shipped, ...lightweight.map(name => bundle(name).patches)])
for (const id of ['preset-standard', 'preset-cordis', 'preset-ptc']) {
const roots = presetRows(composed, id)
const names = flattenRows(roots).map(row => row.name)
for (const name of [
'@deepseek-ai/dsh-tool-session-query',
'@deepseek-ai/dsh-tool-ralph', '@deepseek-ai/dsh-tool-terminal',
]) expect(names).toContain(name)
expect(roots.find(row => row.id === 'optional-ralph')?.isolate).toEqual({ workflowEngine: true })
expect(roots.find(row => row.id === 'optional-persistent-terminals')?.isolate).toEqual({ terminals: true })
const search = roots.find(row => row.id === 'optional-session-search')
expect(search?.isolate).toEqual({ sessionQuery: true })
expect(search?.config).toEqual([
{ id: 'optional-session-query-sqlite', name: '@deepseek-ai/dsh-session-query-sqlite', config: { path: ':memory:', openAt: 'first-search' } },
{ id: 'optional-tool-session-query', name: '@deepseek-ai/dsh-tool-session-query' },
])
}
const names = flattenRows(composed).map(row => row.name)
for (const name of [
'@deepseek-ai/dsh-tool-session-query',
'@deepseek-ai/dsh-tool-ralph', '@deepseek-ai/dsh-tool-terminal',
]) expect(names).toContain(name)
expect(composed.find(row => row.id === 'optional-ralph')?.isolate).toEqual({ workflowEngine: true })
expect(composed.find(row => row.id === 'optional-persistent-terminals')?.isolate).toEqual({ terminals: true })
const search = composed.find(row => row.id === 'optional-session-search')
expect(search?.isolate).toEqual({ sessionQuery: true })
expect(search?.config).toEqual([
{ id: 'optional-session-query-sqlite', name: '@deepseek-ai/dsh-session-query-sqlite', config: { path: ':memory:', openAt: 'first-search' } },
{ id: 'optional-tool-session-query', name: '@deepseek-ai/dsh-tool-session-query' },
])
expect(composed.find(row => row.id === 'session-query-sqlite')?.config).toEqual({ path: ':memory:', openAt: 'never' })
expect(composed.find(row => row.id === 'session-title-llm')?.disabled).toBe(true)
expect(composed.find(row => row.id === 'optional-session-title-all-prompts')?.name)
+162 -67
View File
@@ -224,7 +224,7 @@ describe('accepted persistence baseline', () => {
}
})
it.each(['required field', 'changed type'] as const)('refuses a breaking V4 %s before rendering or writing', (kind) => {
it.each(['required field', 'changed type'] as const)('requires an explicit decision for V4 %s before rendering or writing', (kind) => {
const root = fixture()
finalize(root)
const after = inventory(kind === 'required field' ? { value: 'number', label: 'string' } : { value: 'boolean' }, 4)
@@ -234,13 +234,13 @@ describe('accepted persistence baseline', () => {
rendered = true
return []
})) as { ok: boolean; code: string; changes: PersistenceTypeChange[] }
expect(result).toMatchObject({ ok: false, code: 'finalized-format-changed' })
expect(result).toMatchObject({ ok: false, code: 'decision-required' })
expect(result.changes.length).toBeGreaterThan(0)
expect(result.changes.some(change => change.requiresVersionBump)).toBe(true)
expect(result.changes.some(change => change.requiresCompatibilityReview)).toBe(true)
expect(rendered).toBe(false)
expect(contents(root)).toEqual(before)
commitCurrent(root, after)
expect(() => verifyPersistenceChanges(root, after)).toThrow('Breaking changes relative to the accepted Session format 4 baseline')
expect(() => verifyPersistenceChanges(root, after)).toThrow('unacknowledged persistence type changes')
})
it('allows qualified V4 attribution additions through a new same-version acknowledgement', () => {
@@ -251,7 +251,7 @@ describe('accepted persistence baseline', () => {
finalize(root, current)
const added = attributedRoot([EXISTING_SOURCE, { kind: 'new-attribution' }], { policy: attributionPolicy(['new-attribution']) })
const after = { ...current, roots: current.roots.map(root => root.key === source.key ? added : root) }
expect(classifyPersistenceChange(source, added)).toEqual([expect.objectContaining({ kind: 'attribution-kind-added', requiresVersionBump: false })])
expect(classifyPersistenceChange(source, added)).toEqual([expect.objectContaining({ kind: 'attribution-kind-added', requiresCompatibilityReview: false })])
const before = contents(root)
runPersistenceChanges(['--record', COMPATIBLE_ID, '--prose', proseFile(root)], root, () => after)
expect(runPersistenceChanges(['--check'], root, () => after)).toContain('roots match')
@@ -262,7 +262,7 @@ describe('accepted persistence baseline', () => {
}
})
it('keeps later unaccepted compatible records editable and rejects a breaking successor without its own version transition', () => {
it('keeps later unaccepted records editable and requires an explicit decision for a changed optional field', () => {
const root = fixture()
finalize(root)
const first = inventory({ value: 'number', 'label?': 'string' }, 4)
@@ -273,10 +273,10 @@ describe('accepted persistence baseline', () => {
expect(loadPersistenceFinalization(root, loadPersistenceHistory(root))?.acceptedRecords.has(COMPATIBLE_ID)).toBe(false)
const breaking = inventory({ value: 'number', 'label?': 'number', 'extra?': 'boolean' }, 4)
const accepted = inventory({ value: 'number' }, 4).roots[2]!
expect(classifyPersistenceChange(accepted, breaking.roots[2]!).every(change => !change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(accepted, breaking.roots[2]!).every(change => !change.requiresCompatibilityReview)).toBe(true)
const before = contents(root)
expect(() => runPersistenceChanges(['--record', '2026-09-20-breaking-v4', '--prose', proseFile(root)], root, () => breaking))
.toThrow("this record's own increasing SessionHeader.version")
.toThrow('--decision')
expect(contents(root)).toEqual(before)
})
@@ -303,7 +303,7 @@ describe('accepted persistence baseline', () => {
expect(() => runPersistenceChanges(['--update', FINALIZED_ID], root, () => after)).toThrow('cannot update finalized acknowledgement')
const later = inventory({ value: 'string' }, 5)
expect(() => runPersistenceChanges(['--record', '2026-09-20-without-version', '--prose', proseFile(root)], root, () => later))
.toThrow("this record's own increasing SessionHeader.version")
.toThrow('--decision')
})
it.each([4, 5])('detects a consistently rewritten accepted record while the writer is V%s', (version) => {
@@ -429,7 +429,7 @@ describe('persistence change classification', () => {
finalize(root, before)
const after = { ...before, roots: before.roots.map(root => root.kind === 'event' ? nextEvent : root) }
expect(classifyPersistenceChange(oldEvent, nextEvent))
.toEqual([expect.objectContaining({ kind: 'payload-version-added', requiresVersionBump: false })])
.toEqual([expect.objectContaining({ kind: 'payload-version-added', requiresCompatibilityReview: false })])
runPersistenceChanges(['--record', COMPATIBLE_ID, '--prose', proseFile(root)], root, () => after)
verifyPersistenceChanges(root, after)
})
@@ -443,29 +443,29 @@ describe('persistence change classification', () => {
versionedEvent([...variants, { version: 0.5, mode: 'unknown' }]),
versionedEvent([{ version: 0, mode: 'one-shot', extra: true }, variants[1]!, { version: 1, mode: 'unknown' }]),
versionedEvent([{ version: 1, mode: 'unknown' }]),
]) expect(classifyPersistenceChange(before, after).some(change => change.requiresVersionBump)).toBe(true)
]) expect(classifyPersistenceChange(before, after).some(change => change.requiresCompatibilityReview)).toBe(true)
const expanded = versionedEvent([...variants, { version: 1, mode: 'unknown' }])
expect(classifyPersistenceChange(expanded, before).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(expanded, before).some(change => change.requiresCompatibilityReview)).toBe(true)
expect(classifyPersistenceChange({ ...before, surface: true }, { ...expanded, surface: true })
.some(change => change.requiresVersionBump)).toBe(true)
.some(change => change.requiresCompatibilityReview)).toBe(true)
expect(classifyPersistenceChange({ ...before, kind: 'header' }, { ...expanded, kind: 'header' })
.some(change => change.requiresVersionBump)).toBe(true)
.some(change => change.requiresCompatibilityReview)).toBe(true)
const unversioned = unionBody([[{ name: 'mode', type: 2, optional: false }]])
expect(classifyPersistenceChange(unversioned, expanded).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(unversioned, expanded).some(change => change.requiresCompatibilityReview)).toBe(true)
})
it('treats a new optional payload subtree as one additive change even with required descendants', () => {
const before = typeRoot('event:example/value', { value: 'string' })
const after = typeRoot('event:example/value', { value: 'string', 'details?': { name: 'string', count: 'number' } })
expect(classifyPersistenceChange(before, after)).toEqual([{ path: 'event:example/value.data.details', kind: 'optional-property-added', description: 'optional property added', requiresVersionBump: false }])
expect(classifyPersistenceChange(before, after)).toEqual([{ path: 'event:example/value.data.details', kind: 'optional-property-added', description: 'optional property added', requiresCompatibilityReview: false }])
})
it('permits required-to-optional payload properties while rejecting opposite changes, type changes, and removals', () => {
const required = typeRoot('event:example/value', { value: 'string' })
const optional = typeRoot('event:example/value', { 'value?': 'string' })
expect(classifyPersistenceChange(required, optional).every(change => !change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(required, optional).every(change => !change.requiresCompatibilityReview)).toBe(true)
for (const after of [required, typeRoot('event:example/value', { 'value?': 'number' }), typeRoot('event:example/value', {})]) {
expect(classifyPersistenceChange(optional, after).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(optional, after).some(change => change.requiresCompatibilityReview)).toBe(true)
}
})
@@ -485,7 +485,7 @@ describe('persistence change classification', () => {
}
const differences = classifyPersistenceChange(unionRoot(false), unionRoot(true))
expect(differences.length).toBeGreaterThan(0)
expect(differences.every(change => !change.requiresVersionBump)).toBe(true)
expect(differences.every(change => !change.requiresCompatibilityReview)).toBe(true)
})
it('allows optional additions across undiscriminated union arms', () => {
@@ -493,7 +493,7 @@ describe('persistence change classification', () => {
const second = [{ name: 'b', type: 3, optional: false }]
const extra = { name: 'x', type: 4, optional: true }
const differences = classifyPersistenceChange(unionBody([first, second]), unionBody([[...first, extra], [...second, extra]]))
expect(differences).toEqual([expect.objectContaining({ path: 'event:example/value.data.x', requiresVersionBump: false })])
expect(differences).toEqual([expect.objectContaining({ path: 'event:example/value.data.x', requiresCompatibilityReview: false })])
})
it('allows required-to-optional fields across undiscriminated union arms', () => {
@@ -501,7 +501,7 @@ describe('persistence change classification', () => {
const after = unionBody([[{ name: 'a', type: 2, optional: true }], [{ name: 'b', type: 3, optional: true }]])
const differences = classifyPersistenceChange(before, after)
expect(differences.map(change => change.path).sort()).toEqual(['event:example/value.data.a', 'event:example/value.data.b'])
expect(differences.every(change => change.kind === 'property-made-optional' && !change.requiresVersionBump)).toBe(true)
expect(differences.every(change => change.kind === 'property-made-optional' && !change.requiresCompatibilityReview)).toBe(true)
})
it('finds a complete matching when one union arm has multiple additive successors', () => {
@@ -513,10 +513,10 @@ describe('persistence change classification', () => {
for (const after of [unionBody([flexible, constrained]), unionBody([constrained, flexible])]) {
const differences = classifyPersistenceChange(before, after)
expect(differences.length).toBeGreaterThan(0)
expect(differences.every(change => !change.requiresVersionBump)).toBe(true)
expect(differences.every(change => !change.requiresCompatibilityReview)).toBe(true)
}
const noCompleteMatching = unionBody([flexible, [{ name: 'different', type: 4, optional: true }]])
expect(classifyPersistenceChange(before, noCompleteMatching).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(before, noCompleteMatching).some(change => change.requiresCompatibilityReview)).toBe(true)
})
it('keeps union cardinality, property removal, new required fields, and value types strict', () => {
@@ -531,7 +531,9 @@ describe('persistence change classification', () => {
unionBody([[...first, { name: 'x', type: 4, optional: false }], [...second, { name: 'x', type: 4, optional: true }]]),
unionBody([[{ name: 'a', type: 3, optional: true }], [{ name: 'b', type: 3, optional: true }]]),
]
for (const after of variants) expect(classifyPersistenceChange(before, after).some(change => change.requiresVersionBump)).toBe(true)
for (const after of variants) {
expect(classifyPersistenceChange(before, after).some(change => change.requiresCompatibilityReview)).toBe(true)
}
})
it('checks all recursive union arms without retaining provisional successes between candidates', () => {
@@ -541,10 +543,10 @@ describe('persistence change classification', () => {
const extra = { name: 'x', type: 4, optional: true }
const before = unionBody([first, second])
const additive = unionBody([[...first, extra], [...second, extra]])
expect(classifyPersistenceChange(before, additive).every(change => !change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(before, additive).every(change => !change.requiresCompatibilityReview)).toBe(true)
const invalid = unionBody([[...first, extra], [{ name: 'b', type: 2, optional: false }, next, extra]])
expect(classifyPersistenceChange(before, invalid).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(invalid, before).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(before, invalid).some(change => change.requiresCompatibilityReview)).toBe(true)
expect(classifyPersistenceChange(invalid, before).some(change => change.requiresCompatibilityReview)).toBe(true)
})
it('preserves body scope through arrays and tuples and rejects a simultaneous value-type change', () => {
@@ -559,18 +561,18 @@ describe('persistence change classification', () => {
const schema = canonicalizeSchema(nodes, 0)
return { ...typeRoot('event:example/value', {}), schema, digest: schemaDigest(schema) }
}
expect(classifyPersistenceChange(wrapped(false), wrapped(true)).every(change => !change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(wrapped(false), wrapped(true, 'number')).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(wrapped(false), wrapped(true)).every(change => !change.requiresCompatibilityReview)).toBe(true)
expect(classifyPersistenceChange(wrapped(false), wrapped(true, 'number')).some(change => change.requiresCompatibilityReview)).toBe(true)
})
it('keeps optional header/envelope properties and surface event additions strict', () => {
for (const key of ['SessionHeader', 'JsonlHeaderLine', 'SessionEventEnvelope']) {
expect(classifyPersistenceChange(typeRoot(key, {}), typeRoot(key, { 'metadata?': 'string' }))[0]?.requiresVersionBump).toBe(true)
expect(classifyPersistenceChange(typeRoot(key, {}), typeRoot(key, { 'metadata?': 'string' }))[0]?.requiresCompatibilityReview).toBe(true)
}
const added = typeRoot('event:example/added', { value: 'string' })
expect(classifyPersistenceChange(null, added)[0]?.requiresVersionBump).toBe(false)
expect(classifyPersistenceChange(null, { ...added, surface: true })[0]?.requiresVersionBump).toBe(true)
expect(classifyPersistenceChange(added, null)[0]?.requiresVersionBump).toBe(true)
expect(classifyPersistenceChange(null, added)[0]?.requiresCompatibilityReview).toBe(false)
expect(classifyPersistenceChange(null, { ...added, surface: true })[0]?.requiresCompatibilityReview).toBe(true)
expect(classifyPersistenceChange(added, null)[0]?.requiresCompatibilityReview).toBe(true)
})
})
@@ -594,10 +596,10 @@ describe('persistence history verification', () => {
expect(() => validatePersistenceHistory([base, cycle])).toThrow('cycle')
})
it('binds breaking decisions to the same record header version transition', () => {
it('binds version-bump decisions to the same record header version transition', () => {
const base = entry(BASE_ID, inventory(), null, true)
const breaking = inventory({ value: 'number' }, 4)
expect(() => validatePersistenceHistory([base, entry(NEXT_ID, onlyEvent(breaking), BASE_ID)])).toThrow('requires a format version bump')
expect(validatePersistenceHistory([base, entry(NEXT_ID, onlyEvent(breaking), BASE_ID)]).tips.get('event:example/value')?.id).toBe(NEXT_ID)
expect(() => validatePersistenceHistory([base, entry(NEXT_ID, onlyEvent(breaking), BASE_ID, false, 'version-bump')])).toThrow("this record's own")
const touched = { ...breaking, roots: breaking.roots.filter(root => root.key === 'SessionHeader' || root.kind === 'event') }
expect(validatePersistenceHistory([base, entry(NEXT_ID, touched, BASE_ID, false, 'version-bump')]).tips.get('SessionHeader')?.id).toBe(NEXT_ID)
@@ -704,7 +706,7 @@ describe('persistence changes current-tree commands', () => {
verifyPersistenceChanges(root, after)
})
it('rejects inferred bumps without a header transition and explicit incorrect assertions before writing', () => {
it('requires an explicit decision for structural changes before writing', () => {
const root = fixture()
baseline(root)
const prose = proseFile(root)
@@ -712,16 +714,14 @@ describe('persistence changes current-tree commands', () => {
const files = readdirSync(directory).sort()
const inventoryPath = join(root, 'docs/persistence-schema.json')
const before = readFileSync(inventoryPath, 'utf8')
for (const [args, code] of [
[[], 'version-transition-required'],
[['--decision', 'same-version'], 'version-bump-required'],
] as const) {
const result = jsonResult(runPersistenceChanges(['--record', NEXT_ID, '--prose', prose, '--json', ...args], root,
() => inventory({ value: 'number' })))
expect(result).toMatchObject({ ok: false, code, files: [], changes: [expect.objectContaining({ requiresVersionBump: true })] })
expect(readFileSync(inventoryPath, 'utf8')).toBe(before)
expect(readdirSync(directory).sort()).toEqual(files)
}
const result = jsonResult(runPersistenceChanges(['--record', NEXT_ID, '--prose', prose, '--json'], root,
() => inventory({ value: 'number' })))
expect(result).toMatchObject({ ok: false, code: 'decision-required', files: [],
changes: [expect.objectContaining({ requiresCompatibilityReview: true })] })
expect(readFileSync(inventoryPath, 'utf8')).toBe(before)
expect(readdirSync(directory).sort()).toEqual(files)
expect(jsonResult(runPersistenceChanges(['--record', NEXT_ID, '--decision', 'same-version', '--prose', prose, '--json'], root,
() => inventory({ value: 'number' }))).ok).toBe(true)
})
it('authors a complete pair from explicit prose and reports source changes even when generated artifacts are stale', () => {
@@ -731,7 +731,7 @@ describe('persistence changes current-tree commands', () => {
const pending = jsonResult(runPersistenceChanges(['--check', '--json'], root, () => after))
expect(pending).toMatchObject({ ok: false, operation: 'check', code: 'stale-artifacts',
roots: [{ root: 'event:example/value', kind: 'event', before: inventory().roots[2]?.digest, after: after.roots[2]?.digest }], changes: [
{ root: 'event:example/value', path: 'event:example/value.data.label', kind: 'optional-property-added', requiresVersionBump: false },
{ root: 'event:example/value', path: 'event:example/value.data.label', kind: 'optional-property-added', requiresCompatibilityReview: false },
] })
const written = jsonResult(runPersistenceChanges(['--record', NEXT_ID, '--decision', 'same-version', '--prose', proseFile(root), '--json'], root, () => after))
expect(written).toMatchObject({ ok: true, operation: 'record', recordId: NEXT_ID })
@@ -769,8 +769,9 @@ describe('persistence changes current-tree commands', () => {
it('completes a scaffold through update and rejects invalid prose or decisions before changing files', () => {
const root = fixture()
baseline(root)
const after = inventory({ value: 'string', 'label?': 'string' })
runPersistenceChanges(['--record', NEXT_ID, '--decision', 'same-version'], root, () => after)
const after = inventory({ value: 'number' })
const draft = runPersistenceChanges(['--record', NEXT_ID, '--decision', 'same-version'], root, () => after)
expect(draft).toContain(`--update ${NEXT_ID} --decision same-version --prose FILE`)
const prose = proseFile(root)
runPersistenceChanges(['--update', NEXT_ID, '--decision', 'same-version', '--prose', prose], root, () => after)
expect(runPersistenceChanges(['--check'], root, () => after)).toContain('roots match')
@@ -781,9 +782,9 @@ describe('persistence changes current-tree commands', () => {
expect(pairedPaths.map(path => readFileSync(path, 'utf8'))).toEqual(completed)
const path = join(root, `docs/persistence-changes/${NEXT_ID}.md`)
const before = readFileSync(path, 'utf8')
const invalid = inventory({ value: 'number' })
const result = jsonResult(runPersistenceChanges(['--update', NEXT_ID, '--decision', 'same-version', '--json'], root, () => invalid))
expect(result).toMatchObject({ ok: false, code: 'version-bump-required', changes: [expect.objectContaining({ kind: 'type-changed', requiresVersionBump: true })] })
const invalid = inventory({ value: 'boolean' })
const result = jsonResult(runPersistenceChanges(['--update', NEXT_ID, '--json'], root, () => invalid))
expect(result).toMatchObject({ ok: false, code: 'decision-required', changes: [expect.objectContaining({ kind: 'type-changed', requiresCompatibilityReview: true })] })
expect(readFileSync(path, 'utf8')).toBe(before)
for (const value of [
{ ...AUTHORED_PROSE, extra: 'unsupported' },
@@ -797,7 +798,7 @@ describe('persistence changes current-tree commands', () => {
}
})
// Six real CLI processes and TypeScript extraction share the Windows coverage test budget.
// Real CLI processes and TypeScript extraction share the Windows coverage test budget.
it('executes the real CLI against a source-only temporary checkout without Git history', { timeout: 90_000 }, () => {
const root = fixture()
const physical = join(root, 'packages/session/session-persistence-jsonl/src')
@@ -861,15 +862,36 @@ describe('persistence changes current-tree commands', () => {
expect(rejected.error).toBeUndefined()
expect(rejected.signal).toBeNull()
expect(rejected.status).toBe(1)
expect(String(rejected.stderr)).toContain('version-bump required')
expect(String(rejected.stderr)).toContain('compatibility review required')
const structured = cli('--check', '--json')
expect(structured.error).toBeUndefined()
expect(structured.signal).toBeNull()
expect(structured.status).toBe(1)
const structuredResult: unknown = JSON.parse(String(structured.stdout))
expect(structuredResult).toMatchObject({
ok: false, code: 'unacknowledged-changes', changes: [expect.objectContaining({ kind: 'type-changed', requiresVersionBump: true })],
ok: false, code: 'unacknowledged-changes', changes: [expect.objectContaining({ kind: 'type-changed', requiresCompatibilityReview: true })],
})
const missingDecision = cli('--record', FUTURE_ID, '--prose', prose, '--json')
expect(missingDecision.error).toBeUndefined()
expect(missingDecision.signal).toBeNull()
expect(missingDecision.status).toBe(1)
expect(JSON.parse(String(missingDecision.stdout))).toMatchObject({ ok: false, code: 'decision-required', files: [] })
const chosen = cli('--record', FUTURE_ID, '--decision', 'same-version', '--prose', prose, '--json')
expect(chosen.error).toBeUndefined()
expect(chosen.signal).toBeNull()
expect(chosen.status, String(chosen.stderr)).toBe(0)
expect(JSON.parse(String(chosen.stdout))).toMatchObject({ ok: true, operation: 'record' })
const missingUpdateDecision = cli('--update', FUTURE_ID, '--json')
expect(missingUpdateDecision.error).toBeUndefined()
expect(missingUpdateDecision.signal).toBeNull()
expect(missingUpdateDecision.status).toBe(1)
expect(JSON.parse(String(missingUpdateDecision.stdout))).toMatchObject({ ok: false, code: 'decision-required', files: [] })
const chosenUpdate = cli('--update', FUTURE_ID, '--decision', 'same-version', '--json')
expect(chosenUpdate.error).toBeUndefined()
expect(chosenUpdate.signal).toBeNull()
expect(chosenUpdate.status, String(chosenUpdate.stderr)).toBe(0)
expect(JSON.parse(String(chosenUpdate.stdout))).toMatchObject({ ok: true, operation: 'update' })
verifyPersistenceChanges(root, extractPersistenceSchema(root))
})
})
@@ -927,7 +949,7 @@ describe('recorded source compatibility policy', () => {
const before = attributedRoot([EXISTING_SOURCE], { role })
const after = attributedRoot([EXISTING_SOURCE, NEW_SOURCE], { role, policy: attributionPolicy([NEW_SOURCE.kind], role) })
expect(after.digest).not.toBe(before.digest)
expect(classifyPersistenceChange(before, after)).toEqual([expect.objectContaining({ kind: 'attribution-kind-added', requiresVersionBump: false })])
expect(classifyPersistenceChange(before, after)).toEqual([expect.objectContaining({ kind: 'attribution-kind-added', requiresCompatibilityReview: false })])
const snapshot: PersistenceSchemaInventory = { formatVersion: 2, roots: [after], types: [] }
expect(parsePersistenceSnapshot(JSON.parse(JSON.stringify(snapshot)))).toEqual(snapshot)
})
@@ -937,7 +959,7 @@ describe('recorded source compatibility policy', () => {
const after = attributedRoot([{ ...EXISTING_SOURCE, extra: 'optional' }, NEW_SOURCE], { policy: attributionPolicy([NEW_SOURCE.kind]) })
const changes = classifyPersistenceChange(before, after)
expect(changes.map(change => change.kind).sort()).toEqual(['attribution-kind-added', 'optional-property-added'])
expect(changes.every(change => !change.requiresVersionBump)).toBe(true)
expect(changes.every(change => !change.requiresCompatibilityReview)).toBe(true)
})
it.each([
@@ -948,35 +970,38 @@ describe('recorded source compatibility policy', () => {
['renamed existing kind', [{ ...EXISTING_SOURCE, kind: 'renamed' }, NEW_SOURCE], { policy: attributionPolicy([NEW_SOURCE.kind]) }],
['unrelated required payload', [EXISTING_SOURCE, NEW_SOURCE], { policy: attributionPolicy([NEW_SOURCE.kind]), extra: true }],
['removed promise', [EXISTING_SOURCE, NEW_SOURCE], { policy: null }],
] as const)('keeps %s breaking despite qualified additions', (_name, sources, options) => {
] as const)('requires review for %s despite qualified additions', (_name, sources, options) => {
const changes = classifyPersistenceChange(attributedRoot([EXISTING_SOURCE]), attributedRoot(sources, options))
expect(changes.some(change => change.requiresVersionBump)).toBe(true)
expect(changes.some(change => change.requiresCompatibilityReview)).toBe(true)
})
it('keeps variants within an existing wire-kind group strict', () => {
const before = attributedRoot([EXISTING_SOURCE])
const after = attributedRoot([EXISTING_SOURCE, { ...EXISTING_SOURCE, value: 'number' }, NEW_SOURCE], { policy: attributionPolicy([NEW_SOURCE.kind]) })
expect(classifyPersistenceChange(before, after)).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: 'union-variants-changed', requiresVersionBump: true }),
expect.objectContaining({ kind: 'attribution-kind-added', requiresVersionBump: false }),
expect.objectContaining({ kind: 'union-variants-changed', requiresCompatibilityReview: true }),
expect.objectContaining({ kind: 'attribution-kind-added', requiresCompatibilityReview: false }),
]))
})
it('treats qualification changes for an existing kind as policy changes', () => {
const before = attributedRoot([EXISTING_SOURCE])
const after = attributedRoot([EXISTING_SOURCE], { policy: attributionPolicy([EXISTING_SOURCE.kind]) })
expect(classifyPersistenceChange(before, after)).toEqual([expect.objectContaining({ kind: 'source-policy-changed', requiresVersionBump: true })])
expect(classifyPersistenceChange(before, after)).toEqual([expect.objectContaining({ kind: 'source-policy-changed', requiresCompatibilityReview: true })])
})
it('keeps legacy source transitions strict and never infers promises from the successor', () => {
it('flags legacy source transitions while accepting explicit same-version history', () => {
const before = attributedRoot([EXISTING_SOURCE], { policy: null })
const after = attributedRoot([EXISTING_SOURCE, NEW_SOURCE], { policy: attributionPolicy([NEW_SOURCE.kind]) })
expect(classifyPersistenceChange(before, after).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(before, after).some(change => change.requiresCompatibilityReview)).toBe(true)
const legacyAfter = attributedRoot([EXISTING_SOURCE, NEW_SOURCE], { policy: null })
const snapshots = [before, legacyAfter].map(root => ({ formatVersion: 1 as const, roots: [root], types: [] }))
expect(() => validatePersistenceHistory([
const snapshots: PersistenceSchemaInventory[] = [
{ formatVersion: 1, roots: [...inventory().roots.filter(root => root.kind !== 'event'), before], types: [] },
{ formatVersion: 1, roots: [legacyAfter], types: [] },
]
expect(validatePersistenceHistory([
entry(BASE_ID, snapshots[0]!, null, true), entry(NEXT_ID, snapshots[1]!, BASE_ID),
])).toThrow('requires a format version bump')
]).tips.get('event:example/source')?.id).toBe(NEXT_ID)
})
it('validates policy-bearing history without source names or type records', () => {
@@ -1013,3 +1038,73 @@ describe('recorded source compatibility policy', () => {
expect(() => parsePersistenceSnapshot({ ...snapshot, roots: [unknownKind] })).toThrow('invalid source compatibility')
})
})
describe('explicit same-version decisions', () => {
it.each(['optional header', 'removed field', 'changed field'] as const)('admits an explicitly acknowledged %s after finalization', (kind) => {
const root = fixture()
finalize(root)
const previous = contents(root)
let after: PersistenceSchemaInventory
if (kind === 'optional header') {
const current = inventory({ value: 'number' }, 4)
after = { ...current, roots: current.roots.map((root) => {
if (root.key === 'SessionHeader') return typeRoot('SessionHeader', { id: 'string', 'env?': 'string' }, 4)
return root
}) }
} else if (kind === 'removed field') {
after = inventory({}, 4)
} else {
after = inventory({ value: 'boolean' }, 4)
}
const prose = proseFile(root)
expect(jsonResult(runPersistenceChanges(['--record', NEXT_ID, '--decision', 'same-version', '--prose', prose, '--json'], root, () => after)).ok).toBe(true)
const saved = loadPersistenceHistory(root).entries.find(entry => entry.record.id === NEXT_ID)!
expect(saved.record.changes.every(change => change.decision === 'same-version')).toBe(true)
expect(runPersistenceChanges(['--check'], root, () => after)).toContain('roots match')
for (const [path, content] of Object.entries(previous).filter(([path]) => path.startsWith('docs/persistence-changes/'))) {
expect(readFileSync(join(root, path), 'utf8')).toBe(content)
}
})
it('cannot waive an actual version transition with a same-version decision', () => {
const root = fixture()
baseline(root)
const before = contents(root)
for (const version of [2, 4]) {
expect(jsonResult(runPersistenceChanges(['--record', NEXT_ID, '--decision', 'same-version', '--json'], root, () => inventory({ value: 'string' }, version))))
.toMatchObject({ ok: false, code: 'version-transition-required' })
}
expect(jsonResult(runPersistenceChanges(['--record', NEXT_ID, '--decision', 'version-bump', '--json'], root, () => inventory({ value: 'string' }, 2))))
.toMatchObject({ ok: false, code: 'version-transition-required' })
expect(contents(root)).toEqual(before)
})
it.each(['unchanged', 'changed'] as const)('requires an explicit decision when updating %s flagged schemas', (kind) => {
const root = fixture()
baseline(root)
const after = inventory({ value: 'number' })
runPersistenceChanges(['--record', NEXT_ID, '--decision', 'same-version', '--prose', proseFile(root)], root, () => after)
const current = kind === 'changed' ? inventory({ value: 'boolean' }) : after
const before = contents(root)
expect(jsonResult(runPersistenceChanges(['--update', NEXT_ID, '--json'], root, () => current)))
.toMatchObject({ ok: false, code: 'decision-required', files: [] })
expect(contents(root)).toEqual(before)
runPersistenceChanges(['--update', NEXT_ID, '--decision', 'same-version'], root, () => current)
expect(runPersistenceChanges(['--check'], root, () => current)).toContain('roots match')
})
it('binds same-version decisions to exact history snapshots and leaves uncovered roots rejected', () => {
const root = fixture()
baseline(root)
const after = inventory({ value: 'number' })
runPersistenceChanges(['--record', NEXT_ID, '--decision', 'same-version', '--prose', proseFile(root)], root, () => after)
const uncovered = { ...after, roots: after.roots.map(root => root.key === 'SessionHeader'
? typeRoot('SessionHeader', { id: 'string', 'env?': 'string' }) : root) }
commitCurrent(root, uncovered)
expect(() => verifyPersistenceChanges(root, uncovered)).toThrow('unacknowledged persistence type changes')
const history = loadPersistenceHistory(root)
const recorded = history.entries.find(entry => entry.record.id === NEXT_ID)!
const invalid = { ...recorded, snapshot: onlyEvent(inventory({ value: 'boolean' })) }
expect(() => validatePersistenceHistory([...history.entries.filter(entry => entry !== recorded), invalid])).toThrow('after digest mismatch')
})
})
+26 -36
View File
@@ -50,7 +50,7 @@ export interface PersistenceTypeChange {
readonly kind: PersistenceTypeChangeKind
readonly path: string
readonly description: string
readonly requiresVersionBump: boolean
readonly requiresCompatibilityReview: boolean
}
const CHANGE_DESCRIPTIONS = {
@@ -380,19 +380,19 @@ function matchUnionVariants(candidates: readonly (readonly number[])[]): number[
/** Classify structural differences using the reader promises saved with each schema.
* @param before - predecessor root, or absence for an addition.
* @param after - successor root, or absence for deletion.
* @returns concrete changes and their format-bump requirement.
* @returns concrete changes and their compatibility-review requirement.
*/
export function classifyPersistenceChange(before: PersistenceRoot | null, after: PersistenceRoot | null): PersistenceTypeChange[] {
if (before === null) {
return after === null ? [] : [{ path: after.key, kind: 'root-added', description: 'root added',
requiresVersionBump: after.kind !== 'event' || after.surface !== false }]
requiresCompatibilityReview: after.kind !== 'event' || after.surface !== false }]
}
if (after === null) return [{ path: before.key, kind: 'root-removed', description: 'root removed', requiresVersionBump: true }]
if (after === null) return [{ path: before.key, kind: 'root-removed', description: 'root removed', requiresCompatibilityReview: true }]
const key = after.key
const oldRoot = before
const newRoot = after
const describe = (path: string, kind: PersistenceTypeChangeKind, requiresVersionBump = true): PersistenceTypeChange =>
({ path, kind, description: CHANGE_DESCRIPTIONS[kind], requiresVersionBump })
const describe = (path: string, kind: PersistenceTypeChangeKind, requiresCompatibilityReview = true): PersistenceTypeChange =>
({ path, kind, description: CHANGE_DESCRIPTIONS[kind], requiresCompatibilityReview })
const changes: PersistenceTypeChange[] = []
if (before.kind !== after.kind || before.surface !== after.surface) changes.push(describe(key, 'root-classification-changed'))
if (before.digest === after.digest) return changes
@@ -413,8 +413,8 @@ export function classifyPersistenceChange(before: PersistenceRoot | null, after:
// or unmatched union arm cannot leave a cached success for another candidate.
const active = new Set(ancestors).add(pair)
const differences: PersistenceTypeChange[] = []
const add = (path: string, kind: PersistenceTypeChangeKind, requiresVersionBump = true): void => {
differences.push(describe(path, kind, requiresVersionBump))
const add = (path: string, kind: PersistenceTypeChangeKind, requiresCompatibilityReview = true): void => {
differences.push(describe(path, kind, requiresCompatibilityReview))
}
const descend = (oldType: number, newType: number, child: string, childScope: Scope): void => {
differences.push(...compare(oldType, newType, child, childScope, active))
@@ -511,7 +511,7 @@ export function classifyPersistenceChange(before: PersistenceRoot | null, after:
if (oldTypes.length !== newTypes.length) return [describe(path, 'union-variants-changed')]
const candidates = oldTypes.map(oldType => newTypes.map(newType => compare(oldType, newType, path, scope, active)))
const matching = matchUnionVariants(candidates.map(row => row.flatMap((candidate, index) =>
candidate.every(change => !change.requiresVersionBump) ? [index] : [])))
candidate.every(change => !change.requiresCompatibilityReview) ? [index] : [])))
if (matching !== undefined) return matching.flatMap((next, previous) => candidates[previous]?.[next] ?? [])
const oldByHash = new Map(oldTypes.map(index => [fingerprint(oldRoot.schema, index, 0), index]))
const newByHash = new Map(newTypes.map(index => [fingerprint(newRoot.schema, index, 1), index]))
@@ -522,7 +522,8 @@ export function classifyPersistenceChange(before: PersistenceRoot | null, after:
}
changes.push(...compare(0, 0, key, before.kind === 'event' ? 'event' : 'strict', new Set()))
if (changes.length === 0) changes.push(describe(key, 'type-changed'))
return [...new Map(changes.map(change => [JSON.stringify([change.path, change.kind, change.requiresVersionBump]), change])).values()]
return [...new Map(changes.map(change =>
[JSON.stringify([change.path, change.kind, change.requiresCompatibilityReview]), change])).values()]
}
function parseDocument(source: string, filename: string, allowIncomplete = false): PersistenceChangeRecord {
@@ -604,11 +605,9 @@ export function validatePersistenceHistory(entries: readonly PersistenceHistoryE
if (!found.entry.record.baseline) {
const differences = classifyPersistenceChange(before, after)
if (differences.length === 0) throw new Error(`${id}: unchanged acknowledgement for ${root}`)
if (differences.some(change => change.requiresVersionBump) && found.change.decision !== 'version-bump') {
throw new PersistenceChangeFailure(
`${id}: ${root} requires a format version bump (${differences.filter(change => change.requiresVersionBump).map(change => change.path + ': ' + change.description).join('; ')})`,
'version-bump-required', differences.map(change => ({ root, ...change })), [rootTransition(before, after)],
)
if (root === 'SessionHeader' && headerVersion(before) !== headerVersion(after) && found.change.decision !== 'version-bump') {
throw new PersistenceChangeFailure(`${id}: SessionHeader.version changes require an increasing version-bump transition`,
'version-transition-required', differences.map(change => ({ root, ...change })), [rootTransition(before, after)])
}
if (found.change.decision === 'version-bump') {
const header = found.entry.record.changes.find(change => change.root === 'SessionHeader')
@@ -690,24 +689,6 @@ function verifyFinalization(
if (writer === undefined || writer < finalized.version) {
throw new PersistenceChangeFailure(`Session writer must not precede finalized format ${finalized.version}`, 'finalized-version-order')
}
if (writer > finalized.version) return
const afterRoots = new Map(current.roots.map(root => [root.key, root]))
const changes: ReportedChange[] = []
const transitions: RootTransition[] = []
for (const key of [...new Set([...finalized.roots.keys(), ...afterRoots.keys()])].sort()) {
const before = finalized.roots.get(key) ?? null
const after = afterRoots.get(key) ?? null
const differences = classifyPersistenceChange(before, after)
if (differences.length === 0) continue
changes.push(...differences.map(change => ({ root: key, ...change })))
transitions.push(rootTransition(before, after))
}
if (changes.some(change => change.requiresVersionBump)) {
throw new PersistenceChangeFailure(
`Breaking changes relative to the accepted Session format ${writer} baseline require format ${writer + 1} or later`,
'finalized-format-changed', changes, transitions,
)
}
}
/** Verify current generated output and acknowledgement tips together.
@@ -729,7 +710,7 @@ export function verifyPersistenceChanges(root: string, current: PersistenceSchem
throw new PersistenceChangeFailure(`${CURRENT_SCHEMA} is stale; regenerate the persistence catalog`, 'stale-artifacts', differences, transitions)
}
if (differences.length !== 0) {
const details = differences.map(change => ` ${change.path}: ${change.description} (${change.requiresVersionBump ? 'version-bump required' : 'same-version allowed'})`)
const details = differences.map(change => ` ${change.path}: ${change.description} (${change.requiresCompatibilityReview ? 'compatibility review required' : 'same-version allowed'})`)
throw new PersistenceChangeFailure(`unacknowledged persistence type changes:\n${details.join('\n')}`, 'unacknowledged-changes', differences, transitions)
}
return history
@@ -858,7 +839,16 @@ function executeCommand(
const changed = baseline ? current.roots.map(root => root.key) : currentDifferences(history as PersistenceHistory, current)
if (changed.length === 0) throw new Error('no persistence type changes to acknowledge')
const differences = history === undefined ? [] : reportedDifferences(history, current)
const decision = values.decision ?? (differences.some(change => change.requiresVersionBump) ? 'version-bump' : 'same-version')
const previousHeader = history?.tips.get('SessionHeader')?.root ?? null
const currentHeader = current.roots.find(root => root.key === 'SessionHeader') ?? null
const versionChanged = !baseline && headerVersion(previousHeader) !== headerVersion(currentHeader)
if (values.decision === undefined && !versionChanged && differences.some(change => change.requiresCompatibilityReview)) {
throw new PersistenceChangeFailure(
`${id}: review reader compatibility and choose --decision same-version or --decision version-bump`,
'decision-required', differences, rootTransitions(history, current),
)
}
const decision = values.decision ?? (versionChanged ? 'version-bump' : 'same-version')
const roots = current.roots.filter(root => changed.includes(root.key))
const change: PersistenceChangeRecord = { schemaVersion: 1, id, baseline, changes: changed.sort().map(key => ({
root: key, previous: history?.tips.get(key)?.id ?? null,
@@ -885,7 +875,7 @@ function executeCommand(
for (const file of outputs) writeFileSync(resolve(root, file.path), file.content, { flag: recordFiles.includes(file) && !update ? 'wx' : 'w' })
const completion = baseline
? 'Complete both record documents and refresh their translation pairing.'
: `Complete the compatibility and verification prose with --update ${id} --prose FILE.`
: `Complete the compatibility and verification prose with --update ${id} --decision ${decision} --prose FILE.`
const message = existing === undefined && prose === undefined
? `Created ${HISTORY_DIRECTORY}/${id}.md and paired schema files. ${completion}`
: `${update ? 'Updated' : 'Created'} ${HISTORY_DIRECTORY}/${id}.md; schema artifacts and bilingual pairing are current.`
+3 -3
View File
@@ -23,7 +23,7 @@ function toolAdditionSchema() {
}
describe('developer field compatibility', () => {
it('requires a version bump before a future declaration can use the rejected inline tool field', () => {
it('requires compatibility review before a future declaration can use the rejected inline tool field', () => {
const { before, nodes, blockIndex, block, stringIndex } = toolAdditionSchema()
nodes[blockIndex] = { ...block, properties: [
...block.properties.filter(property => property.name !== 'tool'),
@@ -31,7 +31,7 @@ describe('developer field compatibility', () => {
] }
const schema = canonicalizeSchema(nodes, 0)
expect(classifyPersistenceChange(before, { ...before, schema, digest: schemaDigest(schema) }))
.toContainEqual(expect.objectContaining({ requiresVersionBump: true }))
.toContainEqual(expect.objectContaining({ requiresCompatibilityReview: true }))
const reserved = block.properties.find(property => property.name === 'tool')
expect(reserved).toMatchObject({ optional: true })
expect(before.schema.nodes[reserved!.type]).toEqual({ kind: 'primitive', type: 'never' })
@@ -47,7 +47,7 @@ describe('developer field compatibility', () => {
const { before, nodes, blockIndex, block, stringIndex } = toolAdditionSchema()
nodes[blockIndex] = { ...block, properties: [...block.properties, { name: 'traceId', type: stringIndex, optional: true }] }
const schema = canonicalizeSchema(nodes, 0)
expect(classifyPersistenceChange(before, { ...before, schema, digest: schemaDigest(schema) })).toMatchObject([{ kind: 'optional-property-added', requiresVersionBump: false }])
expect(classifyPersistenceChange(before, { ...before, schema, digest: schemaDigest(schema) })).toMatchObject([{ kind: 'optional-property-added', requiresCompatibilityReview: false }])
const header = { version: 4, id: 'optional-developer-field', createdAt: 1, isSeeded: false, delegationDepth: 0 }
const message = { id: 'developer', role: 'developer', source: { kind: 'tool-registry', extra: true },
content: [{ type: 'tool-addition', toolName: 'search', traceId: 'retained' }], extra: true }
+2 -2
View File
@@ -15,7 +15,7 @@ function property(nodes: readonly SchemaNode[], index: number, name: string): nu
return field.type
}
it('requires a version bump before request headers can carry retired system text', { timeout: 60_000 }, () => {
it('requires compatibility review before request headers can carry retired system text', { timeout: 60_000 }, () => {
const inventory = extractPersistenceSchema(resolve(import.meta.dirname, '..'))
const before = inventory.roots.find(root => root.key === 'event:request/header')
if (before === undefined) throw new Error('generated schema omits request/header')
@@ -41,7 +41,7 @@ it('requires a version bump before request headers can carry retired system text
path: 'event:request/header.data.header.system',
kind: 'type-changed',
description: 'type changed',
requiresVersionBump: true,
requiresCompatibilityReview: true,
}])
expect(() => { assertV4RowAdmission({ type: 'request/header', data: { header: { system: 'retired text' } } }) })
.toThrow('format v4 request/header rejects retired header.system')
+3 -7
View File
@@ -21,10 +21,9 @@ export interface PersistenceFinalizationCheckpoint {
readonly acceptedRecords: Readonly<Record<string, string>>
}
/** Latest accepted baseline schemas and the record ids locked by retained checkpoints. */
/** Latest finalized version and the record ids locked by retained checkpoints. */
export interface PersistenceFinalization {
readonly version: number
readonly roots: ReadonlyMap<string, PersistenceRoot>
readonly acceptedRecords: ReadonlySet<string>
}
@@ -105,7 +104,7 @@ function status(root: string, path: string): number | undefined {
}
/**
* Validate retained checkpoints against accepted history and reconstruct the latest accepted baseline.
* Validate retained checkpoints against accepted history and collect their locked record ids.
* @param root - checkout or isolated fixture root.
* @param history - parsed acknowledgement entries, including later successors.
* @returns finalization state, or undefined for a checkout with neither status nor checkpoints.
@@ -120,7 +119,6 @@ export function loadPersistenceFinalization(root: string, history: Pick<Persiste
if (!files.includes(`v${version}.json`)) throw new Error(`missing persistence finalization checkpoint v${version}.json`)
const entries = new Map(history.entries.map(entry => [entry.record.id, entry]))
const acceptedRecords = new Set<string>()
let latestRoots: Map<string, PersistenceRoot> | undefined
for (const file of files) {
const match = /^v(0|[1-9]\d*)\.json$/u.exec(file)
const capturedVersion = match === null ? NaN : Number(match[1])
@@ -165,8 +163,6 @@ export function loadPersistenceFinalization(root: string, history: Pick<Persiste
|| Object.entries(expected).some(([field, expectedValue]) => value[field] !== expectedValue)
})) throw new Error(`${file}: finalized roots do not match complete accepted history`)
if (writerVersion([...tips.values()]) !== capturedVersion) throw new Error(`${file}: accepted SessionHeader.version does not match finalized version`)
if (capturedVersion === version) latestRoots = tips
}
if (latestRoots === undefined) throw new Error(`missing finalized schemas for Session format ${version}`)
return { version, roots: latestRoots, acceptedRecords }
return { version, acceptedRecords }
}
+4 -4
View File
@@ -35,12 +35,12 @@ function structuralChanges(entry: PersistenceReleaseEntry, language: Language):
: '规范化后的根类型及其传递引用摘要与前一 tag 相同。'
}
const summary = language === 'en'
? `Detected ${count(entry.record.changes.length, 'changed root')} and ${count(entry.differences.length, 'structural difference')}. The minimum below is calculated using current rules for comparison only; it does not assert historical compliance, migration correctness, or runtime compatibility.`
: `检测到 ${entry.record.changes.length} 个根类型变化、${entry.differences.length} 项结构差异。下表的最低要求按当前规则计算,只用于比较;不表示旧版本曾遵守这些规则,也不证明迁移或运行时兼容性。`
const heading = language === 'en' ? '| Path | Change | Current minimum |' : '| 路径 | 变化 | 当前最低要求 |'
? `Detected ${count(entry.record.changes.length, 'changed root')} and ${count(entry.differences.length, 'structural difference')}. The review requirement below is calculated using current rules for comparison only; it does not assert historical compliance, migration correctness, or runtime compatibility.`
: `检测到 ${entry.record.changes.length} 个根类型变化、${entry.differences.length} 项结构差异。下表的审查要求按当前规则计算,只用于比较;不表示旧版本曾遵守这些规则,也不证明迁移或运行时兼容性。`
const heading = language === 'en' ? '| Path | Change | Review requirement |' : '| 路径 | 变化 | 审查要求 |'
const rows = entry.differences.map((change) => {
const path = change.path.replaceAll('`', '\\`').replaceAll('|', '\\|')
return `| \`${path}\` | \`${change.kind}\` | \`${change.requiresVersionBump ? 'version-bump' : 'same-version'}\` |`
return `| \`${path}\` | \`${change.kind}\` | \`${change.requiresCompatibilityReview ? 'review-required' : 'not-required'}\` |`
})
return [summary, '', heading, '|---|---|---|', ...rows].join('\n')
}
+6 -4
View File
@@ -145,7 +145,7 @@ describe('pinned persistence releases', () => {
expect(archive.entries).toHaveLength(3)
expect(archive.entries[0]!.roots.get('event:example/value')!.digest).toBe(event('string').digest)
expect(archive.entries[1]!.roots.get('event:example/value')!.digest).toBe(event('number').digest)
expect(archive.entries[1]!.differences).toEqual([expect.objectContaining({ root: 'event:example/value', kind: 'type-changed', requiresVersionBump: true })])
expect(archive.entries[1]!.differences).toEqual([expect.objectContaining({ root: 'event:example/value', kind: 'type-changed', requiresCompatibilityReview: true })])
expect(archive.entries[2]!.roots).toEqual(archive.entries[1]!.roots)
expect(archive.entries[2]!.differences).toEqual([])
expect(() => parsePersistenceSnapshot(data.snapshots[1])).toThrow('surface metadata')
@@ -249,7 +249,7 @@ describe('pinned persistence releases', () => {
saveRecord(data.directory, { ...data.records[2]!, changes: [{ root: 'event:example/value', before: event('number').digest, after: null }] })
const result = loadPersistenceReleases(data.root)
expect(result.entries[2]!.roots.has('event:example/value')).toBe(false)
expect(result.entries[2]!.differences).toEqual([expect.objectContaining({ kind: 'root-removed', requiresVersionBump: true })])
expect(result.entries[2]!.differences).toEqual([expect.objectContaining({ kind: 'root-removed', requiresCompatibilityReview: true })])
})
it('rejects missing, duplicate, and unreferenced reachable types', () => {
@@ -305,7 +305,9 @@ describe('release facts', () => {
expect(after['README.md']).toContain(`| [${TAGS[1]}](${TAGS[1]}.md) | 2026-09-01 | 0 | 4 / 7 | 1 |`)
expect(after[TAGS[1] + '.md']).toContain('4 roots / 7 types')
expect(after[TAGS[1] + '.md']).toContain('Detected 1 changed root and 1 structural difference.')
expect(after[TAGS[1] + '.md']).toContain('| `event:example/value.data` | `type-changed` | `version-bump` |')
expect(after[TAGS[1] + '.md']).toContain('| `event:example/value.data` | `type-changed` | `review-required` |')
expect(after[TAGS[1] + '.md']).toContain('| Path | Change | Review requirement |')
expect(after[TAGS[1] + '.zh.md']).toContain('| 路径 | 变化 | 审查要求 |')
for (const [name, source] of Object.entries(before)) {
if (name.endsWith('.json')) expect(after[name]).toBe(source)
if (name.endsWith('.md')) {
@@ -342,7 +344,7 @@ describe('release facts', () => {
['difference count', '1 structural difference', '9 structural differences'],
['path', '`event:example/value.data`', '`event:example/value.wrong`'],
['kind', '`type-changed`', '`optional-property-added`'],
['minimum', '`version-bump`', '`same-version`'],
['review requirement', '`review-required`', '`not-required`'],
])('rejects stale record %s', (_label, original, replacement) => {
const data = fixture()
runPersistenceReleases(['--write'], data.root)
+2 -1
View File
@@ -66,6 +66,7 @@ describe('persistence review', () => {
{ root: 'event:user/message', path: 'event:user/message.data.source[kind="tool-registry"]' },
] })
const markdown = renderPersistenceReview(report)
expect(markdown).toContain('## Compatibility review requirements')
expect(markdown).not.toContain('hooks-codex')
expect(markdown).not.toMatch(/source\[\d+\]/u)
for (const root of report.roots) expect(root.changes).toEqual(classifyPersistenceChange(
@@ -81,7 +82,7 @@ describe('persistence review', () => {
const report = reviewPersistenceSchemas(fixture(), fixture({ extraKind: true, changed: true }))
expect(report.evidence.map(item => item.kind).sort()).toEqual(['kind-added', 'type-changed'])
expect(report.evidence.find(item => item.kind === 'type-changed')?.locations[0]?.path).toBe('event:agent/inbox/spliced.data.inserted[].source[kind="changing"].value')
expect(report.roots.every(root => root.changes.some(change => change.requiresVersionBump))).toBe(true)
expect(report.roots.every(root => root.changes.some(change => change.requiresCompatibilityReview))).toBe(true)
})
it('keeps ambiguous kind matches as additions and removals even when another arm is unchanged', () => {
+4 -4
View File
@@ -199,10 +199,10 @@ export function renderPersistenceReview(report: PersistenceReview): string {
`Before: ${item.before === null ? 'absent' : code(item.before)}; after: ${item.after === null ? 'absent' : code(item.after)}.`, '',
...item.locations.map(location => `- ${code(location.path)}`), '', '</details>')
}
lines.push('', '## Authoritative compatibility results', '')
lines.push('', '## Compatibility review requirements', '')
for (const root of report.roots) {
lines.push(`### ${code(root.root)}`, '', `Decision: **${root.changes.some(change => change.requiresVersionBump) ? 'version-bump' : 'same-version'}**.`, '',
...root.changes.map(change => `- ${code(change.path)}: ${change.description} (${code(change.kind)}; requiresVersionBump: ${String(change.requiresVersionBump)}).`), '')
lines.push(`### ${code(root.root)}`, '', `Assessment: **${root.changes.some(change => change.requiresCompatibilityReview) ? 'compatibility review required' : 'same-version allowed'}**.`, '',
...root.changes.map(change => `- ${code(change.path)}: ${change.description} (${code(change.kind)}; requiresCompatibilityReview: ${String(change.requiresCompatibilityReview)}).`), '')
}
return lines.join('\n').trimEnd() + '\n'
}
@@ -210,7 +210,7 @@ export function renderPersistenceReview(report: PersistenceReview): string {
/**
* Read two explicit inventory files and return Markdown or JSON without mutating either.
* @param args - --before and --after paths, and optional --json.
* @returns structural evidence and exact compatibility results.
* @returns structural evidence and per-root compatibility-review requirements.
*/
export function runPersistenceReview(args: readonly string[]): string {
const { values } = parseArgs({ args: [...args], options: { before: { type: 'string' }, after: { type: 'string' }, json: { type: 'boolean' } }, strict: true, allowPositionals: false })
+5 -5
View File
@@ -346,7 +346,7 @@ describe('explicitly reserved JSON properties', () => {
const before = extractedEvent('export interface Payload { id: string;\n/** @persistenceReserved */\ntool?: never }')
const after = extractedEvent('export interface Payload { id: string; tool?: string }')
expect(classifyPersistenceChange(before, after)).toEqual([expect.objectContaining({
path: 'event:test/record.data.tool', kind: 'type-changed', requiresVersionBump: true,
path: 'event:test/record.data.tool', kind: 'type-changed', requiresCompatibilityReview: true,
})])
})
@@ -420,7 +420,7 @@ describe('source compatibility authoring', () => {
expect(property.compatibility).toEqual({ version: 1, policy: 'session-source-attribution', binding: `session.${role}-message.source`,
discriminator: 'kind', unknownKinds: 'preserve', attributionKinds: ['attribution'] })
expect(classifyPersistenceChange(before.roots.find(root => root.kind === 'event')!, root))
.toEqual([expect.objectContaining({ kind: 'attribution-kind-added', requiresVersionBump: false })])
.toEqual([expect.objectContaining({ kind: 'attribution-kind-added', requiresCompatibilityReview: false })])
expect(parsePersistenceSnapshot({ ...after, types: [] })).toEqual({ ...after, types: [] })
})
@@ -502,18 +502,18 @@ declare module './message.js' {
}`)
const after = extractPersistenceSchema(root)
expect(classifyPersistenceChange(before.roots.find(root => root.kind === 'event')!, after.roots.find(root => root.kind === 'event')!))
.toEqual([expect.objectContaining({ kind: 'attribution-kind-added', requiresVersionBump: false })])
.toEqual([expect.objectContaining({ kind: 'attribution-kind-added', requiresCompatibilityReview: false })])
})
it('does not qualify unmarked additions or structurally equal unbound fields', () => {
const before = sourceFixture('')
const after = sourceFixture("unmarked: {kind: 'new'}")
expect(classifyPersistenceChange(extractPersistenceSchema(before).roots.find(root => root.kind === 'event')!,
extractPersistenceSchema(after).roots.find(root => root.kind === 'event')!).some(change => change.requiresVersionBump)).toBe(true)
extractPersistenceSchema(after).roots.find(root => root.kind === 'event')!).some(change => change.requiresCompatibilityReview)).toBe(true)
const left = fixture("export interface Payload { source: {kind: 'old'} }")
const right = fixture("export interface Payload { source: {kind: 'old'} | {kind: 'new'} }")
expect(classifyPersistenceChange(extractPersistenceSchema(left).roots.find(root => root.kind === 'event')!,
extractPersistenceSchema(right).roots.find(root => root.kind === 'event')!).some(change => change.requiresVersionBump)).toBe(true)
extractPersistenceSchema(right).roots.find(root => root.kind === 'event')!).some(change => change.requiresCompatibilityReview)).toBe(true)
})
it.each([
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
# Generate node-gyp's Makefile on the runner, then compile against glibc 2.28.
case "$(uname -m)" in
x86_64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
aarch64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
*) echo 'node-pty release preparation requires native Linux x64 or arm64' >&2; exit 1 ;;
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)
addon="$addon_dir/build/Release/pty.node"
test -f "$addon_dir/build/Makefile"
docker run --rm \
--user "$(id -u):$(id -g)" \
-v "$PWD:$PWD" \
-v "$HOME/.cache/node-gyp:$HOME/.cache/node-gyp:ro" \
-v "$pnpm_setup_root:$pnpm_setup_root:ro" \
-w "$addon_dir" "$image" \
bash -euxo pipefail -c 'rm -rf build/Release && make -C build -j2 BUILDTYPE=Release'
test -f "$addon"
readelf --version-info "$addon"
+5 -1
View File
@@ -1,6 +1,6 @@
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { zipSync } from 'fflate'
@@ -61,7 +61,11 @@ it('extracts a hash-verified cached library without a Python installer or networ
const hash = createHash('sha256').update(libraryWheel).digest('hex')
const archive = join(root, hash)
await writeFile(archive, libraryWheel)
const original = await stat(archive)
expect(await downloadPrimaryRuntimeAsset('https://unused.invalid/library.whl', hash, root)).toBe(archive)
const reused = await stat(archive)
expect(reused.ino).toBe(original.ino)
expect(reused.mtimeMs).toBe(original.mtimeMs)
await unpackPrimaryRuntimeWheel(archive, join(root, 'site-packages'))
expect(await readFile(join(root, 'site-packages/sample.py'), 'utf8')).toBe('sample = 42\n')
await writeFile(archive, 'corrupt archive')
+98 -30
View File
@@ -1,8 +1,8 @@
/** Prepare pinned, relocatable script interpreters without installing into the build host. */
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { createHash, randomUUID } from 'node:crypto'
import { cpSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { cp } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
@@ -10,31 +10,101 @@ import { dirname, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import extractZip from 'extract-zip'
import { x as extractTar } from 'tar'
import { parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '../../packages/skill/tool-workspace-dependencies/src/index.ts'
import { parsePrimaryRuntime, workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
import lock from './lock.json' with { type: 'json' }
import { prunePrimaryRuntimePythonTests } from './prune-python-tests.ts'
/**
* Download or reuse an archive only when its bytes match the release lock.
* @param url - Locked archive URL.
* @param sha256 - Expected SHA-256 digest.
* @param sha256 - Expected hexadecimal digest for the selected algorithm.
* @param cache - Download cache directory.
* @param algorithm - Digest algorithm; npm archives use SHA-512.
* @returns Verified local archive path.
*/
export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string): Promise<string> {
export async function downloadPrimaryRuntimeAsset(url: string, sha256: string, cache: string, algorithm: 'sha256' | 'sha512' = 'sha256'): Promise<string> {
mkdirSync(cache, { recursive: true })
const destination = join(cache, sha256)
let bytes: Buffer
let cached = true
try { bytes = readFileSync(destination) } catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
const response = await fetch(url)
cached = false
const timeout = Number(process.env.DSH_RESOURCE_DOWNLOAD_TIMEOUT_MS ?? '300000')
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2_147_483_647) throw new Error('DSH_RESOURCE_DOWNLOAD_TIMEOUT_MS must be an integer from 1 to 2147483647')
const response = await fetch(url, { signal: AbortSignal.timeout(timeout) })
if (!response.ok) throw new Error(`primary runtime download: ${String(response.status)} ${url}`)
bytes = Buffer.from(await response.arrayBuffer())
}
if (createHash('sha256').update(bytes).digest('hex') !== sha256) throw new Error(`primary runtime download: checksum mismatch for ${url}`)
writeFileSync(destination, bytes)
if (createHash(algorithm).update(bytes).digest('hex') !== sha256) {
rmSync(destination, { force: true })
throw new Error(`primary runtime download: checksum mismatch for ${url}`)
}
if (cached) return destination
const temporary = join(cache, `.${randomUUID()}`)
try {
writeFileSync(temporary, bytes)
renameSync(temporary, destination)
} finally {
rmSync(temporary, { force: true })
}
return destination
}
/** Pinned npm tarball, without dependency resolution or lifecycle scripts. */
export interface RuntimeNpmArchive {
/** Exact published package name. */
readonly name: string
/** Exact published version. */
readonly version: string
/** Registry tarball URL. */
readonly url: string
/** npm SHA-512 integrity. */
readonly integrity: string
}
/**
* Download and unpack one locked npm package without running its scripts.
* @param artifact - Published tarball identity and integrity.
* @param destination - Empty package directory.
* @param cache - Verified archive cache.
* @returns Resolves after package identity validation.
*/
export async function downloadRuntimeNpmPackage(artifact: RuntimeNpmArchive, destination: string, cache: string): Promise<void> {
if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(artifact.integrity)) throw new Error(`runtime download: invalid integrity for ${artifact.name}`)
const checksum = Buffer.from(artifact.integrity.slice(7), 'base64').toString('hex')
const archive = await downloadPrimaryRuntimeAsset(artifact.url, checksum, cache, 'sha512')
mkdirSync(destination, { recursive: true })
await extractTar({ file: archive, cwd: destination, strip: 1 })
const installed = JSON.parse(readFileSync(join(destination, 'package.json'), 'utf8')) as { name?: string; version?: string }
if (installed.name !== artifact.name || installed.version !== artifact.version) throw new Error(`runtime download: package identity mismatch for ${artifact.name}`)
}
/**
* Prepare the locked standalone Node independently of Python and pnpm.
* @param target - Target interpreter platform.
* @param destination - Directory receiving bin/node and LICENSE.
* @param cache - Verified archive cache.
* @returns Resolves after extracting the interpreter.
*/
export async function downloadNodeRuntime(target: PrimaryRuntimeTarget, destination: string, cache: string): Promise<void> {
const artifact = lock.targets[target]
const filename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}`
const archive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${filename}`, artifact.nodeSha256, cache)
const staging = mkdtempSync(join(tmpdir(), 'dsh-node-'))
try {
if (target === 'win-x64') await extractZip(archive, { dir: staging })
else await extractTar({ file: archive, cwd: staging })
const source = join(staging, filename.replace(/\.(?:zip|tar\.gz)$/u, ''))
mkdirSync(join(destination, 'bin'), { recursive: true })
cpSync(join(source, ...(target === 'win-x64' ? ['node.exe'] : ['bin', 'node'])),
join(destination, 'bin', target === 'win-x64' ? 'node.exe' : 'node'))
cpSync(join(source, 'LICENSE'), join(destination, 'LICENSE'))
} finally {
rmSync(staging, { recursive: true, force: true })
}
}
async function pythonArchive(target: keyof typeof lock.targets, cache: string): Promise<string> {
const artifact = lock.targets[target]
const filename = `cpython-${lock.pythonVersion}+${lock.pythonRelease}-${artifact.pythonTarget}-install_only_stripped.tar.gz`
@@ -92,7 +162,7 @@ export async function prepareOfficeSkillAssets(source: string, destination: stri
/** A locked interpreter and wheel target. */
export type PrimaryRuntimeTarget = keyof typeof lock.targets
/** Build-only inputs shared by Desktop and SDK carriers. */
/** Resource preparation inputs shared by builds and explicit SDK downloads. */
export interface PreparePrimaryRuntimeOptions {
/** Target whose archives and wheels are downloaded. */
readonly target: PrimaryRuntimeTarget
@@ -102,6 +172,10 @@ export interface PreparePrimaryRuntimeOptions {
readonly cache: string
/** Carrier release recorded in the legacy desktopVersion manifest field. */
readonly version: string
/** Locked npm pnpm tarball for installed carriers; build hosts otherwise copy their installed pnpm. */
readonly pnpmArchive?: RuntimeNpmArchive
/** Ordinary filesystem Office assets; build hosts otherwise resolve the workspace package. */
readonly skillSource?: string
/** Omit Node.js and pnpm for carriers providing only Python. */
readonly pythonOnly?: boolean
}
@@ -114,7 +188,6 @@ export interface PreparePrimaryRuntimeOptions {
export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOptions): Promise<void> {
const { target } = options
const paths = { runtime: resolve(options.output), downloads: resolve(options.cache) }
const artifact = lock.targets[target]
mkdirSync(paths.runtime, { recursive: true })
mkdirSync(paths.downloads, { recursive: true })
const staging = mkdtempSync(join(tmpdir(), 'dsh-primary-'))
@@ -124,23 +197,18 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
mkdirSync(dependencies, { recursive: true })
let pnpmVersion: string | undefined
if (!options.pythonOnly) {
const nodeFilename = `node-v${lock.nodeVersion}-${artifact.nodeArchive}`
const nodeArchive = await downloadPrimaryRuntimeAsset(`https://nodejs.org/dist/v${lock.nodeVersion}/${nodeFilename}`, artifact.nodeSha256, paths.downloads)
const unpackedNode = join(staging, 'node')
mkdirSync(unpackedNode)
if (target === 'win-x64') await extractZip(nodeArchive, { dir: unpackedNode })
else await extractTar({ file: nodeArchive, cwd: unpackedNode })
const nodeSource = join(unpackedNode, nodeFilename.replace(/\.(?:zip|tar\.gz)$/u, ''))
mkdirSync(join(dependencies, 'node', 'bin'), { recursive: true })
mkdirSync(join(dependencies, 'node', 'node_modules'))
writeFileSync(join(dependencies, 'node', 'node_modules', 'README.txt'), 'Reserved for bundled Node packages. pnpm uses its default installation directories.\n')
cpSync(join(nodeSource, ...(target === 'win-x64' ? ['node.exe'] : ['bin', 'node'])),
join(dependencies, 'node', 'bin', target === 'win-x64' ? 'node.exe' : 'node'))
cpSync(join(nodeSource, 'LICENSE'), join(dependencies, 'node', 'LICENSE'))
const require = createRequire(import.meta.url)
const pnpmManifest = require.resolve('pnpm')
pnpmVersion = (JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }).version
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
const node = join(dependencies, 'node')
await downloadNodeRuntime(target, node, paths.downloads)
mkdirSync(join(node, 'node_modules'))
writeFileSync(join(node, 'node_modules', 'README.txt'), 'Reserved for bundled Node packages. pnpm uses its default installation directories.\n')
if (options.pnpmArchive !== undefined) {
pnpmVersion = options.pnpmArchive.version
await downloadRuntimeNpmPackage(options.pnpmArchive, join(dependencies, 'pnpm'), paths.downloads)
} else {
const pnpmManifest = createRequire(import.meta.url).resolve('pnpm')
pnpmVersion = (JSON.parse(readFileSync(pnpmManifest, 'utf8')) as { version: string }).version
await cp(dirname(pnpmManifest), join(dependencies, 'pnpm'), { recursive: true, dereference: true })
}
}
await extractTar({ file: await pythonArchive(target, paths.downloads), cwd: dependencies })
const manifest: PrimaryRuntimeManifest = {
@@ -153,7 +221,7 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
pythonPackages: lock.pythonPackages,
}
const entries = workspaceDependencyPaths(output, manifest)
for (const wheel of [...artifact.wheels, ...lock.wheels]) {
for (const wheel of [...lock.targets[target].wheels, ...lock.wheels]) {
await unpackPrimaryRuntimeWheel(await downloadPrimaryRuntimeAsset(wheel.url, wheel.sha256, paths.downloads), entries.pythonPackages)
}
prunePrimaryRuntimePythonTests(entries.pythonPackages)
@@ -164,8 +232,8 @@ export async function preparePrimaryRuntime(options: PreparePrimaryRuntimeOption
} finally {
rmSync(staging, { recursive: true, force: true })
}
const require = createRequire(import.meta.url)
await prepareOfficeSkillAssets(join(dirname(require.resolve('@deepseek-ai/dsh-skill-office/package.json')), 'assets'),
const assets = options.skillSource ?? join(dirname(createRequire(import.meta.url).resolve('@deepseek-ai/dsh-skill-office/package.json')), 'assets')
await prepareOfficeSkillAssets(assets,
join(paths.runtime, 'office-skills'))
}
+1
View File
@@ -22,6 +22,7 @@ export const PRODUCT_PACKAGE_POLICY: Readonly<Record<string, ProductPackagePolic
'packages/lsp/lsp-stdio': { category: 'optional', reason: 'Requires an explicit server list and language-server executables.' },
'packages/lsp/tool-lsp': { category: 'optional', reason: 'Model tools selected together with a configured LSP provider.' },
'packages/ssh/ssh': { category: 'optional', reason: 'Explicit remote connection and helper configuration.' },
'packages/ssh/ssh-helper-runtime': { category: 'optional', reason: 'Private executable carrier for explicitly configured SSH connections.' },
'packages/ssh/fs-ssh': { category: 'optional', reason: 'Remote filesystem provider selected in an SSH composition.' },
'packages/ssh/sandbox-ssh': { category: 'optional', reason: 'Remote confinement provider selected in an SSH composition.' },
'packages/ssh/subprocess-ssh': { category: 'optional', reason: 'Remote process provider selected in an SSH composition.' },
+135
View File
@@ -0,0 +1,135 @@
/** Explicit resource acquisition through the Python carrier's private dispatcher. */
import { spawn } from 'node:child_process'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile, copyFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { expect, it } from 'vitest'
async function fixture() {
const root = await mkdtemp(join(tmpdir(), 'dsh-resource-download-'))
const bootstrap = join(root, 'node', 'runtime-bootstrap.mjs')
const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win' : process.platform
const resources = join(root, `${platform}-${process.arch}`)
const cache = join(root, 'cache')
async function file(path: string, content: string) {
await mkdir(dirname(path), { recursive: true })
await writeFile(path, content)
}
await file(join(resources, 'downloads.json'), JSON.stringify({ identity: 'a'.repeat(64), office: [], pnpm: {} }))
await mkdir(dirname(bootstrap), { recursive: true })
await copyFile(new URL('../python/sdk-runtime/runtime-bootstrap.mjs', import.meta.url), bootstrap)
await file(join(root, 'node/node_modules/@deepseek-ai/dsh-http-proxy/package.json'), '{"type":"module","exports":"./index.mjs"}')
await file(join(root, 'node/node_modules/@deepseek-ai/dsh-http-proxy/index.mjs'), 'export async function installProxyFromEnvironment(env) { if (typeof env.get !== "function") throw new Error("missing environment lookup"); return async () => {} }')
await file(join(root, 'node/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/package.json'), '{"type":"module","exports":{"./runner":"./runner.mjs"}}')
await file(join(root, 'node/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/runner.mjs'), '')
await file(join(root, 'node/node_modules/@deepseek-ai/dsh/package.json'), '{"type":"module"}')
await file(join(root, 'node/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'export async function runCli() { if (process.argv[2]) { console.log(JSON.stringify({ argv: process.argv.slice(2) })); return } console.log(JSON.stringify({ primary: process.env.DSH_BUNDLED_PRIMARY_RUNTIME, office: process.env.DSH_BUNDLED_OFFICE_CLI })) }')
await file(join(root, 'node/primary-runtime.mjs'), `
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
export async function downloadNodeRuntime(target, destination) {
if (process.env.DSH_RUNTIME_DOWNLOAD !== undefined) throw new Error("private selector leaked");
await mkdir(join(destination, 'bin'), { recursive: true });
await writeFile(join(destination, 'bin', process.platform === 'win32' ? 'node.exe' : 'node'), 'node');
}
export async function preparePrimaryRuntime({ output }) {
if (process.env.DSH_RUNTIME_DOWNLOAD !== undefined) throw new Error("private selector leaked");
await mkdir(join(output, 'primary-runtime'), { recursive: true });
await writeFile(join(output, 'primary-runtime/runtime.json'), '{}');
}
`)
await file(join(root, 'node/office-sidecar.mjs'), `
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
export async function downloadOfficeSidecar(packages, destination) {
const directory = join(destination, 'node_modules/@deepseek-ai/libreoffice-kit/lib');
await mkdir(directory, { recursive: true });
await writeFile(join(directory, 'cli.js'), 'cli');
if (process.env.DSH_FIXTURE_FAIL) throw new Error('fixture download failed');
if (process.env.DSH_FIXTURE_BARRIER) {
process.stderr.write('ready\\n');
await new Promise(resolve => process.stdin.once('data', resolve));
process.stdin.pause();
}
}
`)
function run(resource?: string, extra: NodeJS.ProcessEnv = {}, args: string[] = []) {
const child = spawn(process.execPath, [bootstrap, ...args], {
env: { ...process.env, DSH_RESOURCE_CACHE: cache, DSH_RUNTIME_DOWNLOAD: resource, DSH_BUNDLED_PRIMARY_RUNTIME: undefined,
DSH_OFFICE_SIDECAR: undefined, DSH_BUNDLED_OFFICE_CLI: undefined, ...extra },
stdio: ['pipe', 'pipe', 'pipe'],
})
let stdout = '', stderr = ''
child.stdout.on('data', (data) => { stdout += String(data) })
let reportReady: (() => void) | undefined
let failReady: ((error: Error) => void) | undefined
const ready = extra.DSH_FIXTURE_BARRIER === undefined ? Promise.resolve()
: new Promise<void>((resolve, reject) => { reportReady = resolve; failReady = reject })
child.stderr.on('data', (data) => { stderr += String(data); if (stderr.includes('ready\n')) reportReady?.() })
const done = new Promise<{ code: number | null; stdout: string; stderr: string }>((resolve, reject) => {
child.once('error', reject)
child.once('close', (code) => { failReady?.(new Error(`download exited before readiness: ${stderr}`)); resolve({ code, stdout, stderr }) })
})
return { child, ready, done }
}
return { root, cache, run }
}
it('starts without downloading, independently downloads either resource, and reuses completed caches', async () => {
const { root, cache, run } = await fixture()
try {
expect(await run().done).toMatchObject({ code: 0, stdout: '{}\n' })
await expect(readdir(cache)).rejects.toMatchObject({ code: 'ENOENT' })
const primary = await run('primary').done
expect(primary.code, primary.stderr).toBe(0)
expect(JSON.parse(primary.stdout)).toBe(join(cache, 'a'.repeat(64), 'primary', 'primary-runtime'))
await expect(readdir(join(cache, 'a'.repeat(64), 'office'))).rejects.toMatchObject({ code: 'ENOENT' })
const office = await run('office').done
expect(office.code, office.stderr).toBe(0)
const location = JSON.parse(office.stdout) as string
expect(await readFile(join(location, 'node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js'), 'utf8')).toBe('cli')
expect(await run('office', { DSH_FIXTURE_FAIL: '1' }).done).toMatchObject({ code: 0, stdout: office.stdout })
} finally { await rm(root, { recursive: true, force: true }) }
})
it('cleans failed staging and allows an explicit retry', async () => {
const { root, cache, run } = await fixture()
try {
const failed = await run('office', { DSH_FIXTURE_FAIL: '1' }).done
expect(failed.code).toBe(1)
expect(failed.stderr).toContain('fixture download failed')
expect(await readdir(join(cache, 'a'.repeat(64)))).toEqual([])
expect((await run('office').done).code).toBe(0)
} finally { await rm(root, { recursive: true, force: true }) }
})
it('publishes one complete cache when two processes download concurrently', async () => {
const { root, cache, run } = await fixture()
const jobs = [run('office', { DSH_FIXTURE_BARRIER: '1' }), run('office', { DSH_FIXTURE_BARRIER: '1' })]
try {
await Promise.all(jobs.map(job => job.ready))
for (const job of jobs) job.child.stdin.end('continue')
const results = await Promise.all(jobs.map(job => job.done))
for (const result of results) expect(result.code, result.stderr).toBe(0)
expect(results[0]!.stdout).toBe(results[1]!.stdout)
expect(await readdir(join(cache, 'a'.repeat(64)))).toEqual(['office'])
} finally {
for (const job of jobs) if (job.child.exitCode === null && job.child.signalCode === null) job.child.kill()
await Promise.allSettled(jobs.map(job => job.done))
await rm(root, { recursive: true, force: true })
}
})
it('leaves ordinary CLI arguments to the CLI and rejects an invalid private download selector', async () => {
const { root, cache, run } = await fixture()
try {
expect(await run(undefined, {}, ['--download-office']).done).toMatchObject({
code: 0, stdout: JSON.stringify({ argv: ['--download-office'] }) + '\n',
})
const rejected = await run('invalid').done
expect(rejected.code).toBe(1)
expect(rejected.stderr).toContain('DSH_RUNTIME_DOWNLOAD must select office or primary')
await expect(readdir(cache)).rejects.toMatchObject({ code: 'ENOENT' })
} finally { await rm(root, { recursive: true, force: true }) }
})
+47 -7
View File
@@ -884,10 +884,18 @@ def main() -> None:
def smoke_sdk_authoring(base_url: str, executable: Path, update_snapshots: bool) -> None:
"""Query the bundled Python and switch skills without replacing that environment."""
with tempfile.TemporaryDirectory(prefix="dsh-sdk-authoring-cache-") as cache_directory:
_smoke_sdk_authoring_cached(base_url, executable, update_snapshots, Path(cache_directory))
def _smoke_sdk_authoring_cached(base_url: str, executable: Path, update_snapshots: bool, cache: Path) -> None:
from deepseek_harness import DeepSeekHarness
resources = executable.with_name(executable.name.removeprefix("deepseek-harness-sdk-runtime-").removesuffix(".exe"))
manifest = json.loads((resources / "primary-runtime/runtime.json").read_text())
cache_environment = {**os.environ, "DSH_RESOURCE_CACHE": str(cache), "DSH_RUNTIME_DOWNLOAD": "primary"}
primary = Path(json.loads(subprocess.check_output([str(executable)], encoding="utf-8", env=cache_environment)))
resources = primary.parent
manifest = json.loads((primary / "runtime.json").read_text())
for mode in ("default", "replacement", "disabled"):
with tempfile.TemporaryDirectory(prefix="dsh-sdk-authoring-") as temporary:
root = Path(temporary).resolve()
@@ -903,7 +911,7 @@ def smoke_sdk_authoring(base_url: str, executable: Path, update_snapshots: bool)
provider="deepseek-official", model="smoke-model", cwd=str(root),
dsh_bin=str(executable), dsh_home=str(home), patches=(str(patch),),
api_key="sk-keyless-smoke", base_url=base_url,
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"},
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", "DSH_RESOURCE_CACHE": str(cache)},
request_timeout_seconds=60,
) as harness:
result = harness.run(AUTHORING_PROMPT, session_id="authoring")
@@ -958,7 +966,9 @@ def smoke_sdk_office(executable: Path) -> None:
shutil.copytree(source, destination)
else:
shutil.copy2(source, destination)
office = root / f"{stem}-office"
cache_environment = {**os.environ, "DSH_RESOURCE_CACHE": str(root / "cache")}
office = Path(json.loads(subprocess.check_output([str(relocated)], encoding="utf-8", env={**cache_environment, "DSH_RUNTIME_DOWNLOAD": "office"})))
assert not (office.parent / "primary").exists(), "Office download unexpectedly prepared the authoring environment"
adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
native = stem.removeprefix("deepseek-harness-sdk-runtime-").replace("win-", "win32-").replace("macos-", "darwin-")
declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {})
@@ -997,7 +1007,7 @@ def smoke_sdk_office(executable: Path) -> None:
patches=(str(patch),),
api_key="sk-keyless-smoke",
base_url="http://127.0.0.1:9",
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1"},
env={"DSH_PERMISSION_MODE": "danger-full-access", "DSH_TELEMETRY_DISABLED": "1", "DSH_RESOURCE_CACHE": str(root / "cache")},
# The startup plugin awaits a converter with a 120-second deadline before JSON-RPC is ready.
initialize_timeout_seconds=180,
request_timeout_seconds=180,
@@ -1007,10 +1017,32 @@ def smoke_sdk_office(executable: Path) -> None:
if result["backend"] != expected_backend:
raise AssertionError(f"Office conversion did not use {expected_backend}: {result}")
if not result["moduleUrl"].startswith(office.as_uri() + "/"):
raise AssertionError(f"Office module was not loaded from the relocated wheel: {result}")
raise AssertionError(f"Office module was not loaded from the downloaded sidecar: {result}")
pdf = output.read_bytes()
if len(pdf) < 100 or not pdf.startswith(b"%PDF-"):
raise AssertionError(f"Office conversion produced an invalid PDF at {output}")
workbook = root / "input.xlsx"
with zipfile.ZipFile(workbook, "w", compression=zipfile.ZIP_DEFLATED) as archive:
archive.writestr("[Content_Types].xml", '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>')
archive.writestr("_rels/.rels", '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>')
archive.writestr("xl/workbook.xml", '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets></workbook>')
archive.writestr("xl/_rels/workbook.xml.rels", '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>')
archive.writestr("xl/worksheets/sheet1.xml", '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData><row r="1"><c r="A1"><f>1+2</f><v>0</v></c></row></sheetData></worksheet>')
subprocess.run([sys.executable, "-c", """
import sys, zipfile
from pathlib import Path
from xml.etree import ElementTree
from deepseek_harness.office import convert, render_images, recalculate
root = Path(sys.argv[1])
assert convert(root / 'document.docx', root / 'python.pdf').backend == sys.argv[2]
rendered = render_images(root / 'python.pdf', root / 'images', pages=[1])
assert rendered.images and rendered.images[0].path.read_bytes().startswith(b'\\x89PNG')
recalculate(root / 'input.xlsx', root / 'calculated.xlsx')
with zipfile.ZipFile(root / 'calculated.xlsx') as archive:
sheet = ElementTree.fromstring(archive.read('xl/worksheets/sheet1.xml'))
assert sheet.find('.//{*}c[@r="A1"]/{*}v').text == '3'
""", str(root), expected_backend], env={name: value for name, value in cache_environment.items() if name != "DSH_OFFICE_SIDECAR"}, check=True, timeout=180)
assert not (office.parent / "primary").exists(), "Python Office API unexpectedly downloaded authoring resources"
print(f"smoke-python-runtime: relocated Office {result['backend']} DOCX produced {len(pdf)} PDF bytes")
@@ -1336,6 +1368,8 @@ def smoke_sdk_dynamic_tools(base_url: str, executable: Path, update_snapshots: b
"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"),
# Pin the tool baseline independently of optional resources in the host cache.
"DSH_PRIMARY_RUNTIME": "",
},
api_key="sk-keyless-smoke", base_url=base_url, request_timeout_seconds=60,
) as harness:
@@ -2307,11 +2341,17 @@ def build_dynamic_tools_snapshot_files(
assert changes == [[], [{"type": "tool_addition", "tool": {"type": "tool_reference", "name": "snapshot_ping"}}], []], changes
assert requests[1]["messages"][:len(requests[0]["messages"])] == requests[0]["messages"], "tool addition changed earlier messages"
replacements = [(str(cwd), "{{cwd}}"), (result.session_id, "{{session}}")]
header_references = {header["seq"]: f"{{{{header:{index + 1}}}}}" for index, header in enumerate(headers)}
snapshot_events = [
{**event, "data": {**event["data"], "headerSeq": header_references[event["data"]["headerSeq"]]}}
if event["type"] == "developer/message" and "headerSeq" in event["data"] else event
for event in events if event["type"] in {"request/header", "request/context", "developer/message"}
]
evidence = {
"finalResponse": result.final_response,
"declarations": declarations,
"requestToolChanges": changes,
"events": [event for event in events if event["type"] in {"request/header", "request/context", "developer/message"}],
"events": snapshot_events,
"toolCalls": calls,
"pingResult": ping,
}
@@ -56,7 +56,6 @@
"job_list",
"job_output",
"list_agents",
"load_workspace_dependencies",
"read",
"read_image",
"send_message",
@@ -87,7 +86,7 @@
},
{
"type": "request/header",
"seq": 20,
"seq": 19,
"time": 0,
"data": {
"header": {
@@ -113,7 +112,6 @@
"job_list",
"job_output",
"list_agents",
"load_workspace_dependencies",
"read",
"read_image",
"send_message",
@@ -135,7 +133,7 @@
},
{
"type": "developer/message",
"seq": 21,
"seq": 20,
"time": 0,
"data": {
"turn": 1,
@@ -153,13 +151,13 @@
"role": "developer",
"id": "{{messageId}}"
},
"headerSeq": 20
"headerSeq": "{{header:2}}"
},
"surfaceOp": "append"
},
{
"type": "request/header",
"seq": 28,
"seq": 26,
"time": 0,
"data": {
"header": {
@@ -185,7 +183,6 @@
"job_list",
"job_output",
"list_agents",
"load_workspace_dependencies",
"read",
"read_image",
"send_message",
@@ -206,7 +203,7 @@
},
{
"type": "developer/message",
"seq": 29,
"seq": 27,
"time": 0,
"data": {
"turn": 1,
+117
View File
@@ -0,0 +1,117 @@
import { cp, lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { create as createTar } from 'tar'
import { expect, it, onTestFinished } from 'vitest'
import { artifactManifestSchema, fileDigest, payloadFiles, SSH_HELPER_TARGETS, verifyPayload } from './artifact.ts'
import { macosDeploymentVersions } from './native.ts'
import { extractHelper } from '../verify-ssh-helper-artifact.ts'
import { prepareSshRelease } from './release.ts'
async function fixture() {
const root = await mkdtemp(join(tmpdir(), 'dsh-helper-archive-'))
onTestFinished(() => rm(root, { recursive: true, force: true }))
const payload = join(root, 'dsh-ssh-helper')
await mkdir(join(payload, 'native/system'), { recursive: true })
await writeFile(join(payload, 'dsh-ssh-helper'), 'fixture executable')
await writeFile(join(payload, 'native/system/package.json'), '{}')
const metadata = {
format: 1, version: '1.0.0', commit: 'a'.repeat(40), dirty: false,
target: 'node24-linux-x64', protocol: 2, nodeVersion: 'v24.1.0',
files: await Promise.all((await payloadFiles(payload)).map(async path => ({
path, sha256: await fileDigest(join(payload, path)), mode: (await lstat(join(payload, path))).mode & 0o777,
}))),
}
const save = async () => { await writeFile(join(payload, 'manifest.json'), JSON.stringify(metadata)) }
await save()
return { root, payload, metadata, save }
}
it('validates the exact unpacked inventory and rejects altered bytes', async () => {
const test = await fixture()
expect(await verifyPayload(test.payload)).toEqual(test.metadata)
await writeFile(join(test.payload, 'dsh-ssh-helper'), 'changed executable')
await expect(verifyPayload(test.payload)).rejects.toThrow('digest differs')
})
it('rejects missing, undeclared and duplicate payload files', async () => {
const test = await fixture()
await writeFile(join(test.payload, 'undeclared'), '')
await expect(verifyPayload(test.payload)).rejects.toThrow('inventory')
await rm(join(test.payload, 'undeclared'))
test.metadata.files.push(test.metadata.files[0]!)
await test.save()
await expect(verifyPayload(test.payload)).rejects.toThrow('inventory')
test.metadata.files.pop()
await test.save()
await rm(join(test.payload, 'native/system/package.json'))
await expect(verifyPayload(test.payload)).rejects.toThrow('inventory')
})
it.each(['../outside', '/absolute', 'a/../b', 'a\\b', 'a//b'])('rejects unsafe manifest path %s', async (path) => {
const test = await fixture()
expect(() => artifactManifestSchema.parse({ ...test.metadata, files: [{ ...test.metadata.files[0], path }] })).toThrow('payload path')
})
it.skipIf(process.platform === 'win32')('rejects links before extracting an archive', async () => {
const test = await fixture()
await symlink('/etc/passwd', join(test.payload, 'outside'))
await expect(payloadFiles(test.payload)).rejects.toThrow('symbolic link')
const archive = join(test.root, 'linked.tar.gz')
await createTar({ cwd: test.root, file: archive, gzip: true }, ['dsh-ssh-helper'])
const extracted = join(test.root, 'unpack')
await mkdir(extracted)
await expect(extractHelper(archive, extracted)).rejects.toThrow('Invalid SSH helper archive entry')
})
it('extracts and verifies a portable archive in a new directory', async () => {
const test = await fixture()
const archive = join(test.root, 'helper.tar.gz')
await createTar({ cwd: test.root, file: archive, gzip: true, portable: true }, ['dsh-ssh-helper'])
const extracted = join(test.root, 'relocated')
await mkdir(extracted)
const directory = await extractHelper(archive, extracted)
expect(dirname(directory)).toBe(extracted)
expect(await verifyPayload(directory)).toEqual(test.metadata)
expect(await readFile(join(directory, 'dsh-ssh-helper'), 'utf8')).toBe('fixture executable')
})
it('reads minimum macOS versions without accepting a linked library version as the OS version', () => {
expect(macosDeploymentVersions('Load command 0\ncmd LC_VERSION_MIN_MACOSX\nversion 11.0\nLoad command 1\ncmd LC_LOAD_DYLIB\ncurrent version 1311.100.3\nLoad command 2\ncmd LC_BUILD_VERSION\nminos 14.0\nsdk 15.1\n'))
.toEqual(['11.0', '14.0'])
expect(macosDeploymentVersions('Load command 0\ncmd LC_LOAD_DYLIB\ncurrent version 14.0\n')).toEqual([])
})
it('publishes only a complete clean-source matrix with native and no-Node evidence', async () => {
const test = await fixture()
const archives = join(test.root, 'release')
await mkdir(archives)
const checks = ['executable-handshake', 'guarded-files-and-streams', 'managed-process-output', 'process-cancellation', 'native-pty', 'native-flock', 'embedded-ptc-and-deadline', 'sandbox-enforcement']
for (const target of SSH_HELPER_TARGETS) {
const parent = join(test.root, target)
await mkdir(parent)
const payload = join(parent, 'dsh-ssh-helper')
await cp(test.payload, payload, { recursive: true })
const manifest = { ...test.metadata, target }
await writeFile(join(payload, 'manifest.json'), JSON.stringify(manifest))
const archive = join(archives, `dsh-ssh-helper-1.0.0-${target.slice('node24-'.length)}.tar.gz`)
await createTar({ cwd: parent, file: archive, gzip: true, portable: true }, ['dsh-ssh-helper'])
await writeFile(`${archive}.sha256`, `${await fileDigest(archive)} archive\n`)
const runtime = { checks, nodeVersion: 'v24.1.0', platform: target.includes('linux') ? 'linux' : 'darwin', sandboxLane: 'required' }
await writeFile(join(archives, `${target}.json`), JSON.stringify({ manifest, runtime, restrictedWorker: true, readonlyInstall: true, concurrentColdStarts: 2 }))
if (target.includes('linux')) {
await writeFile(join(archives, `${target}.ssh.json`), JSON.stringify({ manifest, runtime: { ...runtime, checks: [...checks, 'target-has-no-node'], sandboxLane: 'portable' }, glibc: 'glibc 2.28' }))
await writeFile(join(archives, `${target}.landlock.json`), JSON.stringify({ manifest, runtime, expectedBackend: 'landlock-run' }))
}
}
expect(await prepareSshRelease(archives, '1.0.0', test.metadata.commit)).toHaveLength(5)
expect((await readFile(join(archives, 'SHA256SUMS'), 'utf8')).trim().split('\n')).toHaveLength(4)
await expect(prepareSshRelease(archives, '1.0.0', 'b'.repeat(40))).rejects.toThrow()
const report = join(archives, 'node24-linux-x64.ssh.json')
const evidence = JSON.parse(await readFile(report, 'utf8')) as { runtime: { checks: string[] } }
evidence.runtime.checks = evidence.runtime.checks.filter(check => check !== 'target-has-no-node')
await writeFile(report, JSON.stringify(evidence))
await expect(prepareSshRelease(archives, '1.0.0', test.metadata.commit)).rejects.toThrow('lacks target-has-no-node')
await rm(report)
await expect(prepareSshRelease(archives, '1.0.0', test.metadata.commit)).rejects.toThrow('ENOENT')
})
+88
View File
@@ -0,0 +1,88 @@
/** Versioned archive metadata and file integrity checks for SSH helper releases. */
import { createHash } from 'node:crypto'
import { lstat, readFile, readdir } from 'node:fs/promises'
import { join, relative, sep } from 'node:path'
import { z } from 'zod'
/** Native target identifiers accepted by the builder and release workflow. */
export const SSH_HELPER_TARGETS = ['node24-linux-x64', 'node24-linux-arm64', 'node24-macos-x64', 'node24-macos-arm64'] as const
/** One qualified OS and architecture for a bundled Node 24 helper. */
export type SshHelperTarget = typeof SSH_HELPER_TARGETS[number]
const payloadPath = z.string().min(1).refine(value =>
!value.startsWith('/') && !value.includes('\\') && !value.includes('\0')
&& value.split('/').every(part => part !== '' && part !== '.' && part !== '..'), 'invalid payload path')
/** Parser for metadata supplied beside an executable, including every runtime resource. */
export const artifactManifestSchema = z.object({
format: z.literal(1),
version: z.string().regex(/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/),
commit: z.string().regex(/^[a-f0-9]{40}$/),
dirty: z.boolean(),
target: z.enum(SSH_HELPER_TARGETS),
protocol: z.number().int().positive(),
nodeVersion: z.string().regex(/^v24\./),
files: z.array(z.object({
path: payloadPath, sha256: z.string().regex(/^[a-f0-9]{64}$/), mode: z.number().int().min(0).max(0o777),
}).strict()).min(1),
}).strict()
/** Results written by the isolated Loader consumer after its assertions succeed. */
export const runtimeReportSchema = z.object({
checks: z.array(z.string()), platform: z.enum(['linux', 'darwin']), nodeVersion: z.string(),
sandboxLane: z.enum(['required', 'unavailable', 'portable']),
}).strict()
/** Parsed release metadata; files excludes manifest.json to avoid a self-referential digest. */
export type ArtifactManifest = z.infer<typeof artifactManifestSchema>
/**
* Enumerate an owned payload without following links outside its directory.
* @param root - unpacked payload directory.
* @returns sorted POSIX relative file paths, excluding manifest.json.
*/
export async function payloadFiles(root: string): Promise<string[]> {
const paths: string[] = []
const visit = async (directory: string): Promise<void> => {
for (const name of await readdir(directory)) {
const path = join(directory, name)
const metadata = await lstat(path)
if (metadata.isSymbolicLink()) throw new Error(`SSH helper payload contains a symbolic link: ${path}`)
if (metadata.isDirectory()) await visit(path)
else if (metadata.isFile()) paths.push(relative(root, path).split(sep).join('/'))
else throw new Error(`SSH helper payload contains a non-regular file: ${path}`)
}
}
await visit(root)
return paths.filter(path => path !== 'manifest.json').sort()
}
/**
* Hash the exact file bytes used for installation or publication.
* @param path - regular file path.
* @returns lowercase SHA-256.
*/
export async function fileDigest(path: string): Promise<string> {
return createHash('sha256').update(await readFile(path)).digest('hex')
}
/**
* Verify all payload bytes and permissions before running an unpacked release.
* @param directory - unpacked payload root.
* @returns the validated metadata; missing, duplicate, extra, or altered files reject.
*/
export async function verifyPayload(directory: string): Promise<ArtifactManifest> {
const manifest = artifactManifestSchema.parse(JSON.parse(await readFile(join(directory, 'manifest.json'), 'utf8')))
const actual = await payloadFiles(directory)
const declared = manifest.files.map(file => file.path).sort()
if (new Set(declared).size !== declared.length || JSON.stringify(actual) !== JSON.stringify(declared)) {
throw new Error('SSH helper payload file inventory differs from manifest.json')
}
if (!declared.includes('dsh-ssh-helper') || !declared.includes('native/system/package.json')) throw new Error('SSH helper payload omits required runtime files')
for (const file of manifest.files) {
const path = join(directory, file.path)
if (await fileDigest(path) !== file.sha256) throw new Error(`SSH helper payload digest differs: ${file.path}`)
if (((await lstat(path)).mode & 0o777) !== file.mode) throw new Error(`SSH helper payload permissions differ: ${file.path}`)
}
return manifest
}
+74
View File
@@ -0,0 +1,74 @@
/** Inspect every native payload, including modules embedded in the executable. */
import { execFileSync } from 'node:child_process'
import { open } from 'node:fs/promises'
import { join } from 'node:path'
import { payloadFiles, type SshHelperTarget } from './artifact.ts'
function newer(version: string, ceiling: number[]): boolean {
const components = version.split('.').map(Number)
for (let index = 0; index < Math.max(components.length, ceiling.length); index += 1) {
const difference = (components[index] ?? 0) - (ceiling[index] ?? 0)
if (difference !== 0) return difference > 0
}
return false
}
/**
* Read deployment versions without confusing linked-library versions with OS requirements.
* @param output - otool load-command text, possibly containing multiple architecture slices.
* @returns every minimum macOS version declared by the binary.
*/
export function macosDeploymentVersions(output: string): string[] {
let command: string | undefined
const versions: string[] = []
for (const line of output.split('\n')) {
const text = line.trim()
if (/^Load command \d+$/.test(text)) command = undefined
else if (text.startsWith('cmd ')) command = text.slice(4)
else {
const match = /^(minos|version)\s+(\d+(?:\.\d+)+)$/.exec(text)
if (match !== null && ((command === 'LC_BUILD_VERSION' && match[1] === 'minos') || (command === 'LC_VERSION_MIN_MACOSX' && match[1] === 'version'))) versions.push(match[2] as string)
}
}
return versions
}
/**
* Inspect all ELF or Mach-O files for the requested architecture and deployment baseline.
* @param directory - symlink-free staging or payload directory.
* @param target - build target whose ABI must match every native file.
* @returns relative paths of inspected native files.
*/
export async function verifyNativePayload(directory: string, target: SshHelperTarget): Promise<string[]> {
const inspected: string[] = []
const linux = target.includes('-linux-')
const arch = target.endsWith('-arm64') ? 'arm64' : 'x64'
for (const path of await payloadFiles(directory)) {
const filename = join(directory, path)
const file = await open(filename, 'r')
const header = Buffer.alloc(4)
try { await file.read(header, 0, 4, 0) } finally { await file.close() }
const magic = header.readUInt32BE()
const elf = magic === 0x7f454c46
const macho = [0xcffaedfe, 0xcefaedfe, 0xfeedfacf, 0xfeedface, 0xcafebabe, 0xbebafeca, 0xcafebabf].includes(magic)
if (!elf && !macho) continue
if (linux !== elf) throw new Error(`Native file does not belong to ${target}: ${path}`)
if (elf) {
const elfHeader = execFileSync('readelf', ['--file-header', filename], { encoding: 'utf8' })
const expected = arch === 'arm64' ? /Machine:\s+AArch64/ : /Machine:\s+Advanced Micro Devices X86-64/
if (!expected.test(elfHeader)) throw new Error(`ELF architecture differs from ${target}: ${path}`)
const versions = execFileSync('readelf', ['--version-info', filename], { encoding: 'utf8' })
for (const match of versions.matchAll(/\bGLIBC_(\d+(?:\.\d+)+)/g)) {
if (newer(match[1] as string, [2, 28])) throw new Error(`${path} requires GLIBC_${match[1]}, exceeding 2.28`)
}
} else {
execFileSync('lipo', [filename, '-verify_arch', arch === 'arm64' ? 'arm64' : 'x86_64'], { stdio: 'pipe' })
const loads = execFileSync('otool', ['-l', filename], { encoding: 'utf8' })
const versions = macosDeploymentVersions(loads)
if (versions.length === 0) throw new Error(`Mach-O has no deployment version: ${path}`)
if (versions.some(version => newer(version, [14, 0]))) throw new Error(`${path} requires macOS newer than 14.0`)
}
inspected.push(path)
}
return inspected
}
+42
View File
@@ -0,0 +1,42 @@
/** Short handshake against an actual executable, with bounded process ownership. */
import { spawn } from 'node:child_process'
import { z } from 'zod'
import { SshRpcPeer, SSH_PROTOCOL_VERSION } from '../../packages/ssh/ssh/src/protocol.ts'
import { helloSchema } from '../../packages/ssh/ssh/src/schemas.ts'
/**
* Run a packaged helper and read its runtime version through the private wire protocol.
* @param executable - physical executable under test.
* @param workspace - existing directory supplied for the handshake.
* @returns the validated handshake after the helper has exited.
*/
export async function probeHelper(executable: string, workspace: string): Promise<z.infer<typeof helloSchema>> {
const child = spawn(executable, [], { cwd: workspace, env: { ...process.env, NODE_OPTIONS: '--disable-sigusr1' }, stdio: ['pipe', 'pipe', 'pipe'] })
let diagnostic = ''
child.stderr.on('data', (chunk: Buffer) => { diagnostic = (diagnostic + chunk.toString()).slice(-8192) })
const closed = new Promise<void>((resolve, reject) => {
child.once('error', reject)
child.once('close', (code, signal) => {
if (code === 0 && signal === null) resolve()
else reject(new Error(`SSH helper probe exited (${code ?? signal}): ${diagnostic}`))
})
})
void closed.catch(() => {})
const rpc = new SshRpcPeer(child.stdout, child.stdin, 64 * 1024 * 1024, 128)
child.once('error', (error) => { rpc.close(error) })
const force = setTimeout(() => { child.kill('SIGKILL') }, 30_000)
try {
const hello = await rpc.request('hello', { protocol: SSH_PROTOCOL_VERSION, workspace, leaseMs: 30_000 }, helloSchema, AbortSignal.timeout(20_000))
await rpc.request('close', {}, z.null(), AbortSignal.timeout(5000))
child.stdin.end()
await closed
return hello
} catch (error) {
child.kill('SIGTERM')
await closed.catch(() => {})
throw new Error(`SSH helper handshake failed: ${diagnostic}`, { cause: error })
} finally {
clearTimeout(force)
rpc.close()
}
}
+76
View File
@@ -0,0 +1,76 @@
/** Require four complete, same-commit executable reports before publishing release assets. */
import assert from 'node:assert/strict'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { z } from 'zod'
import { artifactManifestSchema, fileDigest, runtimeReportSchema, SSH_HELPER_TARGETS, verifyPayload, type ArtifactManifest } from './artifact.ts'
import { extractHelper } from '../verify-ssh-helper-artifact.ts'
const reportSchema = z.object({
manifest: artifactManifestSchema,
runtime: runtimeReportSchema,
restrictedWorker: z.boolean().optional(), expectedBackend: z.string().nullable().optional(), glibc: z.string().optional(),
readonlyInstall: z.boolean().optional(), concurrentColdStarts: z.number().optional(),
}).strict()
const coreChecks = ['executable-handshake', 'guarded-files-and-streams', 'managed-process-output', 'process-cancellation', 'native-pty', 'native-flock', 'embedded-ptc-and-deadline']
async function report(path: string, manifest: ArtifactManifest, checks: readonly string[]) {
const evidence = reportSchema.parse(JSON.parse(await readFile(path, 'utf8')))
assert.deepEqual(evidence.manifest, manifest, `Report metadata differs: ${path}`)
assert.equal(evidence.runtime.nodeVersion, manifest.nodeVersion)
assert.equal(evidence.runtime.platform, manifest.target.includes('-linux-') ? 'linux' : 'darwin')
for (const check of [...coreChecks, ...checks]) assert(evidence.runtime.checks.includes(check), `Report ${path} lacks ${check}`)
return evidence
}
/**
* Validate archives and their required native and SSH evidence, then write the release checksum list.
* @param directory - downloaded artifacts from one successful four-target workflow run.
* @param version - version selected by the release tag.
* @param commit - immutable source commit checked out by every build job.
* @returns the exact archive and checksum files permitted for upload.
*/
export async function prepareSshRelease(directory: string, version: string, commit: string): Promise<string[]> {
const temporary = await mkdtemp(join(tmpdir(), 'dsh-helper-release-'))
const archives: string[] = []
const sums: string[] = []
try {
for (const target of SSH_HELPER_TARGETS) {
const archive = join(directory, `dsh-ssh-helper-${version}-${target.slice('node24-'.length)}.tar.gz`)
const digest = await fileDigest(archive)
assert.equal(digest, (await readFile(`${archive}.sha256`, 'utf8')).split(/\s/)[0])
const destination = join(temporary, target)
await mkdir(destination)
const manifest = await verifyPayload(await extractHelper(archive, destination))
assert.equal(manifest.target, target)
assert.equal(manifest.version, version)
assert.equal(manifest.commit, commit)
assert.equal(manifest.dirty, false, 'Release artifacts must come from a clean source checkout')
const native = await report(join(directory, `${target}.json`), manifest, ['sandbox-enforcement'])
assert.equal(native.runtime.sandboxLane, 'required')
assert.equal(native.readonlyInstall, true)
assert.equal(native.concurrentColdStarts, 2)
if (target.includes('-macos-')) assert.equal(native.restrictedWorker, true)
else {
const ssh = await report(join(directory, `${target}.ssh.json`), manifest, ['target-has-no-node'])
assert.equal(ssh.glibc, 'glibc 2.28')
const landlock = await report(join(directory, `${target}.landlock.json`), manifest, ['sandbox-enforcement'])
assert.equal(landlock.expectedBackend, 'landlock-run')
assert.equal(landlock.runtime.sandboxLane, 'required')
}
archives.push(archive)
sums.push(`${digest} ${basename(archive)}`)
}
const checksums = join(directory, 'SHA256SUMS')
await writeFile(checksums, `${sums.join('\n')}\n`)
return [...archives, checksums]
} finally { await rm(temporary, { recursive: true, force: true }) }
}
if (import.meta.main) {
const { values } = parseArgs({ options: { directory: { type: 'string' }, version: { type: 'string' }, commit: { type: 'string' } } })
if (values.directory === undefined || values.version === undefined || values.commit === undefined) throw new Error('Release verification requires --directory, --version and --commit')
console.log((await prepareSshRelease(resolve(values.directory), values.version, values.commit)).join('\n'))
}
+67
View File
@@ -0,0 +1,67 @@
/** Run the embedded worker while macOS denies access to the checkout and host Node. */
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { Duplex, type Readable, type Writable } from 'node:stream'
import { join } from 'node:path'
import { z } from 'zod'
import { JsonChannel } from '../../packages/ptc-runtime/ptc-runtime-node/src/channel.ts'
import { decodePtcJsonWire } from '../../packages/ptc-runtime/ptc-runtime-node/src/json-wire.ts'
import { SUBPROCESS_CONTROL_ENV, SUBPROCESS_CONTROL_FD } from '../../packages/subprocess/subprocess/src/control.ts'
/**
* Execute a PTC program with the host runtime and source files inaccessible.
* @param executable - relocated helper executable.
* @param workspace - isolated program working directory.
* @param home - empty helper home and native-resource cache.
* @param checkout - real source checkout path denied by Seatbelt.
* @param hostNode - real host Node path denied by Seatbelt.
*/
export async function probeRestrictedWorker(
executable: string, workspace: string, home: string, checkout: string, hostNode: string,
): Promise<void> {
const profile = `(version 1)(allow default)${[checkout, hostNode].map(path => `(deny file-read* (subpath ${JSON.stringify(path)}))(deny process-exec (subpath ${JSON.stringify(path)}))`).join('')}`
const child = spawn('/usr/bin/sandbox-exec', ['-p', profile, executable, '1048576'], {
cwd: workspace, env: { HOME: home, TMPDIR: home, PATH: '/usr/bin:/bin', DSH_PTC_RUNTIME_NODE: '1', [SUBPROCESS_CONTROL_ENV]: 'pipe', NODE_OPTIONS: '--disable-sigusr1' },
stdio: ['ignore', 'pipe', 'pipe', 'ignore', 'ignore', 'ignore', 'ignore', 'pipe'],
})
const completed = Promise.withResolvers<unknown>()
let diagnostic = ''
assert(child.stderr !== null && child.stdout !== null)
child.stderr.on('data', (chunk: Buffer) => { diagnostic = (diagnostic + chunk.toString()).slice(-8192) })
child.stdout.resume()
const exited = new Promise<void>((resolve, reject) => {
child.once('error', reject)
child.once('close', (code, signal) => {
if (code === 0 && signal === null) resolve()
else reject(new Error(`Restricted PTC worker exited (${code ?? signal}): ${diagnostic}`))
})
})
void exited.catch((error: unknown) => { completed.reject(error) })
const streams: ReadonlyArray<Readable | Writable | null | undefined> = child.stdio
const control = streams[SUBPROCESS_CONTROL_FD]
assert(control instanceof Duplex)
const channel = new JsonChannel(control, 1_048_576, (raw) => {
const message = z.object({ type: z.string(), value: z.unknown().optional(), error: z.unknown().optional() }).parse(raw)
if (message.type === 'ready') {
const code = `const fs=await import('node:fs/promises'); const denied=[]; for(const path of ${JSON.stringify([join(checkout, 'package.json'), hostNode])}) { try { await fs.readFile(path); denied.push('readable') } catch(error) { denied.push(error.code) } } return {executable:process.execPath,env:Object.keys(process.env),denied};`
void channel.send({ type: 'boot', data: { code, namespaces: [], maxOutputBytes: 65_536 } })
.catch((error: unknown) => { completed.reject(error) })
} else if (message.type === 'done') {
if (message.error !== undefined) completed.reject(new Error(JSON.stringify(message.error)))
else completed.resolve(decodePtcJsonWire(message.value))
channel.close()
} else completed.reject(new Error(`Unexpected restricted-worker message: ${message.type}`))
}, (error) => { completed.reject(error) })
const force = setTimeout(() => { child.kill('SIGKILL') }, 20_000)
try {
const result = z.object({ executable: z.string(), env: z.array(z.string()), denied: z.array(z.enum(['EPERM', 'EACCES'])) }).parse(await completed.promise)
assert.equal(result.executable, executable)
assert.deepEqual(result.env, [])
assert.equal(result.denied.length, 2)
await exited
} finally {
channel.close()
child.kill('SIGTERM')
try { await exited } finally { clearTimeout(force) }
}
}
+57
View File
@@ -0,0 +1,57 @@
import { isIP } from 'node:net'
import { networkInterfaces } from 'node:os'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { probeZonedLoopback } from './test-non-loopback-address.ts'
const binding = vi.hoisted(() => ({ refusal: undefined as Error | undefined }))
vi.mock('node:os', async importOriginal => ({
...await importOriginal<typeof import('node:os')>(),
networkInterfaces: vi.fn(),
}))
// The regression concerns OS interface metadata, not the test host's IPv6 availability.
vi.mock('node:http', () => ({
createServer: () => {
let reject: (error: Error) => void
return {
once(_event: string, listener: (error: Error) => void) { reject = listener },
listen(_options: { host: string; port: number }, ready: () => void) {
if (binding.refusal) reject(binding.refusal)
else ready()
},
close(closed: () => void) { closed() },
}
},
}))
beforeEach(() => {
binding.refusal = undefined
vi.mocked(networkInterfaces).mockReturnValue({
'Loopback Pseudo-Interface 1': [{
address: '::1',
netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
family: 'IPv6',
mac: '00:00:00:00:00:00',
internal: true,
scopeid: 0,
cidr: '::1/128',
}],
})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('returns a valid scoped literal when the interface display name contains spaces', async () => {
const host = await probeZonedLoopback()
expect(isIP(host ?? '')).toBe(6)
expect(host).toContain('%')
})
it('skips a scoped loopback whose listener refuses the bind', async () => {
binding.refusal = new Error('address unavailable')
vi.spyOn(console, 'info').mockImplementation(() => {})
expect(await probeZonedLoopback()).toBeUndefined()
})
+70
View File
@@ -0,0 +1,70 @@
/**
* Address probes for concrete-bind integration cases. Interface membership
* does not guarantee a usable address; hosts without one report why their
* network coverage is skipped.
*/
import { createServer } from 'node:http'
import { networkInterfaces } from 'node:os'
/**
* First candidate whose listener opens and closes successfully.
* @param candidates - local IP literals to probe.
* @param description - address category reported when no candidate can bind.
* @returns a bindable address, or undefined when the scenario must skip.
*/
async function probeBindableAddress(candidates: string[], description: string): Promise<string | undefined> {
const refused: string[] = []
for (const address of candidates) {
const server = createServer()
try {
await new Promise<void>((resolve, reject) => {
server.once('error', reject)
server.listen({ host: address, port: 0 }, resolve)
})
} catch (error) {
// A failed listen owns no socket to close.
refused.push(`${address}: ${error instanceof Error ? error.message : String(error)}`)
continue
}
await new Promise<void>((resolve) => { server.close(() => { resolve() }) })
return address
}
console.info(
`${description} coverage skipped: no bindable ${description} address`
+ ` (${candidates.length === 0 ? 'this host reports none' : `refused ${refused.join('; ')}`})`,
)
return undefined
}
/**
* First local non-loopback IPv4 address that really accepts a listen.
*
* IPv4 only: supported CI hosts need not own a non-loopback IPv6 address.
* @returns the bindable address, or undefined when the scenario must skip.
*/
export async function probeNonLoopbackIpv4(): Promise<string | undefined> {
const candidates: string[] = []
for (const entry of Object.values(networkInterfaces()).flat()) {
if (entry !== undefined && entry.family === 'IPv4' && !entry.internal) candidates.push(entry.address)
}
return probeBindableAddress(candidates, 'non-loopback IPv4')
}
/**
* Bindable IPv6 loopback with the numeric scope reported by the OS.
* Interface display names are not portable IPv6 zone identifiers.
*
* Loopback only: a link-local candidate is no substitute, because these
* scenarios need the loopback address itself — their advertised root and Host
* fence follow from it. A host that reports no bindable `::1` skips them.
* @returns the zoned literal after closing its probe listener, or undefined when none can bind.
*/
export async function probeZonedLoopback(): Promise<string | undefined> {
const candidates: string[] = []
for (const entry of Object.values(networkInterfaces()).flat()) {
if (entry !== undefined && entry.family === 'IPv6' && entry.address === '::1') {
candidates.push(`::1%${entry.scopeid}`)
}
}
return probeBindableAddress(candidates, 'zoned IPv6 loopback')
}
+10
View File
@@ -1916,6 +1916,11 @@
"symbol": "Config",
"source": "packages/host/webserver/src/index.ts"
},
{
"doc": "docs/subsystems/web-server.md",
"symbol": "TlsConfig",
"source": "packages/host/webserver/src/index.ts"
},
{
"doc": "docs/subsystems/storage.md",
"symbol": "StorageForms",
@@ -2306,6 +2311,11 @@
"symbol": "PtcRunSandbox",
"source": "packages/ptc-runtime/ptc-runtime/src/types.ts"
},
{
"doc": "docs/subsystems/ssh.md",
"symbol": "HelperLaunch",
"source": "packages/ssh/ssh/src/index.ts"
},
{
"doc": "docs/subsystems/ssh.md",
"symbol": "Config",
@@ -57,6 +57,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/host/product-telemetry-otel': { kind: 'none', reason: 'The plugin exports explicit analytics records without contributing model context.' },
'packages/api/terminal-controller': { kind: 'indirect', reason: 'Working-directory recovery owns directory context; user terminal screens never enter model requests.' },
'packages/client/ui-sidebar-terminal': { kind: 'none', reason: 'The browser renders user terminal screens without exposing them to the model.' },
'packages/ssh/ssh-helper-runtime': { kind: 'none', reason: 'The private executable carrier registers no model-facing content; its providers and consumers own operation results.' },
'packages/experimental/worktree': { kind: 'indirect', reason: 'The worktree tool renders creation results; the working-directory service owns model-visible directory context.' },
'packages/ssh/ssh': { kind: 'none', reason: 'The connection owner transports private provider operations; consumers own all model-facing content.' },
'packages/ssh/fs-ssh': { kind: 'indirect', reason: 'The remote filesystem delegates model rendering to the existing filesystem consumers.' },
-8
View File
@@ -243,14 +243,6 @@ describe('product package use', () => {
expect(failures(root, declarations())).toContain(`${candidate} must declare dsh.bundle.patch`)
})
it('rejects an on-demand contribution whose preset target is missing', () => {
const root = fixture()
pkg(root, candidateDir, candidate, { dsh: { bundle: { patch: './cordis.patch.yml' } } })
write(root, `${candidateDir}/cordis.patch.yml`, [{ preset: 'missing-preset', insert: [{ id: 'optional', name: core }] }])
profiles(root, [], [candidate])
expect(() => verifyProductUse(root, {})).toThrow(/missing-preset/)
})
it('rejects a missing workspace source mapping instead of consulting built exports', () => {
const root = fixture()
pkg(root, candidateDir, candidate)
+10
View File
@@ -39,6 +39,16 @@ afterEach(() => {
})
describe('verifyRuntimeClosure', () => {
it('checks a private worker closure without requiring application presets', async () => {
const manifestPath = 'packages/ssh/ssh-helper-runtime/package.json'
const root = fixture({ [manifestPath]: { name: 'helper', dependencies: { '@scope/worker': 'workspace:^' } } })
workspace(root, '@scope/worker', { peerDependencies: { '@scope/peer': 'workspace:^' } })
workspace(root, '@scope/peer', {})
expect((await verifyRuntimeClosure(root, manifestPath, false)).failures).toEqual(['helper -> @scope/worker -> @scope/peer'])
writeFileSync(join(root, manifestPath), JSON.stringify({ name: 'helper', dependencies: { '@scope/worker': 'workspace:^', '@scope/peer': 'workspace:^' } }))
expect(await verifyRuntimeClosure(root, manifestPath, false)).toMatchObject({ failures: [], presetCount: 0 })
})
it('requires only plugins active for each published target', async () => {
const root = fixture({
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/shared': 'workspace:^' } },
+6 -4
View File
@@ -42,18 +42,20 @@ export interface RuntimeClosureResult {
* Check that the runtime manifest contains every shipped-preset plugin and workspace peer.
* @param root repository root containing the runtime manifest and shipped presets.
* @param manifestPath runtime manifest path relative to {@link root}.
* @param includePresets whether this carrier includes the shipped application presets.
* @returns the discovered preset count, reachable workspace package count, and violations.
*/
export async function verifyRuntimeClosure(
root: string,
manifestPath = 'python/sdk-runtime/package.json',
includePresets = true,
): Promise<RuntimeClosureResult> {
const runtimeManifest = await loadManifest(resolve(root, manifestPath))
const runtimeName = runtimeManifest.name ?? manifestPath
const workspace = await loadWorkspacePackages(root)
const runtimeDependencies = runtimeManifest.dependencies ?? {}
const platforms = await loadJson<RuntimePlatformManifest>(resolve(root, 'python/sdk-runtime/platforms.json'))
const presetPaths = globSync(AGENT_PRESET_GLOB, { cwd: root }).sort()
const platforms = includePresets ? await loadJson<RuntimePlatformManifest>(resolve(root, 'python/sdk-runtime/platforms.json')) : {}
const presetPaths = includePresets ? globSync(AGENT_PRESET_GLOB, { cwd: root }).sort() : []
const targets = Object.keys(platforms).sort()
const parents = new Map<string, string | undefined>()
const queue: string[] = []
@@ -65,8 +67,8 @@ export async function verifyRuntimeClosure(
}
const failures: string[] = []
if (presetPaths.length === 0) failures.push(`no agent presets matched ${AGENT_PRESET_GLOB}`)
if (targets.length === 0) failures.push('python/sdk-runtime/platforms.json defines no runtime targets')
if (includePresets && presetPaths.length === 0) failures.push(`no agent presets matched ${AGENT_PRESET_GLOB}`)
if (includePresets && targets.length === 0) failures.push('python/sdk-runtime/platforms.json defines no runtime targets')
failures.push(...await missingPresetPlugins(root, runtimeDependencies, presetPaths, targets))
for (let index = 0; index < queue.length; index += 1) {
const packageName = queue[index]
+167
View File
@@ -0,0 +1,167 @@
/** Unpack and run a release archive through the actual Loader-mounted SSH consumers. */
import { spawn } from 'node:child_process'
import { chmod, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { extract, list } from 'tar'
import { fileDigest, runtimeReportSchema, verifyPayload, type ArtifactManifest } from './ssh-helper/artifact.ts'
import { probeRestrictedWorker } from './ssh-helper/worker-probe.ts'
import { verifyNativePayload } from './ssh-helper/native.ts'
const root = resolve(import.meta.dirname, '..')
const fixtures = join(root, 'packages/ssh/ssh-helper-runtime/tests/fixtures')
async function directoryPermissions(directory: string, mode: number): Promise<void> {
await chmod(directory, mode)
for (const entry of await readdir(directory, { withFileTypes: true })) {
if (entry.isDirectory()) await directoryPermissions(join(directory, entry.name), mode)
}
}
async function makeReadonly(directory: string, manifest: ArtifactManifest): Promise<void> {
for (const file of manifest.files) await chmod(join(directory, file.path), file.mode & ~0o222)
await chmod(join(directory, 'manifest.json'), 0o444)
await directoryPermissions(directory, 0o555)
}
/**
* Reject links and escaping paths before extracting a helper's release archive.
* @param archive - gzip tar file supplied by the build.
* @param directory - empty extraction directory owned by the caller.
* @returns the extracted payload root.
*/
export async function extractHelper(archive: string, directory: string): Promise<string> {
const entries: { path: string; type: string }[] = []
await list({ file: archive, onReadEntry: (entry) => { entries.push({ path: entry.path, type: entry.type }) } })
const seen = new Set<string>()
for (const entry of entries) {
const path = entry.path.replace(/\/$/, '')
if (!['File', 'Directory'].includes(entry.type) || path.split('/').some(part => part === '..' || part === '.' || part === '')
|| path.includes('\\') || !path.startsWith('dsh-ssh-helper') || (path !== 'dsh-ssh-helper' && !path.startsWith('dsh-ssh-helper/')) || seen.has(path)) {
throw new Error(`Invalid SSH helper archive entry: ${entry.path} (${entry.type})`)
}
seen.add(path)
}
if (!seen.has('dsh-ssh-helper/manifest.json')) throw new Error('SSH helper archive has no manifest.json')
await extract({ file: archive, cwd: directory, strict: true })
return join(directory, 'dsh-ssh-helper')
}
/**
* Run the isolated consumer process and await all output and process teardown.
* @param config - generated Loader configuration.
* @param report - output report path.
* @param sandbox - expected confinement availability on this lane.
* @param environment - isolated client environment, including any test-owned SSH wrapper.
*/
export async function runArtifactDriver(config: string, report: string, sandbox: string, environment: NodeJS.ProcessEnv): Promise<void> {
const child = spawn(process.execPath, ['--import', 'tsx/esm', join(fixtures, 'driver.ts'), config, report, sandbox], {
cwd: root, stdio: ['ignore', 'pipe', 'pipe'], env: environment,
})
let output = ''
for (const stream of [child.stdout, child.stderr]) stream.on('data', (chunk: Buffer) => {
output = (output + chunk.toString()).slice(-32_768)
})
const force = setTimeout(() => { child.kill('SIGKILL') }, 120_000)
try {
await new Promise<void>((resolve, reject) => {
child.once('error', reject)
child.once('close', (code, signal) => {
if (code === 0 && signal === null) resolve()
else reject(new Error(`SSH artifact consumer exited (${code ?? signal}):\n${output}`))
})
})
} finally { clearTimeout(force) }
}
/**
* Serialize the shared provider composition around a selected helper transport.
* @param connection - concrete Loader connection row, direct fixture or OpenSSH.
* @param workspace - target-side workspace path.
* @returns YAML consumed by the normal application boot path.
*/
export function artifactComposition(connection: { id: string; name: string; config: object }, workspace: string): string {
const rows = [
{ id: 'projections', name: '@deepseek-ai/dsh-session-projection' },
{ id: 'policy', name: '@deepseek-ai/dsh-sandbox-policy', config: { mode: 'workspace-write', workspaceRoot: workspace } },
connection,
{ id: 'fs', name: '@deepseek-ai/dsh-fs-ssh' },
{ id: 'subprocess', name: '@deepseek-ai/dsh-subprocess-ssh' },
{ id: 'sandbox', name: '@deepseek-ai/dsh-sandbox-ssh' },
]
return rows.map(row => `- ${JSON.stringify(row)}`).join('\n')
+ '\n- id: ptc\n name: "@deepseek-ai/dsh-ptc-runtime-node"\n inject: [ssh]\n config:\n launch: !!js ctx.ssh.ptcLaunch\n'
}
/**
* Verify archive integrity and all core operations after relocation outside the checkout.
* @param argv - verifier flags.
*/
export async function verifySshHelper(argv: string[]): Promise<void> {
const { values } = parseArgs({ args: argv, options: {
archive: { type: 'string' }, sandbox: { type: 'string', default: 'required' },
report: { type: 'string' },
backend: { type: 'string' },
} })
if (values.archive === undefined || !['required', 'unavailable'].includes(values.sandbox)) throw new Error('Use --archive=<file> --sandbox=required|unavailable')
if (values.backend !== undefined && !['bwrap', 'landlock-run', 'sandbox-exec'].includes(values.backend)) throw new Error('Unknown expected sandbox backend')
if (values.backend === 'landlock-run' && process.platform !== 'linux') throw new Error('Landlock verification requires Linux')
const archive = resolve(values.archive)
const checksum = (await readFile(`${archive}.sha256`, 'utf8')).split(/\s/)[0]
if (await fileDigest(archive) !== checksum) throw new Error('SSH helper archive checksum mismatch')
const temporary = await realpath(await mkdtemp(join(homedir(), '.dsh-helper-check-')))
try {
const installed = await extractHelper(archive, temporary)
const manifest = await verifyPayload(installed)
const host = `node24-${process.platform === 'darwin' ? 'macos' : process.platform}-${process.arch}`
if (manifest.target !== host) throw new Error(`Artifact ${manifest.target} cannot be validated on ${host}`)
await verifyNativePayload(installed, manifest.target)
await makeReadonly(installed, manifest)
const workspace = join(temporary, 'workspace')
const home = join(temporary, 'home')
await mkdir(workspace); await mkdir(home)
let path = '/usr/bin:/bin'
if (values.backend === 'landlock-run') {
const bin = join(temporary, 'bin')
await mkdir(bin)
await writeFile(join(bin, 'bwrap'), '#!/bin/sh\nexit 127\n')
await chmod(join(bin, 'bwrap'), 0o755)
path = `${bin}:${path}`
}
const executable = join(installed, 'dsh-ssh-helper')
const helperHash = await fileDigest(executable)
const environment = {
HOME: home, PATH: process.env.PATH, DSH_HOME: join(home, 'dsh'),
TSX_TSCONFIG_PATH: join(root, 'tsconfig.base.json'), DSH_TELEMETRY_DISABLED: '1',
...(values.backend === undefined ? {} : { DSH_EXPECT_SANDBOX_RUNNER: values.backend }),
}
const runs = await Promise.allSettled([0, 1].map(async (index) => {
const cwd = join(workspace, String(index))
await mkdir(cwd)
const config = join(temporary, `cordis-${index}.yml`)
const report = join(temporary, `report-${index}.json`)
await writeFile(config, artifactComposition({ id: 'ssh', name: join(fixtures, 'direct-helper.ts'), config: {
executable, helperHash, workspace: cwd, home, path,
} }, cwd))
await runArtifactDriver(config, report, values.sandbox, environment)
}))
const failures = runs.filter(result => result.status === 'rejected').map((result): unknown => result.reason)
if (failures.length > 0) throw new AggregateError(failures, 'Concurrent SSH artifact consumers failed')
if (process.platform === 'darwin') await probeRestrictedWorker(executable, workspace, home, await realpath(root), await realpath(process.execPath))
const evidence = { manifest, runtime: runtimeReportSchema.parse(JSON.parse(await readFile(join(temporary, 'report-0.json'), 'utf8'))),
restrictedWorker: process.platform === 'darwin', expectedBackend: values.backend ?? null,
readonlyInstall: true, concurrentColdStarts: 2 }
if (values.report !== undefined) {
const destination = resolve(values.report)
await mkdir(dirname(destination), { recursive: true })
await writeFile(destination, `${JSON.stringify(evidence, null, 2)}\n`)
}
console.log(`SSH helper ${manifest.target}: ${JSON.stringify(evidence.runtime)}`)
} finally {
await directoryPermissions(temporary, 0o755)
await rm(temporary, { recursive: true, force: true })
}
}
if (import.meta.main) await verifySshHelper(process.argv.slice(2))
+108
View File
@@ -0,0 +1,108 @@
/** Real OpenSSH acceptance against a glibc 2.28 container with no installed Node. */
import assert from 'node:assert/strict'
import { execFile, spawn } from 'node:child_process'
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { parseArgs, promisify } from 'node:util'
import { randomUUID } from 'node:crypto'
import { fileDigest, runtimeReportSchema, verifyPayload } from './ssh-helper/artifact.ts'
import { artifactComposition, extractHelper, runArtifactDriver } from './verify-ssh-helper-artifact.ts'
const run = promisify(execFile)
const root = resolve(import.meta.dirname, '..')
async function docker(...args: string[]): Promise<string> {
return (await run('docker', args, { timeout: 300_000, maxBuffer: 16 * 1024 * 1024 })).stdout.trim()
}
async function sshdReady(container: string): Promise<void> {
const logs = spawn('docker', ['logs', '--follow', container], { stdio: ['ignore', 'pipe', 'pipe'] })
let text = ''
const ready = Promise.withResolvers<void>()
const closed = new Promise<void>((resolve) => { logs.once('close', () => { resolve(); ready.reject(new Error(`sshd exited before readiness: ${text}`)) }) })
const read = (chunk: Buffer): void => {
text = (text + chunk.toString()).slice(-8192)
if (text.includes('Server listening on')) ready.resolve()
}
logs.stdout.on('data', read); logs.stderr.on('data', read)
logs.once('error', (error) => { ready.reject(error) })
const deadline = setTimeout(() => { ready.reject(new Error(`sshd readiness timed out: ${text}`)) }, 30_000)
try { await ready.promise }
finally { clearTimeout(deadline); logs.kill('SIGTERM'); await closed }
}
/**
* Install only a validated release into an isolated SSH server and exercise production providers.
* @param argv - archive and optional report flags.
*/
export async function verifySshTransport(argv: string[]): Promise<void> {
const { values } = parseArgs({ args: argv, options: { archive: { type: 'string' }, report: { type: 'string' } } })
if (values.archive === undefined) throw new Error('Use --archive=<Linux helper archive>')
const archive = resolve(values.archive)
assert.equal(await fileDigest(archive), (await readFile(`${archive}.sha256`, 'utf8')).split(/\s/)[0], 'Archive checksum')
const temporary = await mkdtemp(join(tmpdir(), 'dsh-helper-ssh-'))
const image = `dsh-helper-test:${randomUUID()}`
let imageBuilt = false
let container: string | undefined
try {
const installed = await extractHelper(archive, temporary)
const manifest = await verifyPayload(installed)
if (!manifest.target.includes('-linux-')) throw new Error('OpenSSH container acceptance requires a Linux artifact')
const platform = manifest.target.endsWith('-arm64') ? 'linux/arm64' : 'linux/amd64'
await docker('build', '--platform', platform, '--tag', image, join(root, 'packages/ssh/ssh-helper-runtime/tests/fixtures/sshd'))
imageBuilt = true
const identity = join(temporary, 'identity')
await run('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-f', identity])
container = await docker('run', '--detach', '--platform', platform, '--publish', '127.0.0.1::22',
'--mount', `type=bind,src=${installed},dst=/opt/dsh-helper,readonly`,
'--mount', `type=bind,src=${identity}.pub,dst=/fixture-key.pub,readonly`, image)
await sshdReady(container)
const published = await docker('port', container, '22/tcp')
const port = /^127\.0\.0\.1:(\d+)$/.exec(published)?.[1]
if (port === undefined) throw new Error(`Unexpected Docker port mapping: ${published}`)
const publicKey = await docker('exec', container, 'cat', '/etc/ssh/ssh_host_ed25519_key.pub')
const knownHosts = join(temporary, 'known_hosts')
await writeFile(knownHosts, `[127.0.0.1]:${port} ${publicKey}\n`, { mode: 0o600 })
const sshConfig = join(temporary, 'ssh_config')
await writeFile(sshConfig, `Host artifact\n HostName 127.0.0.1\n Port ${port}\n User dsh\n IdentityFile "${identity}"\n IdentitiesOnly yes\n UserKnownHostsFile "${knownHosts}"\n GlobalKnownHostsFile /dev/null\n`, { mode: 0o600 })
const bin = join(temporary, 'bin')
await mkdir(bin)
const quote = (value: string): string => `'${value.replaceAll("'", "'\\''")}'`
await writeFile(join(bin, 'ssh'), `#!/bin/sh\nexec /usr/bin/ssh -F ${quote(sshConfig)} "$@"\n`)
await chmod(join(bin, 'ssh'), 0o755)
const workspace = '/home/dsh/workspace'
const config = join(temporary, 'cordis.yml')
await writeFile(config, artifactComposition({ id: 'ssh', name: '@deepseek-ai/dsh-ssh', config: {
host: 'artifact', launch: { kind: 'executable' }, helper: '/opt/dsh-helper/dsh-ssh-helper',
helperHash: await fileDigest(join(installed, 'dsh-ssh-helper')), workspace,
} }, workspace))
const home = join(temporary, 'client-home')
await mkdir(home)
const report = join(temporary, 'report.json')
await runArtifactDriver(config, report, 'portable', {
HOME: home, PATH: `${bin}:${process.env.PATH ?? '/usr/bin:/bin'}`, DSH_HOME: join(home, 'dsh'),
TSX_TSCONFIG_PATH: join(root, 'tsconfig.base.json'), DSH_TELEMETRY_DISABLED: '1', DSH_VERIFY_NO_NODE: '1',
})
const evidence = { manifest, runtime: runtimeReportSchema.parse(JSON.parse(await readFile(report, 'utf8'))), glibc: await docker('exec', container, 'getconf', 'GNU_LIBC_VERSION') }
assert.equal(evidence.glibc, 'glibc 2.28')
if (values.report !== undefined) {
const destination = resolve(values.report)
await mkdir(dirname(destination), { recursive: true })
await writeFile(destination, `${JSON.stringify(evidence, null, 2)}\n`)
}
console.log(`SSH helper real SSH ${manifest.target}: ${JSON.stringify(evidence.runtime)}`)
} catch (error) {
if (container === undefined) throw error
const logs = await run('docker', ['logs', container], { maxBuffer: 1024 * 1024 })
throw new Error(`SSH container diagnostics:\n${logs.stdout}\n${logs.stderr}`, { cause: error })
} finally {
try { if (container !== undefined) await docker('rm', '--force', container) }
finally {
try { if (imageBuilt) await docker('image', 'rm', image) }
finally { await rm(temporary, { recursive: true, force: true }) }
}
}
}
if (import.meta.main) await verifySshTransport(process.argv.slice(2))
+70
View File
@@ -0,0 +1,70 @@
import { readFileSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import yaml from 'js-yaml'
import { expect, it } from 'vitest'
import { desktopTargetBuildPaths } from '../apps/desktop/scripts/desktop-build-paths.mjs'
const root = resolve(import.meta.dirname, '..')
const workflow = yaml.load(readFileSync(resolve(root, '.github/workflows/windows-package.yml'), 'utf8')) as {
on: Record<string, { inputs: Record<string, { required?: boolean }> }>
permissions: Record<string, string>
concurrency: { group: string; 'cancel-in-progress': boolean }
jobs: Record<string, {
'runs-on': string
steps: Array<{
id?: string
name?: string
uses?: string
run?: string
if?: string
env?: Record<string, string>
with?: Record<string, unknown>
}>
}>
}
const job = workflow.jobs.package!
it('requires explicit manual dispatch on the dedicated Windows runner', () => {
expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
expect(job['runs-on']).toBe('dsh-win-package-trial')
expect(workflow.concurrency).toEqual({ group: 'windows-desktop-package', 'cancel-in-progress': false })
expect(workflow.permissions).toEqual({ contents: 'read' })
expect(workflow.on.workflow_dispatch!.inputs.policy_origin!.required).toBe(true)
const checkout = job.steps.find(step => step.id === 'checkout')!
expect(checkout.with).toMatchObject({ clean: true, 'persist-credentials': false })
expect(job.steps.find(step => step.uses?.startsWith('pnpm/action-setup@'))?.with?.dest)
.toBe('${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }}')
})
it('passes user input through environment variables and shares the validated version', () => {
const settings = job.steps.find(step => step.id === 'settings')!
expect(settings.env).toEqual({
PACKAGE_BUILD_VERSION: '${{ inputs.build_version }}', PACKAGE_DEPLOYMENT: '${{ inputs.deployment }}',
PACKAGE_POLICY_ORIGIN: '${{ inputs.policy_origin }}', PACKAGE_LOGIN_ORIGINS: '${{ inputs.login_origins }}',
})
const commands = job.steps.filter(step => step.run?.includes('pnpm run package:desktop:win:x64:unsigned'))
expect(commands).toHaveLength(2)
expect(commands[0]!.run).toContain('--check --build-version "$env:PACKAGE_VERSION"')
expect(commands[1]!.run).toContain('unsigned --build-version "$env:PACKAGE_VERSION"')
for (const step of commands) {
expect(step.env?.PACKAGE_VERSION).toBe('${{ steps.settings.outputs.version }}')
expect(step.run).toContain('exit $LASTEXITCODE')
}
for (const step of job.steps) expect(step.run ?? '').not.toContain('${{ inputs.')
expect(job.steps.indexOf(settings)).toBeLessThan(job.steps.indexOf(commands[0]!))
})
it('retains failure diagnostics and only uploads successful installers from the owned output directory', () => {
const uploads = job.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))
const logs = uploads.find(step => step.if === "always() && steps.checkout.outcome == 'success'")!
expect(logs.with).toMatchObject({ path: 'apps/desktop/.desktop-build/packaging-runs/', 'include-hidden-files': true })
const installer = uploads.find(step => step !== logs)!
expect(installer.if).toBeUndefined()
expect(installer.with).toMatchObject({
path: `${relative(root, desktopTargetBuildPaths('win-x64').unsignedArtifacts).replaceAll('\\', '/')}/*.exe`,
'include-hidden-files': true, 'if-no-files-found': 'error',
})
const cleanup = job.steps.find(step => step.run?.includes('Remove-Item -LiteralPath apps/desktop/.env.windows'))!
expect(cleanup.if).toBe("always() && steps.checkout.outcome == 'success'")
expect(cleanup.run).toContain('Test-Path -LiteralPath apps/desktop/.env.windows')
})