Skip to content

fix(compile): survive binary/workspace skew; zstd, DOMException-subclass and panic-dedup gaps hit compiling a real npm CLI - #7021

Merged
proggeramlug merged 34 commits into
PerryTS:mainfrom
jdalton:fix/auto-optimize-feature-skew
Jul 30, 2026
Merged

fix(compile): survive binary/workspace skew; zstd, DOMException-subclass and panic-dedup gaps hit compiling a real npm CLI#7021
proggeramlug merged 34 commits into
PerryTS:mainfrom
jdalton:fix/auto-optimize-feature-skew

Conversation

@jdalton

@jdalton jdalton commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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 perry binary'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. One perry-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() filters perry-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-zlib lacked the zstd surface

undici's web-fetch content decoding references js_zlib_create_zstd_decompress unconditionally. Only perry-stdlib's compression module carried the zstd codecs — and routing node:zlib to 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 in runtime_decls). Fallible zstd context allocation degrades to the same buffer-until-end path createUnzip uses.

3. class X extends DOMException didn't compile to something runnable

undici 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 with TypeError: DOMException is not a function.

Fix: new runtime js_dom_exception_subclass_init (stamps message/name/code onto the subclass instance, defaults matching js_dom_exception_new), wired through both the explicit super() lowering and the implicit-ctor NativeInstanceBase chain 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 is panic=abort. Two composing holes in strip_dedup:

  • the nosharedeps name-containment rule never nominates the wrapper's panic_unwind member (an abort stdlib bundles panic_abort under a different member name), so the stale unwind copy survives;
  • the localize pass hid the wrapper std-cgu's __rust_drop_panic definition while the sibling panic_unwind member still referenced it — and an abort stdlib has no replacement. Link dies on exactly that symbol.

Fix: nominate panic_unwind in 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) and sfw-free: compile → link → run, correct --help output, from their TS entrypoints with 28 npm packages in perry.compilePackages
  • minimal DOMException-subclass repros (with and without explicit ctor) run correctly

Known follow-ups (not in this PR): sfw-registry still hits a startup TypeError: Cannot read properties of undefined (reading 'test'); perry check --check-deps doesn't route through the tsconfig-paths fallback that perry compile uses, so #lib/*-style subpath imports produce false R003 errors.

Summary by CodeRabbit

  • New Features

    • Added Node-compatible Zstandard (zstd) compression/decompression, including synchronous, asynchronous, and streaming APIs.
    • Added correct support for class X extends DOMException so super(message, name) initializes message, name, and error code properly.
    • Calling RegExp(pattern, flags) without new now correctly creates (or returns) a RegExp.
  • Bug Fixes

    • Improved reliability of optimized native compilation by resolving multiple panic-mode and feature-selection edge cases.
    • Added clearer diagnostics when optimized compilation fails.
  • Tests

    • Added coverage for zstd decoder streaming completion behavior.

jdalton added 24 commits July 28, 2026 00:31
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.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR fixes native compilation gaps by filtering stale workspace features, preserving zstd symbols, initializing DOMException subclasses, retaining panic runtime members, and wiring callable RegExp construction.

Changes

Runtime and compiler feature fixes

