diff --git a/Makefile b/Makefile index cd01afa4d..c9d2c6fbb 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ else CARGO_FLAGS = --profile $(PROFILE) endif -.PHONY: build addon addon-fast install-dev uninstall-dev qos-global test verify test-node-matrix bench clean npm-build npm-publish npm-publish-dry +.PHONY: build addon addon-fast install-dev uninstall-dev qos-global test verify fix test-node-matrix bench clean npm-build npm-publish npm-publish-dry build: addon $(CARGO) build $(CARGO_FLAGS) @@ -123,6 +123,23 @@ verify: tests/brand-lint/check-path-literals.sh NUB_SHARED_TARGET="$(CURDIR)/target" "$(RUST_BUILD)" test +# Apply clippy's machine-applicable autofixes across the root workspace AND the +# nub-native workspace (the separate one `verify` lints from inside the crate). +# Run on a dirty tree is intended (--allow-dirty --allow-staged); review the +# diff before committing. +fix: + @# `--all-features` activates `embed-runtime`, whose build.rs reads + @# runtime/addons/nub-native.node for integrity hashing and panics when + @# it is absent (it is gitignored, built by `make addon` / staged in CI). + @# Stage a placeholder — the same trick CI's lint job uses — so `make fix` + @# works on a fresh tree without a full addon build. clippy --fix never + @# runs the binary, so the placeholder is never loaded. + @mkdir -p runtime/addons + @test -s runtime/addons/nub-native.node || printf 'placeholder-addon' > runtime/addons/nub-native.node + @set -e; \ + NUB_SHARED_TARGET="$(CURDIR)/target" "$(RUST_BUILD)" clippy --fix --workspace --all-targets --all-features --profile fast --allow-dirty --allow-staged -- -D warnings; \ + (cd crates/nub-native && NUB_SHARED_TARGET="$(CURDIR)/target" "$(RUST_BUILD)" clippy --fix --all-features --profile fast --allow-dirty --allow-staged -- -D warnings) + # Run the integration suite across a Node version matrix (18.19 floor → 22.15 # fast-path floor) — the local mirror of ci.yml's `test` job. Locates or # downloads each Node under ~/.cache/nub-test-node. See the script header. diff --git a/external-tools.json b/external-tools.json index edbfd655c..01c590062 100644 --- a/external-tools.json +++ b/external-tools.json @@ -3,9 +3,7 @@ "pnpm": { "description": "pnpm — the fleet's package manager.", "version": "11.8.0", - "packageManager": "pnpm", "repository": "github:pnpm/pnpm", - "release": "asset", "notes": [ "Latest soaked pnpm — CI downloads + SRI-verifies (sha512) the pinned asset", "darwin-x64 has no SEA asset upstream; its pin is the npm registry tarball" @@ -35,15 +33,16 @@ "asset": "pnpm-linux-x64-musl.tar.gz", "integrity": "sha512-5WXlo2yCDmoBIue5iHRK3zNSPVAmzci+5RhuxwUA4hnH9W3TFjDnQHyAzDVnkY878mfTXE35THZeEc1OKYPmag==" }, - "win-arm64": { + "win32-arm64": { "asset": "pnpm-win32-arm64.zip", "integrity": "sha512-iv1hJEj9FUiVFae2cmCYjbRK0Xn27Uh+kpglT2LWvQyl4WeIOgs1ouKrptVxk3X3t7DB5vuYKbsmE+9BnoQ4FA==" }, - "win-x64": { + "win32-x64": { "asset": "pnpm-win32-x64.zip", "integrity": "sha512-jyqMgedndbck/xJjXPem5Lw7V0YtDfiUJIB81e/hmnrg3yLKPoyCKO5ILegYkLPIr945IJRcVoJZwjtog8FCqg==" } - } + }, + "origin": "gh-asset" }, "npm": { "notes": [ @@ -53,13 +52,13 @@ "description": "npm — pinned, SRI-verified registry tarball; installed without self-update", "repository": "npm:npm", "version": "12.0.0", - "integrity": "sha512-qzvPQfNSY7louiM6rv7dL0hi5esBGLn1lLwxbdyL5XOIssWzoYMwn8xqvWhYcZL6onTkenYSxrtKxsFrFbUFyw==" + "integrity": "sha512-qzvPQfNSY7louiM6rv7dL0hi5esBGLn1lLwxbdyL5XOIssWzoYMwn8xqvWhYcZL6onTkenYSxrtKxsFrFbUFyw==", + "origin": "npm" }, "sfw-free": { "description": "Socket Firewall (free tier) — malware gate on dep installs.", "version": "1.13.1", "repository": "github:SocketDev/sfw-free", - "release": "asset", "binaryName": "sfw", "notes": [ "Used when SOCKET_SECURITY_KEY is not set", @@ -82,17 +81,17 @@ "asset": "sfw-free-linux-x86_64", "integrity": "sha512-waLrsPG2a7EOv0XuvXDQZGgCZ4MTtOfZh8TmGbM6gn2B6Nh6HI+15jaoKdAS9wgdTyIqTuqU+O+NtVYd+kuFaA==" }, - "win-x64": { + "win32-x64": { "asset": "sfw-free-windows-x86_64.exe", "integrity": "sha512-YYnfwR6M/PHo72LSyKtpY3bAUG4F4ckToJqGx5Fkz4rwg1+48hkxuBaF3hdxHUdHPkfO5grDyoNgXGe7FojGcg==" } - } + }, + "origin": "gh-asset" }, "sfw-enterprise": { "description": "Socket Firewall (enterprise tier) — selected when SOCKET_SECURITY_KEY is set.", "version": "1.13.1", "repository": "github:SocketDev/firewall-release", - "release": "asset", "binaryName": "sfw", "notes": [ "Used when SOCKET_SECURITY_KEY is set (the one env var every Socket product reads)", @@ -115,17 +114,17 @@ "asset": "sfw-linux-x86_64", "integrity": "sha512-lu9h8UzDZt34gdCEVHBGW6goE1Ayykq413EovV5B4nG7jBK27mI0GQstzVbWXA3wWaweT39PehXGtVpdqIDGSA==" }, - "win-x64": { + "win32-x64": { "asset": "sfw-windows-x86_64.exe", "integrity": "sha512-URZXauIsdUT12E2KTc4sfsxRmJm7nRJzAgM+IYGX4Xq+X0cl/eAbH5SpYIJKpsnW9csSztW9ceyPhlM+f3neIQ==" } - } + }, + "origin": "gh-asset" }, "zizmor": { "description": "GitHub Actions security linter — audits .github/ for workflow-injection / credential-leak patterns.", "version": "1.26.1", "repository": "github:zizmorcore/zizmor", - "release": "asset", "notes": [ "Required: CI (blocks merges on medium+ findings)", "Installed by the setup-and-install composite; SRI-verified (sha512) per platform" @@ -147,23 +146,26 @@ "asset": "zizmor-x86_64-unknown-linux-gnu.tar.gz", "integrity": "sha512-zTMERMDd3JfaRX12klj2fhZGDyrXeLkVUY1QJkCv8RRmAa9uVuH88gHOnv4CQtC5hXS7FPRJzvCVOnw38gV83g==" }, - "win-x64": { + "win32-x64": { "asset": "zizmor-x86_64-pc-windows-msvc.zip", "integrity": "sha512-Pijh/CrrOAkZzLiTr2LTHdI8d6+5Ql6B+suY6fXVmL8UVa+4Q36hHL5K67iRFz03v/V/UcrY6+dfhnmot/xfww==" } - } + }, + "origin": "gh-asset" }, "agentshield": { "description": "Claude AI config security scanner (prompt injection, secrets)", "purl": "pkg:npm/ecc-agentshield@1.4.0", - "integrity": "sha512-R98OO1Ujyk2lezDLb+iQmMhF6FwTJCHajy3G4FCB6x7wkSTqR9f8+eAelC5KDzYDsGSbc0sOZvjXOOPRBtMpDg==" + "integrity": "sha512-R98OO1Ujyk2lezDLb+iQmMhF6FwTJCHajy3G4FCB6x7wkSTqR9f8+eAelC5KDzYDsGSbc0sOZvjXOOPRBtMpDg==", + "origin": "npm" }, "skillspector": { "description": "NVIDIA's third-party-skill security scanner (LangGraph-based; YARA + AST + OSV.dev CVE lookups + optional LLM analysis). No PyPI release / no GH tags upstream — pinned to a git SHA on main + installed via a locked uv project (pyproject.toml + uv.lock, `uv sync --locked`; the fleet uv pin + exclude-newer make it reproducible). Sibling to AgentShield: AgentShield audits the operator's .claude/ config; SkillSpector audits untrusted upstream skills before install.", - "release": "uv-project", "repository": "github:NVIDIA/skillspector", "version": "2eb84478", - "versionDate": "2026-05-18" + "versionDate": "2026-05-18", + "origin": "manager", + "manager": "uv" } } } diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index a54954641..7e51cdda4 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -59,7 +59,8 @@ interface ToolPin { description?: string version?: string repository?: string - release?: string + origin?: string + manager?: string binaryName?: string purl?: string integrity?: string @@ -72,7 +73,7 @@ function loadTools(): Record { } function platformKey(): string { - const osKey = { darwin: 'darwin', linux: 'linux', win32: 'win' }[process.platform] + const osKey = { darwin: 'darwin', linux: 'linux', win32: 'win32' }[process.platform] const archKey = { arm64: 'arm64', x64: 'x64' }[process.arch] if (!osKey || !archKey) { throw new Error(`unsupported platform ${process.platform}-${process.arch}`) @@ -94,7 +95,7 @@ export function checkPins(tools: Record): string[] { ...(pin.integrity ? [pin.integrity] : []), ...Object.values(pin.platforms ?? {}).map(p => p.integrity), ] - if (pin.release === 'asset' && integrities.length === 0) { + if (pin.origin === 'gh-asset' && integrities.length === 0) { out.push(`${name}: release asset without any integrity pin`) } for (const sri of integrities) { @@ -403,7 +404,7 @@ export async function installTool(name: string, tools: Record): if (!pin) { throw new Error(`unknown tool ${name} (see external-tools.json)`) } - if (pin.release === 'asset') { + if (pin.origin === 'gh-asset') { await installAssetTool(name, pin) return } @@ -424,15 +425,28 @@ export async function installTool(name: string, tools: Record): await installNpmTarball(name, pin.repository.slice('npm:'.length), pin.version!, pin.integrity!) return } - if (pin.release === 'uv-project') { - // Git-SHA-pinned python project; not auto-installed (needs uv). + if (pin.origin === 'npm') { + // npm registry tarball named only by `origin`. Pins that carry a `purl` + // or an `npm:` repository are already claimed by the branches above, so + // the package name here is always the tool name. + const pkg = name + if (!pin.version || !pin.integrity) { + throw new Error(`${name}: npm pin missing version or integrity`) + } + await installNpmTarball(name, pkg, pin.version, pin.integrity) + return + } + if (pin.origin === 'manager') { + // Git-SHA-pinned python project installed via an external package + // manager (uv); not auto-installed here. const repo = pin.repository!.replace(/^github:/, '') + const mgr = pin.manager ?? 'uv' console.log( - `[external-tools] ${name} is a uv project — run: uvx --from git+https://github.com/${repo}@${pin.version} ${name}`, + `[external-tools] ${name} is a ${mgr} project — run: uvx --from git+https://github.com/${repo}@${pin.version} ${name}`, ) return } - throw new Error(`${name}: no installable shape (release=${pin.release ?? 'none'})`) + throw new Error(`${name}: no installable shape (origin=${pin.origin ?? 'none'})`) } /** diff --git a/scripts/soak/external-tools.test.mts b/scripts/soak/external-tools.test.mts index c24426ec7..a4ec8992d 100644 --- a/scripts/soak/external-tools.test.mts +++ b/scripts/soak/external-tools.test.mts @@ -29,7 +29,7 @@ test('the repo external-tools.json passes checkPins', () => { test('checkPins flags missing pins, bad SRIs, and asset entries with no integrity', () => { assert.equal(checkPins({ a: {} }).length, 1) assert.equal(checkPins({ a: { version: '1.0.0', integrity: 'sha256-abc' } }).length, 1) - assert.equal(checkPins({ a: { version: '1.0.0', release: 'asset' } }).length, 1) + assert.equal(checkPins({ a: { version: '1.0.0', origin: 'gh-asset' } }).length, 1) }) test('checkPins validates soakBypass dates, arithmetic, and expiry', () => { @@ -174,7 +174,7 @@ test('installTool rejects unknown tools, foreign purls, and shapeless pins', asy ) await assert.rejects(installTool('y', { y: { version: '1.0.0' } }), /no installable shape/) await assert.rejects( - installTool('z', { z: { release: 'asset', version: '1.0.0', platforms: {} } }), + installTool('z', { z: { origin: 'gh-asset', version: '1.0.0', platforms: {} } }), /no pinned asset for/, ) }) @@ -194,10 +194,10 @@ test('installTool resolves the sfw flavor from SOCKET_SECURITY_KEY', async t => ) }) -test('installTool prints the uvx line for uv-project pins without installing', async () => { +test('installTool prints the uvx line for manager pins without installing', async () => { const tools = JSON.parse(readFileSync(EXTERNAL_TOOLS_JSON, 'utf8')).tools - const uv = Object.keys(tools).find(name => tools[name].release === 'uv-project') - assert.ok(uv, 'the manifest is expected to pin at least one uv project') + const uv = Object.keys(tools).find(name => tools[name].origin === 'manager') + assert.ok(uv, 'the manifest is expected to pin at least one manager project') await installTool(uv!, tools) })