fix(compile): survive binary/workspace skew; zstd, DOMException-subclass and panic-dedup gaps hit compiling a real npm CLI - #7021
Conversation
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.
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.
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.
…aked 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/.
… 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).
…flows 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.
… 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.
- 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).
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.
- 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.
…ost 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).
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.
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.
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.
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.
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.
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.
…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.
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.
- 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).
… 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.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR fixes native compilation gaps by filtering stale workspace features, preserving zstd symbols, initializing DOMException subclasses, retaining panic runtime members, and wiring callable ChangesRuntime and compiler feature fixes
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (8)
scripts/soak/external-tools.test.mts (1)
113-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrift assertion passes for the wrong reason.
Replacing only 8 hex chars (
sha=deadbeef) makes the line stop matching the 128-hexasset=…;sha=…pattern, so the finding raised is "no asset/sha pin pairs", not the intended sha-mismatch branch. Substituting a full 128-char hex would exercise the comparison you mean to cover.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/soak/external-tools.test.mts` around lines 113 - 119, Update the drift assertion using checkDockerPrebake so the substituted sha remains a valid 128-character hexadecimal value matching the asset/sha pin pattern. Preserve the assertion that the result is non-empty while ensuring it exercises the sha-mismatch comparison branch rather than the missing-pin validation.scripts/soak/external-tools.mts (1)
636-696: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
--fixnever reaches--shims/--install, and--installaccepts a missing operand.Line 645 returns from the check block whenever
--fixis present, so--fix --shimssilently skips shim writing; andargv[++i]!on Line 680 turns a bare trailing--installinto a confusingunknown tool undefined. Neither is hit by the current npm scripts, so this is only worth tightening if the CLI is invoked ad hoc.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/soak/external-tools.mts` around lines 636 - 696, Update the argument flow in the main CLI routine so --fix performs pruning without returning when combined with --shims or --install; only exit the check path when --check is requested, or when --fix is used without actionable install/shim operations. Validate that --install has a following operand before calling installTool, and report the missing tool argument clearly instead of passing undefined.scripts/soak/soak.test.mts (1)
32-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFixtures hard-code the window that the assertions derive from
SOAK_DAYS.
'7 days',min-release-age=7, and10080are literals while other tests compute fromSOAK_DAYS/addDaysIso. ChangingSOAK_DAYSwould leave these tests failing for fixture reasons rather than behavior reasons; deriving them (${SOAK_DAYS} days,SOAK_MINUTES) keeps the suite window-agnostic.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/soak/soak.test.mts` around lines 32 - 55, The soak tests hard-code release-window values instead of deriving them from SOAK_DAYS. Update CLEAN_YAML to use SOAK_MINUTES, and update the cargo and npmrc fixtures/assertions in the corresponding tests to interpolate SOAK_DAYS while preserving the existing mismatch cases and fix expectations.scripts/soak/update-deps.mts (1)
49-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueLoop returns on the first present installer, so the "tried in order" list has no failure fallback.
NPM_INSTALLERSis documented inscripts/soak/paths.mts(Line 46) as "tried in order", but a candidate that exists and then fails ends the function. Alsoargsshadows the outerargsfrom Line 44, which makes the block harder to read. If first-wins is intended, drop the plural framing; otherwise continue on nonzero status.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/soak/update-deps.mts` around lines 49 - 56, The installer loop in the update-deps flow should honor NPM_INSTALLERS’ “tried in order” behavior by continuing to the next candidate when run returns a nonzero status, and only returning success when an installer succeeds. Rename the loop’s destructured args variable to avoid shadowing the outer args, while preserving the existing missing-installer skip and final error return.scripts/soak/soak.mts (2)
494-516: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
--fixcan't repair a missing window key, but the findings advertise it.
fixCargoConfig/fixWorkspaceYamlonly rewrite an existing key, whilecheckCargoConfig(Line 58) andcheckWorkspaceYaml(Line 157) both say "(or run --fix)" for the(missing)case.fixNpmrcalready handles this by appending. Net effect: on a surface missing the key,soak:fixreports nothing fixed and the soak-autofix workflow re-raises the same finding every day with no path to green.Either append the key like
fixNpmrcdoes, or drop "(or run --fix)" from the missing-key hint.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/soak/soak.mts` around lines 494 - 516, Update fixCargoConfig and fixWorkspaceYaml to append their respective missing configuration keys, matching fixNpmrc behavior, while preserving existing-key replacement and file formatting. Ensure checkCargoConfig and checkWorkspaceYaml findings advertising --fix can be resolved by the autofix path.
48-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
SOAK_DAYS = 0opt-out still requires the[unstable]cargo gate.
checkDependabotCooldownshort-circuits onSOAK_DAYS === 0(Line 435), but this check keeps demanding[unstable] min-publish-age = trueplusglobal-min-publish-age = "0 days". The documented "opt out entirely" flow in.claude/skills/soak/SKILL.md(Lines 56-59) then leaves a finding that--fixwon't clear. Consider the same zero-day early return here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/soak/soak.mts` around lines 48 - 71, The checkCargoConfig function should short-circuit when SOAK_DAYS is 0, returning no findings before validating the age or [unstable] Cargo gate. Match the existing early-return behavior used by checkDependabotCooldown so the documented opt-out, including --fix, produces no configuration findings..github/workflows/soak-autofix.yml (1)
113-117: 📐 Maintainability & Code Quality | 🔵 Trivial
gh pr createwith the defaultgithub.tokenneeds the repo/org "Allow GitHub Actions to create and approve pull requests" setting enabled, otherwise this step hard-fails after the push already landed. Worth documenting alongside theSOAK_AUTOFIX_TOKENnote at Lines 41-47.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/soak-autofix.yml around lines 113 - 117, Document alongside the SOAK_AUTOFIX_TOKEN guidance that gh pr create in the workflow requires the repository or organization setting “Allow GitHub Actions to create and approve pull requests” when using the default github.token; note that without it, PR creation fails after the push succeeds..github/zizmor.yml (1)
32-38: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winNarrow
cache-poisoningsuppression withignore:instead ofdisable: true.Zizmor supports per-file ignores, so the rule can stay enabled for non-affected workflows while suppressing only the intended release/publish workflows. A global
disablewould also suppress new workflows that restore a cache while publishing artifacts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/zizmor.yml around lines 32 - 38, Replace the global cache-poisoning disable in the zizmor configuration with an ignore list targeting only the existing release/publish workflow files that trigger this finding. Keep the cache-poisoning rule enabled for all other workflows, including future workflows, and preserve the current suppression scope for the identified artifact-publishing workflows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/security-audit.yml:
- Around line 133-135: Update the skillspector reference in the workflow command
to use the full 40-character commit SHA, matching the exact pinned revision in
external-tools.json and preserving the existing scan invocation.
In @.npmrc:
- Around line 1-9: Add an npm version constraint aligned with the documented npm
11.10+ / 12+ requirement, using the root package metadata so unsupported npm
versions cannot bypass the min-release-age soak gate. Preserve the existing
.npmrc setting and avoid changing the pnpm workspace configuration.
In `@crates/perry-codegen/src/expr/this_super_call.rs`:
- Around line 713-743: Add the same DOMException-specific initialization
handling to the Expr::SuperCallSpread path before it falls through to
js_super_construct_apply. Lower the spread arguments, invoke
js_dom_exception_subclass_init for the current this value using the appropriate
message/name arguments, apply SelfOnly field initializers via
apply_field_initializers_recursive, and return the undefined value consistent
with the existing SuperCall branch.
In `@crates/perry-runtime/src/event_target.rs`:
- Around line 337-358: Update js_dom_exception_subclass_init to root this_value,
message, and name with RuntimeHandleScope before optional_string_from_value or
set_event_field calls. After each operation that may allocate or invoke user
code, reload the exception and coerced string values from their rooted handles
rather than retaining exception, message_ptr, or name_ptr raw pointers across
those operations.
In `@package.json`:
- Around line 4-11: Update package.json to declare the required Node engine
floor of 22.18 or 23.6 or newer, and change the test:scripts command to use
Node’s own test-file matching or directory discovery instead of the
shell-expanded scripts/soak/*.test.mts glob. Keep the existing test target and
command behavior intact across Windows and Unix-like environments.
---
Nitpick comments:
In @.github/workflows/soak-autofix.yml:
- Around line 113-117: Document alongside the SOAK_AUTOFIX_TOKEN guidance that
gh pr create in the workflow requires the repository or organization setting
“Allow GitHub Actions to create and approve pull requests” when using the
default github.token; note that without it, PR creation fails after the push
succeeds.
In @.github/zizmor.yml:
- Around line 32-38: Replace the global cache-poisoning disable in the zizmor
configuration with an ignore list targeting only the existing release/publish
workflow files that trigger this finding. Keep the cache-poisoning rule enabled
for all other workflows, including future workflows, and preserve the current
suppression scope for the identified artifact-publishing workflows.
In `@scripts/soak/external-tools.mts`:
- Around line 636-696: Update the argument flow in the main CLI routine so --fix
performs pruning without returning when combined with --shims or --install; only
exit the check path when --check is requested, or when --fix is used without
actionable install/shim operations. Validate that --install has a following
operand before calling installTool, and report the missing tool argument clearly
instead of passing undefined.
In `@scripts/soak/external-tools.test.mts`:
- Around line 113-119: Update the drift assertion using checkDockerPrebake so
the substituted sha remains a valid 128-character hexadecimal value matching the
asset/sha pin pattern. Preserve the assertion that the result is non-empty while
ensuring it exercises the sha-mismatch comparison branch rather than the
missing-pin validation.
In `@scripts/soak/soak.mts`:
- Around line 494-516: Update fixCargoConfig and fixWorkspaceYaml to append
their respective missing configuration keys, matching fixNpmrc behavior, while
preserving existing-key replacement and file formatting. Ensure checkCargoConfig
and checkWorkspaceYaml findings advertising --fix can be resolved by the autofix
path.
- Around line 48-71: The checkCargoConfig function should short-circuit when
SOAK_DAYS is 0, returning no findings before validating the age or [unstable]
Cargo gate. Match the existing early-return behavior used by
checkDependabotCooldown so the documented opt-out, including --fix, produces no
configuration findings.
In `@scripts/soak/soak.test.mts`:
- Around line 32-55: The soak tests hard-code release-window values instead of
deriving them from SOAK_DAYS. Update CLEAN_YAML to use SOAK_MINUTES, and update
the cargo and npmrc fixtures/assertions in the corresponding tests to
interpolate SOAK_DAYS while preserving the existing mismatch cases and fix
expectations.
In `@scripts/soak/update-deps.mts`:
- Around line 49-56: The installer loop in the update-deps flow should honor
NPM_INSTALLERS’ “tried in order” behavior by continuing to the next candidate
when run returns a nonzero status, and only returning success when an installer
succeeds. Rename the loop’s destructured args variable to avoid shadowing the
outer args, while preserving the existing missing-installer skip and final error
return.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: aa13861a-acfa-4b39-a9b2-1a53fb8fdc53
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.locktools/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (34)
.cargo/config.toml.claude/skills/soak/SKILL.md.github/dependabot.yml.github/workflows/security-audit.yml.github/workflows/soak-autofix.yml.github/workflows/zizmor.yml.github/zizmor.yml.npmrcchangelog.d/6912-security-tooling-soak.mdchangelog.d/7021-real-npm-cli-compile-gaps.mdcrates/perry-codegen/src/expr/this_super_call.rscrates/perry-codegen/src/lower_call/new_helpers.rscrates/perry-codegen/src/runtime_decls/strings_part2.rscrates/perry-ext-zlib/Cargo.tomlcrates/perry-ext-zlib/src/stream.rscrates/perry-runtime/src/event_target.rscrates/perry/src/commands/compile/optimized_libs.rscrates/perry/src/commands/compile/optimized_libs/driver.rscrates/perry/src/commands/compile/optimized_libs/freshness.rscrates/perry/src/commands/compile/optimized_libs/tests.rscrates/perry/src/commands/compile/strip_dedup.rsexternal-tools.jsonpackage.jsonscripts/soak/constants.mtsscripts/soak/external-tools.mtsscripts/soak/external-tools.test.mtsscripts/soak/paths.mtsscripts/soak/soak.mtsscripts/soak/soak.test.mtsscripts/soak/update-deps.mtsscripts/soak/update-deps.test.mtstools/package.jsontools/pnpm-workspace.yamltools/taze.config.mts
…ll 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).
|
Follow-up resolved in this PR: the |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
changelog.d/7021-real-npm-cli-compile-gaps.md (1)
1-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the blocker count.
The heading says “four independent blockers,” but the fragment lists five fixes, including the new RegExp call-form fix. Change “four” to “five” or restructure the bullets so the count is accurate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@changelog.d/7021-real-npm-cli-compile-gaps.md` around lines 1 - 7, The changelog heading count is inconsistent with the five listed fixes. Update the heading in the changelog entry to say “five independent blockers,” preserving all existing bullet content.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/perry-runtime/src/object/global_this/ctor_thunks.rs`:
- Around line 53-63: Update the regexp construction flow in the visible thunk so
the converted pattern and flags strings are rooted using the runtime’s GC handle
mechanism rather than retained only as raw StringHeader pointers. Keep the
pattern handle rooted while js_string_coerce(flags) may allocate or invoke user
code, then reload both current pointers from their handles before calling
js_regexp_new.
---
Outside diff comments:
In `@changelog.d/7021-real-npm-cli-compile-gaps.md`:
- Around line 1-7: The changelog heading count is inconsistent with the five
listed fixes. Update the heading in the changelog entry to say “five independent
blockers,” preserving all existing bullet content.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0a1a9c48-eeb0-4e2c-8296-d7103d68267d
📒 Files selected for processing (4)
changelog.d/7021-real-npm-cli-compile-gaps.mdcrates/perry-runtime/src/object/global_this.rscrates/perry-runtime/src/object/global_this/ctor_thunks.rscrates/perry-runtime/src/object/global_this/populate.rs
# Conflicts: # crates/perry/src/commands/compile/optimized_libs.rs # crates/perry/src/commands/compile/optimized_libs/driver.rs
…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>
Compiling Socket Firewall (
sfw— a TLS-MITM package-manager proxy with undici, node-forge, iovalkey, zod, lodash, … in its module graph) end-to-end surfaced four independent blockers. Each is fixed at its own layer; together they take sfw from 'fails at dependency collection' to a running native arm64 binary.1. Auto-optimize feature skew → silent doomed link
The
perrybinary's baked-in cross-feature list tracks the branch the binary was built from; the auto-optimize cargo build resolves it against the checkout on disk. Oneperry-runtime/<feat>the checkout doesn't declare fails the whole cargo resolve, and the silent prebuilt fallback then links without the ext-pump entrypoints the well-known routing already stripped stdlib features for — surfacing as undefined-js_*link errors two stages away from the cause.Fix:
retain_workspace_declared_features()filtersperry-runtime/*/perry-stdlib/*cross-features against the checkout's declared features (features table + optional deps; fail-open when manifests are unreadable), applied before the build stamp so the stamp keys on what's actually built. Warns with what was dropped and why. The cargo-failure fallback message now states the likely consequence and remedy. Unit tests included.2.
perry-ext-zliblacked the zstd surfaceundici's web-fetch content decoding references
js_zlib_create_zstd_decompressunconditionally. Only perry-stdlib'scompressionmodule carried the zstd codecs — and routingnode:zlibto the ext archive strips that feature, so the link died on that one symbol.Fix: port the full zstd surface into perry-ext-zlib: create factories, sync/async one-shots, and the streaming write-codec (
zstd::stream::write::{Encoder,Decoder}), mirroring stdlib's ABI (declared inruntime_decls). Fallible zstd context allocation degrades to the same buffer-until-end pathcreateUnzipuses.3.
class X extends DOMExceptiondidn't compile to something runnableundici probes DOMException inheritability at module load (
websocketerror.js:class Test extends DOMException { … } new Test()). DOMException was neither in codegen's builtin-parent list nor backed by a subclass initializer, so any binary with undici in its graph died at startup withTypeError: DOMException is not a function.Fix: new runtime
js_dom_exception_subclass_init(stampsmessage/name/codeonto the subclass instance, defaults matchingjs_dom_exception_new), wired through both the explicitsuper()lowering and the implicit-ctorNativeInstanceBasechain walk. Verified with explicit-ctor and no-ctor repros.4. panic-runtime dedup vs panic=abort stdlib
Prebuilt wrapper staticlibs are built
panic=unwind; the auto-optimized stdlib ispanic=abort. Two composing holes instrip_dedup:panic_unwindmember (an abort stdlib bundlespanic_abortunder a different member name), so the stale unwind copy survives;__rust_drop_panicdefinition while the siblingpanic_unwindmember still referenced it — and an abort stdlib has no replacement. Link dies on exactly that symbol.Fix: nominate
panic_unwindin the nosharedeps fixed-point (the existing protection logic keeps it exactly when the stdlib can't cover it — fail-safe), and skip localizing panic symbols a sibling member still references. Allocator shims deliberately stay always-localized: leaving the wrapper's system-malloc shim global would beat the runtime's mimalloc at link and break pointer classification (the known silent-console-loss failure mode) — we hit exactly that regression mid-fix and the comment documents it.Validation
cargo test -p perry --bin perry: 753 passed, 0 failed (includes 2 new tests for the feature filter)sfw(enterprise) andsfw-free: compile → link → run, correct--helpoutput, from their TS entrypoints with 28 npm packages inperry.compilePackagesKnown follow-ups (not in this PR):
sfw-registrystill hits a startupTypeError: Cannot read properties of undefined (reading 'test');perry check --check-depsdoesn't route through the tsconfig-paths fallback thatperry compileuses, so#lib/*-style subpath imports produce false R003 errors.Summary by CodeRabbit
New Features
class X extends DOMExceptionsosuper(message, name)initializes message, name, and error code properly.RegExp(pattern, flags)withoutnewnow correctly creates (or returns) a RegExp.Bug Fixes
Tests