Skip to content

build: 7-day soak across toolchain and deps, cold_path() hints, pinned tooling - #442

Merged
colinhacks merged 34 commits into
nubjs:mainfrom
jdalton:build/rust-195-cold-path
Jul 24, 2026
Merged

build: 7-day soak across toolchain and deps, cold_path() hints, pinned tooling#442
colinhacks merged 34 commits into
nubjs:mainfrom
jdalton:build/rust-195-cold-path

Conversation

@jdalton

@jdalton jdalton commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Gives nub one supply-chain safety rule — a 7-day "soak" — applied everywhere a dependency or tool can enter the repo: when a new version of anything is published, we wait 7 days before adopting it, so a compromised release gets caught by the ecosystem before we ever install it. Also brings workflow-security hardening (zizmor, 267 findings to zero), pinned-by-checksum external tooling, Socket Firewall in CI, and core::hint::cold_path() branch hints in the vendored install engine.

The soak, explained

"Soak time" = the minimum age a release must reach before we install it. Ours is 7 days, defined once in scripts/soak/constants.mts (SOAK_DAYS = 7); every config file below derives from or is checked against it:

Surface File What it controls
Rust toolchain rust-toolchain.toml Builds use a dated nightly that was ≥ 7 days old at adoption
Rust crates .cargo/config.toml cargo's -Zmin-publish-age skips crate versions younger than 7 days
npm deps tools/pnpm-workspace.yaml minimumReleaseAge: 10080 (same rule, in minutes)
npm CLI parity .npmrc min-release-age=7 for npm ≥ 11.17
Version bumps tools/taze.config.mts taze only proposes versions ≥ 7 days old
Renovate PRs .github/renovate.json minimumReleaseAge: "7 days", explicit — Renovate commits updated lockfiles, and -Zmin-publish-age skips already-locked versions, so Renovate itself is the only place this path can be soaked

scripts/soak/soak.mts --check fails CI if any of those drift; --fix rewrites them. The pnpm soak surfaces live in tools/ (not the repo root): a root pnpm-workspace.yaml marks the repo root as a workspace root, and nub's own workspace detection then redirects the node-version test matrix through the root engines — the exact false-green the matrix exists to catch. The root stays npm-only; a tracked fixture anchor plus a harness guard keep the matrix honest as defense-in-depth. Skipping the soak for one package requires a dated # published: YYYY-MM-DD | removable: YYYY-MM-DD comment above the exclude pin (removable = published + 7d), and the gate fails once the window passes — soaked pins must come off. taze itself is exact-pinned in package.json (npm can't parse the catalog: protocol and wpt-worker runs a root npm ci); the workspace catalog entry is the reference, and the gate fails if the two versions drift.

A repo skill (.claude/skills/soak/SKILL.md) documents the procedures — change the window, opt out, add an exclusion, bump the nightly — so agent sessions don't re-derive them; it follows Anthropic's skill-authoring + prompting guidance and links it for future edits.

Workflow security: 267 zizmor findings to zero

zizmor (a GitHub Actions security linter) is now a CI gate (zizmor.yml), so all 21 existing workflows were brought to zero findings first:

  • persist-credentials: false on every checkout that doesn't push (the homebrew-tap bump keeps its credentialed checkout, with an inline ignore explaining why).
  • Explicit permissions: blocks — workflow-level {} plus per-job scopes matching only what each job's steps actually do.
  • Every template-injection site (28 in release.yml alone) moved to env-var indirection: ${{ matrix.foo }} inside a shell script becomes a step-level env: var referenced as "$FOO", so a crafted value can't execute as shell.
  • Deliberate residuals carry inline # zizmor: ignore[rule] reason comments (e.g. caches on gate-only jobs that never feed published artifacts).

No behavior changed — each job kept every permission its steps use.

Pinned tools, Socket Firewall, and the docker context

external-tools.json pins pnpm 11.8.0, npm 12.0.0, sfw, zizmor, agentshield, and skillspector by exact version + sha512 SRI checksum; scripts/soak/external-tools.mts is the only install path (download → verify → rack → PATH symlink). npm-packaged tools get their deps installed by nub itself first (dogfooding), prod-only with scripts skipped.

CI installs the pinned pnpm + npm, then writes firewall shims: bash wrappers named npm/pnpm/cargo/… that route the real command through sfw, wrapping the pinned binaries directly. SOCKET_SECURITY_KEY is the only credential env var — not set yet, which is fine: sfw's free tier is keyless, and the enterprise tier engages automatically when the secret lands.

untracked 1.6.4 (pinned via the same catalog rule) maintains a generated section in .dockerignore to keep docker build contexts small; a package.json whitelist rescues the load-bearing .cargo configs (crates/nub-native/.cargo routes in-container build output to the shared target dir — the docker-smoke builders depend on it).

Toolchain + cold-path hints

rust-toolchain.toml pins nightly-2026-07-04 (newest nightly ≥ 7 days old at adoption; nightly is what activates -Zmin-publish-age), with rust-version moving 1.88 → 1.95 as the stable floor. core::hint::cold_path() (stable since 1.95) marks rare branches in the vendored install engine's hot loops — the per-file link fallbacks, lockfile-parser bails, semver cache misses, and tar validation errors — so the optimizer keeps the common path tight. Hints only; no behavior change.

Testing + review trail
  • 63 node:test unit tests cover the soak checkers, fixers, parsers, and CLIs (pnpm run test:scripts, also in the CI gate). Review findings live on as regression cases (impossible calendar dates, expired bypasses, flow-style exclude lists, catalog drift, the --npm --cargo selector bug), and the security-relevant paths are pinned by tests: the downloader sends GITHUB_TOKEN to github.com only and rejects any checksum mismatch, the sfw flavor flips on SOCKET_SECURITY_KEY, and both CLIs round-trip end-to-end through their entrypoints.
  • Full cargo test green. The two registry-mock test failures first blamed on main turned out to be the Socket Firewall shim MITM-proxying cargo's local-registry mocks; the Test job now sets SFW_SHIM_ACTIVE_CARGO=1 so the shim execs the real cargo for the mock-registry suites while staying active for everything else.
  • Every review-bot finding was fixed, replied to, and resolved — including the standout: writing a shim through its symlink handle overwrote the pinned binary with the shim script, which then executed itself forever. The shim writer now removes the handle first, and a CI step asserts pnpm/npm resolve through the shims on every run.
  • Static skill scanning flagged two real rough edges in skill docs, both hardened (an rsync --delete that could empty a vendor tree on a failed staging step; a cache-wipe recipe that now fails loudly on an empty path substitution).
Hardening rounds after opening (CI + review driven)
  • Windows CI, four quirks deep: tar parses C:\… as a remote host (extract cwd-relative); Git Bash's GNU tar shadows System32 bsdtar and can't read zip (invoke %SystemRoot%\System32\tar.exe); a copied handle strands pnpm's SEA launcher from its dist/ siblings (handles now forward to the absolute rack target via .cmd + bash shims); and the forwarder must strip .exe from the handle base — pwsh resolves pnpm to pnpm.exe first, and a bash script wearing that name is "not a valid application for this OS platform".
  • zizmor follow-ups: scan scoped to .github/ (the vendored aube workflows are inert); 24 dtolnay/rust-toolchain sites repinned — the old SHA was genuine but left dangling by an upstream force-push (impostor-commit finding); docker action pins carry exact version comments (ref-version-mismatch).
  • commit-msg hook: a C-style comment in an awk END block broke BSD awk on every commit, silently skipping the blank-line collapse while the visible half (agent-trailer strip) kept working. Fixed, and the hook's behavior is now pinned by tests that assert clean stderr on every case.
  • Remote map as code: scripts/soak/remotes.mts declares where PRs live vs where branches push (--check/--fix/--print), so tooling reads it instead of rediscovering it.

jdalton added 2 commits July 11, 2026 13:20
Exact stable pin via rust-toolchain.toml so all four workspaces (nub,
nub-native, nub-phantom, vendor/aube) build with one toolchain. 1.95
is the floor for core::hint::cold_path().
core::hint::cold_path() (stable since rust 1.95) annotates the rare
arm so the optimizer sinks it off the hot path. Sites: the per-file
materialize loops' reflink/hardlink fallbacks and link-error returns
(project-local and GVS staging), the pnpm-lock byte-cursor subset
parser's decline-to-serde bails, the thread-local semver range and
version cache misses, and the per-entry tar walk's validation
rejects. No behavior change; hints only.
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Ready Ready Preview, Comment Jul 24, 2026 7:11pm

Request Review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — two minor observations, both non-blocking.

Reviewed changes — pins an exact stable toolchain (rust-toolchain.toml, 1.95.0), bumps every workspace rust-version to 1.95, and adds core::hint::cold_path() codegen hints to rare arms of hot install loops in the vendored aube engine. Hints only, no behavior change.

  • Pin the toolchain to 1.95.0 — new repo-root rust-toolchain.toml fixes channel = "1.95.0" plus rustfmt/clippy so all four workspaces build on one compiler.
  • Bump rust-version to 1.95Cargo.toml, crates/nub-native/Cargo.toml, crates/nub-phantom/Cargo.toml (1.88 → 1.95) and vendor/aube/Cargo.toml (1.93 → 1.95). 1.95.0 is the floor for cold_path().
  • Mark cold branches with cold_path() — link fallbacks in materialize.rs, decline-to-serde/indent-mismatch bails in the pnpm subset parser (subset.rs), semver cache misses in semver_util.rs, and tar-walk validation rejects in tarball.rs.

I verified the load-bearing claim: core::hint::cold_path() is stable since 1.95.0 (const fn), and 1.95.0 is a real stable release (16 Apr 2026). The placements are sound — the cache-miss arms in semver_util.rs are documented as trending to a ~1.0 hit rate after the first BFS layer, so marking the miss path cold is justified.

ℹ️ Stale MSRV in the vendored aube docs

Bumping vendor/aube/Cargo.toml from rust-version = "1.93" to "1.95" leaves the vendored aube's own agent docs claiming MSRV 1.93. Those docs aren't in this diff, but this change is what makes them stale — and the aube-parity doctrine keeps the vendored tree self-consistent so it stays conceivably upstreamable, so the drift is worth closing in the same PR.

Technical details
# Stale MSRV in vendored aube docs after rust-version bump

## Affected sites
- `vendor/aube/.rules:6``msrv: 1.93` (mirrored by `vendor/aube/AGENTS.md`)
- `vendor/aube/.rules:190``edition 2024, latest stable toolchain, msrv 1.93 (...)`

## Required outcome
- The vendored aube MSRV documentation reflects 1.95, matching `vendor/aube/Cargo.toml`'s `rust-version`.

## Open questions for the human
- Whether to update the aube-side docs here or leave them for a dedicated vendor-sync commit. Either is defensible; the risk of leaving them is a future upstream diff carrying a contradictory MSRV.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread rust-toolchain.toml Outdated
Adopt the newest nightly at least 7 days old as of 2026-07-11; 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 across nub and vendor/aube.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The repo-root pin now propagates a nightly toolchain into the release build, so shipped binaries compile on nightly. Worth an explicit decision before merging.

Reviewed changes — since the prior pullfrog review, the single incremental change swaps the repo-root toolchain pin from stable 1.95.0 to nightly-2026-07-04 (with a 7-day-soak adoption comment). The rust-version bumps to 1.95 and the cold_path() hints are unchanged from the prior review.

  • Switch the toolchain pin to nightlyrust-toolchain.toml now pins channel = "nightly-2026-07-04" instead of stable 1.95.0; the comment documents a "newest nightly ≥7 days old" adoption/bump policy.

