Protect Against Supply-Chain Attacks with Release-Age Cooldown
Fromager’s release-age cooldown policy rejects package versions that were published fewer than a configured number of days ago. This protects automated builds from supply-chain attacks where a malicious version is published and immediately pulled in before it can be reviewed.
How It Works
When a cooldown is active, any candidate whose upload-time is more recent
than the cutoff (current time minus the configured minimum age) is not
considered a valid option during constraint resolution. If no versions of a
package satisfy both the cooldown window and any other provided constraints,
resolution fails with an informative error.
The cutoff timestamp is fixed at the start of each run, so all package resolutions within a single bootstrap share the same boundary.
Enabling the Cooldown
Use the global --min-release-age flag, or set the equivalent environment
variable FROMAGER_MIN_RELEASE_AGE:
# Reject versions published in the last 7 days
fromager --min-release-age 7 bootstrap -r requirements.txt
# Same, via environment variable (useful for CI and builder integrations)
FROMAGER_MIN_RELEASE_AGE=7 fromager bootstrap -r requirements.txt
# Disable the cooldown (default)
fromager --min-release-age 0 bootstrap -r requirements.txt
The --min-release-age flag accepts a non-negative integer number of days.
A value of 0 (the default) disables the check entirely.
Scope
The cooldown applies to both sdist resolution and pre-built wheel
resolution — any candidate whose upload-time is more recent than the
cutoff is rejected, regardless of whether it is an sdist or a prebuilt wheel.
The following are not subject to the cooldown:
Fromager’s internal build and cache wheel servers. These are not used for version selection — they are checked only for already-resolved pinned versions — so the cooldown has no insertion point.
Packages resolved from Git URLs. Git timestamps are set by the client, not the server, and cannot be trusted for cooldown enforcement.
Resolution from a private package index (sdist or wheel) depends on
upload-time being present in the index’s PEP 691 JSON responses. If the
index does not provide that metadata, candidates are rejected under the
fail-closed policy described below. Use resolver_dist.min_release_age: 0
to bypass cooldown for packages from indexes that structurally cannot supply
timestamps.
Fail-Closed Behavior
If a candidate has no upload-time metadata — whether it is an sdist or a
wheel — it is rejected when a cooldown is active. Fromager uses the
PEP 691 JSON Simple API when fetching package metadata, which reliably
includes upload timestamps for PyPI.org.
For indexes that only implement the PEP 503 HTML API and cannot supply
timestamps, use the per-package resolver_dist.min_release_age: 0 override
to bypass the cooldown for affected packages rather than disabling it globally.
Note
If you are writing a get_resolver_provider plugin that uses
PyPIProvider with a private index that only
implements the PEP 503 HTML API, pass supports_upload_time=False to
PyPIProvider. This switches the provider from fail-closed to
warn-and-skip, so candidates without upload timestamps are skipped with a
warning rather than causing resolution to fail.
Example
Given a package example-pkg with three available versions:
2.0.0— published 3 days ago1.9.0— published 45 days ago1.8.0— published 120 days ago
With a 7-day cooldown, 2.0.0 is blocked and 1.9.0 is selected:
fromager --min-release-age 7 bootstrap example-pkg
With a 60-day cooldown, both 2.0.0 and 1.9.0 are blocked and 1.8.0
is selected:
fromager --min-release-age 60 bootstrap example-pkg
Per-Package Override
The cooldown can be adjusted on a per-package basis using the
resolver_dist.min_release_age setting in the package’s settings file:
# overrides/settings/my-package.yaml
resolver_dist:
min_release_age: 0 # disable cooldown for this package
# min_release_age: 30 # or use a different number of days
Valid values:
Omit the key (default): inherit the global
--min-release-agesetting.0: disable the cooldown for this package, regardless of the global flag.Positive integer: use this many days instead of the global setting.
This is useful when a specific package is trusted enough to allow recent versions, or when a package’s release cadence makes the global cooldown impractical.
Top-Level == Pin Exemption
Top-level requirements that are a single exact == pin
(e.g. torch==2.5.1) bypass the cooldown automatically – the operator
has explicitly chosen that version.
Wildcard or compound specifiers (for example ==1.* or ==1.0,>0.9)
are not exempt.
== specifiers in transitive dependencies are also not exempt;
without this distinction a malicious package could pin its own dependencies
to bypass cooldown.