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
208 lines
6.8 KiB
Rust
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(¤t, &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()
|
|
);
|
|
}
|
|
}
|