Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
52e8322
[codex-rs] support v2 personal access tokens [ci changed_files]
cooper-oai Jun 2, 2026
102b651
[codex-rs] preserve PAT app-server compatibility [ci changed_files]
cooper-oai Jun 2, 2026
4097f5e
[codex-rs] preserve app-server account email compatibility [ci change…
cooper-oai Jun 2, 2026
4e508fd
[codex-rs] normalize missing app-server account email [ci changed_files]
cooper-oai Jun 2, 2026
bb3aa3c
[codex-rs] complete PAT auth refresh fixtures [ci changed_files]
cooper-oai Jun 2, 2026
56418b3
[codex-rs] handle PAT auth edge cases [ci changed_files]
cooper-oai Jun 2, 2026
1e25316
[codex-rs] simplify PAT auth integration [ci changed_files]
cooper-oai Jun 2, 2026
8b4619b
[codex-rs] expose personal access token auth mode [ci changed_files]
cooper-oai Jun 2, 2026
7851726
[codex-rs] align PAT tests with review feedback [ci changed_files]
cooper-oai Jun 2, 2026
96cb93c
[codex-rs] add PAT turn integration coverage [ci changed_files]
cooper-oai Jun 2, 2026
691c1eb
[codex-rs] validate PAT workspace before persistence [ci changed_files]
cooper-oai Jun 2, 2026
de7e938
[codex-rs] keep PAT auth mode out of v1 [ci changed_files]
cooper-oai Jun 2, 2026
c7d1363
[codex-rs] preserve PAT account compatibility [ci changed_files]
cooper-oai Jun 2, 2026
d0eacff
[codex-rs] update PAT cloud bundle test fixture [ci changed_files]
cooper-oai Jun 3, 2026
dd0336b
[codex-rs] simplify PAT auth branches [ci changed_files]
cooper-oai Jun 3, 2026
9f1db45
[codex-rs] reject PAT metadata without email [ci changed_files]
cooper-oai Jun 3, 2026
3070fde
[codex-rs] use valid PAT metadata test fixtures [ci changed_files]
cooper-oai Jun 3, 2026
41c0903
[codex-rs] align PAT auth modes across app-server APIs [ci changed_fi…
cooper-oai Jun 5, 2026
086ab2b
[codex-rs] enforce PAT workspace restrictions on load [ci changed_files]
cooper-oai Jun 5, 2026
8a0e4f3
[codex-rs] preserve PAT auth rollback compatibility [ci changed_files]
cooper-oai Jun 5, 2026
c4a77fe
[codex-rs] address PAT review feedback [ci changed_files]
cooper-oai Jun 5, 2026
2008f30
[codex-rs] infer PAT auth in doctor [ci changed_files]
cooper-oai Jun 6, 2026
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

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.

15 changes: 15 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ pub enum AuthMode {
#[ts(rename = "agentIdentity")]
#[strum(serialize = "agentIdentity")]
AgentIdentity,
/// Programmatic Codex auth backed by a personal access token.
#[serde(rename = "personalAccessToken")]
#[ts(rename = "personalAccessToken")]
#[strum(serialize = "personalAccessToken")]
PersonalAccessToken,
Comment thread
cooper-oai marked this conversation as resolved.
Comment thread
cooper-oai marked this conversation as resolved.
Comment thread
cooper-oai marked this conversation as resolved.
Comment thread
cooper-oai marked this conversation as resolved.
Comment thread
cooper-oai marked this conversation as resolved.
Comment thread
cooper-oai marked this conversation as resolved.
Comment thread
cooper-oai marked this conversation as resolved.
}

impl AuthMode {
/// Returns whether this mode represents an authenticated human ChatGPT account.
pub fn has_chatgpt_account(self) -> bool {
match self {
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
Self::ApiKey | Self::AgentIdentity => false,
}
}
}

macro_rules! experimental_reason_expr {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ fn remote_control_auth_dot_json(account_id: Option<&str>) -> AuthDotJson {
}),
last_refresh: Some(chrono::Utc::now()),
agent_identity: None,
personal_access_token: None,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1769,6 +1769,7 @@ mod tests {
}),
last_refresh: Some(Utc::now()),
agent_identity: None,
personal_access_token: None,
}
}

Expand Down
3 changes: 2 additions & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco

- **API key (`apiKey`)**: Caller supplies an OpenAI API key via `account/login/start` with `type: "apiKey"`. The API key is saved and used for API requests.
- **ChatGPT managed (`chatgpt`)** (recommended): Codex owns the ChatGPT OAuth flow and refresh tokens. Start via `account/login/start` with `type: "chatgpt"` for the browser flow or `type: "chatgptDeviceCode"` for device code; Codex persists tokens to disk and refreshes them automatically.
- **Personal access token (`personalAccessToken`)**: Codex uses a ChatGPT-backed personal access token loaded outside the app-server login RPCs, such as with `codex login --with-access-token` or `CODEX_ACCESS_TOKEN`.
Comment thread
cooper-oai marked this conversation as resolved.

### API Overview

