mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
auth: let AuthManager own external bearer auth (#16287)
## Summary `AuthManager` and `UnauthorizedRecovery` already own token resolution and staged `401` recovery. The missing piece for provider auth was a bearer-only mode that still fit that design, instead of pushing a second auth abstraction into `codex-core`. This PR keeps the design centered on `AuthManager`: it teaches `codex-login` how to own external bearer auth directly so later provider work can keep calling `AuthManager.auth()` and `UnauthorizedRecovery`. ## Motivation This is the middle layer for #15189. The intended design is still: - `AuthManager` encapsulates token storage and refresh - `UnauthorizedRecovery` powers staged `401` recovery - all request tokens go through `AuthManager.auth()` This PR makes that possible for provider-backed bearer tokens by adding a bearer-only auth mode inside `AuthManager` instead of building parallel request-auth plumbing in `core`. ## What Changed - move `ModelProviderAuthInfo` into `codex-protocol` so `core` and `login` share one config shape - add `login/src/auth/external_bearer.rs`, which runs the configured command, caches the bearer token in memory, and refreshes it after `401` - add `AuthManager::external_bearer_only(...)` for provider-scoped request paths that should use command-backed bearer auth without mutating the shared OpenAI auth manager - add `AuthManager::shared_with_external_chatgpt_auth_refresher(...)` and rename the other `AuthManager` helpers that only apply to external ChatGPT auth so the ChatGPT-only path is explicit at the call site - keep external ChatGPT refresh behavior unchanged while ensuring bearer-only external auth never persists to `auth.json` ## Testing - `cargo test -p codex-login` - `cargo test -p codex-protocol` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16287). * #16288 * __->__ #16287
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::num::NonZeroU64;
|
||||
use std::time::Duration;
|
||||
use strum_macros::Display;
|
||||
use strum_macros::EnumIter;
|
||||
use ts_rs::TS;
|
||||
@@ -261,6 +264,81 @@ pub enum ForcedLoginMethod {
|
||||
Api,
|
||||
}
|
||||
|
||||
const DEFAULT_PROVIDER_AUTH_TIMEOUT_MS: u64 = 5_000;
|
||||
const DEFAULT_PROVIDER_AUTH_REFRESH_INTERVAL_MS: u64 = 300_000;
|
||||
|
||||
/// Configuration for obtaining a provider bearer token from a command.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ModelProviderAuthInfo {
|
||||
/// Command to execute. Bare names are resolved via `PATH`; paths are resolved against `cwd`.
|
||||
pub command: String,
|
||||
|
||||
/// Command arguments.
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
|
||||
/// Maximum time to wait for the token command to exit successfully.
|
||||
#[serde(default = "default_provider_auth_timeout_ms")]
|
||||
pub timeout_ms: NonZeroU64,
|
||||
|
||||
/// Maximum age for the cached token before rerunning the command.
|
||||
#[serde(default = "default_provider_auth_refresh_interval_ms")]
|
||||
pub refresh_interval_ms: NonZeroU64,
|
||||
|
||||
/// Working directory used when running the token command.
|
||||
#[serde(default = "default_provider_auth_cwd")]
|
||||
#[schemars(skip_serializing_if = "is_default_provider_auth_cwd")]
|
||||
pub cwd: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
impl ModelProviderAuthInfo {
|
||||
pub fn timeout(&self) -> Duration {
|
||||
Duration::from_millis(self.timeout_ms.get())
|
||||
}
|
||||
|
||||
pub fn refresh_interval(&self) -> Duration {
|
||||
Duration::from_millis(self.refresh_interval_ms.get())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_provider_auth_timeout_ms() -> NonZeroU64 {
|
||||
non_zero_u64(
|
||||
DEFAULT_PROVIDER_AUTH_TIMEOUT_MS,
|
||||
"model_providers.<id>.auth.timeout_ms",
|
||||
)
|
||||
}
|
||||
|
||||
fn default_provider_auth_refresh_interval_ms() -> NonZeroU64 {
|
||||
non_zero_u64(
|
||||
DEFAULT_PROVIDER_AUTH_REFRESH_INTERVAL_MS,
|
||||
"model_providers.<id>.auth.refresh_interval_ms",
|
||||
)
|
||||
}
|
||||
|
||||
fn non_zero_u64(value: u64, field_name: &str) -> NonZeroU64 {
|
||||
match NonZeroU64::new(value) {
|
||||
Some(value) => value,
|
||||
None => panic!("{field_name} must be non-zero"),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_provider_auth_cwd() -> AbsolutePathBuf {
|
||||
let deserializer = serde::de::value::StrDeserializer::<serde::de::value::Error>::new(".");
|
||||
if let Ok(cwd) = AbsolutePathBuf::deserialize(deserializer) {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
match AbsolutePathBuf::current_dir() {
|
||||
Ok(cwd) => cwd,
|
||||
Err(err) => panic!("provider auth cwd must resolve: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_default_provider_auth_cwd(path: &AbsolutePathBuf) -> bool {
|
||||
path == &default_provider_auth_cwd()
|
||||
}
|
||||
|
||||
/// Represents the trust level for a project directory.
|
||||
/// This determines the approval policy and sandbox mode applied.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
|
||||
|
||||
Reference in New Issue
Block a user