Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ license = "Apache-2.0"
# Internal
app_test_support = { path = "app-server/tests/common" }
codex-analytics = { path = "analytics" }
codex-agent-identity = { path = "agent-identity" }
codex-ansi-escape = { path = "ansi-escape" }
codex-api = { path = "codex-api" }
codex-aws-auth = { path = "aws-auth" }
Expand Down
1 change: 1 addition & 0 deletions codex-rs/agent-identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ codex-protocol = { workspace = true }
crypto_box = { workspace = true }
ed25519-dalek = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
Expand Down
66 changes: 66 additions & 0 deletions codex-rs/agent-identity/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::time::Duration;

use anyhow::Context;
use anyhow::Result;
Expand All @@ -21,6 +22,8 @@ use serde::Serialize;
use sha2::Digest as _;
use sha2::Sha512;

const AGENT_TASK_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(30);

/// Stored key material for a registered agent identity.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AgentIdentityKey<'a> {
Expand Down Expand Up @@ -55,6 +58,24 @@ struct AgentAssertionEnvelope {
signature: String,
}

#[derive(Serialize)]
struct RegisterTaskRequest {
timestamp: String,
signature: String,
}

#[derive(Deserialize)]
struct RegisterTaskResponse {
#[serde(default)]
task_id: Option<String>,
#[serde(default, rename = "taskId")]
task_id_camel: Option<String>,
#[serde(default)]
encrypted_task_id: Option<String>,
#[serde(default, rename = "encryptedTaskId")]
encrypted_task_id_camel: Option<String>,
}

pub fn authorization_header_for_agent_task(
key: AgentIdentityKey<'_>,
target: AgentTaskAuthorizationTarget<'_>,
Expand Down Expand Up @@ -86,6 +107,51 @@ pub fn sign_task_registration_payload(
Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes()))
}

pub async fn register_agent_task(
chatgpt_base_url: &str,
key: AgentIdentityKey<'_>,
) -> Result<String> {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let request = RegisterTaskRequest {
signature: sign_task_registration_payload(key, &timestamp)?,
timestamp,
};

let response = reqwest::Client::builder()
.timeout(AGENT_TASK_REGISTRATION_TIMEOUT)
.build()
.context("failed to build agent task registration client")?
.post(agent_task_registration_url(
chatgpt_base_url,
key.agent_runtime_id,
))
.json(&request)
.send()
.await
.context("failed to register agent task")?
.error_for_status()
.context("failed to register agent task")?
.json()
.await
.context("failed to decode agent task registration response")?;

task_id_from_register_task_response(key, response)
}

fn task_id_from_register_task_response(
key: AgentIdentityKey<'_>,
response: RegisterTaskResponse,
) -> Result<String> {
if let Some(task_id) = response.task_id.or(response.task_id_camel) {
return Ok(task_id);
}
let encrypted_task_id = response
.encrypted_task_id
.or(response.encrypted_task_id_camel)
.context("agent task registration response omitted task id")?;
decrypt_task_id_response(key, &encrypted_task_id)
}