Expand All @@ -1778,7 +1779,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
- `account/login/completed` (notify) — emitted when a login attempt finishes (success or error).
- `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`.
- `account/logout` — sign out; triggers `account/updated`.
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available.
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, `personalAccessToken`, or `null`) and includes the current ChatGPT `planType` when available.
- `account/rateLimits/read` — fetch ChatGPT rate limits and an optional effective monthly credit limit; updates arrive via `account/rateLimits/updated` (notify).
- `account/usage/read` — fetch ChatGPT account token-activity summary and daily buckets.
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change. This is a sparse rolling update; merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot.
Expand Down
1 change: 0 additions & 1 deletion codex-rs/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,6 @@ use codex_mcp::discover_supported_scopes;
use codex_mcp::read_mcp_resource as read_mcp_resource_without_thread;
use codex_mcp::resolve_oauth_scopes;
use codex_memories_write::clear_memory_roots_contents;
use codex_model_provider::ProviderAccountError;
use codex_model_provider::create_model_provider;
use codex_models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets;
use codex_protocol::ThreadId;
Expand Down
44 changes: 22 additions & 22 deletions codex-rs/app-server/src/request_processors/account_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,24 +776,28 @@ impl AccountRequestProcessor {
let permanent_refresh_failure =
self.auth_manager.refresh_failure_for_auth(&auth).is_some();
let auth_mode = auth.api_auth_mode();
let (reported_auth_method, token_opt) =
if matches!(auth, CodexAuth::AgentIdentity(_))
|| include_token && permanent_refresh_failure
{
(Some(auth_mode), None)
} else {
match auth.get_token() {
Ok(token) if !token.is_empty() => {
let tok = if include_token { Some(token) } else { None };
(Some(auth_mode), tok)
}
Ok(_) => (None, None),
Err(err) => {
tracing::warn!("failed to get token for auth status: {err}");
(None, None)
}
let (reported_auth_method, token_opt) = if matches!(
auth,
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_)
) || include_token
&& permanent_refresh_failure
{
// This response cannot represent the metadata needed to reuse these
// credentials.
(Some(auth_mode), None)
} else {
match auth.get_token() {
Ok(token) if !token.is_empty() => {
let tok = if include_token { Some(token) } else { None };
(Some(auth_mode), tok)
}
Ok(_) => (None, None),
Err(err) => {
tracing::warn!("failed to get token for auth status: {err}");
(None, None)
}
};
}
};
GetAuthStatusResponse {
auth_method: reported_auth_method,
auth_token: token_opt,
Expand Down Expand Up @@ -825,11 +829,7 @@ impl AccountRequestProcessor {
);
let account_state = match provider.account_state() {
Ok(account_state) => account_state,
Err(ProviderAccountError::MissingChatgptAccountDetails) => {
return Err(invalid_request(
"email and plan type are required for chatgpt authentication",
));
}
Err(err) => return Err(invalid_request(err.to_string())),
};
let account = account_state.account.map(Account::from);

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 @@ -164,6 +164,7 @@ pub fn write_chatgpt_auth(
tokens: Some(tokens),
last_refresh,
agent_identity: None,
personal_access_token: None,
};

save_auth(codex_home, &auth, cli_auth_credentials_store_mode).context("write auth.json")
Expand Down
59 changes: 59 additions & 0 deletions codex-rs/app-server/tests/suite/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use tokio::time::timeout;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;

Expand Down Expand Up @@ -160,6 +161,64 @@ async fn get_auth_status_with_api_key() -> Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_auth_status_with_personal_access_token_omits_token() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path())?;

let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/user-auth-credential/whoami"))
.and(header("Authorization", "Bearer at-test-token"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"email": "user@example.com",
"chatgpt_user_id": "user-123",
"chatgpt_account_id": "account-123",
"chatgpt_plan_type": "pro",
"chatgpt_account_is_fedramp": false,
})))
.expect(1..)
.mount(&server)
.await;

let authapi_base_url = server.uri();
let mut mcp = TestAppServer::new_with_env(
codex_home.path(),
&[
("OPENAI_API_KEY", None),
("CODEX_ACCESS_TOKEN", Some("at-test-token")),
("CODEX_AUTHAPI_BASE_URL", Some(authapi_base_url.as_str())),
],
)
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

let request_id = mcp
.send_get_auth_status_request(GetAuthStatusParams {
include_token: Some(true),
refresh_token: Some(false),
})
.await?;

let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let status: GetAuthStatusResponse = to_response(resp)?;
assert_eq!(
status,
GetAuthStatusResponse {
auth_method: Some(AuthMode::PersonalAccessToken),
auth_token: None,
requires_openai_auth: Some(true),
}
);

server.verify().await;
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_auth_status_with_api_key_when_auth_not_required() -> Result<()> {
let codex_home = TempDir::new()?;
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 @@ -118,6 +118,7 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
tokens: None,
last_refresh: None,
agent_identity: None,
personal_access_token: None,
},
AuthCredentialsStoreMode::File,
)?;
Expand Down
12 changes: 4 additions & 8 deletions codex-rs/chatgpt/src/workspace_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::sync::RwLock;
use std::time::Duration;
use std::time::Instant;

use anyhow::Context;
use codex_core::config::Config;
use codex_login::CodexAuth;
use serde::Deserialize;
Expand Down Expand Up @@ -93,28 +92,25 @@ pub async fn codex_plugins_enabled_for_workspace(
return Ok(true);
}

let token_data = auth
.get_token_data()
.context("ChatGPT token data is not available")?;
if !token_data.id_token.is_workspace_account() {
if !auth.is_workspace_account() {
return Ok(true);
}

let Some(account_id) = token_data.account_id.as_deref().filter(|id| !id.is_empty()) else {
let Some(account_id) = auth.get_account_id().filter(|id| !id.is_empty()) else {
return Ok(true);
};

let cache_key = WorkspaceSettingsCacheKey {
chatgpt_base_url: config.chatgpt_base_url.clone(),
account_id: account_id.to_string(),
account_id: account_id.clone(),
};
if let Some(cache) = cache
&& let Some(enabled) = cache.get_codex_plugins_enabled(&cache_key)
{
return Ok(enabled);
}

let encoded_account_id = encode_path_segment(account_id);
let encoded_account_id = encode_path_segment(&account_id);
let settings: WorkspaceSettingsResponse = chatgpt_get_request_with_timeout(
config,
format!("/accounts/{encoded_account_id}/settings"),
Expand Down
Loading
Loading