From b6b08beb0d1d81e2a9c43db2a774a1ca5747abd8 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:24:50 +0800 Subject: [PATCH] test(sandbox): derive packed workspace closure The Landlock packed-install rehearsal packed a hand-maintained list of workspace tarballs. When dsh-llm gained the dsh-util-crypto runtime dependency, the list stayed stale and npm tried to fetch the unpublished release candidate from the public registry, failing both Linux master jobs with E404 before confinement ran. Read the current pnpm workspace inventory and traverse dependencies, optionalDependencies, and required peerDependencies from the packed test roots. Verify package identities, fail loudly on unresolved workspace names, sort the closure deterministically, and leave native-family packages to the existing mode-preserving native packer. Cover runtime traversal, optional-peer exclusion, native filtering, and invalid workspace metadata. Remove the obsolete vendoring exact edit for the deleted manual list so future runtime workspace additions are included by their manifests instead of becoming post-merge CI failures. --- ...6-in-repository-landlock-release.i18n.yaml | 4 +- ...26-08-06-in-repository-landlock-release.md | 2 +- ...08-06-in-repository-landlock-release.zh.md | 2 +- .../sandbox-local/tests/packed-install.e2e.ts | 40 +++---- .../tests/packed-workspace-closure.spec.ts | 37 +++++++ .../tests/packed-workspace-closure.ts | 101 ++++++++++++++++++ scripts/rescope-vendor.ts | 18 ---- 7 files changed, 154 insertions(+), 50 deletions(-) create mode 100644 packages/sandbox/sandbox-local/tests/packed-workspace-closure.spec.ts create mode 100644 packages/sandbox/sandbox-local/tests/packed-workspace-closure.ts diff --git a/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.i18n.yaml b/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.i18n.yaml index 56c6d31682..f6b59921ba 100644 --- a/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.md -2026-08-06-in-repository-landlock-release.md: 82b21cc0c30338ad11583797f011794b8dbcc90c -2026-08-06-in-repository-landlock-release.zh.md: 554967fbce454fc9a45b54d735f485006f9dee51 +2026-08-06-in-repository-landlock-release.md: 25c31c3cdcc57cbcc8bd09b82ca24898ebca8268 +2026-08-06-in-repository-landlock-release.zh.md: 71cd2b7fe342003bc458e98ee3d2e25496b535bb diff --git a/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.md b/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.md index 82b21cc0c3..25c31c3cdc 100644 --- a/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.md +++ b/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.md @@ -22,7 +22,7 @@ The public npm boundary is three organization-owned packages with one launcher-f The main repository owns both native CI and publication. `Landlock Run` runs for relevant pull requests and `master` pushes and builds each platform on its matching native runner. The manually dispatched `Landlock Run Release` workflow builds both platform binaries, transfers them as workflow artifacts, assembles and verifies the complete package family, packs immutable npm tarballs, installs and exercises those tarballs, and only then permits the protected publish job. Platform tarballs publish before the entry tarball that optionally depends on them. Publication uses `landlock-run-vX.Y.Z` tags so launcher releases cannot collide with other release families in the monorepo; prereleases use the npm `next` dist-tag. -The sandbox packed-install rehearsal no longer permits the npm registry to supply the launcher. It packs the current checkout's entry and matching native package alongside the harness dependency closure, installs those local tarballs into an external plain-Node consumer, and proves that the installed launcher is executable, byte-identical to the native build, and the correct ELF architecture before testing confinement or fail-closed behavior. +The sandbox packed-install rehearsal does not permit the npm registry to supply the launcher. It derives the harness closure transitively from current workspace `dependencies`, `optionalDependencies`, and required `peerDependencies`; the native family stays separate because its mode-preserving pack script supplies the entry and matching platform package. The rehearsal installs those local tarballs into an external plain-Node consumer and proves that the installed launcher is executable, byte-identical to the native build, and the correct ELF architecture before testing confinement or fail-closed behavior. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.zh.md b/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.zh.md index 554967fbce..71cd2b7fe3 100644 --- a/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.zh.md +++ b/.agents/notes/implemented/process/2026-08-06-in-repository-landlock-release.zh.md @@ -22,7 +22,7 @@ Status: implemented 主仓库同时负责原生 CI 和发布。`Landlock Run` 会为相关 PR 和 `master` 推送运行,并在各自匹配的原生 runner 上构建每个平台包。手动触发的 `Landlock Run Release` 工作流会构建两个平台的二进制文件,将其作为工作流产物传递,组装并验证完整的包家族,打包出内容不可变的 npm tarball,安装并实际运行这些 tarball,之后才允许受保护的发布作业执行。发布顺序是平台 tarball 在前,最后发布将它们列为可选依赖的入口 tarball。发布使用 `landlock-run-vX.Y.Z` tag,避免启动器版本与 monorepo 中其他发布家族发生冲突;预发布版本使用 npm 的 `next` dist-tag。 -沙箱打包安装演练不再允许 npm 注册表提供启动器。它会将当前 checkout 的入口包、匹配的原生包和 harness 依赖闭包一起打包,把这些本地 tarball 安装到仓库外部的纯 Node 消费方中,并在测试约束效果或失败闭合行为之前,证明所安装的启动器可执行、与原生构建产物字节完全一致,且具有正确的 ELF 架构。 +沙箱打包安装演练不允许 npm 注册表提供启动器。它会根据当前 workspace 的 `dependencies`、`optionalDependencies` 与必需 `peerDependencies` 递归推导 harness 闭包;原生包家族保持独立,因为保留文件模式的打包脚本会提供入口包和匹配平台包。演练把这些本地 tarball 安装到仓库外部的纯 Node 消费方中,并在测试约束效果或失败闭合行为之前,证明所安装的启动器可执行、与原生构建产物字节完全一致,且具有正确的 ELF 架构。 ## 曾考虑的替代方案 diff --git a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts index 135ebb8494..8f353ffcd2 100644 --- a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { packedWorkspaceClosure, readWorkspacePackages } from './packed-workspace-closure.ts' /** * Keyless publish-path rehearsal. It packs the provider, its workspace peers, the vendored framework @@ -25,31 +26,7 @@ const nativeDir = join(repoRoot, 'native/landlock-run') const sourceLauncher = join(nativeDir, 'packages', `linux-${process.arch}`, 'bin', 'landlock-run') const platformPackageName = `@deepseek-ai/node-addon-landlock-run-linux-${process.arch}` -/** The harness closure the consumer needs; native tarballs are packed through their mode-preserving release script. */ -const WORKSPACE_CLOSURE = [ - 'packages/sandbox/sandbox-local', - // sandbox-local's win32 chain rung is a runtime dependency: a packed - // consumer resolves it like any other @deepseek-ai peer (koffi arrives - // from the registry). - 'packages/sandbox/sandbox-windows-acl', - 'packages/subprocess/win32-process', - 'packages/sandbox/sandbox', - 'packages/core/session', - 'packages/core/scope', - 'packages/llm/llm', - 'packages/typert/protocol', - 'packages/attachment/attachment', - 'packages/util/brand', - 'packages/util/timeout', - 'packages/runtime-diagnostics/invariants', - // The framework and the vendored packages the closure declares outright: - // rescoped into @deepseek-ai, so the consumer installs this repository's - // copies. Schemastery is a hard dependency of three members above, not a - // peer, so npm resolves it while installing them. - 'vendor/cordis', - 'vendor/cosmokit', - 'vendor/schemastery', -] +const NATIVE_PACKAGE_PREFIX = '@deepseek-ai/node-addon-landlock-run' /** ELF `e_machine` (offset 18, LE) for this host: x86-64 = 62, AArch64 = 183. */ const E_MACHINE = { x64: 62, arm64: 183 }[process.arch as 'x64' | 'arm64'] @@ -93,15 +70,22 @@ describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish- .split('\n') .map(tarball => join(nativePackDest, tarball)) + // Derive the current runtime closure so a newly introduced workspace + // dependency cannot fall through to an unpublished registry version. + const workspaceClosure = packedWorkspaceClosure( + '@deepseek-ai/dsh-sandbox-local', + readWorkspacePackages(repoRoot), + ).filter(member => !member.name.startsWith(NATIVE_PACKAGE_PREFIX)) + // Pack each harness closure member with the exact bytes publish would upload. const tarballs: string[] = [] - for (const pkg of WORKSPACE_CLOSURE) { + for (const pkg of workspaceClosure) { const pack = spawnSync('pnpm', ['pack', '--pack-destination', packDest], { - cwd: join(repoRoot, pkg), + cwd: pkg.directory, encoding: 'utf8', timeout: 120_000, }) - expect(pack.status, `pnpm pack failed for ${pkg}:\n${pack.stdout}\n${pack.stderr}`).toBe(0) + expect(pack.status, `pnpm pack failed for ${pkg.name}:\n${pack.stdout}\n${pack.stderr}`).toBe(0) const lines = pack.stdout.trim().split('\n') tarballs.push(lines[lines.length - 1] as string) } diff --git a/packages/sandbox/sandbox-local/tests/packed-workspace-closure.spec.ts b/packages/sandbox/sandbox-local/tests/packed-workspace-closure.spec.ts new file mode 100644 index 0000000000..103a4e4b16 --- /dev/null +++ b/packages/sandbox/sandbox-local/tests/packed-workspace-closure.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest' +import { packedWorkspaceClosure, type WorkspacePackage } from './packed-workspace-closure.ts' + +function pkg(name: string, manifest: Record = {}): WorkspacePackage { + return { name, directory: `/workspace/${name}`, manifest } +} + +describe('packed workspace closure', () => { + it('follows install edges and required peers but excludes development and optional peers', () => { + const packages = new Map([ + ['root', pkg('root', { + dependencies: { installed: 'workspace:^' }, + optionalDependencies: { optional: 'workspace:^' }, + peerDependencies: { required: 'workspace:^', omitted: 'workspace:^' }, + peerDependenciesMeta: { omitted: { optional: true } }, + devDependencies: { development: 'workspace:^' }, + })], + ['installed', pkg('installed', { dependencies: { transitive: 'workspace:^', external: '^1.0.0' } })], + ['optional', pkg('optional')], + ['required', pkg('required')], + ['omitted', pkg('omitted')], + ['development', pkg('development')], + ['transitive', pkg('transitive')], + ]) + + expect(packedWorkspaceClosure('root', packages).map(entry => entry.name)) + .toEqual(['installed', 'optional', 'required', 'root', 'transitive']) + }) + + it('fails when a workspace dependency is absent from the inventory', () => { + const packages = new Map([ + ['root', pkg('root', { dependencies: { missing: 'workspace:^' } })], + ]) + expect(() => packedWorkspaceClosure('root', packages)) + .toThrow('packed workspace closure cannot resolve missing') + }) +}) diff --git a/packages/sandbox/sandbox-local/tests/packed-workspace-closure.ts b/packages/sandbox/sandbox-local/tests/packed-workspace-closure.ts new file mode 100644 index 0000000000..8f24e953f2 --- /dev/null +++ b/packages/sandbox/sandbox-local/tests/packed-workspace-closure.ts @@ -0,0 +1,101 @@ +import { spawnSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +const RUNTIME_SECTIONS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const + +interface WorkspaceListEntry { + name: string + path: string +} + +/** One workspace manifest available to the packed-install rehearsal. */ +export interface WorkspacePackage { + name: string + directory: string + manifest: Record +} + +function dependencyEntries( + manifest: Record, + section: (typeof RUNTIME_SECTIONS)[number], +): [string, string][] { + const value = manifest[section] + if (value === null || typeof value !== 'object' || Array.isArray(value)) return [] + return Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === 'string') +} + +function optionalPeer(manifest: Record, name: string): boolean { + const metadata = manifest.peerDependenciesMeta + if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) return false + const entry = (metadata as Record)[name] + return entry !== null && typeof entry === 'object' && !Array.isArray(entry) + && (entry as Record).optional === true +} + +/** + * Read the root pnpm workspace inventory and its package manifests. + * @param repoRoot - repository root containing the pnpm workspace. + * @returns Workspace packages indexed by package name. + */ +export function readWorkspacePackages(repoRoot: string): Map { + const listed = spawnSync('pnpm', ['list', '--recursive', '--depth', '-1', '--json'], { + cwd: repoRoot, + encoding: 'utf8', + timeout: 30_000, + }) + if (listed.status !== 0) { + throw new Error(`pnpm workspace inventory failed:\n${listed.stdout}\n${listed.stderr}`) + } + const parsed: unknown = JSON.parse(listed.stdout) + if (!Array.isArray(parsed)) throw new Error('pnpm workspace inventory is not an array') + const packages = new Map() + for (const value of parsed) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('pnpm workspace inventory contains a non-object entry') + } + const { name, path } = value as Partial + if (typeof name !== 'string' || typeof path !== 'string') { + throw new Error('pnpm workspace inventory entry lacks name/path') + } + const parsedManifest: unknown = JSON.parse(readFileSync(join(path, 'package.json'), 'utf8')) + if (parsedManifest === null || typeof parsedManifest !== 'object' || Array.isArray(parsedManifest)) { + throw new Error(`${path}/package.json is not an object`) + } + const manifest = parsedManifest as Record + if (manifest.name !== name) throw new Error(`${path}/package.json does not declare ${name}`) + if (packages.has(name)) throw new Error(`pnpm workspace inventory repeats ${name}`) + packages.set(name, { name, directory: path, manifest }) + } + return packages +} + +/** + * Follow install dependencies and required peers inside one workspace. + * @param rootName - package whose consumer closure is required. + * @param packages - workspace packages indexed by package name. + * @returns Transitive runtime closure sorted by package directory. + */ +export function packedWorkspaceClosure( + rootName: string, + packages: ReadonlyMap, +): WorkspacePackage[] { + const closure: WorkspacePackage[] = [] + const visited = new Set() + const visit = (name: string): void => { + if (visited.has(name)) return + visited.add(name) + const current = packages.get(name) + if (current === undefined) throw new Error(`packed workspace closure cannot resolve ${name}`) + closure.push(current) + for (const section of RUNTIME_SECTIONS) { + for (const [dependency, range] of dependencyEntries(current.manifest, section)) { + if (!range.startsWith('workspace:')) continue + if (section === 'peerDependencies' && optionalPeer(current.manifest, dependency)) continue + visit(dependency) + } + } + } + visit(rootName) + return closure.sort((left, right) => left.directory.localeCompare(right.directory)) +} diff --git a/scripts/rescope-vendor.ts b/scripts/rescope-vendor.ts index ed264e49e2..e9821e16ab 100644 --- a/scripts/rescope-vendor.ts +++ b/scripts/rescope-vendor.ts @@ -430,24 +430,6 @@ const VENDORED_LIBRARY = /^@deepseek-ai\\/(cosmokit|schemastery)(\\/|$)/ replace: 'parseVendoredRows(\'| `cordis/` | `@deepseek-ai/cordis` | cordis | 4.0.0 | https://example.com | `abc123` |\\n\')', expect: 1, }, - { - // The framework peer is a rescoped package, so the rehearsal installs this - // repository's vendored copies; cosmokit arrives as cordis's dependency. - id: 'packed-install-vendored-peer', - file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts', - find: ` 'packages/runtime-diagnostics/invariants', -]`, - replace: ` 'packages/runtime-diagnostics/invariants', - // The framework and the vendored packages the closure declares outright: - // rescoped into @deepseek-ai, so the consumer installs this repository's - // copies. Schemastery is a hard dependency of three members above, not a - // peer, so npm resolves it while installing them. - 'vendor/cordis', - 'vendor/cosmokit', - 'vendor/schemastery', -]`, - expect: 1, - }, { id: 'packed-install-registry-spec', file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',