Validate implicit API keys for Guardian Decisions (#59905)

## Why

Decisions uses a commercial endpoint, so implicitly inherited credentials must exclude FedRAMP API keys.

## What changed

Reject empty, whitespace-padded, and FedRAMP keys selected from cached authentication or `OPENAI_API_KEY`, returning a credentials error without rewriting the key. Explicit `CODEX_GUARDIAN_DECISIONS_API_KEY` overrides retain their existing behavior.

## Testing

Extend the cached-key precedence test to verify that FedRAMP project and service-account keys, including a whitespace-padded key, are rejected even when `OPENAI_API_KEY` provides a fallback.

GitOrigin-RevId: 8904b0e5b30662aaf7eb7535309b01c254bcbb82
This commit is contained in:
jif
2026-11-06 13:06:00 +00:00
committed by copyberry
parent e3fe1ade2c
commit f57966695f
2 changed files with 17 additions and 0 deletions
@@ -160,6 +160,17 @@ fn decisions_prefers_cached_api_key_over_openai_env() -> anyhow::Result<()> {
decisions_api_key("openai", /*base_url*/ None, &cached) == Ok(auth_key.into()),
"cached API-key auth must take priority over OPENAI_API_KEY"
);
for auth_key in [
"sk-proj-fedramp-synthetic",
"sk-svcacct-fedramp-synthetic",
" sk-proj-fedramp-synthetic",
] {
let cached = AuthManager::from_auth_for_testing(CodexAuth::from_api_key(auth_key));
assert!(matches!(
decisions_api_key("openai", /*base_url*/ None, &cached),
Err(DecisionsError::Credentials)
));
}
Ok(())
}
@@ -67,6 +67,7 @@ pub(super) fn decisions_api_key(
.and_then(CodexAuth::api_key)
.map(str::to_owned)
.or_else(codex_login::read_openai_api_key_from_env)
.filter(|key| valid_implicit_decisions_key(key))
.ok_or(DecisionsError::Credentials)
}
Err(std::env::VarError::NotPresent | std::env::VarError::NotUnicode(_)) => {
@@ -75,6 +76,11 @@ pub(super) fn decisions_api_key(
}
}
// Decisions uses a commercial endpoint. Reject padded keys without rewriting credentials.
pub(super) fn valid_implicit_decisions_key(key: &str) -> bool {
!key.is_empty() && key.trim() == key && !codex_login::auth::is_fedramp_api_key(key)
}
pub(super) async fn sampler_config(
input: &ThreadStartInput<'_, Config>,
manager: Option<Arc<ThreadManager>>,