#!/usr/bin/env node /** * P4/D12 preflight — fail a Tauri build LOUDLY when the arch-parameterized * sidecar resources are missing. * * The tauri.conf.json beforeBuildCommand regenerates ONLY the sidecar JS * bundle (build-sidecar.mjs — arch-independent, safe to run arch-blind). The * Node runtime and native modules are arch-PARAMETERIZED (TARGET_ARCH) and * must be staged by the explicit npm scripts / CI steps that set it — the * hook must never regenerate them (an arch-blind re-run on a cross-arch * matrix leg clobbers the staged artifacts; P4 review HIGH). But the bundle * resources glob (`resources/*`) silently tolerates ABSENT files — a raw * `npx tauri build` on a fresh clone would package a binary with no Node * runtime at all. This check turns that silent miss into a hard stop. */ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); const resourcesDir = path.join(root, 'app', 'src-tauri', 'resources'); const stagedDepsDir = path.join(resourcesDir, 'node_modules'); const bundledNpmRuntimeDir = path.join(stagedDepsDir, 'waggle-node-runtime'); const bundledNpmBinDir = path.join(bundledNpmRuntimeDir, 'bin'); const bundledNpmPackageDir = path.join(bundledNpmRuntimeDir, 'node_modules', 'npm'); const marketplaceDbRelative = 'packages/marketplace/marketplace.db'; const targetArch = process.env.TARGET_ARCH || process.arch; const SIDECAR_PROVENANCE_PREFIX = '// Waggle-Sidecar-Provenance: '; const SOURCE_ARTIFACT_PATTERN = /(?:\.map|\.(?:[cm]?ts|tsx)|\.tsbuildinfo)$/i; const FIRST_PARTY_RUNTIME_ENTRY_PATTERN = /^(?:dist|package\.json|licen[cs]e(?:\.(?:md|txt))?|notice(?:\.(?:md|txt))?)$/i; const MANUAL_FIRST_PARTY_RUNTIME_TARGETS = new Map([ ['@waggle/hive-mind-hooks-openclaw', ['dist/handler.bundle.cjs']], ]); const REQUIRED_SHARP_VERSION = '0.35.3'; const REQUIRED_BETTER_SQLITE_RANGE = '>=12.6.2 <13'; const STAGED_DEPENDENCY_VERSION_ALLOWLISTS = new Map([ ['brace-expansion', new Set(['1.1.18', '2.1.4', '5.0.9'])], ['fast-uri', new Set(['3.1.5'])], ['ip-address', new Set(['10.4.0'])], ]); const STAGED_DEPENDENCY_DENYLIST = new Set(['js-yaml']); const missing = []; const unsafe = []; function listFiles(dir) { const files = []; const stack = [dir]; while (stack.length > 0) { const current = stack.pop(); for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const full = path.join(current, entry.name); if (entry.isDirectory()) stack.push(full); else if (entry.isFile()) files.push(full); } } return files; } function resourceRelative(file) { return path.relative(resourcesDir, file).split(path.sep).join('/'); } function sha256File(file) { return createHash('sha256').update(fs.readFileSync(file)).digest('hex'); } function readGitBlob(revision, relative) { return execFileSync( 'git', ['-C', root, 'cat-file', 'blob', `${revision}:${relative}`], { maxBuffer: 64 * 1024 * 1024, windowsHide: true }, ); } function listSqliteSidecars(dir) { if (!fs.existsSync(dir)) return []; const sidecars = []; const stack = [dir]; while (stack.length > 0) { const current = stack.pop(); for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const full = path.join(current, entry.name); if (/\.db-(?:wal|shm|journal)$/i.test(entry.name)) sidecars.push(full); if (entry.isDirectory()) stack.push(full); } } return sidecars.sort(); } function parseSidecarProvenance(serviceBuffer) { const newlineIndex = serviceBuffer.indexOf(0x0a); if (newlineIndex < 0) { throw new Error('resources/service.js is missing embedded provenance'); } const firstLine = serviceBuffer.subarray(0, newlineIndex).toString('utf8'); if (!firstLine.startsWith(SIDECAR_PROVENANCE_PREFIX)) { throw new Error('resources/service.js is missing embedded provenance'); } const encoded = firstLine.slice(SIDECAR_PROVENANCE_PREFIX.length); if ( encoded.length === 0 || encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded) ) { throw new Error('resources/service.js embedded provenance is not canonical base64'); } const decoded = Buffer.from(encoded, 'base64'); if (decoded.toString('base64') !== encoded) { throw new Error('resources/service.js embedded provenance is not canonical base64'); } let provenance; try { provenance = JSON.parse(decoded.toString('utf8')); } catch { throw new Error('resources/service.js embedded provenance is not valid JSON'); } if (!provenance || typeof provenance !== 'object' || Array.isArray(provenance)) { throw new Error('resources/service.js embedded provenance must be an object'); } if (provenance.schemaVersion !== 1) { throw new Error('resources/service.js embedded provenance schemaVersion must be 1'); } if (!/^[0-9a-f]{40}$/.test(provenance.sourceRevision)) { throw new Error('resources/service.js embedded provenance sourceRevision is invalid'); } if (provenance.entryPoint !== 'packages/server/src/local/service.ts') { throw new Error('resources/service.js embedded provenance entryPoint is invalid'); } if (!Array.isArray(provenance.sourceInputs) || provenance.sourceInputs.length === 0) { throw new Error('resources/service.js embedded provenance sourceInputs are missing'); } const requiredInputs = new Set([ 'package-lock.json', 'package.json', 'packages/server/src/local/service.ts', 'scripts/build-sidecar.mjs', ]); let previousPath = null; for (const input of provenance.sourceInputs) { if (!input || typeof input !== 'object' || Array.isArray(input)) { throw new Error('resources/service.js embedded provenance source input is invalid'); } const relative = input.path; if ( typeof relative !== 'string' || relative.length === 0 || relative.includes('\\') || relative.includes('\0') || path.posix.isAbsolute(relative) || relative.split('/').some((part) => part === '' || part === '.' || part === '..') || relative.split('/').includes('node_modules') ) { throw new Error('resources/service.js embedded provenance source input path is unsafe'); } if (previousPath !== null && previousPath >= relative) { throw new Error('resources/service.js embedded provenance source inputs are not unique and sorted'); } if (!/^[0-9a-f]{64}$/.test(input.sha256)) { throw new Error('resources/service.js embedded provenance source input hash is invalid'); } previousPath = relative; requiredInputs.delete(relative); } if (requiredInputs.size > 0) { throw new Error('resources/service.js embedded provenance omits required source inputs'); } const payload = serviceBuffer.subarray(newlineIndex + 1); if ( !provenance.bundlePayload || typeof provenance.bundlePayload !== 'object' || !Number.isSafeInteger(provenance.bundlePayload.sizeBytes) || provenance.bundlePayload.sizeBytes < 1 || !/^[0-9a-f]{64}$/.test(provenance.bundlePayload.sha256) || provenance.bundlePayload.sizeBytes !== payload.byteLength || provenance.bundlePayload.sha256 !== createHash('sha256').update(payload).digest('hex') ) { throw new Error('resources/service.js payload does not match embedded provenance'); } return provenance; } function expectedSourceRevision() { const index = process.argv.indexOf('--expected-source-revision'); const supplied = index >= 0 ? process.argv[index + 1] : null; if (index >= 0 && !supplied) { throw new Error('--expected-source-revision requires a 40-character revision'); } const revision = supplied ?? execFileSync( 'git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8', windowsHide: true }, ).trim(); if (!/^[0-9a-f]{40}$/.test(revision)) { throw new Error('expected source revision must be exactly 40 lowercase hexadecimal characters'); } return revision; } function readManifest(packageDir) { try { return JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')); } catch { return {}; } } function listPackageDirs(nodeModulesDir) { if (!fs.existsSync(nodeModulesDir)) return []; const packageDirs = []; const stack = [nodeModulesDir]; while (stack.length > 0) { const current = stack.pop(); for (const entry of fs.readdirSync(current, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const full = path.join(current, entry.name); if (fs.existsSync(path.join(full, 'package.json'))) packageDirs.push(full); stack.push(full); } } return packageDirs; } function isSupportedBetterSqliteVersion(version) { if (typeof version !== 'string') return false; const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version); if (!match) return false; const [major, minor, patch] = match.slice(1).map(Number); if (![major, minor, patch].every(Number.isSafeInteger)) return false; return major === 12 && (minor > 6 || (minor === 6 && patch >= 2)); } function stagedDependencyVersionFailures(nodeModulesDir, packageManifests) { const manifests = packageManifests ?? listPackageDirs(nodeModulesDir) .map((packageDir) => [packageDir, readManifest(packageDir)]); const failures = []; for (const [packageDir, manifest] of manifests) { const relative = path.relative(nodeModulesDir, packageDir).split(path.sep).join('/'); const normalizedRelative = relative.toLowerCase(); const isBetterSqlitePath = ( normalizedRelative === 'better-sqlite3' || normalizedRelative.endsWith('/node_modules/better-sqlite3') ); const allowedVersions = STAGED_DEPENDENCY_VERSION_ALLOWLISTS.get(manifest.name); if (allowedVersions && !allowedVersions.has(manifest.version)) { failures.push( `node_modules/${relative} contains ${manifest.name}@${manifest.version}; ` + `allowed versions: ${[...allowedVersions].join(', ')}`, ); } if (isBetterSqlitePath && manifest.name !== 'better-sqlite3') { failures.push( `node_modules/${relative} must identify as better-sqlite3; ` + `found name ${JSON.stringify(manifest.name)}`, ); } if ( (isBetterSqlitePath || manifest.name === 'better-sqlite3') && !isSupportedBetterSqliteVersion(manifest.version) ) { failures.push( `node_modules/${relative} contains better-sqlite3@${manifest.version}; ` + `required version: ${REQUIRED_BETTER_SQLITE_RANGE}`, ); } if ( ( manifest.name === 'sharp' || ( typeof manifest.name === 'string' && /^@img\/sharp-(?!libvips-)/.test(manifest.name) ) ) && manifest.version !== REQUIRED_SHARP_VERSION ) { failures.push( `node_modules/${relative} contains ${manifest.name}@${manifest.version}; ` + `required version: ${REQUIRED_SHARP_VERSION}`, ); } if (STAGED_DEPENDENCY_DENYLIST.has(manifest.name)) { failures.push(`node_modules/${relative} contains development-only ${manifest.name}`); } } const bundledBraceDir = path.join( nodeModulesDir, 'waggle-node-runtime', 'node_modules', 'npm', 'node_modules', 'brace-expansion', ); const bundledBraceVersion = readManifest(bundledBraceDir).version; if (bundledBraceVersion !== '2.1.4') { failures.push( `node_modules/waggle-node-runtime/node_modules/npm/node_modules/brace-expansion ` + `must be exactly 2.1.4; found ${bundledBraceVersion ?? 'missing'}`, ); } return failures; } function localWorkspacePackageNames() { const names = new Set(); for (const workspaceRoot of ['packages', 'apps'].map((entry) => path.join(root, entry))) { if (!fs.existsSync(workspaceRoot)) continue; for (const entry of fs.readdirSync(workspaceRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const manifest = readManifest(path.join(workspaceRoot, entry.name)); if (typeof manifest.name === 'string') names.add(manifest.name); } } return names; } function collectRuntimeExportTargets(value, targets, condition = '') { if (condition === 'types') return; if (typeof value === 'string') { targets.add(value); return; } if (Array.isArray(value)) { for (const entry of value) collectRuntimeExportTargets(entry, targets, condition); return; } if (!value || typeof value !== 'object') return; for (const [key, entry] of Object.entries(value)) { collectRuntimeExportTargets(entry, targets, key); } } function firstPartyRuntimeTargets(manifest) { const targets = new Set(); if (typeof manifest.main === 'string') targets.add(manifest.main); if (typeof manifest.module === 'string') targets.add(manifest.module); if (typeof manifest.bin === 'string') targets.add(manifest.bin); else if (manifest.bin && typeof manifest.bin === 'object') { for (const entry of Object.values(manifest.bin)) { if (typeof entry === 'string') targets.add(entry); } } collectRuntimeExportTargets(manifest.exports, targets); for (const entry of MANUAL_FIRST_PARTY_RUNTIME_TARGETS.get(manifest.name) || []) { targets.add(entry); } return targets; } function validateFirstPartyRuntimeTargets(packageDir, manifest) { const failures = []; const distDir = path.resolve(packageDir, 'dist'); for (const target of firstPartyRuntimeTargets(manifest)) { const relative = target.replace(/^\.\//, '').split('/').join(path.sep); const resolved = path.resolve(packageDir, relative); const withinDist = resolved.startsWith(`${distDir}${path.sep}`); let regularRuntimeFile = false; let realDistWithinPackage = false; let realWithinDist = false; if (withinDist && fs.existsSync(resolved)) { const stat = fs.lstatSync(resolved); regularRuntimeFile = stat.isFile() && !stat.isSymbolicLink(); if (regularRuntimeFile) { const realPackageDir = fs.realpathSync.native(packageDir); const realDistDir = fs.realpathSync.native(distDir); const realTarget = fs.realpathSync.native(resolved); realDistWithinPackage = realDistDir.startsWith(`${realPackageDir}${path.sep}`); realWithinDist = realTarget.startsWith(`${realDistDir}${path.sep}`); } } if ( !withinDist || !realDistWithinPackage || !realWithinDist || !regularRuntimeFile || target.includes('*') ) { failures.push(target); } } return failures; } const dependencyOnlyIndex = process.argv.indexOf('--dependency-versions-only'); if (dependencyOnlyIndex >= 0) { const target = process.argv[dependencyOnlyIndex + 1]; if (!target) { console.error('[check-sidecar-resources] --dependency-versions-only requires a directory'); process.exit(1); } const failures = stagedDependencyVersionFailures(path.resolve(target)); if (failures.length > 0) { for (const failure of failures) console.error(failure); process.exit(1); } console.log('[check-sidecar-resources] staged dependency versions are release-safe'); process.exit(0); } let expectedRevision = null; try { expectedRevision = expectedSourceRevision(); } catch (err) { unsafe.push(err instanceof Error ? err.message : String(err)); } const servicePath = path.join(resourcesDir, 'service.js'); if (!fs.existsSync(servicePath)) { missing.push('resources/service.js (run: node scripts/build-sidecar.mjs)'); } else { const serviceBuffer = fs.readFileSync(servicePath); const service = serviceBuffer.toString('utf8'); if (/(?:\/\/|\/\*)[#@]\s*sourceMappingURL\s*=/.test(service)) { unsafe.push('resources/service.js contains a sourceMappingURL directive'); } try { const provenance = parseSidecarProvenance(serviceBuffer); if (expectedRevision && provenance.sourceRevision !== expectedRevision) { throw new Error('resources/service.js source revision does not match expected revision'); } for (const input of provenance.sourceInputs) { const absolute = path.join(root, ...input.path.split('/')); let sourceSafe = false; if (fs.existsSync(absolute)) { const stat = fs.lstatSync(absolute); const relative = path.relative(root, absolute); sourceSafe = stat.isFile() && !stat.isSymbolicLink() && relative !== '' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) && sha256File(absolute) === input.sha256; } if (!sourceSafe) { throw new Error( `resources/service.js source input hash does not match current source: ${input.path}`, ); } } } catch (err) { unsafe.push(err instanceof Error ? err.message : String(err)); } } const canonicalMarketplaceDb = path.join(root, ...marketplaceDbRelative.split('/')); const marketplaceResource = path.join(resourcesDir, 'marketplace.db'); for (const suffix of ['-wal', '-shm', '-journal']) { if (fs.existsSync(`${canonicalMarketplaceDb}${suffix}`)) { unsafe.push(`packages/marketplace/marketplace.db${suffix} must not be present while staging`); } } for (const sidecar of listSqliteSidecars(resourcesDir)) { unsafe.push(`resources/${resourceRelative(sidecar)} must not be packaged`); } const canonicalMarketplaceIsRegular = fs.existsSync(canonicalMarketplaceDb) && fs.lstatSync(canonicalMarketplaceDb).isFile() && !fs.lstatSync(canonicalMarketplaceDb).isSymbolicLink(); const marketplaceResourceIsRegular = fs.existsSync(marketplaceResource) && fs.lstatSync(marketplaceResource).isFile() && !fs.lstatSync(marketplaceResource).isSymbolicLink(); if (!canonicalMarketplaceIsRegular) { missing.push('packages/marketplace/marketplace.db canonical build input'); } let marketplaceGitBlob = null; if (expectedRevision) { try { marketplaceGitBlob = readGitBlob(expectedRevision, marketplaceDbRelative); } catch { unsafe.push('canonical marketplace database is missing from exact source revision'); } } const canonicalMarketplaceBytes = canonicalMarketplaceIsRegular ? fs.readFileSync(canonicalMarketplaceDb) : null; if ( marketplaceGitBlob && canonicalMarketplaceBytes && !canonicalMarketplaceBytes.equals(marketplaceGitBlob) ) { unsafe.push( 'packages/marketplace/marketplace.db canonical marketplace database does not match exact source revision', ); } if (!fs.existsSync(marketplaceResource)) { missing.push('resources/marketplace.db (run: node scripts/build-sidecar.mjs)'); } else if (!marketplaceResourceIsRegular) { unsafe.push('resources/marketplace.db must be a regular file'); } else { const marketplaceResourceBytes = fs.readFileSync(marketplaceResource); if (marketplaceGitBlob && !marketplaceResourceBytes.equals(marketplaceGitBlob)) { unsafe.push( 'resources/marketplace.db does not match the canonical marketplace database at the exact source revision', ); } else if ( canonicalMarketplaceBytes && !marketplaceResourceBytes.equals(canonicalMarketplaceBytes) ) { unsafe.push('resources/marketplace.db does not match the canonical marketplace database'); } } const sourceArtifacts = fs.existsSync(resourcesDir) ? fs.readdirSync(resourcesDir, { withFileTypes: true }) .filter((entry) => /\.(?:map|tsx?)$/i.test(entry.name)) .map((entry) => entry.name) : []; for (const artifact of sourceArtifacts) { unsafe.push(`resources/${artifact} must not be packaged`); } const stagedPackageDirs = listPackageDirs(stagedDepsDir); const stagedPackageManifests = stagedPackageDirs .map((packageDir) => [packageDir, readManifest(packageDir)]); for (const failure of stagedDependencyVersionFailures(stagedDepsDir, stagedPackageManifests)) { unsafe.push(`resources/${failure}`); } const firstPartyRoot = path.join(stagedDepsDir, '@waggle'); const firstPartyPackageDirs = new Map(); if (fs.existsSync(firstPartyRoot)) { for (const packageEntry of fs.readdirSync(firstPartyRoot, { withFileTypes: true })) { if (!packageEntry.isDirectory()) continue; firstPartyPackageDirs.set( path.join(firstPartyRoot, packageEntry.name), `@waggle/${packageEntry.name}`, ); } } const workspacePackageNames = localWorkspacePackageNames(); for (const name of workspacePackageNames) { const directPackageDir = path.join(stagedDepsDir, ...name.split('/')); if (fs.existsSync(directPackageDir)) { firstPartyPackageDirs.set(directPackageDir, name); } } for (const [packageDir, manifest] of stagedPackageManifests) { const { name } = manifest; if ( typeof name === 'string' && (name.startsWith('@waggle/') || workspacePackageNames.has(name)) ) { firstPartyPackageDirs.set(packageDir, name); } } for (const [packageDir, expectedName] of firstPartyPackageDirs) { let manifest = {}; const manifestPath = path.join(packageDir, 'package.json'); try { const stat = fs.lstatSync(manifestPath); if (!stat.isFile() || stat.isSymbolicLink()) { throw new Error('manifest must be a regular file'); } const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('manifest must contain a JSON object'); } if (parsed.name !== expectedName) { throw new Error(`manifest name ${JSON.stringify(parsed.name)} does not match ${expectedName}`); } manifest = parsed; } catch (err) { unsafe.push( `resources/${resourceRelative(manifestPath)} is missing or invalid: ` + (err instanceof Error ? err.message : String(err)), ); } for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) { if (FIRST_PARTY_RUNTIME_ENTRY_PATTERN.test(entry.name)) continue; const relative = resourceRelative(path.join(packageDir, entry.name)); unsafe.push(`resources/${relative} is not a runtime package entry`); } for (const file of listFiles(packageDir)) { if (SOURCE_ARTIFACT_PATTERN.test(file)) { unsafe.push(`resources/${resourceRelative(file)} must not be packaged`); } if ( /\.(?:[cm]?js)$/i.test(file) && /(?:\/\/|\/\*)[#@]\s*sourceMappingURL\s*=/.test(fs.readFileSync(file, 'utf8')) ) { unsafe.push(`resources/${resourceRelative(file)} contains a sourceMappingURL directive`); } } for (const target of validateFirstPartyRuntimeTargets(packageDir, manifest)) { unsafe.push( `resources/${resourceRelative(packageDir)} has an invalid or missing runtime target: ${target}`, ); } } const nodeBinary = process.platform === 'win32' ? 'node.exe' : 'node'; const nodePath = path.join(resourcesDir, nodeBinary); const npmCliPath = path.join(bundledNpmPackageDir, 'bin', 'npm-cli.js'); const npxCliPath = path.join(bundledNpmPackageDir, 'bin', 'npx-cli.js'); const npmWrapperPath = path.join( bundledNpmBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm', ); const npxWrapperPath = path.join( bundledNpmBinDir, process.platform === 'win32' ? 'npx.cmd' : 'npx', ); const bundledNpmFiles = [ path.join(bundledNpmRuntimeDir, 'package.json'), path.join(bundledNpmRuntimeDir, 'NODE-LICENSE'), path.join(bundledNpmPackageDir, 'LICENSE'), npmCliPath, npxCliPath, npmWrapperPath, npxWrapperPath, ]; for (const file of bundledNpmFiles) { if (!fs.existsSync(file) || !fs.lstatSync(file).isFile()) { missing.push(`resources/${resourceRelative(file)} (run: node scripts/bundle-node.mjs)`); } } if (process.platform !== 'win32') { for (const wrapper of [npmWrapperPath, npxWrapperPath]) { if (fs.existsSync(wrapper) && (fs.statSync(wrapper).mode & 0o111) === 0) { unsafe.push(`resources/${resourceRelative(wrapper)} is not executable`); } } } let bundledNodeVersion = null; if (!fs.existsSync(nodePath)) { missing.push(`resources/${nodeBinary} (run: node scripts/bundle-node.mjs)`); } else { try { const bundledRuntime = JSON.parse(execFileSync(nodePath, [ '-p', 'JSON.stringify({ arch: process.arch, version: process.versions.node })', ], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], }).trim()); bundledNodeVersion = bundledRuntime.version; // The checker may run under a different Node major than the bundled runtime. // The native-module probe below is the authoritative ABI compatibility check. if (bundledRuntime.arch !== targetArch) { missing.push( `resources/${nodeBinary} architecture ${bundledRuntime.arch} does not match target ${targetArch}`, ); } } catch (err) { missing.push(`resources/${nodeBinary} is not executable (${err.message})`); } } if ( fs.existsSync(nodePath) && bundledNpmFiles.every((file) => fs.existsSync(file)) ) { try { const runtimeManifest = readManifest(bundledNpmRuntimeDir); const npmManifest = readManifest(bundledNpmPackageDir); if (runtimeManifest.name !== 'waggle-node-runtime') { throw new Error('runtime manifest has an unexpected name'); } if (runtimeManifest.version !== bundledNodeVersion) { throw new Error( `runtime manifest Node ${runtimeManifest.version} does not match bundled Node ${bundledNodeVersion}`, ); } if (typeof npmManifest.version !== 'string' || npmManifest.version.length === 0) { throw new Error('npm manifest has no version'); } const runBundledCli = (cliPath) => execFileSync(nodePath, [cliPath, '--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }).trim(); const runWrapper = (wrapperPath) => { if (process.platform === 'win32') { return execFileSync(process.env.ComSpec || 'cmd.exe', [ '/d', '/s', '/c', `""${wrapperPath}" --version"`, ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsVerbatimArguments: true, }).trim(); } return execFileSync(wrapperPath, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }).trim(); }; const versions = [ runBundledCli(npmCliPath), runBundledCli(npxCliPath), runWrapper(npmWrapperPath), runWrapper(npxWrapperPath), ]; if (versions.some((version) => version !== npmManifest.version)) { throw new Error(`npm/npx version mismatch: ${versions.join(', ')}`); } } catch (err) { missing.push( `resources bundled npm/npx runtime probe failed (${err instanceof Error ? err.message : String(err)})`, ); } } const nativeDir = path.join(resourcesDir, 'native'); const nativeEntries = fs.existsSync(nativeDir) ? fs.readdirSync(nativeDir).filter((e) => e !== '.gitkeep' && e !== 'onnxruntime') : []; const requiredWindowsNativeFiles = [ 'better_sqlite3.node', 'vec0.dll', 'onnxruntime/onnxruntime_binding.node', ]; if (process.platform === 'win32') { for (const entry of requiredWindowsNativeFiles) { if (!fs.existsSync(path.join(nativeDir, ...entry.split('/')))) { missing.push( `resources/native/${entry} (run: node scripts/bundle-native-deps.mjs)`, ); } } } else if (process.platform === 'darwin') { const requiredMacNativeFiles = [ 'better_sqlite3.node', 'vec0.dylib', 'onnxruntime/onnxruntime_binding.node', ]; for (const entry of requiredMacNativeFiles) { if (!fs.existsSync(path.join(nativeDir, ...entry.split('/')))) { missing.push(`resources/native/${entry} (run: node scripts/bundle-native-deps.mjs)`); } } const onnxDir = path.join(nativeDir, 'onnxruntime'); const hasOnnxLibrary = fs.existsSync(onnxDir) && fs.readdirSync(onnxDir).some((entry) => entry.endsWith('.dylib')); if (!hasOnnxLibrary) { missing.push('resources/native/onnxruntime/*.dylib (run: node scripts/bundle-native-deps.mjs)'); } } else if (nativeEntries.length === 0) { missing.push('resources/native/* (run: node scripts/bundle-native-deps.mjs)'); } // Staged production node_modules for the esbuild-externalized packages // (better-sqlite3, @fastify/static, mammoth, …). The bundled service.js // `require()`s these by bare name and resolves them via NODE_PATH=resources/ // node_modules (service.rs). Like the native deps above, staging is done by the // npm scripts / CI (stage-sidecar-deps.mjs), NOT the arch-blind beforeBuildCommand // hook — so a raw `npx tauri build` that skips those would package a sidecar that // dies with MODULE_NOT_FOUND on first boot. Probe a canonical external. const stagedBetterSqlite = path.join(stagedDepsDir, 'better-sqlite3'); const stagedOnnxRuntime = path.join(stagedDepsDir, 'onnxruntime-node'); const stagedTransformers = path.join(stagedDepsDir, '@huggingface', 'transformers'); const stagedTransformersEntry = path.join( stagedTransformers, 'dist', 'transformers.node.cjs', ); const stagedSharp = path.join(stagedDepsDir, 'sharp'); const stagedSharpWindowsBinding = path.join( stagedDepsDir, '@img', 'sharp-win32-x64', 'lib', `sharp-win32-x64-${REQUIRED_SHARP_VERSION}.node`, ); const vecExtension = path.join( nativeDir, `vec0.${process.platform === 'win32' ? 'dll' : process.platform === 'darwin' ? 'dylib' : 'so'}`, ); if (!fs.existsSync(path.join(stagedBetterSqlite, 'package.json'))) { missing.push('resources/node_modules/* (run: node scripts/stage-sidecar-deps.mjs)'); } if (!fs.existsSync(path.join(stagedOnnxRuntime, 'package.json'))) { missing.push('resources/node_modules/onnxruntime-node (run: node scripts/stage-sidecar-deps.mjs)'); } if (!fs.existsSync(path.join(stagedTransformers, 'package.json'))) { missing.push( 'resources/node_modules/@huggingface/transformers ' + '(run: node scripts/stage-sidecar-deps.mjs)', ); } if (!fs.existsSync(stagedTransformersEntry)) { missing.push( 'resources/node_modules/@huggingface/transformers/dist/transformers.node.cjs ' + '(run: node scripts/stage-sidecar-deps.mjs)', ); } if (!fs.existsSync(path.join(stagedSharp, 'package.json'))) { missing.push('resources/node_modules/sharp (run: node scripts/stage-sidecar-deps.mjs)'); } if ( process.platform === 'win32' && targetArch === 'x64' && !fs.existsSync(stagedSharpWindowsBinding) ) { missing.push( `resources/node_modules/@img/sharp-win32-x64/lib/` + `sharp-win32-x64-${REQUIRED_SHARP_VERSION}.node ` + '(run: node scripts/stage-sidecar-deps.mjs)', ); } if ( fs.existsSync(nodePath) && fs.existsSync(path.join(stagedBetterSqlite, 'package.json')) && marketplaceResourceIsRegular ) { let marketplaceProbeRoot; try { marketplaceProbeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-marketplace-probe-')); const marketplaceProbeDb = path.join(marketplaceProbeRoot, 'marketplace.db'); fs.copyFileSync(marketplaceResource, marketplaceProbeDb); const marketplaceProbe = [ 'const Database = require(process.argv[1]);', 'const database = new Database(process.argv[2], { readonly: true, fileMustExist: true });', 'const integrity = database.pragma("integrity_check", { simple: true });', 'if (integrity !== "ok") throw new Error(`integrity_check: ${integrity}`);', 'const foreignKeys = database.pragma("foreign_key_check");', 'if (foreignKeys.length !== 0) throw new Error("foreign_key_check failed");', 'const tables = database.prepare("SELECT name FROM sqlite_master WHERE type = \'table\' AND name IN (\'sources\', \'packages\')").all();', 'database.close();', 'if (new Set(tables.map((row) => row.name)).size !== 2) throw new Error("required tables missing");', ].join(''); execFileSync(nodePath, [ '-e', marketplaceProbe, stagedBetterSqlite, marketplaceProbeDb, ], { cwd: marketplaceProbeRoot, env: { ...process.env, NODE_PATH: stagedDepsDir }, stdio: ['ignore', 'pipe', 'pipe'], }); } catch { unsafe.push('resources/marketplace.db failed its SQLite integrity/schema probe'); } finally { if (marketplaceProbeRoot) { fs.rmSync(marketplaceProbeRoot, { recursive: true, force: true }); } } } if ( fs.existsSync(nodePath) && fs.existsSync(path.join(stagedTransformers, 'package.json')) && fs.existsSync(stagedTransformersEntry) && fs.existsSync(path.join(stagedSharp, 'package.json')) && ( process.platform !== 'win32' || targetArch !== 'x64' || fs.existsSync(stagedSharpWindowsBinding) ) ) { try { const imageProbe = [ 'const { RawImage } = require(process.argv[1]);', 'const sharp = require(process.argv[2]);', 'if (process.argv[3]) require(process.argv[3]);', 'if (sharp.versions?.emscripten) throw new Error("Sharp fell back to WASM");', 'void (async () => {', 'const image = new RawImage(', 'Uint8Array.from([255,0,0,255,0,255,0,255,0,0,255,255,255,255,255,255]),', '2,2,4);', 'const buffer = await image.toSharp().resize(1, 1).png().toBuffer();', 'const signature = Buffer.from([137,80,78,71,13,10,26,10]);', 'if (buffer.length < signature.length || !buffer.subarray(0, 8).equals(signature)) {', 'throw new Error("Sharp PNG probe failed");', '}', '})().catch((error) => { console.error(error); process.exit(1); });', ].join(''); execFileSync(nodePath, [ '-e', imageProbe, stagedTransformersEntry, stagedSharp, ...( process.platform === 'win32' && targetArch === 'x64' ? [stagedSharpWindowsBinding] : [] ), ], { cwd: resourcesDir, env: { ...process.env, NODE_PATH: stagedDepsDir }, stdio: ['ignore', 'pipe', 'pipe'], }); } catch (err) { missing.push( `resources image runtime probe failed for @huggingface/transformers and sharp ` + `using bundled ${nodeBinary} for ${targetArch} ` + `(${err instanceof Error ? err.message : String(err)})`, ); } } if ( fs.existsSync(nodePath) && fs.existsSync(path.join(stagedBetterSqlite, 'package.json')) && fs.existsSync(path.join(stagedOnnxRuntime, 'package.json')) && fs.existsSync(vecExtension) ) { try { const probe = [ 'const Database = require(process.argv[1]);', 'if (process.arch !== process.argv[2]) throw new Error(`architecture ${process.arch}`);', 'const database = new Database(\':memory:\');', 'database.loadExtension(process.argv[3]);', 'const row = database.prepare(\'SELECT 1 AS ok\').get();', 'const vec = database.prepare(\'SELECT vec_version() AS version\').get();', 'database.close();', 'if (row.ok !== 1) throw new Error(\'SQLite query failed\');', 'if (typeof vec.version !== \'string\' || vec.version.length === 0) throw new Error(\'sqlite-vec query failed\');', 'const onnx = require(process.argv[4]);', 'if (typeof onnx.InferenceSession !== \'function\') throw new Error(\'ONNX binding failed\');', ].join(''); execFileSync(nodePath, [ '-e', probe, stagedBetterSqlite, targetArch, vecExtension, stagedOnnxRuntime, ], { cwd: resourcesDir, env: { ...process.env, NODE_PATH: stagedDepsDir }, stdio: ['ignore', 'pipe', 'pipe'], }); } catch { missing.push( `resources native runtime probe failed for better-sqlite3, sqlite-vec, or onnxruntime-node ` + `using bundled ${nodeBinary} for ${targetArch}`, ); } } // External agents and hook management run directly from this staged payload; // none of these packages are available from npm in a packaged installation. const hookRuntimeEntries = [ '@waggle/hive-mind-cli/dist/index.js', '@waggle/hive-mind-hooks-claude-code/dist/bin/claude-code-hooks-cli.js', '@waggle/hive-mind-hooks-claude-desktop/dist/bin/claude-desktop-hooks.js', '@waggle/hive-mind-hooks-codex/dist/bin/codex-hooks.js', '@waggle/hive-mind-hooks-codex-desktop/dist/bin/codex-desktop-hooks.js', '@waggle/hive-mind-hooks-cursor/dist/bin/cursor-hooks.js', '@waggle/hive-mind-hooks-hermes/dist/bin/hermes-hooks.js', '@waggle/hive-mind-hooks-openclaw/dist/bin/openclaw-hooks.js', '@waggle/hive-mind-hooks-openclaw/dist/handler.bundle.cjs', 'waggle-memory-mcp/dist/index.js', ]; for (const entry of hookRuntimeEntries) { if (!fs.existsSync(path.join(stagedDepsDir, ...entry.split('/')))) { missing.push(`resources/node_modules/${entry} (run: node scripts/stage-sidecar-deps.mjs)`); } } if (missing.length > 0 || unsafe.length > 0) { console.error('[check-sidecar-resources] FATAL — sidecar resources are not release-safe:'); for (const m of missing) console.error(` - ${m}`); for (const item of unsafe) console.error(` - ${item}`); console.error( '[check-sidecar-resources] Stage them with the bundle scripts (set TARGET_ARCH for\n' + 'cross-arch builds) or use the npm tauri:build* scripts / CI, which run them for you.', ); process.exit(1); } console.log('[check-sidecar-resources] OK — Node runtime + native deps + staged node_modules present');