Layer / File(s) Summary
DOMException subclass construction
crates/perry-codegen/..., crates/perry-runtime/src/event_target.rs
Adds DOMException-aware lowering for direct and spread super() calls and initializes message, name, and code.
Zstandard zlib support
crates/perry-ext-zlib/*
Adds synchronous, asynchronous, and streaming zstd compression and decompression support.
Compiler build and archive handling
crates/perry/src/commands/compile/..., changelog.d/7021-real-npm-cli-compile-gaps.md
Filters undeclared workspace features and adjusts panic/unwind archive deduplication and fallback diagnostics.
RegExp call-form construction
crates/perry-runtime/src/object/global_this/*, changelog.d/7021-real-npm-cli-compile-gaps.md
Routes callable RegExp through a constructor thunk with arity metadata and the existing-regexp identity shortcut.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels: bug, rust

Suggested reviewers: proggeramlug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main compile/link/runtime fixes, including workspace skew, zstd, DOMException, and panic dedup issues.
Description check ✅ Passed The description is detailed and on-topic, covering the main fixes and validation, though it doesn't follow the template headings exactly.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (8)
scripts/soak/external-tools.test.mts (1)

113-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drift assertion passes for the wrong reason.

Replacing only 8 hex chars (sha=deadbeef) makes the line stop matching the 128-hex asset=…;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

--fix never reaches --shims/--install, and --install accepts a missing operand.

Line 645 returns from the check block whenever --fix is present, so --fix --shims silently skips shim writing; and argv[++i]! on Line 680 turns a bare trailing --install into a confusing unknown 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 value

Fixtures hard-code the window that the assertions derive from SOAK_DAYS.

'7 days', min-release-age=7, and 10080 are literals while other tests compute from SOAK_DAYS/addDaysIso. Changing SOAK_DAYS would 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 value

Loop returns on the first present installer, so the "tried in order" list has no failure fallback.

NPM_INSTALLERS is documented in scripts/soak/paths.mts (Line 46) as "tried in order", but a candidate that exists and then fails ends the function. Also args shadows the outer args from 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

--fix can't repair a missing window key, but the findings advertise it.

fixCargoConfig / fixWorkspaceYaml only rewrite an existing key, while checkCargoConfig (Line 58) and checkWorkspaceYaml (Line 157) both say "(or run --fix)" for the (missing) case. fixNpmrc already handles this by appending. Net effect: on a surface missing the key, soak:fix reports nothing fixed and the soak-autofix workflow re-raises the same finding every day with no path to green.

Either append the key like fixNpmrc does, 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 = 0 opt-out still requires the [unstable] cargo gate.

checkDependabotCooldown short-circuits on SOAK_DAYS === 0 (Line 435), but this check keeps demanding [unstable] min-publish-age = true plus global-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 --fix won'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 create with the default github.token needs 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 the SOAK_AUTOFIX_TOKEN note 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 win

Narrow cache-poisoning suppression with ignore: instead of disable: 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 disable would 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4340bff and e8e529d.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • tools/pnpm-lock.yaml is 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
  • .npmrc
  • changelog.d/6912-security-tooling-soak.md
  • changelog.d/7021-real-npm-cli-compile-gaps.md
  • crates/perry-codegen/src/expr/this_super_call.rs
  • crates/perry-codegen/src/lower_call/new_helpers.rs
  • crates/perry-codegen/src/runtime_decls/strings_part2.rs
  • crates/perry-ext-zlib/Cargo.toml
  • crates/perry-ext-zlib/src/stream.rs
  • crates/perry-runtime/src/event_target.rs
  • crates/perry/src/commands/compile/optimized_libs.rs
  • crates/perry/src/commands/compile/optimized_libs/driver.rs
  • crates/perry/src/commands/compile/optimized_libs/freshness.rs
  • crates/perry/src/commands/compile/optimized_libs/tests.rs
  • crates/perry/src/commands/compile/strip_dedup.rs
  • external-tools.json
  • package.json
  • scripts/soak/constants.mts
  • scripts/soak/external-tools.mts
  • scripts/soak/external-tools.test.mts
  • scripts/soak/paths.mts
  • scripts/soak/soak.mts
  • scripts/soak/soak.test.mts
  • scripts/soak/update-deps.mts
  • scripts/soak/update-deps.test.mts
  • tools/package.json
  • tools/pnpm-workspace.yaml
  • tools/taze.config.mts

Comment thread .github/workflows/security-audit.yml
Comment thread .npmrc
Comment thread crates/perry-codegen/src/expr/this_super_call.rs
Comment thread crates/perry-runtime/src/event_target.rs Outdated
Comment thread package.json
jdalton added 2 commits July 29, 2026 15:39
…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).
@jdalton

jdalton commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up resolved in this PR: the sfw-registry startup TypeError: Cannot read properties of undefined (reading 'test') traced to the globalThis RegExp sentinel — lodash's runInContext rebinds it (var RegExp = context.RegExp) and uses the call form to build reIsNative; the sentinel fell through to the noop thunk and returned undefined. Added regexp_constructor_call_thunk (ECMA-262 22.2.4 semantics incl. the RegExp(re) identity shortcut). All three Socket Firewall binaries (sfw, sfw-free, sfw-registry) now compile and run. 753/753 unit tests still pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Update 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

📥 Commits

Reviewing files that changed from the base of the PR and between e8e529d and 8417353.

📒 Files selected for processing (4)
  • changelog.d/7021-real-npm-cli-compile-gaps.md
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/ctor_thunks.rs
  • crates/perry-runtime/src/object/global_this/populate.rs

Comment thread crates/perry-runtime/src/object/global_this/ctor_thunks.rs Outdated
@proggeramlug
proggeramlug merged commit dbf3c88 into PerryTS:main Jul 30, 2026
6 checks passed
proggeramlug pushed a commit 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