mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
refactor: route Codex auth through AuthProvider (#18811)
## Summary This PR moves Codex backend request authentication from direct bearer-token handling to `AuthProvider`. The new `codex-auth-provider` crate defines the shared request-auth trait. `CodexAuth::provider()` returns a provider that can apply all headers needed for the selected auth mode. This lets ChatGPT token auth and AgentIdentity auth share the same callsite path: - ChatGPT token auth applies bearer auth plus account/FedRAMP headers where needed. - AgentIdentity auth applies AgentAssertion plus account/FedRAMP headers where needed. Reference old stack: https://github.com/openai/codex/pull/17387/changes ## Callsite Migration | Area | Change | | --- | --- | | backend-client | accepts an `AuthProvider` instead of a raw token/header | | chatgpt client/connectors | applies auth through `CodexAuth::provider()` | | cloud tasks | keeps Codex-backend gating, applies auth through provider | | cloud requirements | uses Codex-backend auth checks and provider headers | | app-server remote control | applies provider headers for backend calls | | MCP Apps/connectors | gates on `uses_codex_backend()` and keys caches from generic account getters | | model refresh | treats AgentIdentity as Codex-backend auth | | OpenAI file upload path | rejects non-Codex-backend auth before applying headers | | core client setup | keeps model-provider auth flow and allows AgentIdentity through provider-backed OpenAI auth | ## Stack 1. https://github.com/openai/codex/pull/18757: full revert 2. https://github.com/openai/codex/pull/18871: isolated Agent Identity crate 3. https://github.com/openai/codex/pull/18785: explicit AgentIdentity auth mode and startup task allocation 4. This PR: migrate Codex backend auth callsites through AuthProvider 5. https://github.com/openai/codex/pull/18904: accept AgentIdentity JWTs and load `CODEX_AGENT_IDENTITY` ## Testing Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI.
This commit is contained in:
committed by
GitHub
Unverified
parent
a9f75e5cda
commit
5882f3f95e
Generated
+14
-2
@@ -1773,6 +1773,7 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -1840,6 +1841,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-analytics",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
@@ -1856,6 +1858,7 @@ dependencies = [
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
"codex-otel",
|
||||
@@ -2045,9 +2048,11 @@ name = "codex-backend-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codex-api",
|
||||
"codex-backend-openapi-models",
|
||||
"codex-client",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
@@ -2071,11 +2076,11 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-config",
|
||||
"codex-connectors",
|
||||
"codex-core",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
"pretty_assertions",
|
||||
@@ -2203,7 +2208,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-client",
|
||||
@@ -2212,6 +2216,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-tui",
|
||||
"codex-utils-cli",
|
||||
"crossterm",
|
||||
@@ -2236,6 +2241,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"codex-api",
|
||||
"codex-backend-client",
|
||||
"codex-git-utils",
|
||||
"serde",
|
||||
@@ -2460,6 +2466,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-git-utils",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
@@ -2491,6 +2498,7 @@ dependencies = [
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-skills",
|
||||
@@ -2849,10 +2857,12 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"codex-api",
|
||||
"codex-async-utils",
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-otel",
|
||||
"codex-plugin",
|
||||
"codex-protocol",
|
||||
@@ -2912,6 +2922,7 @@ name = "codex-model-provider"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"codex-agent-identity",
|
||||
"codex-api",
|
||||
"codex-aws-auth",
|
||||
"codex-client",
|
||||
@@ -3154,6 +3165,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
"codex-api",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
|
||||
@@ -16,6 +16,7 @@ workspace = true
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
os_info = { workspace = true }
|
||||
|
||||
@@ -312,16 +312,9 @@ async fn send_track_events(
|
||||
let Some(auth) = auth_manager.auth().await else {
|
||||
return;
|
||||
};
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
return;
|
||||
}
|
||||
let access_token = match auth.get_token() {
|
||||
Ok(token) => token,
|
||||
Err(_) => return,
|
||||
};
|
||||
let Some(account_id) = auth.get_account_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let base_url = base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/codex/analytics-events/events");
|
||||
@@ -330,8 +323,7 @@ async fn send_track_events(
|
||||
let response = create_client()
|
||||
.post(&url)
|
||||
.timeout(ANALYTICS_EVENTS_TIMEOUT)
|
||||
.bearer_auth(&access_token)
|
||||
.header("chatgpt-account-id", &account_id)
|
||||
.headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers())
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()
|
||||
|
||||
@@ -30,6 +30,7 @@ axum = { workspace = true, default-features = false, features = [
|
||||
"ws",
|
||||
] }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
@@ -48,6 +49,7 @@ codex-file-search = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
|
||||
@@ -1943,7 +1943,7 @@ impl CodexMessageProcessor {
|
||||
});
|
||||
};
|
||||
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "chatgpt authentication required to notify workspace owner".to_string(),
|
||||
@@ -1998,7 +1998,7 @@ impl CodexMessageProcessor {
|
||||
});
|
||||
};
|
||||
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "chatgpt authentication required to read rate limits".to_string(),
|
||||
@@ -5909,8 +5909,8 @@ impl CodexMessageProcessor {
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let runtime_environment = match environment_manager.default_environment() {
|
||||
Some(environment) => {
|
||||
// Status listing has no turn cwd. This fallback is used by
|
||||
// stdio MCPs whose config omits `cwd`.
|
||||
// Status listing has no turn cwd. This fallback is used only
|
||||
// by executor-backed stdio MCPs whose config omits `cwd`.
|
||||
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf())
|
||||
}
|
||||
None => McpRuntimeEnvironment::new(
|
||||
@@ -6414,7 +6414,7 @@ impl CodexMessageProcessor {
|
||||
let auth = self.auth_manager.auth().await;
|
||||
if !config
|
||||
.features
|
||||
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth))
|
||||
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
|
||||
{
|
||||
self.outgoing
|
||||
.send_response(
|
||||
|
||||
@@ -1078,7 +1078,7 @@ impl MessageProcessor {
|
||||
let auth = self.auth_manager.auth().await;
|
||||
if !config.features.apps_enabled_for_auth(
|
||||
auth.as_ref()
|
||||
.is_some_and(codex_login::CodexAuth::is_chatgpt_auth),
|
||||
.is_some_and(codex_login::CodexAuth::uses_codex_backend),
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::protocol::EnrollRemoteServerRequest;
|
||||
use super::protocol::EnrollRemoteServerResponse;
|
||||
use super::protocol::RemoteControlTarget;
|
||||
use axum::http::HeaderMap;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_state::RemoteControlEnrollmentRecord;
|
||||
use codex_state::StateRuntime;
|
||||
@@ -27,9 +28,8 @@ pub(super) struct RemoteControlEnrollment {
|
||||
pub(super) server_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct RemoteControlConnectionAuth {
|
||||
pub(super) bearer_token: String,
|
||||
pub(super) auth_provider: SharedAuthProvider,
|
||||
pub(super) account_id: String,
|
||||
}
|
||||
|
||||
@@ -199,10 +199,12 @@ pub(super) async fn enroll_remote_control_server(
|
||||
app_server_version: env!("CARGO_PKG_VERSION"),
|
||||
};
|
||||
let client = build_reqwest_client();
|
||||
let mut auth_headers = HeaderMap::new();
|
||||
auth.auth_provider.add_auth_headers(&mut auth_headers);
|
||||
let http_request = client
|
||||
.post(enroll_url)
|
||||
.timeout(REMOTE_CONTROL_ENROLL_TIMEOUT)
|
||||
.bearer_auth(&auth.bearer_token)
|
||||
.headers(auth_headers)
|
||||
.header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)
|
||||
.json(&request);
|
||||
|
||||
@@ -445,7 +447,7 @@ mod tests {
|
||||
let err = enroll_remote_control_server(
|
||||
&remote_control_target,
|
||||
&RemoteControlConnectionAuth {
|
||||
bearer_token: "Access Token".to_string(),
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
account_id: "account_id".to_string(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -680,11 +680,9 @@ fn build_remote_control_websocket_request(
|
||||
"x-codex-protocol-version",
|
||||
REMOTE_CONTROL_PROTOCOL_VERSION,
|
||||
)?;
|
||||
set_remote_control_header(
|
||||
headers,
|
||||
"authorization",
|
||||
&format!("Bearer {}", auth.bearer_token),
|
||||
)?;
|
||||
let mut auth_headers = tungstenite::http::HeaderMap::new();
|
||||
auth.auth_provider.add_auth_headers(&mut auth_headers);
|
||||
headers.extend(auth_headers);
|
||||
set_remote_control_header(headers, REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)?;
|
||||
if let Some(subscribe_cursor) = subscribe_cursor {
|
||||
set_remote_control_header(
|
||||
@@ -712,7 +710,7 @@ pub(crate) async fn load_remote_control_auth(
|
||||
reloaded = true;
|
||||
continue;
|
||||
};
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
break auth;
|
||||
}
|
||||
if auth.get_account_id().is_none() && !reloaded {
|
||||
@@ -723,7 +721,7 @@ pub(crate) async fn load_remote_control_auth(
|
||||
break auth;
|
||||
};
|
||||
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(io::Error::new(
|
||||
ErrorKind::PermissionDenied,
|
||||
"remote control requires ChatGPT authentication; API key auth is not supported",
|
||||
@@ -731,7 +729,7 @@ pub(crate) async fn load_remote_control_auth(
|
||||
}
|
||||
|
||||
Ok(RemoteControlConnectionAuth {
|
||||
bearer_token: auth.get_token().map_err(io::Error::other)?,
|
||||
auth_provider: codex_model_provider::auth_provider_from_auth(&auth),
|
||||
account_id: auth.get_account_id().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
ErrorKind::WouldBlock,
|
||||
|
||||
@@ -17,8 +17,10 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
codex-backend-openapi-models = { path = "../codex-backend-openapi-models" }
|
||||
codex-api = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::types::RateLimitReachedKind as BackendRateLimitReachedKind;
|
||||
use crate::types::RateLimitStatusPayload;
|
||||
use crate::types::TurnAttemptsSiblingTurnsResponse;
|
||||
use anyhow::Result;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_client::with_chatgpt_cloudflare_cookie_store;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -15,7 +16,6 @@ use codex_protocol::protocol::RateLimitReachedType;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::header::HeaderName;
|
||||
@@ -113,17 +113,33 @@ impl PathStyle {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
base_url: String,
|
||||
http: reqwest::Client,
|
||||
bearer_token: Option<String>,
|
||||
auth_provider: SharedAuthProvider,
|
||||
user_agent: Option<HeaderValue>,
|
||||
chatgpt_account_id: Option<String>,
|
||||
chatgpt_account_is_fedramp: bool,
|
||||
path_style: PathStyle,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Client {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Client")
|
||||
.field("base_url", &self.base_url)
|
||||
.field("auth_provider", &"<provider>")
|
||||
.field("user_agent", &self.user_agent)
|
||||
.field("chatgpt_account_id", &self.chatgpt_account_id)
|
||||
.field(
|
||||
"chatgpt_account_is_fedramp",
|
||||
&self.chatgpt_account_is_fedramp,
|
||||
)
|
||||
.field("path_style", &self.path_style)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(base_url: impl Into<String>) -> Result<Self> {
|
||||
let mut base_url = base_url.into();
|
||||
@@ -145,7 +161,7 @@ impl Client {
|
||||
Ok(Self {
|
||||
base_url,
|
||||
http,
|
||||
bearer_token: None,
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
@@ -154,21 +170,13 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn from_auth(base_url: impl Into<String>, auth: &CodexAuth) -> Result<Self> {
|
||||
let token = auth.get_token().map_err(anyhow::Error::from)?;
|
||||
let mut client = Self::new(base_url)?
|
||||
Ok(Self::new(base_url)?
|
||||
.with_user_agent(get_codex_user_agent())
|
||||
.with_bearer_token(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
client = client.with_chatgpt_account_id(account_id);
|
||||
}
|
||||
if auth.is_fedramp_account() {
|
||||
client = client.with_fedramp_routing_header();
|
||||
}
|
||||
Ok(client)
|
||||
.with_auth_provider(codex_model_provider::auth_provider_from_auth(auth)))
|
||||
}
|
||||
|
||||
pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
|
||||
self.bearer_token = Some(token.into());
|
||||
pub fn with_auth_provider(mut self, auth: SharedAuthProvider) -> Self {
|
||||
self.auth_provider = auth;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -201,12 +209,7 @@ impl Client {
|
||||
} else {
|
||||
h.insert(USER_AGENT, HeaderValue::from_static("codex-cli"));
|
||||
}
|
||||
if let Some(token) = &self.bearer_token {
|
||||
let value = format!("Bearer {token}");
|
||||
if let Ok(hv) = HeaderValue::from_str(&value) {
|
||||
h.insert(AUTHORIZATION, hv);
|
||||
}
|
||||
}
|
||||
self.auth_provider.add_auth_headers(&mut h);
|
||||
if let Some(acc) = &self.chatgpt_account_id
|
||||
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
|
||||
&& let Ok(hv) = HeaderValue::from_str(acc)
|
||||
@@ -819,7 +822,7 @@ mod tests {
|
||||
let codex_client = Client {
|
||||
base_url: "https://example.test".to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
bearer_token: None,
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
@@ -833,7 +836,7 @@ mod tests {
|
||||
let chatgpt_client = Client {
|
||||
base_url: "https://chatgpt.com/backend-api".to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
bearer_token: None,
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
|
||||
@@ -12,10 +12,10 @@ anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-connectors = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -6,7 +6,6 @@ use codex_git_utils::ApplyGitRequest;
|
||||
use codex_git_utils::apply_git_patch;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
|
||||
use crate::chatgpt_token::init_chatgpt_token_from_auth;
|
||||
use crate::get_task::GetTaskResponse;
|
||||
use crate::get_task::OutputItem;
|
||||
use crate::get_task::PrOutputItem;
|
||||
@@ -32,9 +31,6 @@ pub async fn run_apply_command(
|
||||
)
|
||||
.await?;
|
||||
|
||||
init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
|
||||
.await?;
|
||||
|
||||
let task_response = get_task(&config, apply_cli.task_id).await?;
|
||||
apply_diff_from_task(task_response, cwd).await
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use codex_core::config::Config;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::default_client::create_client;
|
||||
|
||||
use crate::chatgpt_token::get_chatgpt_token_data;
|
||||
use crate::chatgpt_token::init_chatgpt_token_from_auth;
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::time::Duration;
|
||||
@@ -22,24 +20,28 @@ pub(crate) async fn chatgpt_get_request_with_timeout<T: DeserializeOwned>(
|
||||
timeout: Option<Duration>,
|
||||
) -> anyhow::Result<T> {
|
||||
let chatgpt_base_url = &config.chatgpt_base_url;
|
||||
init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
|
||||
.await?;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false);
|
||||
let auth = auth_manager
|
||||
.auth()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("ChatGPT auth not available"))?;
|
||||
anyhow::ensure!(
|
||||
auth.uses_codex_backend(),
|
||||
"ChatGPT backend requests require Codex backend auth"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
auth.get_account_id().is_some(),
|
||||
"ChatGPT account ID not available, please re-run `codex login`"
|
||||
);
|
||||
|
||||
// Make direct HTTP request to ChatGPT backend API with the token
|
||||
let client = create_client();
|
||||
let url = format!("{chatgpt_base_url}{path}");
|
||||
|
||||
let token =
|
||||
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
|
||||
|
||||
let account_id = token.account_id.ok_or_else(|| {
|
||||
anyhow::anyhow!("ChatGPT account ID not available, please re-run `codex login`")
|
||||
});
|
||||
|
||||
let mut request = client
|
||||
.get(&url)
|
||||
.bearer_auth(&token.access_token)
|
||||
.header("chatgpt-account-id", account_id?)
|
||||
.headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if let Some(timeout) = timeout {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::token_data::TokenData;
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::RwLock;
|
||||
|
||||
static CHATGPT_TOKEN: LazyLock<RwLock<Option<TokenData>>> = LazyLock::new(|| RwLock::new(None));
|
||||
|
||||
pub fn get_chatgpt_token_data() -> Option<TokenData> {
|
||||
CHATGPT_TOKEN.read().ok()?.clone()
|
||||
}
|
||||
|
||||
pub fn set_chatgpt_token_data(value: TokenData) {
|
||||
if let Ok(mut guard) = CHATGPT_TOKEN.write() {
|
||||
*guard = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the ChatGPT token from auth.json file
|
||||
pub async fn init_chatgpt_token_from_auth(
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<()> {
|
||||
let auth_manager = AuthManager::new(
|
||||
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()?;
|
||||
set_chatgpt_token_data(token_data);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,8 +2,6 @@ use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chatgpt_client::chatgpt_get_request_with_timeout;
|
||||
use crate::chatgpt_token::get_chatgpt_token_data;
|
||||
use crate::chatgpt_token::init_chatgpt_token_from_auth;
|
||||
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_connectors::AllConnectorsCacheKey;
|
||||
@@ -23,22 +21,32 @@ use codex_core::plugins::PluginsManager;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_login::token_data::TokenData;
|
||||
|
||||
const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
async fn apps_enabled(config: &Config) -> bool {
|
||||
let auth_manager = AuthManager::shared(
|
||||
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_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false);
|
||||
let auth = auth_manager.auth().await;
|
||||
config
|
||||
.features
|
||||
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth))
|
||||
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
|
||||
}
|
||||
|
||||
async fn connector_auth(config: &Config) -> anyhow::Result<CodexAuth> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false);
|
||||
let auth = auth_manager
|
||||
.auth()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("ChatGPT auth not available"))?;
|
||||
anyhow::ensure!(
|
||||
auth.uses_codex_backend(),
|
||||
"ChatGPT connectors require Codex backend auth"
|
||||
);
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
pub async fn list_connectors(config: &Config) -> anyhow::Result<Vec<AppInfo>> {
|
||||
if !apps_enabled(config).await {
|
||||
return Ok(Vec::new());
|
||||
@@ -66,14 +74,8 @@ pub async fn list_cached_all_connectors(config: &Config) -> Option<Vec<AppInfo>>
|
||||
return Some(Vec::new());
|
||||
}
|
||||
|
||||
if init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let token_data = get_chatgpt_token_data()?;
|
||||
let cache_key = all_connectors_cache_key(config, &token_data);
|
||||
let auth = connector_auth(config).await.ok()?;
|
||||
let cache_key = all_connectors_cache_key(config, &auth);
|
||||
let connectors = codex_connectors::cached_all_connectors(&cache_key)?;
|
||||
let connectors = merge_plugin_connectors(
|
||||
connectors,
|
||||
@@ -95,15 +97,11 @@ pub async fn list_all_connectors_with_options(
|
||||
if !apps_enabled(config).await {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
init_chatgpt_token_from_auth(&config.codex_home, config.cli_auth_credentials_store_mode)
|
||||
.await?;
|
||||
|
||||
let token_data =
|
||||
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
|
||||
let cache_key = all_connectors_cache_key(config, &token_data);
|
||||
let auth = connector_auth(config).await?;
|
||||
let cache_key = all_connectors_cache_key(config, &auth);
|
||||
let connectors = codex_connectors::list_all_connectors_with_options(
|
||||
cache_key,
|
||||
token_data.id_token.is_workspace_account(),
|
||||
auth.is_workspace_account(),
|
||||
force_refetch,
|
||||
|path| async move {
|
||||
chatgpt_get_request_with_timeout::<DirectoryListResponse>(
|
||||
@@ -128,12 +126,12 @@ pub async fn list_all_connectors_with_options(
|
||||
))
|
||||
}
|
||||
|
||||
fn all_connectors_cache_key(config: &Config, token_data: &TokenData) -> AllConnectorsCacheKey {
|
||||
fn all_connectors_cache_key(config: &Config, auth: &CodexAuth) -> AllConnectorsCacheKey {
|
||||
AllConnectorsCacheKey::new(
|
||||
config.chatgpt_base_url.clone(),
|
||||
token_data.account_id.clone(),
|
||||
token_data.id_token.chatgpt_user_id.clone(),
|
||||
token_data.id_token.is_workspace_account(),
|
||||
auth.get_account_id(),
|
||||
auth.get_chatgpt_user_id(),
|
||||
auth.is_workspace_account(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod apply_command;
|
||||
mod chatgpt_client;
|
||||
mod chatgpt_token;
|
||||
pub mod connectors;
|
||||
pub mod get_task;
|
||||
|
||||
@@ -486,8 +486,12 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
|
||||
|
||||
let mut entries: Vec<_> = mcp_servers.iter().collect();
|
||||
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
let auth_statuses =
|
||||
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await;
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
/*auth*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
if list_args.json {
|
||||
let json_entries: Vec<_> = entries
|
||||
|
||||
@@ -176,13 +176,7 @@ fn verify_cache_signature(payload_bytes: &[u8], signature: &str) -> bool {
|
||||
}
|
||||
|
||||
fn auth_identity(auth: &CodexAuth) -> (Option<String>, Option<String>) {
|
||||
let token_data = auth.get_token_data().ok();
|
||||
let chatgpt_user_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.id_token.chatgpt_user_id.as_deref())
|
||||
.map(str::to_owned);
|
||||
let account_id = auth.get_account_id();
|
||||
(chatgpt_user_id, account_id)
|
||||
(auth.get_chatgpt_user_id(), auth.get_account_id())
|
||||
}
|
||||
|
||||
fn cache_payload_bytes(payload: &CloudRequirementsCacheSignedPayload) -> Option<Vec<u8>> {
|
||||
@@ -338,7 +332,7 @@ impl CloudRequirementsService {
|
||||
let Some(plan_type) = auth.account_plan_type() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !auth.is_chatgpt_auth()
|
||||
if !auth.uses_codex_backend()
|
||||
|| !(plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise))
|
||||
{
|
||||
return Ok(None);
|
||||
@@ -558,7 +552,7 @@ impl CloudRequirementsService {
|
||||
let Some(plan_type) = auth.account_plan_type() else {
|
||||
return false;
|
||||
};
|
||||
if !auth.is_chatgpt_auth()
|
||||
if !auth.uses_codex_backend()
|
||||
|| !(plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise))
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -15,6 +15,7 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
codex-api = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::api::TaskText;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_backend_client as backend;
|
||||
use codex_backend_client::CodeTaskDetailsResponseExt;
|
||||
use codex_git_utils::ApplyGitRequest;
|
||||
@@ -32,13 +33,13 @@ impl HttpClient {
|
||||
Ok(Self { base_url, backend })
|
||||
}
|
||||
|
||||
pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
|
||||
self.backend = self.backend.clone().with_bearer_token(token);
|
||||
pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
|
||||
self.backend = self.backend.clone().with_user_agent(ua);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
|
||||
self.backend = self.backend.clone().with_user_agent(ua);
|
||||
pub fn with_auth_provider(mut self, auth: SharedAuthProvider) -> Self {
|
||||
self.backend = self.backend.clone().with_auth_provider(auth);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-client = { workspace = true }
|
||||
@@ -23,6 +22,7 @@ codex-cloud-tasks-mock-client = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { path = "../login" }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-tui = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
|
||||
@@ -68,7 +68,7 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext>
|
||||
};
|
||||
append_error_log(format!("startup: base_url={base_url} path_style={style}"));
|
||||
|
||||
let auth_manager = util::load_auth_manager().await;
|
||||
let auth_manager = util::load_auth_manager(Some(base_url.clone())).await;
|
||||
let auth = match auth_manager.as_ref() {
|
||||
Some(manager) => manager.auth().await,
|
||||
None => None,
|
||||
@@ -87,23 +87,17 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext>
|
||||
append_error_log(format!("auth: mode=ChatGPT account_id={acc}"));
|
||||
}
|
||||
|
||||
let token = match auth.get_token() {
|
||||
Ok(t) if !t.is_empty() => t,
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
if !auth.uses_codex_backend() {
|
||||
eprintln!(
|
||||
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
http = http.with_bearer_token(token.clone());
|
||||
if let Some(acc) = auth
|
||||
.get_account_id()
|
||||
.or_else(|| util::extract_chatgpt_account_id(&token))
|
||||
{
|
||||
let auth_provider = codex_model_provider::auth_provider_from_auth(&auth);
|
||||
http = http.with_auth_provider(auth_provider);
|
||||
if let Some(acc) = auth.get_account_id() {
|
||||
append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
|
||||
http = http.with_chatgpt_account_id(acc);
|
||||
}
|
||||
|
||||
Ok(BackendContext {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use base64::Engine as _;
|
||||
use chrono::DateTime;
|
||||
use chrono::Local;
|
||||
use chrono::Utc;
|
||||
@@ -42,39 +41,20 @@ pub fn normalize_base_url(input: &str) -> String {
|
||||
base_url
|
||||
}
|
||||
|
||||
/// Extract the ChatGPT account id from a JWT token, when present.
|
||||
pub fn extract_chatgpt_account_id(token: &str) -> Option<String> {
|
||||
let mut parts = token.split('.');
|
||||
let (_h, payload_b64, _s) = match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s),
|
||||
_ => return None,
|
||||
};
|
||||
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(payload_b64)
|
||||
.ok()?;
|
||||
let v: serde_json::Value = serde_json::from_slice(&payload_bytes).ok()?;
|
||||
v.get("https://api.openai.com/auth")
|
||||
.and_then(|auth| auth.get("chatgpt_account_id"))
|
||||
.and_then(|id| id.as_str())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub async fn load_auth_manager() -> Option<AuthManager> {
|
||||
pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<AuthManager> {
|
||||
// TODO: pass in cli overrides once cloud tasks properly support them.
|
||||
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
|
||||
Some(AuthManager::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
Some(config.chatgpt_base_url),
|
||||
chatgpt_base_url.or(Some(config.chatgpt_base_url)),
|
||||
))
|
||||
}
|
||||
|
||||
/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`,
|
||||
/// and optional `ChatGPT-Account-Id`.
|
||||
pub async fn build_chatgpt_headers() -> HeaderMap {
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use reqwest::header::HeaderName;
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::header::USER_AGENT;
|
||||
|
||||
@@ -85,23 +65,11 @@ pub async fn build_chatgpt_headers() -> HeaderMap {
|
||||
USER_AGENT,
|
||||
HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")),
|
||||
);
|
||||
if let Some(am) = load_auth_manager().await
|
||||
if let Some(am) = load_auth_manager(/*chatgpt_base_url*/ None).await
|
||||
&& let Some(auth) = am.auth().await
|
||||
&& let Ok(tok) = auth.get_token()
|
||||
&& !tok.is_empty()
|
||||
&& auth.uses_codex_backend()
|
||||
{
|
||||
let v = format!("Bearer {tok}");
|
||||
if let Ok(hv) = HeaderValue::from_str(&v) {
|
||||
headers.insert(AUTHORIZATION, hv);
|
||||
}
|
||||
if let Some(acc) = auth
|
||||
.get_account_id()
|
||||
.or_else(|| extract_chatgpt_account_id(&tok))
|
||||
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
|
||||
&& let Ok(hv) = HeaderValue::from_str(&acc)
|
||||
{
|
||||
headers.insert(name, hv);
|
||||
}
|
||||
headers.extend(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers());
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ pub trait AuthProvider: Send + Sync {
|
||||
/// used by telemetry and non-HTTP request paths.
|
||||
fn add_auth_headers(&self, headers: &mut HeaderMap);
|
||||
|
||||
/// Returns any auth headers that are available without request body access.
|
||||
fn to_auth_headers(&self) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
self.add_auth_headers(&mut headers);
|
||||
headers
|
||||
}
|
||||
|
||||
/// Applies auth to a complete outbound request and returns the request to send.
|
||||
///
|
||||
/// The input `request` is moved into this method. Implementations may mutate
|
||||
|
||||
@@ -15,9 +15,11 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
codex-async-utils = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_rmcp_client::OAuthProviderError;
|
||||
use codex_rmcp_client::determine_streamable_http_auth_status;
|
||||
@@ -9,8 +12,7 @@ use codex_rmcp_client::discover_streamable_http_oauth;
|
||||
use futures::future::join_all;
|
||||
use tracing::warn;
|
||||
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use super::CODEX_APPS_MCP_SERVER_NAME;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct McpOAuthLoginConfig {
|
||||
@@ -126,6 +128,7 @@ pub struct McpAuthStatusEntry {
|
||||
pub async fn compute_auth_statuses<'a, I>(
|
||||
servers: I,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<String, McpAuthStatusEntry>
|
||||
where
|
||||
I: IntoIterator<Item = (&'a String, &'a McpServerConfig)>,
|
||||
@@ -133,14 +136,24 @@ where
|
||||
let futures = servers.into_iter().map(|(name, config)| {
|
||||
let name = name.clone();
|
||||
let config = config.clone();
|
||||
async move {
|
||||
let auth_status = match compute_auth_status(&name, &config, store_mode).await {
|
||||
Ok(status) => status,
|
||||
Err(error) => {
|
||||
warn!("failed to determine auth status for MCP server `{name}`: {error:?}");
|
||||
McpAuthStatus::Unsupported
|
||||
let has_runtime_auth = name == CODEX_APPS_MCP_SERVER_NAME
|
||||
&& auth.is_some_and(CodexAuth::uses_codex_backend)
|
||||
&& matches!(
|
||||
&config.transport,
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
bearer_token_env_var: None,
|
||||
..
|
||||
}
|
||||
};
|
||||
);
|
||||
async move {
|
||||
let auth_status =
|
||||
match compute_auth_status(&name, &config, store_mode, has_runtime_auth).await {
|
||||
Ok(status) => status,
|
||||
Err(error) => {
|
||||
warn!("failed to determine auth status for MCP server `{name}`: {error:?}");
|
||||
McpAuthStatus::Unsupported
|
||||
}
|
||||
};
|
||||
let entry = McpAuthStatusEntry {
|
||||
config,
|
||||
auth_status,
|
||||
@@ -156,11 +169,16 @@ async fn compute_auth_status(
|
||||
server_name: &str,
|
||||
config: &McpServerConfig,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
has_runtime_auth: bool,
|
||||
) -> Result<McpAuthStatus> {
|
||||
if !config.enabled {
|
||||
return Ok(McpAuthStatus::Unsupported);
|
||||
}
|
||||
|
||||
if has_runtime_auth {
|
||||
return Ok(McpAuthStatus::BearerToken);
|
||||
}
|
||||
|
||||
match &config.transport {
|
||||
McpServerTransportConfig::Stdio { .. } => Ok(McpAuthStatus::Unsupported),
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
|
||||
@@ -205,31 +205,6 @@ fn codex_apps_mcp_bearer_token_env_var() -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_apps_mcp_bearer_token(auth: Option<&CodexAuth>) -> Option<String> {
|
||||
let token = auth.and_then(|auth| auth.get_token().ok())?;
|
||||
let token = token.trim();
|
||||
if token.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(token.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_apps_mcp_http_headers(auth: Option<&CodexAuth>) -> Option<HashMap<String, String>> {
|
||||
let mut headers = HashMap::new();
|
||||
if let Some(token) = codex_apps_mcp_bearer_token(auth) {
|
||||
headers.insert("Authorization".to_string(), format!("Bearer {token}"));
|
||||
}
|
||||
if let Some(account_id) = auth.and_then(CodexAuth::get_account_id) {
|
||||
headers.insert("ChatGPT-Account-ID".to_string(), account_id);
|
||||
}
|
||||
if headers.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(headers)
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_codex_apps_base_url(base_url: &str) -> String {
|
||||
let mut base_url = base_url.trim_end_matches('/').to_string();
|
||||
if (base_url.starts_with("https://chatgpt.com")
|
||||
@@ -256,20 +231,14 @@ pub(crate) fn codex_apps_mcp_url(config: &McpConfig) -> String {
|
||||
codex_apps_mcp_url_for_base_url(&config.chatgpt_base_url)
|
||||
}
|
||||
|
||||
fn codex_apps_mcp_server_config(config: &McpConfig, auth: Option<&CodexAuth>) -> McpServerConfig {
|
||||
let bearer_token_env_var = codex_apps_mcp_bearer_token_env_var();
|
||||
let http_headers = if bearer_token_env_var.is_some() {
|
||||
None
|
||||
} else {
|
||||
codex_apps_mcp_http_headers(auth)
|
||||
};
|
||||
fn codex_apps_mcp_server_config(config: &McpConfig) -> McpServerConfig {
|
||||
let url = codex_apps_mcp_url(config);
|
||||
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
http_headers,
|
||||
bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(),
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
@@ -293,10 +262,10 @@ pub fn with_codex_apps_mcp(
|
||||
auth: Option<&CodexAuth>,
|
||||
config: &McpConfig,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
if config.apps_enabled && auth.is_some_and(CodexAuth::is_chatgpt_auth) {
|
||||
if config.apps_enabled && auth.is_some_and(CodexAuth::uses_codex_backend) {
|
||||
servers.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
codex_apps_mcp_server_config(config, auth),
|
||||
codex_apps_mcp_server_config(config),
|
||||
);
|
||||
} else {
|
||||
servers.remove(CODEX_APPS_MCP_SERVER_NAME);
|
||||
@@ -329,8 +298,12 @@ pub async fn read_mcp_resource(
|
||||
) -> anyhow::Result<ReadResourceResult> {
|
||||
let mut mcp_servers = effective_mcp_servers(config, auth);
|
||||
mcp_servers.retain(|name, _| name == server);
|
||||
let auth_statuses =
|
||||
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await;
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
auth,
|
||||
)
|
||||
.await;
|
||||
let (tx_event, rx_event) = unbounded();
|
||||
drop(rx_event);
|
||||
let (manager, cancel_token) = McpConnectionManager::new(
|
||||
@@ -345,6 +318,7 @@ pub async fn read_mcp_resource(
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance(config),
|
||||
auth,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -395,8 +369,12 @@ pub async fn collect_mcp_snapshot_with_detail(
|
||||
};
|
||||
}
|
||||
|
||||
let auth_status_entries =
|
||||
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await;
|
||||
let auth_status_entries = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
auth,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (tx_event, rx_event) = unbounded();
|
||||
drop(rx_event);
|
||||
@@ -413,6 +391,7 @@ pub async fn collect_mcp_snapshot_with_detail(
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance,
|
||||
auth,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -470,8 +449,12 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
};
|
||||
}
|
||||
|
||||
let auth_status_entries =
|
||||
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await;
|
||||
let auth_status_entries = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
auth,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (tx_event, rx_event) = unbounded();
|
||||
drop(rx_event);
|
||||
@@ -488,6 +471,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance,
|
||||
auth,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_async_utils::CancelErr;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_config::Constrained;
|
||||
@@ -121,21 +122,10 @@ fn sha1_hex(s: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCacheKey {
|
||||
let token_data = auth.and_then(|auth| auth.get_token_data().ok());
|
||||
let account_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.account_id.clone());
|
||||
let chatgpt_user_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.id_token.chatgpt_user_id.clone());
|
||||
let is_workspace_account = token_data
|
||||
.as_ref()
|
||||
.is_some_and(|token_data| token_data.id_token.is_workspace_account());
|
||||
|
||||
CodexAppsToolsCacheKey {
|
||||
account_id,
|
||||
chatgpt_user_id,
|
||||
is_workspace_account,
|
||||
account_id: auth.and_then(CodexAuth::get_account_id),
|
||||
chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id),
|
||||
is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +487,7 @@ impl AsyncManagedClient {
|
||||
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
|
||||
tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Self {
|
||||
let tool_filter = ToolFilter::from_config(&config);
|
||||
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
@@ -519,6 +510,7 @@ impl AsyncManagedClient {
|
||||
config.clone(),
|
||||
store_mode,
|
||||
runtime_environment,
|
||||
runtime_auth_provider,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
@@ -758,6 +750,7 @@ impl McpConnectionManager {
|
||||
codex_home: PathBuf,
|
||||
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
|
||||
tool_plugin_provenance: ToolPluginProvenance,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> (Self, CancellationToken) {
|
||||
let cancel_token = CancellationToken::new();
|
||||
let mut clients = HashMap::new();
|
||||
@@ -767,6 +760,9 @@ impl McpConnectionManager {
|
||||
ElicitationRequestManager::new(approval_policy.value(), initial_sandbox_policy);
|
||||
let tool_plugin_provenance = Arc::new(tool_plugin_provenance);
|
||||
let startup_submit_id = submit_id.clone();
|
||||
let codex_apps_auth_provider = auth
|
||||
.filter(|auth| auth.uses_codex_backend())
|
||||
.map(codex_model_provider::auth_provider_from_auth);
|
||||
let mcp_servers = mcp_servers.clone();
|
||||
for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) {
|
||||
if let Some(origin) = transport_origin(&cfg.transport) {
|
||||
@@ -790,6 +786,19 @@ impl McpConnectionManager {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let uses_env_bearer_token = match &cfg.transport {
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
bearer_token_env_var,
|
||||
..
|
||||
} => bearer_token_env_var.is_some(),
|
||||
McpServerTransportConfig::Stdio { .. } => false,
|
||||
};
|
||||
let runtime_auth_provider =
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME && !uses_env_bearer_token {
|
||||
codex_apps_auth_provider.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let async_managed_client = AsyncManagedClient::new(
|
||||
server_name.clone(),
|
||||
cfg,
|
||||
@@ -800,6 +809,7 @@ impl McpConnectionManager {
|
||||
codex_apps_tools_cache_context,
|
||||
Arc::clone(&tool_plugin_provenance),
|
||||
runtime_environment.clone(),
|
||||
runtime_auth_provider,
|
||||
);
|
||||
clients.insert(server_name.clone(), async_managed_client.clone());
|
||||
let tx_event = tx_event.clone();
|
||||
@@ -1533,6 +1543,7 @@ async fn make_rmcp_client(
|
||||
config: McpServerConfig,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Result<RmcpClient, StartupOutcomeError> {
|
||||
let McpServerConfig {
|
||||
transport,
|
||||
@@ -1612,6 +1623,7 @@ async fn make_rmcp_client(
|
||||
env_http_headers,
|
||||
store_mode,
|
||||
runtime_environment.environment().get_http_client(),
|
||||
runtime_auth_provider,
|
||||
)
|
||||
.await
|
||||
.map_err(StartupOutcomeError::from)
|
||||
|
||||
@@ -19,6 +19,7 @@ codex-core-skills = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
@@ -608,7 +608,7 @@ fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePlu
|
||||
let Some(auth) = auth else {
|
||||
return Err(RemotePluginCatalogError::AuthRequired);
|
||||
};
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(RemotePluginCatalogError::UnsupportedAuthMode);
|
||||
}
|
||||
Ok(auth)
|
||||
@@ -618,16 +618,9 @@ fn authenticated_request(
|
||||
request: RequestBuilder,
|
||||
auth: &CodexAuth,
|
||||
) -> Result<RequestBuilder, RemotePluginCatalogError> {
|
||||
let token = auth
|
||||
.get_token()
|
||||
.map_err(RemotePluginCatalogError::AuthToken)?;
|
||||
let mut request = request
|
||||
Ok(request
|
||||
.timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT)
|
||||
.bearer_auth(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
Ok(request)
|
||||
.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()))
|
||||
}
|
||||
|
||||
async fn send_and_decode<T: for<'de> Deserialize<'de>>(
|
||||
|
||||
@@ -123,23 +123,17 @@ pub async fn fetch_remote_plugin_status(
|
||||
let Some(auth) = auth else {
|
||||
return Err(RemotePluginFetchError::AuthRequired);
|
||||
};
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(RemotePluginFetchError::UnsupportedAuthMode);
|
||||
}
|
||||
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/plugins/list");
|
||||
let client = build_reqwest_client();
|
||||
let token = auth
|
||||
.get_token()
|
||||
.map_err(RemotePluginFetchError::AuthToken)?;
|
||||
let mut request = client
|
||||
let request = client
|
||||
.get(&url)
|
||||
.timeout(REMOTE_PLUGIN_FETCH_TIMEOUT)
|
||||
.bearer_auth(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers());
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
@@ -176,14 +170,9 @@ pub async fn fetch_remote_featured_plugin_ids(
|
||||
)])
|
||||
.timeout(REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT);
|
||||
|
||||
if let Some(auth) = auth.filter(|auth| auth.is_chatgpt_auth()) {
|
||||
let token = auth
|
||||
.get_token()
|
||||
.map_err(RemotePluginFetchError::AuthToken)?;
|
||||
request = request.bearer_auth(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
if let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) {
|
||||
request =
|
||||
request.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers());
|
||||
}
|
||||
|
||||
let response = request
|
||||
@@ -223,11 +212,13 @@ pub async fn uninstall_remote_plugin(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePluginMutationError> {
|
||||
fn ensure_codex_backend_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Result<&CodexAuth, RemotePluginMutationError> {
|
||||
let Some(auth) = auth else {
|
||||
return Err(RemotePluginMutationError::AuthRequired);
|
||||
};
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(RemotePluginMutationError::UnsupportedAuthMode);
|
||||
}
|
||||
Ok(auth)
|
||||
@@ -243,19 +234,13 @@ async fn post_remote_plugin_mutation(
|
||||
plugin_id: &str,
|
||||
action: &str,
|
||||
) -> Result<RemotePluginMutationResponse, RemotePluginMutationError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let auth = ensure_codex_backend_auth(auth)?;
|
||||
let url = remote_plugin_mutation_url(config, plugin_id, action)?;
|
||||
let client = build_reqwest_client();
|
||||
let token = auth
|
||||
.get_token()
|
||||
.map_err(RemotePluginMutationError::AuthToken)?;
|
||||
let mut request = client
|
||||
let request = client
|
||||
.post(url.clone())
|
||||
.timeout(REMOTE_PLUGIN_MUTATION_TIMEOUT)
|
||||
.bearer_auth(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers());
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
|
||||
@@ -19,6 +19,7 @@ codex-app-server-protocol = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-skills = { workspace = true }
|
||||
|
||||
@@ -48,11 +48,11 @@ fn as_query_product_surface(product_surface: RemoteSkillProductSurface) -> &'sta
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth> {
|
||||
fn ensure_codex_backend_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth> {
|
||||
let Some(auth) = auth else {
|
||||
anyhow::bail!("chatgpt authentication required for remote skill scopes");
|
||||
};
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !auth.uses_codex_backend() {
|
||||
anyhow::bail!(
|
||||
"chatgpt authentication required for remote skill scopes; api key auth is not supported"
|
||||
);
|
||||
@@ -94,7 +94,7 @@ pub async fn list_remote_skills(
|
||||
enabled: Option<bool>,
|
||||
) -> Result<Vec<RemoteSkillSummary>> {
|
||||
let base_url = chatgpt_base_url.trim_end_matches('/');
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let auth = ensure_codex_backend_auth(auth)?;
|
||||
|
||||
let url = format!("{base_url}/hazelnuts");
|
||||
let product_surface = as_query_product_surface(product_surface);
|
||||
@@ -108,17 +108,11 @@ pub async fn list_remote_skills(
|
||||
}
|
||||
|
||||
let client = build_reqwest_client();
|
||||
let mut request = client
|
||||
let request = client
|
||||
.get(&url)
|
||||
.timeout(REMOTE_SKILLS_API_TIMEOUT)
|
||||
.query(&query_params);
|
||||
let token = auth
|
||||
.get_token()
|
||||
.context("Failed to read auth token for remote skills")?;
|
||||
request = request.bearer_auth(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
.query(&query_params)
|
||||
.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers());
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
@@ -150,20 +144,15 @@ pub async fn export_remote_skill(
|
||||
auth: Option<&CodexAuth>,
|
||||
skill_id: &str,
|
||||
) -> Result<RemoteSkillDownloadResult> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let auth = ensure_codex_backend_auth(auth)?;
|
||||
|
||||
let client = build_reqwest_client();
|
||||
let base_url = chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/hazelnuts/{skill_id}/export");
|
||||
let mut request = client.get(&url).timeout(REMOTE_SKILLS_API_TIMEOUT);
|
||||
|
||||
let token = auth
|
||||
.get_token()
|
||||
.context("Failed to read auth token for remote skills")?;
|
||||
request = request.bearer_auth(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
let request = client
|
||||
.get(&url)
|
||||
.timeout(REMOTE_SKILLS_API_TIMEOUT)
|
||||
.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers());
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::compact::content_items_to_text;
|
||||
use crate::event_mapping::is_contextual_user_message_content;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -104,28 +103,15 @@ pub(crate) async fn monitor_action(
|
||||
) -> ArcMonitorOutcome {
|
||||
let auth = match turn_context.auth_manager.as_ref() {
|
||||
Some(auth_manager) => match auth_manager.auth().await {
|
||||
Some(auth) if auth.is_chatgpt_auth() => Some(auth),
|
||||
Some(auth) if auth.uses_codex_backend() => Some(auth),
|
||||
_ => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let token = if let Some(token) = read_non_empty_env_var(CODEX_ARC_MONITOR_TOKEN) {
|
||||
token
|
||||
} else {
|
||||
let Some(auth) = auth.as_ref() else {
|
||||
return ArcMonitorOutcome::Ok;
|
||||
};
|
||||
match auth.get_token() {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
error = %err,
|
||||
"skipping safety monitor because auth token is unavailable"
|
||||
);
|
||||
return ArcMonitorOutcome::Ok;
|
||||
}
|
||||
}
|
||||
};
|
||||
let env_token = read_non_empty_env_var(CODEX_ARC_MONITOR_TOKEN);
|
||||
if env_token.is_none() && auth.is_none() {
|
||||
return ArcMonitorOutcome::Ok;
|
||||
}
|
||||
|
||||
let url = read_non_empty_env_var(CODEX_ARC_MONITOR_ENDPOINT_OVERRIDE).unwrap_or_else(|| {
|
||||
format!(
|
||||
@@ -143,13 +129,12 @@ pub(crate) async fn monitor_action(
|
||||
let body =
|
||||
build_arc_monitor_request(sess, turn_context, action, protection_client_callsite).await;
|
||||
let client = build_reqwest_client();
|
||||
let mut request = client
|
||||
.post(&url)
|
||||
.timeout(ARC_MONITOR_TIMEOUT)
|
||||
.json(&body)
|
||||
.bearer_auth(token);
|
||||
if let Some(account_id) = auth.as_ref().and_then(CodexAuth::get_account_id) {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
let mut request = client.post(&url).timeout(ARC_MONITOR_TIMEOUT).json(&body);
|
||||
if let Some(token) = env_token {
|
||||
request = request.bearer_auth(token);
|
||||
} else if let Some(auth) = auth.as_ref() {
|
||||
request =
|
||||
request.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers());
|
||||
}
|
||||
|
||||
let response = match request.send().await {
|
||||
|
||||
@@ -1127,7 +1127,7 @@ impl ModelClientSession {
|
||||
|
||||
fn responses_request_compression(&self, auth: Option<&CodexAuth>) -> Compression {
|
||||
if self.client.state.enable_request_compression
|
||||
&& auth.is_some_and(CodexAuth::is_chatgpt_auth)
|
||||
&& auth.is_some_and(CodexAuth::uses_codex_backend)
|
||||
&& self.client.state.provider.info().is_openai()
|
||||
{
|
||||
Compression::Zstd
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::Context;
|
||||
use async_channel::unbounded;
|
||||
use codex_api::SharedAuthProvider;
|
||||
pub use codex_app_server_protocol::AppBranding;
|
||||
pub use codex_app_server_protocol::AppInfo;
|
||||
pub use codex_app_server_protocol::AppMetadata;
|
||||
@@ -16,7 +17,6 @@ use codex_connectors::DirectoryListResponse;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::EnvironmentManagerArgs;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_login::token_data::TokenData;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
@@ -253,8 +253,12 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
});
|
||||
}
|
||||
|
||||
let auth_status_entries =
|
||||
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await;
|
||||
let auth_status_entries = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (tx_event, rx_event) = unbounded();
|
||||
drop(rx_event);
|
||||
@@ -275,6 +279,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
ToolPluginProvenance::default(),
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -351,16 +356,9 @@ fn accessible_connectors_cache_key(
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> AccessibleConnectorsCacheKey {
|
||||
let token_data: Option<TokenData> = auth.and_then(|auth| auth.get_token_data().ok());
|
||||
let account_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.account_id.clone());
|
||||
let chatgpt_user_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.id_token.chatgpt_user_id.clone());
|
||||
let is_workspace_account = token_data
|
||||
.as_ref()
|
||||
.is_some_and(|token_data| token_data.id_token.is_workspace_account());
|
||||
let account_id = auth.and_then(CodexAuth::get_account_id);
|
||||
let chatgpt_user_id = auth.and_then(CodexAuth::get_chatgpt_user_id);
|
||||
let is_workspace_account = auth.is_some_and(CodexAuth::is_workspace_account);
|
||||
AccessibleConnectorsCacheKey {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
account_id,
|
||||
@@ -431,31 +429,29 @@ async fn list_directory_connectors_for_tool_suggest_with_auth(
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let token_data = if let Some(auth) = auth {
|
||||
auth.get_token_data().ok()
|
||||
let loaded_auth;
|
||||
let auth = if let Some(auth) = auth {
|
||||
Some(auth)
|
||||
} else {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false);
|
||||
auth_manager
|
||||
.auth()
|
||||
.await
|
||||
.and_then(|auth| auth.get_token_data().ok())
|
||||
loaded_auth = auth_manager.auth().await;
|
||||
loaded_auth.as_ref()
|
||||
};
|
||||
let Some(token_data) = token_data else {
|
||||
let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let account_id = match token_data.account_id.as_deref() {
|
||||
let account_id = match auth.get_account_id() {
|
||||
Some(account_id) if !account_id.is_empty() => account_id,
|
||||
_ => return Ok(Vec::new()),
|
||||
};
|
||||
let access_token = token_data.access_token.clone();
|
||||
let account_id = account_id.to_string();
|
||||
let is_workspace_account = token_data.id_token.is_workspace_account();
|
||||
let auth_provider = codex_model_provider::auth_provider_from_auth(auth);
|
||||
let is_workspace_account = auth.is_workspace_account();
|
||||
let cache_key = AllConnectorsCacheKey::new(
|
||||
config.chatgpt_base_url.clone(),
|
||||
Some(account_id.clone()),
|
||||
token_data.id_token.chatgpt_user_id.clone(),
|
||||
auth.get_chatgpt_user_id(),
|
||||
is_workspace_account,
|
||||
);
|
||||
|
||||
@@ -464,14 +460,12 @@ async fn list_directory_connectors_for_tool_suggest_with_auth(
|
||||
is_workspace_account,
|
||||
/*force_refetch*/ false,
|
||||
|path| {
|
||||
let access_token = access_token.clone();
|
||||
let account_id = account_id.clone();
|
||||
let auth_provider = auth_provider.clone();
|
||||
async move {
|
||||
chatgpt_get_request_with_token::<DirectoryListResponse>(
|
||||
chatgpt_get_request_with_auth_provider::<DirectoryListResponse>(
|
||||
config,
|
||||
path,
|
||||
access_token.as_str(),
|
||||
account_id.as_str(),
|
||||
auth_provider,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -480,18 +474,16 @@ async fn list_directory_connectors_for_tool_suggest_with_auth(
|
||||
.await
|
||||
}
|
||||
|
||||
async fn chatgpt_get_request_with_token<T: DeserializeOwned>(
|
||||
async fn chatgpt_get_request_with_auth_provider<T: DeserializeOwned>(
|
||||
config: &Config,
|
||||
path: String,
|
||||
access_token: &str,
|
||||
account_id: &str,
|
||||
auth_provider: SharedAuthProvider,
|
||||
) -> anyhow::Result<T> {
|
||||
let client = create_client();
|
||||
let url = format!("{}{}", config.chatgpt_base_url, path);
|
||||
let response = client
|
||||
.get(&url)
|
||||
.bearer_auth(access_token)
|
||||
.header("chatgpt-account-id", account_id)
|
||||
.headers(auth_provider.to_auth_headers())
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(DIRECTORY_CONNECTORS_TIMEOUT)
|
||||
.send()
|
||||
|
||||
@@ -14,7 +14,6 @@ use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use codex_api::upload_local_file;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider::BearerAuthProvider;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files(
|
||||
@@ -109,17 +108,15 @@ async fn build_uploaded_local_argument_value(
|
||||
"ChatGPT auth is required to upload local files for Codex Apps tools".to_string(),
|
||||
);
|
||||
};
|
||||
let token_data = auth
|
||||
.get_token_data()
|
||||
.map_err(|error| format!("failed to read ChatGPT auth for file upload: {error}"))?;
|
||||
let upload_auth = BearerAuthProvider {
|
||||
token: Some(token_data.access_token),
|
||||
account_id: token_data.account_id,
|
||||
is_fedramp_account: auth.is_fedramp_account(),
|
||||
};
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(
|
||||
"ChatGPT auth is required to upload local files for Codex Apps tools".to_string(),
|
||||
);
|
||||
}
|
||||
let upload_auth = codex_model_provider::auth_provider_from_auth(auth);
|
||||
let uploaded = upload_local_file(
|
||||
turn_context.config.chatgpt_base_url.trim_end_matches('/'),
|
||||
&upload_auth,
|
||||
upload_auth.as_ref(),
|
||||
&resolved_path,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -124,21 +124,11 @@ fn featured_plugin_ids_cache_key(
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> FeaturedPluginIdsCacheKey {
|
||||
let token_data = auth.and_then(|auth| auth.get_token_data().ok());
|
||||
let account_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.account_id.clone());
|
||||
let chatgpt_user_id = token_data
|
||||
.as_ref()
|
||||
.and_then(|token_data| token_data.id_token.chatgpt_user_id.clone());
|
||||
let is_workspace_account = token_data
|
||||
.as_ref()
|
||||
.is_some_and(|token_data| token_data.id_token.is_workspace_account());
|
||||
FeaturedPluginIdsCacheKey {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
account_id,
|
||||
chatgpt_user_id,
|
||||
is_workspace_account,
|
||||
account_id: auth.and_then(CodexAuth::get_account_id),
|
||||
chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id),
|
||||
is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -543,7 +543,12 @@ pub async fn list_mcp_tools(sess: &Session, config: &Arc<Config>, sub_id: String
|
||||
.await;
|
||||
let snapshot = collect_mcp_snapshot_from_manager(
|
||||
&mcp_connection_manager,
|
||||
compute_auth_statuses(mcp_servers.iter(), config.mcp_oauth_credentials_store_mode).await,
|
||||
compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await;
|
||||
let event = Event {
|
||||
|
||||
@@ -219,7 +219,8 @@ impl Session {
|
||||
.tool_plugin_provenance(config.as_ref())
|
||||
.await;
|
||||
let mcp_servers = with_codex_apps_mcp(mcp_servers, auth.as_ref(), &mcp_config);
|
||||
let auth_statuses = compute_auth_statuses(mcp_servers.iter(), store_mode).await;
|
||||
let auth_statuses =
|
||||
compute_auth_statuses(mcp_servers.iter(), store_mode, auth.as_ref()).await;
|
||||
{
|
||||
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
|
||||
guard.cancel();
|
||||
@@ -243,6 +244,7 @@ impl Session {
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
tool_plugin_provenance,
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
{
|
||||
|
||||
@@ -45,7 +45,6 @@ use chrono::Local;
|
||||
use chrono::Utc;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_analytics::SubAgentThreadStartedInput;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::McpServerElicitationRequest;
|
||||
use codex_app_server_protocol::McpServerElicitationRequestParams;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
|
||||
@@ -396,6 +396,7 @@ impl Session {
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config_for_mcp.mcp_oauth_credentials_store_mode,
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
(auth, mcp_servers, auth_statuses)
|
||||
@@ -887,6 +888,7 @@ impl Session {
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance,
|
||||
auth,
|
||||
)
|
||||
.instrument(info_span!(
|
||||
"session_init.mcp_manager_init",
|
||||
|
||||
@@ -7,10 +7,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool {
|
||||
matches!(
|
||||
auth_manager.and_then(AuthManager::auth_mode),
|
||||
Some(AuthMode::Chatgpt)
|
||||
)
|
||||
auth_manager.is_some_and(AuthManager::current_auth_uses_codex_backend)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -105,13 +102,11 @@ impl TurnContext {
|
||||
}
|
||||
|
||||
pub(crate) fn apps_enabled(&self) -> bool {
|
||||
let is_chatgpt_auth = self
|
||||
let uses_codex_backend = self
|
||||
.auth_manager
|
||||
.as_deref()
|
||||
.and_then(AuthManager::auth_cached)
|
||||
.as_ref()
|
||||
.is_some_and(CodexAuth::is_chatgpt_auth);
|
||||
self.features.apps_enabled_for_auth(is_chatgpt_auth)
|
||||
.is_some_and(AuthManager::current_auth_uses_codex_backend);
|
||||
self.features.apps_enabled_for_auth(uses_codex_backend)
|
||||
}
|
||||
|
||||
pub(crate) async fn with_model(&self, model: String, models_manager: &ModelsManager) -> Self {
|
||||
|
||||
@@ -39,6 +39,10 @@ impl AgentIdentityAuth {
|
||||
&self.record
|
||||
}
|
||||
|
||||
pub fn process_task_id(&self) -> Option<&str> {
|
||||
self.process_task_id.get().map(String::as_str)
|
||||
}
|
||||
|
||||
pub async fn ensure_runtime(&self, chatgpt_base_url: Option<String>) -> std::io::Result<()> {
|
||||
self.process_task_id
|
||||
.get_or_try_init(|| async {
|
||||
|
||||
@@ -397,6 +397,11 @@ impl CodexAuth {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_workspace_account(&self) -> bool {
|
||||
self.account_plan_type()
|
||||
.is_some_and(AccountPlanType::is_workspace_account)
|
||||
}
|
||||
|
||||
/// Returns `None` if token-backed ChatGPT auth is unavailable.
|
||||
fn get_current_auth_json(&self) -> Option<AuthDotJson> {
|
||||
let state = match self {
|
||||
@@ -1709,6 +1714,13 @@ impl AuthManager {
|
||||
self.auth_cached().as_ref().map(CodexAuth::auth_mode)
|
||||
}
|
||||
|
||||
pub fn current_auth_uses_codex_backend(&self) -> bool {
|
||||
matches!(
|
||||
self.auth_mode(),
|
||||
Some(AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens | AuthMode::AgentIdentity)
|
||||
)
|
||||
}
|
||||
|
||||
fn is_stale_for_proactive_refresh(auth: &CodexAuth) -> bool {
|
||||
let chatgpt_auth = match auth {
|
||||
CodexAuth::Chatgpt(chatgpt_auth) => chatgpt_auth,
|
||||
|
||||
@@ -15,6 +15,7 @@ workspace = true
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-agent-identity = { workspace = true }
|
||||
codex-aws-auth = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,73 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_agent_identity::AgentIdentityKey;
|
||||
use codex_agent_identity::AgentTaskAuthorizationTarget;
|
||||
use codex_agent_identity::authorization_header_for_agent_task;
|
||||
use codex_api::AuthProvider;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
|
||||
use crate::bearer_auth_provider::BearerAuthProvider;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AgentIdentityAuthProvider {
|
||||
auth: codex_login::auth::AgentIdentityAuth,
|
||||
}
|
||||
|
||||
impl AuthProvider for AgentIdentityAuthProvider {
|
||||
fn add_auth_headers(&self, headers: &mut HeaderMap) {
|
||||
let record = self.auth.record();
|
||||
let header_value = self
|
||||
.auth
|
||||
.process_task_id()
|
||||
.ok_or_else(|| std::io::Error::other("agent identity process task is not initialized"))
|
||||
.and_then(|task_id| {
|
||||
authorization_header_for_agent_task(
|
||||
AgentIdentityKey {
|
||||
agent_runtime_id: &record.agent_runtime_id,
|
||||
private_key_pkcs8_base64: &record.agent_private_key,
|
||||
},
|
||||
AgentTaskAuthorizationTarget {
|
||||
agent_runtime_id: &record.agent_runtime_id,
|
||||
task_id,
|
||||
},
|
||||
)
|
||||
.map_err(std::io::Error::other)
|
||||
});
|
||||
|
||||
if let Ok(header_value) = header_value
|
||||
&& let Ok(header) = HeaderValue::from_str(&header_value)
|
||||
{
|
||||
let _ = headers.insert(http::header::AUTHORIZATION, header);
|
||||
}
|
||||
|
||||
if let Ok(header) = HeaderValue::from_str(self.auth.account_id()) {
|
||||
let _ = headers.insert("ChatGPT-Account-ID", header);
|
||||
}
|
||||
|
||||
if self.auth.is_fedramp_account() {
|
||||
let _ = headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some providers are meant to send no auth headers. Examples include local OSS
|
||||
// providers and custom test providers with `requires_openai_auth = false`.
|
||||
#[derive(Clone, Debug)]
|
||||
struct UnauthenticatedAuthProvider;
|
||||
|
||||
impl AuthProvider for UnauthenticatedAuthProvider {
|
||||
fn add_auth_headers(&self, _headers: &mut HeaderMap) {}
|
||||
}
|
||||
|
||||
pub fn unauthenticated_auth_provider() -> SharedAuthProvider {
|
||||
Arc::new(UnauthenticatedAuthProvider)
|
||||
}
|
||||
|
||||
/// Returns the provider-scoped auth manager when this provider uses command-backed auth.
|
||||
///
|
||||
/// Providers without custom auth continue using the caller-supplied base manager, when present.
|
||||
@@ -20,45 +81,63 @@ pub(crate) fn auth_manager_for_provider(
|
||||
}
|
||||
}
|
||||
|
||||
fn bearer_auth_provider_from_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> codex_protocol::error::Result<BearerAuthProvider> {
|
||||
if let Some(api_key) = provider.api_key()? {
|
||||
return Ok(BearerAuthProvider {
|
||||
token: Some(api_key),
|
||||
account_id: None,
|
||||
is_fedramp_account: false,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(token) = provider.experimental_bearer_token.clone() {
|
||||
return Ok(BearerAuthProvider {
|
||||
token: Some(token),
|
||||
account_id: None,
|
||||
is_fedramp_account: false,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(auth) = auth {
|
||||
let token = auth.get_token()?;
|
||||
Ok(BearerAuthProvider {
|
||||
token: Some(token),
|
||||
account_id: auth.get_account_id(),
|
||||
is_fedramp_account: auth.is_fedramp_account(),
|
||||
})
|
||||
} else {
|
||||
Ok(BearerAuthProvider {
|
||||
token: None,
|
||||
account_id: None,
|
||||
is_fedramp_account: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_provider_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> codex_protocol::error::Result<SharedAuthProvider> {
|
||||
Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?))
|
||||
if let Some(auth) = bearer_auth_for_provider(provider)? {
|
||||
return Ok(Arc::new(auth));
|
||||
}
|
||||
|
||||
Ok(match auth {
|
||||
Some(auth) => auth_provider_from_auth(auth),
|
||||
None => unauthenticated_auth_provider(),
|
||||
})
|
||||
}
|
||||
|
||||
fn bearer_auth_for_provider(
|
||||
provider: &ModelProviderInfo,
|
||||
) -> codex_protocol::error::Result<Option<BearerAuthProvider>> {
|
||||
if let Some(api_key) = provider.api_key()? {
|
||||
return Ok(Some(BearerAuthProvider::new(api_key)));
|
||||
}
|
||||
|
||||
if let Some(token) = provider.experimental_bearer_token.clone() {
|
||||
return Ok(Some(BearerAuthProvider::new(token)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Builds request-header auth for a first-party Codex auth snapshot.
|
||||
pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider {
|
||||
match auth {
|
||||
CodexAuth::AgentIdentity(auth) => {
|
||||
Arc::new(AgentIdentityAuthProvider { auth: auth.clone() })
|
||||
}
|
||||
CodexAuth::ApiKey(_) | CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
|
||||
Arc::new(BearerAuthProvider {
|
||||
token: auth.get_token().ok(),
|
||||
account_id: auth.get_account_id(),
|
||||
is_fedramp_account: auth.is_fedramp_account(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_model_provider_info::create_oss_provider_with_base_url;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unauthenticated_auth_provider_adds_no_headers() {
|
||||
let provider =
|
||||
create_oss_provider_with_base_url("http://localhost:11434/v1", WireApi::Responses);
|
||||
let auth = resolve_provider_auth(/*auth*/ None, &provider).expect("auth should resolve");
|
||||
|
||||
assert!(auth.to_auth_headers().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@ pub struct BearerAuthProvider {
|
||||
}
|
||||
|
||||
impl BearerAuthProvider {
|
||||
pub fn new(token: String) -> Self {
|
||||
Self {
|
||||
token: Some(token),
|
||||
account_id: None,
|
||||
is_fedramp_account: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_test(token: Option<&str>, account_id: Option<&str>) -> Self {
|
||||
Self {
|
||||
token: token.map(str::to_string),
|
||||
|
||||
@@ -3,6 +3,8 @@ mod auth;
|
||||
mod bearer_auth_provider;
|
||||
mod provider;
|
||||
|
||||
pub use auth::auth_provider_from_auth;
|
||||
pub use auth::unauthenticated_auth_provider;
|
||||
pub use bearer_auth_provider::BearerAuthProvider;
|
||||
pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider;
|
||||
pub use provider::ModelProvider;
|
||||
|
||||
@@ -9,7 +9,6 @@ use codex_api::ReqwestTransport;
|
||||
use codex_api::TransportError;
|
||||
use codex_api::auth_header_telemetry;
|
||||
use codex_api::map_api_error;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_feedback::FeedbackRequestTags;
|
||||
use codex_feedback::emit_feedback_request_tags_with_auth_env;
|
||||
use codex_login::AuthEnvTelemetry;
|
||||
@@ -407,11 +406,13 @@ impl ModelsManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auth_mode = self
|
||||
let uses_codex_backend = self
|
||||
.provider
|
||||
.auth_manager()
|
||||
.and_then(|auth_manager| auth_manager.auth_mode());
|
||||
if auth_mode != Some(AuthMode::Chatgpt) && !self.provider.info().has_command_auth() {
|
||||
.auth()
|
||||
.await
|
||||
.as_ref()
|
||||
.is_some_and(CodexAuth::uses_codex_backend);
|
||||
if !uses_codex_backend && !self.provider.info().has_command_auth() {
|
||||
if matches!(
|
||||
refresh_strategy,
|
||||
RefreshStrategy::Offline | RefreshStrategy::OnlineIfUncached
|
||||
@@ -536,12 +537,12 @@ impl ModelsManager {
|
||||
remote_models.sort_by(|a, b| a.priority.cmp(&b.priority));
|
||||
|
||||
let mut presets: Vec<ModelPreset> = remote_models.into_iter().map(Into::into).collect();
|
||||
let auth_mode = self
|
||||
let uses_codex_backend = self
|
||||
.provider
|
||||
.auth_manager()
|
||||
.and_then(|auth_manager| auth_manager.auth_mode());
|
||||
let chatgpt_mode = matches!(auth_mode, Some(AuthMode::Chatgpt));
|
||||
presets = ModelPreset::filter_by_auth(presets, chatgpt_mode);
|
||||
.as_deref()
|
||||
.is_some_and(AuthManager::current_auth_uses_codex_backend);
|
||||
presets = ModelPreset::filter_by_auth(presets, uses_codex_backend);
|
||||
|
||||
ModelPreset::mark_default_by_picker_visibility(&mut presets);
|
||||
|
||||
|
||||
@@ -35,6 +35,18 @@ impl PlanType {
|
||||
pub fn is_business_like(self) -> bool {
|
||||
matches!(self, Self::Business | Self::EnterpriseCbpUsageBased)
|
||||
}
|
||||
|
||||
pub fn is_workspace_account(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Team
|
||||
| Self::SelfServeBusinessUsageBased
|
||||
| Self::Business
|
||||
| Self::EnterpriseCbpUsageBased
|
||||
| Self::Enterprise
|
||||
| Self::Edu
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -84,4 +96,21 @@ mod tests {
|
||||
assert_eq!(PlanType::EnterpriseCbpUsageBased.is_business_like(), true);
|
||||
assert_eq!(PlanType::Team.is_business_like(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_account_helper_includes_usage_based_workspace_plans() {
|
||||
assert_eq!(PlanType::Team.is_workspace_account(), true);
|
||||
assert_eq!(
|
||||
PlanType::SelfServeBusinessUsageBased.is_workspace_account(),
|
||||
true
|
||||
);
|
||||
assert_eq!(PlanType::Business.is_workspace_account(), true);
|
||||
assert_eq!(
|
||||
PlanType::EnterpriseCbpUsageBased.is_workspace_account(),
|
||||
true
|
||||
);
|
||||
assert_eq!(PlanType::Enterprise.is_workspace_account(), true);
|
||||
assert_eq!(PlanType::Edu.is_workspace_account(), true);
|
||||
assert_eq!(PlanType::Pro.is_workspace_account(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ axum = { workspace = true, default-features = false, features = [
|
||||
"http1",
|
||||
"tokio",
|
||||
] }
|
||||
codex-api = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_exec_server::ExecServerError;
|
||||
use codex_exec_server::HttpClient;
|
||||
use codex_exec_server::HttpHeader;
|
||||
@@ -43,6 +44,7 @@ const NON_JSON_RESPONSE_BODY_PREVIEW_BYTES: usize = 8_192;
|
||||
pub(crate) struct StreamableHttpClientAdapter {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
default_headers: HeaderMap,
|
||||
auth_provider: Option<SharedAuthProvider>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -56,10 +58,15 @@ pub(crate) enum StreamableHttpClientAdapterError {
|
||||
}
|
||||
|
||||
impl StreamableHttpClientAdapter {
|
||||
pub(crate) fn new(http_client: Arc<dyn HttpClient>, default_headers: HeaderMap) -> Self {
|
||||
pub(crate) fn new(
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
default_headers: HeaderMap,
|
||||
auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Self {
|
||||
Self {
|
||||
http_client,
|
||||
default_headers,
|
||||
auth_provider,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,6 +82,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
|
||||
auth_token: Option<String>,
|
||||
) -> std::result::Result<StreamableHttpPostResponse, StreamableHttpError<Self::Error>> {
|
||||
let mut headers = self.default_headers.clone();
|
||||
self.add_auth_headers(&mut headers);
|
||||
insert_header(
|
||||
&mut headers,
|
||||
ACCEPT,
|
||||
@@ -171,6 +179,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
|
||||
auth_token: Option<String>,
|
||||
) -> std::result::Result<(), StreamableHttpError<Self::Error>> {
|
||||
let mut headers = self.default_headers.clone();
|
||||
self.add_auth_headers(&mut headers);
|
||||
if let Some(auth_token) = auth_token {
|
||||
insert_header(
|
||||
&mut headers,
|
||||
@@ -223,6 +232,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
|
||||
StreamableHttpError<Self::Error>,
|
||||
> {
|
||||
let mut headers = self.default_headers.clone();
|
||||
self.add_auth_headers(&mut headers);
|
||||
insert_header(
|
||||
&mut headers,
|
||||
ACCEPT,
|
||||
@@ -297,6 +307,14 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamableHttpClientAdapter {
|
||||
fn add_auth_headers(&self, headers: &mut HeaderMap) {
|
||||
if let Some(auth_provider) = &self.auth_provider {
|
||||
headers.extend(auth_provider.to_auth_headers());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn body_preview(body: impl Into<String>) -> String {
|
||||
let mut body_preview = body.into();
|
||||
let body_len = body_preview.len();
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_exec_server::HttpClient;
|
||||
@@ -108,6 +109,7 @@ enum TransportRecipe {
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
auth_provider: Option<SharedAuthProvider>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -306,6 +308,7 @@ impl RmcpClient {
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Result<Self> {
|
||||
let transport_recipe = TransportRecipe::StreamableHttp {
|
||||
server_name: server_name.to_string(),
|
||||
@@ -315,6 +318,7 @@ impl RmcpClient {
|
||||
env_http_headers,
|
||||
store_mode,
|
||||
http_client,
|
||||
auth_provider,
|
||||
};
|
||||
let transport = Self::create_pending_transport(&transport_recipe).await?;
|
||||
Ok(Self {
|
||||
@@ -667,22 +671,25 @@ impl RmcpClient {
|
||||
env_http_headers,
|
||||
store_mode,
|
||||
http_client,
|
||||
auth_provider,
|
||||
} => {
|
||||
let default_headers =
|
||||
build_default_headers(http_headers.clone(), env_http_headers.clone())?;
|
||||
|
||||
let initial_oauth_tokens =
|
||||
if bearer_token.is_none() && !default_headers.contains_key(AUTHORIZATION) {
|
||||
match load_oauth_tokens(server_name, url, *store_mode) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(err) => {
|
||||
warn!("failed to read tokens for server `{server_name}`: {err}");
|
||||
None
|
||||
}
|
||||
let initial_oauth_tokens = if bearer_token.is_none()
|
||||
&& auth_provider.is_none()
|
||||
&& !default_headers.contains_key(AUTHORIZATION)
|
||||
{
|
||||
match load_oauth_tokens(server_name, url, *store_mode) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(err) => {
|
||||
warn!("failed to read tokens for server `{server_name}`: {err}");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(initial_tokens) = initial_oauth_tokens.clone() {
|
||||
match create_oauth_transport_and_runtime(
|
||||
@@ -722,6 +729,7 @@ impl RmcpClient {
|
||||
StreamableHttpClientAdapter::new(
|
||||
Arc::clone(http_client),
|
||||
default_headers,
|
||||
/*auth_provider*/ None,
|
||||
),
|
||||
http_config,
|
||||
);
|
||||
@@ -737,7 +745,11 @@ impl RmcpClient {
|
||||
}
|
||||
|
||||
let transport = StreamableHttpClientTransport::with_client(
|
||||
StreamableHttpClientAdapter::new(Arc::clone(http_client), default_headers),
|
||||
StreamableHttpClientAdapter::new(
|
||||
Arc::clone(http_client),
|
||||
default_headers,
|
||||
auth_provider.clone(),
|
||||
),
|
||||
http_config,
|
||||
);
|
||||
Ok(PendingTransport::StreamableHttp { transport })
|
||||
@@ -958,7 +970,7 @@ async fn create_oauth_transport_and_runtime(
|
||||
};
|
||||
|
||||
let auth_client = AuthClient::new(
|
||||
StreamableHttpClientAdapter::new(http_client, default_headers),
|
||||
StreamableHttpClientAdapter::new(http_client, default_headers, /*auth_provider*/ None),
|
||||
manager,
|
||||
);
|
||||
let auth_manager = auth_client.auth_manager.clone();
|
||||
|
||||
@@ -98,6 +98,7 @@ pub(crate) async fn create_client(base_url: &str) -> anyhow::Result<RmcpClient>
|
||||
/*env_http_headers*/ None,
|
||||
OAuthCredentialsStoreMode::File,
|
||||
Environment::default_for_tests().get_http_client(),
|
||||
/*auth_provider*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -135,6 +136,7 @@ pub(crate) async fn create_remote_client(
|
||||
/*env_http_headers*/ None,
|
||||
OAuthCredentialsStoreMode::File,
|
||||
Arc::new(http_client),
|
||||
/*auth_provider*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user