mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex-rs] support v2 personal access tokens (#25731)
## Summary - add v2 personal access token support for `codex login --with-access-token` and `CODEX_ACCESS_TOKEN` - classify opaque `at-` tokens separately from legacy Agent Identity JWTs - hydrate required ChatGPT account metadata through AuthAPI `/v1/user-auth-credential/whoami` - use PATs directly as bearer tokens while preserving existing ChatGPT account surfaces - expose PAT-backed auth as the explicit `personalAccessToken` app-server auth mode ## Implementation PAT auth is intentionally small and stateless. Loading a PAT performs one AuthAPI metadata request, stores the hydrated metadata in the in-memory auth object, and redacts the secret from debug output. Legacy Agent Identity JWT handling remains unchanged. The shared access-token classifier lives in a private neutral module because it dispatches between both credential types. PAT hydration fails closed when AuthAPI omits any required metadata, including email. Hydrated metadata is intentionally not persisted: startup performs a live `whoami` preflight so revoked tokens or changed account metadata are not accepted from a stale cache. ## Workspace restriction scope This change intentionally does **not** apply `forced_chatgpt_workspace_id` to PAT authentication. The setting is a client-side config guardrail, not an authorization boundary, and PAT does not currently require workspace-ID parity. The PAT login and `CODEX_ACCESS_TOKEN` paths therefore validate through AuthAPI without threading workspace-restriction state through access-token loading. Existing workspace checks for non-PAT auth remain on their established paths. ## App-server compatibility The public app-server `AuthMode` is shared across v1 and v2, and PAT-backed auth reports `personalAccessToken` through both APIs. Following human review, this intentionally removes the temporary v1 compatibility mapping that reported PATs as `chatgpt`; the deprecated v1 API is kept in parity with v2 rather than maintaining a separate closed enum. Clients with exhaustive auth-mode handling in either API version must add the new case and should generally treat it as ChatGPT-backed unless they need PAT-specific behavior. The v1 auth-status response still omits the raw PAT when `includeToken` is requested because that response cannot carry the account metadata needed to reuse the credential safely. Persisted PAT auth also omits the new enum value so older Codex builds can deserialize `auth.json` and infer PAT auth from the credential field after a rollback. ## Validation Latest review-fix validation: - `CARGO_INCREMENTAL=0 just test -p codex-login` (126 passed) - `CARGO_INCREMENTAL=0 just test -p codex-cli` (263 passed) - `CARGO_INCREMENTAL=0 just test -p codex-cli stored_auth_validation_handles_personal_access_token` - `CARGO_INCREMENTAL=0 just test -p codex-app-server-protocol` (226 passed) - `CARGO_INCREMENTAL=0 just test -p codex-models-manager refresh_available_models_uses_remote_only_catalog_for_chatgpt_auth` - `CARGO_INCREMENTAL=0 just test -p codex-tui existing_non_oauth_chatgpt_login_counts_as_signed_in` - `CARGO_INCREMENTAL=0 just fix -p codex-login -p codex-app-server-protocol -p codex-models-manager -p codex-tui -p codex-cli` - `just fmt` - `git diff --check` The broader `codex-tui` suite previously compiled and ran 2,834 tests. Three unrelated environment-sensitive guardian/IDE-socket tests failed after retries; the PAT-relevant TUI coverage passed.
This commit is contained in:
@@ -1308,6 +1308,7 @@ fn stored_auth_mode(auth: &codex_login::AuthDotJson) -> &'static str {
|
||||
codex_app_server_protocol::AuthMode::Chatgpt => "chatgpt",
|
||||
codex_app_server_protocol::AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens",
|
||||
codex_app_server_protocol::AuthMode::AgentIdentity => "agent_identity",
|
||||
codex_app_server_protocol::AuthMode::PersonalAccessToken => "personal_access_token",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1317,6 +1318,8 @@ fn stored_auth_mode_value(auth: &AuthDotJson) -> codex_app_server_protocol::Auth
|
||||
}
|
||||
if auth.openai_api_key.is_some() {
|
||||
codex_app_server_protocol::AuthMode::ApiKey
|
||||
} else if auth.personal_access_token.is_some() {
|
||||
codex_app_server_protocol::AuthMode::PersonalAccessToken
|
||||
} else {
|
||||
codex_app_server_protocol::AuthMode::Chatgpt
|
||||
}
|
||||
@@ -1380,6 +1383,15 @@ fn stored_auth_issues(
|
||||
issues.push("agent identity auth is missing an agent identity token");
|
||||
}
|
||||
}
|
||||
codex_app_server_protocol::AuthMode::PersonalAccessToken => {
|
||||
if auth
|
||||
.personal_access_token
|
||||
.as_deref()
|
||||
.is_none_or(|token| token.trim().is_empty())
|
||||
{
|
||||
issues.push("personal access token auth is missing a personal access token");
|
||||
}
|
||||
}
|
||||
}
|
||||
issues
|
||||
}
|
||||
@@ -2408,6 +2420,7 @@ fn auth_mode_name(auth: &CodexAuth) -> &'static str {
|
||||
codex_app_server_protocol::AuthMode::Chatgpt => "chatgpt",
|
||||
codex_app_server_protocol::AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens",
|
||||
codex_app_server_protocol::AuthMode::AgentIdentity => "agent_identity",
|
||||
codex_app_server_protocol::AuthMode::PersonalAccessToken => "personal_access_token",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2541,7 +2554,8 @@ fn provider_auth_reachability_mode_from_auth(
|
||||
Some(
|
||||
codex_app_server_protocol::AuthMode::Chatgpt
|
||||
| codex_app_server_protocol::AuthMode::ChatgptAuthTokens
|
||||
| codex_app_server_protocol::AuthMode::AgentIdentity,
|
||||
| codex_app_server_protocol::AuthMode::AgentIdentity
|
||||
| codex_app_server_protocol::AuthMode::PersonalAccessToken,
|
||||
)
|
||||
| None => ProviderAuthReachabilityMode::Chatgpt,
|
||||
}
|
||||
@@ -3401,6 +3415,7 @@ mod tests {
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -3418,6 +3433,7 @@ mod tests {
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -3429,6 +3445,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_auth_validation_handles_personal_access_token() {
|
||||
let mut auth = AuthDotJson {
|
||||
auth_mode: None,
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: Some("at-test".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(stored_auth_mode(&auth), "personal_access_token");
|
||||
assert!(stored_auth_issues(&auth, |_| false).is_empty());
|
||||
|
||||
auth.auth_mode = Some(codex_app_server_protocol::AuthMode::PersonalAccessToken);
|
||||
auth.personal_access_token = None;
|
||||
assert_eq!(
|
||||
stored_auth_issues(&auth, |_| false),
|
||||
vec!["personal access token auth is missing a personal access token"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_reachability_mode_uses_api_key_auth() {
|
||||
let api_key_auth = AuthDotJson {
|
||||
@@ -3437,6 +3475,7 @@ mod tests {
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: None,
|
||||
personal_access_token: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -391,6 +391,10 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
eprintln!("Logged in using access token");
|
||||
std::process::exit(0);
|
||||
}
|
||||
AuthMode::PersonalAccessToken => {
|
||||
eprintln!("Logged in using personal access token");
|
||||
std::process::exit(0);
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
eprintln!("Not logged in");
|
||||
|
||||
Reference in New Issue
Block a user