Register agent identities behind use_agent_identity (#17386)

## Summary

Stack PR 2 of 4 for feature-gated agent identity support.

This PR adds agent identity registration behind
`features.use_agent_identity`. It keeps the app-server protocol
unchanged and starts registration after ChatGPT auth exists rather than
requiring a client restart.

## Stack

- PR1: https://github.com/openai/codex/pull/17385 - add
`features.use_agent_identity`
- PR2: https://github.com/openai/codex/pull/17386 - this PR
- PR3: https://github.com/openai/codex/pull/17387 - register agent tasks
when enabled
- PR4: https://github.com/openai/codex/pull/17388 - use `AgentAssertion`
downstream when enabled

## Validation

Covered as part of the local stack validation pass:

- `just fmt`
- `cargo test -p codex-core --lib agent_identity`
- `cargo test -p codex-core --lib agent_assertion`
- `cargo test -p codex-core --lib websocket_agent_task`
- `cargo test -p codex-api api_bridge`
- `cargo build -p codex-cli --bin codex`

## Notes

The full local app-server E2E path is still being debugged after PR
creation. The current branch stack is directionally ready for review
while that follow-up continues.
This commit is contained in:
Adrian authored and GitHub committed 2026-04-15 10:08:27 -07:00
1 parent 1dead46c90
commit 8e784bba2f
23 files changed
+1340 -26

No files matched your search

+85 -9
View File
@@ -13,6 +13,7 @@ use std::sync::Arc;
use std::sync::Mutex;
use std::sync::RwLock;
use tokio::sync::Mutex as AsyncMutex;
use tokio::sync::watch;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::AuthMode as ApiAuthMode;
@@ -20,6 +21,7 @@ use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ModelProviderAuthInfo;
use super::external_bearer::BearerTokenRefresher;
pub use crate::auth::storage::AgentIdentityAuthRecord;
pub use crate::auth::storage::AuthDotJson;
use crate::auth::storage::AuthStorageBackend;
use crate::auth::storage::create_auth_storage;
@@ -60,6 +62,7 @@ pub struct ChatgptAuth {
#[derive(Debug, Clone)]
pub struct ChatgptAuthTokens {
state: ChatgptAuthState,
storage: Arc<dyn AuthStorageBackend>,
}
#[derive(Debug, Clone)]
@@ -204,14 +207,13 @@ impl CodexAuth {
client,
};
let storage = create_auth_storage(codex_home.to_path_buf(), storage_mode);
match auth_mode {
ApiAuthMode::Chatgpt => {
let storage = create_auth_storage(codex_home.to_path_buf(), storage_mode);
Ok(Self::Chatgpt(ChatgptAuth { state, storage }))
}
ApiAuthMode::ChatgptAuthTokens => {
Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens { state }))
}
ApiAuthMode::Chatgpt => Ok(Self::Chatgpt(ChatgptAuth { state, storage })),
ApiAuthMode::ChatgptAuthTokens => Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens {
state,
storage,
})),
ApiAuthMode::ApiKey => unreachable!("api key mode is handled above"),
}
}
@@ -352,6 +354,52 @@ impl CodexAuth {
self.get_current_auth_json().and_then(|t| t.tokens)
}
pub fn get_agent_identity(&self, workspace_id: &str) -> Option<AgentIdentityAuthRecord> {
self.get_current_auth_json()
.and_then(|auth| auth.agent_identity)
.filter(|identity| identity.workspace_id == workspace_id)
}
pub fn set_agent_identity(&self, record: AgentIdentityAuthRecord) -> std::io::Result<()> {
let (state, storage) = match self {
Self::Chatgpt(auth) => (&auth.state, &auth.storage),
Self::ChatgptAuthTokens(auth) => (&auth.state, &auth.storage),
Self::ApiKey(_) => return Ok(()),
};
let mut guard = state
.auth_dot_json
.lock()
.map_err(|_| std::io::Error::other("failed to lock auth state"))?;
let mut auth = guard
.clone()
.ok_or_else(|| std::io::Error::other("auth data is not available"))?;
auth.agent_identity = Some(record);
storage.save(&auth)?;
*guard = Some(auth);
Ok(())
}
pub fn remove_agent_identity(&self) -> std::io::Result<bool> {
let (state, storage) = match self {
Self::Chatgpt(auth) => (&auth.state, &auth.storage),
Self::ChatgptAuthTokens(auth) => (&auth.state, &auth.storage),
Self::ApiKey(_) => return Ok(false),
};
let mut guard = state
.auth_dot_json
.lock()
.map_err(|_| std::io::Error::other("failed to lock auth state"))?;
let Some(mut auth) = guard.clone() else {
return Ok(false);
};
let removed = auth.agent_identity.take().is_some();
if removed {
storage.save(&auth)?;
*guard = Some(auth);
}
Ok(removed)
}
/// Consider this private to integration tests.
pub fn create_dummy_chatgpt_auth_for_testing() -> Self {
let auth_dot_json = AuthDotJson {
@@ -364,6 +412,7 @@ impl CodexAuth {
account_id: Some("account_id".to_string()),
}),
last_refresh: Some(Utc::now()),
agent_identity: None,
};
let client = create_client();
@@ -439,6 +488,7 @@ pub fn login_with_api_key(
openai_api_key: Some(api_key.to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
};
save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode)
}
@@ -812,6 +862,7 @@ impl AuthDotJson {
openai_api_key: None,
tokens: Some(tokens),
last_refresh: Some(Utc::now()),
agent_identity: None,
})
}
@@ -1107,6 +1158,7 @@ pub struct AuthManager {
forced_chatgpt_workspace_id: RwLock<Option<String>>,
refresh_lock: AsyncMutex<()>,
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
auth_state_tx: watch::Sender<()>,
}
/// Configuration view required to construct a shared [`AuthManager`].
@@ -1155,6 +1207,7 @@ impl AuthManager {
enable_codex_api_key_env: bool,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> Self {
let (auth_state_tx, _) = watch::channel(());
let managed_auth = load_auth(
&codex_home,
enable_codex_api_key_env,
@@ -1173,11 +1226,13 @@ impl AuthManager {
forced_chatgpt_workspace_id: RwLock::new(None),
refresh_lock: AsyncMutex::new(()),
external_auth: RwLock::new(None),
auth_state_tx,
}
}
/// Create an AuthManager with a specific CodexAuth, for testing only.
pub fn from_auth_for_testing(auth: CodexAuth) -> Arc<Self> {
let (auth_state_tx, _) = watch::channel(());
let cached = CachedAuth {
auth: Some(auth),
permanent_refresh_failure: None,
@@ -1191,11 +1246,13 @@ impl AuthManager {
forced_chatgpt_workspace_id: RwLock::new(None),
refresh_lock: AsyncMutex::new(()),
external_auth: RwLock::new(None),
auth_state_tx,
})
}
/// Create an AuthManager with a specific CodexAuth and codex home, for testing only.
pub fn from_auth_for_testing_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc<Self> {
let (auth_state_tx, _) = watch::channel(());
let cached = CachedAuth {
auth: Some(auth),
permanent_refresh_failure: None,
@@ -1208,10 +1265,12 @@ impl AuthManager {
forced_chatgpt_workspace_id: RwLock::new(None),
refresh_lock: AsyncMutex::new(()),
external_auth: RwLock::new(None),
auth_state_tx,
})
}
pub fn external_bearer_only(config: ModelProviderAuthInfo) -> Arc<Self> {
let (auth_state_tx, _) = watch::channel(());
Arc::new(Self {
codex_home: PathBuf::from("non-existent"),
inner: RwLock::new(CachedAuth {
@@ -1225,6 +1284,7 @@ impl AuthManager {
external_auth: RwLock::new(Some(
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
)),
auth_state_tx,
})
}
@@ -1364,6 +1424,7 @@ impl AuthManager {
}
tracing::info!("Reloaded auth, changed: {changed}");
guard.auth = new_auth;
self.auth_state_tx.send_replace(());
changed
} else {
false
@@ -1373,18 +1434,23 @@ impl AuthManager {
pub fn set_external_auth(&self, external_auth: Arc<dyn ExternalAuth>) {
if let Ok(mut guard) = self.external_auth.write() {
*guard = Some(external_auth);
self.auth_state_tx.send_replace(());
}
}
pub fn clear_external_auth(&self) {
if let Ok(mut guard) = self.external_auth.write() {
*guard = None;
self.auth_state_tx.send_replace(());
}
}
pub fn set_forced_chatgpt_workspace_id(&self, workspace_id: Option<String>) {
if let Ok(mut guard) = self.forced_chatgpt_workspace_id.write() {
if let Ok(mut guard) = self.forced_chatgpt_workspace_id.write()
&& *guard != workspace_id
{
*guard = workspace_id;
self.auth_state_tx.send_replace(());
}
}
@@ -1395,6 +1461,10 @@ impl AuthManager {
.and_then(|guard| guard.clone())
}
pub fn subscribe_auth_state(&self) -> watch::Receiver<()> {
self.auth_state_tx.subscribe()
}
pub fn has_external_auth(&self) -> bool {
self.external_auth().is_some()
}
@@ -1638,8 +1708,14 @@ impl AuthManager {
),
)));
}
let auth_dot_json =
let mut auth_dot_json =
AuthDotJson::from_external_tokens(&refreshed).map_err(RefreshTokenError::Transient)?;
if let Some(previous_auth) = self
.auth_cached()
.and_then(|auth| auth.get_current_auth_json())
{
auth_dot_json.agent_identity = previous_auth.agent_identity;
}
save_auth(
&self.codex_home,
&auth_dot_json,