use super::*;
use mj_core::config::{Config, HarnessKind};
use std::collections::BTreeSet;
use std::path::PathBuf;
type ConfigSource = (PathBuf, Option<String>);
#[derive(Clone, Default)]
pub(crate) struct ProviderAuthCache {
files: BTreeMap<ConfigSource, (mj_core::codex_provider::ConfigFiles, Option<bool>)>,
pub(crate) schemes: BTreeMap<String, bool>,
#[cfg(test)]
parses: usize,
}
impl ProviderAuthCache {
pub(crate) fn refresh(&mut self, config: &Config) -> bool {
let source = |profile: &mj_core::config::HarnessProfile| -> ConfigSource {
(profile.home.clone(), profile.codex_profile.clone())
};
let sources: BTreeSet<_> = config
.profiles
.values()
.filter(|profile| profile.kind == HarnessKind::Codex)
.map(source)
.collect();
self.files.retain(|source, _| sources.contains(source));
for (home, overlay) in sources {
let described = mj_core::codex_provider::describe_config(&home, overlay.as_deref());
let files = match mj_core::codex_provider::ConfigFiles::read(&home, overlay.as_deref())
{
Ok(files) => files,
Err(error) => {
tracing::warn!(error = %format!("{error:#}"), config = %described, "cannot read credential provider configuration");
self.files.remove(&(home, overlay));
continue;
}
};
let source = (home, overlay);
if self
.files
.get(&source)
.is_some_and(|(previous, _)| *previous == files)
{
continue;
}
let scheme = match files.layer(&source.0) {
Ok(Some(text)) => {
#[cfg(test)]
{
self.parses += 1;
}
match mj_core::codex_provider::parse_config(&text, &described) {
Ok(provider) => {
Some(provider.is_some_and(|provider| provider.skips_login_file_sync()))
}
Err(error) => {
tracing::warn!(%error, config = %described, "invalid credential provider configuration");
None
}
}
}
Ok(None) => Some(false),
Err(error) => {
tracing::warn!(error = %format!("{error:#}"), config = %described, "invalid credential provider configuration");
None
}
};
self.files.insert(source, (files, scheme));
}
let schemes = config
.profiles
.iter()
.filter_map(|(id, profile)| {
let scheme = if profile.kind == HarnessKind::Codex {
self.files.get(&source(profile))?.1?
} else {
false
};
Some((id.clone(), scheme))
})
.collect();
if self.schemes == schemes {
false
} else {
self.schemes = schemes;
true
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shared_homes_parse_once_and_provider_edits_change_login_file_sync() {
let home = tempfile::tempdir().unwrap();
let path = home.path().join("config.toml");
let mut config = Config::default();
let profile: mj_core::config::HarnessProfile = serde_json::from_value(serde_json::json!({
"kind": "codex", "home": home.path(),
}))
.unwrap();
config.profiles.insert("first".into(), profile.clone());
config.profiles.insert("second".into(), profile);
let mut cache = ProviderAuthCache::default();
assert!(cache.refresh(&config));
assert!(!cache.schemes["first"]);
assert_eq!(cache.parses, 0);
std::fs::write(&path, "# native login\n".repeat(10_000)).unwrap();
assert!(!cache.refresh(&config));
for _ in 0..50 {
assert!(!cache.refresh(&config));
}
assert_eq!(
cache.parses, 1,
"unchanged bytes and shared homes must not be parsed again"
);
std::fs::write(
&path,
r#"model_provider = "custom"
[model_providers.custom]
base_url = "https://example.test/v1"
wire_api = "responses"
env_key = "FIXTURE_KEY"
"#,
)
.unwrap();
assert!(cache.refresh(&config));
assert!(cache.schemes.values().all(|api| *api));
assert_eq!(cache.parses, 2);
std::fs::write(&path, "invalid = [").unwrap();
assert!(cache.refresh(&config));
assert!(cache.schemes.is_empty());
assert!(!cache.refresh(&config));
assert_eq!(
cache.parses, 3,
"a reported malformed file is retried when its bytes change"
);
std::fs::remove_file(&path).unwrap();
assert!(cache.refresh(&config));
assert!(cache.schemes.values().all(|api| !*api));
std::fs::write(
&path,
"model = \"global.openai.gpt-6-luna\"\n\
model_provider = \"amazon-bedrock-runtime\"\n\
[model_providers.amazon-bedrock-runtime.aws]\n\
region = \"us-east-1\"\n",
)
.unwrap();
assert!(cache.refresh(&config));
assert!(cache.schemes.values().all(|skip_sync| *skip_sync));
assert_eq!(cache.parses, 4);
let overlay: mj_core::config::HarnessProfile = serde_json::from_value(
serde_json::json!({"kind": "codex", "home": home.path(), "codex_profile": "ds"}),
)
.unwrap();
config.profiles.insert("third".into(), overlay);
assert!(!cache.refresh(&config));
assert!(!cache.schemes.contains_key("third"));
let source = (home.path().to_path_buf(), Some("ds".to_owned()));
assert_eq!(cache.files[&source].1, None, "the failure stays cached");
assert!(!cache.refresh(&config));
assert_eq!(
cache.parses, 4,
"an unchanged missing overlay is not retried"
);
std::fs::write(
home.path().join("ds.config.toml"),
"model_provider = \"custom\"\n\
[model_providers.custom]\n\
base_url = \"https://example.test/v1\"\n\
wire_api = \"responses\"\n\
env_key = \"FIXTURE_KEY\"\n",
)
.unwrap();
assert!(cache.refresh(&config));
assert!(cache.schemes["third"]);
assert_eq!(cache.parses, 5);
config.profiles.clear();
assert!(cache.refresh(&config));
assert!(cache.files.is_empty());
}
#[test]
fn an_inline_key_provider_skips_login_file_sync_like_its_auth_scheme() {
let home = tempfile::tempdir().unwrap();
std::fs::write(
home.path().join("config.toml"),
"model = \"deepseek-flash\"\n\
model_provider = \"deepseek\"\n\
[model_providers.deepseek]\n\
base_url = \"https://api.deepseek.com/v1\"\n\
experimental_bearer_token = \"inline-deepseek-key\"\n\
wire_api = \"responses\"\n",
)
.unwrap();
let mut config = Config::default();
let profile: mj_core::config::HarnessProfile = serde_json::from_value(serde_json::json!({
"kind": "codex", "home": home.path(),
}))
.unwrap();
config.profiles.insert("codex".into(), profile.clone());
let mut cache = ProviderAuthCache::default();
assert!(cache.refresh(&config));
assert!(
cache.schemes["codex"],
"an inline-key provider has no Codex login file to converge"
);
assert_eq!(
cache.schemes["codex"],
!profile.auth_scheme().uses_native_login_file(),
"the cached skip must agree with the profile's own auth scheme"
);
}
}