From 254a9c4dacab6fd8c08b5f45ee93a520e6481e22 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 13:11:18 -0400 Subject: [PATCH 01/29] build: pin rust 1.95.0 toolchain and bump msrv to 1.95 The pin is exact so the PGO/BOLT flow (mise run build:pgo, release upload-assets-pgo) instruments and optimizes with one toolchain and the collected profile matches the LLVM version that consumes it. llvm-tools-preview in components removes the manual 'rustup component add llvm-tools-preview' setup step. MSRV moves to 1.95 for core::hint::cold_path(). --- .rules | 6 +++--- Cargo.toml | 2 +- rust-toolchain.toml | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/.rules b/.rules index da4a6197c..a0f29d560 100644 --- a/.rules +++ b/.rules @@ -3,7 +3,7 @@ project: kind: Node.js package manager, Rust workspace binary_surface: drop-in for pnpm CLI language: Rust 2024 - msrv: 1.93 + msrv: 1.95 (pinned exact via rust-toolchain.toml so PGO instrument+use share one LLVM) async_runtime: tokio (multi-thread for install, current-thread for nested islands) errors: miette (user-facing, top-level), thiserror (per-library crate) error_codes: every error/warning carries a stable ERR_AUBE_*/WARN_AUBE_* identifier from aube-codes crate; bespoke exit codes for a curated subset; see error_codes section below for conventions @@ -187,7 +187,7 @@ testing: rust: critical[5]: - - edition 2024, latest stable toolchain, msrv 1.93 (rust 1.90 default-LLD-on-Linux + workspace cargo publish available) + - edition 2024, toolchain pinned to the exact stable in rust-toolchain.toml, msrv 1.95 (core::hint::cold_path stable; rust 1.90 default-LLD-on-Linux + workspace cargo publish available) - no .unwrap()/.expect() in public library APIs or cross-crate boundaries. local-invariant unwraps where panic is genuinely impossible are ok, annotate with WHY - crates/aube-*/ (libraries) use tracing (info!/warn!/error!/debug!/trace!). crates/aube/src/commands/*.rs CLI command impls may print to stdout via println!/writeln! when the whole point is CLI output (view, list, outdated, why, cat-index, etc.) - cargo clippy -- -D warnings must pass with zero warnings @@ -202,7 +202,7 @@ rust: - shared reqwest::Client with pool, never one per request (aube-registry/src/client.rs does this right) - DashMap for concurrent maps, OnceLock/LazyLock for init-once globals, rayon par_iter for CPU-bound parallel passes off the tokio runtime (never spawn tokio tasks for pure CPU) - pre-size Vec/HashMap when bounds are known; SmallVec<[T; N]> for hot paths with known-small size; Arc/Box over String when immutable - - aube already adopts mimalloc (global allocator), sonic_rs (packument decode), BLAKE3 (non-crypto hashing). these are settled; don't re-propose them + - aube already adopts mimalloc (global allocator), sonic_rs (packument decode), BLAKE3 (non-crypto hashing), core::hint::cold_path() on rare arms of hot install loops (linker materialize, pnpm subset parser, semver caches, tar walk). these are settled; don't re-propose them async[7]: - tokio multi-thread runtime for install (main.rs:586), nested current_thread islands inside spawn_blocking are fine - never block in async fn, offload via spawn_blocking (IO that can't be async, tarball decode, tar extract) diff --git a/Cargo.toml b/Cargo.toml index 8d6fc1b97..e36c6907b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ codegen-units = 1 [workspace.package] version = "1.26.0" edition = "2024" -rust-version = "1.93" +rust-version = "1.95" license = "MIT" repository = "https://github.com/jdx/aube" homepage = "https://github.com/jdx/aube" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..ffaec3a2f --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,8 @@ +# Pinned STABLE, exact. cold_path() needs 1.95+; the pin is exact so the +# whole PGO/BOLT flow (mise run build:pgo, release upload-assets-pgo) +# instruments and optimizes with one toolchain and the collected profile +# matches the LLVM version that consumes it. llvm-tools-preview ships the +# llvm-profdata that benchmarks/pgo.bash otherwise requires by hand. +[toolchain] +channel = "1.95.0" +components = ["rustfmt", "clippy", "llvm-tools-preview"] From c99106d24d1eca35e4da282e4caa7d74d1286b27 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 13:11:18 -0400 Subject: [PATCH 02/29] perf: mark rare branches in hot install loops with cold_path() core::hint::cold_path() (stable since rust 1.95) annotates the rare arm so LLVM sinks it off the hot path. Sites: the per-file linker materialize loop's reflink/hardlink fallbacks and link-error return, the pnpm-lock byte-cursor subset parser's decline-to-serde bails, the thread-local semver range/version cache misses, and the per-entry tar walk's validation rejects. x86_64 Linux release binaries get PGO, but macOS/arm64/Windows ship plain release where static hints are the only branch-layout signal, and hints cover paths the hermetic PGO training run does not exercise. --- crates/aube-linker/src/materialize.rs | 3 +++ crates/aube-lockfile/src/pnpm/subset.rs | 28 +++++++++++++++++++++---- crates/aube-resolver/src/semver_util.rs | 2 ++ crates/aube-store/src/tarball.rs | 4 ++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/aube-linker/src/materialize.rs b/crates/aube-linker/src/materialize.rs index 60f166ca6..ef6e7312b 100644 --- a/crates/aube-linker/src/materialize.rs +++ b/crates/aube-linker/src/materialize.rs @@ -561,6 +561,7 @@ impl Linker { let target = pkg_nm_dir.join(rel_path); if let Err(e) = self.link_file_fresh(stored, rel_path, &target) { + core::hint::cold_path(); if let Error::MissingStoreFile { .. } = &e { invalidate_stale_index_for_package(&self.store, pkg); } @@ -785,6 +786,7 @@ impl Linker { } }; if let Err(e) = reflink_result { + core::hint::cold_path(); // Source-missing short-circuit avoids the misleading // "fell back to copy" trace and the redundant copy // attempt that would just ENOENT for the same reason. @@ -840,6 +842,7 @@ impl Linker { } LinkStrategy::Hardlink => { if let Err(e) = std::fs::hard_link(&stored.store_path, dst) { + core::hint::cold_path(); if !stored.store_path.exists() { return Err(missing_source()); } diff --git a/crates/aube-lockfile/src/pnpm/subset.rs b/crates/aube-lockfile/src/pnpm/subset.rs index 41fc67d80..9090b6d74 100644 --- a/crates/aube-lockfile/src/pnpm/subset.rs +++ b/crates/aube-lockfile/src/pnpm/subset.rs @@ -224,7 +224,10 @@ impl<'a> Parser<'a> { } // Any top-level key we don't recognize: bail to serde so // we never silently drop a field a future pnpm adds. - _ => return None, + _ => { + core::hint::cold_path(); + return None; + } } } @@ -304,6 +307,7 @@ impl<'a> Parser<'a> { break; } if line.indent != min_indent { + core::hint::cold_path(); return None; } let (k, v) = split_key(line.body)?; @@ -350,6 +354,7 @@ impl<'a> Parser<'a> { break; } if line.indent != 2 { + core::hint::cold_path(); return None; } // `:` header. pnpm writes an empty importer @@ -395,7 +400,10 @@ impl<'a> Parser<'a> { b"devDependencies" => dev_dependencies = Some(specs), b"optionalDependencies" => optional_dependencies = Some(specs), b"skippedOptionalDependencies" => skipped_optional_dependencies = Some(specs), - _ => return None, + _ => { + core::hint::cold_path(); + return None; + } } } Some(RawImporter { @@ -415,6 +423,7 @@ impl<'a> Parser<'a> { break; } if line.indent != entry_indent { + core::hint::cold_path(); return None; } let (name, rest) = split_key(line.body)?; @@ -432,6 +441,7 @@ impl<'a> Parser<'a> { break; } if c.indent != child_indent { + core::hint::cold_path(); return None; } let (ck, cv) = split_key(c.body)?; @@ -440,7 +450,10 @@ impl<'a> Parser<'a> { match ck { b"specifier" => specifier = Some(scalar_string(cv)?), b"version" => version = Some(scalar_string(cv)?), - _ => return None, + _ => { + core::hint::cold_path(); + return None; + } } } map.insert( @@ -468,6 +481,7 @@ impl<'a> Parser<'a> { break; } if line.indent != 2 { + core::hint::cold_path(); return None; } let (key, rest) = split_key(line.body)?; @@ -510,6 +524,7 @@ impl<'a> Parser<'a> { break; } if line.indent != 2 { + core::hint::cold_path(); return None; } let (key, rest) = split_key(line.body)?; @@ -540,6 +555,7 @@ impl<'a> Parser<'a> { break; } if line.indent != indent { + core::hint::cold_path(); return None; } let (key, rest) = split_key(line.body)?; @@ -560,7 +576,10 @@ impl<'a> Parser<'a> { b"transitivePeerDependencies" => { transitive_peer_dependencies = Some(self.parse_seq_value(rest, indent + 2)?); } - _ => return None, + _ => { + core::hint::cold_path(); + return None; + } } } Some(RawSnapshot { @@ -637,6 +656,7 @@ fn split_key(body: &[u8]) -> Option<(&[u8], Option<&[u8]>)> { } i += 1; } + core::hint::cold_path(); None } diff --git a/crates/aube-resolver/src/semver_util.rs b/crates/aube-resolver/src/semver_util.rs index 4dc152fb4..a5b829ec3 100644 --- a/crates/aube-resolver/src/semver_util.rs +++ b/crates/aube-resolver/src/semver_util.rs @@ -353,6 +353,7 @@ fn with_cached_range(range_str: &str, f: impl FnOnce(Option<&node_semver::Ran CACHE.with(|cell| { let mut map = cell.borrow_mut(); if !map.contains_key(range_str) { + core::hint::cold_path(); let parsed = node_semver::Range::parse(range_str).ok(); map.insert(range_str.to_string(), parsed); } @@ -371,6 +372,7 @@ fn with_cached_version(version: &str, f: impl FnOnce(Option<&node_semver::Ver CACHE.with(|cell| { let mut map = cell.borrow_mut(); if !map.contains_key(version) { + core::hint::cold_path(); let parsed = node_semver::Version::parse(version).ok(); map.insert(version.to_string(), parsed); } diff --git a/crates/aube-store/src/tarball.rs b/crates/aube-store/src/tarball.rs index 17ca6a3ed..8d5f03170 100644 --- a/crates/aube-store/src/tarball.rs +++ b/crates/aube-store/src/tarball.rs @@ -188,6 +188,7 @@ impl Store { for entry in archive.entries().map_err(|e| Error::Tar(e.to_string()))? { entries_seen += 1; if entries_seen > MAX_TARBALL_ENTRIES { + core::hint::cold_path(); return Err(Error::Tar(format!( "tarball exceeds entry cap of {MAX_TARBALL_ENTRIES}" ))); @@ -226,6 +227,7 @@ impl Store { entry_type, tar::EntryType::Regular | tar::EntryType::Continuous ) { + core::hint::cold_path(); return Err(Error::Tar(format!( "tarball entry type {entry_type:?} is not allowed" ))); @@ -240,6 +242,7 @@ impl Store { .size() .map_err(|e| Error::Tar(e.to_string()))?; if declared > MAX_TARBALL_ENTRY_BYTES { + core::hint::cold_path(); return Err(Error::Tar(format!( "tarball entry exceeds per-entry cap: {declared} bytes > {MAX_TARBALL_ENTRY_BYTES}" ))); @@ -270,6 +273,7 @@ impl Store { // non-empty stream. Synthetic-entry injection: header // claims empty file, real bytes go to disk. if declared == 0 && !content.is_empty() { + core::hint::cold_path(); return Err(Error::Tar(format!( "tarball entry declared 0 bytes but yielded {} bytes", content.len() From c27c5fece1c4451fc87bac1bc28b3b9d764f4930 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 13:26:00 -0400 Subject: [PATCH 03/29] build: switch toolchain pin to nightly-2026-07-04 (7-day soak) Adopt the newest nightly at least 7 days old as of 2026-07-11 and bump the pin under the same soak rule. rust-version stays 1.95 as the stable msrv floor. Verified on nightly-2026-07-04: clippy --all-targets -D warnings clean, fmt --check clean. --- .rules | 4 ++-- rust-toolchain.toml | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.rules b/.rules index a0f29d560..69375c3d3 100644 --- a/.rules +++ b/.rules @@ -3,7 +3,7 @@ project: kind: Node.js package manager, Rust workspace binary_surface: drop-in for pnpm CLI language: Rust 2024 - msrv: 1.95 (pinned exact via rust-toolchain.toml so PGO instrument+use share one LLVM) + msrv: 1.95 (stable floor; builds use the dated nightly pinned in rust-toolchain.toml, adopted after a 7-day soak, so PGO instrument+use share one LLVM) async_runtime: tokio (multi-thread for install, current-thread for nested islands) errors: miette (user-facing, top-level), thiserror (per-library crate) error_codes: every error/warning carries a stable ERR_AUBE_*/WARN_AUBE_* identifier from aube-codes crate; bespoke exit codes for a curated subset; see error_codes section below for conventions @@ -187,7 +187,7 @@ testing: rust: critical[5]: - - edition 2024, toolchain pinned to the exact stable in rust-toolchain.toml, msrv 1.95 (core::hint::cold_path stable; rust 1.90 default-LLD-on-Linux + workspace cargo publish available) + - edition 2024, toolchain pinned to a dated nightly in rust-toolchain.toml (adopt the newest nightly >=7 days old, same soak rule for bumps), msrv 1.95 stable floor (core::hint::cold_path stable; rust 1.90 default-LLD-on-Linux + workspace cargo publish available) - no .unwrap()/.expect() in public library APIs or cross-crate boundaries. local-invariant unwraps where panic is genuinely impossible are ok, annotate with WHY - crates/aube-*/ (libraries) use tracing (info!/warn!/error!/debug!/trace!). crates/aube/src/commands/*.rs CLI command impls may print to stdout via println!/writeln! when the whole point is CLI output (view, list, outdated, why, cat-index, etc.) - cargo clippy -- -D warnings must pass with zero warnings diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ffaec3a2f..042efd083 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,8 +1,11 @@ -# Pinned STABLE, exact. cold_path() needs 1.95+; the pin is exact so the -# whole PGO/BOLT flow (mise run build:pgo, release upload-assets-pgo) -# instruments and optimizes with one toolchain and the collected profile -# matches the LLVM version that consumes it. llvm-tools-preview ships the -# llvm-profdata that benchmarks/pgo.bash otherwise requires by hand. +# Pinned dated NIGHTLY, adopted under a 7-day soak: the pin is the newest +# nightly at least 7 days old on the adoption date (2026-07-11), and bumps +# follow the same rule. The pin is exact so the whole PGO/BOLT flow +# (mise run build:pgo, release upload-assets-pgo) instruments and optimizes +# with one toolchain and the collected profile matches the LLVM version +# that consumes it. llvm-tools-preview ships the llvm-profdata that +# benchmarks/pgo.bash otherwise requires by hand. rust-version in +# Cargo.toml stays the STABLE msrv floor (1.95, core::hint::cold_path). [toolchain] -channel = "1.95.0" +channel = "nightly-2026-07-04" components = ["rustfmt", "clippy", "llvm-tools-preview"] From 297e659b0d46ab8a12f2ea123b37018ed71490fe Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 13:32:58 -0400 Subject: [PATCH 04/29] build: enable cargo min-publish-age with a 7-day soak Crate versions published less than 7 days ago are pubtime-incompatible and the resolver skips them unless already in Cargo.lock. Unstable cargo feature, active via the nightly pinned in rust-toolchain.toml; stable cargo ignores the [unstable] table. Verified on nightly-2026-07-04: cargo update --dry-run holds back ~25 fresh versions vs a zero-age run and annotates the held ones (e.g. 'available: v0.1.8, published 3 days ago'). --- .cargo/config.toml | 12 ++++++++++++ .rules | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 77940a30b..f377b0114 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -15,3 +15,15 @@ # (7.07 s → 9.57 s) — acceptable given the safety guarantee. [env] RUST_TEST_THREADS = "1" + +# Supply-chain soak for cargo's own dependency resolution — the same +# 7-day rule aube offers npm users via minimumReleaseAge: crate versions +# published less than 7 days ago are pubtime-incompatible and the +# resolver skips them unless already in Cargo.lock +# (resolver.incompatible-publish-age defaults to "deny"). The unstable +# feature is active because rust-toolchain.toml pins a nightly. +[unstable] +min-publish-age = true + +[registry] +global-min-publish-age = "7 days" diff --git a/.rules b/.rules index 69375c3d3..43450748a 100644 --- a/.rules +++ b/.rules @@ -234,9 +234,10 @@ deps: policy: minimize, prefer std over external, every added dep must justify >=20 LOC saved tls: rustls (no OpenSSL / native-tls) reqwest_features: minimal (rustls-tls + http2 + json + stream + charset + system-proxy) - supply_chain[2]: + supply_chain[3]: - cargo audit: CVE database scan (RustSec advisories) before every release, required in CI - cargo deny: license policy, banned crates, duplicate detection, approved sources. deny.toml checked in + - cargo min-publish-age: 7-day soak on new crate versions (.cargo/config.toml [registry] global-min-publish-age; unstable feature, active via the pinned nightly). resolver skips versions younger than the window unless already locked git: forbidden[2]: From 8e4684da7989792aa3d2be9033c49e77742969a6 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 13:37:45 -0400 Subject: [PATCH 05/29] ci: pre-bake pinned nightly and 1.95 msrv floor in buildkite image The agent image installed stable + 1.93.0 while rust-toolchain.toml now pins nightly-2026-07-04; pre-install the pinned toolchain (with llvm-tools-preview for PGO) and the 1.95 msrv floor so jobs don't rustup-download at job time. --- .buildkite/linux-agent.Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.buildkite/linux-agent.Dockerfile b/.buildkite/linux-agent.Dockerfile index 6cd69f115..689e3fa64 100644 --- a/.buildkite/linux-agent.Dockerfile +++ b/.buildkite/linux-agent.Dockerfile @@ -18,9 +18,13 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* RUN curl https://mise.run | sh +# Pre-bake the repo's pinned toolchain (rust-toolchain.toml) with the +# llvm-tools-preview the PGO flow needs, plus the 1.95 msrv floor, so +# agent jobs don't rustup-download at job time. RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile default --default-toolchain stable \ - && /root/.cargo/bin/rustup toolchain install 1.93.0 --profile default \ + && /root/.cargo/bin/rustup toolchain install nightly-2026-07-04 --profile default -c llvm-tools-preview \ + && /root/.cargo/bin/rustup toolchain install 1.95.0 --profile default \ && /root/.cargo/bin/rustup component add rustfmt clippy --toolchain stable ENV PATH="/root/.cargo/bin:/root/.local/bin:/root/.local/share/mise/shims:${PATH}" From 5a228bc029bfc45b294354d16daa4b89c0cc71bb Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:06:36 -0400 Subject: [PATCH 06/29] build: add 7-day soak manager, taze via catalog, pinned external tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/soak/constants.mts is the ONE source for the release-age window; soak.mts parity-checks and fixes every surface (.cargo min-publish-age, docs/pnpm-workspace.yaml minimumReleaseAge in minutes, docs/.npmrc min-release-age in days, taze maturityPeriod via direct import). Version-pinned soak excludes require a '# published: | removable:' date annotation and expired pins are pruned by --fix. update-deps.mts bumps npm (taze) and cargo through the same window, refusing a non-rustup cargo that would silently skip the [unstable] soak. external-tools.json pins sfw/zizmor/agentshield/ skillspector exact with sha512 SRI; external-tools.mts is the only install path. taze lands through the docs/pnpm-workspace.yaml catalog: (kept in docs/ — a root workspace yaml would re-anchor the docs install), resolved by aube itself into aube-lock.yaml. --- .rules | 15 +- docs/.gitignore | 1 - docs/.npmrc | 4 + docs/aube-lock.yaml | 209 ++++++++++++++++++++ docs/package.json | 1 + docs/pnpm-workspace.yaml | 20 ++ docs/taze.config.mts | 14 ++ external-tools.json | 115 +++++++++++ mise.toml | 15 ++ scripts/soak/constants.mts | 42 ++++ scripts/soak/external-tools.mts | 316 ++++++++++++++++++++++++++++++ scripts/soak/soak.mts | 328 ++++++++++++++++++++++++++++++++ scripts/soak/update-deps.mts | 91 +++++++++ 13 files changed, 1169 insertions(+), 2 deletions(-) create mode 100644 docs/.npmrc create mode 100644 docs/pnpm-workspace.yaml create mode 100644 docs/taze.config.mts create mode 100644 external-tools.json create mode 100644 scripts/soak/constants.mts create mode 100644 scripts/soak/external-tools.mts create mode 100644 scripts/soak/soak.mts create mode 100644 scripts/soak/update-deps.mts diff --git a/.rules b/.rules index 43450748a..d26f815ba 100644 --- a/.rules +++ b/.rules @@ -234,10 +234,23 @@ deps: policy: minimize, prefer std over external, every added dep must justify >=20 LOC saved tls: rustls (no OpenSSL / native-tls) reqwest_features: minimal (rustls-tls + http2 + json + stream + charset + system-proxy) - supply_chain[3]: + supply_chain[4]: - cargo audit: CVE database scan (RustSec advisories) before every release, required in CI - cargo deny: license policy, banned crates, duplicate detection, approved sources. deny.toml checked in - cargo min-publish-age: 7-day soak on new crate versions (.cargo/config.toml [registry] global-min-publish-age; unstable feature, active via the pinned nightly). resolver skips versions younger than the window unless already locked + - external tools: sfw/zizmor/agentshield/skillspector pinned exact with sha512 SRI in external-tools.json; installed/verified only via scripts/soak/external-tools.mts (mise run tools:install / tools:check) + +soak: + window: SOAK_DAYS=7 in scripts/soak/constants.mts — the ONE source; every surface derives or is parity-checked against it + surfaces[5]: + - .cargo/config.toml global-min-publish-age ("7 days") + - rust-toolchain.toml dated nightly (adopt only nightlies >= 7 days old) + - docs/pnpm-workspace.yaml minimumReleaseAge (10080 minutes; also the taze catalog: home — aube reads this file for docs installs, and a ROOT pnpm-workspace.yaml would re-anchor the docs install at repo root, so it must stay in docs/) + - docs/.npmrc min-release-age (7 days, npm >= 11.17 parity) + - docs/taze.config.mts maturityPeriod (imports SOAK_DAYS) + manager: mise run soak (check, CI-gated in test-linux) / soak:fix (syncs windows, prunes expired excludes) / deps:update (taze + cargo update, both soaked) + excludes: version-pinned minimumReleaseAgeExclude entries REQUIRE `# published: YYYY-MM-DD | removable: YYYY-MM-DD` on the line above (removable = published + 7d); bare names/globs are standing trust and need no annotation; expired pins are pruned by soak:fix + socket_firewall: CI installs sfw (free tier keyless; enterprise auto-selected when the SOCKET_SECURITY_KEY repo secret lands, fed to SOCKET_API_KEY/SOCKET_API_TOKEN) and shims npm/yarn/pnpm/pip/uv/cargo via scripts/soak/external-tools.mts --shims; the buildkite agent image pre-bakes the same binary + shims git: forbidden[2]: diff --git a/docs/.gitignore b/docs/.gitignore index 1b198bcb9..1b7b7a493 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -2,4 +2,3 @@ node_modules .vitepress/dist .vitepress/cache .pnpm-store -pnpm-workspace.yaml diff --git a/docs/.npmrc b/docs/.npmrc new file mode 100644 index 000000000..81cf986d0 --- /dev/null +++ b/docs/.npmrc @@ -0,0 +1,4 @@ +# npm-cli parity for the soak window (npm >= 11.17, DAYS). aube reads the +# workspace yaml (minutes) which takes precedence; this covers stray npm/npx +# usage in docs/. Managed by scripts/soak/soak.mts. +min-release-age=7 diff --git a/docs/aube-lock.yaml b/docs/aube-lock.yaml index 9943f8f71..e72d6536d 100644 --- a/docs/aube-lock.yaml +++ b/docs/aube-lock.yaml @@ -4,6 +4,12 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + taze: + specifier: 19.14.1 + version: 19.14.1 + importers: .: @@ -15,6 +21,9 @@ importers: sharp: specifier: ^0.35.0 version: 0.35.2 + taze: + specifier: 'catalog:' + version: 19.14.1 vitepress: specifier: ^1.5.0 version: 1.6.4(postcss@8.5.15) @@ -97,6 +106,11 @@ packages: resolution: {integrity: sha512-Yyyne4l//vDSdg4MhYJkaVne+KEPi833eCj3/T/87ernTwrvP6j9biXXZELsN8sLI/f2ndV/vugDIy2jdJQB6g==} engines: {node: '>= 14.0.0'} + '@antfu/ni@30.2.0': + resolution: {integrity: sha512-/FOdAP1w8COnANVD3TtNj/tnpt/36RkU/ysKZTqx86x9acdhCqTFjDXNYVDyBg6UzcrTwWPUeY75ng7CWLNr+g==} + engines: {node: '>=20.19.0'} + hasBin: true + '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -161,6 +175,9 @@ packages: cpu: [x64] os: [linux] + '@henrygd/queue@1.2.0': + resolution: {integrity: sha512-jW/BLSTpcvExDhqJGxtIPgGr2O0IFF8XUNDwEbfCfhrXT8a4xztQ9Lv6U/vbYzYC0xVWn+3zv6YnLUh3bEFUKA==} + '@iconify-json/simple-icons@1.2.87': resolution: {integrity: sha512-8YciStObhSji3OZFmWAWK6kBujyqO5bLCxeDwLxf3CR3F4PVelq7keC2LBvgTqviWzSTysj5/g4PCFLiAMVGsw==} @@ -248,6 +265,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@rollup/rollup-darwin-arm64@4.62.2': resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} cpu: [arm64] @@ -435,6 +455,10 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -454,10 +478,16 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -480,6 +510,15 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + focus-trap@7.8.0: resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} @@ -488,6 +527,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fzf@0.5.2: + resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -504,6 +546,10 @@ packages: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -528,6 +574,10 @@ packages: micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + minisearch@7.2.0: resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} @@ -539,15 +589,38 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + package-manager-detector@1.7.0: + resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pnpm-workspace-yaml@1.6.1: + resolution: {integrity: sha512-yTeZntGWi8m9WNuhoVsP0DpFc4sC1U0+rr/qR6Zi9n2g3sxXY+JfccjXjjruNz96tM8I09yaJUA86doRnNLkbg==} + postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} @@ -558,6 +631,9 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -567,6 +643,10 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -590,6 +670,10 @@ packages: shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -611,9 +695,30 @@ packages: tabbable@6.5.0: resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + taze@19.14.1: + resolution: {integrity: sha512-+wf/IqGReU68vBE/iJ7JCuV5QeD6zQBp9MI6YphN7bT2vf/YIHd0oVA4AJiX3uANI1hQY58MrVmDwLv0x/q3BA==} + hasBin: true + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -686,6 +791,11 @@ packages: typescript: optional: true + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -796,6 +906,13 @@ snapshots: dependencies: '@algolia/client-common': 5.55.0 + '@antfu/ni@30.2.0': + dependencies: + fzf: 0.5.2 + package-manager-detector: 1.7.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} @@ -840,6 +957,8 @@ snapshots: '@esbuild/linux-x64@0.21.5': optional: true + '@henrygd/queue@1.2.0': {} + '@iconify-json/simple-icons@1.2.87': dependencies: '@iconify/types': 2.0.0 @@ -898,6 +1017,10 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + '@rollup/rollup-darwin-arm64@4.62.2': optional: true @@ -1103,6 +1226,8 @@ snapshots: birpc@2.9.0: {} + cac@7.0.0: {} + ccount@2.0.1: {} character-entities-html4@2.1.0: {} @@ -1117,8 +1242,12 @@ snapshots: csstype@3.2.3: {} + defu@6.1.7: {} + dequal@2.0.3: {} + destr@2.0.5: {} + detect-libc@2.1.2: {} devlop@1.1.0: @@ -1138,6 +1267,10 @@ snapshots: estree-walker@2.0.2: {} + fdir@6.5.0(picomatch@4.0.5): + dependencies: + picomatch: 4.0.5 + focus-trap@7.8.0: dependencies: tabbable: 6.5.0 @@ -1145,6 +1278,8 @@ snapshots: fsevents@2.3.3: optional: true + fzf@0.5.2: {} + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -1169,6 +1304,8 @@ snapshots: is-what@5.5.0: {} + jiti@2.7.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1204,22 +1341,46 @@ snapshots: micromark-util-types@2.0.2: {} + mimic-function@5.0.1: {} + minisearch@7.2.0: {} mitt@3.0.1: {} nanoid@3.3.15: {} + node-fetch-native@1.6.7: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + oniguruma-to-es@3.1.1: dependencies: emoji-regex-xs: 1.0.0 regex: 6.1.0 regex-recursion: 6.0.2 + package-manager-detector@1.7.0: {} + + pathe@2.0.3: {} + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} + picomatch@4.0.5: {} + + pnpm-workspace-yaml@1.6.1: + dependencies: + yaml: 2.9.0 + postcss@8.5.15: dependencies: nanoid: 3.3.15 @@ -1230,6 +1391,8 @@ snapshots: property-information@7.2.0: {} + quansync@1.0.0: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -1240,6 +1403,11 @@ snapshots: dependencies: regex-utilities: 2.3.0 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + rfdc@1.4.1: {} rollup@4.62.2: @@ -1288,6 +1456,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} space-separated-tokens@2.0.2: {} @@ -1305,8 +1475,45 @@ snapshots: tabbable@6.5.0: {} + taze@19.14.1: + dependencies: + '@antfu/ni': 30.2.0 + '@henrygd/queue': 1.2.0 + cac: 7.0.0 + ofetch: 1.5.1 + package-manager-detector: 1.7.0 + pathe: 2.0.3 + pnpm-workspace-yaml: 1.6.1 + restore-cursor: 5.1.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + unconfig: 7.5.0 + yaml: 2.9.0 + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + trim-lines@3.0.1: {} + ufo@1.6.4: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.7 + jiti: 2.7.0 + quansync: 1.0.0 + unconfig-core: 7.5.0 + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -1403,4 +1610,6 @@ snapshots: '@vue/server-renderer': 3.5.38(vue@3.5.38) '@vue/shared': 3.5.38 + yaml@2.9.0: {} + zwitch@2.0.4: {} diff --git a/docs/package.json b/docs/package.json index dd4bd5b68..4c698d13d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "sharp": "^0.35.0", + "taze": "catalog:", "vitepress": "^1.5.0" } } diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml new file mode 100644 index 000000000..cebb8ca07 --- /dev/null +++ b/docs/pnpm-workspace.yaml @@ -0,0 +1,20 @@ +# Workspace config for the docs package — also the npm-side soak + catalog +# surface aube itself reads when installing docs deps (minimumReleaseAge in +# MINUTES; workspace yaml wins over .npmrc). Lives in docs/ on purpose: a +# repo-root pnpm-workspace.yaml would re-anchor the docs install (lockfile + +# virtual store) at the repo root. +# +# Managed by scripts/soak/soak.mts (`mise run soak` / `mise run soak:fix`). + +catalog: + taze: 19.14.1 + +# Wait 7 days (10080 minutes) before installing newly published packages. +minimumReleaseAge: 10080 + +# Exclusions: bare names / scope globs express standing trust; version pins +# are dated soak bypasses and REQUIRE, on the line above: +# # published: YYYY-MM-DD | removable: YYYY-MM-DD (removable = published + 7d) +# `mise run soak` rejects unannotated or expired pins; `soak:fix` prunes +# expired ones. +# minimumReleaseAgeExclude: [] diff --git a/docs/taze.config.mts b/docs/taze.config.mts new file mode 100644 index 000000000..f9ba42458 --- /dev/null +++ b/docs/taze.config.mts @@ -0,0 +1,14 @@ +import { defineConfig } from 'taze' + +// Cooldown derives from the canonical SOAK_DAYS so it can't drift from +// docs/pnpm-workspace.yaml minimumReleaseAge / docs/.npmrc min-release-age +// (scripts/soak/soak.mts asserts the data files match the same constant). +import { SOAK_DAYS } from '../scripts/soak/constants.mts' + +export default defineConfig({ + interactive: false, + loglevel: 'warn', + maturityPeriod: SOAK_DAYS, + mode: 'latest', + write: true, +}) diff --git a/external-tools.json b/external-tools.json new file mode 100644 index 000000000..b5978fd2b --- /dev/null +++ b/external-tools.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://raw.githubusercontent.com/SocketDev/socket-wheelhouse/main/packages/build-infra/lib/external-tools-schema.json", + "tools": { + "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 neither SOCKET_API_KEY (primary) nor SOCKET_API_TOKEN (forward-canonical) is set", + "Shims npm/yarn/pnpm so every install call passes through the firewall" + ], + "platforms": { + "darwin-arm64": { + "asset": "sfw-free-macos-arm64", + "integrity": "sha512-T6wBOJGdRVSI8577lGqRNzNd6Q+1vqKyaqGgOA8G4M5MU2vcsUnXuJTgP2MMZjUqROSXUlFL0mHguuxXT2QadQ==" + }, + "darwin-x64": { + "asset": "sfw-free-macos-x86_64", + "integrity": "sha512-4G/AIY5UGU81wcepDKErY5u0nY85D8UM9nXTEPv8CR2rOV/s4IcmrkxywwZ3ipejHVQB7QmCVt0/SsqWglGikw==" + }, + "linux-arm64": { + "asset": "sfw-free-linux-arm64", + "integrity": "sha512-FYRYR52SL+KKFldW4ogYOUnTH5OSqvtXwzGFeWi0W2x+75KZcPiGzWBbhMmh0f5QtgYLV+4qdREgmKCBEayNtA==" + }, + "linux-x64": { + "asset": "sfw-free-linux-x86_64", + "integrity": "sha512-waLrsPG2a7EOv0XuvXDQZGgCZ4MTtOfZh8TmGbM6gn2B6Nh6HI+15jaoKdAS9wgdTyIqTuqU+O+NtVYd+kuFaA==" + }, + "win-x64": { + "asset": "sfw-free-windows-x86_64.exe", + "integrity": "sha512-YYnfwR6M/PHo72LSyKtpY3bAUG4F4ckToJqGx5Fkz4rwg1+48hkxuBaF3hdxHUdHPkfO5grDyoNgXGe7FojGcg==" + } + } + }, + "sfw-enterprise": { + "description": "Socket Firewall (enterprise tier) — selected when SOCKET_API_KEY (or SOCKET_API_TOKEN) is set.", + "version": "1.13.1", + "repository": "github:SocketDev/firewall-release", + "release": "asset", + "binaryName": "sfw", + "notes": [ + "Used when SOCKET_API_KEY is set (primary, universally supported). SOCKET_API_TOKEN is accepted as the forward-canonical secondary read.", + "Same shims as sfw-free, broader ecosystem support (Ruby, .NET, Go on Linux)" + ], + "platforms": { + "darwin-arm64": { + "asset": "sfw-macos-arm64", + "integrity": "sha512-ZDy2C6leKyTHZFvcZZpG2eQqVzs7buk+Hs92fkaMYME829QzyxdGQVVgwEVaGJpedGdUvhksKKcvT9IynI1kxg==" + }, + "darwin-x64": { + "asset": "sfw-macos-x86_64", + "integrity": "sha512-cm76we0sn7kqPOya/ZGQpPyhjRDyFT5lHigeT5Qso+QaPL6Cmwi0FVs2L7l63j+WR/9eYPU1WjjGOto5NbWsEQ==" + }, + "linux-arm64": { + "asset": "sfw-linux-arm64", + "integrity": "sha512-9qPi3mobBfyq1k+pD2GDG0tZkhy16f7FXE9oGiwmwPvy5PXwnlzqEXnYld3qGsKIVwNhunev/If26oROTHbrHA==" + }, + "linux-x64": { + "asset": "sfw-linux-x86_64", + "integrity": "sha512-lu9h8UzDZt34gdCEVHBGW6goE1Ayykq413EovV5B4nG7jBK27mI0GQstzVbWXA3wWaweT39PehXGtVpdqIDGSA==" + }, + "win-x64": { + "asset": "sfw-windows-x86_64.exe", + "integrity": "sha512-URZXauIsdUT12E2KTc4sfsxRmJm7nRJzAgM+IYGX4Xq+X0cl/eAbH5SpYIJKpsnW9csSztW9ceyPhlM+f3neIQ==" + } + } + }, + "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" + ], + "platforms": { + "darwin-arm64": { + "asset": "zizmor-aarch64-apple-darwin.tar.gz", + "integrity": "sha512-UfLPPdYejR8fvrFwr9Tos7LKFvqr7YcAHWZlAmeo7imYI/fIucYnCY6HeDyk2cbFUvDpQTdps6WQS1QpqCtTfQ==" + }, + "darwin-x64": { + "asset": "zizmor-x86_64-apple-darwin.tar.gz", + "integrity": "sha512-SCbcEzF/zy2qNuNaocLPIUC4Wuq5GnHt0iUFve3qx6bJyFdzU6pDFZkF64dojGC7M5gAcK/4acXGPx3YnMDy/g==" + }, + "linux-arm64": { + "asset": "zizmor-aarch64-unknown-linux-gnu.tar.gz", + "integrity": "sha512-TkGvwt0zYdmiJ7LZmy6Bz9CdkqcuEpFKhXUK9m0MhSPxBx2gYBsrw6OxGSttMEQ3bvxdvyG6rqbnMgjyDZB1zA==" + }, + "linux-x64": { + "asset": "zizmor-x86_64-unknown-linux-gnu.tar.gz", + "integrity": "sha512-zTMERMDd3JfaRX12klj2fhZGDyrXeLkVUY1QJkCv8RRmAa9uVuH88gHOnv4CQtC5hXS7FPRJzvCVOnw38gV83g==" + }, + "win-x64": { + "asset": "zizmor-x86_64-pc-windows-msvc.zip", + "integrity": "sha512-Pijh/CrrOAkZzLiTr2LTHdI8d6+5Ql6B+suY6fXVmL8UVa+4Q36hHL5K67iRFz03v/V/UcrY6+dfhnmot/xfww==" + } + } + }, + "agentshield": { + "description": "Claude AI config security scanner (prompt injection, secrets)", + "purl": "pkg:npm/ecc-agentshield@1.4.0", + "integrity": "sha512-R98OO1Ujyk2lezDLb+iQmMhF6FwTJCHajy3G4FCB6x7wkSTqR9f8+eAelC5KDzYDsGSbc0sOZvjXOOPRBtMpDg==" + }, + "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" + } + } +} diff --git a/mise.toml b/mise.toml index 363207c5f..fe149cb72 100644 --- a/mise.toml +++ b/mise.toml @@ -27,6 +27,21 @@ run = "cargo build" [tasks.test] run = "cargo test" +[tasks.soak] +run = "node scripts/soak/soak.mts --check" + +[tasks."soak:fix"] +run = "node scripts/soak/soak.mts --fix" + +[tasks."deps:update"] +run = "node scripts/soak/update-deps.mts" + +[tasks."tools:check"] +run = "node scripts/soak/external-tools.mts --check" + +[tasks."tools:install"] +run = "node scripts/soak/external-tools.mts --install-all --shims" + [tasks."docs:dev"] depends = ["build"] dir = "docs" diff --git a/scripts/soak/constants.mts b/scripts/soak/constants.mts new file mode 100644 index 000000000..e72ba1d53 --- /dev/null +++ b/scripts/soak/constants.mts @@ -0,0 +1,42 @@ +/** + * @file Canonical soak window — the ONE source for every release-age surface. + * A new or bumped third-party dependency must have been published at least + * this long before the repo adopts it: the cooldown catches a compromised + * upstream before it lands. Every soak surface DERIVES from `SOAK_DAYS` + * instead of hand-copying the number: + * + * - `.cargo/config.toml` -> `global-min-publish-age = " days"` (cargo -Zmin-publish-age) + * - `rust-toolchain.toml` -> dated nightly adopted only once >= SOAK_DAYS old + * - `docs/pnpm-workspace.yaml` -> `minimumReleaseAge: ` (aube reads minutes) + * - `docs/.npmrc` -> `min-release-age=` (npm >= 11.17, days) + * - `docs/taze.config.mts` -> `maturityPeriod: SOAK_DAYS` (imports this) + * + * The data files can't import this module, so `scripts/soak/soak.mts` + * asserts they match (code-is-law parity gate). + */ + +export const SOAK_DAYS = 7 + +// pnpm/aube `minimumReleaseAge` is expressed in MINUTES. +export const SOAK_MINUTES = SOAK_DAYS * 24 * 60 + +// Exclusion annotation carried on the line ABOVE every version-pinned +// `minimumReleaseAgeExclude` entry. `removable` = `published` + SOAK_DAYS; +// once `removable` is in the past the pin has soaked and must be pruned. +export const ANNOTATION_RE = + /^#\s*published:\s*(\d{4}-\d{2}-\d{2})\s*\|\s*removable:\s*(\d{4}-\d{2}-\d{2})\s*$/ + +// A version-pinned exclude entry (`'name@1.2.3'` / `'@scope/name@1.2.3'`), +// as opposed to a bare name or `@scope/*` glob (which need no annotation: +// they express standing trust, not a dated soak bypass). +export const VERSION_PIN_RE = /^(@?[^@\s]+)@[^@\s]+$/ + +export function todayIso(): string { + return new Date().toISOString().slice(0, 10) +} + +export function addDaysIso(iso: string, days: number): string { + const d = new Date(`${iso}T00:00:00Z`) + d.setUTCDate(d.getUTCDate() + days) + return d.toISOString().slice(0, 10) +} diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts new file mode 100644 index 000000000..2d73b6764 --- /dev/null +++ b/scripts/soak/external-tools.mts @@ -0,0 +1,316 @@ +#!/usr/bin/env node +/** + * @file Pinned external security tooling — download, verify, shim. + * `external-tools.json` (repo root) pins every tool to an exact version + * with a sha512 SRI integrity per platform asset (same schema as + * socket-wheelhouse). This script is the only way those tools reach a + * machine: nothing here trusts "latest". + * + * - `--check` validate every pin (shape, SRI prefix, soak + * annotations on any soakBypass) — CI gate, no network + * - `--install ` download + SRI-verify + install into the shared + * rack `~/.socket/_wheelhouse/rack///` + * with a PATH handle in `~/.socket/_wheelhouse/bin/` + * - `--install-all` every installable pin + * - `--shims` write sfw shims (npm/yarn/pnpm/pip/pip3/uv/cargo) + * into the wheelhouse bin dir so installs route + * through the firewall + * + * `sfw` resolves to sfw-enterprise when SOCKET_API_KEY or SOCKET_API_TOKEN + * is set (either may be fed from a repo secret such as + * SOCKET_SECURITY_KEY), else sfw-free — free tier needs no key, so CI is + * firewalled from day one and upgrades itself when the secret lands. + */ + +import { createHash } from 'node:crypto' +import { spawnSync } from 'node:child_process' +import { + chmodSync, + existsSync, + mkdirSync, + readFileSync, + rmSync, + symlinkSync, + unlinkSync, + writeFileSync, +} from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +import { ANNOTATION_RE, SOAK_DAYS, addDaysIso } from './constants.mts' + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') +const TOOLS_JSON = path.join(REPO_ROOT, 'external-tools.json') +const WHEELHOUSE = path.join(os.homedir(), '.socket', '_wheelhouse') +const RACK = path.join(WHEELHOUSE, 'rack') +const BIN = path.join(WHEELHOUSE, 'bin') + +const SFW_ECOSYSTEMS = ['npm', 'yarn', 'pnpm', 'pip', 'pip3', 'uv', 'cargo'] + +interface PlatformPin { + asset: string + integrity: string +} + +interface ToolPin { + description?: string + version?: string + repository?: string + release?: string + binaryName?: string + purl?: string + integrity?: string + platforms?: Record + soakBypass?: { version: string; published: string; removable: string } +} + +function loadTools(): Record { + return JSON.parse(readFileSync(TOOLS_JSON, 'utf8')).tools +} + +function platformKey(): string { + const osKey = { darwin: 'darwin', linux: 'linux', win32: 'win' }[process.platform] + const archKey = { arm64: 'arm64', x64: 'x64' }[process.arch] + if (!osKey || !archKey) { + throw new Error(`unsupported platform ${process.platform}-${process.arch}`) + } + return `${osKey}-${archKey}` +} + +function sriSha512(buf: Buffer): string { + return `sha512-${createHash('sha512').update(buf).digest('base64')}` +} + +export function checkPins(tools: Record): string[] { + const out: string[] = [] + for (const [name, pin] of Object.entries(tools)) { + if (!pin.version && !pin.purl) { + out.push(`${name}: no version or purl pin`) + } + const integrities = [ + ...(pin.integrity ? [pin.integrity] : []), + ...Object.values(pin.platforms ?? {}).map(p => p.integrity), + ] + if (pin.release === 'asset' && integrities.length === 0) { + out.push(`${name}: release asset without any integrity pin`) + } + for (const sri of integrities) { + if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(sri)) { + out.push(`${name}: integrity is not a sha512 SRI: ${sri}`) + } + } + if (pin.soakBypass) { + const { published, removable } = pin.soakBypass + const expected = addDaysIso(published, SOAK_DAYS) + if (removable !== expected) { + out.push(`${name}: soakBypass removable ${removable}, wanted ${expected} (published + ${SOAK_DAYS}d)`) + } + if (!ANNOTATION_RE.test(`# published: ${published} | removable: ${removable}`)) { + out.push(`${name}: soakBypass dates are not YYYY-MM-DD`) + } + } + } + return out +} + +async function download(url: string, expectedSri: string): Promise { + const headers: Record = {} + if (process.env.GITHUB_TOKEN) { + headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}` + } + const res = await fetch(url, { headers, redirect: 'follow' }) + if (!res.ok) { + throw new Error(`download failed ${res.status} ${url}`) + } + const buf = Buffer.from(await res.arrayBuffer()) + const actual = sriSha512(buf) + if (actual !== expectedSri) { + throw new Error(`integrity mismatch for ${url}\n expected ${expectedSri}\n actual ${actual}`) + } + return buf +} + +function linkHandle(target: string, name: string): void { + mkdirSync(BIN, { recursive: true }) + const handle = path.join(BIN, name) + if (existsSync(handle)) { + unlinkSync(handle) + } + symlinkSync(target, handle) +} + +async function installAssetTool(name: string, pin: ToolPin): Promise { + const plat = pin.platforms?.[platformKey()] + if (!plat) { + throw new Error(`${name}: no pinned asset for ${platformKey()}`) + } + const repo = pin.repository!.replace(/^github:/, '') + const url = `https://github.com/${repo}/releases/download/v${pin.version}/${plat.asset}` + const binName = pin.binaryName ?? name + const destDir = path.join(RACK, name, pin.version!) + const destBin = path.join(destDir, binName) + if (existsSync(destBin)) { + linkHandle(destBin, binName) + console.log(`[external-tools] ${name}@${pin.version} already installed`) + return + } + console.log(`[external-tools] downloading ${name}@${pin.version} (${plat.asset})`) + const buf = await download(url, plat.integrity) + mkdirSync(destDir, { recursive: true }) + if (plat.asset.endsWith('.tar.gz')) { + const archive = path.join(destDir, plat.asset) + writeFileSync(archive, buf) + const res = spawnSync('tar', ['-xzf', archive, '-C', destDir], { stdio: 'inherit' }) + rmSync(archive) + if (res.status !== 0) { + throw new Error(`${name}: tar extract failed`) + } + } else { + writeFileSync(destBin, buf) + } + chmodSync(destBin, 0o755) + linkHandle(destBin, binName) + console.log(`[external-tools] installed ${name}@${pin.version} -> ${destBin}`) +} + +function hasSocketToken(): boolean { + return Boolean(process.env.SOCKET_API_KEY || process.env.SOCKET_API_TOKEN) +} + +async function installTool(name: string, tools: Record): Promise { + // `sfw` is a flavor pair: the enterprise binary when a Socket token is + // present (repo secret), the keyless free tier otherwise. + if (name === 'sfw') { + name = hasSocketToken() ? 'sfw-enterprise' : 'sfw-free' + } + const pin = tools[name] + if (!pin) { + throw new Error(`unknown tool ${name} (see external-tools.json)`) + } + if (pin.release === 'asset') { + await installAssetTool(name, pin) + return + } + if (pin.purl) { + // npm-packaged scanner (agentshield): verify the registry tarball + // against the pinned SRI, then run via the extracted package. + const m = /^pkg:npm\/(.+)@([^@]+)$/.exec(pin.purl) + if (!m) { + throw new Error(`${name}: unsupported purl ${pin.purl}`) + } + const [, pkg, version] = m + const base = pkg!.split('/').pop() + const url = `https://registry.npmjs.org/${pkg}/-/${base}-${version}.tgz` + const destDir = path.join(RACK, name, version!) + if (!existsSync(destDir)) { + console.log(`[external-tools] downloading ${name}@${version} (npm)`) + const buf = await download(url, pin.integrity!) + mkdirSync(destDir, { recursive: true }) + const archive = path.join(destDir, 'package.tgz') + writeFileSync(archive, buf) + const res = spawnSync('tar', ['-xzf', archive, '-C', destDir], { stdio: 'inherit' }) + rmSync(archive) + if (res.status !== 0) { + throw new Error(`${name}: tar extract failed`) + } + } + const pkgJson = JSON.parse(readFileSync(path.join(destDir, 'package/package.json'), 'utf8')) + const binRel = typeof pkgJson.bin === 'string' ? pkgJson.bin : Object.values(pkgJson.bin ?? {})[0] + if (binRel) { + const wrapper = path.join(RACK, name, `${name}-wrapper`) + writeFileSync( + wrapper, + `#!/usr/bin/env bash\nexec node '${path.join(destDir, 'package', binRel as string)}' "$@"\n`, + ) + chmodSync(wrapper, 0o755) + linkHandle(wrapper, name) + } + console.log(`[external-tools] installed ${name}@${version}`) + return + } + if (pin.release === 'uv-project') { + // Git-SHA-pinned python project; not auto-installed (needs uv). + const repo = pin.repository!.replace(/^github:/, '') + console.log( + `[external-tools] ${name} is a uv project — run: uvx --from git+https://github.com/${repo}@${pin.version} ${name}`, + ) + return + } + throw new Error(`${name}: no installable shape (release=${pin.release ?? 'none'})`) +} + +/** + * sfw shims: tiny wrappers named after each package manager that route the + * real invocation through the firewall. A sentinel env var breaks the + * recursion when sfw itself re-invokes the tool; the real binary is found + * by stripping the wheelhouse bin dir out of PATH. + */ +function writeShims(): void { + mkdirSync(BIN, { recursive: true }) + for (const cmd of SFW_ECOSYSTEMS) { + const sentinel = `SOCKET_SHIM_ACTIVE_${cmd.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}` + const body = `#!/usr/bin/env bash +# sfw shim for ${cmd} — managed by scripts/soak/external-tools.mts --shims +set -euo pipefail +CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\\n' | grep -vFx '${BIN}' | paste -sd ':' -) +REAL=$(PATH="$CLEAN_PATH" command -v '${cmd}' || true) +if [ -n "\${${sentinel}:-}" ] || [ -z "$REAL" ] || ! command -v sfw >/dev/null 2>&1; then + [ -n "$REAL" ] && exec "$REAL" "$@" + echo "${cmd}: not found" >&2; exit 127 +fi +export ${sentinel}=1 +exec sfw '${cmd}' "$@" +` + const shim = path.join(BIN, cmd) + writeFileSync(shim, body) + chmodSync(shim, 0o755) + } + console.log(`[external-tools] wrote sfw shims for ${SFW_ECOSYSTEMS.join(', ')} in ${BIN}`) + console.log(`[external-tools] prepend ${BIN} to PATH to activate`) +} + +async function main(argv: string[] = process.argv.slice(2)): Promise { + const tools = loadTools() + if (argv.includes('--check') || argv.length === 0) { + const problems = checkPins(tools) + for (const p of problems) { + console.error(`[external-tools] ${p}`) + } + if (problems.length === 0) { + console.log(`[external-tools] ${Object.keys(tools).length} pins valid`) + } + return problems.length === 0 ? 0 : 1 + } + const installIdx = argv.indexOf('--install') + if (installIdx !== -1) { + await installTool(argv[installIdx + 1]!, tools) + } + if (argv.includes('--install-all')) { + for (const name of Object.keys(tools)) { + if (name === 'sfw-enterprise' || name === 'sfw-free') { + continue + } + await installTool(name, tools) + } + await installTool('sfw', tools) + } + if (argv.includes('--shims')) { + writeShims() + } + return 0 +} + +const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}` +if (isMain) { + main().then( + code => { + process.exitCode = code + }, + err => { + console.error(`[external-tools] ${err.message}`) + process.exitCode = 1 + }, + ) +} diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts new file mode 100644 index 000000000..9042cc656 --- /dev/null +++ b/scripts/soak/soak.mts @@ -0,0 +1,328 @@ +#!/usr/bin/env node +/** + * @file Soak manager — parity gate + fixer for the release-age cooldown. + * The window is ONE value (`SOAK_DAYS` in ./constants.mts); this script + * asserts every data surface matches it and that soak exclusions carry a + * valid, unexpired `# published: | removable:` annotation: + * + * - `.cargo/config.toml` `global-min-publish-age` + `[unstable] min-publish-age` + * - `docs/pnpm-workspace.yaml` `minimumReleaseAge` (minutes) + annotated excludes + * - `docs/.npmrc` `min-release-age` (days) + * - `docs/taze.config.mts` imports SOAK_DAYS (existence + import check) + * + * `--check` (default) fails loud with What / Saw / Wanted / Fix on drift. + * `--fix` rewrites window values in place and prunes excludes whose + * `removable` date has passed (a cleared pin is dead weight — pruning it + * re-arms the soak for the next publish of that package). + * + * Usage: node scripts/soak/soak.mts [--check|--fix] [--quiet] + */ + +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +import { + ANNOTATION_RE, + SOAK_DAYS, + SOAK_MINUTES, + VERSION_PIN_RE, + addDaysIso, + todayIso, +} from './constants.mts' + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') + +// Per-repo surface map — the only block that differs between repos +// carrying this script (aube keeps its npm surfaces in docs/, where the +// package.json lives and aube itself reads workspace yaml + npmrc). +export const SURFACES = { + cargoConfig: '.cargo/config.toml', + npmrc: 'docs/.npmrc', + workspaceYaml: 'docs/pnpm-workspace.yaml', + tazeConfig: 'docs/taze.config.mts', +} + +export interface Finding { + file: string + what: string + saw: string + wanted: string + fix: string +} + +export function checkCargoConfig(body: string, file: string): Finding[] { + const out: Finding[] = [] + const age = /^global-min-publish-age\s*=\s*"([^"]*)"/m.exec(body)?.[1] + const wanted = `${SOAK_DAYS} days` + if (age !== wanted) { + out.push({ + file, + what: 'cargo min-publish-age window', + saw: age ?? '(missing)', + wanted, + fix: `set [registry] global-min-publish-age = "${wanted}" (or run --fix)`, + }) + } + if (!/^\[unstable\][^[]*^min-publish-age\s*=\s*true/ms.test(body)) { + out.push({ + file, + what: 'cargo unstable feature gate', + saw: '[unstable] min-publish-age missing or false', + wanted: 'min-publish-age = true under [unstable]', + fix: 'add `[unstable]\\nmin-publish-age = true` (nightly-only; the pinned toolchain provides it)', + }) + } + return out +} + +export function checkNpmrc(body: string, file: string): Finding[] { + const days = /^min-release-age=(\d+)\s*$/m.exec(body)?.[1] + if (Number(days) === SOAK_DAYS) { + return [] + } + return [ + { + file, + what: 'npm min-release-age window', + saw: days ?? '(missing)', + wanted: String(SOAK_DAYS), + fix: `set min-release-age=${SOAK_DAYS} (or run --fix)`, + }, + ] +} + +export function checkWorkspaceYaml(body: string, file: string): Finding[] { + const out: Finding[] = [] + const minutes = /^minimumReleaseAge:\s*(\d+)\s*$/m.exec(body)?.[1] + if (Number(minutes) !== SOAK_MINUTES) { + out.push({ + file, + what: 'minimumReleaseAge window', + saw: minutes ?? '(missing)', + wanted: `${SOAK_MINUTES} (SOAK_DAYS ${SOAK_DAYS} x 1440 minutes)`, + fix: `set minimumReleaseAge: ${SOAK_MINUTES} (or run --fix)`, + }) + } + out.push(...checkExcludeAnnotations(body, file)) + return out +} + +/** + * Every version-pinned `minimumReleaseAgeExclude` entry must carry, on the + * line directly above, `# published: YYYY-MM-DD | removable: YYYY-MM-DD` + * with `removable = published + SOAK_DAYS`, and must be pruned once + * `removable` is strictly in the past. Bare names and `@scope/*` globs are + * standing trust, not dated bypasses — no annotation required. + */ +export function checkExcludeAnnotations(body: string, file: string): Finding[] { + const out: Finding[] = [] + const today = todayIso() + for (const entry of parseExcludeEntries(body)) { + if (!VERSION_PIN_RE.test(entry.name)) { + continue + } + if (!entry.annotation) { + out.push({ + file, + what: `soak exclude '${entry.name}' annotation`, + saw: '(no annotation on the line above)', + wanted: `# published: YYYY-MM-DD | removable: `, + fix: `annotate the pin with its real registry publish date`, + }) + continue + } + const { published, removable } = entry.annotation + const expected = addDaysIso(published, SOAK_DAYS) + if (removable !== expected) { + out.push({ + file, + what: `soak exclude '${entry.name}' removable date`, + saw: removable, + wanted: `${expected} (published ${published} + ${SOAK_DAYS} days)`, + fix: 'correct the removable date', + }) + } + if (removable < today) { + out.push({ + file, + what: `soak exclude '${entry.name}' expired`, + saw: `removable ${removable} < today ${today}`, + wanted: 'entry pruned once its window has passed', + fix: 'delete the pin + its annotation (or run --fix)', + }) + } + } + return out +} + +interface ExcludeEntry { + name: string + line: number + annotation?: { published: string; removable: string } +} + +export function parseExcludeEntries(body: string): ExcludeEntry[] { + const lines = body.split('\n') + const out: ExcludeEntry[] = [] + let inBlock = false + let blockIndent = 0 + for (let i = 0; i < lines.length; i++) { + const line = lines[i]! + if (/^minimumReleaseAgeExclude:\s*$/.test(line)) { + inBlock = true + blockIndent = -1 + continue + } + if (!inBlock) { + continue + } + const item = /^(\s+)-\s*['"]?([^'"#\s]+)['"]?\s*$/.exec(line) + if (!item) { + // Comments stay inside the block; anything else at column 0 ends it. + if (/^\S/.test(line)) { + inBlock = false + } + continue + } + if (blockIndent === -1) { + blockIndent = item[1]!.length + } + if (item[1]!.length !== blockIndent) { + continue + } + const prev = lines[i - 1]?.trim() ?? '' + const ann = ANNOTATION_RE.exec(prev) + out.push({ + name: item[2]!, + line: i + 1, + ...(ann ? { annotation: { published: ann[1]!, removable: ann[2]! } } : {}), + }) + } + return out +} + +export function checkTazeConfig(body: string, file: string): Finding[] { + const out: Finding[] = [] + if (!body.includes('maturityPeriod')) { + out.push({ + file, + what: 'taze maturityPeriod', + saw: '(not set)', + wanted: 'maturityPeriod: SOAK_DAYS', + fix: 'set maturityPeriod: SOAK_DAYS in the taze config', + }) + } + if (!body.includes('constants.mts')) { + out.push({ + file, + what: 'taze config soak import', + saw: 'window not imported from scripts/soak/constants.mts', + wanted: "import { SOAK_DAYS } from '/scripts/soak/constants.mts'", + fix: 'import SOAK_DAYS instead of hand-copying the number', + }) + } + return out +} + +export function fixCargoConfig(body: string): string { + return body.replace( + /^(global-min-publish-age\s*=\s*)"[^"]*"/m, + `$1"${SOAK_DAYS} days"`, + ) +} + +export function fixNpmrc(body: string): string { + if (/^min-release-age=\d+\s*$/m.test(body)) { + return body.replace(/^min-release-age=\d+\s*$/m, `min-release-age=${SOAK_DAYS}`) + } + return `${body.trimEnd()}\nmin-release-age=${SOAK_DAYS}\n` +} + +export function fixWorkspaceYaml(body: string): string { + let out = body.replace( + /^(minimumReleaseAge:\s*)\d+\s*$/m, + `$1${SOAK_MINUTES}`, + ) + // Prune expired pins together with their annotation line. + const today = todayIso() + const lines = out.split('\n') + const drop = new Set() + for (const entry of parseExcludeEntries(out)) { + if (entry.annotation && entry.annotation.removable < today) { + drop.add(entry.line - 1) + if (ANNOTATION_RE.test(lines[entry.line - 2]?.trim() ?? '')) { + drop.add(entry.line - 2) + } + } + } + if (drop.size > 0) { + out = lines.filter((_, i) => !drop.has(i)).join('\n') + } + return out +} + +function report(findings: Finding[], quiet: boolean): void { + for (const f of findings) { + console.error(`[soak] ${f.file}: ${f.what}`) + console.error(` saw: ${f.saw}`) + console.error(` wanted: ${f.wanted}`) + console.error(` fix: ${f.fix}`) + } + if (!quiet && findings.length === 0) { + console.log(`[soak] all surfaces match SOAK_DAYS=${SOAK_DAYS} and no exclude has drifted`) + } +} + +export function main(argv: string[] = process.argv.slice(2)): number { + const fix = argv.includes('--fix') + const quiet = argv.includes('--quiet') + const findings: Finding[] = [] + + const surfaces: Array<{ + rel: string + check: (body: string, file: string) => Finding[] + fixer?: (body: string) => string + required: boolean + }> = [ + { rel: SURFACES.cargoConfig, check: checkCargoConfig, fixer: fixCargoConfig, required: true }, + { rel: SURFACES.npmrc, check: checkNpmrc, fixer: fixNpmrc, required: true }, + { rel: SURFACES.workspaceYaml, check: checkWorkspaceYaml, fixer: fixWorkspaceYaml, required: true }, + { rel: SURFACES.tazeConfig, check: checkTazeConfig, required: true }, + ] + + for (const s of surfaces) { + const abs = path.join(REPO_ROOT, s.rel) + if (!existsSync(abs)) { + if (s.required) { + findings.push({ + file: s.rel, + what: 'soak surface missing', + saw: '(file absent)', + wanted: 'file present and carrying the soak window', + fix: `create ${s.rel} — see scripts/soak/constants.mts header for the expected key`, + }) + } + continue + } + let body = readFileSync(abs, 'utf8') + if (fix && s.fixer) { + const fixed = s.fixer(body) + if (fixed !== body) { + writeFileSync(abs, fixed) + console.log(`[soak] fixed ${s.rel}`) + body = fixed + } + } + findings.push(...s.check(body, s.rel)) + } + + report(findings, quiet) + return findings.length === 0 ? 0 : 1 +} + +const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}` +if (isMain) { + process.exitCode = main() +} diff --git a/scripts/soak/update-deps.mts b/scripts/soak/update-deps.mts new file mode 100644 index 000000000..0fcd51eb3 --- /dev/null +++ b/scripts/soak/update-deps.mts @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * @file Soaked dependency updater — every ecosystem bumps through the same + * cooldown: + * + * - npm: taze (maturityPeriod = SOAK_DAYS via the taze config next to the + * package.json) rewrites ranges, then the repo's own installer refreshes + * the lockfile. + * - cargo: `cargo update` under the pinned nightly, where + * `.cargo/config.toml` min-publish-age enforces the same window + * (too-new crate versions are skipped unless already locked). + * + * Usage: node scripts/soak/update-deps.mts [--npm|--cargo] [--dry-run] + * (no ecosystem flag = both) + */ + +import { spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') + +// Per-repo knobs — the only block that differs between repos carrying this +// script (aube's npm package lives in docs/ and installs with aube itself). +const NPM_DIR = path.join(REPO_ROOT, 'docs') +const INSTALLERS = [ + [path.join(REPO_ROOT, 'target/debug/aube'), 'install'], + ['aube', 'install'], +] + +function run(cmd: string, args: string[], cwd: string): number { + console.log(`[update-deps] ${cmd} ${args.join(' ')} (in ${path.relative(REPO_ROOT, cwd) || '.'})`) + const res = spawnSync(cmd, args, { cwd, stdio: 'inherit' }) + return res.status ?? 1 +} + +function updateNpm(dryRun: boolean): number { + const taze = path.join(NPM_DIR, 'node_modules/.bin/taze') + if (!existsSync(taze)) { + console.error(`[update-deps] taze not installed — run the installer in ${NPM_DIR} first`) + return 1 + } + const args = dryRun ? [] : ['--write'] + const status = run(taze, args, NPM_DIR) + if (status !== 0 || dryRun) { + return status + } + for (const [cmd, ...args] of INSTALLERS) { + if (cmd.includes('/') && !existsSync(cmd)) { + continue + } + return run(cmd!, args, NPM_DIR) + } + console.error('[update-deps] no installer found — refresh the lockfile manually') + return 1 +} + +function updateCargo(dryRun: boolean): number { + // The min-publish-age soak is an [unstable] cargo feature: only the + // rust-toolchain.toml nightly honors it, and only rustup's cargo shim + // reads rust-toolchain.toml. A non-rustup cargo (e.g. Homebrew stable) + // would silently update WITHOUT the soak — refuse that. + const rustupCargo = path.join(os.homedir(), '.cargo/bin/cargo') + if (!existsSync(rustupCargo)) { + console.error('[update-deps] rustup cargo shim not found — cargo update would bypass the min-publish-age soak') + return 1 + } + return run(rustupCargo, dryRun ? ['update', '--dry-run'] : ['update'], REPO_ROOT) +} + +function main(argv: string[] = process.argv.slice(2)): number { + const dryRun = argv.includes('--dry-run') + const onlyNpm = argv.includes('--npm') + const onlyCargo = argv.includes('--cargo') + let status = 0 + if (!onlyCargo) { + status ||= updateNpm(dryRun) + } + if (!onlyNpm) { + status ||= updateCargo(dryRun) + } + return status +} + +const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}` +if (isMain) { + process.exitCode = main() +} From 1d06f6ae395398586a1000022e1a00e65a0ba6ea Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:06:37 -0400 Subject: [PATCH 07/29] ci: gate soak + tool pins and route installs through socket firewall test-linux runs soak.mts --check and external-tools.mts --check, then installs sfw and its npm/yarn/pnpm/pip/uv/cargo shims. The SOCKET_SECURITY_KEY repo secret (not yet set) feeds SOCKET_API_KEY + SOCKET_API_TOKEN; until it lands sfw runs its keyless free tier, so the firewall is active either way and upgrades itself when the secret appears. --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92792543b..66dce16ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,12 @@ jobs: contents: read env: RUSTFLAGS: "-D warnings" + # Socket credentials: the SOCKET_SECURITY_KEY repo secret feeds both + # names sfw reads (SOCKET_API_KEY primary, SOCKET_API_TOKEN + # forward-canonical). Until the secret lands these are empty and sfw + # runs in its keyless free tier — the firewall is active either way. + SOCKET_API_KEY: ${{ secrets.SOCKET_SECURITY_KEY }} + SOCKET_API_TOKEN: ${{ secrets.SOCKET_SECURITY_KEY }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -82,6 +88,14 @@ jobs: - uses: jdx/mise-action@e6a8b3978addb5a52f2b4cd9d91eafa7f0ab959d # v4.2.0 with: cache: false + - name: soak + external-tool pin gates + run: | + node scripts/soak/soak.mts --check + node scripts/soak/external-tools.mts --check + - name: socket firewall shims + run: | + node scripts/soak/external-tools.mts --install sfw --shims + echo "$HOME/.socket/_wheelhouse/bin" >> "$GITHUB_PATH" - run: node benchmarks/validate-results.mts - run: mise run test - run: mise run lint From b813143f488f7f9a686c081ae500f6988276eff4 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:06:37 -0400 Subject: [PATCH 08/29] ci: shrink buildkite image via minimal toolchains, prebake sfw shims rustup --profile minimal with explicit components drops rust-docs (~600 MB per toolchain) from all three toolchains; rustup download caches are purged in-layer. Pre-bakes sfw-free 1.13.1 (sha512-pinned per arch, matching external-tools.json) plus the package-manager shims from a COPY'd template so agent jobs are firewalled without a per-job download. --- .buildkite/linux-agent.Dockerfile | 46 +++++++++++++++++++++++++------ .buildkite/sfw-shim-template.sh | 16 +++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 .buildkite/sfw-shim-template.sh diff --git a/.buildkite/linux-agent.Dockerfile b/.buildkite/linux-agent.Dockerfile index 689e3fa64..6a71da575 100644 --- a/.buildkite/linux-agent.Dockerfile +++ b/.buildkite/linux-agent.Dockerfile @@ -18,13 +18,43 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* RUN curl https://mise.run | sh -# Pre-bake the repo's pinned toolchain (rust-toolchain.toml) with the -# llvm-tools-preview the PGO flow needs, plus the 1.95 msrv floor, so -# agent jobs don't rustup-download at job time. +# Pre-bake the repo's toolchains with MINIMAL profiles + only the components +# jobs use (the default profile ships rust-docs, ~600 MB per toolchain): +# - nightly-2026-07-04: the rust-toolchain.toml pin (rustfmt/clippy for +# lint jobs, llvm-tools-preview for the PGO flow) +# - 1.95.0: the msrv floor cargo-msrv verifies against +# - stable: rustup's default for anything outside the repo checkout RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile default --default-toolchain stable \ - && /root/.cargo/bin/rustup toolchain install nightly-2026-07-04 --profile default -c llvm-tools-preview \ - && /root/.cargo/bin/rustup toolchain install 1.95.0 --profile default \ - && /root/.cargo/bin/rustup component add rustfmt clippy --toolchain stable + | sh -s -- -y --profile minimal --default-toolchain stable \ + && /root/.cargo/bin/rustup toolchain install nightly-2026-07-04 --profile minimal \ + -c rustfmt -c clippy -c llvm-tools-preview \ + && /root/.cargo/bin/rustup toolchain install 1.95.0 --profile minimal \ + && rm -rf /root/.rustup/downloads /root/.rustup/tmp -ENV PATH="/root/.cargo/bin:/root/.local/bin:/root/.local/share/mise/shims:${PATH}" +# Pre-bake Socket Firewall (free tier — keyless; jobs holding a Socket key +# select enterprise at job time) plus its package-manager shims, pinned + +# integrity-checked against external-tools.json (sfw-free 1.13.1, sha512 +# hex below = that file's SRI decoded). +COPY sfw-shim-template.sh /tmp/sfw-shim-template.sh +RUN set -eux; \ + arch="$(uname -m)"; \ + case "$arch" in \ + x86_64) asset=sfw-free-linux-x86_64; sha=c1a2ebb0f1b66bb10ebf45eebd70d0646802678313b4e7d987c4e619b33a827d81e8d87a1c8fb5e636a829d012f7081d4f222a4eea94f8ef8db5561dfa4b8568 ;; \ + aarch64) asset=sfw-free-linux-arm64; sha=158458479d922fe28a165756e288183949d31f9392aafb57c331857968b45b6c7eef929970f886cd605b84c9a1d1fe50b6060b57ee2a75112098a08111ac8db4 ;; \ + *) echo "unsupported arch $arch" >&2; exit 1 ;; \ + esac; \ + mkdir -p /root/.socket/_wheelhouse/rack/sfw-free/1.13.1 /root/.socket/_wheelhouse/bin; \ + curl -fsSL -o /root/.socket/_wheelhouse/rack/sfw-free/1.13.1/sfw \ + "https://github.com/SocketDev/sfw-free/releases/download/v1.13.1/$asset"; \ + echo "$sha /root/.socket/_wheelhouse/rack/sfw-free/1.13.1/sfw" | sha512sum -c -; \ + chmod 0755 /root/.socket/_wheelhouse/rack/sfw-free/1.13.1/sfw; \ + ln -sf /root/.socket/_wheelhouse/rack/sfw-free/1.13.1/sfw /root/.socket/_wheelhouse/bin/sfw; \ + for cmd in npm yarn pnpm pip pip3 uv cargo; do \ + sentinel="SOCKET_SHIM_ACTIVE_$(printf '%s' "$cmd" | tr '[:lower:]' '[:upper:]' | tr -c 'A-Z0-9' '_')"; \ + sed -e "s/__CMD__/$cmd/g" -e "s/__SENTINEL__/$sentinel/g" \ + /tmp/sfw-shim-template.sh > "/root/.socket/_wheelhouse/bin/$cmd"; \ + chmod 0755 "/root/.socket/_wheelhouse/bin/$cmd"; \ + done; \ + rm /tmp/sfw-shim-template.sh + +ENV PATH="/root/.socket/_wheelhouse/bin:/root/.cargo/bin:/root/.local/bin:/root/.local/share/mise/shims:${PATH}" diff --git a/.buildkite/sfw-shim-template.sh b/.buildkite/sfw-shim-template.sh new file mode 100644 index 000000000..115d100f1 --- /dev/null +++ b/.buildkite/sfw-shim-template.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# sfw shim for __CMD__ — pre-baked into the agent image; mirrors what +# scripts/soak/external-tools.mts --shims writes on dev machines. Routes the +# real package-manager invocation through Socket Firewall; the sentinel env +# var breaks recursion when sfw re-invokes the tool, and the real binary is +# found by stripping the wheelhouse bin dir out of PATH. +set -euo pipefail +CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vFx '/root/.socket/_wheelhouse/bin' | paste -sd ':' -) +REAL=$(PATH="$CLEAN_PATH" command -v '__CMD__' || true) +if [ -n "${__SENTINEL__:-}" ] || [ -z "$REAL" ] || ! command -v sfw >/dev/null 2>&1; then + [ -n "$REAL" ] && exec "$REAL" "$@" + echo "__CMD__: not found" >&2 + exit 127 +fi +export __SENTINEL__=1 +exec sfw '__CMD__' "$@" From 19abecd3f2024d0d004711632d86e75fc2151a0e Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:21:03 -0400 Subject: [PATCH 09/29] refactor(soak): single paths.mts reference, neutral tool-rack layout paths.mts is the one declaration of every location the soak + external-tools scripts touch (1 path, 1 reference); soak.mts, update-deps.mts, and external-tools.mts import from it instead of re-deriving. The tool rack moves to the repo-neutral $XDG_DATA_HOME/aube/dev-tools/{rack,bin} (CI appends it via the new --print-bin flag), the shim sentinel is SFW_SHIM_ACTIVE_*, and the external-tools.json $schema pointer is dropped. --- .buildkite/linux-agent.Dockerfile | 18 +++++----- .buildkite/sfw-shim-template.sh | 4 +-- .github/workflows/ci.yml | 2 +- external-tools.json | 1 - scripts/soak/external-tools.mts | 54 ++++++++++++++--------------- scripts/soak/paths.mts | 56 +++++++++++++++++++++++++++++++ scripts/soak/soak.mts | 14 +------- scripts/soak/update-deps.mts | 29 +++++----------- 8 files changed, 103 insertions(+), 75 deletions(-) create mode 100644 scripts/soak/paths.mts diff --git a/.buildkite/linux-agent.Dockerfile b/.buildkite/linux-agent.Dockerfile index 6a71da575..1ab71dfdb 100644 --- a/.buildkite/linux-agent.Dockerfile +++ b/.buildkite/linux-agent.Dockerfile @@ -43,18 +43,18 @@ RUN set -eux; \ aarch64) asset=sfw-free-linux-arm64; sha=158458479d922fe28a165756e288183949d31f9392aafb57c331857968b45b6c7eef929970f886cd605b84c9a1d1fe50b6060b57ee2a75112098a08111ac8db4 ;; \ *) echo "unsupported arch $arch" >&2; exit 1 ;; \ esac; \ - mkdir -p /root/.socket/_wheelhouse/rack/sfw-free/1.13.1 /root/.socket/_wheelhouse/bin; \ - curl -fsSL -o /root/.socket/_wheelhouse/rack/sfw-free/1.13.1/sfw \ + mkdir -p /root/.local/share/aube/dev-tools/rack/sfw-free/1.13.1 /root/.local/share/aube/dev-tools/bin; \ + curl -fsSL -o /root/.local/share/aube/dev-tools/rack/sfw-free/1.13.1/sfw \ "https://github.com/SocketDev/sfw-free/releases/download/v1.13.1/$asset"; \ - echo "$sha /root/.socket/_wheelhouse/rack/sfw-free/1.13.1/sfw" | sha512sum -c -; \ - chmod 0755 /root/.socket/_wheelhouse/rack/sfw-free/1.13.1/sfw; \ - ln -sf /root/.socket/_wheelhouse/rack/sfw-free/1.13.1/sfw /root/.socket/_wheelhouse/bin/sfw; \ + echo "$sha /root/.local/share/aube/dev-tools/rack/sfw-free/1.13.1/sfw" | sha512sum -c -; \ + chmod 0755 /root/.local/share/aube/dev-tools/rack/sfw-free/1.13.1/sfw; \ + ln -sf /root/.local/share/aube/dev-tools/rack/sfw-free/1.13.1/sfw /root/.local/share/aube/dev-tools/bin/sfw; \ for cmd in npm yarn pnpm pip pip3 uv cargo; do \ - sentinel="SOCKET_SHIM_ACTIVE_$(printf '%s' "$cmd" | tr '[:lower:]' '[:upper:]' | tr -c 'A-Z0-9' '_')"; \ + sentinel="SFW_SHIM_ACTIVE_$(printf '%s' "$cmd" | tr '[:lower:]' '[:upper:]' | tr -c 'A-Z0-9' '_')"; \ sed -e "s/__CMD__/$cmd/g" -e "s/__SENTINEL__/$sentinel/g" \ - /tmp/sfw-shim-template.sh > "/root/.socket/_wheelhouse/bin/$cmd"; \ - chmod 0755 "/root/.socket/_wheelhouse/bin/$cmd"; \ + /tmp/sfw-shim-template.sh > "/root/.local/share/aube/dev-tools/bin/$cmd"; \ + chmod 0755 "/root/.local/share/aube/dev-tools/bin/$cmd"; \ done; \ rm /tmp/sfw-shim-template.sh -ENV PATH="/root/.socket/_wheelhouse/bin:/root/.cargo/bin:/root/.local/bin:/root/.local/share/mise/shims:${PATH}" +ENV PATH="/root/.local/share/aube/dev-tools/bin:/root/.cargo/bin:/root/.local/bin:/root/.local/share/mise/shims:${PATH}" diff --git a/.buildkite/sfw-shim-template.sh b/.buildkite/sfw-shim-template.sh index 115d100f1..eca1604b3 100644 --- a/.buildkite/sfw-shim-template.sh +++ b/.buildkite/sfw-shim-template.sh @@ -3,9 +3,9 @@ # scripts/soak/external-tools.mts --shims writes on dev machines. Routes the # real package-manager invocation through Socket Firewall; the sentinel env # var breaks recursion when sfw re-invokes the tool, and the real binary is -# found by stripping the wheelhouse bin dir out of PATH. +# found by stripping the tool-rack bin dir out of PATH. set -euo pipefail -CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vFx '/root/.socket/_wheelhouse/bin' | paste -sd ':' -) +CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vFx '/root/.local/share/aube/dev-tools/bin' | paste -sd ':' -) REAL=$(PATH="$CLEAN_PATH" command -v '__CMD__' || true) if [ -n "${__SENTINEL__:-}" ] || [ -z "$REAL" ] || ! command -v sfw >/dev/null 2>&1; then [ -n "$REAL" ] && exec "$REAL" "$@" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66dce16ce..02beb4527 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: - name: socket firewall shims run: | node scripts/soak/external-tools.mts --install sfw --shims - echo "$HOME/.socket/_wheelhouse/bin" >> "$GITHUB_PATH" + node scripts/soak/external-tools.mts --print-bin >> "$GITHUB_PATH" - run: node benchmarks/validate-results.mts - run: mise run test - run: mise run lint diff --git a/external-tools.json b/external-tools.json index b5978fd2b..eebf454ef 100644 --- a/external-tools.json +++ b/external-tools.json @@ -1,5 +1,4 @@ { - "$schema": "https://raw.githubusercontent.com/SocketDev/socket-wheelhouse/main/packages/build-infra/lib/external-tools-schema.json", "tools": { "sfw-free": { "description": "Socket Firewall (free tier) — malware gate on dep installs.", diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 2d73b6764..c1b3357a9 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -2,19 +2,18 @@ /** * @file Pinned external security tooling — download, verify, shim. * `external-tools.json` (repo root) pins every tool to an exact version - * with a sha512 SRI integrity per platform asset (same schema as - * socket-wheelhouse). This script is the only way those tools reach a - * machine: nothing here trusts "latest". + * with a sha512 SRI integrity per platform asset. This script is the only + * way those tools reach a machine: nothing here trusts "latest". * * - `--check` validate every pin (shape, SRI prefix, soak * annotations on any soakBypass) — CI gate, no network - * - `--install ` download + SRI-verify + install into the shared - * rack `~/.socket/_wheelhouse/rack///` - * with a PATH handle in `~/.socket/_wheelhouse/bin/` + * - `--install ` download + SRI-verify + install into the local + * tool rack (see paths.mts RACK_DIR) with a PATH + * handle in BIN_DIR * - `--install-all` every installable pin * - `--shims` write sfw shims (npm/yarn/pnpm/pip/pip3/uv/cargo) - * into the wheelhouse bin dir so installs route - * through the firewall + * into BIN_DIR so installs route through the firewall + * - `--print-bin` print BIN_DIR (for `>> $GITHUB_PATH` in CI) * * `sfw` resolves to sfw-enterprise when SOCKET_API_KEY or SOCKET_API_TOKEN * is set (either may be fed from a repo secret such as @@ -34,18 +33,11 @@ import { unlinkSync, writeFileSync, } from 'node:fs' -import os from 'node:os' import path from 'node:path' import process from 'node:process' -import { fileURLToPath } from 'node:url' import { ANNOTATION_RE, SOAK_DAYS, addDaysIso } from './constants.mts' - -const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') -const TOOLS_JSON = path.join(REPO_ROOT, 'external-tools.json') -const WHEELHOUSE = path.join(os.homedir(), '.socket', '_wheelhouse') -const RACK = path.join(WHEELHOUSE, 'rack') -const BIN = path.join(WHEELHOUSE, 'bin') +import { BIN_DIR, EXTERNAL_TOOLS_JSON, RACK_DIR } from './paths.mts' const SFW_ECOSYSTEMS = ['npm', 'yarn', 'pnpm', 'pip', 'pip3', 'uv', 'cargo'] @@ -67,7 +59,7 @@ interface ToolPin { } function loadTools(): Record { - return JSON.parse(readFileSync(TOOLS_JSON, 'utf8')).tools + return JSON.parse(readFileSync(EXTERNAL_TOOLS_JSON, 'utf8')).tools } function platformKey(): string { @@ -133,8 +125,8 @@ async function download(url: string, expectedSri: string): Promise { } function linkHandle(target: string, name: string): void { - mkdirSync(BIN, { recursive: true }) - const handle = path.join(BIN, name) + mkdirSync(BIN_DIR, { recursive: true }) + const handle = path.join(BIN_DIR, name) if (existsSync(handle)) { unlinkSync(handle) } @@ -149,7 +141,7 @@ async function installAssetTool(name: string, pin: ToolPin): Promise { const repo = pin.repository!.replace(/^github:/, '') const url = `https://github.com/${repo}/releases/download/v${pin.version}/${plat.asset}` const binName = pin.binaryName ?? name - const destDir = path.join(RACK, name, pin.version!) + const destDir = path.join(RACK_DIR, name, pin.version!) const destBin = path.join(destDir, binName) if (existsSync(destBin)) { linkHandle(destBin, binName) @@ -203,7 +195,7 @@ async function installTool(name: string, tools: Record): Promis const [, pkg, version] = m const base = pkg!.split('/').pop() const url = `https://registry.npmjs.org/${pkg}/-/${base}-${version}.tgz` - const destDir = path.join(RACK, name, version!) + const destDir = path.join(RACK_DIR, name, version!) if (!existsSync(destDir)) { console.log(`[external-tools] downloading ${name}@${version} (npm)`) const buf = await download(url, pin.integrity!) @@ -219,7 +211,7 @@ async function installTool(name: string, tools: Record): Promis const pkgJson = JSON.parse(readFileSync(path.join(destDir, 'package/package.json'), 'utf8')) const binRel = typeof pkgJson.bin === 'string' ? pkgJson.bin : Object.values(pkgJson.bin ?? {})[0] if (binRel) { - const wrapper = path.join(RACK, name, `${name}-wrapper`) + const wrapper = path.join(RACK_DIR, name, `${name}-wrapper`) writeFileSync( wrapper, `#!/usr/bin/env bash\nexec node '${path.join(destDir, 'package', binRel as string)}' "$@"\n`, @@ -245,16 +237,16 @@ async function installTool(name: string, tools: Record): Promis * sfw shims: tiny wrappers named after each package manager that route the * real invocation through the firewall. A sentinel env var breaks the * recursion when sfw itself re-invokes the tool; the real binary is found - * by stripping the wheelhouse bin dir out of PATH. + * by stripping the rack's bin dir out of PATH. */ function writeShims(): void { - mkdirSync(BIN, { recursive: true }) + mkdirSync(BIN_DIR, { recursive: true }) for (const cmd of SFW_ECOSYSTEMS) { - const sentinel = `SOCKET_SHIM_ACTIVE_${cmd.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}` + const sentinel = `SFW_SHIM_ACTIVE_${cmd.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}` const body = `#!/usr/bin/env bash # sfw shim for ${cmd} — managed by scripts/soak/external-tools.mts --shims set -euo pipefail -CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\\n' | grep -vFx '${BIN}' | paste -sd ':' -) +CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\\n' | grep -vFx '${BIN_DIR}' | paste -sd ':' -) REAL=$(PATH="$CLEAN_PATH" command -v '${cmd}' || true) if [ -n "\${${sentinel}:-}" ] || [ -z "$REAL" ] || ! command -v sfw >/dev/null 2>&1; then [ -n "$REAL" ] && exec "$REAL" "$@" @@ -263,15 +255,19 @@ fi export ${sentinel}=1 exec sfw '${cmd}' "$@" ` - const shim = path.join(BIN, cmd) + const shim = path.join(BIN_DIR, cmd) writeFileSync(shim, body) chmodSync(shim, 0o755) } - console.log(`[external-tools] wrote sfw shims for ${SFW_ECOSYSTEMS.join(', ')} in ${BIN}`) - console.log(`[external-tools] prepend ${BIN} to PATH to activate`) + console.log(`[external-tools] wrote sfw shims for ${SFW_ECOSYSTEMS.join(', ')} in ${BIN_DIR}`) + console.log(`[external-tools] prepend ${BIN_DIR} to PATH to activate`) } async function main(argv: string[] = process.argv.slice(2)): Promise { + if (argv.includes('--print-bin')) { + console.log(BIN_DIR) + return 0 + } const tools = loadTools() if (argv.includes('--check') || argv.length === 0) { const problems = checkPins(tools) diff --git a/scripts/soak/paths.mts b/scripts/soak/paths.mts new file mode 100644 index 000000000..7800ddf1d --- /dev/null +++ b/scripts/soak/paths.mts @@ -0,0 +1,56 @@ +/** + * @file 1 path, 1 reference — every filesystem location the soak + + * external-tools scripts touch is declared here exactly once. Scripts + * import from this module instead of re-deriving paths, so a surface can + * move (or differ between repos carrying these scripts) with a one-line + * change. + */ + +import { existsSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') + +// Soak surfaces (repo-relative). aube keeps its npm surfaces in docs/ — +// that's where the package.json lives and where aube itself reads workspace +// yaml + npmrc; a ROOT pnpm-workspace.yaml would re-anchor the docs install +// at the repo root. +export const SURFACES = { + cargoConfig: '.cargo/config.toml', + npmrc: 'docs/.npmrc', + workspaceYaml: 'docs/pnpm-workspace.yaml', + tazeConfig: 'docs/taze.config.mts', +} + +// The directory holding the npm package the soak governs (taze runs here, +// the repo's installer refreshes this package's lockfile). +export const NPM_PKG_DIR = path.join(REPO_ROOT, 'docs') + +// Lockfile refreshers tried in order after taze rewrites package.json. +export const NPM_INSTALLERS: string[][] = [ + [path.join(REPO_ROOT, 'target/debug/aube'), 'install'], + ['aube', 'install'], +] + +// rustup's cargo shim — the only cargo that reads rust-toolchain.toml and +// therefore the only one whose `cargo update` honors the [unstable] +// min-publish-age soak. +export const RUSTUP_CARGO = path.join(os.homedir(), '.cargo/bin/cargo') + +// Pinned external tool manifest + the local tool rack it installs into: +// exact versions under rack///, flat PATH handles in bin/. +export const EXTERNAL_TOOLS_JSON = path.join(REPO_ROOT, 'external-tools.json') + +const XDG_DATA_HOME = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share') +export const DEV_TOOLS_DIR = path.join(XDG_DATA_HOME, 'aube/dev-tools') +export const RACK_DIR = path.join(DEV_TOOLS_DIR, 'rack') +export const BIN_DIR = path.join(DEV_TOOLS_DIR, 'bin') + +export function assertRepoRoot(): void { + if (!existsSync(path.join(REPO_ROOT, 'external-tools.json'))) { + throw new Error(`repo root not found at ${REPO_ROOT}`) + } +} diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts index 9042cc656..103ec3ee9 100644 --- a/scripts/soak/soak.mts +++ b/scripts/soak/soak.mts @@ -21,7 +21,6 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import process from 'node:process' -import { fileURLToPath } from 'node:url' import { ANNOTATION_RE, @@ -31,18 +30,7 @@ import { addDaysIso, todayIso, } from './constants.mts' - -const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') - -// Per-repo surface map — the only block that differs between repos -// carrying this script (aube keeps its npm surfaces in docs/, where the -// package.json lives and aube itself reads workspace yaml + npmrc). -export const SURFACES = { - cargoConfig: '.cargo/config.toml', - npmrc: 'docs/.npmrc', - workspaceYaml: 'docs/pnpm-workspace.yaml', - tazeConfig: 'docs/taze.config.mts', -} +import { REPO_ROOT, SURFACES } from './paths.mts' export interface Finding { file: string diff --git a/scripts/soak/update-deps.mts b/scripts/soak/update-deps.mts index 0fcd51eb3..9cba5f828 100644 --- a/scripts/soak/update-deps.mts +++ b/scripts/soak/update-deps.mts @@ -16,20 +16,10 @@ import { spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' -import os from 'node:os' import path from 'node:path' import process from 'node:process' -import { fileURLToPath } from 'node:url' -const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') - -// Per-repo knobs — the only block that differs between repos carrying this -// script (aube's npm package lives in docs/ and installs with aube itself). -const NPM_DIR = path.join(REPO_ROOT, 'docs') -const INSTALLERS = [ - [path.join(REPO_ROOT, 'target/debug/aube'), 'install'], - ['aube', 'install'], -] +import { NPM_INSTALLERS, NPM_PKG_DIR, REPO_ROOT, RUSTUP_CARGO } from './paths.mts' function run(cmd: string, args: string[], cwd: string): number { console.log(`[update-deps] ${cmd} ${args.join(' ')} (in ${path.relative(REPO_ROOT, cwd) || '.'})`) @@ -38,21 +28,21 @@ function run(cmd: string, args: string[], cwd: string): number { } function updateNpm(dryRun: boolean): number { - const taze = path.join(NPM_DIR, 'node_modules/.bin/taze') + const taze = path.join(NPM_PKG_DIR, 'node_modules/.bin/taze') if (!existsSync(taze)) { - console.error(`[update-deps] taze not installed — run the installer in ${NPM_DIR} first`) + console.error(`[update-deps] taze not installed — run the installer in ${NPM_PKG_DIR} first`) return 1 } const args = dryRun ? [] : ['--write'] - const status = run(taze, args, NPM_DIR) + const status = run(taze, args, NPM_PKG_DIR) if (status !== 0 || dryRun) { return status } - for (const [cmd, ...args] of INSTALLERS) { - if (cmd.includes('/') && !existsSync(cmd)) { + for (const [cmd, ...args] of NPM_INSTALLERS) { + if (cmd!.includes('/') && !existsSync(cmd!)) { continue } - return run(cmd!, args, NPM_DIR) + return run(cmd!, args, NPM_PKG_DIR) } console.error('[update-deps] no installer found — refresh the lockfile manually') return 1 @@ -63,12 +53,11 @@ function updateCargo(dryRun: boolean): number { // rust-toolchain.toml nightly honors it, and only rustup's cargo shim // reads rust-toolchain.toml. A non-rustup cargo (e.g. Homebrew stable) // would silently update WITHOUT the soak — refuse that. - const rustupCargo = path.join(os.homedir(), '.cargo/bin/cargo') - if (!existsSync(rustupCargo)) { + if (!existsSync(RUSTUP_CARGO)) { console.error('[update-deps] rustup cargo shim not found — cargo update would bypass the min-publish-age soak') return 1 } - return run(rustupCargo, dryRun ? ['update', '--dry-run'] : ['update'], REPO_ROOT) + return run(RUSTUP_CARGO, dryRun ? ['update', '--dry-run'] : ['update'], REPO_ROOT) } function main(argv: string[] = process.argv.slice(2)): number { From 5cd4f689691659e86597b6e6e6fae6663351342e Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:29:51 -0400 Subject: [PATCH 10/29] feat(soak): enforce catalog-shadowed pin lockstep When a package.json next to the workspace yaml pins a cataloged package to an exact version instead of catalog: (npm-cli compat), soak.mts now fails check unless the two versions match. catalog: references no-op. --- scripts/soak/soak.mts | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts index 103ec3ee9..34d6adcd2 100644 --- a/scripts/soak/soak.mts +++ b/scripts/soak/soak.mts @@ -191,6 +191,44 @@ export function parseExcludeEntries(body: string): ExcludeEntry[] { return out } +/** + * Catalog-shadowed pins stay in lockstep: when a package.json next to the + * workspace yaml pins a cataloged package to an exact version instead of + * `catalog:` (npm-cli compat — npm can't parse the protocol), the two + * versions must match. `catalog:` references no-op here. + */ +export function checkCatalogParity( + yamlBody: string, + pkgJson: string, + yamlFile: string, +): Finding[] { + const out: Finding[] = [] + const catalog: Record = {} + const block = /^catalog:\s*\n((?:[ \t]+\S.*\n?)*)/m.exec(yamlBody)?.[1] ?? '' + for (const m of block.matchAll(/^[ \t]+['"]?([^'":\s]+)['"]?:\s*['"]?([^'"\s]+)['"]?\s*$/gm)) { + catalog[m[1]!] = m[2]! + } + const pkg = JSON.parse(pkgJson) + const declared: Record = { + ...pkg.dependencies, + ...pkg.devDependencies, + } + for (const [name, version] of Object.entries(catalog)) { + const spec = declared[name] + if (spec === undefined || spec === 'catalog:' || spec === version) { + continue + } + out.push({ + file: yamlFile, + what: `catalog-shadowed pin '${name}' out of lockstep`, + saw: `catalog ${version} vs package.json ${spec}`, + wanted: 'identical versions (or a catalog: reference)', + fix: `bump both together — the catalog entry is the reference`, + }) + } + return out +} + export function checkTazeConfig(body: string, file: string): Finding[] { const out: Finding[] = [] if (!body.includes('maturityPeriod')) { @@ -306,6 +344,19 @@ export function main(argv: string[] = process.argv.slice(2)): number { findings.push(...s.check(body, s.rel)) } + // Catalog <-> package.json lockstep for the package next to the yaml. + const yamlAbs = path.join(REPO_ROOT, SURFACES.workspaceYaml) + const pkgAbs = path.join(path.dirname(yamlAbs), 'package.json') + if (existsSync(yamlAbs) && existsSync(pkgAbs)) { + findings.push( + ...checkCatalogParity( + readFileSync(yamlAbs, 'utf8'), + readFileSync(pkgAbs, 'utf8'), + SURFACES.workspaceYaml, + ), + ) + } + report(findings, quiet) return findings.length === 0 ? 0 : 1 } From aa2f1722a69e3f625b20282aad69ec95d4e9b8ec Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:33:48 -0400 Subject: [PATCH 11/29] fix(soak): make update-deps dry-run actually dry The taze config sets write: true, so a dry run must pass --no-write explicitly or it rewrites package.json; also suppress the ni install prompt with --no-install. --- scripts/soak/update-deps.mts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/soak/update-deps.mts b/scripts/soak/update-deps.mts index 9cba5f828..9227148e9 100644 --- a/scripts/soak/update-deps.mts +++ b/scripts/soak/update-deps.mts @@ -33,7 +33,9 @@ function updateNpm(dryRun: boolean): number { console.error(`[update-deps] taze not installed — run the installer in ${NPM_PKG_DIR} first`) return 1 } - const args = dryRun ? [] : ['--write'] + // The taze config sets `write: true`; a dry run must override it + // explicitly or "dry" would still rewrite package.json. + const args = dryRun ? ['--no-write', '--no-install'] : ['--write'] const status = run(taze, args, NPM_PKG_DIR) if (status !== 0 || dryRun) { return status From 7a593b5e9888425acaee3d0462466a38b9e7ac07 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:35:35 -0400 Subject: [PATCH 12/29] fix(soak): timeout stalled external-tool downloads A hung release or registry response would stall CI indefinitely; abort after 120s (generous for the largest pinned binary on a slow runner). --- scripts/soak/external-tools.mts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index c1b3357a9..41dd32cf1 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -112,7 +112,13 @@ async function download(url: string, expectedSri: string): Promise { if (process.env.GITHUB_TOKEN) { headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}` } - const res = await fetch(url, { headers, redirect: 'follow' }) + // Fail fast on a stalled release/registry response instead of hanging + // CI; 120s is generous for the largest pinned binary on a slow runner. + const res = await fetch(url, { + headers, + redirect: 'follow', + signal: AbortSignal.timeout(120_000), + }) if (!res.ok) { throw new Error(`download failed ${res.status} ${url}`) } From f2c6590c20f424ce682e5162ce62639f556afa14 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:47:59 -0400 Subject: [PATCH 13/29] fix(soak): harden gates and installer against review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reject flow-style minimumReleaseAgeExclude (invisible to the block parser = unvalidated bypass) and parse entries with trailing comments; catalog parity tolerates blank lines in the block - entrypoint guard uses realpath + pathToFileURL so symlinked or space-containing checkouts don't silently no-op the gate - linkHandle force-removes dangling handles (EEXIST wedge) - extract .zip release assets via bsdtar and verify the binary landed - GITHUB_TOKEN only sent to github.com, never the npm registry - npm-purl installs key completion off the extracted manifest so an interrupted extract self-heals - update-deps runs every requested ecosystem and reports spawn errors - sfw install/shims skip cleanly on windows (POSIX-only mechanism) - Socket credential is SOCKET_SECURITY_KEY everywhere — no SOCKET_API_* aliasing in CI env or the enterprise-tier check - buildkite image verifies the mise install instead of trusting an unchecked curl|sh --- .buildkite/linux-agent.Dockerfile | 9 ++++- .github/workflows/ci.yml | 10 ++--- .rules | 2 +- docs/pnpm-workspace.yaml | 8 ++-- scripts/soak/external-tools.mts | 61 ++++++++++++++++++++++--------- scripts/soak/soak.mts | 24 ++++++++++-- scripts/soak/update-deps.mts | 30 +++++++++------ 7 files changed, 100 insertions(+), 44 deletions(-) diff --git a/.buildkite/linux-agent.Dockerfile b/.buildkite/linux-agent.Dockerfile index 1ab71dfdb..f629fcde0 100644 --- a/.buildkite/linux-agent.Dockerfile +++ b/.buildkite/linux-agent.Dockerfile @@ -17,7 +17,14 @@ RUN apt-get update \ xz-utils \ && rm -rf /var/lib/apt/lists/* -RUN curl https://mise.run | sh +# -f + explicit installer file: a piped `curl | sh` with no -f would let a +# network-level curl failure produce an empty script and a silently +# mise-less image; verifying the binary afterwards fails the layer loudly. +RUN set -eux; \ + curl --proto '=https' --tlsv1.2 -fsSL https://mise.run -o /tmp/mise-install.sh; \ + sh /tmp/mise-install.sh; \ + rm /tmp/mise-install.sh; \ + /root/.local/bin/mise --version # Pre-bake the repo's toolchains with MINIMAL profiles + only the components # jobs use (the default profile ships rust-docs, ~600 MB per toolchain): # - nightly-2026-07-04: the rust-toolchain.toml pin (rustfmt/clippy for diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02beb4527..bdff4807b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,12 +72,10 @@ jobs: contents: read env: RUSTFLAGS: "-D warnings" - # Socket credentials: the SOCKET_SECURITY_KEY repo secret feeds both - # names sfw reads (SOCKET_API_KEY primary, SOCKET_API_TOKEN - # forward-canonical). Until the secret lands these are empty and sfw - # runs in its keyless free tier — the firewall is active either way. - SOCKET_API_KEY: ${{ secrets.SOCKET_SECURITY_KEY }} - SOCKET_API_TOKEN: ${{ secrets.SOCKET_SECURITY_KEY }} + # Until the SOCKET_SECURITY_KEY repo secret lands this is empty and + # sfw runs in its keyless free tier — the firewall is active either + # way and upgrades itself to enterprise when the secret appears. + SOCKET_SECURITY_KEY: ${{ secrets.SOCKET_SECURITY_KEY }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: diff --git a/.rules b/.rules index d26f815ba..a6ccd541f 100644 --- a/.rules +++ b/.rules @@ -250,7 +250,7 @@ soak: - docs/taze.config.mts maturityPeriod (imports SOAK_DAYS) manager: mise run soak (check, CI-gated in test-linux) / soak:fix (syncs windows, prunes expired excludes) / deps:update (taze + cargo update, both soaked) excludes: version-pinned minimumReleaseAgeExclude entries REQUIRE `# published: YYYY-MM-DD | removable: YYYY-MM-DD` on the line above (removable = published + 7d); bare names/globs are standing trust and need no annotation; expired pins are pruned by soak:fix - socket_firewall: CI installs sfw (free tier keyless; enterprise auto-selected when the SOCKET_SECURITY_KEY repo secret lands, fed to SOCKET_API_KEY/SOCKET_API_TOKEN) and shims npm/yarn/pnpm/pip/uv/cargo via scripts/soak/external-tools.mts --shims; the buildkite agent image pre-bakes the same binary + shims + socket_firewall: CI installs sfw (free tier keyless; enterprise auto-selected when the SOCKET_SECURITY_KEY repo secret lands — the one env var every Socket product reads, no aliasing) and shims npm/yarn/pnpm/pip/uv/cargo via scripts/soak/external-tools.mts --shims; the buildkite agent image pre-bakes the same binary + shims git: forbidden[2]: diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml index cebb8ca07..b570153d4 100644 --- a/docs/pnpm-workspace.yaml +++ b/docs/pnpm-workspace.yaml @@ -15,6 +15,8 @@ minimumReleaseAge: 10080 # Exclusions: bare names / scope globs express standing trust; version pins # are dated soak bypasses and REQUIRE, on the line above: # # published: YYYY-MM-DD | removable: YYYY-MM-DD (removable = published + 7d) -# `mise run soak` rejects unannotated or expired pins; `soak:fix` prunes -# expired ones. -# minimumReleaseAgeExclude: [] +# `mise run soak` rejects unannotated or expired pins (and flow-style [..] +# lists, which the gate can't validate); `soak:fix` prunes expired ones. +# minimumReleaseAgeExclude: +# # published: 2026-01-01 | removable: 2026-01-08 +# - 'example@1.2.3' diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 41dd32cf1..09dc9b72d 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -15,10 +15,10 @@ * into BIN_DIR so installs route through the firewall * - `--print-bin` print BIN_DIR (for `>> $GITHUB_PATH` in CI) * - * `sfw` resolves to sfw-enterprise when SOCKET_API_KEY or SOCKET_API_TOKEN - * is set (either may be fed from a repo secret such as - * SOCKET_SECURITY_KEY), else sfw-free — free tier needs no key, so CI is - * firewalled from day one and upgrades itself when the secret lands. + * `sfw` resolves to sfw-enterprise when SOCKET_SECURITY_KEY is set (the + * one env var every Socket product reads), else sfw-free — free tier + * needs no key, so CI is firewalled from day one and upgrades itself + * when the repo secret lands. */ import { createHash } from 'node:crypto' @@ -28,13 +28,14 @@ import { existsSync, mkdirSync, readFileSync, + realpathSync, rmSync, symlinkSync, - unlinkSync, writeFileSync, } from 'node:fs' import path from 'node:path' import process from 'node:process' +import { pathToFileURL } from 'node:url' import { ANNOTATION_RE, SOAK_DAYS, addDaysIso } from './constants.mts' import { BIN_DIR, EXTERNAL_TOOLS_JSON, RACK_DIR } from './paths.mts' @@ -109,7 +110,10 @@ export function checkPins(tools: Record): string[] { async function download(url: string, expectedSri: string): Promise { const headers: Record = {} - if (process.env.GITHUB_TOKEN) { + // Only GitHub gets the token (private release assets); sending it to any + // other host (e.g. the npm registry for purl tools) would leak the + // credential. Cross-origin redirects strip the header automatically. + if (process.env.GITHUB_TOKEN && new URL(url).hostname === 'github.com') { headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}` } // Fail fast on a stalled release/registry response instead of hanging @@ -133,9 +137,9 @@ async function download(url: string, expectedSri: string): Promise { function linkHandle(target: string, name: string): void { mkdirSync(BIN_DIR, { recursive: true }) const handle = path.join(BIN_DIR, name) - if (existsSync(handle)) { - unlinkSync(handle) - } + // force also removes a DANGLING handle (existsSync would report false + // for one and a bare symlink would then throw EEXIST). + rmSync(handle, { force: true }) symlinkSync(target, handle) } @@ -157,13 +161,18 @@ async function installAssetTool(name: string, pin: ToolPin): Promise { console.log(`[external-tools] downloading ${name}@${pin.version} (${plat.asset})`) const buf = await download(url, plat.integrity) mkdirSync(destDir, { recursive: true }) - if (plat.asset.endsWith('.tar.gz')) { + if (plat.asset.endsWith('.tar.gz') || plat.asset.endsWith('.zip')) { const archive = path.join(destDir, plat.asset) writeFileSync(archive, buf) - const res = spawnSync('tar', ['-xzf', archive, '-C', destDir], { stdio: 'inherit' }) + // bsdtar (macOS + Windows runners) extracts zip via plain -xf too. + const args = plat.asset.endsWith('.zip') ? ['-xf', archive, '-C', destDir] : ['-xzf', archive, '-C', destDir] + const res = spawnSync('tar', args, { stdio: 'inherit' }) rmSync(archive) if (res.status !== 0) { - throw new Error(`${name}: tar extract failed`) + throw new Error(`${name}: archive extract failed`) + } + if (!existsSync(destBin)) { + throw new Error(`${name}: ${binName} not found in extracted archive`) } } else { writeFileSync(destBin, buf) @@ -174,13 +183,19 @@ async function installAssetTool(name: string, pin: ToolPin): Promise { } function hasSocketToken(): boolean { - return Boolean(process.env.SOCKET_API_KEY || process.env.SOCKET_API_TOKEN) + return Boolean(process.env.SOCKET_SECURITY_KEY) } async function installTool(name: string, tools: Record): Promise { // `sfw` is a flavor pair: the enterprise binary when a Socket token is - // present (repo secret), the keyless free tier otherwise. + // present (repo secret), the keyless free tier otherwise. The firewall + // shim mechanism is POSIX (bash shims, extension-less symlink handles) — + // skip cleanly on Windows rather than install something unusable. if (name === 'sfw') { + if (process.platform === 'win32') { + console.log('[external-tools] sfw shims are POSIX-only — skipping on windows') + return + } name = hasSocketToken() ? 'sfw-enterprise' : 'sfw-free' } const pin = tools[name] @@ -202,7 +217,12 @@ async function installTool(name: string, tools: Record): Promis const base = pkg!.split('/').pop() const url = `https://registry.npmjs.org/${pkg}/-/${base}-${version}.tgz` const destDir = path.join(RACK_DIR, name, version!) - if (!existsSync(destDir)) { + // Completion marker is the extracted manifest, not the directory: a + // failed/interrupted extract leaves the dir behind and would otherwise + // wedge every later install. Reset and redo instead. + const manifest = path.join(destDir, 'package/package.json') + if (!existsSync(manifest)) { + rmSync(destDir, { recursive: true, force: true }) console.log(`[external-tools] downloading ${name}@${version} (npm)`) const buf = await download(url, pin.integrity!) mkdirSync(destDir, { recursive: true }) @@ -214,7 +234,7 @@ async function installTool(name: string, tools: Record): Promis throw new Error(`${name}: tar extract failed`) } } - const pkgJson = JSON.parse(readFileSync(path.join(destDir, 'package/package.json'), 'utf8')) + const pkgJson = JSON.parse(readFileSync(manifest, 'utf8')) const binRel = typeof pkgJson.bin === 'string' ? pkgJson.bin : Object.values(pkgJson.bin ?? {})[0] if (binRel) { const wrapper = path.join(RACK_DIR, name, `${name}-wrapper`) @@ -246,6 +266,10 @@ async function installTool(name: string, tools: Record): Promis * by stripping the rack's bin dir out of PATH. */ function writeShims(): void { + if (process.platform === 'win32') { + console.log('[external-tools] sfw shims are POSIX-only — skipping on windows') + return + } mkdirSync(BIN_DIR, { recursive: true }) for (const cmd of SFW_ECOSYSTEMS) { const sentinel = `SFW_SHIM_ACTIVE_${cmd.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}` @@ -304,7 +328,10 @@ async function main(argv: string[] = process.argv.slice(2)): Promise { return 0 } -const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}` +// realpath + pathToFileURL so symlinked checkouts and paths needing URL +// encoding still register as the entrypoint (ESM realpaths import.meta.url). +const isMain = + process.argv[1] && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url if (isMain) { main().then( code => { diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts index 34d6adcd2..7eb31c1fe 100644 --- a/scripts/soak/soak.mts +++ b/scripts/soak/soak.mts @@ -18,9 +18,10 @@ * Usage: node scripts/soak/soak.mts [--check|--fix] [--quiet] */ -import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs' import path from 'node:path' import process from 'node:process' +import { pathToFileURL } from 'node:url' import { ANNOTATION_RE, @@ -107,6 +108,18 @@ export function checkWorkspaceYaml(body: string, file: string): Finding[] { export function checkExcludeAnnotations(body: string, file: string): Finding[] { const out: Finding[] = [] const today = todayIso() + // Flow style would be invisible to the block parser below — an + // unvalidated, never-expiring bypass. One canonical shape only. + if (/^minimumReleaseAgeExclude:\s*\[/m.test(body)) { + out.push({ + file, + what: 'minimumReleaseAgeExclude flow style', + saw: 'inline [...] list', + wanted: 'a block list (one annotated `- entry` per line)', + fix: 'rewrite as a block list so every pin can carry its annotation', + }) + return out + } for (const entry of parseExcludeEntries(body)) { if (!VERSION_PIN_RE.test(entry.name)) { continue @@ -166,7 +179,7 @@ export function parseExcludeEntries(body: string): ExcludeEntry[] { if (!inBlock) { continue } - const item = /^(\s+)-\s*['"]?([^'"#\s]+)['"]?\s*$/.exec(line) + const item = /^(\s+)-\s*['"]?([^'"#\s]+)['"]?\s*(?:#.*)?$/.exec(line) if (!item) { // Comments stay inside the block; anything else at column 0 ends it. if (/^\S/.test(line)) { @@ -204,7 +217,7 @@ export function checkCatalogParity( ): Finding[] { const out: Finding[] = [] const catalog: Record = {} - const block = /^catalog:\s*\n((?:[ \t]+\S.*\n?)*)/m.exec(yamlBody)?.[1] ?? '' + const block = /^catalog:\s*\n((?:[ \t]+\S.*\n?|\s*\n)*)/m.exec(yamlBody)?.[1] ?? '' for (const m of block.matchAll(/^[ \t]+['"]?([^'":\s]+)['"]?:\s*['"]?([^'"\s]+)['"]?\s*$/gm)) { catalog[m[1]!] = m[2]! } @@ -361,7 +374,10 @@ export function main(argv: string[] = process.argv.slice(2)): number { return findings.length === 0 ? 0 : 1 } -const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}` +// realpath + pathToFileURL so symlinked checkouts and paths needing URL +// encoding still register as the entrypoint (ESM realpaths import.meta.url). +const isMain = + process.argv[1] && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url if (isMain) { process.exitCode = main() } diff --git a/scripts/soak/update-deps.mts b/scripts/soak/update-deps.mts index 9227148e9..b6cabe654 100644 --- a/scripts/soak/update-deps.mts +++ b/scripts/soak/update-deps.mts @@ -15,15 +15,19 @@ */ import { spawnSync } from 'node:child_process' -import { existsSync } from 'node:fs' +import { existsSync, realpathSync } from 'node:fs' import path from 'node:path' import process from 'node:process' +import { pathToFileURL } from 'node:url' import { NPM_INSTALLERS, NPM_PKG_DIR, REPO_ROOT, RUSTUP_CARGO } from './paths.mts' function run(cmd: string, args: string[], cwd: string): number { console.log(`[update-deps] ${cmd} ${args.join(' ')} (in ${path.relative(REPO_ROOT, cwd) || '.'})`) const res = spawnSync(cmd, args, { cwd, stdio: 'inherit' }) + if (res.error) { + console.error(`[update-deps] ${cmd}: ${res.error.message}`) + } return res.status ?? 1 } @@ -64,19 +68,21 @@ function updateCargo(dryRun: boolean): number { function main(argv: string[] = process.argv.slice(2)): number { const dryRun = argv.includes('--dry-run') - const onlyNpm = argv.includes('--npm') - const onlyCargo = argv.includes('--cargo') - let status = 0 - if (!onlyCargo) { - status ||= updateNpm(dryRun) - } - if (!onlyNpm) { - status ||= updateCargo(dryRun) - } - return status + const npmFlag = argv.includes('--npm') + const cargoFlag = argv.includes('--cargo') + // No flag = both; naming both explicitly also means both. Run every + // requested ecosystem even if an earlier one fails, then aggregate. + const wantNpm = npmFlag || !cargoFlag + const wantCargo = cargoFlag || !npmFlag + const npmStatus = wantNpm ? updateNpm(dryRun) : 0 + const cargoStatus = wantCargo ? updateCargo(dryRun) : 0 + return npmStatus || cargoStatus } -const isMain = process.argv[1] && import.meta.url === `file://${process.argv[1]}` +// realpath + pathToFileURL so symlinked checkouts and paths needing URL +// encoding still register as the entrypoint (ESM realpaths import.meta.url). +const isMain = + process.argv[1] && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url if (isMain) { process.exitCode = main() } From 269bfab41b0c49b8fc29653b0768b0ef5763a147 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 14:57:13 -0400 Subject: [PATCH 14/29] build: gate docker prebake pins against tracked sources, add untracked The buildkite image build context is .buildkite/ alone, so its embedded sfw pin (version + per-arch sha512 hex) and toolchain channels can't derive from external-tools.json / rust-toolchain.toml directly; external-tools.mts --check now hex-decodes the SRI pins and fails on any drift between the Dockerfile copies and the tracked sources (same parity idiom as the soak data files). Adds untracked 1.6.4 through the docs catalog for ignore-file generation on docker-context builds. --- docs/aube-lock.yaml | 636 ++++++++++++++++++++++++++++++++ docs/package.json | 1 + docs/pnpm-workspace.yaml | 1 + scripts/soak/external-tools.mts | 73 +++- scripts/soak/paths.mts | 7 + 5 files changed, 717 insertions(+), 1 deletion(-) diff --git a/docs/aube-lock.yaml b/docs/aube-lock.yaml index e72d6536d..6411799db 100644 --- a/docs/aube-lock.yaml +++ b/docs/aube-lock.yaml @@ -9,6 +9,9 @@ catalogs: taze: specifier: 19.14.1 version: 19.14.1 + untracked: + specifier: 1.6.4 + version: 1.6.4 importers: @@ -24,6 +27,9 @@ importers: taze: specifier: 'catalog:' version: 19.14.1 + untracked: + specifier: 'catalog:' + version: 1.6.4 vitepress: specifier: ^1.5.0 version: 1.6.4(postcss@8.5.15) @@ -326,6 +332,14 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@0.14.0': + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + + '@szmarczak/http-timer@1.1.2': + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} @@ -452,32 +466,95 @@ packages: resolution: {integrity: sha512-af+rI+tUVeS9KWHPAZQHIHPOIC3StPRR6IwQu2nz1aQoTL6Gs5Ty3KsHCgbXMHOpoh9QqSjq8F3KJ8xmaCZSBA==} engines: {node: '>= 14.0.0'} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} + cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + configstore@5.0.1: + resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} + engines: {node: '>=8'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -495,9 +572,22 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -507,6 +597,10 @@ packages: engines: {node: '>=12'} hasBin: true + escape-goat@2.1.1: + resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} + engines: {node: '>=8'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -530,6 +624,33 @@ packages: fzf@0.5.2: resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-yarn@2.1.0: + resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} + engines: {node: '>=8'} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -542,17 +663,91 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + import-lazy@2.1.0: + resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} + engines: {node: '>=4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-npm@5.0.0: + resolution: {integrity: sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==} + engines: {node: '>=10'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + is-yarn-global@0.3.0: + resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + + keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + + latest-version@5.1.0: + resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} + engines: {node: '>=8'} + + lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -578,12 +773,23 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minisearch@7.2.0: resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + nanoid@3.3.15: resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -592,9 +798,16 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -602,6 +815,14 @@ packages: oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + + package-json@6.5.0: + resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} + engines: {node: '>=8'} + package-manager-detector@1.7.0: resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} @@ -628,12 +849,27 @@ packages: preact@10.29.2: resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pupa@2.1.1: + resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} + engines: {node: '>=8'} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -643,6 +879,17 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + registry-auth-token@4.2.2: + resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} + engines: {node: '>=6.0.0'} + + registry-url@5.1.0: + resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} + engines: {node: '>=8'} + + responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -658,6 +905,14 @@ packages: search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + semver-diff@3.1.1: + resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} + engines: {node: '>=8'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -670,6 +925,9 @@ packages: shiki@2.5.0: resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -685,13 +943,29 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + tabbable@6.5.0: resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} @@ -707,9 +981,24 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyspawn@1.5.7: + resolution: {integrity: sha512-EUrfYrlL9SvWuDjyzgF1NdILYHAOI/fosveOHbIcB9ZTaCu+IqPrA7df4M6Olf4vZUFrO9U0qPqOATLApk8WjA==} + engines: {node: '>= 20'} + + to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} @@ -719,6 +1008,10 @@ packages: unconfig@7.5.0: resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -734,6 +1027,19 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + untracked@1.6.4: + resolution: {integrity: sha512-Hd3JAnjdUYKbGpzr6xvAm9JkBw2yAHiD/dCZEh377veDHdSlnnKAHYOAvQ6UxxQCWq5KAggnPPhkg8MrQhLkRA==} + engines: {node: '>= 6'} + hasBin: true + + update-notifier@5.1.0: + resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==} + engines: {node: '>=10'} + + url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -791,6 +1097,24 @@ packages: typescript: optional: true + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -1079,6 +1403,12 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@0.14.0': {} + + '@szmarczak/http-timer@1.1.2': + dependencies: + defer-to-connect: 1.1.3 + '@types/estree@1.0.9': {} '@types/hast@3.0.4': @@ -1224,24 +1554,95 @@ snapshots: '@algolia/requester-fetch': 5.55.0 '@algolia/requester-node-http': 5.55.0 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + birpc@2.9.0: {} + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + cac@7.0.0: {} + cacheable-request@6.1.0: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 3.1.0 + lowercase-keys: 2.0.0 + normalize-url: 4.5.1 + responselike: 1.0.2 + + camelcase@6.3.0: {} + ccount@2.0.1: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} + ci-info@2.0.0: {} + + cli-boxes@2.2.1: {} + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + configstore@5.0.1: + dependencies: + dot-prop: 5.3.0 + graceful-fs: 4.2.11 + make-dir: 3.1.0 + unique-string: 2.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 4.0.0 + copy-anything@4.0.5: dependencies: is-what: 5.5.0 + crypto-random-string@2.0.0: {} + csstype@3.2.3: {} + decompress-response@3.3.0: + dependencies: + mimic-response: 1.0.1 + + deep-extend@0.6.0: {} + + defer-to-connect@1.1.3: {} + defu@6.1.7: {} dequal@2.0.3: {} @@ -1254,8 +1655,20 @@ snapshots: dependencies: dequal: 2.0.3 + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + duplexer3@0.1.5: {} + emoji-regex-xs@1.0.0: {} + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + entities@7.0.1: {} esbuild@0.21.5: @@ -1265,6 +1678,8 @@ snapshots: '@esbuild/linux-arm64': 0.21.5 '@esbuild/linux-x64': 0.21.5 + escape-goat@2.1.1: {} + estree-walker@2.0.2: {} fdir@6.5.0(picomatch@4.0.5): @@ -1280,6 +1695,38 @@ snapshots: fzf@0.5.2: {} + get-stream@4.1.0: + dependencies: + pump: 3.0.4 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + got@9.6.0: + dependencies: + '@sindresorhus/is': 0.14.0 + '@szmarczak/http-timer': 1.1.2 + cacheable-request: 6.1.0 + decompress-response: 3.3.0 + duplexer3: 0.1.5 + get-stream: 4.1.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 1.1.0 + to-readable-stream: 1.0.0 + url-parse-lax: 3.0.0 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-yarn@2.1.0: {} + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -1302,14 +1749,65 @@ snapshots: html-void-elements@3.0.0: {} + http-cache-semantics@4.2.0: {} + + import-lazy@2.1.0: {} + + imurmurhash@0.1.4: {} + + ini@1.3.8: {} + + ini@2.0.0: {} + + is-ci@2.0.0: + dependencies: + ci-info: 2.0.0 + + is-fullwidth-code-point@3.0.0: {} + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-npm@5.0.0: {} + + is-obj@2.0.0: {} + + is-path-inside@3.0.3: {} + + is-typedarray@1.0.0: {} + is-what@5.5.0: {} + is-yarn-global@0.3.0: {} + jiti@2.7.0: {} + joycon@3.1.1: {} + + json-buffer@3.0.0: {} + + keyv@3.1.0: + dependencies: + json-buffer: 3.0.0 + + latest-version@5.1.0: + dependencies: + package-json: 6.5.0 + + lowercase-keys@1.0.1: {} + + lowercase-keys@2.0.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + mark.js@8.11.1: {} mdast-util-to-hast@13.2.1: @@ -1343,20 +1841,32 @@ snapshots: mimic-function@5.0.1: {} + mimic-response@1.0.1: {} + + minimist@1.2.8: {} + minisearch@7.2.0: {} mitt@3.0.1: {} + mri@1.2.0: {} + nanoid@3.3.15: {} node-fetch-native@1.6.7: {} + normalize-url@4.5.1: {} + ofetch@1.5.1: dependencies: destr: 2.0.5 node-fetch-native: 1.6.7 ufo: 1.6.4 + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -1367,6 +1877,15 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + p-cancelable@1.1.0: {} + + package-json@6.5.0: + dependencies: + got: 9.6.0 + registry-auth-token: 4.2.2 + registry-url: 5.1.0 + semver: 6.3.1 + package-manager-detector@1.7.0: {} pathe@2.0.3: {} @@ -1389,10 +1908,28 @@ snapshots: preact@10.29.2: {} + prepend-http@2.0.0: {} + property-information@7.2.0: {} + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pupa@2.1.1: + dependencies: + escape-goat: 2.1.1 + quansync@1.0.0: {} + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -1403,6 +1940,18 @@ snapshots: dependencies: regex-utilities: 2.3.0 + registry-auth-token@4.2.2: + dependencies: + rc: 1.2.8 + + registry-url@5.1.0: + dependencies: + rc: 1.2.8 + + responselike@1.0.2: + dependencies: + lowercase-keys: 1.0.1 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -1424,6 +1973,12 @@ snapshots: search-insights@2.17.3: {} + semver-diff@3.1.1: + dependencies: + semver: 6.3.1 + + semver@6.3.1: {} + semver@7.8.5: {} sharp@0.35.2: @@ -1456,6 +2011,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} source-map-js@1.2.1: {} @@ -1464,15 +2021,31 @@ snapshots: speakingurl@14.0.1: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + superjson@2.2.6: dependencies: copy-anything: 4.0.5 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + tabbable@6.5.0: {} taze@19.14.1: @@ -1497,8 +2070,18 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinyspawn@1.5.7: {} + + to-readable-stream@1.0.0: {} + trim-lines@3.0.1: {} + type-fest@0.20.2: {} + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + ufo@1.6.4: {} unconfig-core@7.5.0: @@ -1514,6 +2097,10 @@ snapshots: quansync: 1.0.0 unconfig-core: 7.5.0 + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -1537,6 +2124,34 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + untracked@1.6.4: + dependencies: + joycon: 3.1.1 + mri: 1.2.0 + tinyspawn: 1.5.7 + update-notifier: 5.1.0 + + update-notifier@5.1.0: + dependencies: + boxen: 5.1.2 + chalk: 4.1.2 + configstore: 5.0.1 + has-yarn: 2.1.0 + import-lazy: 2.1.0 + is-ci: 2.0.0 + is-installed-globally: 0.4.0 + is-npm: 5.0.0 + is-yarn-global: 0.3.0 + latest-version: 5.1.0 + pupa: 2.1.1 + semver: 7.8.5 + semver-diff: 3.1.1 + xdg-basedir: 4.0.0 + + url-parse-lax@3.0.0: + dependencies: + prepend-http: 2.0.0 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -1610,6 +2225,27 @@ snapshots: '@vue/server-renderer': 3.5.38(vue@3.5.38) '@vue/shared': 3.5.38 + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + xdg-basedir@4.0.0: {} + yaml@2.9.0: {} zwitch@2.0.4: {} diff --git a/docs/package.json b/docs/package.json index 4c698d13d..009d05681 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,6 +24,7 @@ "devDependencies": { "sharp": "^0.35.0", "taze": "catalog:", + "untracked": "catalog:", "vitepress": "^1.5.0" } } diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml index b570153d4..121355d15 100644 --- a/docs/pnpm-workspace.yaml +++ b/docs/pnpm-workspace.yaml @@ -8,6 +8,7 @@ catalog: taze: 19.14.1 + untracked: 1.6.4 # Wait 7 days (10080 minutes) before installing newly published packages. minimumReleaseAge: 10080 diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 09dc9b72d..b75fecbff 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -38,7 +38,14 @@ import process from 'node:process' import { pathToFileURL } from 'node:url' import { ANNOTATION_RE, SOAK_DAYS, addDaysIso } from './constants.mts' -import { BIN_DIR, EXTERNAL_TOOLS_JSON, RACK_DIR } from './paths.mts' +import { + BIN_DIR, + DOCKER_PREBAKE, + EXTERNAL_TOOLS_JSON, + RACK_DIR, + REPO_ROOT, + RUST_TOOLCHAIN_TOML, +} from './paths.mts' const SFW_ECOSYSTEMS = ['npm', 'yarn', 'pnpm', 'pip', 'pip3', 'uv', 'cargo'] @@ -108,6 +115,57 @@ export function checkPins(tools: Record): string[] { return out } +function sriToHex(sri: string): string { + return Buffer.from(sri.slice('sha512-'.length), 'base64').toString('hex') +} + +/** + * Parity gate for the CI agent image: its build context can't reach the + * tracked pin sources, so the Dockerfile embeds copies of the sfw pin + * (version + per-arch sha512 hex) and the toolchain channels. Assert the + * copies match external-tools.json / rust-toolchain.toml so a pin bump + * can't silently strand the image on old bits. + */ +export function checkDockerPrebake( + dockerBody: string, + tools: Record, + toolchainToml: string, +): string[] { + const out: string[] = [] + const sfw = tools['sfw-free'] + const version = /rack\/sfw-free\/([^/\s]+)\//.exec(dockerBody)?.[1] + if (version !== sfw?.version) { + out.push( + `docker prebake: sfw-free version ${version ?? '(missing)'} != external-tools.json ${sfw?.version}`, + ) + } + const urlVersion = /sfw-free\/releases\/download\/v([^/\s]+)\//.exec(dockerBody)?.[1] + if (urlVersion !== sfw?.version) { + out.push( + `docker prebake: sfw-free download url v${urlVersion ?? '(missing)'} != external-tools.json ${sfw?.version}`, + ) + } + const pairs = [...dockerBody.matchAll(/asset=(\S+);\s*sha=([0-9a-f]{128})/g)] + if (pairs.length === 0) { + out.push('docker prebake: no asset/sha pin pairs found in the Dockerfile') + } + for (const [, asset, hex] of pairs) { + const plat = Object.values(sfw?.platforms ?? {}).find(p => p.asset === asset) + if (!plat) { + out.push(`docker prebake: asset ${asset} has no pin in external-tools.json`) + continue + } + if (sriToHex(plat.integrity) !== hex) { + out.push(`docker prebake: sha for ${asset} != hex of external-tools.json SRI`) + } + } + const channel = /^channel\s*=\s*"([^"]+)"/m.exec(toolchainToml)?.[1] + if (channel && !dockerBody.includes(`toolchain install ${channel} `)) { + out.push(`docker prebake: image does not pre-install the pinned toolchain ${channel}`) + } + return out +} + async function download(url: string, expectedSri: string): Promise { const headers: Record = {} // Only GitHub gets the token (private release assets); sending it to any @@ -301,6 +359,19 @@ async function main(argv: string[] = process.argv.slice(2)): Promise { const tools = loadTools() if (argv.includes('--check') || argv.length === 0) { const problems = checkPins(tools) + if (DOCKER_PREBAKE) { + const dockerAbs = path.join(REPO_ROOT, DOCKER_PREBAKE) + const toolchainAbs = path.join(REPO_ROOT, RUST_TOOLCHAIN_TOML) + if (existsSync(dockerAbs)) { + problems.push( + ...checkDockerPrebake( + readFileSync(dockerAbs, 'utf8'), + tools, + existsSync(toolchainAbs) ? readFileSync(toolchainAbs, 'utf8') : '', + ), + ) + } + } for (const p of problems) { console.error(`[external-tools] ${p}`) } diff --git a/scripts/soak/paths.mts b/scripts/soak/paths.mts index 7800ddf1d..cbb98e172 100644 --- a/scripts/soak/paths.mts +++ b/scripts/soak/paths.mts @@ -44,6 +44,13 @@ export const RUSTUP_CARGO = path.join(os.homedir(), '.cargo/bin/cargo') // exact versions under rack///, flat PATH handles in bin/. export const EXTERNAL_TOOLS_JSON = path.join(REPO_ROOT, 'external-tools.json') +// CI agent image that pre-bakes the pinned toolchain + sfw (null when the +// repo has no such image). The image builder's context is .buildkite/ +// alone, so the Dockerfile can't COPY the tracked pin sources — it embeds +// copies, and external-tools.mts --check asserts they haven't drifted. +export const DOCKER_PREBAKE: string | null = '.buildkite/linux-agent.Dockerfile' +export const RUST_TOOLCHAIN_TOML = 'rust-toolchain.toml' + const XDG_DATA_HOME = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share') export const DEV_TOOLS_DIR = path.join(XDG_DATA_HOME, 'aube/dev-tools') export const RACK_DIR = path.join(DEV_TOOLS_DIR, 'rack') From e16c3792ffd7fb8b808291b9d99c3a17a18686fb Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 15:20:31 -0400 Subject: [PATCH 15/29] build: pin pnpm 11.8 + npm 12 via the tool rack, dogfood dep installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pnpm 11.8.0 (per-platform SRI assets) and npm 12.0.0 (single registry tarball, soakBypass-annotated — inside the 7-day window by deliberate dated exception) join external-tools.json; CI installs both before the firewall shims, and the shims wrap the PINNED binaries directly (rack target embedded at shim-write time) instead of whatever PATH finds. External npm-packaged tools now materialize their runtime deps through the repo's own package manager first (aube, then pnpm/npm fallbacks) — installs are script-skipping and prod-only. CI asserts pnpm/npm resolve through the shims after install. --- .github/workflows/ci.yml | 8 +- external-tools.json | 60 ++++++++++++ scripts/soak/external-tools.mts | 156 +++++++++++++++++++++++--------- scripts/soak/paths.mts | 9 ++ 4 files changed, 190 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdff4807b..dc5eaaa01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,10 +90,14 @@ jobs: run: | node scripts/soak/soak.mts --check node scripts/soak/external-tools.mts --check - - name: socket firewall shims + - name: pinned package managers + socket firewall run: | - node scripts/soak/external-tools.mts --install sfw --shims + node scripts/soak/external-tools.mts --install pnpm --install npm --install sfw --shims node scripts/soak/external-tools.mts --print-bin >> "$GITHUB_PATH" + - name: assert pinned pms resolve through the shims + run: | + pnpm --version + npm --version - run: node benchmarks/validate-results.mts - run: mise run test - run: mise run lint diff --git a/external-tools.json b/external-tools.json index eebf454ef..efe0f3f7d 100644 --- a/external-tools.json +++ b/external-tools.json @@ -1,5 +1,65 @@ { "tools": { + "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" + ], + "platforms": { + "darwin-arm64": { + "asset": "pnpm-darwin-arm64.tar.gz", + "integrity": "sha512-kgYcLu653+Gx7l7qEtM2zMNLBvb96ABcRSkzOrbfvkndb+veMt6lHJK5r3wTfuRYlDb8Lnk/h0wUD6sP3taJ9g==" + }, + "darwin-x64": { + "asset": "pnpm-11.8.0.tgz", + "integrity": "sha512-wfXnxMskHI8XS3Q4UdgvQrgCMkr8iw8Ra5atsVqgZmSUjd42lgo7oQebpbSyndAUATW5S1tfUmNZIknWjlVfJg==" + }, + "linux-arm64": { + "asset": "pnpm-linux-arm64.tar.gz", + "integrity": "sha512-p1IcVUlwYf3OJQYmdHGFr08d1BOSatHEmLJSBSdoNPGqOqfslFHjSqiuZ/3yuQxxypqp8OfnM3ci3Jh7ZEBlSw==" + }, + "linux-arm64-musl": { + "asset": "pnpm-linux-arm64-musl.tar.gz", + "integrity": "sha512-u5Do3diwK7FL5vk+i/x2I4q4ujdZ5gSoLNgmlt1w2C7NOjAZOpDK0CJYM4gDa5BetEwzdNpuN/gaMqzJk6I5NA==" + }, + "linux-x64": { + "asset": "pnpm-linux-x64.tar.gz", + "integrity": "sha512-Sn7hG4Xsq6pmi8TE8lpIkwRzAYzf5qtFk8zSsZWkSJFcmv5FlsrO8UP3lLRI+ppcRNYS10FMCTwXe3Nt66A0Pg==" + }, + "linux-x64-musl": { + "asset": "pnpm-linux-x64-musl.tar.gz", + "integrity": "sha512-5WXlo2yCDmoBIue5iHRK3zNSPVAmzci+5RhuxwUA4hnH9W3TFjDnQHyAzDVnkY878mfTXE35THZeEc1OKYPmag==" + }, + "win-arm64": { + "asset": "pnpm-win32-arm64.zip", + "integrity": "sha512-iv1hJEj9FUiVFae2cmCYjbRK0Xn27Uh+kpglT2LWvQyl4WeIOgs1ouKrptVxk3X3t7DB5vuYKbsmE+9BnoQ4FA==" + }, + "win-x64": { + "asset": "pnpm-win32-x64.zip", + "integrity": "sha512-jyqMgedndbck/xJjXPem5Lw7V0YtDfiUJIB81e/hmnrg3yLKPoyCKO5ILegYkLPIr945IJRcVoJZwjtog8FCqg==" + } + } + }, + "npm": { + "notes": [ + "npm 12 (min-release-age support). ONE platform-agnostic registry tarball, single integrity", + "Installed from the pinned tarball only — never npm install -g npm, no self-update path" + ], + "description": "npm — pinned, SRI-verified registry tarball; installed without self-update", + "repository": "npm:npm", + "version": "12.0.0", + "soakBypass": { + "version": "12.0.0", + "published": "2026-07-09", + "removable": "2026-07-16" + }, + "integrity": "sha512-qzvPQfNSY7louiM6rv7dL0hi5esBGLn1lLwxbdyL5XOIssWzoYMwn8xqvWhYcZL6onTkenYSxrtKxsFrFbUFyw==" + }, "sfw-free": { "description": "Socket Firewall (free tier) — malware gate on dep installs.", "version": "1.13.1", diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index b75fecbff..8b5fe8387 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -28,6 +28,7 @@ import { existsSync, mkdirSync, readFileSync, + readlinkSync, realpathSync, rmSync, symlinkSync, @@ -42,6 +43,7 @@ import { BIN_DIR, DOCKER_PREBAKE, EXTERNAL_TOOLS_JSON, + PM_DEP_INSTALLERS, RACK_DIR, REPO_ROOT, RUST_TOOLCHAIN_TOML, @@ -206,6 +208,12 @@ async function installAssetTool(name: string, pin: ToolPin): Promise { if (!plat) { throw new Error(`${name}: no pinned asset for ${platformKey()}`) } + // A platform pinned to a registry .tgz (pnpm has no darwin-x64 SEA + // upstream) routes through the npm-tarball path instead. + if (plat.asset.endsWith('.tgz')) { + await installNpmTarball(name, name, pin.version!, plat.integrity) + return + } const repo = pin.repository!.replace(/^github:/, '') const url = `https://github.com/${repo}/releases/download/v${pin.version}/${plat.asset}` const binName = pin.binaryName ?? name @@ -240,6 +248,80 @@ async function installAssetTool(name: string, pin: ToolPin): Promise { console.log(`[external-tools] installed ${name}@${pin.version} -> ${destBin}`) } +/** + * Registry-tarball install: verify against the pinned SRI, extract into the + * rack, materialize runtime deps with the repo's own package manager (this + * repo IS one — dogfood it, with pnpm/npm as last-resort fallbacks), and + * link a node wrapper for the package's bin. Tarballs that bundle their + * node_modules (npm does) skip the dependency install. + */ +async function installNpmTarball( + name: string, + pkg: string, + version: string, + integrity: string, +): Promise { + const base = pkg.split('/').pop() + const url = `https://registry.npmjs.org/${pkg}/-/${base}-${version}.tgz` + const destDir = path.join(RACK_DIR, name, version) + const pkgDir = path.join(destDir, 'package') + // Completion marker is the extracted manifest, not the directory: a + // failed/interrupted extract leaves the dir behind and would otherwise + // wedge every later install. Reset and redo instead. + const manifest = path.join(pkgDir, 'package.json') + if (!existsSync(manifest)) { + rmSync(destDir, { recursive: true, force: true }) + console.log(`[external-tools] downloading ${name}@${version} (npm registry)`) + const buf = await download(url, integrity) + mkdirSync(destDir, { recursive: true }) + const archive = path.join(destDir, 'package.tgz') + writeFileSync(archive, buf) + const res = spawnSync('tar', ['-xzf', archive, '-C', destDir], { stdio: 'inherit' }) + rmSync(archive) + if (res.status !== 0) { + throw new Error(`${name}: tar extract failed`) + } + const pkgJson = JSON.parse(readFileSync(manifest, 'utf8')) + const hasDeps = Object.keys(pkgJson.dependencies ?? {}).length > 0 + if (hasDeps && !existsSync(path.join(pkgDir, 'node_modules'))) { + if (installDeps(name, pkgDir) !== 0) { + rmSync(destDir, { recursive: true, force: true }) + throw new Error(`${name}: dependency install failed`) + } + } + } + const pkgJson = JSON.parse(readFileSync(manifest, 'utf8')) + const bins = + typeof pkgJson.bin === 'string' ? { [name]: pkgJson.bin } : (pkgJson.bin ?? {}) + const binRel = bins[name] ?? Object.values(bins)[0] + if (binRel) { + const wrapper = path.join(RACK_DIR, name, `${name}-wrapper`) + writeFileSync( + wrapper, + `#!/usr/bin/env bash\nexec node '${path.join(pkgDir, binRel as string)}' "$@"\n`, + ) + chmodSync(wrapper, 0o755) + linkHandle(wrapper, name) + } + console.log(`[external-tools] installed ${name}@${version}`) +} + +function installDeps(name: string, pkgDir: string): number { + for (const [cmd, ...args] of PM_DEP_INSTALLERS) { + if (cmd!.includes('/') && !existsSync(cmd!)) { + continue + } + console.log(`[external-tools] ${name}: installing deps via ${path.basename(cmd!)}`) + const res = spawnSync(cmd!, args, { cwd: pkgDir, stdio: 'inherit' }) + if (res.error) { + continue + } + return res.status ?? 1 + } + console.error(`[external-tools] ${name}: no package manager available for deps`) + return 1 +} + function hasSocketToken(): boolean { return Boolean(process.env.SOCKET_SECURITY_KEY) } @@ -271,39 +353,14 @@ async function installTool(name: string, tools: Record): Promis if (!m) { throw new Error(`${name}: unsupported purl ${pin.purl}`) } - const [, pkg, version] = m - const base = pkg!.split('/').pop() - const url = `https://registry.npmjs.org/${pkg}/-/${base}-${version}.tgz` - const destDir = path.join(RACK_DIR, name, version!) - // Completion marker is the extracted manifest, not the directory: a - // failed/interrupted extract leaves the dir behind and would otherwise - // wedge every later install. Reset and redo instead. - const manifest = path.join(destDir, 'package/package.json') - if (!existsSync(manifest)) { - rmSync(destDir, { recursive: true, force: true }) - console.log(`[external-tools] downloading ${name}@${version} (npm)`) - const buf = await download(url, pin.integrity!) - mkdirSync(destDir, { recursive: true }) - const archive = path.join(destDir, 'package.tgz') - writeFileSync(archive, buf) - const res = spawnSync('tar', ['-xzf', archive, '-C', destDir], { stdio: 'inherit' }) - rmSync(archive) - if (res.status !== 0) { - throw new Error(`${name}: tar extract failed`) - } - } - const pkgJson = JSON.parse(readFileSync(manifest, 'utf8')) - const binRel = typeof pkgJson.bin === 'string' ? pkgJson.bin : Object.values(pkgJson.bin ?? {})[0] - if (binRel) { - const wrapper = path.join(RACK_DIR, name, `${name}-wrapper`) - writeFileSync( - wrapper, - `#!/usr/bin/env bash\nexec node '${path.join(destDir, 'package', binRel as string)}' "$@"\n`, - ) - chmodSync(wrapper, 0o755) - linkHandle(wrapper, name) - } - console.log(`[external-tools] installed ${name}@${version}`) + await installNpmTarball(name, m[1]!, m[2]!, pin.integrity!) + return + } + if (pin.repository?.startsWith('npm:')) { + // Platform-agnostic registry tarball (npm itself ships this way): one + // tarball, one integrity, pure JS run through node. Never installed + // via `npm install -g npm` — no self-update path. + await installNpmTarball(name, pin.repository.slice('npm:'.length), pin.version!, pin.integrity!) return } if (pin.release === 'uv-project') { @@ -331,11 +388,28 @@ function writeShims(): void { mkdirSync(BIN_DIR, { recursive: true }) for (const cmd of SFW_ECOSYSTEMS) { const sentinel = `SFW_SHIM_ACTIVE_${cmd.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}` + // When the command itself is rack-pinned (pnpm/npm), the shim wraps the + // PINNED binary — the shim replaces its bin handle, so resolve the rack + // target now and embed it. Unpinned commands resolve at run time by + // stripping the shim dir out of PATH. + const handle = path.join(BIN_DIR, cmd) + let rackReal = '' + try { + const target = readlinkSync(handle) + if (target.startsWith(RACK_DIR)) { + rackReal = target + } + } catch { + // not a symlink or absent — no rack pin for this command + } + const resolveReal = rackReal + ? `REAL='${rackReal}'` + : `CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\\n' | grep -vFx '${BIN_DIR}' | paste -sd ':' -) +REAL=$(PATH="$CLEAN_PATH" command -v '${cmd}' || true)` const body = `#!/usr/bin/env bash # sfw shim for ${cmd} — managed by scripts/soak/external-tools.mts --shims set -euo pipefail -CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\\n' | grep -vFx '${BIN_DIR}' | paste -sd ':' -) -REAL=$(PATH="$CLEAN_PATH" command -v '${cmd}' || true) +${resolveReal} if [ -n "\${${sentinel}:-}" ] || [ -z "$REAL" ] || ! command -v sfw >/dev/null 2>&1; then [ -n "$REAL" ] && exec "$REAL" "$@" echo "${cmd}: not found" >&2; exit 127 @@ -343,9 +417,8 @@ fi export ${sentinel}=1 exec sfw '${cmd}' "$@" ` - const shim = path.join(BIN_DIR, cmd) - writeFileSync(shim, body) - chmodSync(shim, 0o755) + writeFileSync(handle, body) + chmodSync(handle, 0o755) } console.log(`[external-tools] wrote sfw shims for ${SFW_ECOSYSTEMS.join(', ')} in ${BIN_DIR}`) console.log(`[external-tools] prepend ${BIN_DIR} to PATH to activate`) @@ -380,9 +453,10 @@ async function main(argv: string[] = process.argv.slice(2)): Promise { } return problems.length === 0 ? 0 : 1 } - const installIdx = argv.indexOf('--install') - if (installIdx !== -1) { - await installTool(argv[installIdx + 1]!, tools) + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--install') { + await installTool(argv[++i]!, tools) + } } if (argv.includes('--install-all')) { for (const name of Object.keys(tools)) { diff --git a/scripts/soak/paths.mts b/scripts/soak/paths.mts index cbb98e172..4c9415ac7 100644 --- a/scripts/soak/paths.mts +++ b/scripts/soak/paths.mts @@ -61,3 +61,12 @@ export function assertRepoRoot(): void { throw new Error(`repo root not found at ${REPO_ROOT}`) } } + +// Candidates (tried in order) for installing an extracted external tool's +// runtime deps — the repo's own package manager first, of course. +export const PM_DEP_INSTALLERS: string[][] = [ + [path.join(REPO_ROOT, 'target/debug/aube'), 'install', '--prod'], + ['aube', 'install', '--prod'], + ['pnpm', 'install', '--prod', '--ignore-scripts'], + ['npm', 'install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], +] From 0676cf01336efe835343edd6e417d89f8965812c Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 15:38:52 -0400 Subject: [PATCH 16/29] fix(soak): shim self-clobber, windows .exe resolve, date guards; add unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit writeShims wrote THROUGH the rack symlink handle, overwriting the pinned binary with the shim body — which then exec'd itself forever (the mysterious local hang). Remove the handle before writing. Windows .zip archives resolve .exe before failing, and a failed extract cleans its dir so retries re-extract. Annotation and soakBypass dates must be real calendar dates (finding, not a RangeError crash), and an expired soakBypass is now flagged like any other soaked pin. 23 node:test unit tests cover the checkers, fixers, and parsers (mise run test:scripts; also in the CI gate). --- .github/workflows/ci.yml | 1 + mise.toml | 3 + scripts/soak/constants.mts | 8 ++ scripts/soak/constants.test.mts | 48 +++++++++++ scripts/soak/external-tools.mts | 28 +++++-- scripts/soak/external-tools.test.mts | 60 ++++++++++++++ scripts/soak/soak.mts | 11 +++ scripts/soak/soak.test.mts | 117 +++++++++++++++++++++++++++ 8 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 scripts/soak/constants.test.mts create mode 100644 scripts/soak/external-tools.test.mts create mode 100644 scripts/soak/soak.test.mts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc5eaaa01..9b119b33a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,7 @@ jobs: run: | node scripts/soak/soak.mts --check node scripts/soak/external-tools.mts --check + node --test scripts/soak/*.test.mts - name: pinned package managers + socket firewall run: | node scripts/soak/external-tools.mts --install pnpm --install npm --install sfw --shims diff --git a/mise.toml b/mise.toml index fe149cb72..3ce32f169 100644 --- a/mise.toml +++ b/mise.toml @@ -30,6 +30,9 @@ run = "cargo test" [tasks.soak] run = "node scripts/soak/soak.mts --check" +[tasks."test:scripts"] +run = "node --test scripts/soak/*.test.mts" + [tasks."soak:fix"] run = "node scripts/soak/soak.mts --fix" diff --git a/scripts/soak/constants.mts b/scripts/soak/constants.mts index e72ba1d53..3e5c1e46f 100644 --- a/scripts/soak/constants.mts +++ b/scripts/soak/constants.mts @@ -35,6 +35,14 @@ export function todayIso(): string { return new Date().toISOString().slice(0, 10) } +// Shape-valid but impossible dates (2026-13-45) round-trip differently (or +// produce Invalid Date), so callers can reject them with a finding instead +// of crashing on Invalid Date arithmetic. +export function isValidIsoDate(iso: string): boolean { + const d = new Date(`${iso}T00:00:00Z`) + return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === iso +} + export function addDaysIso(iso: string, days: number): string { const d = new Date(`${iso}T00:00:00Z`) d.setUTCDate(d.getUTCDate() + days) diff --git a/scripts/soak/constants.test.mts b/scripts/soak/constants.test.mts new file mode 100644 index 000000000..3ee5b28d9 --- /dev/null +++ b/scripts/soak/constants.test.mts @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' + +import { + ANNOTATION_RE, + SOAK_DAYS, + SOAK_MINUTES, + VERSION_PIN_RE, + addDaysIso, + isValidIsoDate, + todayIso, +} from './constants.mts' + +test('SOAK_MINUTES derives from SOAK_DAYS', () => { + assert.equal(SOAK_MINUTES, SOAK_DAYS * 24 * 60) +}) + +test('todayIso returns a YYYY-MM-DD string', () => { + assert.match(todayIso(), /^\d{4}-\d{2}-\d{2}$/) +}) + +test('isValidIsoDate accepts real dates and rejects impossible ones', () => { + assert.equal(isValidIsoDate('2026-07-11'), true) + assert.equal(isValidIsoDate('2024-02-29'), true) // leap day + assert.equal(isValidIsoDate('2026-13-45'), false) // shape-valid, not real + assert.equal(isValidIsoDate('2026-02-30'), false) + assert.equal(isValidIsoDate('not-a-date'), false) +}) + +test('addDaysIso rolls over months and years', () => { + assert.equal(addDaysIso('2026-07-11', 7), '2026-07-18') + assert.equal(addDaysIso('2026-12-28', 7), '2027-01-04') + assert.equal(addDaysIso('2026-02-26', 7), '2026-03-05') +}) + +test('ANNOTATION_RE matches the published | removable comment shape', () => { + assert.ok(ANNOTATION_RE.test('# published: 2026-07-08 | removable: 2026-07-15')) + assert.ok(ANNOTATION_RE.test('#published: 2026-07-08|removable: 2026-07-15')) + assert.ok(!ANNOTATION_RE.test('# published 2026-07-08 removable 2026-07-15')) + assert.ok(!ANNOTATION_RE.test('- some-entry@1.2.3')) +}) + +test('VERSION_PIN_RE distinguishes dated pins from standing trust', () => { + assert.ok(VERSION_PIN_RE.test('left-pad@1.3.0')) + assert.ok(VERSION_PIN_RE.test('@scope/name@1.2.3')) + assert.ok(!VERSION_PIN_RE.test('react')) // bare name + assert.ok(!VERSION_PIN_RE.test('@myorg/*')) // scope glob +}) diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 8b5fe8387..ef3635da0 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -38,7 +38,7 @@ import path from 'node:path' import process from 'node:process' import { pathToFileURL } from 'node:url' -import { ANNOTATION_RE, SOAK_DAYS, addDaysIso } from './constants.mts' +import { SOAK_DAYS, addDaysIso, isValidIsoDate, todayIso } from './constants.mts' import { BIN_DIR, DOCKER_PREBAKE, @@ -105,12 +105,19 @@ export function checkPins(tools: Record): string[] { } if (pin.soakBypass) { const { published, removable } = pin.soakBypass + if (!isValidIsoDate(published) || !isValidIsoDate(removable)) { + out.push(`${name}: soakBypass dates are not real YYYY-MM-DD calendar dates`) + continue + } const expected = addDaysIso(published, SOAK_DAYS) if (removable !== expected) { out.push(`${name}: soakBypass removable ${removable}, wanted ${expected} (published + ${SOAK_DAYS}d)`) } - if (!ANNOTATION_RE.test(`# published: ${published} | removable: ${removable}`)) { - out.push(`${name}: soakBypass dates are not YYYY-MM-DD`) + // A bypass whose window has passed is dead weight: the version has + // soaked, so the annotation must come off (same rule the workspace + // yaml excludes live under). + if (removable < todayIso()) { + out.push(`${name}: soakBypass expired (removable ${removable}) — the pin has soaked, remove the annotation`) } } } @@ -216,9 +223,9 @@ async function installAssetTool(name: string, pin: ToolPin): Promise { } const repo = pin.repository!.replace(/^github:/, '') const url = `https://github.com/${repo}/releases/download/v${pin.version}/${plat.asset}` - const binName = pin.binaryName ?? name + let binName = pin.binaryName ?? name const destDir = path.join(RACK_DIR, name, pin.version!) - const destBin = path.join(destDir, binName) + let destBin = path.join(destDir, binName) if (existsSync(destBin)) { linkHandle(destBin, binName) console.log(`[external-tools] ${name}@${pin.version} already installed`) @@ -237,7 +244,14 @@ async function installAssetTool(name: string, pin: ToolPin): Promise { if (res.status !== 0) { throw new Error(`${name}: archive extract failed`) } + // Windows archives ship `.exe`; resolve it before giving up, and + // clean the partial dir so a retry re-extracts instead of wedging. + if (!existsSync(destBin) && existsSync(`${destBin}.exe`)) { + destBin = `${destBin}.exe` + binName = `${binName}.exe` + } if (!existsSync(destBin)) { + rmSync(destDir, { recursive: true, force: true }) throw new Error(`${name}: ${binName} not found in extracted archive`) } } else { @@ -417,6 +431,10 @@ fi export ${sentinel}=1 exec sfw '${cmd}' "$@" ` + // Remove the handle before writing: writeFileSync FOLLOWS a symlink, so + // writing through a rack handle would overwrite the pinned binary itself + // with the shim body (which then execs itself forever). + rmSync(handle, { force: true }) writeFileSync(handle, body) chmodSync(handle, 0o755) } diff --git a/scripts/soak/external-tools.test.mts b/scripts/soak/external-tools.test.mts new file mode 100644 index 000000000..182cd5886 --- /dev/null +++ b/scripts/soak/external-tools.test.mts @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' +import { test } from 'node:test' + +import { addDaysIso, todayIso } from './constants.mts' +import { checkDockerPrebake, checkPins } from './external-tools.mts' +import { DOCKER_PREBAKE, EXTERNAL_TOOLS_JSON, REPO_ROOT, RUST_TOOLCHAIN_TOML } from './paths.mts' + +const GOOD_SRI = + 'sha512-waLrsPG2a7EOv0XuvXDQZGgCZ4MTtOfZh8TmGbM6gn2B6Nh6HI+15jaoKdAS9wgdTyIqTuqU+O+NtVYd+kuFaA==' + +test('the repo external-tools.json passes checkPins', () => { + const tools = JSON.parse(readFileSync(EXTERNAL_TOOLS_JSON, 'utf8')).tools + assert.deepEqual(checkPins(tools), []) +}) + +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) +}) + +test('checkPins validates soakBypass dates, arithmetic, and expiry', () => { + const pub = addDaysIso(todayIso(), -1) + const good = { + a: { + version: '1.0.0', + integrity: GOOD_SRI, + soakBypass: { version: '1.0.0', published: pub, removable: addDaysIso(pub, 7) }, + }, + } + assert.deepEqual(checkPins(good), []) + const wrongMath = structuredClone(good) + wrongMath.a.soakBypass.removable = addDaysIso(pub, 3) + assert.match(checkPins(wrongMath)[0]!, /removable/) + const expired = structuredClone(good) + expired.a.soakBypass = { version: '1.0.0', published: '2020-01-01', removable: '2020-01-08' } + assert.match(checkPins(expired)[0]!, /expired/) + const impossible = structuredClone(good) + impossible.a.soakBypass = { version: '1.0.0', published: '2026-13-45', removable: '2026-13-52' } + assert.match(checkPins(impossible)[0]!, /calendar/) +}) + +test('the repo Dockerfile prebake (when present) matches the tracked pins', t => { + if (!DOCKER_PREBAKE || !existsSync(path.join(REPO_ROOT, DOCKER_PREBAKE))) { + t.skip('repo has no prebake image') + return + } + const tools = JSON.parse(readFileSync(EXTERNAL_TOOLS_JSON, 'utf8')).tools + const docker = readFileSync(path.join(REPO_ROOT, DOCKER_PREBAKE), 'utf8') + const toolchain = readFileSync(path.join(REPO_ROOT, RUST_TOOLCHAIN_TOML), 'utf8') + assert.deepEqual(checkDockerPrebake(docker, tools, toolchain), []) + // and drift in any direction is caught + assert.ok(checkDockerPrebake(docker.replace(/sha=[0-9a-f]{8}/, 'sha=deadbeef'), tools, toolchain).length > 0) + assert.ok( + checkDockerPrebake(docker, tools, toolchain.replace(/channel = ".*"/, 'channel = "nightly-1999-01-01"')).length > + 0, + ) +}) diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts index 7eb31c1fe..7fd06f5a8 100644 --- a/scripts/soak/soak.mts +++ b/scripts/soak/soak.mts @@ -29,6 +29,7 @@ import { SOAK_MINUTES, VERSION_PIN_RE, addDaysIso, + isValidIsoDate, todayIso, } from './constants.mts' import { REPO_ROOT, SURFACES } from './paths.mts' @@ -135,6 +136,16 @@ export function checkExcludeAnnotations(body: string, file: string): Finding[] { continue } const { published, removable } = entry.annotation + if (!isValidIsoDate(published) || !isValidIsoDate(removable)) { + out.push({ + file, + what: `soak exclude '${entry.name}' annotation dates`, + saw: `${published} | ${removable}`, + wanted: 'real YYYY-MM-DD calendar dates', + fix: 'correct the annotation to the real registry publish date', + }) + continue + } const expected = addDaysIso(published, SOAK_DAYS) if (removable !== expected) { out.push({ diff --git a/scripts/soak/soak.test.mts b/scripts/soak/soak.test.mts new file mode 100644 index 000000000..eb3f2cd44 --- /dev/null +++ b/scripts/soak/soak.test.mts @@ -0,0 +1,117 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' + +import { addDaysIso, todayIso } from './constants.mts' +import { + checkCargoConfig, + checkCatalogParity, + checkExcludeAnnotations, + checkNpmrc, + checkTazeConfig, + checkWorkspaceYaml, + fixNpmrc, + fixWorkspaceYaml, + parseExcludeEntries, +} from './soak.mts' + +// A pin published yesterday is inside its window; one published long ago +// has expired. Built relative to today so the tests never go stale. +const FRESH_PUB = addDaysIso(todayIso(), -1) +const FRESH_REM = addDaysIso(FRESH_PUB, 7) + +const CLEAN_YAML = `catalog: + taze: 19.14.1 +minimumReleaseAge: 10080 +minimumReleaseAgeExclude: + # published: ${FRESH_PUB} | removable: ${FRESH_REM} + - 'left-pad@1.3.0' + - '@myorg/*' + - react +` + +test('cargo config: wrong window and missing unstable gate are findings', () => { + const good = '[unstable]\nmin-publish-age = true\n\n[registry]\nglobal-min-publish-age = "7 days"\n' + assert.equal(checkCargoConfig(good, 'c').length, 0) + assert.equal(checkCargoConfig(good.replace('7 days', '3 days'), 'c').length, 1) + assert.equal(checkCargoConfig('[registry]\nglobal-min-publish-age = "7 days"\n', 'c').length, 1) +}) + +test('npmrc: window must match SOAK_DAYS and fix writes it', () => { + assert.equal(checkNpmrc('min-release-age=7\n', 'n').length, 0) + assert.equal(checkNpmrc('min-release-age=3\n', 'n').length, 1) + assert.equal(checkNpmrc('# nothing\n', 'n').length, 1) + assert.match(fixNpmrc('# nothing\n'), /min-release-age=7/) + assert.match(fixNpmrc('min-release-age=3\n'), /min-release-age=7/) +}) + +test('workspace yaml: clean fixture passes', () => { + assert.deepEqual(checkWorkspaceYaml(CLEAN_YAML, 'y'), []) +}) + +test('workspace yaml: wrong minutes value is a finding', () => { + const bad = CLEAN_YAML.replace('10080', '1440') + assert.equal(checkWorkspaceYaml(bad, 'y').filter(f => f.what.includes('minimumReleaseAge')).length, 1) +}) + +test('excludes: flow-style list is rejected outright', () => { + const flow = "minimumReleaseAge: 10080\nminimumReleaseAgeExclude: ['left-pad@1.3.0']\n" + const findings = checkExcludeAnnotations(flow, 'y') + assert.equal(findings.length, 1) + assert.match(findings[0]!.what, /flow style/) +}) + +test('excludes: unannotated version pin is a finding, bare/glob are not', () => { + const yaml = 'minimumReleaseAgeExclude:\n - lodash@4.17.21\n - react\n - "@myorg/*"\n' + const findings = checkExcludeAnnotations(yaml, 'y') + assert.equal(findings.length, 1) + assert.match(findings[0]!.what, /lodash@4\.17\.21/) +}) + +test('excludes: wrong removable date and expiry are findings', () => { + const wrong = `minimumReleaseAgeExclude:\n # published: ${FRESH_PUB} | removable: ${addDaysIso(FRESH_PUB, 3)}\n - 'a@1.0.0'\n` + assert.match(checkExcludeAnnotations(wrong, 'y')[0]!.what, /removable date/) + const expired = `minimumReleaseAgeExclude:\n # published: 2020-01-01 | removable: 2020-01-08\n - 'b@1.0.0'\n` + assert.match(checkExcludeAnnotations(expired, 'y')[0]!.what, /expired/) +}) + +test('excludes: impossible calendar dates are findings, not crashes', () => { + const bad = `minimumReleaseAgeExclude:\n # published: 2026-13-45 | removable: 2026-13-52\n - 'c@1.0.0'\n` + const findings = checkExcludeAnnotations(bad, 'y') + assert.equal(findings.length, 1) + assert.match(findings[0]!.what, /annotation dates/) +}) + +test('excludes: entries with trailing comments still parse', () => { + const yaml = `minimumReleaseAgeExclude:\n # published: ${FRESH_PUB} | removable: ${FRESH_REM}\n - 'd@2.0.0' # temp\n` + assert.deepEqual(parseExcludeEntries(yaml).map(e => e.name), ['d@2.0.0']) + assert.equal(checkExcludeAnnotations(yaml, 'y').length, 0) +}) + +test('fix prunes expired pins together with their annotations', () => { + const yaml = `minimumReleaseAge: 10080\nminimumReleaseAgeExclude:\n # published: 2020-01-01 | removable: 2020-01-08\n - 'old@1.0.0'\n # published: ${FRESH_PUB} | removable: ${FRESH_REM}\n - 'fresh@1.0.0'\n` + const fixed = fixWorkspaceYaml(yaml) + assert.ok(!fixed.includes('old@1.0.0')) + assert.ok(!fixed.includes('2020-01-01')) + assert.ok(fixed.includes('fresh@1.0.0')) +}) + +test('catalog parity: exact pin must match, catalog: protocol no-ops', () => { + const yaml = 'catalog:\n taze: 19.14.1\n' + const pin = (v: string) => JSON.stringify({ devDependencies: { taze: v } }) + assert.equal(checkCatalogParity(yaml, pin('19.14.1'), 'y').length, 0) + assert.equal(checkCatalogParity(yaml, pin('19.14.2'), 'y').length, 1) + assert.equal(checkCatalogParity(yaml, pin('catalog:'), 'y').length, 0) +}) + +test('catalog parity: entries after a blank line are still checked', () => { + const yaml = 'catalog:\n taze: 19.14.1\n\n untracked: 1.6.4\n' + const pkg = JSON.stringify({ devDependencies: { taze: '19.14.1', untracked: '1.0.0' } }) + assert.equal(checkCatalogParity(yaml, pkg, 'y').length, 1) +}) + +test('taze config: window must be imported, not hand-copied', () => { + const good = "import { SOAK_DAYS } from './scripts/soak/constants.mts'\nexport default { maturityPeriod: SOAK_DAYS }\n" + assert.equal(checkTazeConfig(good, 't').length, 0) + assert.equal(checkTazeConfig('export default { maturityPeriod: 7 }\n', 't').length, 1) + assert.equal(checkTazeConfig('export default {}\n', 't').length, 2) +}) From 9166cfda18d0d64444651380b25b427846fa9cf3 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 15:59:01 -0400 Subject: [PATCH 17/29] =?UTF-8?q?refactor(soak):=20perfectionist=20pass=20?= =?UTF-8?q?=E2=80=94=20dead=20code=20out,=20tests=20isolated,=20gates=20wi?= =?UTF-8?q?dened?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drop uncalled assertRepoRoot and the never-false 'required' surface knob; inline the single-use hasSocketToken wrapper (own forbidden rule); shared extractArchive replaces two near-duplicate tar blocks; installNpmTarball parses the manifest once - writeShims resolves rack pins from the manifest + rack contents instead of the bin handle, making a --shims re-run keep the pinned target (idempotence regression covered by tests) - new tmpdir-isolated rack tests (XDG_DATA_HOME sandbox): the symlink-clobber and dangling-handle regressions, rackRealFor resolution, shim idempotence; dockerignore surface tests (skip here — no repo-context docker builds) - docker prebake gate also asserts the shim ecosystem list and the msrv toolchain; test fixtures derive from SOAK_DAYS instead of hand-copying 7; CI gate defers to the mise tasks; number-carrying comments next to managed values are now number-free; prose names SOCKET_SECURITY_KEY (the env var the code reads) --- .cargo/config.toml | 4 +- .github/workflows/ci.yml | 6 +- .rules | 4 +- docs/pnpm-workspace.yaml | 5 +- external-tools.json | 6 +- scripts/soak/dockerignore.test.mts | 51 ++++++++++++++ scripts/soak/external-tools.mts | 102 +++++++++++++++------------ scripts/soak/external-tools.test.mts | 4 +- scripts/soak/paths.mts | 10 ++- scripts/soak/rack.test.mts | 81 +++++++++++++++++++++ scripts/soak/soak.mts | 25 +++---- scripts/soak/soak.test.mts | 4 +- 12 files changed, 222 insertions(+), 80 deletions(-) create mode 100644 scripts/soak/dockerignore.test.mts create mode 100644 scripts/soak/rack.test.mts diff --git a/.cargo/config.toml b/.cargo/config.toml index f377b0114..5064cef7b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -17,8 +17,8 @@ RUST_TEST_THREADS = "1" # Supply-chain soak for cargo's own dependency resolution — the same -# 7-day rule aube offers npm users via minimumReleaseAge: crate versions -# published less than 7 days ago are pubtime-incompatible and the +# soak-window rule aube offers npm users via minimumReleaseAge: crate +# versions younger than the window are pubtime-incompatible and the # resolver skips them unless already in Cargo.lock # (resolver.incompatible-publish-age defaults to "deny"). The unstable # feature is active because rust-toolchain.toml pins a nightly. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b119b33a..ae7278c44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,9 +88,9 @@ jobs: cache: false - name: soak + external-tool pin gates run: | - node scripts/soak/soak.mts --check - node scripts/soak/external-tools.mts --check - node --test scripts/soak/*.test.mts + mise run soak + mise run tools:check + mise run test:scripts - name: pinned package managers + socket firewall run: | node scripts/soak/external-tools.mts --install pnpm --install npm --install sfw --shims diff --git a/.rules b/.rules index a6ccd541f..551e5a473 100644 --- a/.rules +++ b/.rules @@ -238,7 +238,7 @@ deps: - cargo audit: CVE database scan (RustSec advisories) before every release, required in CI - cargo deny: license policy, banned crates, duplicate detection, approved sources. deny.toml checked in - cargo min-publish-age: 7-day soak on new crate versions (.cargo/config.toml [registry] global-min-publish-age; unstable feature, active via the pinned nightly). resolver skips versions younger than the window unless already locked - - external tools: sfw/zizmor/agentshield/skillspector pinned exact with sha512 SRI in external-tools.json; installed/verified only via scripts/soak/external-tools.mts (mise run tools:install / tools:check) + - external tools: pnpm/npm/sfw/zizmor/agentshield/skillspector pinned exact with sha512 SRI in external-tools.json; installed/verified only via scripts/soak/external-tools.mts (mise run tools:install / tools:check) soak: window: SOAK_DAYS=7 in scripts/soak/constants.mts — the ONE source; every surface derives or is parity-checked against it @@ -250,7 +250,7 @@ soak: - docs/taze.config.mts maturityPeriod (imports SOAK_DAYS) manager: mise run soak (check, CI-gated in test-linux) / soak:fix (syncs windows, prunes expired excludes) / deps:update (taze + cargo update, both soaked) excludes: version-pinned minimumReleaseAgeExclude entries REQUIRE `# published: YYYY-MM-DD | removable: YYYY-MM-DD` on the line above (removable = published + 7d); bare names/globs are standing trust and need no annotation; expired pins are pruned by soak:fix - socket_firewall: CI installs sfw (free tier keyless; enterprise auto-selected when the SOCKET_SECURITY_KEY repo secret lands — the one env var every Socket product reads, no aliasing) and shims npm/yarn/pnpm/pip/uv/cargo via scripts/soak/external-tools.mts --shims; the buildkite agent image pre-bakes the same binary + shims + socket_firewall: CI installs sfw (free tier keyless; enterprise auto-selected when the SOCKET_SECURITY_KEY repo secret lands — the one env var every Socket product reads, no aliasing) and shims npm/yarn/pnpm/pip/pip3/uv/cargo via scripts/soak/external-tools.mts --shims; the buildkite agent image pre-bakes the same binary + shims git: forbidden[2]: diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml index 121355d15..4c6db5b61 100644 --- a/docs/pnpm-workspace.yaml +++ b/docs/pnpm-workspace.yaml @@ -8,9 +8,12 @@ catalog: taze: 19.14.1 + # no docs consumer yet — catalog-pinned for parity with nub, where + # untracked manages the .dockerignore build-context reduction untracked: 1.6.4 -# Wait 7 days (10080 minutes) before installing newly published packages. +# Cooldown (minutes) before newly published packages install — value managed +# by soak:fix; the window is SOAK_DAYS in scripts/soak/constants.mts. minimumReleaseAge: 10080 # Exclusions: bare names / scope globs express standing trust; version pins diff --git a/external-tools.json b/external-tools.json index efe0f3f7d..5bcc98358 100644 --- a/external-tools.json +++ b/external-tools.json @@ -67,7 +67,7 @@ "release": "asset", "binaryName": "sfw", "notes": [ - "Used when neither SOCKET_API_KEY (primary) nor SOCKET_API_TOKEN (forward-canonical) is set", + "Used when SOCKET_SECURITY_KEY is not set", "Shims npm/yarn/pnpm so every install call passes through the firewall" ], "platforms": { @@ -94,13 +94,13 @@ } }, "sfw-enterprise": { - "description": "Socket Firewall (enterprise tier) — selected when SOCKET_API_KEY (or SOCKET_API_TOKEN) is set.", + "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_API_KEY is set (primary, universally supported). SOCKET_API_TOKEN is accepted as the forward-canonical secondary read.", + "Used when SOCKET_SECURITY_KEY is set (the one env var every Socket product reads)", "Same shims as sfw-free, broader ecosystem support (Ruby, .NET, Go on Linux)" ], "platforms": { diff --git a/scripts/soak/dockerignore.test.mts b/scripts/soak/dockerignore.test.mts new file mode 100644 index 000000000..ac09a67dd --- /dev/null +++ b/scripts/soak/dockerignore.test.mts @@ -0,0 +1,51 @@ +// Tests for the docker-context reduction: the `untracked`-managed +// .dockerignore section. Skips wholesale in a repo with no repo-context +// docker builds (DOCKERIGNORE is null there). +import assert from 'node:assert/strict' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' +import { test } from 'node:test' + +import { DOCKERIGNORE, REPO_ROOT } from './paths.mts' + +const skip = DOCKERIGNORE ? false : 'repo has no repo-context docker builds' + +const START = '### start auto generated using `untracked`' +const END = '### finished auto generated using `untracked`' + +const body = DOCKERIGNORE ? readFileSync(path.join(REPO_ROOT, DOCKERIGNORE), 'utf8') : '' + +test('managed .dockerignore carries the untracked markers with content between', { skip }, () => { + const start = body.indexOf(START) + const end = body.indexOf(END) + assert.ok(start !== -1 && end !== -1, 'both markers present') + assert.ok(end - start > 100, 'generated section is not empty') +}) + +test('hand-written custom rules survive above the managed section', { skip }, () => { + const custom = body.slice(0, body.indexOf(START)) + assert.match(custom, /^target\//m, 'build-artifact rule preserved') + assert.match(custom, /^node_modules\//m, 'node_modules rule preserved') +}) + +test('the package.json whitelist is honored as trailing negations', { skip }, () => { + const pkg = JSON.parse(readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8')) + const whitelist: string[] = pkg.untracked?.whitelist ?? [] + assert.ok(whitelist.length > 0, 'whitelist declares the load-bearing paths') + for (const entry of whitelist) { + // Docker applies patterns anchored to the context root, so only + // root-level entries need an explicit negation to survive the `.*` + // dotfile rule; nested ones are untouched by it but must exist. + if (!entry.includes('/')) { + assert.ok(body.includes(`!${entry}`), `negation for whitelisted '${entry}'`) + } + assert.ok(existsSync(path.join(REPO_ROOT, entry)), `whitelisted path '${entry}' exists`) + } +}) + +test('the load-bearing nested .cargo config is not root-anchored away', { skip }, () => { + // The in-container builders depend on crates/nub-native/.cargo/config.toml; + // the generated `.*` rule is context-root-anchored so it must NOT appear in + // a form that matches nested dotdirs. + assert.ok(!body.includes('**/.*'), 'no recursive dotfile exclusion') +}) diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index ef3635da0..3ad2269aa 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -28,7 +28,6 @@ import { existsSync, mkdirSync, readFileSync, - readlinkSync, realpathSync, rmSync, symlinkSync, @@ -139,8 +138,16 @@ export function checkDockerPrebake( dockerBody: string, tools: Record, toolchainToml: string, + rustVersion = '', ): string[] { const out: string[] = [] + const shimList = /for cmd in ([^;]+);/.exec(dockerBody)?.[1]?.trim().split(/\s+/) + if (shimList && shimList.join(' ') !== SFW_ECOSYSTEMS.join(' ')) { + out.push(`docker prebake: shim list [${shimList.join(' ')}] != SFW_ECOSYSTEMS [${SFW_ECOSYSTEMS.join(' ')}]`) + } + if (rustVersion && !dockerBody.includes(`toolchain install ${rustVersion}`)) { + out.push(`docker prebake: image does not pre-install the ${rustVersion} msrv toolchain`) + } const sfw = tools['sfw-free'] const version = /rack\/sfw-free\/([^/\s]+)\//.exec(dockerBody)?.[1] if (version !== sfw?.version) { @@ -201,7 +208,19 @@ async function download(url: string, expectedSri: string): Promise { return buf } -function linkHandle(target: string, name: string): void { +function extractArchive(name: string, destDir: string, asset: string, buf: Buffer): void { + const archive = path.join(destDir, asset) + writeFileSync(archive, buf) + // bsdtar (macOS + Windows runners) extracts zip via plain -xf too. + const flags = asset.endsWith('.zip') ? '-xf' : '-xzf' + const res = spawnSync('tar', [flags, archive, '-C', destDir], { stdio: 'inherit' }) + rmSync(archive) + if (res.status !== 0) { + throw new Error(`${name}: archive extract failed`) + } +} + +export function linkHandle(target: string, name: string): void { mkdirSync(BIN_DIR, { recursive: true }) const handle = path.join(BIN_DIR, name) // force also removes a DANGLING handle (existsSync would report false @@ -235,15 +254,7 @@ async function installAssetTool(name: string, pin: ToolPin): Promise { const buf = await download(url, plat.integrity) mkdirSync(destDir, { recursive: true }) if (plat.asset.endsWith('.tar.gz') || plat.asset.endsWith('.zip')) { - const archive = path.join(destDir, plat.asset) - writeFileSync(archive, buf) - // bsdtar (macOS + Windows runners) extracts zip via plain -xf too. - const args = plat.asset.endsWith('.zip') ? ['-xf', archive, '-C', destDir] : ['-xzf', archive, '-C', destDir] - const res = spawnSync('tar', args, { stdio: 'inherit' }) - rmSync(archive) - if (res.status !== 0) { - throw new Error(`${name}: archive extract failed`) - } + extractArchive(name, destDir, plat.asset, buf) // Windows archives ship `.exe`; resolve it before giving up, and // clean the partial dir so a retry re-extracts instead of wedging. if (!existsSync(destBin) && existsSync(`${destBin}.exe`)) { @@ -288,23 +299,16 @@ async function installNpmTarball( console.log(`[external-tools] downloading ${name}@${version} (npm registry)`) const buf = await download(url, integrity) mkdirSync(destDir, { recursive: true }) - const archive = path.join(destDir, 'package.tgz') - writeFileSync(archive, buf) - const res = spawnSync('tar', ['-xzf', archive, '-C', destDir], { stdio: 'inherit' }) - rmSync(archive) - if (res.status !== 0) { - throw new Error(`${name}: tar extract failed`) - } - const pkgJson = JSON.parse(readFileSync(manifest, 'utf8')) - const hasDeps = Object.keys(pkgJson.dependencies ?? {}).length > 0 - if (hasDeps && !existsSync(path.join(pkgDir, 'node_modules'))) { - if (installDeps(name, pkgDir) !== 0) { - rmSync(destDir, { recursive: true, force: true }) - throw new Error(`${name}: dependency install failed`) - } - } + extractArchive(name, destDir, 'package.tgz', buf) } const pkgJson = JSON.parse(readFileSync(manifest, 'utf8')) + const hasDeps = Object.keys(pkgJson.dependencies ?? {}).length > 0 + if (hasDeps && !existsSync(path.join(pkgDir, 'node_modules'))) { + if (installDeps(name, pkgDir) !== 0) { + rmSync(destDir, { recursive: true, force: true }) + throw new Error(`${name}: dependency install failed`) + } + } const bins = typeof pkgJson.bin === 'string' ? { [name]: pkgJson.bin } : (pkgJson.bin ?? {}) const binRel = bins[name] ?? Object.values(bins)[0] @@ -336,10 +340,6 @@ function installDeps(name: string, pkgDir: string): number { return 1 } -function hasSocketToken(): boolean { - return Boolean(process.env.SOCKET_SECURITY_KEY) -} - async function installTool(name: string, tools: Record): Promise { // `sfw` is a flavor pair: the enterprise binary when a Socket token is // present (repo secret), the keyless free tier otherwise. The firewall @@ -350,7 +350,7 @@ async function installTool(name: string, tools: Record): Promis console.log('[external-tools] sfw shims are POSIX-only — skipping on windows') return } - name = hasSocketToken() ? 'sfw-enterprise' : 'sfw-free' + name = process.env.SOCKET_SECURITY_KEY ? 'sfw-enterprise' : 'sfw-free' } const pin = tools[name] if (!pin) { @@ -388,13 +388,31 @@ async function installTool(name: string, tools: Record): Promis throw new Error(`${name}: no installable shape (release=${pin.release ?? 'none'})`) } +/** + * The rack location of a pinned command's runnable entry, or '' when the + * command isn't rack-pinned/installed. Resolved from the manifest + rack + * contents (never from the bin handle: after a --shims run the handle is + * the shim itself, and resolving through it wouldn't survive a re-run). + */ +export function rackRealFor(cmd: string, tools: Record): string { + const pin = tools[cmd] + if (!pin) { + return '' + } + const candidates = [ + path.join(RACK_DIR, cmd, `${cmd}-wrapper`), + ...(pin.version ? [path.join(RACK_DIR, cmd, pin.version, pin.binaryName ?? cmd)] : []), + ] + return candidates.find(c => existsSync(c)) ?? '' +} + /** * sfw shims: tiny wrappers named after each package manager that route the * real invocation through the firewall. A sentinel env var breaks the * recursion when sfw itself re-invokes the tool; the real binary is found * by stripping the rack's bin dir out of PATH. */ -function writeShims(): void { +export function writeShims(tools: Record): void { if (process.platform === 'win32') { console.log('[external-tools] sfw shims are POSIX-only — skipping on windows') return @@ -403,19 +421,10 @@ function writeShims(): void { for (const cmd of SFW_ECOSYSTEMS) { const sentinel = `SFW_SHIM_ACTIVE_${cmd.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}` // When the command itself is rack-pinned (pnpm/npm), the shim wraps the - // PINNED binary — the shim replaces its bin handle, so resolve the rack - // target now and embed it. Unpinned commands resolve at run time by - // stripping the shim dir out of PATH. + // PINNED binary; unpinned commands resolve at run time by stripping the + // shim dir out of PATH. const handle = path.join(BIN_DIR, cmd) - let rackReal = '' - try { - const target = readlinkSync(handle) - if (target.startsWith(RACK_DIR)) { - rackReal = target - } - } catch { - // not a symlink or absent — no rack pin for this command - } + const rackReal = rackRealFor(cmd, tools) const resolveReal = rackReal ? `REAL='${rackReal}'` : `CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\\n' | grep -vFx '${BIN_DIR}' | paste -sd ':' -) @@ -459,6 +468,9 @@ async function main(argv: string[] = process.argv.slice(2)): Promise { readFileSync(dockerAbs, 'utf8'), tools, existsSync(toolchainAbs) ? readFileSync(toolchainAbs, 'utf8') : '', + /^rust-version\s*=\s*"([^"]+)"/m.exec( + readFileSync(path.join(REPO_ROOT, 'Cargo.toml'), 'utf8'), + )?.[1] ?? '', ), ) } @@ -486,7 +498,7 @@ async function main(argv: string[] = process.argv.slice(2)): Promise { await installTool('sfw', tools) } if (argv.includes('--shims')) { - writeShims() + writeShims(tools) } return 0 } diff --git a/scripts/soak/external-tools.test.mts b/scripts/soak/external-tools.test.mts index 182cd5886..1e99c7628 100644 --- a/scripts/soak/external-tools.test.mts +++ b/scripts/soak/external-tools.test.mts @@ -3,7 +3,7 @@ import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' import { test } from 'node:test' -import { addDaysIso, todayIso } from './constants.mts' +import { SOAK_DAYS, addDaysIso, todayIso } from './constants.mts' import { checkDockerPrebake, checkPins } from './external-tools.mts' import { DOCKER_PREBAKE, EXTERNAL_TOOLS_JSON, REPO_ROOT, RUST_TOOLCHAIN_TOML } from './paths.mts' @@ -27,7 +27,7 @@ test('checkPins validates soakBypass dates, arithmetic, and expiry', () => { a: { version: '1.0.0', integrity: GOOD_SRI, - soakBypass: { version: '1.0.0', published: pub, removable: addDaysIso(pub, 7) }, + soakBypass: { version: '1.0.0', published: pub, removable: addDaysIso(pub, SOAK_DAYS) }, }, } assert.deepEqual(checkPins(good), []) diff --git a/scripts/soak/paths.mts b/scripts/soak/paths.mts index 4c9415ac7..c809a1abe 100644 --- a/scripts/soak/paths.mts +++ b/scripts/soak/paths.mts @@ -6,7 +6,6 @@ * change. */ -import { existsSync } from 'node:fs' import os from 'node:os' import path from 'node:path' import process from 'node:process' @@ -51,16 +50,15 @@ export const EXTERNAL_TOOLS_JSON = path.join(REPO_ROOT, 'external-tools.json') export const DOCKER_PREBAKE: string | null = '.buildkite/linux-agent.Dockerfile' export const RUST_TOOLCHAIN_TOML = 'rust-toolchain.toml' +// .dockerignore managed by `untracked` (null when the repo has no +// repo-context docker builds — aube's agent image never copies the repo). +export const DOCKERIGNORE: string | null = null + const XDG_DATA_HOME = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share') export const DEV_TOOLS_DIR = path.join(XDG_DATA_HOME, 'aube/dev-tools') export const RACK_DIR = path.join(DEV_TOOLS_DIR, 'rack') export const BIN_DIR = path.join(DEV_TOOLS_DIR, 'bin') -export function assertRepoRoot(): void { - if (!existsSync(path.join(REPO_ROOT, 'external-tools.json'))) { - throw new Error(`repo root not found at ${REPO_ROOT}`) - } -} // Candidates (tried in order) for installing an extracted external tool's // runtime deps — the repo's own package manager first, of course. diff --git a/scripts/soak/rack.test.mts b/scripts/soak/rack.test.mts new file mode 100644 index 000000000..640dbf304 --- /dev/null +++ b/scripts/soak/rack.test.mts @@ -0,0 +1,81 @@ +// Filesystem-touching tests run against a throwaway rack under os.tmpdir(): +// XDG_DATA_HOME is pointed at a fresh temp dir BEFORE paths.mts is imported +// (dynamic imports below), so nothing here can touch the real rack. +import assert from 'node:assert/strict' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { after, test } from 'node:test' + +const SANDBOX = mkdtempSync(path.join(os.tmpdir(), 'soak-rack-test-')) +process.env.XDG_DATA_HOME = SANDBOX + +const { linkHandle, rackRealFor, writeShims } = await import('./external-tools.mts') +const { BIN_DIR, RACK_DIR } = await import('./paths.mts') + +after(() => { + rmSync(SANDBOX, { recursive: true, force: true }) +}) + +function plantRackBinary(cmd: string, version: string): string { + const dir = path.join(RACK_DIR, cmd, version) + mkdirSync(dir, { recursive: true }) + const bin = path.join(dir, cmd) + writeFileSync(bin, '#!/usr/bin/env bash\necho real-binary\n') + return bin +} + +test('the sandbox rack lives under os.tmpdir()', () => { + assert.ok(BIN_DIR.startsWith(os.tmpdir().replace(/\/$/, '')) || BIN_DIR.startsWith(SANDBOX)) +}) + +test('linkHandle replaces existing and DANGLING handles without throwing', () => { + const bin = plantRackBinary('fakea', '1.0.0') + linkHandle(bin, 'fakea') + // replace an existing handle + linkHandle(bin, 'fakea') + // dangling: remove the target, the handle now points nowhere + rmSync(bin) + const bin2 = plantRackBinary('fakea', '2.0.0') + linkHandle(bin2, 'fakea') + assert.equal(readFileSync(path.join(BIN_DIR, 'fakea'), 'utf8').includes('real-binary'), true) +}) + +test('rackRealFor resolves version binaries and wrappers, else empty', () => { + const bin = plantRackBinary('fakeb', '3.1.4') + assert.equal(rackRealFor('fakeb', { fakeb: { version: '3.1.4' } }), bin) + const wrapper = path.join(RACK_DIR, 'fakec', 'fakec-wrapper') + mkdirSync(path.dirname(wrapper), { recursive: true }) + writeFileSync(wrapper, '#!/usr/bin/env bash\n') + assert.equal(rackRealFor('fakec', { fakec: { purl: 'pkg:npm/fakec@1.0.0' } }), wrapper) + assert.equal(rackRealFor('missing', {}), '') +}) + +test('writeShims never clobbers a rack binary and embeds its path', t => { + if (process.platform === 'win32') { + t.skip('shims are POSIX-only') + return + } + const tools = { pnpm: { version: '11.8.0' } } + const bin = plantRackBinary('pnpm', '11.8.0') + linkHandle(bin, 'pnpm') + writeShims(tools) + // regression (writing through the symlink): the rack binary must be intact + assert.match(readFileSync(bin, 'utf8'), /real-binary/) + const shim = readFileSync(path.join(BIN_DIR, 'pnpm'), 'utf8') + assert.ok(shim.includes(`REAL='${bin}'`), 'pinned command embeds its rack path') + // unpinned commands fall back to PATH-stripping resolution + assert.match(readFileSync(path.join(BIN_DIR, 'cargo'), 'utf8'), /CLEAN_PATH/) +}) + +test('writeShims is idempotent: a re-run keeps the rack pin embedded', t => { + if (process.platform === 'win32') { + t.skip('shims are POSIX-only') + return + } + const tools = { pnpm: { version: '11.8.0' } } + writeShims(tools) + writeShims(tools) // handle is now a regular shim file, not a symlink + const shim = readFileSync(path.join(BIN_DIR, 'pnpm'), 'utf8') + assert.ok(shim.includes("REAL='"), 'rack pin survives a second --shims run') +}) diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts index 7fd06f5a8..fc0bbba63 100644 --- a/scripts/soak/soak.mts +++ b/scripts/soak/soak.mts @@ -334,26 +334,23 @@ export function main(argv: string[] = process.argv.slice(2)): number { rel: string check: (body: string, file: string) => Finding[] fixer?: (body: string) => string - required: boolean }> = [ - { rel: SURFACES.cargoConfig, check: checkCargoConfig, fixer: fixCargoConfig, required: true }, - { rel: SURFACES.npmrc, check: checkNpmrc, fixer: fixNpmrc, required: true }, - { rel: SURFACES.workspaceYaml, check: checkWorkspaceYaml, fixer: fixWorkspaceYaml, required: true }, - { rel: SURFACES.tazeConfig, check: checkTazeConfig, required: true }, + { rel: SURFACES.cargoConfig, check: checkCargoConfig, fixer: fixCargoConfig }, + { rel: SURFACES.npmrc, check: checkNpmrc, fixer: fixNpmrc }, + { rel: SURFACES.workspaceYaml, check: checkWorkspaceYaml, fixer: fixWorkspaceYaml }, + { rel: SURFACES.tazeConfig, check: checkTazeConfig }, ] for (const s of surfaces) { const abs = path.join(REPO_ROOT, s.rel) if (!existsSync(abs)) { - if (s.required) { - findings.push({ - file: s.rel, - what: 'soak surface missing', - saw: '(file absent)', - wanted: 'file present and carrying the soak window', - fix: `create ${s.rel} — see scripts/soak/constants.mts header for the expected key`, - }) - } + findings.push({ + file: s.rel, + what: 'soak surface missing', + saw: '(file absent)', + wanted: 'file present and carrying the soak window', + fix: `create ${s.rel} — see scripts/soak/constants.mts header for the expected key`, + }) continue } let body = readFileSync(abs, 'utf8') diff --git a/scripts/soak/soak.test.mts b/scripts/soak/soak.test.mts index eb3f2cd44..63d704e7c 100644 --- a/scripts/soak/soak.test.mts +++ b/scripts/soak/soak.test.mts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import { test } from 'node:test' -import { addDaysIso, todayIso } from './constants.mts' +import { SOAK_DAYS, addDaysIso, todayIso } from './constants.mts' import { checkCargoConfig, checkCatalogParity, @@ -17,7 +17,7 @@ import { // A pin published yesterday is inside its window; one published long ago // has expired. Built relative to today so the tests never go stale. const FRESH_PUB = addDaysIso(todayIso(), -1) -const FRESH_REM = addDaysIso(FRESH_PUB, 7) +const FRESH_REM = addDaysIso(FRESH_PUB, SOAK_DAYS) const CLEAN_YAML = `catalog: taze: 19.14.1 From 1caeae6be5d8060f2b312ed4c51e72a8fde872b8 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 16:08:43 -0400 Subject: [PATCH 18/29] feat(soak): enforce the toolchain nightly soak, add the soak skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The toolchain rule was prose; now rust-toolchain.toml carries a machine-read '# adopted: YYYY-MM-DD' line and soak.mts enforces adopted >= nightly date + SOAK_DAYS as a fifth checked surface (stable pins pass freely; missing/impossible dates are findings). .claude/skills/soak/SKILL.md documents the whole system pointing at scripts/soak/: the one-place window change (edit SOAK_DAYS, run soak:fix), the committed-only opt-out (SOAK_DAYS = 0 — deliberately no env bypass), per-package dated excludes, per-tool soakBypass, and the nightly bump procedure. .gitignore opens /.claude/skills/ for tracking (contents-exclusion + negation, since git cannot re-include children of an excluded directory). --- .claude/skills/soak/SKILL.md | 66 ++++++++++++++++++++++++++++++++++++ .gitignore | 6 +++- .rules | 4 +-- rust-toolchain.toml | 21 +++++++----- scripts/soak/paths.mts | 1 + scripts/soak/soak.mts | 48 ++++++++++++++++++++++++++ scripts/soak/soak.test.mts | 12 +++++++ 7 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 .claude/skills/soak/SKILL.md diff --git a/.claude/skills/soak/SKILL.md b/.claude/skills/soak/SKILL.md new file mode 100644 index 000000000..83e4dc5e1 --- /dev/null +++ b/.claude/skills/soak/SKILL.md @@ -0,0 +1,66 @@ +--- +name: soak +description: Manage the repo's supply-chain soak window (SOAK_DAYS) — check/fix the derived surfaces, bump or opt out of the window, add a per-package exclusion with its dated annotation, or bump a pinned external tool. Use whenever a task touches minimumReleaseAge, min-release-age, min-publish-age, the nightly toolchain pin, external-tools.json, taze cooldowns, or a "why won't this fresh version install" question. +--- + +# The soak window + +One rule: a release must be at least `SOAK_DAYS` old before this repo adopts +it. The window is defined exactly once — `scripts/soak/constants.mts` +(`SOAK_DAYS = 7`) — and every surface derives from or is parity-checked +against it: + +| Surface | Key | Units | +|---|---|---| +| `.cargo/config.toml` | `global-min-publish-age` | `"N days"` | +| `rust-toolchain.toml` | nightly channel date vs `# adopted:` line | days | +| `docs/pnpm-workspace.yaml` | `minimumReleaseAge` | minutes | +| `docs/.npmrc` | `min-release-age` | days | +| `docs/taze.config.mts` | `maturityPeriod` | imports `SOAK_DAYS` | +| `external-tools.json` | `soakBypass` annotations | days | + +## Commands (mise tasks — the scripts live in `scripts/soak/`) + +- `mise run soak` — parity-check every surface (CI-gated in test-linux) +- `mise run soak:fix` — rewrite drifted windows, prune expired excludes +- `mise run deps:update` — bump npm (taze) + cargo deps through the window +- `mise run tools:check` / `tools:install` — validate / install the + SRI-pinned external tools (`external-tools.json`) +- `mise run test:scripts` — the scripts' own unit tests + +## Change the window (one place) + +1. Edit `SOAK_DAYS` in `scripts/soak/constants.mts`. +2. `mise run soak:fix` (rewrites cargo/npmrc/yaml; taze follows by import). +3. `mise run soak` + `mise run test:scripts` — existing exclude annotations + encode the old window and will be flagged; re-date or remove them. + +**Opt out entirely**: set `SOAK_DAYS = 0` and run the same two steps — +cargo, pnpm/aube (`minimumReleaseAge: 0`), npm, and taze all treat zero as +disabled. There is deliberately no env-var bypass: opting out is a +committed, reviewable change, never a silent one. + +## Skip the soak for ONE package (dated, temporary) + +Add to `minimumReleaseAgeExclude` in `docs/pnpm-workspace.yaml` with the +annotation on the line above (block list only — flow `[..]` is rejected): + +```yaml +# published: 2026-07-08 | removable: 2026-07-15 +- 'name@1.2.3' +``` + +`removable` = `published + SOAK_DAYS`. `published` must be the real registry +publish date. Once `removable` passes, `mise run soak` fails until the pin +is pruned (`soak:fix` does it). Bare names / `@scope/*` globs are standing +trust and need no annotation. External tools use the same shape via a +`soakBypass` object in `external-tools.json`. + +## Bump the nightly toolchain + +Pick the newest nightly at least `SOAK_DAYS` old **today**, set it as +`channel`, and update the `# adopted: ` line in +`rust-toolchain.toml` — `mise run soak` enforces the arithmetic. Also bump +the pre-baked toolchain in `.buildkite/linux-agent.Dockerfile` +(`mise run tools:check` fails on any drift between the image and the +tracked pins). diff --git a/.gitignore b/.gitignore index 529a8cb2e..85d06f22f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ /scratch/ /sandbox/ /bench-install.sh -/.claude/ +# Local agent state stays untracked, but committed skills are allowed: +# git cannot re-include children of an excluded DIRECTORY, so exclude the +# dir's contents (/*) and negate the skills subtree. +/.claude/* +!/.claude/skills/ /crates/aube-resolver/data/primer-top*.json /crates/aube-resolver/data/primer-top*.rkyv.zst diff --git a/.rules b/.rules index 551e5a473..2d210d0e5 100644 --- a/.rules +++ b/.rules @@ -244,11 +244,11 @@ soak: window: SOAK_DAYS=7 in scripts/soak/constants.mts — the ONE source; every surface derives or is parity-checked against it surfaces[5]: - .cargo/config.toml global-min-publish-age ("7 days") - - rust-toolchain.toml dated nightly (adopt only nightlies >= 7 days old) + - rust-toolchain.toml dated nightly (channel date vs the machine-read `# adopted:` line — soak.mts enforces adopted >= channel + SOAK_DAYS) - docs/pnpm-workspace.yaml minimumReleaseAge (10080 minutes; also the taze catalog: home — aube reads this file for docs installs, and a ROOT pnpm-workspace.yaml would re-anchor the docs install at repo root, so it must stay in docs/) - docs/.npmrc min-release-age (7 days, npm >= 11.17 parity) - docs/taze.config.mts maturityPeriod (imports SOAK_DAYS) - manager: mise run soak (check, CI-gated in test-linux) / soak:fix (syncs windows, prunes expired excludes) / deps:update (taze + cargo update, both soaked) + manager: .claude/skills/soak/SKILL.md is the how-to; mise run soak (check, CI-gated in test-linux) / soak:fix (syncs windows, prunes expired excludes) / deps:update (taze + cargo update, both soaked) excludes: version-pinned minimumReleaseAgeExclude entries REQUIRE `# published: YYYY-MM-DD | removable: YYYY-MM-DD` on the line above (removable = published + 7d); bare names/globs are standing trust and need no annotation; expired pins are pruned by soak:fix socket_firewall: CI installs sfw (free tier keyless; enterprise auto-selected when the SOCKET_SECURITY_KEY repo secret lands — the one env var every Socket product reads, no aliasing) and shims npm/yarn/pnpm/pip/pip3/uv/cargo via scripts/soak/external-tools.mts --shims; the buildkite agent image pre-bakes the same binary + shims diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 042efd083..132fa3e04 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,11 +1,16 @@ -# Pinned dated NIGHTLY, adopted under a 7-day soak: the pin is the newest -# nightly at least 7 days old on the adoption date (2026-07-11), and bumps -# follow the same rule. The pin is exact so the whole PGO/BOLT flow -# (mise run build:pgo, release upload-assets-pgo) instruments and optimizes -# with one toolchain and the collected profile matches the LLVM version -# that consumes it. llvm-tools-preview ships the llvm-profdata that -# benchmarks/pgo.bash otherwise requires by hand. rust-version in -# Cargo.toml stays the STABLE msrv floor (1.95, core::hint::cold_path). +# Pinned dated NIGHTLY, adopted under the soak window (SOAK_DAYS in +# scripts/soak/constants.mts): the pin is the newest nightly at least that +# old on the adoption date, and bumps follow the same rule. The `adopted:` +# line below is machine-read by `mise run soak` to enforce it. +# +# The pin is exact so the whole PGO/BOLT flow (mise run build:pgo, release +# upload-assets-pgo) instruments and optimizes with one toolchain and the +# collected profile matches the LLVM version that consumes it. +# llvm-tools-preview ships the llvm-profdata that benchmarks/pgo.bash +# otherwise requires by hand. rust-version in Cargo.toml stays the STABLE +# msrv floor (1.95, core::hint::cold_path). +# +# adopted: 2026-07-11 [toolchain] channel = "nightly-2026-07-04" components = ["rustfmt", "clippy", "llvm-tools-preview"] diff --git a/scripts/soak/paths.mts b/scripts/soak/paths.mts index c809a1abe..4b764f30e 100644 --- a/scripts/soak/paths.mts +++ b/scripts/soak/paths.mts @@ -22,6 +22,7 @@ export const SURFACES = { npmrc: 'docs/.npmrc', workspaceYaml: 'docs/pnpm-workspace.yaml', tazeConfig: 'docs/taze.config.mts', + toolchainToml: 'rust-toolchain.toml', } // The directory holding the npm package the soak governs (taze runs here, diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts index fc0bbba63..31f87c2b7 100644 --- a/scripts/soak/soak.mts +++ b/scripts/soak/soak.mts @@ -253,6 +253,53 @@ export function checkCatalogParity( return out } +/** + * The toolchain pin obeys the same soak: a dated nightly must have been at + * least SOAK_DAYS old on its recorded adoption date (`# adopted:` line, + * machine-read here). Stable channel pins carry no date and pass freely. + */ +export function checkToolchainSoak(body: string, file: string): Finding[] { + const channelDate = /^channel\s*=\s*"nightly-(\d{4}-\d{2}-\d{2})"/m.exec(body)?.[1] + if (!channelDate) { + return [] + } + const adopted = /^#\s*adopted:\s*(\d{4}-\d{2}-\d{2})\s*$/m.exec(body)?.[1] + if (!adopted) { + return [ + { + file, + what: 'toolchain adoption date', + saw: '(no `# adopted: YYYY-MM-DD` line)', + wanted: 'a recorded adoption date so the nightly soak is checkable', + fix: 'add `# adopted: ` above [toolchain]', + }, + ] + } + if (!isValidIsoDate(channelDate) || !isValidIsoDate(adopted)) { + return [ + { + file, + what: 'toolchain soak dates', + saw: `${channelDate} | ${adopted}`, + wanted: 'real YYYY-MM-DD calendar dates', + fix: 'correct the nightly channel / adopted dates', + }, + ] + } + if (addDaysIso(channelDate, SOAK_DAYS) > adopted) { + return [ + { + file, + what: 'toolchain nightly soak', + saw: `nightly-${channelDate} adopted ${adopted}`, + wanted: `a nightly at least ${SOAK_DAYS} days old at adoption`, + fix: 'pin the newest nightly that had cleared the window on the adoption date', + }, + ] + } + return [] +} + export function checkTazeConfig(body: string, file: string): Finding[] { const out: Finding[] = [] if (!body.includes('maturityPeriod')) { @@ -339,6 +386,7 @@ export function main(argv: string[] = process.argv.slice(2)): number { { rel: SURFACES.npmrc, check: checkNpmrc, fixer: fixNpmrc }, { rel: SURFACES.workspaceYaml, check: checkWorkspaceYaml, fixer: fixWorkspaceYaml }, { rel: SURFACES.tazeConfig, check: checkTazeConfig }, + { rel: SURFACES.toolchainToml, check: checkToolchainSoak }, ] for (const s of surfaces) { diff --git a/scripts/soak/soak.test.mts b/scripts/soak/soak.test.mts index 63d704e7c..835728415 100644 --- a/scripts/soak/soak.test.mts +++ b/scripts/soak/soak.test.mts @@ -8,6 +8,7 @@ import { checkExcludeAnnotations, checkNpmrc, checkTazeConfig, + checkToolchainSoak, checkWorkspaceYaml, fixNpmrc, fixWorkspaceYaml, @@ -115,3 +116,14 @@ test('taze config: window must be imported, not hand-copied', () => { assert.equal(checkTazeConfig('export default { maturityPeriod: 7 }\n', 't').length, 1) assert.equal(checkTazeConfig('export default {}\n', 't').length, 2) }) + +test('toolchain soak: nightly must be SOAK_DAYS old at adoption; stable passes', () => { + const good = '# adopted: 2026-07-11\n[toolchain]\nchannel = "nightly-2026-07-04"\n' + assert.equal(checkToolchainSoak(good, 't').length, 0) + const tooFresh = '# adopted: 2026-07-11\n[toolchain]\nchannel = "nightly-2026-07-08"\n' + assert.match(checkToolchainSoak(tooFresh, 't')[0]!.what, /nightly soak/) + const noDate = '[toolchain]\nchannel = "nightly-2026-07-04"\n' + assert.match(checkToolchainSoak(noDate, 't')[0]!.what, /adoption date/) + const stable = '[toolchain]\nchannel = "1.95.0"\n' + assert.equal(checkToolchainSoak(stable, 't').length, 0) +}) From 3c1e24ed7d58c07e2b448a8bc4c05e2d600d6301 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 16:18:38 -0400 Subject: [PATCH 19/29] docs(soak): align skill with claude prompting and skill-authoring guidance --- .claude/skills/soak/SKILL.md | 48 ++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/.claude/skills/soak/SKILL.md b/.claude/skills/soak/SKILL.md index 83e4dc5e1..1fc973eef 100644 --- a/.claude/skills/soak/SKILL.md +++ b/.claude/skills/soak/SKILL.md @@ -1,13 +1,15 @@ --- name: soak -description: Manage the repo's supply-chain soak window (SOAK_DAYS) — check/fix the derived surfaces, bump or opt out of the window, add a per-package exclusion with its dated annotation, or bump a pinned external tool. Use whenever a task touches minimumReleaseAge, min-release-age, min-publish-age, the nightly toolchain pin, external-tools.json, taze cooldowns, or a "why won't this fresh version install" question. +description: Manages the repo's supply-chain soak window (SOAK_DAYS) — checks and fixes the derived surfaces, bumps or disables the window, adds dated per-package exclusions, and bumps pinned external tools. Use when a task touches minimumReleaseAge, min-release-age, min-publish-age, the nightly toolchain pin, external-tools.json, or taze cooldowns, or when investigating why a freshly published version won't install. --- # The soak window -One rule: a release must be at least `SOAK_DAYS` old before this repo adopts -it. The window is defined exactly once — `scripts/soak/constants.mts` -(`SOAK_DAYS = 7`) — and every surface derives from or is parity-checked +One rule: a release must be at least `SOAK_DAYS` old before this repo +adopts it. The delay gives the ecosystem time to catch a malicious or +yanked release before we ever install it. The window is defined exactly +once — read the current value from `scripts/soak/constants.mts` and never +hardcode it elsewhere. Every surface derives from or is parity-checked against it: | Surface | Key | Units | @@ -22,18 +24,22 @@ against it: ## Commands (mise tasks — the scripts live in `scripts/soak/`) - `mise run soak` — parity-check every surface (CI-gated in test-linux) -- `mise run soak:fix` — rewrite drifted windows, prune expired excludes +- `mise run soak:fix` — rewrite drifted windows, prune expired exclusions - `mise run deps:update` — bump npm (taze) + cargo deps through the window - `mise run tools:check` / `tools:install` — validate / install the SRI-pinned external tools (`external-tools.json`) - `mise run test:scripts` — the scripts' own unit tests +A soak change is done when `mise run soak` and `mise run test:scripts` +both exit 0 — the same gates CI runs. Re-run them after every fix. + ## Change the window (one place) 1. Edit `SOAK_DAYS` in `scripts/soak/constants.mts`. 2. `mise run soak:fix` (rewrites cargo/npmrc/yaml; taze follows by import). -3. `mise run soak` + `mise run test:scripts` — existing exclude annotations - encode the old window and will be flagged; re-date or remove them. +3. `mise run soak` + `mise run test:scripts` — existing exclusion + annotations encode the old window and will be flagged; re-date or + remove them, then re-run until both pass. **Opt out entirely**: set `SOAK_DAYS = 0` and run the same two steps — cargo, pnpm/aube (`minimumReleaseAge: 0`), npm, and taze all treat zero as @@ -43,18 +49,20 @@ committed, reviewable change, never a silent one. ## Skip the soak for ONE package (dated, temporary) Add to `minimumReleaseAgeExclude` in `docs/pnpm-workspace.yaml` with the -annotation on the line above (block list only — flow `[..]` is rejected): +annotation on the line above (block list only — flow `[..]` is rejected +because a comment line can't attach to an inline entry): ```yaml # published: 2026-07-08 | removable: 2026-07-15 - 'name@1.2.3' ``` -`removable` = `published + SOAK_DAYS`. `published` must be the real registry -publish date. Once `removable` passes, `mise run soak` fails until the pin -is pruned (`soak:fix` does it). Bare names / `@scope/*` globs are standing -trust and need no annotation. External tools use the same shape via a -`soakBypass` object in `external-tools.json`. +`removable` = `published + SOAK_DAYS` (this example assumes a 7-day +window). `published` must be the real registry publish date. Once +`removable` passes, `mise run soak` fails until the pin is pruned +(`soak:fix` does it). Bare names / `@scope/*` globs are standing trust and +need no annotation. External tools use the same shape via a `soakBypass` +object in `external-tools.json`. ## Bump the nightly toolchain @@ -64,3 +72,17 @@ Pick the newest nightly at least `SOAK_DAYS` old **today**, set it as the pre-baked toolchain in `.buildkite/linux-agent.Dockerfile` (`mise run tools:check` fails on any drift between the image and the tracked pins). + +## Maintaining this skill + +`scripts/soak/` is the law; this file only documents it — when they +disagree, fix this file. When editing, follow Anthropic's guidance: + +- [Prompting best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices) +- [Prompting Claude Fable 5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5) +- [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) +- [Write an effective CLAUDE.md](https://code.claude.com/docs/en/best-practices#write-an-effective-claude-md) + +Keep it concise (goal + constraints, not step enumeration), keep the +description in third person with explicit "use when" triggers, and keep +the window value in `constants.mts` rather than restating it here. From cb6edd61a2856be0e2660e1dcb7e18e12ff10a3c Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 17:10:22 -0400 Subject: [PATCH 20/29] test(soak): cover cli paths, fixers, download token gating, installer guards --- scripts/soak/external-tools.mts | 8 +- scripts/soak/external-tools.test.mts | 161 ++++++++++++++++++++++++++- scripts/soak/soak.test.mts | 40 +++++++ scripts/soak/update-deps.mts | 22 ++-- scripts/soak/update-deps.test.mts | 18 +++ 5 files changed, 235 insertions(+), 14 deletions(-) create mode 100644 scripts/soak/update-deps.test.mts diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 3ad2269aa..2e2ee8282 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -182,7 +182,7 @@ export function checkDockerPrebake( return out } -async function download(url: string, expectedSri: string): Promise { +export async function download(url: string, expectedSri: string): Promise { const headers: Record = {} // Only GitHub gets the token (private release assets); sending it to any // other host (e.g. the npm registry for purl tools) would leak the @@ -208,7 +208,7 @@ async function download(url: string, expectedSri: string): Promise { return buf } -function extractArchive(name: string, destDir: string, asset: string, buf: Buffer): void { +export function extractArchive(name: string, destDir: string, asset: string, buf: Buffer): void { const archive = path.join(destDir, asset) writeFileSync(archive, buf) // bsdtar (macOS + Windows runners) extracts zip via plain -xf too. @@ -340,7 +340,7 @@ function installDeps(name: string, pkgDir: string): number { return 1 } -async function installTool(name: string, tools: Record): Promise { +export async function installTool(name: string, tools: Record): Promise { // `sfw` is a flavor pair: the enterprise binary when a Socket token is // present (repo secret), the keyless free tier otherwise. The firewall // shim mechanism is POSIX (bash shims, extension-less symlink handles) — @@ -451,7 +451,7 @@ exec sfw '${cmd}' "$@" console.log(`[external-tools] prepend ${BIN_DIR} to PATH to activate`) } -async function main(argv: string[] = process.argv.slice(2)): Promise { +export async function main(argv: string[] = process.argv.slice(2)): Promise { if (argv.includes('--print-bin')) { console.log(BIN_DIR) return 0 diff --git a/scripts/soak/external-tools.test.mts b/scripts/soak/external-tools.test.mts index 1e99c7628..ccf3163db 100644 --- a/scripts/soak/external-tools.test.mts +++ b/scripts/soak/external-tools.test.mts @@ -1,10 +1,21 @@ import assert from 'node:assert/strict' -import { existsSync, readFileSync } from 'node:fs' +import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import os from 'node:os' import path from 'node:path' import { test } from 'node:test' +import { fileURLToPath } from 'node:url' import { SOAK_DAYS, addDaysIso, todayIso } from './constants.mts' -import { checkDockerPrebake, checkPins } from './external-tools.mts' +import { + checkDockerPrebake, + checkPins, + download, + extractArchive, + installTool, + main, +} from './external-tools.mts' import { DOCKER_PREBAKE, EXTERNAL_TOOLS_JSON, REPO_ROOT, RUST_TOOLCHAIN_TOML } from './paths.mts' const GOOD_SRI = @@ -58,3 +69,149 @@ test('the repo Dockerfile prebake (when present) matches the tracked pins', t => 0, ) }) + +const sriOf = (buf: Buffer) => `sha512-${createHash('sha512').update(buf).digest('base64')}` + +function withEnv(name: string, value: string | undefined, fn: () => Promise): Promise { + const saved = process.env[name] + if (value === undefined) { + delete process.env[name] + } else { + process.env[name] = value + } + return fn().finally(() => { + if (saved === undefined) { + delete process.env[name] + } else { + process.env[name] = saved + } + }) +} + +// Hermetic fixture: one line per drift class, so every checkDockerPrebake +// failure branch is exercised even in a repo with no prebake image. +test('checkDockerPrebake flags every drift class (synthetic image)', () => { + const tools = { + 'sfw-free': { + version: '1.0.0', + platforms: { 'linux-arm64': { asset: 'sfw-linux-arm64', integrity: GOOD_SRI } }, + }, + } + const wrongHex = 'ab'.repeat(64) + const body = [ + 'for cmd in npm yarn; do make_shim "$cmd"; done', + 'RUN curl -o /x https://github.com/SocketDev/sfw-free/releases/download/v0.9.9/sfw-linux-arm64', + 'COPY rack/sfw-free/0.9.9/sfw /usr/local/bin/sfw', + `RUN asset=ghost-asset; sha=${wrongHex} verify`, + `RUN asset=sfw-linux-arm64; sha=${wrongHex} verify`, + ].join('\n') + const problems = checkDockerPrebake(body, tools, 'channel = "nightly-2026-07-04"\n', '9.9.9') + for (const needle of [ + /shim list/, + /msrv toolchain/, + /version 0\.9\.9/, + /download url v0\.9\.9/, + /ghost-asset has no pin/, + /sha for sfw-linux-arm64/, + /pinned toolchain nightly-2026-07-04/, + ]) { + assert.ok(problems.some(p => needle.test(p)), `expected a finding matching ${needle}`) + } + assert.ok( + checkDockerPrebake('nothing pinned here', tools, '').some(p => + /no asset\/sha pin pairs/.test(p), + ), + ) +}) + +test('download sends the GitHub token to github.com only', async t => { + const payload = Buffer.from('pinned-bytes') + const seen: Array<{ host: string; auth: string | undefined }> = [] + t.mock.method(globalThis, 'fetch', async (url: string | URL, init?: RequestInit) => { + seen.push({ + host: new URL(String(url)).hostname, + auth: (init?.headers as Record | undefined)?.authorization, + }) + return new Response(payload) + }) + await withEnv('GITHUB_TOKEN', 'ghs_test_token', async () => { + await download('https://github.com/o/r/releases/download/v1/a.tgz', sriOf(payload)) + await download('https://registry.npmjs.org/x/-/x-1.0.0.tgz', sriOf(payload)) + }) + assert.equal(seen[0]!.auth, 'Bearer ghs_test_token') + assert.equal(seen[1]!.auth, undefined) +}) + +test('download rejects http errors and integrity mismatches', async t => { + const payload = Buffer.from('served-bytes') + let status = 503 + t.mock.method(globalThis, 'fetch', async () => new Response(payload, { status })) + await assert.rejects(download('https://example.com/a', sriOf(payload)), /download failed 503/) + status = 200 + await assert.rejects(download('https://example.com/a', 'sha512-AAAA'), /integrity mismatch/) + assert.deepEqual(await download('https://example.com/a', sriOf(payload)), payload) +}) + +test('extractArchive unpacks a tar.gz, removes the archive, throws on junk', t => { + const dir = mkdtempSync(path.join(os.tmpdir(), 'soak-extract-')) + t.after(() => rmSync(dir, { recursive: true, force: true })) + writeFileSync(path.join(dir, 'hello.txt'), 'hi\n') + spawnSync('tar', ['-czf', path.join(dir, 'a.tar.gz'), '-C', dir, 'hello.txt']) + const buf = readFileSync(path.join(dir, 'a.tar.gz')) + const dest = mkdtempSync(path.join(os.tmpdir(), 'soak-extract-dest-')) + t.after(() => rmSync(dest, { recursive: true, force: true })) + extractArchive('t', dest, 'a.tar.gz', buf) + assert.ok(existsSync(path.join(dest, 'hello.txt'))) + assert.ok(!existsSync(path.join(dest, 'a.tar.gz'))) + assert.throws(() => extractArchive('t', dest, 'bad.tgz', Buffer.from('junk')), /extract failed/) +}) + +test('installTool rejects unknown tools, foreign purls, and shapeless pins', async () => { + await assert.rejects(installTool('ghost', {}), /unknown tool/) + await assert.rejects( + installTool('x', { x: { purl: 'pkg:pypi/foo@1.0.0', integrity: GOOD_SRI } }), + /unsupported purl/, + ) + 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: {} } }), + /no pinned asset for/, + ) +}) + +test('installTool resolves the sfw flavor from SOCKET_SECURITY_KEY', async t => { + if (process.platform === 'win32') { + t.skip('sfw shims are POSIX-only') + return + } + // An empty tools record makes the resolved flavor observable in the + // rejection message — no download is ever attempted. + await withEnv('SOCKET_SECURITY_KEY', undefined, () => + assert.rejects(installTool('sfw', {}), /unknown tool sfw-free/), + ) + await withEnv('SOCKET_SECURITY_KEY', 'sk_test', () => + assert.rejects(installTool('sfw', {}), /unknown tool sfw-enterprise/), + ) +}) + +test('installTool prints the uvx line for uv-project 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') + await installTool(uv!, tools) +}) + +// Glue: main's read-only CLI paths against the tracked manifest — the same +// gate CI runs, exercised in-process so main() itself stays covered. +test('main --print-bin and --check are read-only and exit 0', async () => { + assert.equal(await main(['--print-bin']), 0) + assert.equal(await main(['--check']), 0) +}) + +// End to end through the entrypoint guard: the CLI must resolve as main +// (realpath + file URL) and exit 0 on the tracked manifest. +test('CLI: node external-tools.mts --check exits 0', () => { + const script = fileURLToPath(new URL('./external-tools.mts', import.meta.url)) + const res = spawnSync(process.execPath, [script, '--check'], { encoding: 'utf8' }) + assert.equal(res.status, 0, res.stderr) +}) diff --git a/scripts/soak/soak.test.mts b/scripts/soak/soak.test.mts index 835728415..c8dec61bb 100644 --- a/scripts/soak/soak.test.mts +++ b/scripts/soak/soak.test.mts @@ -1,5 +1,7 @@ import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' import { test } from 'node:test' +import { fileURLToPath } from 'node:url' import { SOAK_DAYS, addDaysIso, todayIso } from './constants.mts' import { @@ -10,8 +12,10 @@ import { checkTazeConfig, checkToolchainSoak, checkWorkspaceYaml, + fixCargoConfig, fixNpmrc, fixWorkspaceYaml, + main, parseExcludeEntries, } from './soak.mts' @@ -127,3 +131,39 @@ test('toolchain soak: nightly must be SOAK_DAYS old at adoption; stable passes', const stable = '[toolchain]\nchannel = "1.95.0"\n' assert.equal(checkToolchainSoak(stable, 't').length, 0) }) + +test('toolchain soak: impossible calendar dates are findings, not crashes', () => { + const bad = '# adopted: 2026-13-45\n[toolchain]\nchannel = "nightly-2026-07-04"\n' + assert.match(checkToolchainSoak(bad, 't')[0]!.what, /soak dates/) +}) + +test('parser: a column-0 line ends the exclude block', () => { + const yaml = 'minimumReleaseAgeExclude:\n - react\nonlyBuiltDependencies:\n - esbuild\n' + assert.deepEqual(parseExcludeEntries(yaml).map(e => e.name), ['react']) +}) + +test('parser: items at a different indent are not exclude entries', () => { + const yaml = 'minimumReleaseAgeExclude:\n - react\n - not-an-entry\n - vue\n' + assert.deepEqual(parseExcludeEntries(yaml).map(e => e.name), ['react', 'vue']) +}) + +test('fix rewrites a drifted cargo window and leaves a clean one alone', () => { + const fixed = fixCargoConfig('[registry]\nglobal-min-publish-age = "3 days"\n') + assert.ok(fixed.includes(`"${SOAK_DAYS} days"`)) + assert.equal(fixCargoConfig(fixed), fixed) +}) + +// Glue: the tracked surfaces of THIS repo must satisfy the gate — the same +// check CI runs, exercised in-process so main() itself stays covered. +test('main --check passes against the tracked repo surfaces', () => { + assert.equal(main([]), 0) + assert.equal(main(['--quiet']), 0) +}) + +// End to end through the entrypoint guard: the CLI must resolve as main +// (realpath + file URL) and exit 0 on a clean tree. +test('CLI: node soak.mts --check --quiet exits 0', () => { + const script = fileURLToPath(new URL('./soak.mts', import.meta.url)) + const res = spawnSync(process.execPath, [script, '--check', '--quiet'], { encoding: 'utf8' }) + assert.equal(res.status, 0, res.stderr) +}) diff --git a/scripts/soak/update-deps.mts b/scripts/soak/update-deps.mts index b6cabe654..23e43f9b3 100644 --- a/scripts/soak/update-deps.mts +++ b/scripts/soak/update-deps.mts @@ -66,16 +66,22 @@ function updateCargo(dryRun: boolean): number { return run(RUSTUP_CARGO, dryRun ? ['update', '--dry-run'] : ['update'], REPO_ROOT) } -function main(argv: string[] = process.argv.slice(2)): number { - const dryRun = argv.includes('--dry-run') +// No flag = both; naming both explicitly also means both — a naive +// "flag present = only that one" reading once made `--npm --cargo` run +// NEITHER, so this rule lives in one exported, regression-tested place. +export function selectEcosystems(argv: string[]): { npm: boolean; cargo: boolean } { const npmFlag = argv.includes('--npm') const cargoFlag = argv.includes('--cargo') - // No flag = both; naming both explicitly also means both. Run every - // requested ecosystem even if an earlier one fails, then aggregate. - const wantNpm = npmFlag || !cargoFlag - const wantCargo = cargoFlag || !npmFlag - const npmStatus = wantNpm ? updateNpm(dryRun) : 0 - const cargoStatus = wantCargo ? updateCargo(dryRun) : 0 + return { npm: npmFlag || !cargoFlag, cargo: cargoFlag || !npmFlag } +} + +function main(argv: string[] = process.argv.slice(2)): number { + const dryRun = argv.includes('--dry-run') + const { npm, cargo } = selectEcosystems(argv) + // Run every requested ecosystem even if an earlier one fails, then + // aggregate, so one broken ecosystem can't hide the other's drift. + const npmStatus = npm ? updateNpm(dryRun) : 0 + const cargoStatus = cargo ? updateCargo(dryRun) : 0 return npmStatus || cargoStatus } diff --git a/scripts/soak/update-deps.test.mts b/scripts/soak/update-deps.test.mts new file mode 100644 index 000000000..bc159c7b9 --- /dev/null +++ b/scripts/soak/update-deps.test.mts @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' + +import { selectEcosystems } from './update-deps.mts' + +test('no ecosystem flag updates both', () => { + assert.deepEqual(selectEcosystems([]), { npm: true, cargo: true }) + assert.deepEqual(selectEcosystems(['--dry-run']), { npm: true, cargo: true }) +}) + +test('a single flag selects only that ecosystem', () => { + assert.deepEqual(selectEcosystems(['--npm']), { npm: true, cargo: false }) + assert.deepEqual(selectEcosystems(['--cargo', '--dry-run']), { npm: false, cargo: true }) +}) + +test('naming both explicitly means both, not neither (regression)', () => { + assert.deepEqual(selectEcosystems(['--npm', '--cargo']), { npm: true, cargo: true }) +}) From eb8928183d01916634fb9754a10e7890b0e541c7 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 18:33:21 -0400 Subject: [PATCH 21/29] refactor(soak): dedupe the rust-toolchain.toml path, complete the header inventory --- scripts/soak/external-tools.mts | 4 ++-- scripts/soak/external-tools.test.mts | 4 ++-- scripts/soak/paths.mts | 1 - scripts/soak/soak.mts | 1 + 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 2e2ee8282..40eba2681 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -45,7 +45,7 @@ import { PM_DEP_INSTALLERS, RACK_DIR, REPO_ROOT, - RUST_TOOLCHAIN_TOML, + SURFACES, } from './paths.mts' const SFW_ECOSYSTEMS = ['npm', 'yarn', 'pnpm', 'pip', 'pip3', 'uv', 'cargo'] @@ -461,7 +461,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise } const tools = JSON.parse(readFileSync(EXTERNAL_TOOLS_JSON, 'utf8')).tools const docker = readFileSync(path.join(REPO_ROOT, DOCKER_PREBAKE), 'utf8') - const toolchain = readFileSync(path.join(REPO_ROOT, RUST_TOOLCHAIN_TOML), 'utf8') + const toolchain = readFileSync(path.join(REPO_ROOT, SURFACES.toolchainToml), 'utf8') assert.deepEqual(checkDockerPrebake(docker, tools, toolchain), []) // and drift in any direction is caught assert.ok(checkDockerPrebake(docker.replace(/sha=[0-9a-f]{8}/, 'sha=deadbeef'), tools, toolchain).length > 0) diff --git a/scripts/soak/paths.mts b/scripts/soak/paths.mts index 4b764f30e..70e63f132 100644 --- a/scripts/soak/paths.mts +++ b/scripts/soak/paths.mts @@ -49,7 +49,6 @@ export const EXTERNAL_TOOLS_JSON = path.join(REPO_ROOT, 'external-tools.json') // alone, so the Dockerfile can't COPY the tracked pin sources — it embeds // copies, and external-tools.mts --check asserts they haven't drifted. export const DOCKER_PREBAKE: string | null = '.buildkite/linux-agent.Dockerfile' -export const RUST_TOOLCHAIN_TOML = 'rust-toolchain.toml' // .dockerignore managed by `untracked` (null when the repo has no // repo-context docker builds — aube's agent image never copies the repo). diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts index 31f87c2b7..4bd3773e6 100644 --- a/scripts/soak/soak.mts +++ b/scripts/soak/soak.mts @@ -6,6 +6,7 @@ * valid, unexpired `# published: | removable:` annotation: * * - `.cargo/config.toml` `global-min-publish-age` + `[unstable] min-publish-age` + * - `rust-toolchain.toml` nightly channel vs `# adopted:` (dated-nightly soak) * - `docs/pnpm-workspace.yaml` `minimumReleaseAge` (minutes) + annotated excludes * - `docs/.npmrc` `min-release-age` (days) * - `docs/taze.config.mts` imports SOAK_DAYS (existence + import check) From d7d95f372f4da2e3f6fc527738f747b02aafd140 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 19:23:44 -0400 Subject: [PATCH 22/29] =?UTF-8?q?ci:=20bypass=20sfw=20for=20cargo=20?= =?UTF-8?q?=E2=80=94=20the=20proxy=20breaks=20local=20registry-mock=20test?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae7278c44..eea854cb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,12 @@ jobs: # sfw runs in its keyless free tier — the firewall is active either # way and upgrades itself to enterprise when the secret appears. SOCKET_SECURITY_KEY: ${{ secrets.SOCKET_SECURITY_KEY }} + # cargo test spins LOCAL registry mocks; routing `cargo` through the + # sfw shim would wrap that traffic in the firewall proxy and mangle + # it (405s on registry writes, bodies replaced by proxy errors). The + # sentinel makes the cargo shim exec the real cargo directly — + # npm/pnpm installs stay firewalled. + SFW_SHIM_ACTIVE_CARGO: "1" steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: From 4d99f93b46455b2a6863110c7d175c25cffa1c2c Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 21:12:15 -0400 Subject: [PATCH 23/29] style(lockfile): satisfy nightly clippy question_mark in local source parse --- crates/aube-lockfile/src/source.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/aube-lockfile/src/source.rs b/crates/aube-lockfile/src/source.rs index 662dea2df..abb9f9e21 100644 --- a/crates/aube-lockfile/src/source.rs +++ b/crates/aube-lockfile/src/source.rs @@ -248,10 +248,9 @@ impl LocalSource { ("link", r) } else if let Some(r) = spec.strip_prefix("portal:") { ("portal", r) - } else if let Some(r) = spec.strip_prefix("exec:") { - return Some(LocalSource::Exec(PathBuf::from(r))); } else { - return None; + let r = spec.strip_prefix("exec:")?; + return Some(LocalSource::Exec(PathBuf::from(r))); }; let rel = PathBuf::from(rest); let abs = project_root.join(&rel); From 806bc952b5623c2dd9a5c710daebfea6d23fa6c6 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sat, 11 Jul 2026 22:43:42 -0400 Subject: [PATCH 24/29] fix(soak): windows-safe relative tar extraction; shfmt the shim template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tar on Windows parses an absolute C:\ archive path as a remote host:path ("Cannot connect to C") — extract with a cwd-relative archive name instead - the buildkite shim template's if-body used spaces where the repo's shfmt profile mandates tabs (surfaced once clippy stopped failing first) --- .buildkite/sfw-shim-template.sh | 6 +++--- scripts/soak/external-tools.mts | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.buildkite/sfw-shim-template.sh b/.buildkite/sfw-shim-template.sh index eca1604b3..84e81e93d 100644 --- a/.buildkite/sfw-shim-template.sh +++ b/.buildkite/sfw-shim-template.sh @@ -8,9 +8,9 @@ set -euo pipefail CLEAN_PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vFx '/root/.local/share/aube/dev-tools/bin' | paste -sd ':' -) REAL=$(PATH="$CLEAN_PATH" command -v '__CMD__' || true) if [ -n "${__SENTINEL__:-}" ] || [ -z "$REAL" ] || ! command -v sfw >/dev/null 2>&1; then - [ -n "$REAL" ] && exec "$REAL" "$@" - echo "__CMD__: not found" >&2 - exit 127 + [ -n "$REAL" ] && exec "$REAL" "$@" + echo "__CMD__: not found" >&2 + exit 127 fi export __SENTINEL__=1 exec sfw '__CMD__' "$@" diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 40eba2681..0a4e9c509 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -211,9 +211,12 @@ export async function download(url: string, expectedSri: string): Promise Date: Sat, 11 Jul 2026 23:34:33 -0400 Subject: [PATCH 25/29] =?UTF-8?q?fix(soak):=20use=20System32=20bsdtar=20on?= =?UTF-8?q?=20windows=20=E2=80=94=20Git=20Bash's=20GNU=20tar=20can't=20rea?= =?UTF-8?q?d=20zip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/soak/external-tools.mts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 0a4e9c509..68408a8e9 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -211,12 +211,18 @@ export async function download(url: string, expectedSri: string): Promise Date: Sun, 12 Jul 2026 00:12:51 -0400 Subject: [PATCH 26/29] =?UTF-8?q?fix(soak):=20windows=20handles=20forward?= =?UTF-8?q?=20to=20the=20rack=20=E2=80=94=20symlinks=20strand=20SEA=20sibl?= =?UTF-8?q?ings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pnpm's Windows SEA launcher resolves dist/pnpm.mjs relative to the invoked handle's directory, so a symlinked/copied bin handle crashes with MODULE_NOT_FOUND. On win32 the handle is now a forwarding pair (.cmd for cmd/pwsh + extensionless bash shim for Git Bash) that invokes the absolute rack target; registry-tarball tools forward through node directly instead of a bash-only wrapper pwsh can't run. --- scripts/soak/external-tools.mts | 39 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 68408a8e9..07adab0de 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -235,6 +235,27 @@ export function linkHandle(target: string, name: string): void { // force also removes a DANGLING handle (existsSync would report false // for one and a bare symlink would then throw EEXIST). rmSync(handle, { force: true }) + if (process.platform === 'win32') { + // A symlinked/copied handle breaks Windows SEA binaries: pnpm.exe + // resolves its dist/ siblings from the handle's OWN directory, not the + // rack. Forward to the absolute rack target instead — a .cmd for + // cmd/pwsh and an extensionless bash shim for Git Bash. Non-.exe + // targets are node entry scripts (registry-tarball tools). + const viaExe = target.endsWith('.exe') + rmSync(`${handle}.cmd`, { force: true }) + writeFileSync( + `${handle}.cmd`, + viaExe ? `@echo off\r\n"${target}" %*\r\n` : `@echo off\r\nnode "${target}" %*\r\n`, + ) + writeFileSync( + handle, + viaExe + ? `#!/usr/bin/env bash\nexec "${target}" "$@"\n` + : `#!/usr/bin/env bash\nexec node "${target}" "$@"\n`, + ) + chmodSync(handle, 0o755) + return + } symlinkSync(target, handle) } @@ -322,13 +343,17 @@ async function installNpmTarball( typeof pkgJson.bin === 'string' ? { [name]: pkgJson.bin } : (pkgJson.bin ?? {}) const binRel = bins[name] ?? Object.values(bins)[0] if (binRel) { - const wrapper = path.join(RACK_DIR, name, `${name}-wrapper`) - writeFileSync( - wrapper, - `#!/usr/bin/env bash\nexec node '${path.join(pkgDir, binRel as string)}' "$@"\n`, - ) - chmodSync(wrapper, 0o755) - linkHandle(wrapper, name) + const binAbs = path.join(pkgDir, binRel as string) + if (process.platform === 'win32') { + // linkHandle writes node-invoking .cmd + bash forwarders for a + // non-.exe target — no bash-only wrapper to strand under pwsh. + linkHandle(binAbs, name) + } else { + const wrapper = path.join(RACK_DIR, name, `${name}-wrapper`) + writeFileSync(wrapper, `#!/usr/bin/env bash\nexec node '${binAbs}' "$@"\n`) + chmodSync(wrapper, 0o755) + linkHandle(wrapper, name) + } } console.log(`[external-tools] installed ${name}@${version}`) } From 8be5a02ffaf74adfbb869b521dc168552353f7cb Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 12 Jul 2026 00:42:28 -0400 Subject: [PATCH 27/29] fix(deps): strip .exe from win32 tool handle base name --- scripts/soak/external-tools.mts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/soak/external-tools.mts b/scripts/soak/external-tools.mts index 07adab0de..898275457 100644 --- a/scripts/soak/external-tools.mts +++ b/scripts/soak/external-tools.mts @@ -240,20 +240,26 @@ export function linkHandle(target: string, name: string): void { // resolves its dist/ siblings from the handle's OWN directory, not the // rack. Forward to the absolute rack target instead — a .cmd for // cmd/pwsh and an extensionless bash shim for Git Bash. Non-.exe - // targets are node entry scripts (registry-tarball tools). + // targets are node entry scripts (registry-tarball tools). The handle + // BASE must not keep a caller-supplied .exe suffix: pwsh resolves + // `pnpm` to `pnpm.exe` first, and a bash text file wearing that name + // is "not a valid application for this OS platform". + const base = path.join(BIN_DIR, name.replace(/\.exe$/, '')) const viaExe = target.endsWith('.exe') - rmSync(`${handle}.cmd`, { force: true }) + rmSync(base, { force: true }) + rmSync(`${base}.cmd`, { force: true }) + rmSync(`${base}.exe`, { force: true }) writeFileSync( - `${handle}.cmd`, + `${base}.cmd`, viaExe ? `@echo off\r\n"${target}" %*\r\n` : `@echo off\r\nnode "${target}" %*\r\n`, ) writeFileSync( - handle, + base, viaExe ? `#!/usr/bin/env bash\nexec "${target}" "$@"\n` : `#!/usr/bin/env bash\nexec node "${target}" "$@"\n`, ) - chmodSync(handle, 0o755) + chmodSync(base, 0o755) return } symlinkSync(target, handle) From 7117e19e246820f4f3b0051e88cb1b8b260e59b1 Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 12 Jul 2026 01:26:38 -0400 Subject: [PATCH 28/29] chore: encode PR home + push fork as a checked remote map --- mise.toml | 6 ++ scripts/soak/remotes.mts | 135 ++++++++++++++++++++++++++++++++++ scripts/soak/remotes.test.mts | 71 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 scripts/soak/remotes.mts create mode 100644 scripts/soak/remotes.test.mts diff --git a/mise.toml b/mise.toml index 3ce32f169..86b334cec 100644 --- a/mise.toml +++ b/mise.toml @@ -42,6 +42,12 @@ run = "node scripts/soak/update-deps.mts" [tasks."tools:check"] run = "node scripts/soak/external-tools.mts --check" +[tasks."remotes:check"] +run = "node scripts/soak/remotes.mts --check" + +[tasks."remotes:fix"] +run = "node scripts/soak/remotes.mts --fix" + [tasks."tools:install"] run = "node scripts/soak/external-tools.mts --install-all --shims" diff --git a/scripts/soak/remotes.mts b/scripts/soak/remotes.mts new file mode 100644 index 000000000..7afa4f6fb --- /dev/null +++ b/scripts/soak/remotes.mts @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/** + * @file 1 remote map, 1 reference — where this repo's PRs live and where + * branches push, declared in code instead of rediscovered. The two homes + * differ here (PRs open on the upstream, branches push to a fork), and + * tooling that guesses wrong burns API round-trips proving 404s — worse + * when gh auth is unavailable and the only fallback is the anonymous API, + * which answers only on the PR's BASE repo (and needs the full 40-char + * SHA for check-runs). + * + * Usage: node scripts/soak/remotes.mts [--check|--fix|--print] + * --check verifies the clone's git remotes match the declaration + * (skipped in CI — runner checkouts legitimately differ), --fix adds or + * repoints the `fork` remote (origin drift is report-only: repointing + * origin is a human decision), --print emits the map as JSON for + * tooling. + */ + +import { spawnSync } from 'node:child_process' +import process from 'node:process' +import { realpathSync } from 'node:fs' +import { pathToFileURL } from 'node:url' + +import { REPO_ROOT } from './paths.mts' + +// PRs open on `origin` (the upstream); work branches push to `fork`. +// Protocol is irrelevant — only owner/repo is law — but --fix writes SSH +// because it keeps working when gh's stored https credentials get wiped. +export const PR_HOME = 'jdx/aube' +export const PUSH_HOME = 'jdalton/aube' +export const EXPECTED_REMOTES: Record = { + origin: PR_HOME, + fork: PUSH_HOME, +} + +/** + * Reduce a GitHub remote URL to its `owner/repo` identity: https, ssh + * (scp-like and ssh://), with or without `.git`. Null for anything that is + * not github.com — a non-GitHub URL on a declared remote is drift, not a + * parse error. + */ +export function normalizeGitHubRepo(url: string): string | null { + const m = + /^(?:https:\/\/|ssh:\/\/git@|git@)github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?\/?$/.exec(url.trim()) + return m ? m[1]! : null +} + +/** + * Parse `git remote -v` output into name -> fetch URL. + */ +export function parseRemotes(gitRemoteOutput: string): Record { + const remotes: Record = {} + for (const line of gitRemoteOutput.split('\n')) { + const m = /^(\S+)\t(\S+)\s+\(fetch\)$/.exec(line) + if (m) { + remotes[m[1]!] = m[2]! + } + } + return remotes +} + +export interface RemoteDrift { + name: string + expected: string + actual: string | null +} + +/** + * Every declared remote must exist and resolve to its declared owner/repo + * (case-insensitively — GitHub is). Undeclared extra remotes are fine. + */ +export function checkRemotes(remotes: Record): RemoteDrift[] { + const drift: RemoteDrift[] = [] + for (const [name, expected] of Object.entries(EXPECTED_REMOTES)) { + const url = remotes[name] + const actual = url === undefined ? null : normalizeGitHubRepo(url) + if (actual?.toLowerCase() !== expected.toLowerCase()) { + drift.push({ name, expected, actual }) + } + } + return drift +} + +function readClone(): Record { + const res = spawnSync('git', ['remote', '-v'], { cwd: REPO_ROOT, encoding: 'utf8' }) + if (res.status !== 0) { + console.error(`[remotes] git remote -v failed: ${res.stderr?.trim() || res.error?.message}`) + process.exit(1) + } + return parseRemotes(res.stdout) +} + +function main(argv: string[] = process.argv.slice(2)): number { + if (argv.includes('--print')) { + console.log(JSON.stringify({ prHome: PR_HOME, pushHome: PUSH_HOME, pushRemote: 'fork' })) + return 0 + } + const fix = argv.includes('--fix') + if (!fix && process.env.CI) { + console.log('[remotes] CI checkout — remote-map law is a dev-machine guard, skipping') + return 0 + } + const drift = checkRemotes(readClone()) + if (!drift.length) { + console.log(`[remotes] ok — PRs: ${PR_HOME} (origin), pushes: ${PUSH_HOME} (fork)`) + return 0 + } + let unfixed = 0 + for (const d of drift) { + if (fix && d.name === 'fork') { + const url = `git@github.com:${EXPECTED_REMOTES['fork']}.git` + const args = d.actual === null && !readClone()['fork'] + ? ['remote', 'add', 'fork', url] + : ['remote', 'set-url', 'fork', url] + const res = spawnSync('git', args, { cwd: REPO_ROOT, stdio: 'inherit' }) + if (res.status === 0) { + console.log(`[remotes] fixed: fork -> ${url}`) + continue + } + } + unfixed += 1 + console.error( + `[remotes] drift: remote '${d.name}' should be github.com/${d.expected}, ` + + `found ${d.actual ?? 'missing/non-github'}` + + (d.name === 'fork' ? ' (run with --fix)' : ' (repoint manually — origin is never auto-rewritten)'), + ) + } + return unfixed ? 1 : 0 +} + +const isMain = + process.argv[1] && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url +if (isMain) { + process.exitCode = main() +} diff --git a/scripts/soak/remotes.test.mts b/scripts/soak/remotes.test.mts new file mode 100644 index 000000000..3aef02dd2 --- /dev/null +++ b/scripts/soak/remotes.test.mts @@ -0,0 +1,71 @@ +/** + * @file Unit tests for the remote-map law's pure core: URL identity + * normalization, `git remote -v` parsing, and drift classification — + * both arms of each. The git/process phases stay behind these functions + * so the suite runs offline on any clone. + */ + +import assert from 'node:assert/strict' +import { test } from 'node:test' + +import { + EXPECTED_REMOTES, + PR_HOME, + PUSH_HOME, + checkRemotes, + normalizeGitHubRepo, + parseRemotes, +} from './remotes.mts' + +test('normalizeGitHubRepo accepts https, scp-ssh, ssh://, with/without .git', () => { + assert.equal(normalizeGitHubRepo('https://github.com/jdx/aube.git'), 'jdx/aube') + assert.equal(normalizeGitHubRepo('https://github.com/jdx/aube'), 'jdx/aube') + assert.equal(normalizeGitHubRepo('git@github.com:jdx/aube.git'), 'jdx/aube') + assert.equal(normalizeGitHubRepo('ssh://git@github.com/jdx/aube'), 'jdx/aube') + assert.equal(normalizeGitHubRepo('https://github.com/jdx/aube/'), 'jdx/aube') +}) + +test('normalizeGitHubRepo rejects non-github and malformed URLs', () => { + assert.equal(normalizeGitHubRepo('https://gitlab.com/jdx/aube.git'), null) + assert.equal(normalizeGitHubRepo('git@github.com:aube.git'), null) + assert.equal(normalizeGitHubRepo('/local/path/aube'), null) +}) + +test('parseRemotes keeps one fetch URL per remote and ignores push lines', () => { + const remotes = parseRemotes( + [ + 'origin\thttps://github.com/jdx/aube.git (fetch)', + 'origin\thttps://github.com/jdx/aube.git (push)', + 'fork\tgit@github.com:jdalton/aube.git (fetch)', + 'fork\tgit@github.com:jdalton/aube.git (push)', + '', + ].join('\n'), + ) + assert.deepEqual(remotes, { + origin: 'https://github.com/jdx/aube.git', + fork: 'git@github.com:jdalton/aube.git', + }) +}) + +test('checkRemotes passes when declared remotes match, any protocol or case', () => { + assert.deepEqual( + checkRemotes({ + origin: `https://github.com/${PR_HOME}.git`, + fork: `git@github.com:${PUSH_HOME.toUpperCase()}.git`, + extra: 'https://github.com/somebody/else.git', + }), + [], + ) +}) + +test('checkRemotes flags a missing fork and a mispointed origin', () => { + const drift = checkRemotes({ origin: 'https://github.com/wrong/home.git' }) + assert.deepEqual(drift, [ + { name: 'origin', expected: PR_HOME, actual: 'wrong/home' }, + { name: 'fork', expected: PUSH_HOME, actual: null }, + ]) +}) + +test('the declared map covers exactly the PR home and the push fork', () => { + assert.deepEqual(EXPECTED_REMOTES, { origin: PR_HOME, fork: PUSH_HOME }) +}) From d7cf256b1c5bb43dea67871532115a627b3a97dd Mon Sep 17 00:00:00 2001 From: jdalton Date: Sun, 12 Jul 2026 01:41:17 -0400 Subject: [PATCH 29/29] =?UTF-8?q?fix(deps):=20close=20the=20renovate=20soa?= =?UTF-8?q?k=20bypass=20=E2=80=94=20explicit=20minimumReleaseAge,=20parity?= =?UTF-8?q?-checked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/soak/SKILL.md | 3 +- .github/renovate.json | 2 ++ .rules | 3 +- scripts/soak/paths.mts | 1 + scripts/soak/soak.mts | 57 ++++++++++++++++++++++++++++++++++++ scripts/soak/soak.test.mts | 24 +++++++++++++++ 6 files changed, 88 insertions(+), 2 deletions(-) diff --git a/.claude/skills/soak/SKILL.md b/.claude/skills/soak/SKILL.md index 1fc973eef..c27ea3533 100644 --- a/.claude/skills/soak/SKILL.md +++ b/.claude/skills/soak/SKILL.md @@ -1,6 +1,6 @@ --- name: soak -description: Manages the repo's supply-chain soak window (SOAK_DAYS) — checks and fixes the derived surfaces, bumps or disables the window, adds dated per-package exclusions, and bumps pinned external tools. Use when a task touches minimumReleaseAge, min-release-age, min-publish-age, the nightly toolchain pin, external-tools.json, or taze cooldowns, or when investigating why a freshly published version won't install. +description: Manages the repo's supply-chain soak window (SOAK_DAYS) — checks and fixes the derived surfaces, bumps or disables the window, adds dated per-package exclusions, and bumps pinned external tools. Use when a task touches minimumReleaseAge, min-release-age, min-publish-age, the nightly toolchain pin, external-tools.json, renovate.json, or taze cooldowns, or when investigating why a freshly published version won't install. --- # The soak window @@ -20,6 +20,7 @@ against it: | `docs/.npmrc` | `min-release-age` | days | | `docs/taze.config.mts` | `maturityPeriod` | imports `SOAK_DAYS` | | `external-tools.json` | `soakBypass` annotations | days | +| `.github/renovate.json` | `minimumReleaseAge` (explicit — an `extends:` preset doesn't count) | `"N days"` | ## Commands (mise tasks — the scripts live in `scripts/soak/`) diff --git a/.github/renovate.json b/.github/renovate.json index f94204c37..112bad3a4 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,6 +1,8 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["local>jdx/renovate-config"], + "minimumReleaseAge": "7 days", + "internalChecksFilter": "strict", "ignorePaths": ["fixtures/**"], "packageRules": [ { diff --git a/.rules b/.rules index 2d210d0e5..ca9349bce 100644 --- a/.rules +++ b/.rules @@ -242,12 +242,13 @@ deps: soak: window: SOAK_DAYS=7 in scripts/soak/constants.mts — the ONE source; every surface derives or is parity-checked against it - surfaces[5]: + surfaces[6]: - .cargo/config.toml global-min-publish-age ("7 days") - rust-toolchain.toml dated nightly (channel date vs the machine-read `# adopted:` line — soak.mts enforces adopted >= channel + SOAK_DAYS) - docs/pnpm-workspace.yaml minimumReleaseAge (10080 minutes; also the taze catalog: home — aube reads this file for docs installs, and a ROOT pnpm-workspace.yaml would re-anchor the docs install at repo root, so it must stay in docs/) - docs/.npmrc min-release-age (7 days, npm >= 11.17 parity) - docs/taze.config.mts maturityPeriod (imports SOAK_DAYS) + - .github/renovate.json minimumReleaseAge ("7 days", explicit — extends: presets don't count; renovate PRs commit updated lockfiles and cargo min-publish-age skips already-locked versions, so renovate itself is the only place this dep path can be soaked) manager: .claude/skills/soak/SKILL.md is the how-to; mise run soak (check, CI-gated in test-linux) / soak:fix (syncs windows, prunes expired excludes) / deps:update (taze + cargo update, both soaked) excludes: version-pinned minimumReleaseAgeExclude entries REQUIRE `# published: YYYY-MM-DD | removable: YYYY-MM-DD` on the line above (removable = published + 7d); bare names/globs are standing trust and need no annotation; expired pins are pruned by soak:fix socket_firewall: CI installs sfw (free tier keyless; enterprise auto-selected when the SOCKET_SECURITY_KEY repo secret lands — the one env var every Socket product reads, no aliasing) and shims npm/yarn/pnpm/pip/pip3/uv/cargo via scripts/soak/external-tools.mts --shims; the buildkite agent image pre-bakes the same binary + shims diff --git a/scripts/soak/paths.mts b/scripts/soak/paths.mts index 70e63f132..823737aa5 100644 --- a/scripts/soak/paths.mts +++ b/scripts/soak/paths.mts @@ -23,6 +23,7 @@ export const SURFACES = { workspaceYaml: 'docs/pnpm-workspace.yaml', tazeConfig: 'docs/taze.config.mts', toolchainToml: 'rust-toolchain.toml', + renovateJson: '.github/renovate.json', } // The directory holding the npm package the soak governs (taze runs here, diff --git a/scripts/soak/soak.mts b/scripts/soak/soak.mts index 4bd3773e6..aa1a4c229 100644 --- a/scripts/soak/soak.mts +++ b/scripts/soak/soak.mts @@ -10,6 +10,7 @@ * - `docs/pnpm-workspace.yaml` `minimumReleaseAge` (minutes) + annotated excludes * - `docs/.npmrc` `min-release-age` (days) * - `docs/taze.config.mts` imports SOAK_DAYS (existence + import check) + * - `.github/renovate.json` `minimumReleaseAge` ("N days", explicit — not preset-inherited) * * `--check` (default) fails loud with What / Saw / Wanted / Fix on drift. * `--fix` rewrites window values in place and prunes excludes whose @@ -324,6 +325,61 @@ export function checkTazeConfig(body: string, file: string): Finding[] { return out } +/** + * Renovate must carry the window EXPLICITLY in this repo's renovate.json. + * Renovate bumps manifests + lockfiles server-side, and cargo's + * min-publish-age skips already-locked versions — so a Renovate PR is the + * one dependency path none of the local soak surfaces can stop. An + * inherited preset value (extends:) doesn't count: presets change without + * a commit here, which is exactly the silent drift this gate exists to + * catch. + */ +export function checkRenovateConfig(body: string, file: string): Finding[] { + let config: Record + try { + config = JSON.parse(body) + } catch { + return [ + { + file, + what: 'renovate config parse', + saw: '(invalid JSON)', + wanted: 'parseable JSON carrying the soak window', + fix: 'repair the JSON, then set minimumReleaseAge (or run --fix)', + }, + ] + } + const wanted = `${SOAK_DAYS} days` + const saw = config['minimumReleaseAge'] + if (SOAK_DAYS === 0 ? saw === undefined : saw === wanted) { + return [] + } + return [ + { + file, + what: 'renovate minimumReleaseAge window', + saw: saw === undefined ? '(missing — an extends: preset does not count)' : String(saw), + wanted: SOAK_DAYS === 0 ? '(absent — soak disabled)' : wanted, + fix: `set "minimumReleaseAge": "${wanted}" at the top level (or run --fix)`, + }, + ] +} + +export function fixRenovateConfig(body: string): string { + let config: Record + try { + config = JSON.parse(body) + } catch { + return body + } + if (SOAK_DAYS === 0) { + delete config['minimumReleaseAge'] + } else { + config['minimumReleaseAge'] = `${SOAK_DAYS} days` + } + return `${JSON.stringify(config, null, 2)}\n` +} + export function fixCargoConfig(body: string): string { return body.replace( /^(global-min-publish-age\s*=\s*)"[^"]*"/m, @@ -388,6 +444,7 @@ export function main(argv: string[] = process.argv.slice(2)): number { { rel: SURFACES.workspaceYaml, check: checkWorkspaceYaml, fixer: fixWorkspaceYaml }, { rel: SURFACES.tazeConfig, check: checkTazeConfig }, { rel: SURFACES.toolchainToml, check: checkToolchainSoak }, + { rel: SURFACES.renovateJson, check: checkRenovateConfig, fixer: fixRenovateConfig }, ] for (const s of surfaces) { diff --git a/scripts/soak/soak.test.mts b/scripts/soak/soak.test.mts index c8dec61bb..d80688823 100644 --- a/scripts/soak/soak.test.mts +++ b/scripts/soak/soak.test.mts @@ -9,11 +9,13 @@ import { checkCatalogParity, checkExcludeAnnotations, checkNpmrc, + checkRenovateConfig, checkTazeConfig, checkToolchainSoak, checkWorkspaceYaml, fixCargoConfig, fixNpmrc, + fixRenovateConfig, fixWorkspaceYaml, main, parseExcludeEntries, @@ -153,6 +155,28 @@ test('fix rewrites a drifted cargo window and leaves a clean one alone', () => { assert.equal(fixCargoConfig(fixed), fixed) }) +test('renovate: window must be explicit in-repo; preset inheritance is drift', () => { + const good = `{ "extends": ["some>preset"], "minimumReleaseAge": "${SOAK_DAYS} days" }` + assert.equal(checkRenovateConfig(good, 'r').length, 0) + // Missing key = inherited-at-best: the preset can change without a + // commit here, so the gate demands the explicit value. + assert.equal(checkRenovateConfig('{ "extends": ["some>preset"] }', 'r').length, 1) + assert.equal(checkRenovateConfig('{ "minimumReleaseAge": "3 days" }', 'r').length, 1) + assert.equal(checkRenovateConfig('{ "minimumReleaseAge": 7 }', 'r').length, 1) + assert.equal(checkRenovateConfig('not json', 'r').length, 1) +}) + +test('renovate fix sets the window, preserves other keys, and is idempotent', () => { + const fixed = fixRenovateConfig('{\n "labels": ["dependencies"],\n "minimumReleaseAge": "3 days"\n}\n') + const parsed = JSON.parse(fixed) + assert.equal(parsed.minimumReleaseAge, `${SOAK_DAYS} days`) + assert.deepEqual(parsed.labels, ['dependencies']) + assert.equal(fixRenovateConfig(fixed), fixed) + assert.equal(JSON.parse(fixRenovateConfig('{}')).minimumReleaseAge, `${SOAK_DAYS} days`) + // Unparseable input is left for a human — never rewritten blind. + assert.equal(fixRenovateConfig('not json'), 'not json') +}) + // Glue: the tracked surfaces of THIS repo must satisfy the gate — the same // check CI runs, exercised in-process so main() itself stays covered. test('main --check passes against the tracked repo surfaces', () => {