grok-build-upstream-mirror/crates/codegen/xai-grok-update/src/version_policy.rs
grokkybara[bot] 69f0ba880a Synced from monorepo
Synced from monorepo

Changes:
- Workspace server: report `/ready` as failed with dwell on hub connect failure
- Refresh OIDC token for the Grok agent in the shell
- ACP terminal output recorder
- Cross-platform provider auth commands in the shell
- Default `/resume` to Grok sessions with a hint for hidden external sessions
- Resume sessions by title with `--resume`
- Limit app-builder archive size
- Data-driven tag labels for slash commands
- Doctor fixes for tmux
- Custom provider gateways and subprocess environment policy in the shell
- `/tutorial` — opt-in onboarding tour of Grok Build
- Soft and required CLI version checks in the shell
- Privacy banner env overrides survive live settings updates
- Add remote flag to override the image-edit model
- Return profile fields from auth info even when the access token is expired
- Add edit control on queued prompt rows
- Keep fail-closed policy when clearing orphans with no team
- Setting to disable the Ctrl+Space/F8 voice shortcut
- Pass `--raw` to pw-record so Linux dictation works on older PipeWire
- Validate git URLs when adding marketplace entries
- Stop shipping stale tool-doc parameter and tool names
- Re-point dashboard attach after `/fork` only when the parent was attached
- Surface Grok Computer media-generation results as file-path chunks
- Clear web background-task tray on kill and keep the task description
- Show privacy upsell banner in agent view until acted on
- Add tools-server client callback surface
- Protect persistent global hook sources

Source-Revision: 95d84f443eddcbed6cbfd6eed22e2eafe6b3939d
2026-07-23 17:12:33 +00:00

208 lines
6.8 KiB
Rust

