codex_runtime/runtime/client/
compat_guard.rs1use 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}