Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ minimum_release_age = "7d" # only install versions released more than 7 days ag
Supports relative durations (`7d`, `6mo`, `1y`) and absolute dates (`2024-06-01`). For most
backends, this only affects fuzzy version resolution, such as `node@20` or `latest`.
Explicitly pinned versions like `node@22.5.0` bypass the filter.
During ordinary toolset resolution, already-installed fuzzy matches remain eligible:
`minimum_release_age` limits remote version selection and does not make an installed version
inactive. Lockfile generation may re-check fuzzy installed matches against release metadata.

Capability depends on the backend:

Expand Down
2 changes: 1 addition & 1 deletion e2e/cli/test_lock
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ output=$(mise lock --before 2019-01-01 --platform linux-x64 --dry-run 2>&1)
assert_contains "echo '$output'" "jq@1.7.1 for linux-x64"

rm -f mise.lock
mkdir -p "$MISE_DATA_DIR/installs/jq/1.7.1"
cat <<EOF >mise.toml
[settings]
install_before = "2019-01-01"
Expand Down Expand Up @@ -139,7 +140,6 @@ output=$(mise lock --minimum-release-age 2019-01-01 --platform linux-x64 --dry-r
assert_contains "echo '$output'" "jq@1.6 for linux-x64"
assert_not_contains "echo '$output'" "jq@1.7.1"

mkdir -p "$MISE_DATA_DIR/installs/jq/1.7.1"
output=$(mise lock --minimum-release-age 2019-01-01 --platform linux-x64 --dry-run 2>&1)
assert_contains "echo '$output'" "jq@1.6 for linux-x64"
assert_not_contains "echo '$output'" "jq@1.7.1"
Expand Down
43 changes: 43 additions & 0 deletions e2e/cli/test_minimum_release_age_installed_resolution
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash

# minimum_release_age gates remote version selection. It must not make an
# already-installed version ineligible during ordinary toolset resolution.

marker="$HOME/remote-versions-fetched"
cp -RL "$MISE_DATA_DIR/plugins/dummy" "$MISE_DATA_DIR/plugins/spy"

mise install spy@1.0.0
mise install spy@2.0.0

# Make a remote lookup observable and fatal. Ordinary resolution should use
# the latest installed match without calling this script, even with an
# explicit release-age policy.
cat >"$MISE_DATA_DIR/plugins/spy/bin/list-all" <<EOF
#!/usr/bin/env bash
touch "$marker"
exit 1
EOF
cat >"$MISE_DATA_DIR/plugins/spy/bin/latest-stable" <<EOF
#!/usr/bin/env bash
touch "$marker"
exit 1
EOF
rm -rf "$MISE_CACHE_DIR/spy"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

cat >mise.toml <<EOF
[settings]
minimum_release_age = "2d"

[tools]
spy = "latest"
EOF

# Resolution is concrete, while bin-paths intentionally exposes the fuzzy
# request's runtime symlink.
assert "mise current spy" "2.0.0"
assert "mise bin-paths" "$MISE_DATA_DIR/installs/spy/latest/bin"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
assert "test ! -e '$marker'"

output="$(mise prune --dry-run --tools --yes 2>&1)"
assert_contains_text "$output" "spy@1.0.0"
assert_not_contains_text "$output" "spy@2.0.0"
4 changes: 2 additions & 2 deletions e2e/cli/test_which_no_remote_fetch
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
# invocation into a remote version-list fetch per tool (~65s shell startups).
#
# aqua-backed jq is used because the built-in default applies to it (the asdf
# dummy plugin is exempt). An explicit `minimum_release_age` setting still
# forces date-aware remote resolution — only the built-in default must not.
# dummy plugin is exempt). Installed matches remain eligible regardless of
# whether the cutoff is implicit or explicitly configured.

mise install jq@1.7.1

Expand Down
13 changes: 5 additions & 8 deletions e2e/cli/test_which_prefer_offline
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
#
# `mise which` is a binary lookup and must not require HTTP calls when an
# installed version already satisfies the request. It is now in the
# prefer-offline auto-enable list (like `where`, `ls`, `exec`), so even an
# explicit `minimum_release_age` cutoff -- which normally forces date-aware
# remote resolution on regular commands -- must keep resolving from installed
# versions instead of fetching a remote version list.
# prefer-offline auto-enable list (like `where`, `ls`, `exec`), so it keeps
# resolving from installed versions instead of fetching a remote version list.
#
# Scope: this covers config-file tools. An explicit `--tool=spy@1` argument is
# a `ToolSource::Argument` request, which intentionally opts out of
Expand Down Expand Up @@ -38,10 +36,9 @@ mise install spy@1.0.0
rm -f "$marker"
rm -rf "$MISE_CACHE_DIR/spy"

# Fuzzy requests ("latest", a prefix) are the dangerous ones: an explicit
# cutoff pushes those onto the remote date-aware resolution path. Newer remote
# versions (1.1.0, 2.0.0) exist, so a regressed remote resolution would also
# report a non-installed version below. Test both with no cutoff (built-in
# Fuzzy requests ("latest", a prefix) must reuse installed matches. Newer
# remote versions (1.1.0, 2.0.0) exist, so a regressed remote resolution would
# also report a non-installed version below. Test both with no cutoff (built-in
# default) and an explicit cutoff.
for mra in unset 24h; do
if [[ $mra == unset ]]; then
Expand Down
1 change: 1 addition & 0 deletions src/cli/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ impl Install {
latest_versions: true,
before_date: self.get_before_date()?,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
offline: false,
refresh_remote_versions: false,
inactive: false,
Expand Down
11 changes: 10 additions & 1 deletion src/cli/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,17 @@ impl Lock {
let before_date = self.get_before_date()?;
let lock_resolve_options = ResolveOptions {
before_date,
filter_installed_versions_by_release_date: true,
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
..Default::default()
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
let has_configured_release_age = settings.minimum_release_age.is_some()
|| config
.get_tool_request_set()
.await?
.tools
.values()
.flatten()
.any(|request| request.options().minimum_release_age().is_some());
let monorepo_union = if !self.global && config.monorepo_lockfile_root().is_some() {
Some(config.monorepo_union().await?)
} else {
Expand All @@ -107,7 +116,7 @@ impl Lock {
.await?;
ts_owned = monorepo_ts;
&ts_owned
} else if before_date.is_some() {
} else if before_date.is_some() || has_configured_release_age {
let builder = ToolsetBuilder::new().with_resolve_options(lock_resolve_options.clone());
ts_owned = builder.build(&config).await?;
&ts_owned
Expand Down
2 changes: 2 additions & 0 deletions src/cli/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ impl Upgrade {
latest_versions: true,
before_date,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
offline: false,
refresh_remote_versions: false,
inactive: self.inactive,
Expand Down Expand Up @@ -294,6 +295,7 @@ impl Upgrade {
latest_versions: true,
before_date,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
offline: false,
refresh_remote_versions: false,
inactive: self.inactive,
Expand Down
1 change: 1 addition & 0 deletions src/cli/use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ impl Use {
use_locked_version: true,
before_date: self.get_before_date()?,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
offline: false,
refresh_remote_versions: false,
inactive: false,
Expand Down
62 changes: 19 additions & 43 deletions src/toolset/tool_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ impl ToolVersion {
use_locked_version: false,
before_date: base_opts.before_date,
before_date_from_default: base_opts.before_date_from_default,
filter_installed_versions_by_release_date: base_opts
.filter_installed_versions_by_release_date,
offline: base_opts.offline,
refresh_remote_versions: base_opts.refresh_remote_versions,
inactive: base_opts.inactive,
Expand Down Expand Up @@ -388,17 +390,12 @@ impl ToolVersion {
let is_offline = settings.offline() || opts.offline;
let prefer_offline =
settings.prefer_offline() && !matches!(request.source(), ToolSource::Argument);
let prefer_offline_without_offline = prefer_offline && !is_offline;
let should_filter_installed_versions =
opts.filters_installed_versions() && !is_offline && !prefer_offline;

let should_filter_installed_versions = opts.filter_installed_versions_by_release_date
&& opts.before_date.is_some()
&& !opts.before_date_from_default
&& !is_offline
&& !prefer_offline;
if v == "latest" {
if prefer_offline_without_offline
&& !opts.latest_versions
&& let Some(v) = backend.latest_installed_version(None)?
{
return build(v);
}
if !opts.latest_versions
&& !should_filter_installed_versions
&& let Some(v) = backend.latest_installed_version(None)?
Expand Down Expand Up @@ -446,12 +443,6 @@ impl ToolVersion {
// zig@master), never an unrelated installed release, so we don't
// short-circuit zig@master to a stable version that happens to be
// installed.
if prefer_offline
&& !opts.latest_versions
&& let Some(installed) = backend.latest_installed_channel_version(&v)
{
return build(installed);
}
if !opts.latest_versions
&& !should_filter_installed_versions
&& let Some(installed) = backend.latest_installed_channel_version(&v)
Expand Down Expand Up @@ -487,15 +478,6 @@ impl ToolVersion {
if crate::config::config_file::idiomatic_version::package_json::is_package_json(path)
) && crate::semver::is_npm_semver_range_query(&v)
{
if prefer_offline && !opts.latest_versions {
let installed_versions = backend.list_installed_versions();
if let Some(matches) =
crate::semver::npm_semver_range_filter(&installed_versions, &v)
&& let Some(v) = matches.last()
{
return build(v.clone());
}
}
if !opts.latest_versions && !should_filter_installed_versions {
let installed_versions = backend.list_installed_versions();
if let Some(matches) =
Expand Down Expand Up @@ -633,14 +615,11 @@ impl ToolVersion {
let is_offline = settings.offline() || opts.offline;
let prefer_offline =
settings.prefer_offline() && !matches!(request.source(), ToolSource::Argument);
let should_filter_installed_versions =
opts.filters_installed_versions() && !is_offline && !prefer_offline;
if prefer_offline
&& !opts.latest_versions
&& let Some(v) = backend.list_installed_versions_matching(prefix).last()
{
return Ok(Self::new(request, v.to_string()));
}
let should_filter_installed_versions = opts.filter_installed_versions_by_release_date
&& opts.before_date.is_some()
&& !opts.before_date_from_default
&& !is_offline
&& !prefer_offline;
if !opts.latest_versions
&& !should_filter_installed_versions
&& let Some(v) = backend.list_installed_versions_matching(prefix).last()
Expand Down Expand Up @@ -738,10 +717,13 @@ pub struct ResolveOptions {
pub before_date: Option<Timestamp>,
/// `before_date` came from the built-in default release age rather than
/// explicit configuration (CLI flag, per-tool option, or the
/// `minimum_release_age` setting). The default only gates which versions
/// remote resolution may pick — it must not disable installed-version
/// fast paths (https://github.com/jdx/mise/discussions/10308).
/// `minimum_release_age` setting). This provenance is included in trace
/// output to distinguish the implicit policy from a user-selected cutoff.
pub before_date_from_default: bool,
/// Resolve fuzzy installed matches against release metadata when a cutoff
/// is active. This is reserved for discovery flows such as lockfile
/// generation; ordinary resolution treats installed versions as eligible.
pub filter_installed_versions_by_release_date: bool,
/// Additive to `Settings::offline()` — either being true skips remote version listing.
pub offline: bool,
/// Ignore cached remote version lists while resolving this request.
Expand All @@ -759,6 +741,7 @@ impl Default for ResolveOptions {
use_locked_version: true,
before_date: None,
before_date_from_default: false,
filter_installed_versions_by_release_date: false,
offline: false,
refresh_remote_versions: false,
inactive: false,
Expand Down Expand Up @@ -796,13 +779,6 @@ impl ResolveOptions {
}
Ok(())
}

/// An explicit cutoff means fuzzy requests must resolve date-aware even
/// when an installed version matches; the built-in default must not force
/// that remote resolution.
fn filters_installed_versions(&self) -> bool {
self.before_date.is_some() && !self.before_date_from_default
}
}

/// Check if a tool has any user-linked versions (created by `mise link`).
Expand Down
Loading