use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use tracing::warn;
use super::{CapabilityCachePolicy, CodexCapabilities, CodexFeatureFlags, CodexVersionInfo};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CapabilityTtlDecision {
pub should_probe: bool,
pub policy: CapabilityCachePolicy,
}
pub fn capability_cache_ttl_decision(
snapshot: Option<&CodexCapabilities>,
ttl: Duration,
now: SystemTime,
) -> CapabilityTtlDecision {
let default_policy = CapabilityCachePolicy::PreferCache;
let Some(snapshot) = snapshot else {
return CapabilityTtlDecision {
should_probe: true,
policy: default_policy,
};
};
let expired = now
.duration_since(snapshot.collected_at)
.map(|elapsed| elapsed >= ttl)
.unwrap_or(true);
if !expired {
return CapabilityTtlDecision {
should_probe: false,
policy: default_policy,
};
}
let policy = if snapshot.fingerprint.is_some() {
CapabilityCachePolicy::Refresh
} else {
CapabilityCachePolicy::Bypass
};
CapabilityTtlDecision {
should_probe: true,
policy,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CapabilitySupport {
Supported,
Unsupported,
Unknown,
}
impl CapabilitySupport {
pub const fn is_supported(self) -> bool {
matches!(self, CapabilitySupport::Supported)
}
pub const fn is_unknown(self) -> bool {
matches!(self, CapabilitySupport::Unknown)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CapabilityFeature {
OutputSchema,
AddDir,
McpLogin,
FeaturesList,
}
impl CapabilityFeature {
fn label(self) -> &'static str {
match self {
CapabilityFeature::OutputSchema => "--output-schema",
CapabilityFeature::AddDir => "codex add-dir",
CapabilityFeature::McpLogin => "codex login --mcp",
CapabilityFeature::FeaturesList => "codex features list",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapabilityGuard {
pub feature: CapabilityFeature,
pub support: CapabilitySupport,
pub notes: Vec<String>,
}
impl CapabilityGuard {
fn supported(feature: CapabilityFeature, note: impl Into<String>) -> Self {
CapabilityGuard {
feature,
support: CapabilitySupport::Supported,
notes: vec![note.into()],
}
}
fn unsupported(feature: CapabilityFeature, note: impl Into<String>) -> Self {
CapabilityGuard {
feature,
support: CapabilitySupport::Unsupported,
notes: vec![note.into()],
}
}
fn unknown(feature: CapabilityFeature, notes: Vec<String>) -> Self {
CapabilityGuard {
feature,
support: CapabilitySupport::Unknown,
notes,
}
}
pub const fn is_supported(&self) -> bool {
self.support.is_supported()
}
pub const fn is_unknown(&self) -> bool {
self.support.is_unknown()
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CapabilityProbePlan {
pub steps: Vec<CapabilityProbeStep>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CapabilityProbeStep {
VersionFlag,
FeaturesListJson,
FeaturesListText,
HelpFallback,
ManualOverride,
}
impl CodexCapabilities {
pub fn guard_output_schema(&self) -> CapabilityGuard {
self.guard_feature(CapabilityFeature::OutputSchema)
}
pub fn guard_add_dir(&self) -> CapabilityGuard {
self.guard_feature(CapabilityFeature::AddDir)
}
pub fn guard_mcp_login(&self) -> CapabilityGuard {
self.guard_feature(CapabilityFeature::McpLogin)
}
pub fn guard_features_list(&self) -> CapabilityGuard {
self.guard_feature(CapabilityFeature::FeaturesList)
}
pub fn guard_feature(&self, feature: CapabilityFeature) -> CapabilityGuard {
guard_feature_support(feature, &self.features, self.version.as_ref())
}
}
fn guard_feature_support(
feature: CapabilityFeature,
flags: &CodexFeatureFlags,
version: Option<&CodexVersionInfo>,
) -> CapabilityGuard {
let supported = match feature {
CapabilityFeature::OutputSchema => flags.supports_output_schema,
CapabilityFeature::AddDir => flags.supports_add_dir,
CapabilityFeature::McpLogin => flags.supports_mcp_login,
CapabilityFeature::FeaturesList => flags.supports_features_list,
};
if supported {
return CapabilityGuard::supported(
feature,
format!("Support for {} reported by Codex probe.", feature.label()),
);
}
if feature == CapabilityFeature::FeaturesList {
let mut notes = vec![format!(
"Support for {} could not be confirmed; feature list probes failed or were unavailable.",
feature.label()
)];
if version.is_none() {
notes.push(
"Version was unavailable; assuming compatibility with older Codex builds."
.to_string(),
);
}
return CapabilityGuard::unknown(feature, notes);
}
if flags.supports_features_list {
return CapabilityGuard::unsupported(
feature,
format!(
"`{}` did not advertise {}; skipping related flag to stay compatible.",
CapabilityFeature::FeaturesList.label(),
feature.label()
),
);
}
let mut notes = vec![format!(
"Support for {} is unknown because {} is unavailable; disable the flag for compatibility.",
feature.label(),
CapabilityFeature::FeaturesList.label()
)];
if version.is_none() {
notes.push(
"Version could not be parsed; treating feature support conservatively to avoid CLI errors."
.to_string(),
);
}
CapabilityGuard::unknown(feature, notes)
}
pub(crate) fn guard_is_supported(guard: &CapabilityGuard) -> bool {
matches!(guard.support, CapabilitySupport::Supported)
}
pub(crate) fn log_guard_skip(guard: &CapabilityGuard) {
warn!(
feature = guard.feature.label(),
support = ?guard.support,
notes = ?guard.notes,
"Skipping requested Codex capability because support was not confirmed"
);
}