//! Startup enforcement of the version policy. The hard `required_*` bounds gate
//! startup; `minimum`/`maximum` are updater-only. Every knob fails open.
use crate::version::get_installed_grok_version;
use semver::Version;
use tracing::warn;
use xai_grok_shell::util::config::VersionPolicy;
#[derive(Debug, Clone, PartialEq, Eq)]
enum RequiredRangeDecision {
InRange,
Below { current: String, minimum: String },
Above { current: String, maximum: String },
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum VersionPolicyError {
#[error(
"Cannot install Grok {target}: the minimum allowed version is {minimum}. \
Run `grok update` to install the latest allowed version."
)]
TargetBelowFloor { target: String, minimum: String },
}
/// Fails open: a contradictory range or an unparseable running version yields
/// `InRange`.
fn evaluate_required_range(current_version: &str, policy: &VersionPolicy) -> RequiredRangeDecision {
if policy.has_contradictory_required_range() {
warn!(
required_min = ?policy.required_minimum,
required_max = ?policy.required_maximum,
"required version range is contradictory (min > max); ignoring"
);
return RequiredRangeDecision::InRange;
}
let Ok(cur) = Version::parse(current_version) else {
return RequiredRangeDecision::InRange;
};
if let Some(mn) = &policy.required_minimum
&& cur < *mn
{
return RequiredRangeDecision::Below {
current: cur.to_string(),
minimum: mn.to_string(),
};
}
if let Some(mx) = &policy.required_maximum
&& cur > *mx
{
return RequiredRangeDecision::Above {
current: cur.to_string(),
maximum: mx.to_string(),
};
}
RequiredRangeDecision::InRange
}
/// Reject an explicit `--version` pin below the hard floor. A pin above the
/// ceiling is allowed so a too-new install can recover.
pub(crate) fn check_install_target(
policy: &VersionPolicy,
target: &str,
) -> Result<(), VersionPolicyError> {
let Some(min) = policy.installable_floor() else {
return Ok(());
};
if !matches!(Version::parse(target), Ok(t) if t >= min) {
return Err(VersionPolicyError::TargetBelowFloor {
target: target.to_string(),
minimum: min.to_string(),
});
}
Ok(())
}
fn required_range_message(decision: &RequiredRangeDecision) -> Option<String> {
match decision {
RequiredRangeDecision::InRange => None,
RequiredRangeDecision::Below { current, minimum } => Some(format!(
"This version of Grok ({current}) is older than the minimum required \
by your organization ({minimum}).\n\n\
Update to an approved version through your organization's approved \
method (for example, run `grok update`)."
)),
RequiredRangeDecision::Above { current, maximum } => Some(format!(
"This version of Grok ({current}) is newer than the maximum allowed \
by your organization ({maximum}).\n\n\
Install an approved version through your organization's approved \
method (for example, run `grok update --version {maximum}`)."
)),
}
}
/// Refuse to start when the running version is outside the required range.
/// Recovery subcommands return before this, so they stay usable.
pub fn enforce_version_policy_or_exit() {
let policy = VersionPolicy::resolve();
let current = get_installed_grok_version();
let decision = evaluate_required_range(&current, &policy);
if let Some(message) = required_range_message(&decision) {
warn!(?decision, "required version range: refusing to start");
eprintln!("{message}");
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(s: &str) -> Version {
Version::parse(s).unwrap()
}
fn pol(
min: Option<&str>,
max: Option<&str>,
rmin: Option<&str>,
rmax: Option<&str>,
) -> VersionPolicy {
VersionPolicy {
minimum: min.map(v),
maximum: max.map(v),
required_minimum: rmin.map(v),
required_maximum: rmax.map(v),
}
}
#[test]
fn check_install_target_enforces_only_the_hard_floor() {
assert!(check_install_target(&pol(Some("0.2.100"), None, None, None), "0.2.50").is_ok());
let hard = pol(None, None, Some("0.1.100"), None);
assert!(check_install_target(&hard, "0.1.150").is_ok());
assert!(matches!(
check_install_target(&hard, "0.1.50").unwrap_err(),
VersionPolicyError::TargetBelowFloor { .. }
));
assert!(matches!(
check_install_target(&hard, "garbage").unwrap_err(),
VersionPolicyError::TargetBelowFloor { .. }
));
assert!(check_install_target(&pol(None, None, None, None), "garbage").is_ok());
assert!(
check_install_target(&pol(None, None, Some("0.3.0"), Some("0.2.0")), "0.1.0").is_ok()
);
assert!(
check_install_target(
&pol(None, None, Some("0.2.100"), Some("0.2.150")),
"0.2.200"
)
.is_ok()
);
}
#[test]
fn evaluate_required_range_gates_and_fails_open() {
use RequiredRangeDecision::{Above, Below, InRange};
assert_eq!(
evaluate_required_range(
"0.2.100",
&pol(None, None, Some("0.2.100"), Some("0.2.150"))
),
InRange
);
assert!(matches!(
evaluate_required_range("0.2.99", &pol(None, None, Some("0.2.100"), None)),
Below { .. }
));
assert!(matches!(
evaluate_required_range("0.2.200", &pol(None, None, None, Some("0.2.150"))),
Above { .. }
));
assert_eq!(
evaluate_required_range("0.2.50", &pol(None, None, Some("0.3.0"), Some("0.2.0"))),
InRange
);
assert_eq!(
evaluate_required_range("dev-build", &pol(None, None, Some("0.2.100"), None)),
InRange
);
assert_eq!(
evaluate_required_range("0.2.50", &pol(Some("0.2.100"), None, None, None)),
InRange
);
}
#[test]
fn required_range_message_is_none_only_when_in_range() {
assert!(required_range_message(&RequiredRangeDecision::InRange).is_none());
assert!(
required_range_message(&RequiredRangeDecision::Below {
current: "0.2.99".into(),
minimum: "0.2.100".into(),
})
.is_some()
);
assert!(
required_range_message(&RequiredRangeDecision::Above {
current: "0.2.200".into(),
maximum: "0.2.150".into(),
})
.is_some()
);
}
}