pub fn decrypt_task_id_response(
key: AgentIdentityKey<'_>,
encrypted_task_id: &str,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion codex-rs/app-server-protocol/schema/typescript/AuthMode.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ pub enum AuthMode {
#[ts(rename = "chatgptAuthTokens")]
#[strum(serialize = "chatgptAuthTokens")]
ChatgptAuthTokens,
/// Programmatic Codex auth backed by a registered Agent Identity.
#[serde(rename = "agentIdentity")]
#[ts(rename = "agentIdentity")]
#[strum(serialize = "agentIdentity")]
AgentIdentity,
}

macro_rules! experimental_reason_expr {
Expand Down
8 changes: 6 additions & 2 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1782,7 +1782,9 @@ impl CodexMessageProcessor {
self.auth_manager.refresh_failure_for_auth(&auth).is_some();
let auth_mode = auth.api_auth_mode();
let (reported_auth_method, token_opt) =
if include_token && permanent_refresh_failure {
if matches!(auth, CodexAuth::AgentIdentity(_))
|| include_token && permanent_refresh_failure
{
(Some(auth_mode), None)
} else {
match auth.get_token() {
Expand Down Expand Up @@ -1834,7 +1836,9 @@ impl CodexMessageProcessor {
let account = match self.auth_manager.auth_cached() {
Some(auth) => match auth.auth_mode() {
CoreAuthMode::ApiKey => Some(Account::ApiKey {}),
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
CoreAuthMode::Chatgpt
| CoreAuthMode::ChatgptAuthTokens
| CoreAuthMode::AgentIdentity => {
let email = auth.get_account_email();
let plan_type = auth.account_plan_type();

Expand Down
3 changes: 3 additions & 0 deletions codex-rs/app-server/src/transport/remote_control/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ fn remote_control_auth_dot_json(account_id: Option<&str>) -> AuthDotJson {
account_id: account_id.map(str::to_string),
}),
last_refresh: Some(chrono::Utc::now()),
agent_identity: None,
}
}

Expand Down Expand Up @@ -495,6 +496,7 @@ async fn remote_control_start_allows_missing_auth_when_enabled() {
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
);
let (transport_event_tx, _transport_event_rx) =
mpsc::channel::<TransportEvent>(CHANNEL_CAPACITY);
Expand Down Expand Up @@ -1082,6 +1084,7 @@ async fn remote_control_waits_for_account_id_before_enrolling() {
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
);
let expected_server_name = gethostname().to_string_lossy().trim().to_string();
let expected_enrollment = RemoteControlEnrollment {
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/app-server/src/transport/remote_control/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,7 @@ mod tests {
account_id: Some("account_id".to_string()),
}),
last_refresh: Some(Utc::now()),
agent_identity: None,
}
}

Expand Down Expand Up @@ -1090,6 +1091,7 @@ mod tests {
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
);
let mut auth_recovery = auth_manager.unauthorized_recovery();
let mut enrollment = Some(RemoteControlEnrollment {
Expand Down Expand Up @@ -1171,6 +1173,7 @@ mod tests {
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
);
let mut auth_recovery = auth_manager.unauthorized_recovery();
let mut enrollment = None;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/common/auth_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ pub fn write_chatgpt_auth(
openai_api_key: None,
tokens: Some(tokens),
last_refresh,
agent_identity: None,
};

save_auth(codex_home, &auth, cli_auth_credentials_store_mode).context("write auth.json")
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/app_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
openai_api_key: Some("test-api-key".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
},
AuthCredentialsStoreMode::File,
)?;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/chatgpt/src/chatgpt_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub async fn init_chatgpt_token_from_auth(
codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
auth_credentials_store_mode,
/*chatgpt_base_url*/ None,
);
if let Some(auth) = auth_manager.auth().await {
let token_data = auth.get_token_data()?;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/chatgpt/src/connectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async fn apps_enabled(config: &Config) -> bool {
config.codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
config.cli_auth_credentials_store_mode,
Some(config.chatgpt_base_url.clone()),
);
let auth = auth_manager.auth().await;
config
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/cli/src/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
eprintln!("Logged in using ChatGPT");
std::process::exit(0);
}
AuthMode::AgentIdentity => {
eprintln!("Logged in using Agent Identity");
std::process::exit(0);
}
},
Ok(None) => {
eprintln!("Not logged in");
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/cloud-requirements/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,7 @@ pub fn cloud_requirements_loader_for_storage(
codex_home.clone(),
enable_codex_api_key_env,
credentials_store_mode,
Some(chatgpt_base_url.clone()),
);
cloud_requirements_loader(auth_manager, chatgpt_base_url, codex_home)
}
Expand Down Expand Up @@ -858,6 +859,7 @@ mod tests {
tmp.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
))
}

Expand All @@ -882,6 +884,7 @@ mod tests {
tmp.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
))
}

Expand Down Expand Up @@ -990,6 +993,7 @@ mod tests {
home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
)),
_home: home,
}
Expand Down Expand Up @@ -1394,6 +1398,7 @@ enabled = false
auth_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
));

write_auth_json(
Expand Down Expand Up @@ -1466,6 +1471,7 @@ enabled = false
auth_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
));

write_auth_json(
Expand Down Expand Up @@ -1596,6 +1602,7 @@ enabled = false
auth_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
));

let fetcher = Arc::new(UnauthorizedFetcher {
Expand Down
1 change: 1 addition & 0 deletions codex-rs/cloud-tasks/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub async fn load_auth_manager() -> Option<AuthManager> {
config.codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
config.cli_auth_credentials_store_mode,
Some(config.chatgpt_base_url),
))
}

Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1703,7 +1703,9 @@ impl AuthRequestTelemetryContext {
Self {
auth_mode: auth_mode.map(|mode| match mode {
AuthMode::ApiKey => "ApiKey",
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => "Chatgpt",
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens | AuthMode::AgentIdentity => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth mentioning agent identity

"Chatgpt"
}
}),
auth_header_attached: auth_telemetry.attached,
auth_header_name: auth_telemetry.name,
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,10 @@ impl AuthManagerConfig for Config {
fn forced_chatgpt_workspace_id(&self) -> Option<String> {
self.forced_chatgpt_workspace_id.clone()
}

fn chatgpt_base_url(&self) -> String {
self.chatgpt_base_url.clone()
}
}

#[derive(Clone)]
Expand Down
Loading
Loading