Revert "build: consolidate desktop runtime into asar"

This reverts commit 97e057d4d4.
This commit is contained in:
ZP
2026-08-23 11:29:20 +08:00
parent 80e01ed123
commit 3caa88b7e7
11 changed files with 320 additions and 253 deletions
+6 -13
View File
@@ -285,20 +285,13 @@
"build": {
"appId": "ai.deepseek.dsh.desktop",
"productName": "DSH Desktop",
"asar": {
"smartUnpack": false
},
"asar": true,
"asarUnpack": [
"lib/diagnostic-export-worker.js",
"node_modules/@deepseek-ai/dsh/config/agent-presets/**",
"node_modules/@deepseek-ai/node-addon-landlock-run-linux-*/**",
"node_modules/@img/sharp-*/**",
"node_modules/@img/sharp-libvips-*/**",
"node_modules/@koromix/koffi-*/**",
"node_modules/@vscode/ripgrep-*/**",
"node_modules/node-addon-require-builtin-*/**",
"node_modules/node-pty/prebuilds/**",
"node_modules/pnpm/dist/vendor/**"
"package.json",
"cordis.patch.yml",
"build/**",
"lib/**",
"node_modules/**"
],
"afterPack": "./scripts/verify-packaged-runtime.ts",
"electronFuses": {
@@ -1,8 +1,9 @@
/** Fail-loud verification of the runtime entries sealed into Electron's app.asar. */
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { isAbsolute, join, relative, sep } from 'node:path'
import { Worker } from 'node:worker_threads'
import { listPackage } from '@electron/asar'
import AdmZip from 'adm-zip'
@@ -56,12 +57,39 @@ export const REQUIRED_PACKAGED_RUNTIME_ENTRIES = [
'node_modules/pnpm/bin/pnpm.mjs',
] as const
/** Platform-independent files consumed by an API that requires a physical path. */
/** Physical entries required because profile fallback symlinks cannot target ASAR paths. */
export const REQUIRED_UNPACKED_RUNTIME_ENTRIES = [
'package.json',
'cordis.patch.yml',
'build/app-icon.png',
'build/app-icon-mac.png',
'build/tray-iconTemplate.png',
'build/tray-icon-blue.png',
'lib/main.js',
'lib/client.js',
'lib/native-ui/profile-create.html',
'lib/native-ui/recovery.html',
'lib/index.js',
'lib/profile.js',
'lib/profile-manager.js',
'lib/profile-service.js',
'lib/pnpm.js',
'lib/profiles.js',
'lib/diagnostics.js',
'lib/diagnostic-export-worker.js',
'lib/terminal.js',
'lib/update-download.js',
'lib/updates.js',
'lib/windows-agent-presets.js',
'lib/windows-pwsh-sandbox.js',
'node_modules/@deepseek-ai/dsh/package.json',
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/agent.cordis.yml',
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md',
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md',
'node_modules/@deepseek-ai/dsh/lib/bin.js',
'node_modules/@deepseek-ai/dsh-app-boot/lib/index.js',
'node_modules/@deepseek-ai/dsh-web-frontend/dist/index.html',
'node_modules/pnpm/bin/pnpm.mjs',
] as const
/** Prebuilt Node-API modules required when the Windows package skips native source rebuilds. */
@@ -72,17 +100,6 @@ export const REQUIRED_WINDOWS_X64_NODE_PTY_ENTRIES = [
'node_modules/node-pty/prebuilds/win32-x64/conpty/conpty.dll',
] as const
/** Other native executables and libraries required by the Windows x64 package. */
export const REQUIRED_WINDOWS_X64_NATIVE_ENTRIES = [
'node_modules/@img/sharp-win32-x64/lib/libvips-42.dll',
'node_modules/@img/sharp-win32-x64/lib/libvips-cpp-8.18.3.dll',
'node_modules/@img/sharp-win32-x64/lib/sharp-win32-x64-0.35.3.node',
'node_modules/@koromix/koffi-win32-x64/win32_x64/koffi.node',
'node_modules/@vscode/ripgrep-win32-x64/bin/rg.exe',
'node_modules/node-addon-require-builtin-win32-x64-msvc/prebuilt/win32-x64-msvc-napi-v9.node',
'node_modules/pnpm/dist/vendor/fastlist-0.3.0-x64.exe',
] as const
/** Native and build-only trees that must not inflate a Windows x64 installation. */
export const FORBIDDEN_WINDOWS_X64_RUNTIME_PREFIXES = [
'node_modules/@vscode/ripgrep-win32-arm64/',
@@ -105,12 +122,35 @@ export const REQUIRED_MACOS_UNIVERSAL_ENTRIES = [
...MACOS_UNIVERSAL_NATIVE_ENTRIES.map(entry => entry.path),
] as const
/** Package exports that profile fallback links must resolve from the physical application tree. */
export const REQUIRED_UNPACKED_PACKAGE_SPECIFIERS = [
'dsh-plugin-desktop',
'dsh-plugin-desktop/profile',
'dsh-plugin-desktop/client',
'dsh-plugin-desktop/terminal',
'dsh-plugin-desktop/pnpm',
'dsh-plugin-desktop/profile-service',
'dsh-plugin-desktop/profiles',
'dsh-plugin-desktop/diagnostics',
'dsh-plugin-desktop/notifications',
'dsh-plugin-desktop/updates',
'dsh-plugin-desktop/windows-agent-presets',
'dsh-plugin-desktop/windows-pwsh-sandbox',
'dsh-plugin-desktop/package.json',
'@deepseek-ai/dsh-base/package.json',
'@deepseek-ai/schemastery/package.json',
'@deepseek-ai/dsh-web-app/package.json',
] as const
/** Injectable archive listing seam used by focused tests. */
export type ArchiveLister = (archivePath: string, options: { isPack: boolean }) => readonly string[]
/** Injectable physical-file probe used by focused tests. */
export type FileProbe = (filename: string) => boolean
/** Injectable Node package resolver used by focused tests. */
export type PackageResolver = (specifier: string) => string
/** Inputs understood by the bundled diagnostics Worker. */
export interface PackagedDiagnosticWorkerData {
readonly logsDir: string
@@ -284,72 +324,81 @@ export function verifyPackagedAsar(
}
/**
* Return whether one archive entry is allowed to exist beside app.asar.
* Parent directories are included because Electron Builder materializes them
* while unpacking a whitelisted descendant.
* Require every ASAR header entry to have a physical counterpart.
*
* The Desktop packaging contract unpacks every included application file so
* profile fallback links and Node ESM resolution never target virtual paths.
* Checking the complete header closes the gap left by a curated entry list:
* a collector regression cannot silently omit transitive packages such as
* yaml, zod, or typebox from app.asar.unpacked.
*/
export function allowedUnpackedRuntimeEntry(entry: string): boolean {
const exact = new Set<string>([
...REQUIRED_UNPACKED_RUNTIME_ENTRIES,
...REQUIRED_WINDOWS_X64_NODE_PTY_ENTRIES,
...REQUIRED_WINDOWS_X64_NATIVE_ENTRIES,
...REQUIRED_MACOS_UNIVERSAL_ENTRIES,
])
const prefixes = [
'node_modules/@deepseek-ai/dsh/config/agent-presets/',
'node_modules/@deepseek-ai/node-addon-landlock-run-linux-',
'node_modules/@img/sharp-',
'node_modules/@img/sharp-libvips-',
'node_modules/@koromix/koffi-',
'node_modules/@vscode/ripgrep-',
'node_modules/node-addon-require-builtin-',
'node_modules/node-pty/prebuilds/',
'node_modules/pnpm/dist/vendor/',
]
if (exact.has(entry) || prefixes.some(prefix => entry.startsWith(prefix))) return true
const directory = entry.length === 0 ? '' : `${entry}/`
return [...exact].some(path => path.startsWith(directory))
|| prefixes.some(prefix => prefix.startsWith(directory))
}
/** Reject ordinary JavaScript and resources leaking back into app.asar.unpacked. */
export function verifyUnpackedRuntimeScope(
export function verifyUnpackedArchiveMirror(
archiveEntries: ReadonlySet<string>,
unpackedRoot: string,
exists: FileProbe = existsSync,
): void {
const unexpected = [...archiveEntries].filter(entry =>
entry.length > 0
&& exists(join(unpackedRoot, entry))
&& !allowedUnpackedRuntimeEntry(entry),
)
if (unexpected.length > 0) {
const missing = [...archiveEntries]
.filter(entry => entry.length > 0 && !exists(join(unpackedRoot, entry)))
if (missing.length > 0) {
throw new Error(
`dsh-plugin-desktop: unpacked runtime at ${unpackedRoot} contains non-whitelisted physical entries: ${unexpected.join(', ')}`,
`dsh-plugin-desktop: unpacked runtime at ${unpackedRoot} is missing ASAR-declared physical entries: ${missing.join(', ')}`,
)
}
}
/**
* Verify package exports resolve through the physical tree instead of the build workspace.
* @param unpackedRoot - absolute path to app.asar.unpacked.
* @param resolvePackage - package resolver anchored at the physical root manifest.
* @returns Nothing; failure rejects missing exports and paths outside app.asar.unpacked.
*/
export function verifyUnpackedPackageResolution(
unpackedRoot: string,
resolvePackage: PackageResolver = createRequire(join(unpackedRoot, 'package.json')).resolve,
): void {
for (const specifier of REQUIRED_UNPACKED_PACKAGE_SPECIFIERS) {
let resolvedPath: string
try {
resolvedPath = resolvePackage(specifier)
} catch (cause) {
throw new Error(
`dsh-plugin-desktop: packaged runtime at ${unpackedRoot} cannot resolve required package export ${specifier}`,
{ cause },
)
}
const relativePath = relative(unpackedRoot, resolvedPath)
if (
!isAbsolute(resolvedPath)
|| relativePath === '..'
|| relativePath.startsWith(`..${sep}`)
|| isAbsolute(relativePath)
) {
throw new Error(
`dsh-plugin-desktop: required package export ${specifier} resolved outside ${unpackedRoot}: ${resolvedPath}`,
)
}
}
}
/**
* Verify Electron Builder's completed application before signing begins.
* @param context - Electron Builder's afterPack context.
* @param list - ASAR listing implementation.
* @param exists - physical-file probe for the unpacked CLI dependency tree.
* @param resolvePackage - package resolver anchored at the physical root manifest.
* @returns Nothing; failure rejects the package before signing.
*/
export function verifyPackagedRuntime(
context: PackagedRuntimeContext,
list: ArchiveLister = listPackage,
exists: FileProbe = existsSync,
resolvePackage?: PackageResolver,
): void {
const archiveEntries = verifyPackagedAsar(resolvePackagedAsarPath(context), list)
const unpackedRoot = resolvePackagedUnpackedRoot(context)
const requiredPhysicalEntries = context.electronPlatformName === 'win32'
? [
...REQUIRED_UNPACKED_RUNTIME_ENTRIES,
...REQUIRED_WINDOWS_X64_NODE_PTY_ENTRIES,
...REQUIRED_WINDOWS_X64_NATIVE_ENTRIES,
]
? [...REQUIRED_UNPACKED_RUNTIME_ENTRIES, ...REQUIRED_WINDOWS_X64_NODE_PTY_ENTRIES]
: context.electronPlatformName === 'darwin' && context.arch === 4
? [...REQUIRED_UNPACKED_RUNTIME_ENTRIES, ...REQUIRED_MACOS_UNIVERSAL_ENTRIES]
: REQUIRED_UNPACKED_RUNTIME_ENTRIES
@@ -378,7 +427,8 @@ export function verifyPackagedRuntime(
)
}
}
verifyUnpackedRuntimeScope(archiveEntries, unpackedRoot, exists)
verifyUnpackedArchiveMirror(archiveEntries, unpackedRoot, exists)
verifyUnpackedPackageResolution(unpackedRoot, resolvePackage)
}
/**
+10 -20
View File
@@ -1,7 +1,8 @@
/** Profile-relative package resolution for Electron's restricted Node runtime. */
import Module, { registerHooks } from 'node:module'
import { fileURLToPath } from 'node:url'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { unpackedAsarPath } from './packaged-runtime-path.ts'
import {
findOverlayPackage,
packageNameFromSpecifier,
@@ -9,8 +10,12 @@ import {
} from './package-overlay.ts'
const LOADER_ENTRY_URL = import.meta.resolve('@deepseek-ai/cordis-plugin-loader')
const DESKTOP_ENTRY_URL = new URL('../lib/index.js', import.meta.url).href
const DESKTOP_PACKAGE_URL = new URL('../package.json', import.meta.url).href
const DESKTOP_ENTRY_URL = pathToFileURL(
unpackedAsarPath(fileURLToPath(new URL('../lib/index.js', import.meta.url))),
).href
const DESKTOP_PACKAGE_URL = pathToFileURL(
unpackedAsarPath(fileURLToPath(new URL('../package.json', import.meta.url))),
).href
interface CommonJsModuleResolver {
_resolveFilename(
@@ -33,10 +38,6 @@ function isBareSpecifier(specifier: string): boolean {
return !specifier.startsWith('.') && !specifier.startsWith('/') && !URL.canParse(specifier)
}
function overlayParentUrl(source: 'install' | 'profile', profileBaseUrl: string): string {
return source === 'profile' ? profileBaseUrl : DESKTOP_ENTRY_URL
}
/**
* Resolve Cordis Loader bare imports from the selected persistent profile.
* @param profileBaseUrl - file URL inside the profile that owns plugin dependencies.
@@ -88,7 +89,7 @@ export function installProfilePackageResolver(profileBaseUrl: string): () => voi
})
const resolved = nextResolve(specifier, {
...context,
parentURL: overlayParentUrl(overlay.selected.source, profileBaseUrl),
parentURL: overlay.selected.source === 'profile' ? profileBaseUrl : DESKTOP_ENTRY_URL,
})
overlayModuleUrls.add(resolved.url)
return resolved
@@ -107,18 +108,7 @@ export function installProfilePackageResolver(profileBaseUrl: string): () => voi
return resolved
} catch (cause) {
if ((cause as NodeJS.ErrnoException).code !== 'ERR_MODULE_NOT_FOUND') throw cause
const packageName = packageNameFromSpecifier(specifier)
const overlay = packageName === undefined
? undefined
: findOverlayPackage(packageName, {
installPackageUrl: DESKTOP_PACKAGE_URL,
profilePackageUrl: profileBaseUrl,
})
if (overlay === undefined) throw cause
const resolved = nextResolve(specifier, {
...context,
parentURL: overlayParentUrl(overlay.selected.source, profileBaseUrl),
})
const resolved = nextResolve(specifier, { ...context, parentURL: profileBaseUrl })
overlayModuleUrls.add(resolved.url)
return resolved
}
@@ -1,4 +1,4 @@
/** Runtime-path helpers for Electron ASAR dependencies and explicitly unpacked assets. */
/** Physical-path helpers for dependencies Electron Builder removes from app.asar. */
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
@@ -46,7 +46,7 @@ export function unpackedAsarPath(filename: string): string {
* Resolve one dependency entry from a built desktop module.
* @param moduleUrl - URL of a module emitted below the package's `lib` directory.
* @param dependencyEntry - package subpath resolved through Node module lookup.
* @returns a logical path suitable for Electron's RunAsNode module loader.
* @returns a physical path suitable for Node execution and profile symlinks.
*/
export function packagedDependencyPath(moduleUrl: string, dependencyEntry: string): string {
const segments = dependencyEntry.split('/')
@@ -63,5 +63,5 @@ export function packagedDependencyPath(moduleUrl: string, dependencyEntry: strin
resolvePackageRoot(moduleUrl, packageName),
...segments.slice(packageSegments),
)
return logicalPath
return unpackedAsarPath(logicalPath)
}
+3 -1
View File
@@ -8,6 +8,7 @@ import { evaluate, isJsExpr, type EntryOptions } from '@deepseek-ai/cordis-plugi
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import {
composeEntries,
healProfilesModuleFallback,
initProfile,
loadOptionalPatches,
loadOverlayPatches,
@@ -54,7 +55,7 @@ const BIN_NAME = DESKTOP_PACKAGE_NAME
const REQUIRED_BUNDLES = requiredWebBundles()
const REQUIRED_BUNDLE_SET = new Set(REQUIRED_BUNDLES)
const OBSOLETE_DESKTOP_BUNDLE_SET = new Set(['@deepseek-ai/dsh-desktop-app'])
const INSTALL_ANCHOR = fileURLToPath(new URL('../package.json', import.meta.url))
const INSTALL_ANCHOR = unpackedAsarPath(fileURLToPath(new URL('../package.json', import.meta.url)))
const DESKTOP_PATCH_PATH = fileURLToPath(new URL('../cordis.patch.yml', import.meta.url))
const DIRECTORY_PICKER_ROW_ID = 'directory-picker'
const AUTO_PICKER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
@@ -582,6 +583,7 @@ export function prepareDesktopProfile(
const profileDir = profileName === DESKTOP_PROFILE_NAME
? ensureDesktopProfile(home)
: resolveProfileDir(profileName, home)
healProfilesModuleFallback(INSTALL_ANCHOR, home)
// `plugin-management` is the community market's user-facing scope. Startup
// recovery has its own state file so switching to another provider cannot
// reapply a stale community-market disable, while a recovery disable always
+3 -3
View File
@@ -182,7 +182,7 @@ describe('packaged dsh bootstrap', () => {
}
})
it('maps only explicitly physical assets to the unpacked Electron tree', () => {
it('uses the physical unpacked dependency tree only inside an Electron package', () => {
expect(unpackedAsarPath('/Applications/DSH Desktop.app/Contents/Resources/app.asar/node_modules/pkg'))
.toBe('/Applications/DSH Desktop.app/Contents/Resources/app.asar.unpacked/node_modules/pkg')
expect(unpackedAsarPath('C:\\Program Files\\DSH Desktop\\resources\\app.asar\\node_modules\\pkg'))
@@ -194,7 +194,7 @@ describe('packaged dsh bootstrap', () => {
.toThrow('relative POSIX path')
})
it('keeps a resolved JavaScript dependency inside ASAR for RunAsNode', () => {
it('maps a resolved ASAR dependency to its physical unpacked path', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-desktop-asar-profile-'))
const desktopLib = join(root, 'app.asar', 'lib')
const dshPackage = join(root, 'app.asar', 'node_modules', '@deepseek-ai', 'dsh')
@@ -210,7 +210,7 @@ describe('packaged dsh bootstrap', () => {
const moduleUrl = pathToFileURL(join(desktopLib, 'desktop-cli.js')).href
expect(packagedDependencyPath(moduleUrl, '@deepseek-ai/dsh/lib/bin.js')).toBe(join(
realpathSync(root),
'app.asar',
'app.asar.unpacked',
'node_modules',
'@deepseek-ai',
'dsh',
@@ -154,27 +154,6 @@ describe('installProfilePackageResolver', () => {
expect(harness.resolve?.('profile-peer', { parentURL: desktopPluginUrl }, nextResolve)).toEqual({ url: profilePeerUrl })
})
it('allows a Profile-selected package to use a missing dependency from the Desktop archive', () => {
const profileBaseUrl = 'file:///C:/Users/test/profile/package.json'
const profilePluginUrl = 'file:///C:/Users/test/profile/node_modules/plugin/index.js'
const desktopPeerUrl = 'file:///Applications/DSH.app/Contents/Resources/app.asar/node_modules/desktop-peer/index.js'
harness.sources.set('plugin', 'profile')
harness.sources.set('desktop-peer', 'install')
installProfilePackageResolver(profileBaseUrl)
const nextResolve = vi.fn((specifier: string, context: { parentURL?: string }) => {
if (specifier === 'plugin' && context.parentURL === profileBaseUrl) return { url: profilePluginUrl }
if (specifier === 'desktop-peer' && context.parentURL?.endsWith('/lib/index.js')) {
return { url: desktopPeerUrl }
}
throw missing(specifier, context.parentURL)
})
const loaderEntryUrl = import.meta.resolve('@deepseek-ai/cordis-plugin-loader')
expect(harness.resolve?.('plugin', { parentURL: loaderEntryUrl }, nextResolve)).toEqual({ url: profilePluginUrl })
expect(harness.resolve?.('desktop-peer', { parentURL: profilePluginUrl }, nextResolve))
.toEqual({ url: desktopPeerUrl })
})
it('does not expose Profile dependencies to unrelated modules', () => {
const profileBaseUrl = 'file:///C:/Users/test/profile/package.json'
installProfilePackageResolver(profileBaseUrl)
@@ -210,7 +189,7 @@ describe('installProfilePackageResolver', () => {
)).toBe('ordinary:@deepseek-ai/dsh-client-modules/package.json')
expect(resolveFilename(
'@deepseek-ai/dsh-client-modules/client.js',
{ filename: profileManifestPath },
{ filename: '/tmp/dsh-profile/package.json' },
false,
)).toBe('ordinary:@deepseek-ai/dsh-client-modules/client.js')
+5 -13
View File
@@ -29,7 +29,6 @@ const manifest = JSON.parse(readFileSync(new URL('package.json', packageRoot), '
build?: {
productName?: unknown
appId?: unknown
asar?: unknown
asarUnpack?: unknown
afterPack?: unknown
electronFuses?: unknown
@@ -373,7 +372,6 @@ describe('published package surface', () => {
expect(config).toContain("diagnostics: 'src/diagnostics.ts'")
expect(config).toContain("notifications: 'src/notifications.ts'")
expect(config).toContain("'diagnostic-export-worker': 'src/diagnostic-export-worker.ts'")
expect(config).toContain('codeSplitting: false')
expect(config).toContain("entry: { preload: 'src/preload.ts' }")
expect(config).toContain("entryFileNames: 'preload.cjs'")
expect(config).toContain("terminal: 'src/terminal.ts'")
@@ -569,18 +567,12 @@ describe('published package surface', () => {
expect(manifest.version).toBe(workspaceManifest.version)
expect(manifest.build?.productName).toBe('DSH Desktop')
expect(manifest.build?.appId).toBe('ai.deepseek.dsh.desktop')
expect(manifest.build?.asar).toEqual({ smartUnpack: false })
expect(manifest.build?.asarUnpack).toEqual([
'lib/diagnostic-export-worker.js',
'node_modules/@deepseek-ai/dsh/config/agent-presets/**',
'node_modules/@deepseek-ai/node-addon-landlock-run-linux-*/**',
'node_modules/@img/sharp-*/**',
'node_modules/@img/sharp-libvips-*/**',
'node_modules/@koromix/koffi-*/**',
'node_modules/@vscode/ripgrep-*/**',
'node_modules/node-addon-require-builtin-*/**',
'node_modules/node-pty/prebuilds/**',
'node_modules/pnpm/dist/vendor/**',
'package.json',
'cordis.patch.yml',
'build/**',
'lib/**',
'node_modules/**',
])
expect(manifest.build?.electronFuses).toEqual({ runAsNode: true })
expect(manifest.build?.toolsets).toEqual({ nsis: '1.2.1' })
+1 -9
View File
@@ -1,4 +1,4 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
@@ -149,14 +149,6 @@ describe('desktop profile composition', {
expect(repaired.custom.preserved).toBe(true)
})
it('does not materialize the installation dependency closure in the profiles directory', () => {
const home = temporaryHome()
prepareDesktopProfile(undefined, home, 'win32')
expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false)
})
it('migrates the obsolete Desktop bundle before loading a historical profile', () => {
const home = temporaryHome()
const dir = ensureDesktopProfile(home)
@@ -4,22 +4,22 @@ import { describe, expect, it, vi } from 'vitest'
import AdmZip from 'adm-zip'
import {
afterPack,
allowedUnpackedRuntimeEntry,
FORBIDDEN_WINDOWS_X64_RUNTIME_PREFIXES,
REQUIRED_MACOS_UNIVERSAL_ENTRIES,
REQUIRED_PACKAGED_RUNTIME_ENTRIES,
REQUIRED_MACOS_UNIVERSAL_ENTRIES,
REQUIRED_UNPACKED_PACKAGE_SPECIFIERS,
REQUIRED_UNPACKED_RUNTIME_ENTRIES,
REQUIRED_WINDOWS_X64_NATIVE_ENTRIES,
REQUIRED_WINDOWS_X64_NODE_PTY_ENTRIES,
resolvePackagedAsarPath,
resolvePackagedUnpackedRoot,
smokePackagedDiagnosticWorker,
verifyUnpackedArchiveMirror,
verifyPackagedRuntime,
verifyUnpackedRuntimeScope,
type ArchiveLister,
type FileProbe,
type PackagedDiagnosticWorkerLauncher,
type PackageResolver,
type PackagedRuntimeContext,
type PackagedDiagnosticWorkerLauncher,
} from '../scripts/verify-packaged-runtime.ts'
import { FORBIDDEN_MACOS_UNIVERSAL_ENTRIES } from '../scripts/mac-universal.ts'
@@ -40,33 +40,8 @@ function completeArchiveEntries(separator = '/'): string[] {
return REQUIRED_PACKAGED_RUNTIME_ENTRIES.map(entry => `${separator}${entry.replaceAll('/', separator)}`)
}
function requiredPhysicalEntries(runtimeContext: PackagedRuntimeContext): readonly string[] {
if (runtimeContext.electronPlatformName === 'win32') {
return [
...REQUIRED_UNPACKED_RUNTIME_ENTRIES,
...REQUIRED_WINDOWS_X64_NODE_PTY_ENTRIES,
...REQUIRED_WINDOWS_X64_NATIVE_ENTRIES,
]
}
if (runtimeContext.electronPlatformName === 'darwin' && runtimeContext.arch === 4) {
return [
...REQUIRED_UNPACKED_RUNTIME_ENTRIES,
...REQUIRED_MACOS_UNIVERSAL_ENTRIES,
]
}
return REQUIRED_UNPACKED_RUNTIME_ENTRIES
}
function physicalProbe(
runtimeContext: PackagedRuntimeContext,
options: { readonly missing?: string, readonly extra?: string } = {},
): FileProbe {
const unpackedRoot = resolvePackagedUnpackedRoot(runtimeContext)
const present = new Set(requiredPhysicalEntries(runtimeContext)
.filter(entry => entry !== options.missing)
.map(entry => join(unpackedRoot, entry)))
if (options.extra !== undefined) present.add(join(unpackedRoot, options.extra))
return filename => present.has(filename)
function completePackageResolver(unpackedRoot: string): PackageResolver {
return specifier => join(unpackedRoot, 'resolved', `${specifier.replaceAll('/', '-')}.js`)
}
describe('packaged desktop runtime verification', () => {
@@ -96,7 +71,10 @@ describe('packaged desktop runtime verification', () => {
expect(workerPath).toBe(join(unpackedRoot, 'lib', 'diagnostic-export-worker.js'))
expect(readFileSync(join(workerData.logsDir, 'dsh-2000-01-01.log'), 'utf8'))
.toBe('packaged worker smoke\n')
expect(workerData.appVersion).toBe('packaged-smoke')
expect(workerData.maxEvidenceBytes).toBe(1024)
const crashDump = readFileSync(join(workerData.crashDumpsDir, 'pending', 'packaged-smoke.dmp'))
expect(crashDump.toString('utf8')).toBe('packaged crash dump smoke\n')
const outDir = join(workerData.userDataDir, 'diagnostics')
mkdirSync(outDir)
const output = join(outDir, 'diagnostics-smoke.zip')
@@ -127,6 +105,42 @@ describe('packaged desktop runtime verification', () => {
expect(calls).toEqual(['static', resolvePackagedUnpackedRoot(runtimeContext)])
})
it('tracks the ConPTY-only native surface shipped by node-pty 1.2', () => {
expect(REQUIRED_WINDOWS_X64_NODE_PTY_ENTRIES).toEqual([
'node_modules/node-pty/prebuilds/win32-x64/conpty.node',
'node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node',
'node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe',
'node_modules/node-pty/prebuilds/win32-x64/conpty/conpty.dll',
])
})
it.each([
'lib/main.js.map',
'node_modules/example/index.js.map',
'node_modules/example/source.ts',
'node_modules/example/source.mts',
'node_modules/example/source.cts',
])('rejects excluded development source %s', (forbidden) => {
expect(() => verifyPackagedRuntime(
context('/build', 'win32'),
() => [...completeArchiveEntries(), `/${forbidden}`],
() => true,
)).toThrow(`contains excluded development sources: ${forbidden}`)
})
it.each(FORBIDDEN_WINDOWS_X64_RUNTIME_PREFIXES)(
'rejects forbidden Windows x64 runtime prefix %s',
(prefix) => {
const forbidden = `${prefix}payload.bin`
expect(() => verifyPackagedRuntime(
context('/build', 'win32'),
() => [...completeArchiveEntries(), `/${forbidden}`],
() => true,
)).toThrow(`contains non-x64 or build-only entries: ${forbidden}`)
},
)
it.each([
[
'darwin',
@@ -137,14 +151,25 @@ describe('packaged desktop runtime verification', () => {
join('/build', 'resources', 'app.asar'),
],
])('inspects the %s app.asar path', (platform, expectedPath) => {
const runtimeContext = context('/build', platform)
const list = vi.fn<ArchiveLister>(() => completeArchiveEntries(platform === 'win32' ? '\\' : '/'))
verifyPackagedRuntime(runtimeContext, list, physicalProbe(runtimeContext))
const exists = vi.fn<FileProbe>(() => true)
const unpackedRoot = `${expectedPath}.unpacked`
const resolvePackage = vi.fn<PackageResolver>(completePackageResolver(unpackedRoot))
expect(resolvePackagedAsarPath(runtimeContext)).toBe(expectedPath)
verifyPackagedRuntime(context('/build', platform), list, exists, resolvePackage)
expect(resolvePackagedAsarPath(context('/build', platform))).toBe(expectedPath)
expect(list).toHaveBeenCalledOnce()
expect(list).toHaveBeenCalledWith(expectedPath, { isPack: false })
expect(resolvePackagedUnpackedRoot(runtimeContext)).toBe(`${expectedPath}.unpacked`)
expect(resolvePackagedUnpackedRoot(context('/build', platform))).toBe(unpackedRoot)
expect(exists).toHaveBeenCalledTimes(
REQUIRED_UNPACKED_RUNTIME_ENTRIES.length
+ (platform === 'win32' ? REQUIRED_WINDOWS_X64_NODE_PTY_ENTRIES.length : 0)
+ completeArchiveEntries().length,
)
expect(resolvePackage.mock.calls.map(([specifier]) => specifier))
.toEqual(REQUIRED_UNPACKED_PACKAGE_SPECIFIERS)
})
it('rejects an unsupported platform instead of guessing an archive layout', () => {
@@ -152,80 +177,57 @@ describe('packaged desktop runtime verification', () => {
.toThrow('unsupported Electron afterPack platform "mas"')
})
it.each([
'lib/main.js.map',
'node_modules/example/index.js.map',
'node_modules/example/source.ts',
'node_modules/example/source.mts',
'node_modules/example/source.cts',
])('rejects excluded development source %s', (forbidden) => {
const runtimeContext = context('/build', 'win32')
expect(() => verifyPackagedRuntime(
runtimeContext,
() => [...completeArchiveEntries(), `/${forbidden}`],
physicalProbe(runtimeContext),
)).toThrow(`contains excluded development sources: ${forbidden}`)
})
it.each(FORBIDDEN_WINDOWS_X64_RUNTIME_PREFIXES)(
'rejects forbidden Windows x64 runtime prefix %s',
(prefix) => {
const forbidden = `${prefix}payload.bin`
const runtimeContext = context('/build', 'win32')
expect(() => verifyPackagedRuntime(
runtimeContext,
() => [...completeArchiveEntries(), `/${forbidden}`],
physicalProbe(runtimeContext),
)).toThrow(`contains non-x64 or build-only entries: ${forbidden}`)
},
)
it('allows only native, executable, preset, and Worker paths outside ASAR', () => {
expect(allowedUnpackedRuntimeEntry('lib/diagnostic-export-worker.js')).toBe(true)
expect(allowedUnpackedRuntimeEntry('node_modules/@deepseek-ai/node-addon-landlock-run-linux-x64/native.node')).toBe(true)
expect(allowedUnpackedRuntimeEntry('node_modules/@vscode/ripgrep-win32-x64/bin/rg.exe')).toBe(true)
expect(allowedUnpackedRuntimeEntry('node_modules')).toBe(true)
expect(allowedUnpackedRuntimeEntry('lib/main.js')).toBe(false)
expect(allowedUnpackedRuntimeEntry('node_modules/yaml/dist/index.js')).toBe(false)
})
it('rejects ordinary JavaScript leaking into app.asar.unpacked', () => {
const unpackedRoot = join('/build', 'resources', 'app.asar.unpacked')
const leaked = 'node_modules/yaml/dist/index.js'
expect(() => verifyUnpackedRuntimeScope(
new Set(['lib/main.js', leaked]),
unpackedRoot,
filename => filename === join(unpackedRoot, leaked),
)).toThrow(`contains non-whitelisted physical entries: ${leaked}`)
})
it('requires both CPU variants from a universal macOS runtime', () => {
const runtimeContext = context('/build', 'darwin', 4)
const unpackedRoot = resolvePackagedUnpackedRoot(runtimeContext)
const missing = 'node_modules/@vscode/ripgrep-darwin-x64/bin/rg'
expect(() => verifyPackagedRuntime(
runtimeContext,
() => completeArchiveEntries(),
physicalProbe(runtimeContext, { missing }),
filename => filename !== join(unpackedRoot, missing),
completePackageResolver(unpackedRoot),
)).toThrow(`missing required physical entries: ${missing}`)
const exists = vi.fn<FileProbe>(filename => !FORBIDDEN_MACOS_UNIVERSAL_ENTRIES
.some(entry => filename === join(unpackedRoot, entry)))
verifyPackagedRuntime(
runtimeContext,
() => completeArchiveEntries(),
physicalProbe(runtimeContext),
exists,
completePackageResolver(unpackedRoot),
)
expect(exists).toHaveBeenCalledTimes(
REQUIRED_UNPACKED_RUNTIME_ENTRIES.length
+ REQUIRED_MACOS_UNIVERSAL_ENTRIES.length
+ FORBIDDEN_MACOS_UNIVERSAL_ENTRIES.length
+ completeArchiveEntries().length,
)
})
it('rejects any ASAR-declared unpacked dependency missing from the physical tree', () => {
const unpackedRoot = join('/build', 'resources', 'app.asar.unpacked')
const missing = 'node_modules/yaml/dist/index.js'
expect(() => verifyUnpackedArchiveMirror(
new Set(['lib/main.js', missing, 'node_modules/zod/index.js']),
unpackedRoot,
filename => filename !== join(unpackedRoot, missing),
)).toThrow(`missing ASAR-declared physical entries: ${missing}`)
})
it('rejects a host-architecture node-pty build from a universal app', () => {
const runtimeContext = context('/build', 'darwin', 4)
const unpackedRoot = resolvePackagedUnpackedRoot(runtimeContext)
const forbidden = FORBIDDEN_MACOS_UNIVERSAL_ENTRIES[0]
expect(() => verifyPackagedRuntime(
runtimeContext,
() => completeArchiveEntries(),
physicalProbe(runtimeContext, { extra: forbidden }),
filename => filename === join(unpackedRoot, forbidden)
|| !FORBIDDEN_MACOS_UNIVERSAL_ENTRIES
.some(entry => filename === join(unpackedRoot, entry)),
completePackageResolver(unpackedRoot),
)).toThrow(`contains host-architecture build output: ${forbidden}`)
})
@@ -239,29 +241,111 @@ describe('packaged desktop runtime verification', () => {
'lib/update-download.js',
'lib/windows-agent-presets.js',
])('fails loud when required runtime entry %s is absent', (missing) => {
const runtimeContext = context('/build', 'win32')
const entries = completeArchiveEntries().filter(entry => entry !== `/${missing}`)
expect(() => verifyPackagedRuntime(runtimeContext, () => entries, physicalProbe(runtimeContext)))
expect(() => verifyPackagedRuntime(context('/build', 'win32'), () => entries, () => true))
.toThrow(`missing required ASAR entries: ${missing}`)
})
it.each([
'package.json',
'build/app-icon-mac.png',
'build/tray-iconTemplate.png',
'lib/terminal.js',
'lib/diagnostics.js',
'lib/diagnostic-export-worker.js',
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/agent.cordis.yml',
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md',
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md',
'node_modules/@img/sharp-win32-x64/lib/sharp-win32-x64-0.35.3.node',
'node_modules/@vscode/ripgrep-win32-x64/bin/rg.exe',
'lib/update-download.js',
'lib/windows-agent-presets.js',
'node_modules/@deepseek-ai/dsh/lib/bin.js',
'node_modules/pnpm/bin/pnpm.mjs',
'node_modules/node-pty/prebuilds/win32-x64/conpty.node',
'node_modules/pnpm/dist/vendor/fastlist-0.3.0-x64.exe',
])('fails loud when required physical runtime entry %s is absent', (missing) => {
])('fails loud when physical runtime entry %s is absent from app.asar.unpacked', (missing) => {
const runtimeContext = context('/build', 'win32')
const unpackedRoot = resolvePackagedUnpackedRoot(runtimeContext)
const missingPath = join(unpackedRoot, missing)
expect(() => verifyPackagedRuntime(
runtimeContext,
() => completeArchiveEntries(),
physicalProbe(runtimeContext, { missing }),
filename => filename !== missingPath,
completePackageResolver(unpackedRoot),
)).toThrow(`missing required physical entries: ${missing}`)
})
it('requires the physical Cordis preset and its bundled skills', () => {
const runtimeContext = context('/build', 'win32')
const unpackedRoot = resolvePackagedUnpackedRoot(runtimeContext)
const requiredPresetEntries = [
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/agent.cordis.yml',
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md',
'node_modules/@deepseek-ai/dsh/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md',
]
for (const missing of requiredPresetEntries) {
expect(() => verifyPackagedRuntime(
runtimeContext,
() => completeArchiveEntries(),
filename => filename !== join(unpackedRoot, missing),
completePackageResolver(unpackedRoot),
)).toThrow(`missing required physical entries: ${missing}`)
}
})
it('fails loud when a required package export cannot resolve from app.asar.unpacked', () => {
const runtimeContext = context('/build', 'win32')
const unpackedRoot = resolvePackagedUnpackedRoot(runtimeContext)
const resolvePackage = vi.fn<PackageResolver>((specifier) => {
if (specifier === 'dsh-plugin-desktop/profiles') {
throw new Error('missing export')
}
return completePackageResolver(unpackedRoot)(specifier)
})
expect(() => verifyPackagedRuntime(
runtimeContext,
() => completeArchiveEntries(),
() => true,
resolvePackage,
)).toThrow(
`packaged runtime at ${unpackedRoot} cannot resolve required package export dsh-plugin-desktop/profiles`,
)
})
it('fails loud when schemastery is absent from app.asar.unpacked', () => {
const runtimeContext = context('/build', 'win32')
const unpackedRoot = resolvePackagedUnpackedRoot(runtimeContext)
const specifier = '@deepseek-ai/schemastery/package.json'
const resolvePackage = vi.fn<PackageResolver>((requested) => {
if (requested === specifier) throw new Error('missing package')
return completePackageResolver(unpackedRoot)(requested)
})
expect(() => verifyPackagedRuntime(
runtimeContext,
() => completeArchiveEntries(),
() => true,
resolvePackage,
)).toThrow(
`packaged runtime at ${unpackedRoot} cannot resolve required package export ${specifier}`,
)
})
it('fails loud when a required package export escapes app.asar.unpacked', () => {
const runtimeContext = context('/build', 'win32')
const unpackedRoot = resolvePackagedUnpackedRoot(runtimeContext)
const escapedPath = join('/workspace', 'node_modules', '@deepseek-ai', 'dsh-base', 'lib', 'index.js')
const resolvePackage = vi.fn<PackageResolver>((specifier) => {
if (specifier === '@deepseek-ai/dsh-base/package.json') return escapedPath
return completePackageResolver(unpackedRoot)(specifier)
})
expect(() => verifyPackagedRuntime(
runtimeContext,
() => completeArchiveEntries(),
() => true,
resolvePackage,
)).toThrow(
`required package export @deepseek-ai/dsh-base/package.json resolved outside ${unpackedRoot}: ${escapedPath}`,
)
})
})
+1 -16
View File
@@ -17,6 +17,7 @@ export default defineConfig([
profiles: 'src/profiles.ts',
diagnostics: 'src/diagnostics.ts',
notifications: 'src/notifications.ts',
'diagnostic-export-worker': 'src/diagnostic-export-worker.ts',
runtime: 'src/runtime.ts',
'electron-runtime': 'src/electron-runtime.ts',
'desktop-runtime-environment': 'src/desktop-runtime-environment.ts',
@@ -40,22 +41,6 @@ export default defineConfig([
clean: false,
sourcemap: true,
},
{
name: `${PACKAGE_NAME}/diagnostic-export-worker`,
entry: { 'diagnostic-export-worker': 'src/diagnostic-export-worker.ts' },
outDir: 'lib',
format: 'esm',
platform: 'node',
target: 'es2024',
fixedExtension: false,
dts: false,
clean: false,
sourcemap: true,
outputOptions: {
entryFileNames: 'diagnostic-export-worker.js',
codeSplitting: false,
},
},
{
name: `${PACKAGE_NAME}/bin`,
entry: { bin: 'src/bin.ts' },