You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
spec.overrides and spec.constraints (added in #669) are static. When renovate bumps the parent package, nothing checks whether the override should stay, move, or go away. Raised while reviewing #469, where the override happened to remain correct across the bump, but only because the upstream pin did not move.
What existing CI already catches
The Grype gate runs on every bump, so these are covered today:
Override became insufficient — a new advisory affects our target version. The gate fails.
Override became incompatible — the new parent version cannot work with our forced version. The build or scan fails.
What nothing catches
Silent downgrade. Upstream raises its own floor above our pin, and because npx overrides are exact versions, ours drags it back down. The gate still passes, so this is invisible and is the only case that causes active harm.
Lingering unnecessary override. Upstream fixed their pin and ours is now dead weight. The gate passes either way, so it just accumulates.
The two kinds behave differently
Worth separating, because it concentrates the risk:
npx overrides are exact pins (version: "1.26.0"). They go stale and can downgrade.
uvx constraints are usually floors (cryptography>=50.0.0, mcp[cli]>=1.28.1,<2). A floor cannot downgrade anything, and insufficiency is already caught by the gate.
So the problem is almost entirely on the npx side. Current inventory is 5 specs and 6 overrides:
Spec
Override
Direct or transitive
npx/brightdata-mcp
@modelcontextprotocol/sdk 1.26.0
direct
npx/mcp-jetbrains
@modelcontextprotocol/sdk 1.26.0
direct
npx/onchain-mcp
@modelcontextprotocol/sdk 1.26.0
direct
npx/astra-db-mcp
undici 6.28.0
transitive, via @actions/http-client
npx/browserbase-mcp-server
sharp 0.35.3
direct
npx/browserbase-mcp-server
undici 6.28.0
transitive, via @ai-sdk/provider-utils
Proposed starting point: a downgrade check
Deterministic, cheap, and it targets the one failure mode that silently causes harm.
Reading what upstream declares is not sufficient, because two of the six overrides are transitive and the parent does not declare them at all. The approach that handles both uniformly is to resolve the tree without the override and compare:
That takes about 11 seconds for two servers and downloads no tarballs, so it is viable in CI.
One design subtlety found while prototyping. The lockfile resolves a package to a set of versions, not one, because npm keeps multiple copies. For the two servers above:
@modelcontextprotocol/sdk resolves to both 1.21.2 and 1.30.0
undici resolves to both 5.29.0 and 7.29.0
npm overrides are tree-global, so undici: 6.28.0 is simultaneously an upgrade of the 5.29.0 copy and a downgrade of the 7.29.0 one. A scalar "resolved vs pinned" comparison would be wrong. The check should:
Resolve without overrides and collect every version of the overridden package in the tree.
Report which copies the override raises and which it lowers.
Flag when the pin sits below the highest version otherwise present, since that is the downgrade case.
Open question for implementation: whether that flag should hard-fail the build or post a comment. A legitimate case exists where upstream's floor exceeds our pin and our pin is still the security-correct target, which is exactly why undici was pinned to the last 6.x rather than 7.x. Leaning toward a comment plus a label, with the Grype gate remaining the only hard gate.
Follow-ons, not part of the starting point
A renovate customManager for npx override versions. Idiomatic here: renovate.json already has 4, all matching spec: -> package: -> version: per protocol directory, and an override entry is the same shape. That turns "nothing re-assesses the target" into "the target is bumped like any other dependency", with the Grype gate as the safety net. Deliberately not for uvx constraints, since rewriting inside a PEP 508 range is fiddly and a floor does not go stale the same way.
A periodic necessity audit. Answering "is this override still needed?" means building without it and comparing, which is too expensive per-PR. Better as a weekly report, and it could hang off the periodic scan fixed in fix(ci): make the periodic security scan actually report findings #834.
Rejected
A blocking comment on every renovate PR touching an override-bearing spec. It would only fire on 5 specs so it is not especially noisy, but it is toil: a human re-deriving by hand what the check above can answer mechanically.
spec.overridesandspec.constraints(added in #669) are static. When renovate bumps the parent package, nothing checks whether the override should stay, move, or go away. Raised while reviewing #469, where the override happened to remain correct across the bump, but only because the upstream pin did not move.What existing CI already catches
The Grype gate runs on every bump, so these are covered today:
What nothing catches
The two kinds behave differently
Worth separating, because it concentrates the risk:
overridesare exact pins (version: "1.26.0"). They go stale and can downgrade.constraintsare usually floors (cryptography>=50.0.0,mcp[cli]>=1.28.1,<2). A floor cannot downgrade anything, and insufficiency is already caught by the gate.So the problem is almost entirely on the npx side. Current inventory is 5 specs and 6 overrides:
npx/brightdata-mcp@modelcontextprotocol/sdk1.26.0npx/mcp-jetbrains@modelcontextprotocol/sdk1.26.0npx/onchain-mcp@modelcontextprotocol/sdk1.26.0npx/astra-db-mcpundici6.28.0@actions/http-clientnpx/browserbase-mcp-serversharp0.35.3npx/browserbase-mcp-serverundici6.28.0@ai-sdk/provider-utilsProposed starting point: a downgrade check
Deterministic, cheap, and it targets the one failure mode that silently causes harm.
Reading what upstream declares is not sufficient, because two of the six overrides are transitive and the parent does not declare them at all. The approach that handles both uniformly is to resolve the tree without the override and compare:
That takes about 11 seconds for two servers and downloads no tarballs, so it is viable in CI.
One design subtlety found while prototyping. The lockfile resolves a package to a set of versions, not one, because npm keeps multiple copies. For the two servers above:
@modelcontextprotocol/sdkresolves to both1.21.2and1.30.0undiciresolves to both5.29.0and7.29.0npm
overridesare tree-global, soundici: 6.28.0is simultaneously an upgrade of the 5.29.0 copy and a downgrade of the 7.29.0 one. A scalar "resolved vs pinned" comparison would be wrong. The check should:Open question for implementation: whether that flag should hard-fail the build or post a comment. A legitimate case exists where upstream's floor exceeds our pin and our pin is still the security-correct target, which is exactly why
undiciwas pinned to the last 6.x rather than 7.x. Leaning toward a comment plus a label, with the Grype gate remaining the only hard gate.Follow-ons, not part of the starting point
customManagerfor npx override versions. Idiomatic here:renovate.jsonalready has 4, all matchingspec: -> package: -> version:per protocol directory, and an override entry is the same shape. That turns "nothing re-assesses the target" into "the target is bumped like any other dependency", with the Grype gate as the safety net. Deliberately not for uvx constraints, since rewriting inside a PEP 508 range is fiddly and a floor does not go stale the same way.Rejected
A blocking comment on every renovate PR touching an override-bearing spec. It would only fire on 5 specs so it is not especially noisy, but it is toil: a human re-deriving by hand what the check above can answer mechanically.