1use serde::{Deserialize, Serialize};
2use std::time::{Duration, SystemTime};
3use tracing::warn;
4
5use super::{CapabilityCachePolicy, CodexCapabilities, CodexFeatureFlags, CodexVersionInfo};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct CapabilityTtlDecision {
10 pub should_probe: bool,
12 pub policy: CapabilityCachePolicy,
14}
15
16pub fn capability_cache_ttl_decision(
28 snapshot: Option<&CodexCapabilities>,
29 ttl: Duration,
30 now: SystemTime,
31) -> CapabilityTtlDecision {
32 let default_policy = CapabilityCachePolicy::PreferCache;
33 let Some(snapshot) = snapshot else {
34 return CapabilityTtlDecision {
35 should_probe: true,
36 policy: default_policy,
37 };
38 };
39
40 let expired = now
41 .duration_since(snapshot.collected_at)
42 .map(|elapsed| elapsed >= ttl)
43 .unwrap_or(true);
44
45 if !expired {
46 return CapabilityTtlDecision {
47 should_probe: false,
48 policy: default_policy,
49 };
50 }
51
52 let policy = if snapshot.fingerprint.is_some() {
53 CapabilityCachePolicy::Refresh
54 } else {
55 CapabilityCachePolicy::Bypass
56 };
57
58 CapabilityTtlDecision {
59 should_probe: true,
60 policy,
61 }
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum CapabilitySupport {
67 Supported,
68 Unsupported,
69 Unknown,
70}
71
72impl CapabilitySupport {
73 pub const fn is_supported(self) -> bool {
75 matches!(self, CapabilitySupport::Supported)
76 }
77
78 pub const fn is_unknown(self) -> bool {
80 matches!(self, CapabilitySupport::Unknown)
81 }
82}
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum CapabilityFeature {
87 OutputSchema,
88 AddDir,
89 McpLogin,
90 FeaturesList,
91}
92
93impl CapabilityFeature {
94 fn label(self) -> &'static str {
95 match self {
96 CapabilityFeature::OutputSchema => "--output-schema",
97 CapabilityFeature::AddDir => "codex add-dir",
98 CapabilityFeature::McpLogin => "codex login --mcp",
99 CapabilityFeature::FeaturesList => "codex features list",
100 }
101 }
102}
103
104#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct CapabilityGuard {
107 pub feature: CapabilityFeature,
109 pub support: CapabilitySupport,
111 pub notes: Vec<String>,
113}
114
115impl CapabilityGuard {
116 fn supported(feature: CapabilityFeature, note: impl Into<String>) -> Self {
117 CapabilityGuard {
118 feature,
119 support: CapabilitySupport::Supported,
120 notes: vec![note.into()],
121 }
122 }
123
124 fn unsupported(feature: CapabilityFeature, note: impl Into<String>) -> Self {
125 CapabilityGuard {
126 feature,
127 support: CapabilitySupport::Unsupported,
128 notes: vec![note.into()],
129 }
130 }
131
132 fn unknown(feature: CapabilityFeature, notes: Vec<String>) -> Self {
133 CapabilityGuard {
134 feature,
135 support: CapabilitySupport::Unknown,
136 notes,
137 }
138 }
139
140 pub const fn is_supported(&self) -> bool {
142 self.support.is_supported()
143 }
144
145 pub const fn is_unknown(&self) -> bool {
147 self.support.is_unknown()
148 }
149}
150
151#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
157pub struct CapabilityProbePlan {
158 pub steps: Vec<CapabilityProbeStep>,
160}
161
162#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
164pub enum CapabilityProbeStep {
165 VersionFlag,
167 FeaturesListJson,
169 FeaturesListText,
171 HelpFallback,
173 ManualOverride,
175}
176
177impl CodexCapabilities {
178 pub fn guard_output_schema(&self) -> CapabilityGuard {
180 self.guard_feature(CapabilityFeature::OutputSchema)
181 }
182
183 pub fn guard_add_dir(&self) -> CapabilityGuard {
185 self.guard_feature(CapabilityFeature::AddDir)
186 }
187
188 pub fn guard_mcp_login(&self) -> CapabilityGuard {
190 self.guard_feature(CapabilityFeature::McpLogin)
191 }
192
193 pub fn guard_features_list(&self) -> CapabilityGuard {
195 self.guard_feature(CapabilityFeature::FeaturesList)
196 }
197
198 pub fn guard_feature(&self, feature: CapabilityFeature) -> CapabilityGuard {
203 guard_feature_support(feature, &self.features, self.version.as_ref())
204 }
205}
206
207fn guard_feature_support(
208 feature: CapabilityFeature,
209 flags: &CodexFeatureFlags,
210 version: Option<&CodexVersionInfo>,
211) -> CapabilityGuard {
212 let supported = match feature {
213 CapabilityFeature::OutputSchema => flags.supports_output_schema,
214 CapabilityFeature::AddDir => flags.supports_add_dir,
215 CapabilityFeature::McpLogin => flags.supports_mcp_login,
216 CapabilityFeature::FeaturesList => flags.supports_features_list,
217 };
218
219 if supported {
220 return CapabilityGuard::supported(
221 feature,
222 format!("Support for {} reported by Codex probe.", feature.label()),
223 );
224 }
225
226 if feature == CapabilityFeature::FeaturesList {
227 let mut notes = vec![format!(
228 "Support for {} could not be confirmed; feature list probes failed or were unavailable.",
229 feature.label()
230 )];
231 if version.is_none() {
232 notes.push(
233 "Version was unavailable; assuming compatibility with older Codex builds."
234 .to_string(),
235 );
236 }
237 return CapabilityGuard::unknown(feature, notes);
238 }
239
240 if flags.supports_features_list {
241 return CapabilityGuard::unsupported(
242 feature,
243 format!(
244 "`{}` did not advertise {}; skipping related flag to stay compatible.",
245 CapabilityFeature::FeaturesList.label(),
246 feature.label()
247 ),
248 );
249 }
250
251 let mut notes = vec![format!(
252 "Support for {} is unknown because {} is unavailable; disable the flag for compatibility.",
253 feature.label(),
254 CapabilityFeature::FeaturesList.label()
255 )];
256 if version.is_none() {
257 notes.push(
258 "Version could not be parsed; treating feature support conservatively to avoid CLI errors."
259 .to_string(),
260 );
261 }
262
263 CapabilityGuard::unknown(feature, notes)
264}
265
266pub(crate) fn guard_is_supported(guard: &CapabilityGuard) -> bool {
267 matches!(guard.support, CapabilitySupport::Supported)
268}
269
270pub(crate) fn log_guard_skip(guard: &CapabilityGuard) {
271 warn!(
272 feature = guard.feature.label(),
273 support = ?guard.support,
274 notes = ?guard.notes,
275 "Skipping requested Codex capability because support was not confirmed"
276 );
277}