Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ 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.
Versions that are already installed also remain eligible: `minimum_release_age` limits remote
version selection and does not make an installed version inactive.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

Capability depends on the backend:

Expand Down
35 changes: 35 additions & 0 deletions e2e/cli/test_minimum_release_age_installed_resolution
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/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
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

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
1 change: 1 addition & 0 deletions src/cli/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ 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 monorepo_union = if !self.global && config.monorepo_lockfile_root().is_some() {
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
60 changes: 17 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,11 @@ 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()
&& !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 +442,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 +477,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 +614,10 @@ 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()
&& !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 +715,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 +739,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 +777,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