⚠️ Shipped release binaries now build on nightly

Every CI workflow installs dtolnay/rust-toolchain@... # stable, but the repo-root rust-toolchain.toml overrides that for all cargo invocations. The per-platform release build runs at repo root, so the 8-platform binaries published to npm now compile on nightly-2026-07-04 rather than stable. Nightly carries weaker stability and soundness guarantees than stable; making it the compiler for distributed artifacts is a posture change worth an explicit, conscious decision rather than a side effect of the dev-toolchain pin.

Technical details
# Release binaries compile on the pinned nightly

## Affected sites
- `rust-toolchain.toml:8``channel = "nightly-2026-07-04"` at repo root applies to all cargo invocations.
- `.github/workflows/release.yml:466-471``cargo build --release -p nub-cli --features embed-runtime --target <t>` / `cross build --release ...` build the shipped binaries at repo root, so the pin applies. `cross` reads `rust-toolchain.toml` and provisions the pinned toolchain in-container, so the container path is not an escape.
- `.github/workflows/release.yml:266` and every other workflow — `dtolnay/rust-toolchain@... # stable` installs stable but the pin supersedes it for cargo.

## Required outcome
- A deliberate decision on whether production release binaries should build on a pinned nightly, and if so, that intent recorded where the release toolchain is chosen (not only in the dev-tooling comment).

## Open questions for the human
- Is nightly intended to apply to the release build, or only to local/CI dev builds? If the latter, the release job could pin an explicit stable toolchain (e.g. a scoped `+stable` / `RUSTUP_TOOLCHAIN`), decoupling ship-toolchain from dev-toolchain.

ℹ️ rust-version = 1.95 is no longer verified by any build

Nothing in the tree requires nightly to compile — the stated justification (core::hint::cold_path()) is stable since 1.95.0, and there are no #![feature(...)] gates anywhere. With the toolchain pinned to nightly and rust-version declaring a stable-1.95 floor, no build actually exercises stable 1.95, so a nightly-only construct or a stable-1.95 regression would pass CI undetected. The declared MSRV becomes aspirational rather than enforced.

Technical details
# MSRV floor is decoupled from the build toolchain

## Affected sites
- `Cargo.toml:95`, `crates/nub-native/Cargo.toml:34`, `crates/nub-phantom/Cargo.toml:22`, `vendor/aube/Cargo.toml:23``rust-version = "1.95"`.
- `rust-toolchain.toml:8` — build toolchain is `nightly-2026-07-04`, not stable 1.95.

## Required outcome
- Either an MSRV-verification signal (a job that builds on stable 1.95 / `rust-version`), or an explicit acknowledgement that `rust-version` is a documentation-only floor while the build tracks nightly.

## Open questions for the human
- Is a nightly toolchain actually needed, given `cold_path()` is stable on 1.95? If not, pinning stable 1.95 keeps ship-toolchain, dev-toolchain, and the declared MSRV consistent — which was the state at the prior review.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

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. Unignore the root
.cargo/config.toml the same way as the nub-native one.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the incremental change is correct; one observation ties it to the still-open nightly-posture discussion.

Reviewed changes — since the prior pullfrog review, the single incremental change adds a repo-root .cargo/config.toml enabling cargo's min-publish-age unstable feature and the .gitignore allow-entries needed to track it. The nightly pin, rust-version bumps, and cold_path() hints are unchanged from the prior review.

  • Enable a min-publish-age supply-chain soak — new .cargo/config.toml sets [unstable] min-publish-age = true and [registry] global-min-publish-age = "7 days", so cargo's resolver skips crate versions published in the last 7 days unless already in Cargo.lock, mirroring the PM's minimumReleaseAge discipline.
  • Track the root .cargo/ past the broad ignore.gitignore adds !.cargo/ and !.cargo/config.toml, matching the existing crates/nub-native/.cargo/ allow-entry so the broad .* rule doesn't drop the config.

I verified the config against the Cargo Book's unstable-features page: [unstable] min-publish-age = true is the correct enabling key, global-min-publish-age accepts the "N days" format, and resolver.incompatible-publish-age defaults to "deny" (ignore too-new versions unless already locked) — so the comment's claims are accurate and the file is well-formed.

ℹ️ The soak config makes the nightly pin load-bearing, reinforcing the open nightly-posture question

min-publish-age is a nightly-only cargo feature — the [unstable] table is silently ignored on stable cargo. Before this commit, the prior review noted nothing in the tree actually required nightly (cold_path() is stable on 1.95). This commit changes that: the build config now genuinely depends on nightly, so reverting to stable 1.95 would silently no-op the soak rather than fail. This doesn't introduce a bug — it strengthens the premise of the two still-open findings from the prior review (shipped binaries building on nightly, and rust-version = 1.95 not being verified by any build), which remain human decisions.

Technical details
# min-publish-age couples the supply-chain soak to the nightly pin

## Affected sites
- `.cargo/config.toml:7-8``[unstable] min-publish-age = true` is nightly-only; ignored by stable cargo.
- `rust-toolchain.toml:8``channel = "nightly-2026-07-04"` is what makes the `[unstable]` key active.

## Required outcome
- No code change required — the config is correct. This is a note that the nightly pin is now load-bearing for the soak, not only a policy convenience for the (stable) `cold_path()` hints.

## Open questions for the human
- The prior review's open questions still stand and are now coupled: if release binaries were to pin stable to decouple ship-toolchain from dev-toolchain, the `min-publish-age` soak would silently stop applying to that build. Worth deciding whether the soak should hold on the release path specifically.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

@jdalton

jdalton commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

On the nightly-posture question: deliberate. The dated-nightly pin (7-day soak on toolchain adoption) is intentionally load-bearing for the -Zmin-publish-age soak; decoupling release builds to stable would silently drop the soak there, so the release path keeps the pinned nightly. rust-version=1.95 stays the declared stable floor. Tracking: rust-lang/cargo#15973 (now rust-lang/cargo#17009) — revisit the pin when min-publish-age stabilizes.

jdalton added 2 commits July 11, 2026 14:30
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, pnpm-workspace.yaml minimumReleaseAge in minutes,
.npmrc min-release-age in days, taze maturityPeriod via direct
import) and enforces lockstep between catalog entries and
catalog-shadowed exact pins. 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 external security
tools exact with sha512 SRI; external-tools.mts is the only install
path; paths.mts is the single declaration of every location the
scripts touch. taze is exact-pinned in package.json (npm cannot parse
the catalog: protocol and wpt-worker runs a root npm ci) with the
catalog entry as the enforced reference.
…d zizmor

docs-links runs soak.mts --check and external-tools.mts --check; the
test job installs sfw and its package-manager shims before pnpm
install. 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. zizmor.yml lints workflow files on PRs.
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.
@jdalton jdalton changed the title build: pin rust 1.95.0 toolchain and adopt cold_path() branch hints build: 7-day soak across toolchain and deps, cold_path() hints, pinned tooling Jul 11, 2026
A hung release or registry response would stall CI indefinitely;
abort after 120s (generous for the largest pinned binary on a slow
runner).
- 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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The socket firewall doesn't actually take effect on the Windows CI leg, so pnpm install there runs unfirewalled without any signal. Worth addressing before merge.

Reviewed changes — since the prior pullfrog review (1012a75), this run covers the two new commits that add the supply-chain soak machinery and route CI installs through a firewall. The nightly pin, rust-version bumps, .cargo/config.toml, and cold_path() hints are unchanged.

  • Add the scripts/soak/ cooldown manager — one canonical SOAK_DAYS in constants.mts; soak.mts parity-gates .cargo/config.toml / .npmrc / pnpm-workspace.yaml / taze.config.mts against it, enforces annotated-exclude expiry, and checks catalog↔package.json lockstep.
  • Add pinned external security toolingexternal-tools.json pins sfw, zizmor, agentshield, skillspector to exact versions with per-platform sha512; external-tools.mts downloads, SRI-verifies, installs, and writes sfw firewall shims for npm/yarn/pnpm/pip/uv/cargo.
  • Wire the firewall + gates into CIci.yml installs the sfw shims and prepends the rack bin to $GITHUB_PATH before pnpm install, and feeds SOCKET_API_KEY/SOCKET_API_TOKEN from a repo secret; the docs job runs the soak + tool-pin --check gates.
  • Add the zizmor workflow-security lint — new zizmor.yml with permissions: {} at workflow scope and persist-credentials: false.
  • Add taze as a soaked dependency updater — devDep + catalog entry; update-deps.mts runs taze then pnpm install, and cargo update only via the rustup shim so the min-publish-age soak applies.

The prior review's inline thread on the # stable CI / nightly / MSRV interaction was addressed by the author's "Deliberate" reply and resolved, so those posture questions are settled and not re-raised here.

ℹ️ Nitpicks

  • scripts/soak/update-deps.mts:64-73 — passing both --npm and --cargo makes !onlyCargo and !onlyNpm both false, so neither ecosystem updates and the script exits 0. Reject the combination or treat the flags as additive selectors.
  • .github/workflows/ci.yml:547-549SOCKET_API_KEY/SOCKET_API_TOKEN are set at job scope, so the Socket secret is ambient to cargo build/cargo test (which run third-party build scripts) and the side-Node curl steps too. Scoping to just the shim + install steps would cut the blast radius.
  • scripts/soak/external-tools.mts:154-161,203-210 — the tar -xzf extraction has no path-traversal/entry-type guard, unlike this repo's own vendor/aube/crates/aube-store/src/tarball.rs. Defense-in-depth only (requires a prior SRI/pin compromise), but the asymmetry is worth a note.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread .github/workflows/ci.yml Outdated
Comment thread scripts/soak/constants.mts
Comment thread scripts/soak/external-tools.mts
Comment thread scripts/soak/external-tools.mts
untracked 1.6.4 (catalog-shadowed pin, lockstep-enforced) maintains a
generated exclusion section in .dockerignore between markers
(pnpm run dockerignore:update), shrinking the docker build context
for the from-source images; the package.json whitelist rescues the
load-bearing .cargo configs (crates/nub-native/.cargo routes
in-container build output to the shared target dir). external-tools
--check gains a docker-prebake parity gate (null surface here — nub's
docker images are product artifacts, not dev-tool prebakes).

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the fix commits. Two prior findings remain open (both latent).

Reviewed changes — this run covers the three fix commits pushed in response to the prior review, which harden the soak gates and the external-tool installer. The delta introduces no new issues.

  • Skip the sfw firewall cleanly on WindowsinstallTool and writeShims short-circuit on win32 with a log line, so the POSIX-only shim mechanism is an explicit, visible gap rather than a silent fail-open on the Windows CI leg.
  • Scope the GITHUB_TOKEN to GitHub hostsdownload() attaches the authorization header only when the URL host is github.com, so the token is no longer sent to the npm registry on the purl path.
  • Make update-deps flags additive and the dry run actually dry — passing both --npm and --cargo now runs both, and --dry-run overrides the taze config's write: true with --no-write --no-install.
  • Harden the installer — download timeout (AbortSignal.timeout), .zip extraction plus a missing-binary check, dangling-symlink handling, and a manifest-based completion marker so an interrupted npm extract self-heals.
  • Tighten the soak parser — reject flow-style minimumReleaseAgeExclude: [...] (invisible to the block parser), tolerate trailing comments on exclude entries, handle blank lines inside the catalog: block, and make isMain detection use realpath + pathToFileURL.

