Harden public package installation and release validation
Some checks failed
Release package / package (macos-latest) (push) Has been cancelled
Release package / package (ubuntu-latest) (push) Has been cancelled
Release package / package (windows-latest) (push) Has been cancelled

This commit is contained in:
oliver 2026-10-11 13:32:36 +08:00
parent 9271494b37
commit 9b2166e5b6
41 changed files with 994 additions and 363 deletions

View file

@ -12,6 +12,7 @@
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { recordBuild } from './build-integrity.mjs'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const entry = join(root, 'src/client/index.ts')
@ -35,6 +36,7 @@ const result = await Bun.build({
format: 'cjs',
sourcemap: 'none',
minify: false,
metafile: true,
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
@ -80,4 +82,5 @@ ${body}
mkdirSync(dirname(outFile), { recursive: true })
writeFileSync(outFile, artifact, 'utf8')
recordBuild(outFile, result, 'scripts/build-client.mjs')
console.log(`wrote ${outFile} (${artifact.length} bytes); requires: ${[...new Set(requires)].join(', ') || '(none)'}`)

View file

@ -10,6 +10,7 @@
import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { recordBuild } from './build-integrity.mjs'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const outDir = join(root, 'lib')
@ -66,6 +67,7 @@ for (const { entry, out } of entries) {
format: 'esm',
sourcemap: 'none',
minify: false,
metafile: true,
external,
})
@ -76,5 +78,6 @@ for (const { entry, out } of entries) {
const text = await result.outputs[0].text()
writeFileSync(out, text, 'utf8')
recordBuild(out, result, 'scripts/build-host.mjs')
console.log(`wrote ${out} (${text.length} bytes)`)
}

View file

@ -0,0 +1,44 @@
/** Track the actual bundle inputs so a release cannot silently ship stale JS. */
import { createHash } from 'node:crypto'
import { readFileSync, writeFileSync } from 'node:fs'
import { dirname, isAbsolute, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
export const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const manifestPath = resolve(root, '.release-build.json')
export const hashText = text => createHash('sha256').update(text.replace(/\r\n/g, '\n')).digest('hex')
export const hashFile = path => hashText(readFileSync(path, 'utf8'))
export function recordBuild(output, result, buildScript) {
if (!result.metafile) throw new Error('Build metadata is missing; use Bun >=1.3.13.')
const inputs = [...new Set([
'package.json', 'pnpm-lock.yaml', buildScript, 'scripts/build-integrity.mjs',
...Object.keys(result.metafile.inputs).map(path => relative(root, resolve(path)).replaceAll('\\', '/'))
.filter(path => path.startsWith('src/')),
])].sort()
if (!inputs.some(path => path.startsWith('src/'))) throw new Error('Bundle has no recorded source inputs.')
for (const path of inputs) {
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Build input escapes repository: ${path}`)
}
let builds = {}
try { builds = JSON.parse(readFileSync(manifestPath, 'utf8')).builds ?? {} } catch { /* First build. */ }
const key = relative(root, output).replaceAll('\\', '/')
builds[key] = { output: hashFile(output), inputs: Object.fromEntries(inputs.map(path => [path, hashFile(resolve(root, path))])) }
writeFileSync(manifestPath, `${JSON.stringify({ format: 1, builds: Object.fromEntries(Object.entries(builds).sort()) }, null, 2)}\n`)
}
export function checkBuilds(directory = root) {
const manifest = JSON.parse(readFileSync(resolve(directory, '.release-build.json'), 'utf8'))
if (manifest.format !== 1) throw new Error('Unknown build manifest format.')
const pkg = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'))
const outputs = [...new Set(Object.values(pkg.exports).filter(path => path.endsWith('.js')).map(path => path.replace(/^\.\//, '')))]
for (const output of outputs) {
const build = manifest.builds?.[output]
if (!build || hashFile(resolve(directory, output)) !== build.output) throw new Error(`Missing or modified artifact: ${output}; run bun run bundle.`)
for (const [path, hash] of Object.entries(build.inputs)) {
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Invalid build input: ${path}`)
if (hashFile(resolve(directory, path)) !== hash) throw new Error(`Stale artifact ${output}: ${path} changed; run bun run bundle.`)
}
}
return outputs
}

