Skip to main content

codex_runtime/runtime/client/
compat_guard.rs

1use crate::runtime::core::Runtime;
2
3use super::ClientError;
4
5const DEFAULT_MIN_CODEX_VERSION: SemVerTriplet = SemVerTriplet {
6    major: 0,
7    minor: 104,
8    patch: 0,
9};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
12pub struct SemVerTriplet {
13    pub major: u32,
14    pub minor: u32,
15    pub patch: u32,
16}
17
18impl SemVerTriplet {
19    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
20        Self {
21            major,
22            minor,
23            patch,
24        }
25    }
26}
27
28impl std::fmt::Display for SemVerTriplet {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
31    }
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct CompatibilityGuard {
36    pub require_initialize_user_agent: bool,
37    pub min_codex_version: Option<SemVerTriplet>,
38}
39
40impl Default for CompatibilityGuard {
41    fn default() -> Self {
42        Self {
43            require_initialize_user_agent: true,
44            min_codex_version: Some(DEFAULT_MIN_CODEX_VERSION),
45        }
46    }
47}
48
49pub(super) fn validate_runtime_compatibility(
50    runtime: &Runtime,
51    guard: &CompatibilityGuard,
52) -> Result<(), ClientError> {
53    if !guard.require_initialize_user_agent && guard.min_codex_version.is_none() {
54        return Ok(());
55    }
56
57    let Some(user_agent) = runtime.server_user_agent() else {
58        if guard.require_initialize_user_agent {
59            return Err(ClientError::MissingInitializeUserAgent);
60        }
61        return Ok(());
62    };
63    let (product, version) = parse_initialize_user_agent(&user_agent)
64        .ok_or_else(|| ClientError::InvalidInitializeUserAgent(user_agent.clone()))?;
65    let is_codex_product = product.starts_with("Codex ");
66
67    if is_codex_product {
68        if let Some(min_required) = guard.min_codex_version {
69            if version < min_required {
70                return Err(ClientError::IncompatibleCodexVersion {
71                    detected: version.to_string(),
72                    required: min_required.to_string(),
73                    user_agent,
74                });
75            }
76        }
77    }
78
79    Ok(())
80}
81
82pub(super) fn parse_initialize_user_agent(value: &str) -> Option<(String, SemVerTriplet)> {
83    let slash = value.find('/')?;
84    let product = value.get(..slash)?.trim().to_owned();
85    if product.is_empty() {
86        return None;
87    }
88
89    let version_part = value
90        .get(slash + 1..)?
91        .chars()
92        .take_while(|ch| ch.is_ascii_digit() || *ch == '.')
93        .collect::<String>();
94    let mut parts = version_part.split('.');
95    let major = parts.next()?.parse::<u32>().ok()?;
96    let minor = parts.next()?.parse::<u32>().ok()?;
97    let patch = parts.next()?.parse::<u32>().ok()?;
98
99    Some((product, SemVerTriplet::new(major, minor, patch)))
100}