Fix agent assertion tests for auth-backed identities

This commit is contained in:
adrian
2026-04-13 15:47:49 -07:00
parent 6b25092c94
commit 041795a80b
4 changed files with 130 additions and 25 deletions

View File

@@ -365,7 +365,7 @@ impl AgentIdentityManager {
let stored_identity = StoredAgentIdentity {
binding_id: binding.binding_id.clone(),
chatgpt_account_id: binding.chatgpt_account_id.clone(),
chatgpt_user_id: binding.chatgpt_user_id.clone(),
chatgpt_user_id: binding.chatgpt_user_id,
agent_runtime_id: agent_runtime_id.to_string(),
private_key_pkcs8_base64: key_material.private_key_pkcs8_base64,
public_key_ssh: key_material.public_key_ssh,

View File

@@ -90,8 +90,9 @@ mod tests {
#[tokio::test]
async fn authorization_header_for_task_skips_when_feature_is_disabled() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let codex_home = tempfile::tempdir().expect("tempdir");
let auth = make_chatgpt_auth(codex_home.path(), "account-123", Some("user-123"));
let auth_manager = AuthManager::from_auth_for_testing(auth);
let manager = AgentIdentityManager::new_for_tests(
auth_manager,
/*feature_enabled*/ false,
@@ -99,9 +100,9 @@ mod tests {
SessionSource::Cli,
);
let agent_task = RegisteredAgentTask {
binding_id: "chatgpt-account-account_id".to_string(),
chatgpt_account_id: "account_id".to_string(),
chatgpt_user_id: None,
binding_id: "chatgpt-account-account-123".to_string(),
chatgpt_account_id: "account-123".to_string(),
chatgpt_user_id: Some("user-123".to_string()),
agent_runtime_id: "agent-123".to_string(),
task_id: "task-123".to_string(),
registered_at: "2026-03-23T12:00:00Z".to_string(),
@@ -118,8 +119,9 @@ mod tests {
#[tokio::test]
async fn authorization_header_for_task_serializes_signed_agent_assertion() {
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let codex_home = tempfile::tempdir().expect("tempdir");
let auth = make_chatgpt_auth(codex_home.path(), "account-123", Some("user-123"));
let auth_manager = AuthManager::from_auth_for_testing(auth);
let manager = AgentIdentityManager::new_for_tests(
auth_manager,
/*feature_enabled*/ true,
@@ -131,9 +133,9 @@ mod tests {
.await
.expect("seed test identity");
let agent_task = RegisteredAgentTask {
binding_id: "chatgpt-account-account_id".to_string(),
chatgpt_account_id: "account_id".to_string(),
chatgpt_user_id: None,
binding_id: "chatgpt-account-account-123".to_string(),
chatgpt_account_id: "account-123".to_string(),
chatgpt_user_id: Some("user-123".to_string()),
agent_runtime_id: "agent-123".to_string(),
task_id: "task-123".to_string(),
registered_at: "2026-03-23T12:00:00Z".to_string(),
@@ -179,4 +181,50 @@ mod tests {
)
.expect("signature should verify");
}
fn make_chatgpt_auth(
codex_home: &std::path::Path,
account_id: &str,
user_id: Option<&str>,
) -> CodexAuth {
let auth_json = codex_login::AuthDotJson {
auth_mode: Some(codex_app_server_protocol::AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(codex_login::token_data::TokenData {
id_token: codex_login::token_data::IdTokenInfo {
email: None,
chatgpt_plan_type: None,
chatgpt_user_id: user_id.map(ToOwned::to_owned),
chatgpt_account_id: Some(account_id.to_string()),
raw_jwt: fake_id_token(account_id, user_id),
},
access_token: format!("access-token-{account_id}"),
refresh_token: "refresh-token".to_string(),
account_id: Some(account_id.to_string()),
}),
last_refresh: Some(chrono::Utc::now()),
agent_identity: None,
};
codex_login::save_auth(
codex_home,
&auth_json,
codex_login::AuthCredentialsStoreMode::File,
)
.expect("save auth");
CodexAuth::from_auth_storage(codex_home, codex_login::AuthCredentialsStoreMode::File)
.expect("load auth")
.expect("auth")
}
fn fake_id_token(account_id: &str, user_id: Option<&str>) -> String {
let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
let payload = serde_json::json!({
"https://api.openai.com/auth": {
"chatgpt_user_id": user_id,
"chatgpt_account_id": account_id,
}
});
let payload = URL_SAFE_NO_PAD.encode(payload.to_string());
format!("{header}.{payload}.signature")
}
}

View File

@@ -17,11 +17,16 @@ use crate::agent_identity::RegisteredAgentTask;
use crate::agent_identity::StoredAgentIdentity;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use chrono::Utc;
use codex_api::CoreAuthProvider;
use codex_app_server_protocol::AuthMode;
use codex_keyring_store::tests::MockKeyringStore;
use codex_login::AuthCredentialsStoreMode;
use codex_login::AuthDotJson;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::save_auth;
use codex_login::token_data::IdTokenInfo;
use codex_login::token_data::TokenData;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
use codex_model_provider_info::create_oss_provider_with_base_url;
@@ -33,8 +38,6 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_secrets::SecretsBackendKind;
use codex_secrets::SecretsManager;
use core_test_support::responses;
use ed25519_dalek::Signature;
use ed25519_dalek::Verifier as _;
@@ -136,20 +139,13 @@ async fn model_client_with_agent_task(
StoredAgentIdentity,
) {
let codex_home = tempfile::tempdir().expect("tempdir");
let keyring_store = Arc::new(MockKeyringStore::default());
let secrets_manager = SecretsManager::new_with_keyring_store(
codex_home.path().to_path_buf(),
SecretsBackendKind::Local,
keyring_store,
);
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let auth = make_chatgpt_auth(codex_home.path(), "account-123", Some("user-123"));
let auth_manager = AuthManager::from_auth_for_testing(auth);
let agent_identity_manager = Arc::new(AgentIdentityManager::new_for_tests(
Arc::clone(&auth_manager),
/*feature_enabled*/ true,
"https://chatgpt.com/backend-api/".to_string(),
SessionSource::Cli,
secrets_manager,
));
let stored_identity = agent_identity_manager
.seed_generated_identity_for_tests("agent-123")
@@ -178,6 +174,47 @@ async fn model_client_with_agent_task(
(codex_home, client, agent_task, stored_identity)
}
fn make_chatgpt_auth(
codex_home: &std::path::Path,
account_id: &str,
user_id: Option<&str>,
) -> CodexAuth {
let auth_json = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(TokenData {
id_token: IdTokenInfo {
email: None,
chatgpt_plan_type: None,
chatgpt_user_id: user_id.map(ToOwned::to_owned),
chatgpt_account_id: Some(account_id.to_string()),
raw_jwt: fake_id_token(account_id, user_id),
},
access_token: format!("access-token-{account_id}"),
refresh_token: "refresh-token".to_string(),
account_id: Some(account_id.to_string()),
}),
last_refresh: Some(Utc::now()),
agent_identity: None,
};
save_auth(codex_home, &auth_json, AuthCredentialsStoreMode::File).expect("save auth");
CodexAuth::from_auth_storage(codex_home, AuthCredentialsStoreMode::File)
.expect("load auth")
.expect("auth")
}
fn fake_id_token(account_id: &str, user_id: Option<&str>) -> String {
let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
let payload = serde_json::json!({
"https://api.openai.com/auth": {
"chatgpt_user_id": user_id,
"chatgpt_account_id": account_id,
}
});
let payload = URL_SAFE_NO_PAD.encode(payload.to_string());
format!("{header}.{payload}.signature")
}
fn assert_agent_assertion_header(
authorization_header: &str,
stored_identity: &StoredAgentIdentity,
@@ -420,7 +457,7 @@ async fn websocket_agent_task_bypasses_cached_bearer_prewarm() {
assert_eq!(handshakes.len(), 2);
assert_eq!(
handshakes[0].header("authorization"),
Some("Bearer Access Token".to_string())
Some("Bearer access-token-account-123".to_string())
);
let agent_authorization = handshakes[1]
.header("authorization")

View File

@@ -138,6 +138,10 @@ async fn responses_api_proxy_dumps_parent_and_subagent_identity_headers() -> Res
let proxy_base_url = proxy.base_url();
let mut builder = test_codex().with_config(move |config| {
config.model_provider.base_url = Some(proxy_base_url);
config
.features
.enable(Feature::Collab)
.expect("test config should allow feature update");
config
.features
.disable(Feature::EnableRequestCompression)
@@ -179,7 +183,23 @@ async fn responses_api_proxy_dumps_parent_and_subagent_identity_headers() -> Res
}
fn request_body_contains(req: &wiremock::Request, text: &str) -> bool {
std::str::from_utf8(&req.body).is_ok_and(|body| body.contains(text))
let is_zstd = req
.headers
.get("content-encoding")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| {
value
.split(',')
.any(|entry| entry.trim().eq_ignore_ascii_case("zstd"))
});
let bytes = if is_zstd {
zstd::stream::decode_all(std::io::Cursor::new(&req.body)).ok()
} else {
Some(req.body.clone())
};
bytes
.and_then(|body| String::from_utf8(body).ok())
.is_some_and(|body| body.contains(text))
}
fn wait_for_proxy_request_dumps(dump_dir: &Path) -> Result<Vec<Value>> {