The two ⚠️/ℹ️ threads the fix commits addressed (Windows firewall skip, GITHUB_TOKEN host-gating) are resolved. Two informational threads remain open — addDaysIso throwing on a shape-valid-but-invalid date, and checkPins not flagging an expired soakBypass. Both are latent (no current data exercises either), so they don't block merge, but they're left open since the code doesn't yet address them.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

jdalton added 2 commits July 11, 2026 15:20
267 findings to zero at the default persona: persist-credentials:
false on every checkout that doesn't push (the homebrew-tap bump
keeps its credentialed checkout, inline-ignored with the reason);
workflow-level permissions: {} with per-job scopes matched to what
each job's steps actually use; all template-injection sites moved to
env-var indirection (28 in release.yml alone, zero behavior change);
adhoc package installs documented as already env-pinned; deliberate
residuals carry inline zizmor ignores with per-site verified reasons
(cache-poisoning on gate-only jobs, secrets-inherit where docker.yml
declares no workflow_call secrets).
Same mechanism as the sibling repos: SRI-pinned pnpm/npm in
external-tools.json (npm 12.0.0 carries a dated soakBypass
annotation), CI installs both before the firewall shims, shims wrap
the pinned binaries directly, and npm-packaged external tools
materialize runtime deps through nub itself first (pnpm/npm
fallbacks), prod-only with scripts skipped. Also hardens two skill
docs flagged by static skill scanning: the vendor-sync rsync
--delete now guards against an empty staging tree, and the bench
cache-wipe recipe fails loudly on an empty cache-path substitution.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new pnpm/npm firewall-shim wiring overwrites the pinned pnpm/npm wrapper it depends on, so pnpm/npm cannot resolve through the shims on the Linux and macOS test legs. One inline finding; worth fixing before merge.

Reviewed changes — since the prior pullfrog review (7e422e3), this run covers the two new commits: workflow-security hardening across all workflows, and pinning pnpm/npm into the tool rack so CI installs run through the pinned binaries + the socket firewall.

  • Harden every workflow against zizmor findings — workflow-level permissions: {}, per-job least-privilege scopes, persist-credentials: false on all checkouts, SHA-pinned actions, and env-var indirection for every ${{ github.* }}/${{ steps.* }} interpolation in run: steps (values now read from a step env: block), with justified inline zizmor: ignore on the gate-only cache and vendored-template cases.
  • Pin pnpm 11.8 + npm 12 via the tool rackexternal-tools.json adds a per-platform pnpm SEA (plus a darwin-x64 registry .tgz fallback) and a single npm registry tarball with a soakBypass; external-tools.mts factors the registry-tarball install into a shared installNpmTarball and materializes tool deps through PM_DEP_INSTALLERS (dogfooding nub install --prod first, then pnpm/npm).
  • Route CI through the pinned PMs — the test job installs pnpm/npm/sfw and writes the firewall shims in one call, then asserts pnpm --version/npm --version resolve.

The zizmor hygiene commit is mechanical and correct — the env-var-indirection rewrites preserve the original interpolated values and the inline ignores are individually justified. The prior two latent threads (addDaysIso on an invalid date, checkPins not flagging an expired soakBypass) are unchanged by these commits and remain open.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread scripts/soak/external-tools.mts
…unit tests

writeShims wrote THROUGH the rack symlink handle, overwriting the
pinned binary with the shim body — which then exec'd itself forever.
Remove the handle before writing. Windows .zip archives resolve
<bin>.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 flagged like any other soaked pin. node:test unit
tests cover the checkers, fixers, and parsers (pnpm run
test:scripts; also in the CI gate).

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found in the incremental change. One prior non-blocking thread remains open.