View file

@ -0,0 +1,17 @@
/** Developer fallback: use the same composition and preservation rules as Host activation. */
import { existsSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { composeNetxopsPresetArtifacts, installDirectoryPreset, syncDeclarativeIntoProfilePatch } from '../lib/index.js'
const index = process.argv.indexOf('--profile')
const profile = index < 0 ? 'web' : process.argv[index + 1]
if (!profile || !/^[\w-]+$/.test(profile)) throw new Error('Invalid profile name.')
const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
if (!existsSync(join(dshHome, 'profiles', profile, 'package.json'))) {
throw new Error(`Harness profile is not installed: ${profile}. Start this Host first.`)
}
const artifacts = composeNetxopsPresetArtifacts(profile)
installDirectoryPreset(artifacts, console)
syncDeclarativeIntoProfilePatch(profile, artifacts.declarativePatch, console)
console.log(`Netx Ops preset prepared for ${profile}. Restart this Host to load its patch.`)

View file

@ -38,18 +38,13 @@ function profileArg() {
}
/**
* Prefer the shared healed fallback `profiles/node_modules` (one coherent peer
* set). Do NOT prefer the Desktop
* profile's schemastery first — Desktop 0.2 ships 3.18.4 which, when linked
* alone into this package, makes `Config({})` return cosmokit volatile proxies
* instead of defaults and Host apply fails ("web boot: dsh-netxops failed").
* Do NOT createRequire() — that can walk into an unrelated checkout via NODE_PATH.
* Select one complete set from the requested profile or shared Host fallback.
* Never combine packages from different Host generations or other profiles.
*/
function candidateDirs(profile) {
return [
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai'),
join(dshHome, 'profiles', profile, 'node_modules', '@deepseek-ai'),
join(dshHome, 'profiles', 'web', 'node_modules', '@deepseek-ai'),
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai'),
]
}
@ -82,6 +77,7 @@ function linkOne(shortName, target) {
}
const profile = profileArg()
if (!/^[\w-]+$/.test(profile)) throw new Error('Invalid profile name.')
const dirs = candidateDirs(profile)
console.log(`DSH_HOME=${dshHome} profile=${profile}`)
console.log(`search:\n ${dirs.join('\n ')}`)

View file

@ -1,82 +1,13 @@
# Link DSH peer packages into this checkout for local `dsh plugin add <path>` / `link:`.
#
# Node resolves imports from the realpath of a linked package. A junction out of
# ~/.dsh/profiles/<name>/node_modules therefore cannot see the profile's
# hoisted peers or the healed profiles/node_modules fallback. Point the peers
# this package imports at the shared installation copy instead.
#
# Usage (PowerShell):
# .\scripts\link-dsh-peers.ps1
# Optional:
# .\scripts\link-dsh-peers.ps1 -DshHome $env:DSH_HOME
param(
[string]$DshHome = $(if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }),
[string]$Profile = 'desktop'
[string]$DshHome = $env:DSH_HOME,
[ValidatePattern('^[\w-]+$')][string]$Profile = 'desktop'
)
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
$DstRoot = Join-Path $Root 'node_modules\@deepseek-ai'
# Select one complete peer set, avoiding packages from different Host generations.
$SearchRoots = @(
(Join-Path $DshHome 'profiles\node_modules\@deepseek-ai'),
(Join-Path $DshHome "profiles\$Profile\node_modules\@deepseek-ai"),
(Join-Path $DshHome 'profiles\web\node_modules\@deepseek-ai')
)
# Runtime imports from src/index.ts (and peers they commonly need nearby).
$Peers = @(
'schemastery',
'cordis',
'dsh-credentials',
'dsh-settings',
'dsh-tools',
'dsh-scope',
'dsh-llm'
)
New-Item -ItemType Directory -Force -Path $DstRoot | Out-Null
Write-Host "DSH_HOME=$DshHome profile=$Profile"
$CoherentRoot = $null
foreach ($searchRoot in $SearchRoots) {
$complete = $true
foreach ($peer in $Peers) {
if (-not (Test-Path -LiteralPath (Join-Path $searchRoot "$peer\package.json"))) { $complete = $false; break }
}
if ($complete) { $CoherentRoot = $searchRoot; break }
$PreviousDshHome = $env:DSH_HOME
try {
if ($DshHome) { $env:DSH_HOME = $DshHome }
& node (Join-Path $PSScriptRoot 'link-dsh-peers.mjs') --profile $Profile
if ($LASTEXITCODE -ne 0) { throw 'Peer linking failed.' }
} finally {
$env:DSH_HOME = $PreviousDshHome
}
if (-not $CoherentRoot) { throw 'No complete Harness peer set found; refusing to mix package generations.' }
foreach ($name in $Peers) {
$src = $null
foreach ($root in @($CoherentRoot)) {
$candidate = Join-Path $root $name
if (Test-Path (Join-Path $candidate 'package.json')) {
$src = $candidate
break
}
}
if (-not $src) {
throw "Peer @deepseek-ai/$name not found under:`n $($SearchRoots -join "`n ")"
}
$dst = Join-Path $DstRoot $name
$absoluteDst = [IO.Path]::GetFullPath($dst)
$absolutePeerRoot = [IO.Path]::GetFullPath($DstRoot).TrimEnd('\')
if ([IO.Path]::GetDirectoryName($absoluteDst) -ne $absolutePeerRoot) {
throw "Refusing to replace a path outside the peer directory: $absoluteDst"
}
if (Test-Path $dst) {
$item = Get-Item $dst -Force
if ($item.LinkType -eq 'Junction' -or $item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
Remove-Item -LiteralPath $absoluteDst -Force
} else {
throw "Refusing to replace a non-link peer directory: $absoluteDst"
}
}
New-Item -ItemType Junction -Path $dst -Target $src | Out-Null
$ver = (Get-Content (Join-Path $src 'package.json') -Raw | ConvertFrom-Json).version
Write-Host "linked @deepseek-ai/$name@$ver <- $src"
}
Write-Host "Done. Restart DeepSeek Harness Desktop (or disable/enable dsh-netxops)."

View file

@ -1,29 +1,4 @@
#!/usr/bin/env bash
# Link DSH peer packages into this checkout for local `dsh plugin add <path>` / `link:`.
# See link-dsh-peers.ps1 for why.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
SRC_ROOT="$DSH_HOME/profiles/node_modules/@deepseek-ai"
DST_ROOT="$ROOT/node_modules/@deepseek-ai"
PEERS=(schemastery cordis dsh-credentials dsh-settings dsh-mcp-client dsh-tools)
if [[ ! -d "$SRC_ROOT" ]]; then
echo "DSH installation fallback missing: $SRC_ROOT" >&2
echo "Run dsh web once so dsh heals profiles/node_modules." >&2
exit 1
fi
mkdir -p "$DST_ROOT"
for name in "${PEERS[@]}"; do
src="$SRC_ROOT/$name"
dst="$DST_ROOT/$name"
if [[ ! -e "$src" ]]; then
echo "Peer not found: $src" >&2
exit 1
fi
rm -rf "$dst"
ln -s "$src" "$dst"
echo "linked @deepseek-ai/$name -> $src"
done
echo "Done. Re-run: dsh web"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
exec node "$SCRIPT_DIR/link-dsh-peers.mjs" "$@"

View file

@ -1,21 +1,4 @@
# Link / install presets/netxops into the DSH user agent-presets root.
# Prefer a real directory copy: DSH discovery skips Windows junctions
# (Dirent.isDirectory() is false for reparse points).
$ErrorActionPreference = "Stop"
$RepoRoot = Split-Path -Parent $PSScriptRoot
$Source = Join-Path $RepoRoot "presets\netxops"
$DshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE ".dsh" }
$TargetParent = Join-Path $DshHome ".agent-presets"
$Target = Join-Path $TargetParent "netxops"
if (-not (Test-Path (Join-Path $Source "agent.cordis.yml"))) {
throw "Missing preset dir: $Source"
}
New-Item -ItemType Directory -Force -Path $TargetParent | Out-Null
if (Test-Path $Target) {
Remove-Item -Recurse -Force $Target
}
Copy-Item -Recurse -Force $Source $Target
Set-Content -Path (Join-Path $Target ".dsh-netxops-managed") -Value ((Get-Date).ToString("o"))
Write-Host "Installed $Target (copy from $Source)"
Write-Host "Open Settings → Agent presets → Custom → Netx Ops (or new session)."
param([ValidatePattern('^[\w-]+$')][string]$Profile = 'desktop')
$ErrorActionPreference = 'Stop'
& node (Join-Path $PSScriptRoot 'install-preset.mjs') --profile $Profile
if ($LASTEXITCODE -ne 0) { throw 'Preset installation failed.' }

View file

@ -1,19 +1,4 @@
#!/usr/bin/env bash
# Install presets/netxops into $DSH_HOME/.agent-presets (copy, not symlink).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SRC="$ROOT/presets/netxops"
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
DEST_PARENT="$DSH_HOME/.agent-presets"
DEST="$DEST_PARENT/netxops"
if [[ ! -f "$SRC/agent.cordis.yml" ]]; then
echo "Missing preset dir: $SRC" >&2
exit 1
fi
mkdir -p "$DEST_PARENT"
rm -rf "$DEST"
cp -R "$SRC" "$DEST"
date -u +%Y-%m-%dT%H:%M:%SZ > "$DEST/.dsh-netxops-managed"
echo "Installed $DEST"
echo "Open Settings → Agent presets → Custom → Netx Ops"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
exec node "$SCRIPT_DIR/install-preset.mjs" "$@"

17
scripts/npm-cli.mjs Normal file
View file

@ -0,0 +1,17 @@
/** Resolve the npm bundled with Node instead of an unrelated global npm shim. */
import assert from 'node:assert/strict'
import { existsSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
export function npmRuntime() {
const node = process.versions.bun
? spawnSync('node', ['-p', 'process.execPath'], { encoding: 'utf8', windowsHide: true }).stdout.trim()
: process.execPath
const npm = [process.env.NETXOPS_NPM_CLI,
join(dirname(node), 'node_modules/npm/bin/npm-cli.js'),
resolve(dirname(node), '../lib/node_modules/npm/bin/npm-cli.js'),
].filter(Boolean).find(existsSync)
assert.ok(npm, 'Node.js must include npm to build and test the public installation package.')
return { node, npm }
}

21
scripts/pack-release.mjs Normal file
View file

@ -0,0 +1,21 @@
import { createHash } from 'node:crypto'
import { readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
import { root } from './build-integrity.mjs'
import { npmRuntime } from './npm-cli.mjs'
import { verifyRelease } from './verify-release.mjs'
const verified = verifyRelease()
const { node, npm } = npmRuntime()
const packed = spawnSync(node, [npm, 'pack', '--json', '--ignore-scripts', '--cache', join(tmpdir(), 'netxops-npm-cache')], {
cwd: root, encoding: 'utf8', windowsHide: true,
})
if (packed.status !== 0) throw new Error(packed.stderr || String(packed.error))
const info = JSON.parse(packed.stdout)[0]
const file = join(root, info.filename)
const sha = createHash('sha256').update(readFileSync(file)).digest('hex')
writeFileSync(`${file}.sha256`, `${sha} ${info.filename}\n`)
console.log(`Release ${verified.version}: ${info.files.length} files, ${(info.size / 1024).toFixed(1)} KiB`)
console.log(`Package: ${file}\nSHA256: ${sha}`)

View file

@ -0,0 +1,94 @@
/** Publisher check. No install lifecycle invokes this on a user's machine. */
import assert from 'node:assert/strict'
import { existsSync, readFileSync } from 'node:fs'
import { isBuiltin } from 'node:module'
import { resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import vm from 'node:vm'
import { load } from 'js-yaml'
import ts from 'typescript'
import { checkBuilds, root } from './build-integrity.mjs'
export function verifyRelease(directory = root) {
const read = path => readFileSync(resolve(directory, path), 'utf8')
const pkg = JSON.parse(read('package.json'))
const outputs = checkBuilds(directory)
assert.equal(pkg.name, 'dsh-netxops')
assert.match(pkg.version, /^\d+\.\d+\.\d+$/)
assert.equal(pkg.type, 'module')
for (const script of ['preinstall', 'install', 'postinstall', 'prepare', 'prepack', 'postpack', 'prepublish']) {
assert.ok(!pkg.scripts?.[script], `User installs must not run ${script}.`)
}
assert.deepEqual(pkg.dependencies, {}, 'Runtime third-party dependencies must be bundled.')
for (const [name, range] of Object.entries(pkg.peerDependencies)) {
assert.equal(pkg.peerDependenciesMeta[name]?.optional, true, `${name} must be supplied by Harness.`)
assert.doesNotMatch(range, /workspace:|file:|link:|github:/)
}
for (const path of Object.values(pkg.exports)) {
assert.ok(path.startsWith('./') && !path.includes('..'), `Unsafe export: ${path}`)
if (!path.includes('*')) assert.ok(existsSync(resolve(directory, path)), `Missing export: ${path}`)
}
assert.ok(read(pkg.icon).includes('<svg'), 'Missing plugin icon.')
for (const lang of ['en', 'zh']) {
const locale = JSON.parse(read(`locale/${lang}.json`))
assert.ok(locale.meta.title && locale.meta.description, `Missing ${lang} plugin metadata.`)
}
assert.equal(pkg.dsh.client.immediately, false)
const patch = load(read(pkg.dsh.bundle.patch))
assert.ok(Array.isArray(patch) && patch.length > 0)
const hosts = patch.flatMap(row => row.insert ?? []).filter(row => row.name === pkg.name)
assert.equal(hosts.length, 1, 'Bundle must insert exactly one Host bridge.')
assert.equal(hosts[0].id, 'netxops')
const declared = new Set(Object.keys(pkg.peerDependencies))
for (const output of outputs) {
const text = read(output)
const parsed = spawnSync(process.execPath, ['--check', resolve(directory, output)], { encoding: 'utf8', windowsHide: true })
assert.equal(parsed.status, 0, `Invalid JS in ${output}: ${parsed.stderr}`)
assert.doesNotMatch(text, /\bBun\.|react\/jsx-dev-runtime|agent-tools-shared|(?:from|import\()\s*["']\.\//)
if (output !== 'lib/client.js') {
const source = ts.createSourceFile(output, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
const visit = node => {
const specifier = ts.isImportDeclaration(node) || ts.isExportDeclaration(node)
? node.moduleSpecifier
: ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword ? node.arguments[0] : undefined
if (specifier && ts.isStringLiteral(specifier)) {
assert.ok(isBuiltin(specifier.text) || declared.has(specifier.text), `${output} needs undeclared dependency ${specifier.text}`)
}
ts.forEachChild(node, visit)
}
visit(source)
}
}
let client
vm.runInNewContext(read('lib/client.js'), { window: { __ModuleLoader__: { load(value) { client = value } } } })
assert.equal(client.id, pkg.name)
assert.equal(typeof client.factory, 'function')
const clientExports = client.factory(name => {
assert.ok(['react', 'react/jsx-runtime'].includes(name), `Unseeded browser dependency: ${name}`)
return {}
})
assert.equal(typeof clientExports.apply, 'function')
assert.deepEqual(Array.from(clientExports.inject), ['slots', 'locale'])
for (const group of ['ops/netx-ops', 'topology/netx-topology', 'biz-monitor/netx-biz-monitor']) {
const skill = read(`presets/netxops/skills/${group}/SKILL.md`).replace(/\r\n/g, '\n')
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(skill)
assert.ok(frontmatter, `Missing skill frontmatter: ${group}`)
const parsed = load(frontmatter[1])
assert.equal(typeof parsed.name, 'string')
assert.equal(typeof parsed.description, 'string')
assert.ok(skill.slice(frontmatter[0].length).trim())
}
for (const path of ['presets/netxops/PERSONA.md', 'presets/netxops/preset.yml']) assert.ok(read(path).trim())
return { version: pkg.version, outputs }
}
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
try {
const result = verifyRelease()
console.log(`Release ${result.version}: ${result.outputs.length} JS artifacts, bundle and skills verified.`)
} catch (error) {
console.error(error.message)
process.exitCode = 1
}
}