feat(provider): add dedicated Codex Agent Identity flow

This commit is contained in:
elky
2026-07-22 20:19:29 +08:00
parent c7cc8fd7db
commit fdbc2607ec
14 changed files with 240 additions and 93 deletions
@@ -4,8 +4,9 @@ use crate::handlers::admin::provider::shared::support::{
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::{provider_key_status_snapshot_payload, unix_secs_to_rfc3339};
use crate::provider_key_auth::{
provider_key_auth_config_uses_header_authorization, provider_key_auth_semantics,
provider_key_can_refresh_oauth, provider_key_effective_api_formats,
provider_key_auth_config_is_agent_identity, provider_key_auth_config_uses_header_authorization,
provider_key_auth_semantics, provider_key_can_refresh_oauth,
provider_key_effective_api_formats,
};
use aether_admin::provider::pool as admin_provider_pool_pure;
use aether_admin::provider::quota as admin_provider_quota_pure;
@@ -1156,6 +1157,8 @@ pub(super) fn build_admin_pool_key_payload(
.unwrap_or(false);
let oauth_header_auth = auth_semantics.oauth_managed()
&& provider_key_auth_config_uses_header_authorization(auth_config.as_ref());
let agent_identity =
provider_key_auth_config_is_agent_identity(provider_type, auth_config.as_ref());
let account_status_code = admin_pool_trimmed_string_from_map(account_snapshot, "code");
let account_status_label =
admin_pool_trimmed_string(account_snapshot.and_then(|item| item.get("label")));
@@ -1220,6 +1223,7 @@ pub(super) fn build_admin_pool_key_payload(
"oauth_managed".to_string(),
json!(auth_semantics.oauth_managed()),
);
payload.insert("agent_identity".to_string(), json!(agent_identity));
payload.insert(
"can_refresh_oauth".to_string(),
json!(provider_key_can_refresh_oauth(
@@ -7,8 +7,8 @@ use super::{
};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::provider_key_auth::{
provider_key_auth_config_uses_header_authorization, provider_key_auth_semantics,
provider_key_can_refresh_oauth,
provider_key_auth_config_is_agent_identity, provider_key_auth_config_uses_header_authorization,
provider_key_auth_semantics, provider_key_can_refresh_oauth,
};
use crate::GatewayError;
use aether_admin::provider::pool as admin_provider_pool_pure;
@@ -152,6 +152,10 @@ pub(super) async fn build_admin_pool_resolve_selection_response(
"credential_kind": auth_semantics.credential_kind().as_str(),
"runtime_auth_kind": auth_semantics.runtime_auth_kind().as_str(),
"oauth_managed": auth_semantics.oauth_managed(),
"agent_identity": provider_key_auth_config_is_agent_identity(
&provider_type,
auth_config.as_ref(),
),
"can_refresh_oauth": provider_key_can_refresh_oauth(auth_semantics, auth_config.as_ref()),
"can_export_oauth": auth_semantics.can_export_oauth(),
"can_edit_oauth": auth_semantics.can_edit_oauth(),
@@ -1,8 +1,8 @@
use crate::handlers::shared::{json_string_list, unix_secs_to_rfc3339};
use crate::provider_key_auth::{
provider_key_auth_config_uses_header_authorization, provider_key_auth_semantics,
provider_key_can_refresh_oauth, provider_key_configured_api_formats,
provider_key_inherits_provider_api_formats,
provider_key_auth_config_is_agent_identity, provider_key_auth_config_uses_header_authorization,
provider_key_auth_semantics, provider_key_can_refresh_oauth,
provider_key_configured_api_formats, provider_key_inherits_provider_api_formats,
};
use crate::AppState;
use aether_admin::provider::quota as admin_provider_quota_pure;
@@ -2461,6 +2461,8 @@ pub(crate) fn build_admin_provider_key_response(
.unwrap_or(false);
let oauth_header_auth = auth_semantics.oauth_managed()
&& provider_key_auth_config_uses_header_authorization(auth_config.as_ref());
let agent_identity =
provider_key_auth_config_is_agent_identity(provider_type, auth_config.as_ref());
let oauth_plan_type = derive_catalog_oauth_plan_type(key, provider_type, auth_config.as_ref());
let (
health_score,
@@ -2494,7 +2496,11 @@ pub(crate) fn build_admin_provider_key_response(
);
payload.insert(
"api_key_masked".to_string(),
json!(masked_catalog_api_key(state, key)),
json!(if agent_identity {
"[Agent Identity]".to_string()
} else {
masked_catalog_api_key(state, key)
}),
);
payload.insert("api_key_plain".to_string(), serde_json::Value::Null);
payload.insert("auth_type".to_string(), json!(key.auth_type));
@@ -2518,6 +2524,7 @@ pub(crate) fn build_admin_provider_key_response(
"oauth_managed".to_string(),
json!(auth_semantics.oauth_managed()),
);
payload.insert("agent_identity".to_string(), json!(agent_identity));
payload.insert(
"can_refresh_oauth".to_string(),
json!(provider_key_can_refresh_oauth(
+45 -5
View File
@@ -112,6 +112,23 @@ pub(crate) fn provider_key_auth_config_uses_header_authorization(
})
}
/// Returns whether a Codex key uses the registered Agent Identity credential shape.
///
/// The key itself remains `auth_type = oauth` so it can use the existing OAuth
/// pipeline. Keep this as an explicit server-side classification rather than
/// making the frontend infer it from refresh capabilities.
pub(crate) fn provider_key_auth_config_is_agent_identity(
provider_type: &str,
auth_config: Option<&Map<String, Value>>,
) -> bool {
provider_type.trim().eq_ignore_ascii_case("codex")
&& auth_config.is_some_and(|config| {
aether_provider_transport::is_codex_agent_identity_auth_config_value(&Value::Object(
config.clone(),
))
})
}
fn normalized_auth_type(key: &StoredProviderCatalogKey) -> String {
key.auth_type.trim().to_ascii_lowercase()
}
@@ -272,11 +289,11 @@ pub(crate) fn provider_key_effective_api_formats(
#[cfg(test)]
mod tests {
use super::{
provider_active_api_formats, provider_key_auth_config_uses_header_authorization,
provider_key_auth_semantics, provider_key_can_refresh_oauth,
provider_key_configured_api_formats, provider_key_effective_api_formats,
provider_key_inherits_provider_api_formats, ProviderKeyCredentialKind,
ProviderKeyRuntimeAuthKind,
provider_active_api_formats, provider_key_auth_config_is_agent_identity,
provider_key_auth_config_uses_header_authorization, provider_key_auth_semantics,
provider_key_can_refresh_oauth, provider_key_configured_api_formats,
provider_key_effective_api_formats, provider_key_inherits_provider_api_formats,
ProviderKeyCredentialKind, ProviderKeyRuntimeAuthKind,
};
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
@@ -415,6 +432,29 @@ mod tests {
));
}
#[test]
fn detects_codex_agent_identity_auth_config() {
let config = json!({
"auth_mode": "agentIdentity",
"agent_runtime_id": "runtime-1",
"agent_private_key": "base64-private-key",
"task_id": "task-1"
});
assert!(provider_key_auth_config_is_agent_identity(
"codex",
config.as_object()
));
assert!(!provider_key_auth_config_is_agent_identity(
"openai",
config.as_object()
));
assert!(!provider_key_auth_config_is_agent_identity(
"codex",
json!({ "refresh_token": "refresh-token" }).as_object()
));
}
#[test]
fn recognizes_legacy_kiro_bearer_key_with_auth_config_as_oauth_managed() {
let mut key = sample_key("bearer");
@@ -3060,7 +3060,7 @@ async fn gateway_pool_plan_free_selector_prefers_upstream_plan_type() {
}
#[tokio::test]
async fn gateway_pool_keys_mark_oauth_header_auth() {
async fn gateway_pool_keys_classify_oauth_credentials() {
let mut provider = sample_provider("provider-codex", "codex", 10).with_transport_fields(
true,
false,
@@ -3091,11 +3091,25 @@ async fn gateway_pool_keys_mark_oauth_header_auth() {
)
.expect("auth config should encrypt"),
);
let mut agent_key = sample_key(
"key-codex-agent-identity",
"provider-codex",
"openai:responses",
"",
);
agent_key.auth_type = "oauth".to_string();
agent_key.encrypted_auth_config = Some(
encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
r#"{"provider_type":"codex","auth_mode":"agentIdentity","agent_runtime_id":"runtime-1","agent_private_key":"base64-private-key","task_id":"task-1"}"#,
)
.expect("Agent Identity auth config should encrypt"),
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
Vec::new(),
vec![key],
vec![key, agent_key],
));
let state = AppState::new()
.expect("gateway should build")
@@ -3120,8 +3134,19 @@ async fn gateway_pool_keys_mark_oauth_header_auth() {
)
.expect("json body should parse");
let keys = payload["keys"].as_array().expect("keys should be array");
assert_eq!(keys.len(), 1);
assert_eq!(keys[0]["oauth_header_auth"], true);
assert_eq!(keys.len(), 2);
let oauth_header_key = keys
.iter()
.find(|key| key["key_id"] == "key-codex-oauth-header")
.expect("OAuth Header key should exist");
assert_eq!(oauth_header_key["oauth_header_auth"], true);
assert_eq!(oauth_header_key["agent_identity"], false);
let agent_identity_key = keys
.iter()
.find(|key| key["key_id"] == "key-codex-agent-identity")
.expect("Agent Identity key should exist");
assert_eq!(agent_identity_key["oauth_header_auth"], false);
assert_eq!(agent_identity_key["agent_identity"], true);
}
#[tokio::test]