Reviewed changes — the single new commit 49610a3 adds a dev-machine remote-map guard: a standalone script declaring where this repo's PRs open versus where branches push, plus unit tests. It is not wired into any CI gate or soak surface.

  • Declare the two GitHub homes as law — new scripts/soak/remotes.mts fixes PR_HOME (nubjs/nub, the origin PRs open on) and PUSH_HOME (jdalton/nub, the fork branches push to). --check verifies a clone's remotes match, --fix adds or repoints only the fork remote (origin is report-only, never auto-rewritten), and --print emits the map as JSON.
  • Cover the pure core — new scripts/soak/remotes.test.mts exercises both arms of normalizeGitHubRepo (https / scp-ssh / ssh://, with and without .git, non-github rejected), parseRemotes, and checkRemotes drift classification; remotes:check/remotes:fix package.json scripts drive the CLI.

I verified the load-bearing pieces. The normalizeGitHubRepo regex requires an exact owner/repo pair, so a truncated scp URL (git@github.com:aube.git) correctly returns null, and checkRemotes compares case-insensitively while ignoring undeclared extra remotes. The --check path self-skips under CI (unless --fix), the --fix path touches only the fork remote and writes an SSH URL, and the test suite is hermetic — it imports only node: builtins plus the local module and exercises the pure functions with no git spawn, so the test:scripts glob picks it up safely on the CI docs job.

One prior open thread — the rack.test.mts win32 skip-guard nit — is untouched by this commit and left open. It's a local-Windows-dev-only test failure (CI runs test:scripts on ubuntu-latest), so it doesn't block merge.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the incremental change. One prior non-blocking thread remains open.

Reviewed changes — since the prior pullfrog review (49610a3, whose own commit was already reviewed), the one new commit ff97dbf closes the last dependency path the local soak surfaces couldn't reach: Renovate updates.

  • Gate renovate.json against the soak window — new checkRenovateConfig/fixRenovateConfig in scripts/soak/soak.mts demand an explicit top-level minimumReleaseAge of "7 days" (an extends: preset value doesn't count, since a preset can drift without a commit here); registered as a fixer-backed surface and exported alongside the other checks.
  • Carry the window in renovate.json — the config gains top-level "minimumReleaseAge": "7 days" plus "internalChecksFilter": "strict", so Renovate actually holds a too-fresh PR back rather than only annotating it.
  • Register the surface and document itpaths.mts adds SURFACES.renovateJson, and the soak skill's description + surface table list renovate.json.
  • Cover both functions — two hermetic tests assert preset-inheritance-is-drift and that --fix sets the window, preserves other keys, and is idempotent.

The rationale is sound: Renovate bumps manifests and lockfiles server-side, and cargo's min-publish-age skips already-locked versions, so a Renovate PR would otherwise sidestep every local soak surface. Top-level minimumReleaseAge cascades to all enabledManagers per the Renovate schema, and internalChecksFilter: "strict" is the load-bearing half that makes the age check block rather than advise. Verified locally: node scripts/soak/soak.mts --check exits 0 with the renovate surface included, and all 22 soak.test.mts cases pass, including the two new renovate tests and main --check against the tracked repo state.

The one prior thread — the rack.test.mts:41 win32 skip-guard nit — is untouched by this commit and remains open. It's a local-Windows-dev-only test failure (CI runs test:scripts on ubuntu-latest), so it doesn't block merge.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the incremental change. One prior non-blocking thread remains open.

Reviewed changes — since the prior pullfrog review (ff97dbf), the two new commits fix a portability bug in the commit-msg hook and pin its behavior with tests.

  • Fix the BSD-awk breakage in the commit-msg hook.githooks/commit-msg drops the END { /* ... */ } block whose C-style comment broke BSD awk, silently skipping the trailing-blank collapse. The rewrite relies on trailing blanks never being flushed at EOF, so the collapse-runs-to-one and drop-trailing semantics are preserved without an END block.
  • Pin the hook's behavior — new hermetic scripts/soak/commit-msg-hook.test.mts asserts exit 0 and empty stderr across five cases (agent-trailer strip, human co-author preserved, blank-run collapse plus trailing-drop, subject-only leaves no trailing blank, hook present at the core.hooksPath location). Every case asserts an empty stderr, so a tool erroring mid-pipeline is a failure even when the message still comes out right — which is the incident this pins.

The awk rewrite is behavior-preserving: blank lines accumulate a counter and a run is emitted as at most one blank only before the next non-blank line, so EOF-trailing blanks are dropped as intended (traced against the feat: subject\n\n\n\nbody line\n\n\nfeat: subject\n\nbody line\n case). The tests are contract-named and comprehensive without bloat.

The one prior open thread — the rack.test.mts:41 win32 skip-guard nit — is untouched by these commits and remains open. It's a local-Windows-dev-only test failure (CI runs test:scripts on ubuntu-latest), so it doesn't block merge.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

The post-extract path resolves <bin>.exe for windows archives, but the
pre-download idempotency guard checked only the suffix-less destBin, so a
second --install on windows re-downloaded and re-extracted (and wedges on
a restricted/offline runner). Mirror the same .exe resolution in the
early-exit. Mirrors jdx/aube#1028.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the incremental change. One prior non-blocking thread remains open.

Reviewed changes — the single new commit 003447a makes a repeat Windows tool install a no-op instead of forcing a re-download, closing the last idempotency gap in the pinned-tool installer.

  • Resolve <bin>.exe in the pre-download early-exitinstallAssetTool now checks for a <bin>.exe variant before deciding to download, mirroring the identical resolution the post-extract path already does. On Windows a tool installed as <bin>.exe was invisible to the "already installed" check, so a second --install re-downloaded — which wedges on a restricted/offline runner.

The change is sound and behavior-preserving off Windows. The new block is logically identical to the post-extract block from d947ec8, and passing the .exe-suffixed binName into linkHandle is safe: on win32 linkHandle strips the trailing .exe from the handle base and sets viaExe = true, so the .cmd/bash forwarders dispatch to the .exe directly — exactly matching a fresh install. On non-Windows platforms no <bin>.exe exists, so the block is a no-op. Only fully provable on the windows-latest test leg.

The one prior open thread — the rack.test.mts:41 win32 skip-guard nit — is untouched by this commit and remains open. It's a local-Windows-dev-only test failure (CI runs test:scripts on ubuntu-latest), so it doesn't block merge.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

# Conflicts:
#	.claude/skills/aube-sync/SKILL.md
#	.github/workflows/ci.yml
#	Cargo.toml
#	crates/nub-phantom/Cargo.toml
#	vendor/aube/Cargo.toml
A repo-root rust-toolchain.toml outranks `rustup default` in rustup's
toolchain precedence, and dtolnay/rust-toolchain only sets the default.
Pinning nightly there therefore redirected every version-pinned CI job --
including the MSRV Check legs that compile on 1.93.0 and 1.95.0 -- and
built the released binaries on nightly rather than stable.

min-publish-age is the only thing that needed nightly, and it only
matters where versions are chosen. Request it per invocation there:
update-deps.mts now runs `cargo +nightly update`. Every CI job and every
shipped binary builds on stable again, and the MSRV legs verify their
declared floors.

Drops the now-unreachable toolchain-soak checker and its tests; the
remaining surfaces are unchanged.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the incremental change. One prior non-blocking thread remains open.

Reviewed changes — the latest commit drops the repo-root nightly toolchain pin and, together with two merges from main, this leaves the PR carrying no Rust changes at all. The remaining supply-chain-soak, workflow-security, and tooling work is unchanged and internally consistent.

  • Drop the repo-root nightly pinrust-toolchain.toml is deleted; the cargo min-publish-age soak is now requested per-invocation via cargo +nightly update in update-deps.mts (the only step that picks versions), because a repo-root toolchain file would outrank rustup default and silently redirect the version-pinned CI/MSRV legs and the release build.
  • Remove the toolchain soak gatecheckToolchainSoak and its two tests are removed from soak.mts / soak.test.mts, and rust-toolchain.toml is dropped from the surfaces array; no dangling references remain.
  • Pin the primary Rust checks job to 1.93.0 — the Rust checks job now installs toolchain: 1.93.0, consistent with the reverted rust-version = "1.93"; other Rust jobs stay on # stable.
  • Retire the expired npm soakBypass — the external-tools.json npm entry drops its soakBypass now that 12.0.0 has cleared the 7-day window.
  • Reword the soak docs + guard a benchmark cache wipe.cargo/config.toml and the soak skill explain the no-nightly-pin posture; the benchmarking skill guards its cache-dir rm -rf against an empty substitution.

Verified locally on Node 24: soak --check (all surfaces match SOAK_DAYS=7), tools:check (7 pins valid), and test:scripts (61 pass / 1 win32-skip / 0 fail) are all green — the three gates CI runs. The 1.93.0 pin is consistent with the reverted MSRV and no cold_path() remains in the tree, so there is no 1.95 floor to break.

ℹ️ PR title and description no longer match the diff

The title and description still headline cold_path() hints, the pinned nightly toolchain, and the rust-version bump to 1.95 — but the final diff (53 files) contains no .rs files, no Cargo.toml changes, and no rust-toolchain.toml. Those Rust changes were reconciled out by the merges from main (main's vendored aube tree evolved past the cold_path() patch) and the nightly-pin drop. The PR is coherent and mergeable as-is; this is only a note that the metadata now over-promises relative to what ships.

Technical details
# PR metadata advertises Rust work the diff no longer contains

## Affected sites
- PR #442 title: "build: 7-day soak across toolchain and deps, cold_path() hints, pinned tooling"
- PR #442 body: the "Toolchain + cold-path hints" section describes `nightly-2026-07-04`, `rust-version` 1.88 → 1.95, and `cold_path()` in `materialize.rs`/`subset.rs`/`semver_util.rs`/`tarball.rs`.
- Final diff (`426042a`): 0 `.rs` files, 0 `Cargo.toml`, 0 `rust-toolchain.toml`. Working tree `rust-version` = 1.93 (root) / 1.91 (vendor/aube); grep of `vendor/aube` = 0 `cold_path`.

## Required outcome
- A conscious decision on whether the dropped Rust work (nightly pin, 1.95 bump, `cold_path()` hints) is out of scope for this PR — and if so, the title and description updated to describe only the soak/security/tooling changes that actually ship.

## Open questions for the human
- Are the `cold_path()` hints intended to return in a follow-up (against main's current aube tree), or are they abandoned? If returning, the 1.95 floor decision comes back with them.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

- Correct the actions/checkout@93cb6efe pin comments from the moving major
  tag "v5" to the exact immutable tag v5.0.1 the SHA resolves to (comment
  only; no SHA changed). Clears 53 ref-version-mismatch findings.
- Suppress impostor-commit and ref-version-mismatch on the dtolnay/rust-toolchain
  pins; the repo force-pushes its version tags, so pinned SHAs go unreachable
  and cannot be pinned to a stable tag. Both SHAs verified as genuine upstream
  commits.
- Add persist-credentials: false to the two verify-install.yml checkout steps
  flagged by artipacked; neither job pushes to the repo.
- Suppress dangerous-triggers on trunk-red.yml; it is a report-only workflow_run
  notifier that never checks out or runs PR-controlled code.
@colinhacks
colinhacks merged commit 1efff7d into nubjs:main Jul 24, 2026
71 checks passed
@colinhacks

Copy link
Copy Markdown
Contributor

Thanks, love this! Merged.

Rebased onto main (had drifted ~112 commits incl. the aube v1.32.0 sync), with a few tweaks:

  • Dropped the repo-root nightly pin — a root rust-toolchain.toml outranks rustup default, so it shipped binaries on nightly and overrode the MSRV Check job. The cargo soak now runs via cargo +nightly update; the rest on stable.
  • Reverted the cold_path() hints — they lift aube's MSRV to 1.95, but it's pinned at 1.91 for the mise embed.
  • Pruned the expired npm soakBypass; cleared the zizmor findings (tag labels, a dtolnay suppression, two persist-credentials).

CI green. The soak manager, hardening, and pinned tools are as you built them.

colinhacks added a commit that referenced this pull request Jul 24, 2026
)

* perf(pm): restore cold_path() hints in the vendored aube hot loops

nub vendors aube and builds every binary on current stable, so aube's
upstream MSRV floor (1.91, held there so mise's distro packaging can embed
the crates) does not bind nub. Re-add core::hint::cold_path() on the rare
arms of the hot install loops: linker link-fallbacks, resolver semver
cache-misses, lockfile subset-parser bails, tarball validation rejects.
Behaviorally inert — a codegen hint only.

Raise the vendored aube rust-version to 1.95 (documented in Cargo.toml as a
deliberate fork delta, preserved on every aube bump) and, because nub-cli
depends on aube, the nub root rust-version to 1.95 as well. nub-native
already required 1.95, so this only aligns the root floor with what the full
build needs. The CI MSRV Check job runs the root + native legs on 1.95.0;
nub-phantom (aube-free) keeps its 1.93.0 leg.

Refs #442

* fix(msrv): scope the 1.95 floor to nub-cli, keep the workspace at 1.93

Raising the workspace rust-version to 1.95 propagated to nub-phantom-core
and nub-phantom-scan (root members), which the aube-free nub-phantom eval
tool depends on — so the MSRV Check job's nub-phantom-on-1.93 leg failed
with "requires rustc 1.95". Only nub-cli actually pulls in the vendored
aube (the sole user of core::hint::cold_path), so set rust-version = "1.95"
on nub-cli alone and restore the workspace floor to 1.93. Every other crate
keeps its 1.93 floor.
@colinhacks

Copy link
Copy Markdown
Contributor

cold_path() is back on main via #553 — the 22 hints reverted here, re-placed onto the current aube v1.32.0 source.

Kept stable-only (no nightly): cold_path() has been stable since 1.95, so the floor just moves to 1.95 on nub-cli (the crate that compiles aube) and on the vendored aube — scoped there rather than raising the whole workspace, so nub-phantom and friends stay at 1.93. Documented in vendor/aube/Cargo.toml as a deliberate fork delta so aube syncs preserve it.

Nothing else here was re-touched — the soak, zizmor hardening, and pinned tooling all stand as merged.

@colinhacks

Copy link
Copy Markdown
Contributor

Shipped in v0.6.0: https://github.com/nubjs/nub/releases/tag/v0.6.0

proggeramlug pushed a commit to PerryTS/perry that referenced this pull request Jul 28, 2026
…, agent/skills scanners, taze) (#6912)

* build: 7-day supply-chain soak + pinned security tooling

Ports the nub soak/security stack (nubjs/nub#442) with the wheelhouse
shim workarounds baked in:

- scripts/soak/: soak window parity gate + fixer (SOAK_DAYS=7 in
  constants.mts is the single source), soaked dependency updater
  (taze for npm, rustup cargo for crates), and the external-tools
  installer (SRI-verified rack + PATH handles + sfw firewall shims).
- Soak surfaces: .npmrc min-release-age, tools/pnpm-workspace.yaml
  minimumReleaseAge (pnpm domain isolated in tools/ so the root stays
  npm-only), tools/taze.config.mts (imports SOAK_DAYS),
  .cargo/config.toml min-publish-age (nightly-only; inert on stable),
  .github/dependabot.yml cooldown per update block (the renovate-check
  equivalent, reworked for dependabot).
- external-tools.json: exact pins + sha512 SRI for pnpm 11.8.0,
  npm 12, sfw-free/-enterprise 1.13.1, zizmor 1.26.1, agentshield
  1.4.0, skillspector @2eb84478.
- sfw shims carry the known workarounds: per-command recursion
  sentinel (not PATH-strip), fail-open when sfw is absent, symlink
  clobber guard, rack-pinned pnpm/npm resolution.
- zizmor workflow (hash-pinned action, gate at high) with a documented
  starting config: official actions ref-pin, six existing third-party
  actions grandfathered pending a digest-pin sweep, cache-poisoning
  (49 Low-confidence findings) disabled, release-packages.yml
  excessive-permissions scoped-ignored pending a job-level split.
- security-audit.yml gains always-run soak-gate, agent-scan
  (AgentShield over .claude/), and skills-scan (NVIDIA SkillSpector
  over .claude/skills/, static --no-llm path) jobs.
- .claude/skills/soak/SKILL.md documents the workflow.

* docs: changelog fragment for #6912

* ci(zizmor): run the SRI-pinned binary instead of the marketplace action

The zizmorcore/zizmor-action run hit startup_failure (repo Actions
allowlist), and the rack binary is the better shape anyway: one pin
source (external-tools.json), and local tools:install audits with the
exact bits CI uses. Token-only-when-nonempty works around zizmor
treating an empty --gh-token as real and then fatally erroring.

* deps(sfw): bump firewall pins to 1.14.0 via dated soakBypass

1.14.0 (published 2026-07-23) fixes two things the shims care about:
sfw's own diagnostics now go to stderr (stdout stays transparent for
callers capturing `pnpm --version` through a shim), and the child env
gains a NO_PROXY loopback exemption (localhost,127.0.0.1,::1) so
locally-mocked registries are never proxied.

Inside the 7-day window until 2026-07-30, so both pins carry the
dated soakBypass annotation; tools:check will demand its removal
once the window clears — prune the two annotations then.

* deps(tools): bump zizmor 1.28.0, pnpm 11.15.1, npm 12.0.1 — newest soaked releases

All three cleared the 7-day window (zizmor 1.28.0 published 07-21,
pnpm 11.15.1 07-19, npm 12.0.1 07-10), so no bypass annotations.
pnpm 11.16/11.17 and taze 19.16.0 are still soaking; skillspector
upstream (2.4/2.5) is entirely inside the window — follow-up bumps
once cleared. Gate re-verified: zizmor 1.28.0 with the shipped config
reports 0 findings at high across .github/.

* docs: fragment says rack-pinned zizmor, not marketplace action

* feat(soak): auto-prune expired bypass annotations — fixer + scheduled bot PR

The soak gates fail closed by design: the day a soakBypass window
clears, tools:check goes red until the two annotation lines come off.
Failing closed is right; making a human notice is not. So:

- external-tools.mts gains --fix (npm run tools:fix): prunes soakBypass
  annotations whose removable date has passed, then re-runs the checks.
  Valid-but-expired only — malformed dates stay findings for a human.
- soak-autofix.yml (daily cron + dispatch) runs soak:fix + tools:fix,
  and when anything changed commits to bot/soak-autofix and opens (or
  force-updates) a PR. Note in-workflow: PRs opened with the default
  github.token don't trigger CI; set the optional SOAK_AUTOFIX_TOKEN
  secret to make the bot PRs run checks like any other.

Verified end-to-end with a planted expired annotation: --fix prunes it,
check returns green, and the fixer is idempotent (unit-tested).

* ci(soak-autofix): bind the artipacked ignore to the checkout line

* ci: pin actions/* to latest release-tag SHAs in the new security workflows

checkout v7.0.1 (3d3c42e5) + setup-node v7.0.0 (82076278) across
soak-autofix / zizmor / security-audit — both releases cleared the
7-day window (07-20 / 07-14). Also splits the PATH export in
agent-scan (SC2155). The legacy workflow fleet stays on ref pins per
the documented digest-pin sweep in .github/zizmor.yml.

* fix(soak): expired annotations warn instead of failing — stale is not unsafe

An EXPIRED soakBypass / exclude pin means the version has fully soaked:
the bypass no longer bypasses anything and the pin stays SRI-verified.
Failing closed on that turned a no-risk cosmetic state into a red
required check that flips overnight with zero code change — the exact
noise that trains people to admin-bypass (and the model wheelhouse
deliberately avoids: informational + auto-drop).

Now: expired-but-VALID annotations are warnings (exit 0), surfaced by
staleBypasses / staleExcludes and pruned by --fix + the daily
soak-autofix workflow. Missing, malformed, or wrong-arithmetic
annotations stay hard failures — unauditable IS unsafe. This also
defuses the 2026-07-30 expiry of this PR's own sfw annotations.

* fix(soak): address review-bot findings across the port

- platformKey(): detect musl via the loader heuristic — the -musl pnpm
  pins were dead keys and a musl host silently installed glibc bits;
  tools with no -musl pin now fail loud instead.
- RUSTUP_CARGO honors CARGO_HOME (custom cargo homes reported the
  rustup shim as missing).
- parseExcludeEntries: tolerate a trailing comment on the
  minimumReleaseAgeExclude key line — previously the block never opened
  and every entry beneath escaped validation.
- checkCatalogParity: malformed package.json is a Finding, not a crash.
- soak-autofix workflow: main-ref guard (dispatch on a topic branch
  can't force-push the bot branch), concurrency group, and fixer exit
  status captured + re-raised AFTER the mechanical commit instead of
  '|| true' masking runtime failures.
- sfw shims: fail-open is no longer silent-open — one stderr line when
  sfw is missing (never on the sentinel re-entry path).
- GITHUB_TOKEN on the CI install steps (github.com release fetches).
- schematic YYYY-MM-DD example dates in the yaml + skill (the concrete
  examples were expired copy-paste bait); em-dashes restored in
  external-tools.json (ensure_ascii artifact).

* fix(sfw): export SFW_UNKNOWN_HOST_ACTION=ignore in the shims

Wheelhouse lesson: enterprise sfw defaults to BLOCK for non-registry
hosts, which breaks ordinary dev flows (API calls, git clones) the day
a SOCKET_SECURITY_KEY lands. Free tier hardcodes ignore and disregards
the var, so setting it unconditionally is always safe.
proggeramlug pushed a commit to PerryTS/perry that referenced this pull request Jul 30, 2026
…ass and panic-dedup gaps hit compiling a real npm CLI (#7021)

* build: 7-day supply-chain soak + pinned security tooling

Ports the nub soak/security stack (nubjs/nub#442) with the wheelhouse
shim workarounds baked in:

- scripts/soak/: soak window parity gate + fixer (SOAK_DAYS=7 in
  constants.mts is the single source), soaked dependency updater
  (taze for npm, rustup cargo for crates), and the external-tools
  installer (SRI-verified rack + PATH handles + sfw firewall shims).
- Soak surfaces: .npmrc min-release-age, tools/pnpm-workspace.yaml
  minimumReleaseAge (pnpm domain isolated in tools/ so the root stays
  npm-only), tools/taze.config.mts (imports SOAK_DAYS),
  .cargo/config.toml min-publish-age (nightly-only; inert on stable),
  .github/dependabot.yml cooldown per update block (the renovate-check
  equivalent, reworked for dependabot).
- external-tools.json: exact pins + sha512 SRI for pnpm 11.8.0,
  npm 12, sfw-free/-enterprise 1.13.1, zizmor 1.26.1, agentshield
  1.4.0, skillspector @2eb84478.
- sfw shims carry the known workarounds: per-command recursion
  sentinel (not PATH-strip), fail-open when sfw is absent, symlink
  clobber guard, rack-pinned pnpm/npm resolution.
- zizmor workflow (hash-pinned action, gate at high) with a documented
  starting config: official actions ref-pin, six existing third-party
  actions grandfathered pending a digest-pin sweep, cache-poisoning
  (49 Low-confidence findings) disabled, release-packages.yml
  excessive-permissions scoped-ignored pending a job-level split.
- security-audit.yml gains always-run soak-gate, agent-scan
  (AgentShield over .claude/), and skills-scan (NVIDIA SkillSpector
  over .claude/skills/, static --no-llm path) jobs.
- .claude/skills/soak/SKILL.md documents the workflow.

* docs: changelog fragment for #6912

* ci(zizmor): run the SRI-pinned binary instead of the marketplace action

The zizmorcore/zizmor-action run hit startup_failure (repo Actions
allowlist), and the rack binary is the better shape anyway: one pin
source (external-tools.json), and local tools:install audits with the
exact bits CI uses. Token-only-when-nonempty works around zizmor
treating an empty --gh-token as real and then fatally erroring.

* deps(sfw): bump firewall pins to 1.14.0 via dated soakBypass

1.14.0 (published 2026-07-23) fixes two things the shims care about:
sfw's own diagnostics now go to stderr (stdout stays transparent for
callers capturing `pnpm --version` through a shim), and the child env
gains a NO_PROXY loopback exemption (localhost,127.0.0.1,::1) so
locally-mocked registries are never proxied.

Inside the 7-day window until 2026-07-30, so both pins carry the
dated soakBypass annotation; tools:check will demand its removal
once the window clears — prune the two annotations then.

* deps(tools): bump zizmor 1.28.0, pnpm 11.15.1, npm 12.0.1 — newest soaked releases

All three cleared the 7-day window (zizmor 1.28.0 published 07-21,
pnpm 11.15.1 07-19, npm 12.0.1 07-10), so no bypass annotations.
pnpm 11.16/11.17 and taze 19.16.0 are still soaking; skillspector
upstream (2.4/2.5) is entirely inside the window — follow-up bumps
once cleared. Gate re-verified: zizmor 1.28.0 with the shipped config
reports 0 findings at high across .github/.

* docs: fragment says rack-pinned zizmor, not marketplace action

* feat(soak): auto-prune expired bypass annotations — fixer + scheduled bot PR

The soak gates fail closed by design: the day a soakBypass window
clears, tools:check goes red until the two annotation lines come off.
Failing closed is right; making a human notice is not. So:

- external-tools.mts gains --fix (npm run tools:fix): prunes soakBypass
  annotations whose removable date has passed, then re-runs the checks.
  Valid-but-expired only — malformed dates stay findings for a human.
- soak-autofix.yml (daily cron + dispatch) runs soak:fix + tools:fix,
  and when anything changed commits to bot/soak-autofix and opens (or
  force-updates) a PR. Note in-workflow: PRs opened with the default
  github.token don't trigger CI; set the optional SOAK_AUTOFIX_TOKEN
  secret to make the bot PRs run checks like any other.

Verified end-to-end with a planted expired annotation: --fix prunes it,
check returns green, and the fixer is idempotent (unit-tested).

* ci(soak-autofix): bind the artipacked ignore to the checkout line

* ci: pin actions/* to latest release-tag SHAs in the new security workflows

checkout v7.0.1 (3d3c42e5) + setup-node v7.0.0 (82076278) across
soak-autofix / zizmor / security-audit — both releases cleared the
7-day window (07-20 / 07-14). Also splits the PATH export in
agent-scan (SC2155). The legacy workflow fleet stays on ref pins per
the documented digest-pin sweep in .github/zizmor.yml.

* fix(soak): expired annotations warn instead of failing — stale is not unsafe

An EXPIRED soakBypass / exclude pin means the version has fully soaked:
the bypass no longer bypasses anything and the pin stays SRI-verified.
Failing closed on that turned a no-risk cosmetic state into a red
required check that flips overnight with zero code change — the exact
noise that trains people to admin-bypass (and the model wheelhouse
deliberately avoids: informational + auto-drop).

Now: expired-but-VALID annotations are warnings (exit 0), surfaced by
staleBypasses / staleExcludes and pruned by --fix + the daily
soak-autofix workflow. Missing, malformed, or wrong-arithmetic
annotations stay hard failures — unauditable IS unsafe. This also
defuses the 2026-07-30 expiry of this PR's own sfw annotations.

* fix(soak): address review-bot findings across the port

- platformKey(): detect musl via the loader heuristic — the -musl pnpm
  pins were dead keys and a musl host silently installed glibc bits;
  tools with no -musl pin now fail loud instead.
- RUSTUP_CARGO honors CARGO_HOME (custom cargo homes reported the
  rustup shim as missing).
- parseExcludeEntries: tolerate a trailing comment on the
  minimumReleaseAgeExclude key line — previously the block never opened
  and every entry beneath escaped validation.
- checkCatalogParity: malformed package.json is a Finding, not a crash.
- soak-autofix workflow: main-ref guard (dispatch on a topic branch
  can't force-push the bot branch), concurrency group, and fixer exit
  status captured + re-raised AFTER the mechanical commit instead of
  '|| true' masking runtime failures.
- sfw shims: fail-open is no longer silent-open — one stderr line when
  sfw is missing (never on the sentinel re-entry path).
- GITHUB_TOKEN on the CI install steps (github.com release fetches).
- schematic YYYY-MM-DD example dates in the yaml + skill (the concrete
  examples were expired copy-paste bait); em-dashes restored in
  external-tools.json (ensure_ascii artifact).

* fix(sfw): export SFW_UNKNOWN_HOST_ACTION=ignore in the shims

Wheelhouse lesson: enterprise sfw defaults to BLOCK for non-registry
hosts, which breaks ordinary dev flows (API calls, git clones) the day
a SOCKET_SECURITY_KEY lands. Free tier hardcodes ignore and disregards
the var, so setting it unconditionally is always safe.

* fix(soak): take review fixes surfaced on the aube twin

- checkDockerPrebake: parse the rustup install line's argument list
  instead of substring-matching the msrv (a multi-toolchain install
  line false-failed the check).
- RUSTUP_CARGO resolves cargo.exe on win32.
- soak-autofix: lease-checked force push (fetch the bot branch, then
  --force-with-lease) so a concurrent actor's commits are never
  clobbered.

* docs(soak): align prose with warn-not-fail; source-cite the unknown-host comment

Same drift pullfrog flagged on the nub twin: the skill still said the
gates "fail closed when a bypass window clears" — expired-but-valid
annotations warn and get pruned by soak:fix / the soak-autofix
workflow; invalid annotations are what fail. The shim comment now
claims only what the source shows about SFW_UNKNOWN_HOST_ACTION (the
enterprise config parses it; inert for free).

* fix(soak): never prune a wrong-arithmetic annotation as "cleared"

Greptile P1 on the aube twin: the pruners and stale lists accepted any
valid-ISO annotation whose removable date had passed — including one
whose removable was WRONG (earlier than published + SOAK_DAYS). Such an
annotation must surface as the hard check failure it is; treating it as
soaked would silently delete a bypass whose real window may still be
open. All four surfaces (staleExcludes, fixWorkspaceYaml,
staleBypasses, pruneExpiredSoakBypasses) now require the arithmetic to
hold before an annotation counts as stale or prunable; regression
tests cover the wrong-math-expired case.

* fix(soak): downloads fall back to unauthenticated and retry once on 5xx

The nub node-18 compat leg died on `download failed 500` — the first
authed fetch of a PUBLIC sfw release asset after GITHUB_TOKEN was added
to the step env. Whether that 500 was token-induced (an Actions token
against a cross-org public asset endpoint) or a transient GitHub blip,
one attempt was too brittle: download() now retries without auth when
an authed fetch fails (public assets need no credential), and once more
after 2s on a 5xx. Regression test pins the fallback dropping the
Authorization header.

* fix(soak): stop the fixers reformatting files they do not own

Adversarial self-review of the renovate/npmrc/yaml fixers, prompted by
the 20-line diff my own soak:fix produced on aube's renovate.json:

- fixRenovateConfig rewrote the WHOLE file via JSON.parse +
  re-stringify, collapsing hand-written single-line arrays and
  reformatting unrelated packageRules (aube's decmpfs musl hold among
  them). It is now a targeted text edit: only the minimumReleaseAge
  line changes, every other byte is preserved. A regression test
  asserts exactly one changed line and that the decmpfs rule survives
  verbatim.
- The insert path produced INVALID JSON for a minimal `{}` config
  (`{,\n ...}`); guarded and covered by a test.
- fixNpmrc / fixWorkspaceYaml matched trailing `\s*$` under /m — `\s`
  matches newlines, so the replacement swallowed blank lines after the
  key. Now `[ \t]*$`; verified soak:fix is a no-op on a clean tree.
- checkRenovateConfig now also requires `internalChecksFilter: strict`.
  Without it renovate's default flexible mode raises updates that have
  NOT cleared minimumReleaseAge — the window silently stops biting.
- The no-pinned-asset error names the musl case and lists the pinned
  platforms: sfw ships no musl asset, so an alpine runner hits this,
  and the old message gave nothing to act on.

Verified alongside: decmpfs stays at 0.1.0 under `cargo update` (the
`=0.1.0` requirement holds, so the soak updater cannot smuggle in the
musl-breaking 0.1.2), and `--force-with-lease` correctly rejects a
concurrent update even when the preceding fetch fails, and still
creates the branch on a first run.

* fix(soak): stop the fixers reformatting files they do not own

Adversarial self-review of the renovate/npmrc/yaml fixers, prompted by
the 20-line diff my own soak:fix produced on aube's renovate.json:

- fixRenovateConfig rewrote the WHOLE file via JSON.parse +
  re-stringify, collapsing hand-written single-line arrays and
  reformatting unrelated packageRules (aube's decmpfs musl hold among
  them). It is now a targeted text edit: only the minimumReleaseAge
  line changes, every other byte is preserved. A regression test
  asserts exactly one changed line and that the decmpfs rule survives
  verbatim.
- The insert path produced INVALID JSON for a minimal `{}` config
  (`{,\n ...}`); guarded and covered by a test.
- fixNpmrc / fixWorkspaceYaml matched trailing `\s*$` under /m — `\s`
  matches newlines, so the replacement swallowed blank lines after the
  key. Now `[ \t]*$`; verified soak:fix is a no-op on a clean tree.
- checkRenovateConfig now also requires `internalChecksFilter: strict`.
  Without it renovate's default flexible mode raises updates that have
  NOT cleared minimumReleaseAge — the window silently stops biting.
- The no-pinned-asset error names the musl case and lists the pinned
  platforms: sfw ships no musl asset, so an alpine runner hits this,
  and the old message gave nothing to act on.

Verified alongside: decmpfs stays at 0.1.0 under `cargo update` (the
`=0.1.0` requirement holds, so the soak updater cannot smuggle in the
musl-breaking 0.1.2), and `--force-with-lease` correctly rejects a
concurrent update even when the preceding fetch fails, and still
creates the branch on a first run.

* feat(soak): gate npm's min-release-age-exclude entries too

Auditing a sibling fleet repo (abitious) for compatibility surfaced an
unguarded bypass: npm >= 11.17 has its OWN exclude surface,
`min-release-age-exclude[]=<spec>`, parallel to pnpm's
`minimumReleaseAgeExclude` block — and the gate validated only the pnpm
side. `min-release-age-exclude[]=lodash@1.2.3` was therefore an
unvalidated, never-expiring hole in exactly the rule the yaml side
enforces.

checkNpmrc now applies the same law to .npmrc: bare names and `@scope/*`
globs are standing trust (the shape real repos use for trusted scopes,
so this is not a churn tax), while a VERSION-PINNED entry needs the
`# published: | removable:` annotation with correct arithmetic and real
calendar dates. Tests cover trusted-glob, unannotated, correct,
wrong-arithmetic, and impossible-date cases.

* fix(soak): fail loudly when cargo silently ignores min-publish-age

Verified rather than assumed, and the assumption was wrong: cargo treats
an [unstable] key it does not implement as a WARNING ("unused config key
`unstable.min-publish-age`") and exits 0. Measured on nightly
2026-03-21, which has no such -Z — so `cargo +nightly update` on a
merely-OLD nightly resolved every crate with NO window at all while the
run reported success. The tooling was claiming a protection it had not
applied.

updateCargo now captures stderr and treats that warning as a hard
failure: the lockfile changes are unsoaked, so say so and exit nonzero
with the fix (`rustup update nightly`). The detector is an exported,
unit-tested predicate pinning cargo's exact wording.

perry rides stable, where the key is expected to be inert, so there the
same detection downgrades to an explicit note naming dependabot cooldown
as the enforcing surface for cargo deps — no silent no-op either way.

* feat(soak): explain a window-blocked cargo re-resolution, refuse the env bypass

Re-measured on a current nightly (2026-07-27, cargo 1.99.0-nightly): the
`-Z min-publish-age` feature IS implemented there and the window visibly
bites — it holds a too-fresh release back ("available: v0.2.189,
published 7 days ago"). Both measurements are now recorded in the
comment and the skill, since the OLD nightly (2026-03-21) is the
evidence that a stale toolchain skips the window silently.

Running the real updater surfaced the other half of the contract: the
window can make re-resolution IMPOSSIBLE, not just conservative. When a
requirement's only candidate is inside the window (aube today:
`clap_usage = "^4"`, whose 4.0.0 shipped 3 days ago) cargo fails the
whole update — correct behavior, but its own help line advertises
`CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow`, a blanket env-var
bypass this design deliberately does not have. The updater now detects
that failure and prints ordered options (wait it out, repin so a soaked
version satisfies the requirement, or adopt the fresh release as a
reviewable commit) with an explicit warning against the env bypass.
Predicate is exported and unit-tested against cargo's real wording.

* fix(soak): take the adversarial-review findings

An independent hostile review of the three sibling PRs found real
defects, including one where my own test had verified only the safe half
of the case:

- soak-autofix no longer force-pushes at all. `git fetch origin $BRANCH`
  UPDATES the remote-tracking ref (actions/checkout leaves the default
  wildcard refspec), so the following --force-with-lease took its lease
  against whatever another actor had just pushed and overwrote it — the
  classic fetch-before-lease anti-pattern, and the inline comment
  asserting "a concurrent actor's commits are never clobbered" was
  false. Demonstrated: a human commit onto the open autofix PR was
  discarded by the next scheduled run. My earlier test only covered the
  fetch-FAILS path (which is genuinely safe, rejecting with "stale
  info"). The step now stashes the fixes, bases the work on the existing
  bot branch when there is one, and plain-pushes: human commits survive
  by construction, an empty re-run exits 0 instead of pushing a no-op
  commit, and a genuine conflict fails loudly instead of being resolved
  by deletion.

- fixWorkspaceYaml's prune set must EQUAL staleExcludes' warn set: it was
  missing the VERSION_PIN_RE guard, so a bare-name / `@scope/*`
  standing-trust entry sitting under an expired annotation line was
  deleted by --fix, silently re-arming the soak for a whole scope inside
  a bot commit advertised as touching only annotation lines.

- download() retry semantics split by meaning: 401/403/404 with a token
  means the credential is the problem (retry unauthenticated), >=500 is
  transient (retry with the SAME auth). Dropping auth on 5xx made a
  private asset 404 on the retry, report a bogus "download failed 404",
  and never be able to succeed. The SRI is verified either way, so no
  retry can substitute a different artifact.

* fix(soak): take the review findings — one is a regression I introduced

- fixRenovateConfig still matched a trailing `\s*$`, which under /m eats
  the NEWLINES after the value: replacing through it silently deleted the
  blank line that followed. That is the exact defect the same commit
  fixed in fixNpmrc and fixWorkspaceYaml, kept in the third fixer.
  Verified with a config carrying a blank line after the key: it
  disappeared before, survives now.

- checkPins now rejects a soakBypass whose `version` is not the version
  actually pinned. Bump a pin and leave the annotation behind and the
  ledger vouches for a release that is no longer installed — "1.13.1 was
  adopted early" while 1.14.0 ships unreviewed. A mismatch is
  unauditable, so it is a hard finding, not a stale-annotation warning.

- soak-autofix.yml's header still described the gates as failing closed
  on a cleared window; the fourth and last sibling of that stale premise.
  Expired is a warning, invalid still fails, and the workflow's job is
  convergence rather than rescue.

- Two paths were changed without a test covering them, both added: the
  multi-arg `rustup toolchain install 1.91.0 1.93.0` case that motivated
  replacing the substring msrv match (only the negative case was
  covered), and the `>= 500` retry branch that the retry commit is named
  for (the existing test exercises only the auth fallback).

* fix(compile): survive binary/workspace skew and complete the surfaces a real npm CLI needs

Compiling Socket Firewall (sfw — a TLS-MITM proxy CLI with undici,
node-forge, iovalkey, zod, … in its graph) end-to-end surfaced four
independent blockers. Fixed here:

1. auto-optimize feature skew (driver.rs / freshness.rs): the perry
   binary's baked-in cross-feature list tracks the branch it was BUILT
   from, but the auto-optimize cargo build resolves against the checkout
   on disk. One unknown `perry-runtime/<feat>` failed the whole resolve,
   and the silent prebuilt fallback linked without the routed ext-pump
   entrypoints — undefined-js_* errors two stages from the cause. New
   retain_workspace_declared_features() drops names the checkout's
   perry-runtime / perry-stdlib don't declare (features table + optional
   deps, fail-open on unreadable manifests) before the build stamp is
   computed, and the cargo-failure fallback now says what the
   consequence and remedy are.

2. perry-ext-zlib zstd surface: undici's web-fetch content decoding
   references js_zlib_create_zstd_decompress unconditionally, but only
   perry-stdlib's `compression` module carried the zstd codecs — and
   routing node:zlib to the ext archive strips that feature. Port the
   full surface (create factories, sync/async one-shots, streaming
   write-codec via zstd::stream::write) so the routed archive is
   self-sufficient.

3. class X extends DOMException (codegen + runtime): undici probes
   DOMException inheritability at module load (websocketerror.js), and
   the name was neither in the builtin-parent list nor backed by a
   subclass initializer — the compiled binary died at startup with
   'DOMException is not a function'. Add js_dom_exception_subclass_init
   (stamps message/name/code onto the subclass instance) wired through
   both the explicit super() lowering and the implicit-ctor
   NativeInstanceBase chain walk.

4. panic-runtime dedup for prebuilt (panic=unwind) wrappers co-linked
   with a panic=abort auto-optimized stdlib (strip_dedup.rs): the
   name-containment rule never nominated the wrapper's panic_unwind
   member (stdlib bundles panic_abort under a different name), and the
   localize pass severed the std-cgu → panic_unwind __rust_drop_panic
   edge that abort stdlibs cannot re-provide. Nominate panic_unwind in
   the nosharedeps fixed-point (protected exactly when the stdlib can't
   cover it), and skip localizing panic symbols a sibling member still
   references. Allocator shims stay always-localized: leaving the
   wrapper's system-malloc shim global beats the runtime's mimalloc at
   link and breaks pointer classification (silent console loss).

With these, sfw and sfw-free compile, link, and run as native arm64
binaries straight from their TypeScript entrypoints.

* docs: changelog fragment for #7021

* fix(runtime): make the rebound RegExp global constructible via its call form

ECMA-262 22.2.4: `RegExp(pattern, flags)` without `new` constructs
exactly like `new RegExp`, with the identity shortcut `RegExp(re)` →
`re`. The globalThis sentinel fell through to the noop thunk and
returned undefined — which is how lodash's module init died:
runInContext rebinds the global (`var RegExp = context.RegExp`) and
builds `reIsNative` through the call form, so the immediately
following `reIsNative.test(...)` threw 'Cannot read properties of
undefined'. New regexp_constructor_call_thunk (arity 2) mirrors the
dynamic-new RegExp arm in class_registry/construct.rs; without the
regex-engine feature it keeps the old noop behavior.

Unblocks sfw-registry (lodash via registry/proxy-request.ts).

* docs: extend #7021 changelog fragment with the RegExp call-form fix

* fix: address stacked stdlib review

* fix: address compile stack review

* docs: correct compile-gap blocker count

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug pushed a commit to PerryTS/perry that referenced this pull request Jul 30, 2026
…s") support (#7028)

* build: 7-day supply-chain soak + pinned security tooling

Ports the nub soak/security stack (nubjs/nub#442) with the wheelhouse
shim workarounds baked in:

- scripts/soak/: soak window parity gate + fixer (SOAK_DAYS=7 in
  constants.mts is the single source), soaked dependency updater
  (taze for npm, rustup cargo for crates), and the external-tools
  installer (SRI-verified rack + PATH handles + sfw firewall shims).
- Soak surfaces: .npmrc min-release-age, tools/pnpm-workspace.yaml
  minimumReleaseAge (pnpm domain isolated in tools/ so the root stays
  npm-only), tools/taze.config.mts (imports SOAK_DAYS),
  .cargo/config.toml min-publish-age (nightly-only; inert on stable),
  .github/dependabot.yml cooldown per update block (the renovate-check
  equivalent, reworked for dependabot).
- external-tools.json: exact pins + sha512 SRI for pnpm 11.8.0,
  npm 12, sfw-free/-enterprise 1.13.1, zizmor 1.26.1, agentshield
  1.4.0, skillspector @2eb84478.
- sfw shims carry the known workarounds: per-command recursion
  sentinel (not PATH-strip), fail-open when sfw is absent, symlink
  clobber guard, rack-pinned pnpm/npm resolution.
- zizmor workflow (hash-pinned action, gate at high) with a documented
  starting config: official actions ref-pin, six existing third-party
  actions grandfathered pending a digest-pin sweep, cache-poisoning
  (49 Low-confidence findings) disabled, release-packages.yml
  excessive-permissions scoped-ignored pending a job-level split.
- security-audit.yml gains always-run soak-gate, agent-scan
  (AgentShield over .claude/), and skills-scan (NVIDIA SkillSpector
  over .claude/skills/, static --no-llm path) jobs.
- .claude/skills/soak/SKILL.md documents the workflow.

* docs: changelog fragment for #6912

* ci(zizmor): run the SRI-pinned binary instead of the marketplace action

The zizmorcore/zizmor-action run hit startup_failure (repo Actions
allowlist), and the rack binary is the better shape anyway: one pin
source (external-tools.json), and local tools:install audits with the
exact bits CI uses. Token-only-when-nonempty works around zizmor
treating an empty --gh-token as real and then fatally erroring.

* deps(sfw): bump firewall pins to 1.14.0 via dated soakBypass

1.14.0 (published 2026-07-23) fixes two things the shims care about:
sfw's own diagnostics now go to stderr (stdout stays transparent for
callers capturing `pnpm --version` through a shim), and the child env
gains a NO_PROXY loopback exemption (localhost,127.0.0.1,::1) so
locally-mocked registries are never proxied.

Inside the 7-day window until 2026-07-30, so both pins carry the
dated soakBypass annotation; tools:check will demand its removal
once the window clears — prune the two annotations then.

* deps(tools): bump zizmor 1.28.0, pnpm 11.15.1, npm 12.0.1 — newest soaked releases

All three cleared the 7-day window (zizmor 1.28.0 published 07-21,
pnpm 11.15.1 07-19, npm 12.0.1 07-10), so no bypass annotations.
pnpm 11.16/11.17 and taze 19.16.0 are still soaking; skillspector
upstream (2.4/2.5) is entirely inside the window — follow-up bumps
once cleared. Gate re-verified: zizmor 1.28.0 with the shipped config
reports 0 findings at high across .github/.

* docs: fragment says rack-pinned zizmor, not marketplace action

* feat(soak): auto-prune expired bypass annotations — fixer + scheduled bot PR

The soak gates fail closed by design: the day a soakBypass window
clears, tools:check goes red until the two annotation lines come off.
Failing closed is right; making a human notice is not. So:

- external-tools.mts gains --fix (npm run tools:fix): prunes soakBypass
  annotations whose removable date has passed, then re-runs the checks.
  Valid-but-expired only — malformed dates stay findings for a human.
- soak-autofix.yml (daily cron + dispatch) runs soak:fix + tools:fix,
  and when anything changed commits to bot/soak-autofix and opens (or
  force-updates) a PR. Note in-workflow: PRs opened with the default
  github.token don't trigger CI; set the optional SOAK_AUTOFIX_TOKEN
  secret to make the bot PRs run checks like any other.

Verified end-to-end with a planted expired annotation: --fix prunes it,
check returns green, and the fixer is idempotent (unit-tested).

* ci(soak-autofix): bind the artipacked ignore to the checkout line

* ci: pin actions/* to latest release-tag SHAs in the new security workflows

checkout v7.0.1 (3d3c42e5) + setup-node v7.0.0 (82076278) across
soak-autofix / zizmor / security-audit — both releases cleared the
7-day window (07-20 / 07-14). Also splits the PATH export in
agent-scan (SC2155). The legacy workflow fleet stays on ref pins per
the documented digest-pin sweep in .github/zizmor.yml.

* fix(soak): expired annotations warn instead of failing — stale is not unsafe

An EXPIRED soakBypass / exclude pin means the version has fully soaked:
the bypass no longer bypasses anything and the pin stays SRI-verified.
Failing closed on that turned a no-risk cosmetic state into a red
required check that flips overnight with zero code change — the exact
noise that trains people to admin-bypass (and the model wheelhouse
deliberately avoids: informational + auto-drop).

Now: expired-but-VALID annotations are warnings (exit 0), surfaced by
staleBypasses / staleExcludes and pruned by --fix + the daily
soak-autofix workflow. Missing, malformed, or wrong-arithmetic
annotations stay hard failures — unauditable IS unsafe. This also
defuses the 2026-07-30 expiry of this PR's own sfw annotations.

* fix(soak): address review-bot findings across the port

- platformKey(): detect musl via the loader heuristic — the -musl pnpm
  pins were dead keys and a musl host silently installed glibc bits;
  tools with no -musl pin now fail loud instead.
- RUSTUP_CARGO honors CARGO_HOME (custom cargo homes reported the
  rustup shim as missing).
- parseExcludeEntries: tolerate a trailing comment on the
  minimumReleaseAgeExclude key line — previously the block never opened
  and every entry beneath escaped validation.
- checkCatalogParity: malformed package.json is a Finding, not a crash.
- soak-autofix workflow: main-ref guard (dispatch on a topic branch
  can't force-push the bot branch), concurrency group, and fixer exit
  status captured + re-raised AFTER the mechanical commit instead of
  '|| true' masking runtime failures.
- sfw shims: fail-open is no longer silent-open — one stderr line when
  sfw is missing (never on the sentinel re-entry path).
- GITHUB_TOKEN on the CI install steps (github.com release fetches).
- schematic YYYY-MM-DD example dates in the yaml + skill (the concrete
  examples were expired copy-paste bait); em-dashes restored in
  external-tools.json (ensure_ascii artifact).

* fix(sfw): export SFW_UNKNOWN_HOST_ACTION=ignore in the shims

Wheelhouse lesson: enterprise sfw defaults to BLOCK for non-registry
hosts, which breaks ordinary dev flows (API calls, git clones) the day
a SOCKET_SECURITY_KEY lands. Free tier hardcodes ignore and disregards
the var, so setting it unconditionally is always safe.

* fix(soak): take review fixes surfaced on the aube twin

- checkDockerPrebake: parse the rustup install line's argument list
  instead of substring-matching the msrv (a multi-toolchain install
  line false-failed the check).
- RUSTUP_CARGO resolves cargo.exe on win32.
- soak-autofix: lease-checked force push (fetch the bot branch, then
  --force-with-lease) so a concurrent actor's commits are never
  clobbered.

* docs(soak): align prose with warn-not-fail; source-cite the unknown-host comment

Same drift pullfrog flagged on the nub twin: the skill still said the
gates "fail closed when a bypass window clears" — expired-but-valid
annotations warn and get pruned by soak:fix / the soak-autofix
workflow; invalid annotations are what fail. The shim comment now
claims only what the source shows about SFW_UNKNOWN_HOST_ACTION (the
enterprise config parses it; inert for free).

* fix(soak): never prune a wrong-arithmetic annotation as "cleared"

Greptile P1 on the aube twin: the pruners and stale lists accepted any
valid-ISO annotation whose removable date had passed — including one
whose removable was WRONG (earlier than published + SOAK_DAYS). Such an
annotation must surface as the hard check failure it is; treating it as
soaked would silently delete a bypass whose real window may still be
open. All four surfaces (staleExcludes, fixWorkspaceYaml,
staleBypasses, pruneExpiredSoakBypasses) now require the arithmetic to
hold before an annotation counts as stale or prunable; regression
tests cover the wrong-math-expired case.

* fix(soak): downloads fall back to unauthenticated and retry once on 5xx

The nub node-18 compat leg died on `download failed 500` — the first
authed fetch of a PUBLIC sfw release asset after GITHUB_TOKEN was added
to the step env. Whether that 500 was token-induced (an Actions token
against a cross-org public asset endpoint) or a transient GitHub blip,
one attempt was too brittle: download() now retries without auth when
an authed fetch fails (public assets need no credential), and once more
after 2s on a 5xx. Regression test pins the fallback dropping the
Authorization header.

* fix(soak): stop the fixers reformatting files they do not own

Adversarial self-review of the renovate/npmrc/yaml fixers, prompted by
the 20-line diff my own soak:fix produced on aube's renovate.json:

- fixRenovateConfig rewrote the WHOLE file via JSON.parse +
  re-stringify, collapsing hand-written single-line arrays and
  reformatting unrelated packageRules (aube's decmpfs musl hold among
  them). It is now a targeted text edit: only the minimumReleaseAge
  line changes, every other byte is preserved. A regression test
  asserts exactly one changed line and that the decmpfs rule survives
  verbatim.
- The insert path produced INVALID JSON for a minimal `{}` config
  (`{,\n ...}`); guarded and covered by a test.
- fixNpmrc / fixWorkspaceYaml matched trailing `\s*$` under /m — `\s`
  matches newlines, so the replacement swallowed blank lines after the
  key. Now `[ \t]*$`; verified soak:fix is a no-op on a clean tree.
- checkRenovateConfig now also requires `internalChecksFilter: strict`.
  Without it renovate's default flexible mode raises updates that have
  NOT cleared minimumReleaseAge — the window silently stops biting.
- The no-pinned-asset error names the musl case and lists the pinned
  platforms: sfw ships no musl asset, so an alpine runner hits this,
  and the old message gave nothing to act on.

Verified alongside: decmpfs stays at 0.1.0 under `cargo update` (the
`=0.1.0` requirement holds, so the soak updater cannot smuggle in the
musl-breaking 0.1.2), and `--force-with-lease` correctly rejects a
concurrent update even when the preceding fetch fails, and still
creates the branch on a first run.

* fix(soak): stop the fixers reformatting files they do not own

Adversarial self-review of the renovate/npmrc/yaml fixers, prompted by
the 20-line diff my own soak:fix produced on aube's renovate.json:

- fixRenovateConfig rewrote the WHOLE file via JSON.parse +
  re-stringify, collapsing hand-written single-line arrays and
  reformatting unrelated packageRules (aube's decmpfs musl hold among
  them). It is now a targeted text edit: only the minimumReleaseAge
  line changes, every other byte is preserved. A regression test
  asserts exactly one changed line and that the decmpfs rule survives
  verbatim.
- The insert path produced INVALID JSON for a minimal `{}` config
  (`{,\n ...}`); guarded and covered by a test.
- fixNpmrc / fixWorkspaceYaml matched trailing `\s*$` under /m — `\s`
  matches newlines, so the replacement swallowed blank lines after the
  key. Now `[ \t]*$`; verified soak:fix is a no-op on a clean tree.
- checkRenovateConfig now also requires `internalChecksFilter: strict`.
  Without it renovate's default flexible mode raises updates that have
  NOT cleared minimumReleaseAge — the window silently stops biting.
- The no-pinned-asset error names the musl case and lists the pinned
  platforms: sfw ships no musl asset, so an alpine runner hits this,
  and the old message gave nothing to act on.

Verified alongside: decmpfs stays at 0.1.0 under `cargo update` (the
`=0.1.0` requirement holds, so the soak updater cannot smuggle in the
musl-breaking 0.1.2), and `--force-with-lease` correctly rejects a
concurrent update even when the preceding fetch fails, and still
creates the branch on a first run.

* feat(soak): gate npm's min-release-age-exclude entries too

Auditing a sibling fleet repo (abitious) for compatibility surfaced an
unguarded bypass: npm >= 11.17 has its OWN exclude surface,
`min-release-age-exclude[]=<spec>`, parallel to pnpm's
`minimumReleaseAgeExclude` block — and the gate validated only the pnpm
side. `min-release-age-exclude[]=lodash@1.2.3` was therefore an
unvalidated, never-expiring hole in exactly the rule the yaml side
enforces.

checkNpmrc now applies the same law to .npmrc: bare names and `@scope/*`
globs are standing trust (the shape real repos use for trusted scopes,
so this is not a churn tax), while a VERSION-PINNED entry needs the
`# published: | removable:` annotation with correct arithmetic and real
calendar dates. Tests cover trusted-glob, unannotated, correct,
wrong-arithmetic, and impossible-date cases.

* fix(soak): fail loudly when cargo silently ignores min-publish-age

Verified rather than assumed, and the assumption was wrong: cargo treats
an [unstable] key it does not implement as a WARNING ("unused config key
`unstable.min-publish-age`") and exits 0. Measured on nightly
2026-03-21, which has no such -Z — so `cargo +nightly update` on a
merely-OLD nightly resolved every crate with NO window at all while the
run reported success. The tooling was claiming a protection it had not
applied.

updateCargo now captures stderr and treats that warning as a hard
failure: the lockfile changes are unsoaked, so say so and exit nonzero
with the fix (`rustup update nightly`). The detector is an exported,
unit-tested predicate pinning cargo's exact wording.

perry rides stable, where the key is expected to be inert, so there the
same detection downgrades to an explicit note naming dependabot cooldown
as the enforcing surface for cargo deps — no silent no-op either way.

* feat(soak): explain a window-blocked cargo re-resolution, refuse the env bypass

Re-measured on a current nightly (2026-07-27, cargo 1.99.0-nightly): the
`-Z min-publish-age` feature IS implemented there and the window visibly
bites — it holds a too-fresh release back ("available: v0.2.189,
published 7 days ago"). Both measurements are now recorded in the
comment and the skill, since the OLD nightly (2026-03-21) is the
evidence that a stale toolchain skips the window silently.

Running the real updater surfaced the other half of the contract: the
window can make re-resolution IMPOSSIBLE, not just conservative. When a
requirement's only candidate is inside the window (aube today:
`clap_usage = "^4"`, whose 4.0.0 shipped 3 days ago) cargo fails the
whole update — correct behavior, but its own help line advertises
`CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow`, a blanket env-var
bypass this design deliberately does not have. The updater now detects
that failure and prints ordered options (wait it out, repin so a soaked
version satisfies the requirement, or adopt the fresh release as a
reviewable commit) with an explicit warning against the env bypass.
Predicate is exported and unit-tested against cargo's real wording.

* fix(soak): take the adversarial-review findings

An independent hostile review of the three sibling PRs found real
defects, including one where my own test had verified only the safe half
of the case:

- soak-autofix no longer force-pushes at all. `git fetch origin $BRANCH`
  UPDATES the remote-tracking ref (actions/checkout leaves the default
  wildcard refspec), so the following --force-with-lease took its lease
  against whatever another actor had just pushed and overwrote it — the
  classic fetch-before-lease anti-pattern, and the inline comment
  asserting "a concurrent actor's commits are never clobbered" was
  false. Demonstrated: a human commit onto the open autofix PR was
  discarded by the next scheduled run. My earlier test only covered the
  fetch-FAILS path (which is genuinely safe, rejecting with "stale
  info"). The step now stashes the fixes, bases the work on the existing
  bot branch when there is one, and plain-pushes: human commits survive
  by construction, an empty re-run exits 0 instead of pushing a no-op
  commit, and a genuine conflict fails loudly instead of being resolved
  by deletion.

- fixWorkspaceYaml's prune set must EQUAL staleExcludes' warn set: it was
  missing the VERSION_PIN_RE guard, so a bare-name / `@scope/*`
  standing-trust entry sitting under an expired annotation line was
  deleted by --fix, silently re-arming the soak for a whole scope inside
  a bot commit advertised as touching only annotation lines.

- download() retry semantics split by meaning: 401/403/404 with a token
  means the credential is the problem (retry unauthenticated), >=500 is
  transient (retry with the SAME auth). Dropping auth on 5xx made a
  private asset 404 on the retry, report a bogus "download failed 404",
  and never be able to succeed. The SRI is verified either way, so no
  retry can substitute a different artifact.

* fix(soak): take the review findings — one is a regression I introduced

- fixRenovateConfig still matched a trailing `\s*$`, which under /m eats
  the NEWLINES after the value: replacing through it silently deleted the
  blank line that followed. That is the exact defect the same commit
  fixed in fixNpmrc and fixWorkspaceYaml, kept in the third fixer.
  Verified with a config carrying a blank line after the key: it
  disappeared before, survives now.

- checkPins now rejects a soakBypass whose `version` is not the version
  actually pinned. Bump a pin and leave the annotation behind and the
  ledger vouches for a release that is no longer installed — "1.13.1 was
  adopted early" while 1.14.0 ships unreviewed. A mismatch is
  unauditable, so it is a hard finding, not a stale-annotation warning.

- soak-autofix.yml's header still described the gates as failing closed
  on a cleared window; the fourth and last sibling of that stale premise.
  Expired is a warning, invalid still fails, and the workflow's job is
  convergence rather than rescue.

- Two paths were changed without a test covering them, both added: the
  multi-arg `rustup toolchain install 1.91.0 1.93.0` case that motivated
  replacing the substring msrv match (only the negative case was
  covered), and the `>= 500` retry branch that the retry commit is named
  for (the existing test exercises only the auth fallback).

* fix(compile): survive binary/workspace skew and complete the surfaces a real npm CLI needs

Compiling Socket Firewall (sfw — a TLS-MITM proxy CLI with undici,
node-forge, iovalkey, zod, … in its graph) end-to-end surfaced four
independent blockers. Fixed here:

1. auto-optimize feature skew (driver.rs / freshness.rs): the perry
   binary's baked-in cross-feature list tracks the branch it was BUILT
   from, but the auto-optimize cargo build resolves against the checkout
   on disk. One unknown `perry-runtime/<feat>` failed the whole resolve,
   and the silent prebuilt fallback linked without the routed ext-pump
   entrypoints — undefined-js_* errors two stages from the cause. New
   retain_workspace_declared_features() drops names the checkout's
   perry-runtime / perry-stdlib don't declare (features table + optional
   deps, fail-open on unreadable manifests) before the build stamp is
   computed, and the cargo-failure fallback now says what the
   consequence and remedy are.

2. perry-ext-zlib zstd surface: undici's web-fetch content decoding
   references js_zlib_create_zstd_decompress unconditionally, but only
   perry-stdlib's `compression` module carried the zstd codecs — and
   routing node:zlib to the ext archive strips that feature. Port the
   full surface (create factories, sync/async one-shots, streaming
   write-codec via zstd::stream::write) so the routed archive is
   self-sufficient.

3. class X extends DOMException (codegen + runtime): undici probes
   DOMException inheritability at module load (websocketerror.js), and
   the name was neither in the builtin-parent list nor backed by a
   subclass initializer — the compiled binary died at startup with
   'DOMException is not a function'. Add js_dom_exception_subclass_init
   (stamps message/name/code onto the subclass instance) wired through
   both the explicit super() lowering and the implicit-ctor
   NativeInstanceBase chain walk.

4. panic-runtime dedup for prebuilt (panic=unwind) wrappers co-linked
   with a panic=abort auto-optimized stdlib (strip_dedup.rs): the
   name-containment rule never nominated the wrapper's panic_unwind
   member (stdlib bundles panic_abort under a different name), and the
   localize pass severed the std-cgu → panic_unwind __rust_drop_panic
   edge that abort stdlibs cannot re-provide. Nominate panic_unwind in
   the nosharedeps fixed-point (protected exactly when the stdlib can't
   cover it), and skip localizing panic symbols a sibling member still
   references. Allocator shims stay always-localized: leaving the
   wrapper's system-malloc shim global beats the runtime's mimalloc at
   link and breaks pointer classification (silent console loss).

With these, sfw and sfw-free compile, link, and run as native arm64
binaries straight from their TypeScript entrypoints.

* docs: changelog fragment for #7021

* feat(resolve): full Node.js '#' subpath-imports (package.json "imports") support

Replace the happy-path '#' handling from #5039 with a spec-complete
PACKAGE_IMPORTS_RESOLVE implementation (resolve/subpath_imports.rs):

- package scope walk to the nearest package.json with an "imports"
  object, stopping at node_modules boundaries
- exact keys; '*' wildcard patterns with Node's best-match rule
  (longest prefix, patternKeyCompare tie-break)
- string / fallback-array / conditional-object targets; conditions
  matched in the exports resolver's priority order (perry, node,
  import, module, default, require - node above default)
- bare-package targets re-enter node_modules resolution ('node:'
  builtins included)
- spec rejections with descriptive errors: '#', '#/...', trailing '/',
  and targets or wildcard captures traversing '..'/node_modules or
  escaping the package directory
- perry's TS-first extension probing, so "#lib/*": "./src/lib/*"
  resolves #lib/foo to src/lib/foo.ts

Wired before the tsconfig-paths fallback in resolve_import (spec
resolution outranks aliasing; falls through when no imports map
governs the importer), and into check --check-deps so '#' imports
stop producing false R003 "not found in node_modules" errors.

* docs: key the changelog fragment to PR #7028

* fix: address stacked stdlib review

* fix: address subpath imports review

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants