Skip to content

fix(fix-target): never recommend a yanked or deprecated release (FX-H5) - #976

Merged
runyourempire merged 3 commits into
mainfrom
fix/fx-h5-withdrawn-fix-targets
Oct 17, 2026
Merged

runyourempire merged 3 commits into
mainfrom
fix/fx-h5-withdrawn-fix-targets

Conversation

@runyourempire

@runyourempire runyourempire commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem (FX-H5)

Fix targets came only from OSV bounds (fourda-depengine/src/fix_target.rs). Nothing checked whether the registry had withdrawn a release:

  • lodash 4.17.21 was told "update to >= 4.18.0". npm view lodash@4.18.0 deprecated returns "Bad release. Please use lodash@4.17.21 instead." 4.18.1 is the latest release, it is clean, and it is not deprecated.
  • A yanked crate target breaks the command outright, because Cargo commands name the exact version (cargo update -p ... --precise <target>).
  • Shared vector ft-007 required "4.18.0", so the test suite enforced the defect.

Fix

Pure crate (crates/fourda-depengine)

  • New input registry::RegistryReleases { published, excluded }.
  • The walk skips excluded releases exactly as it skips pre-releases.
  • published joins the advisory-named releases, so the walk can name a release no advisory mentions.
  • If a window's only exits are withdrawn, it has no way out. The result then has no target, and WalkOutcome.withheld names the withdrawn releases. The engine never returns an excluded version.
  • installable_fix / installable_line_fix apply the same rule to line fixes and to an advisory's listed fix (the unconfirmed-finding fallback).
  • match_inventory_with takes a RegistryIndex. MatchedDependency.withheld is serde default and skipped when empty.
  • The JSON API (fix_target, match_inventory) accepts an optional registry.
  • The crate stays pure: no I/O, and the wasm module has 0 imports.

App

  • New osv::release_facts. After each OSV sync, for packages with a version-confirmed advisory only, it reads:

    Registry Request Excluded
    crates.io /api/v1/crates/{crate} yanked
    PyPI /pypi/{pkg}/json a release whose every file is yanked
    npm /{pkg}, abbreviated packument deprecated, but only when a later, non-deprecated, stable release exists on the same caret line (same major; same minor for 0.x). A package whose latest is deprecated excludes nothing.
  • Why the npm rule is narrow (review B1): npm deprecated is often a whole-major "please upgrade" notice, not a withdrawn release. glob deprecates 160 of 168 versions and tar every 6.x. Excluding all of them sent glob 10.4.5 to 12.0.0 and tar 6.1.11 to 7.5.8, major jumps that break the per-line contract.

  • Each registry is read only while its source toggle (npm_registry, crates_io, pypi) is enabled (S3).

  • Only crates.io requests are paced, 1.1 s apart (S4).

  • Requests get a 90 s budget, because next's packument is about 26 MB and boto3's PyPI JSON about 3.3 MB (S5).

  • PyPI still uses the JSON API rather than PEP 691 Simple JSON. PEP 691 files carry no version field, so I would have to parse wheel and sdist filenames; that is a follow-up if releases is ever removed.

  • The morning audit (G4 version_is_true) accepts versions listed in the stored registry facts (S2).

  • The OSV live feed text annotates a withdrawn "Fixed in" release: (withdrawn by the registry; use X) (nit 7).

  • Go retract is not covered (it needs the newest version's go.mod) and is documented as such.

  • Facts are cached in kv_store: fresh for 24 h, used for at most 30 d, at most 60 packages per sync.

  • If the facts are unavailable (offline, never fetched), behaviour is exactly as before: targets come from the advisory data. A stale published list can only make a target higher, never vulnerable.

  • No new hosts. NETWORK.md §2c and the §4 derived-data table list the new paths, including the npm /{package} path for the desktop app.

  • Upgrade Plan wording: when every fixing release is withdrawn, the row reads "No installable fix for X" and its note names the withdrawn release. It never says "No fix published" in that case, and never prints the withdrawn version as a target.

  • The scorer's security-verdict fixed_version goes through the same rule.

Before / after

Built wasm engine, pinned OSV records, live registry facts (2026-10-16):

npm lodash 4.17.21
  before: target=["4.18.0"] finding_fixes=["4.18.0","4.17.23"] withheld=[]
  after:  target=["4.18.1"] finding_fixes=["4.18.1","4.17.23"] withheld=["4.18.0"]
crates.io h2 0.3.16   (RUSTSEC-2023-0034 + GHSA twin, fixed 0.3.17, which is yanked)
  before: target=["0.3.17"] finding_fixes=["0.3.17"] withheld=[]
  after:  target=["0.3.18"] finding_fixes=["0.3.18"] withheld=["0.3.17"]

Review B1, live npm data on 2026-10-17, same engine. "first revision" means every deprecated release excluded; "now" means the caret-line rule:

Package Advisory First revision Now
glob 10.4.5 GHSA-5j98-mcp5-4vw2 12.0.0 10.5.0
tar 6.1.11 GHSA-f5x3-32g6-xq36 7.5.8 6.2.1
lodash 4.17.21 GHSA-f23m-r3pf-42rh 4.18.1 4.18.1

End-to-end through the DB:

  • upgrade_plan_tests::a_deprecated_fix_release_is_never_the_plan_target: the title reads >= 4.18.1, and no item or work-order step contains 4.18.0.
  • every_fixing_release_withdrawn_reads_no_installable_fix: the title reads "No installable fix for h2".

Vector diff (src-tauri/tests/conformance/vectors/fix_target.jsonl)

Line Change
ft-007 (changed) "target":"4.18.0" becomes "target":"4.18.1". Adds "published":["4.17.23","4.18.0","4.18.1"] and "excluded":["4.18.0"]. The rule and source text are extended.
ft-062 (new) h2 crates.io 0.3.16, records [GHSA-f8vr-r385-rh5r, RUSTSEC-2023-0034]. Excluded ["0.3.17"] (yanked), published 0.3.17..0.4.20. Target 0.3.18, clears true.
ft-063 (new) brace-expansion npm 3.0.2, record [GHSA-mh99-v99m-4gvg]. Excluded ["3.0.3"] (deprecated "contains bloat files"). Target 3.0.4, clears true.
ft-064 (new, by hand) The ft-062 records with published ["0.3.17"] and excluded ["0.3.17"]. Target null, withheld:["0.3.17"].
ft-067 (new) glob npm 10.4.5, record [GHSA-5j98-mcp5-4vw2], excluded [] (a whole-major notice is not a withdrawal). Target 10.5.0, clears true.
ft-068 (new) tar npm 6.1.11, record [GHSA-f5x3-32g6-xq36], excluded 7.0.0..7.5.7 (7.5.8 is clean). Target 6.2.1, clears true.

TypeScript engine: the exact changed and new lines went to the peer session 4da-b0 before commit, and their final ids after the renumbering. They will mirror the npm rule exactly. The TS side lands with their MCP 6.2.5 integration PR. Until then the MCP runner will disagree on ft-007, ft-062..064 and ft-067/068.

Verification

Crate CI steps (crates/fourda-depengine):

  • cargo fmt --check: ok
  • cargo clippy --locked -- -D warnings: ok
  • cargo clippy --locked --target wasm32-unknown-unknown -- -D warnings: ok
  • cargo test --locked: 36 lib + 2 integration tests pass
  • wasm build plus scripts/check-wasm.cjs: 0 imports, all checks ok, wasm == native on the corpus case. A new check covers a withdrawn fix.
  • check-lock-drift: ok

Targeted src-tauri tests after rebasing onto aaba1438e (CARGO_TARGET_DIR=C:\cargo-targets\fxh5). The fix_path, fix_target and matching counts are from the first round; nothing those filters cover changed in the review round.

Filter Passed Ignored
osv:: 196 2
vectors 12 0
release_facts 12 0
osv_live 5 0
morning 37 2
fix_path 17 0
fix_target 18 0
matching 68 0
upgrade_plan 37 2
evidence:: 168 4
preemption 112 3
brief_facts 34 2
security_verdict 11 0
  • cargo clippy --lib -D warnings: clean.
  • --all-targets reports only lints in files this PR does not touch, or on lines it does not touch.
  • The full cargo test --lib was not run here (machine load); the merge-queue CI runs it.

Notes for reviewers

  • Files owned by 4da-b0 (fix_path.rs, parent_hint.rs, scanner.rs, brief_facts_copies.rs) are untouched.
  • morning_audit.rs (where t2 has a follow-up) gains one clause in version_is_true; 4da-b0 was told the exact hunk.
  • preemption.rs, identity.rs and three test files only gain withheld: Vec::new(), in MatchedDependency test literals.
  • Live verification against the running app was not done (no app started, per instructions). The registry fetch is exercised by unit tests on trimmed real responses and by the live wasm run above.

🤖 Generated with Claude Code

https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7

runyourempire added a commit that referenced this pull request Oct 17, 2026
…review of #976

B1: npm `deprecated` is often a whole-major "please upgrade" notice, not a
withdrawn release (glob deprecates 160 of 168 versions; tar every 6.x).
Excluding them all sent glob 10.4.5 (GHSA-5j98-mcp5-4vw2) to 12.0.0 and
tar 6.1.11 to 7.5.8. A deprecated npm release is now excluded only when a
later, non-deprecated, stable release exists on its caret line (same
major; same minor for 0.x); a package whose `latest` is deprecated
excludes nothing. lodash 4.18.0 and brace-expansion 3.0.3 stay excluded.
New vectors ft-065 (glob 10.4.5 -> 10.5.0) and ft-066 (tar 6.1.11 ->
6.2.1) from live data, 2026-10-17.

S2: morning audit (G4) accepts a version the stored registry facts list,
so a registry-only target such as lodash 4.18.1 is not a version fault.
S3: each registry is read only while its source (npm_registry, crates_io,
pypi) is enabled; NETWORK.md says so.
S4: only crates.io requests are paced (1.1 s apart).
S5: 90 s request budget for large documents (next ~26 MB, boto3 ~3.3 MB).
Nits: no default client on a builder error; OSV live feed text annotates a
withdrawn "Fixed in" release with the one to use; the security verdict
reads registry facts once per verdict.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
@runyourempire
runyourempire force-pushed the fix/fx-h5-withdrawn-fix-targets branch from 722db8e to 6befaa4 Compare October 17, 2026 03:23
runyourempire and others added 3 commits October 18, 2026 00:46
lodash 4.17.21 was told "update to >= 4.18.0"; npm marks 4.18.0
deprecated ("Bad release"), 4.18.1 is the release to take. A yanked
crate target would make `cargo update --precise` fail outright.

- fourda-depengine: new `registry::RegistryReleases {published, excluded}`
  input. The walk skips excluded releases exactly as it skips
  pre-releases, can step to any published release, and reports the
  withdrawn releases it passed over (`WalkOutcome.withheld`). A window
  whose every exit is withdrawn has no way out: no target, honestly
  named. `installable_fix` / `installable_line_fix` apply the same rule to
  line fixes and listed fixes. `match_inventory_with` takes a
  `RegistryIndex`; `MatchedDependency.withheld` carries the names. JSON
  API ops accept an optional `registry`. Crate stays pure (wasm: 0 imports).
- app: `osv::release_facts` reads crates.io `yanked`, PyPI `yanked` (all
  files) and npm `deprecated` (abbreviated packument; not when `latest` is
  deprecated too) for packages with a confirmed advisory, after each OSV
  sync, cached in kv_store (24 h fresh, 30 d max). Go `retract` is not
  covered. Offline / never fetched: advisory-data-only, as before.
- Upgrade Plan: when every fixing release is withdrawn the row reads
  "No installable fix" and names the withdrawn release, never "No fix
  published". The scorer's security verdict fix goes through the same rule.
- Vectors: ft-007 now 4.18.1 (excluded 4.18.0); new ft-062 (h2 0.3.17
  yanked -> 0.3.18), ft-063 (brace-expansion 3.0.3 deprecated -> 3.0.4),
  ft-064 (all clearing releases withdrawn -> no target, withheld named).
  Optional `published`/`excluded`/`withheld` fields documented in the
  vectors README. NETWORK.md lists the new registry reads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
…review of #976

B1: npm `deprecated` is often a whole-major "please upgrade" notice, not a
withdrawn release (glob deprecates 160 of 168 versions; tar every 6.x).
Excluding them all sent glob 10.4.5 (GHSA-5j98-mcp5-4vw2) to 12.0.0 and
tar 6.1.11 to 7.5.8. A deprecated npm release is now excluded only when a
later, non-deprecated, stable release exists on its caret line (same
major; same minor for 0.x); a package whose `latest` is deprecated
excludes nothing. lodash 4.18.0 and brace-expansion 3.0.3 stay excluded.
New vectors ft-065 (glob 10.4.5 -> 10.5.0) and ft-066 (tar 6.1.11 ->
6.2.1) from live data, 2026-10-17.

S2: morning audit (G4) accepts a version the stored registry facts list,
so a registry-only target such as lodash 4.18.1 is not a version fault.
S3: each registry is read only while its source (npm_registry, crates_io,
pypi) is enabled; NETWORK.md says so.
S4: only crates.io requests are paced (1.1 s apart).
S5: 90 s request budget for large documents (next ~26 MB, boto3 ~3.3 MB).
Nits: no default client on a builder error; OSV live feed text annotates a
withdrawn "Fixed in" release with the one to use; the security verdict
reads registry facts once per verdict.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
…68 (#972 holds ft-065/066)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
@runyourempire
runyourempire force-pushed the fix/fx-h5-withdrawn-fix-targets branch from 66fa88f to 78cf9b2 Compare October 17, 2026 14:46
@runyourempire
runyourempire added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit 026bbfd Oct 17, 2026
25 checks passed
@runyourempire
runyourempire deleted the fix/fx-h5-withdrawn-fix-targets branch October 17, 2026 15:07
runyourempire added a commit that referenced this pull request Oct 17, 2026
…ry (audit H1)

A vulnerable transitive copy whose parent pins it said "upgrade express
(check that it admits path-to-regexp >= 0.1.13)" and stopped. The registry
already knew the answer. The plan, Preemption and the Brief now name it,
by the rules of @4da/mcp-server fix/planner-advice-625 (1840dab):

- the smallest parent release above the installed one whose own
  requirement resolves to a clean copy at or above the target, clear of
  the parent's own advisories (osv/parent_release.rs, pure);
- one release per dependency: a parent with its own vulnerable row has
  that row raised to the highest release the transitive rows need from it
  (no major crossed, no own advisory affects it), and a lower row is
  lifted to it when it still admits a clean copy;
- the refresh after the install when the new requirement still admits
  the installed copy (npm 10.9.2 keeps it);
- a parent that cannot be tied to a declared dependency gets its release
  but no command; a command goes through osv::direct_upgrade (declared
  operator kept); a major move is flagged;
- a parent reached through a declared dependency names that dependency's
  release that brings a fixed parent in (adjacent lever).

Data: the npm abbreviated document #976 already reads now also keeps,
for the parents that pin a vulnerable copy (and their declared lever),
each release's requirement on that package plus the latest tag. Same
request, same cap, cache, source toggle; NETWORK.md says so. npm only.

NodeGoat (express 4.16.4): the express, path-to-regexp and qs rows all
name express 4.22.3 (qs forces it over 4.22.0).

New shared vector kind parent_release (pr-001..pr-006), pinned to live
registry and OSV data of 2026-10-18, run against the real functions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
runyourempire added a commit that referenced this pull request Oct 17, 2026
…y; the Brief and the plan name the same one

- SF1: with_direct_fix gets the installed version only from a
  version-confirmed copy. Without one, the version, is_direct and the
  target are the alert's fallbacks (the raw highest OSV fix, which skips
  the withdrawn-release rule of #976), and the Upgrade Plan names no
  command there either.
- SF2: the verbatim-quote version check covers npm, Cargo and Go commands
  and the PyPI manifest edit.
- SF3: one seeded project, built by both builders: the Brief's direct row
  and the Upgrade Plan's direct row print the same command; with no
  confirmed copy the Brief names none.
- A changed target still changes the act-now signature; no installed
  version or an unread ecosystem names nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KSXWNneouUjTiVV3Rc3FH
runyourempire added a commit that referenced this pull request Oct 18, 2026
…ry (audit H1)

A vulnerable transitive copy whose parent pins it said "upgrade express
(check that it admits path-to-regexp >= 0.1.13)" and stopped. The registry
already knew the answer. The plan, Preemption and the Brief now name it,
by the rules of @4da/mcp-server fix/planner-advice-625 (1840dab):

- the smallest parent release above the installed one whose own
  requirement resolves to a clean copy at or above the target, clear of
  the parent's own advisories (osv/parent_release.rs, pure);
- one release per dependency: a parent with its own vulnerable row has
  that row raised to the highest release the transitive rows need from it
  (no major crossed, no own advisory affects it), and a lower row is
  lifted to it when it still admits a clean copy;
- the refresh after the install when the new requirement still admits
  the installed copy (npm 10.9.2 keeps it);
- a parent that cannot be tied to a declared dependency gets its release
  but no command; a command goes through osv::direct_upgrade (declared
  operator kept); a major move is flagged;
- a parent reached through a declared dependency names that dependency's
  release that brings a fixed parent in (adjacent lever).

Data: the npm abbreviated document #976 already reads now also keeps,
for the parents that pin a vulnerable copy (and their declared lever),
each release's requirement on that package plus the latest tag. Same
request, same cap, cache, source toggle; NETWORK.md says so. npm only.

NodeGoat (express 4.16.4): the express, path-to-regexp and qs rows all
name express 4.22.3 (qs forces it over 4.22.0).

New shared vector kind parent_release (pr-001..pr-006), pinned to live
registry and OSV data of 2026-10-18, run against the real functions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
runyourempire added a commit that referenced this pull request Oct 18, 2026
…aming a command toward it

A newly published malicious (OSV MAL- / GitHub malware) or advisory-affected
minor inside a declared range was announced in the stack-change stream with a
ready-to-run `npm update`, the command that installs it; the Brief's upgrade
facts had the same blind spot (release_grade never consults advisories).

osv/release_advisories.rs checks the release itself with the predicate behind
every "this version is clear" (check_version_affected over the package's
stored advisories, conservative on unreadable windows), never a registry-
withdrawn release (#976), and an all-clear only from a mirror that is ready
(osv::readiness) and synced after the release was published; otherwise the
status is unknown.

Stack-change stream: affected -> no command, title/explanation name the
advisories (malware: "do not install", urgency High, says a refresh would
pull it in when the range admits it), osv citations; when every affecting
advisory already hits the installed copy the item is left to the advisory
lane; withdrawn / unknown -> no command, said so. Brief: affected or
withdrawn releases are not presented as upgrades.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
runyourempire added a commit that referenced this pull request Oct 18, 2026
…ry (audit H1)

A vulnerable transitive copy whose parent pins it said "upgrade express
(check that it admits path-to-regexp >= 0.1.13)" and stopped. The registry
already knew the answer. The plan, Preemption and the Brief now name it,
by the rules of @4da/mcp-server fix/planner-advice-625 (1840dab):

- the smallest parent release above the installed one whose own
  requirement resolves to a clean copy at or above the target, clear of
  the parent's own advisories (osv/parent_release.rs, pure);
- one release per dependency: a parent with its own vulnerable row has
  that row raised to the highest release the transitive rows need from it
  (no major crossed, no own advisory affects it), and a lower row is
  lifted to it when it still admits a clean copy;
- the refresh after the install when the new requirement still admits
  the installed copy (npm 10.9.2 keeps it);
- a parent that cannot be tied to a declared dependency gets its release
  but no command; a command goes through osv::direct_upgrade (declared
  operator kept); a major move is flagged;
- a parent reached through a declared dependency names that dependency's
  release that brings a fixed parent in (adjacent lever).

Data: the npm abbreviated document #976 already reads now also keeps,
for the parents that pin a vulnerable copy (and their declared lever),
each release's requirement on that package plus the latest tag. Same
request, same cap, cache, source toggle; NETWORK.md says so. npm only.

NodeGoat (express 4.16.4): the express, path-to-regexp and qs rows all
name express 4.22.3 (qs forces it over 4.22.0).

New shared vector kind parent_release (pr-001..pr-006), pinned to live
registry and OSV data of 2026-10-18, run against the real functions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018hjX3pEMmC7CHfG41PiPj7
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.

1 participant