mirror of
https://github.com/openai/codex.git
synced 2026-05-09 13:52:41 +00:00
Compare commits
3 Commits
dev/ningyi
...
dev/adrian
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bbaf6f714 | ||
|
|
2e59854488 | ||
|
|
1e12fb9620 |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -2385,7 +2385,6 @@ dependencies = [
|
||||
"codex-terminal-detection",
|
||||
"codex-utils-template",
|
||||
"core_test_support",
|
||||
"crypto_box",
|
||||
"ed25519-dalek",
|
||||
"keyring",
|
||||
"once_cell",
|
||||
|
||||
@@ -297,7 +297,7 @@ impl AnalyticsEventsClient {
|
||||
}
|
||||
|
||||
async fn send_track_events(
|
||||
auth_manager: &Arc<AuthManager>,
|
||||
auth_manager: &AuthManager,
|
||||
base_url: &str,
|
||||
events: Vec<TrackEventRequest>,
|
||||
) {
|
||||
@@ -310,11 +310,9 @@ async fn send_track_events(
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return;
|
||||
}
|
||||
let Some(authorization_header_value) = auth_manager
|
||||
.chatgpt_authorization_header_for_auth(&auth)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
let access_token = match auth.get_token() {
|
||||
Ok(token) => token,
|
||||
Err(_) => return,
|
||||
};
|
||||
let Some(account_id) = auth.get_account_id() else {
|
||||
return;
|
||||
@@ -324,17 +322,15 @@ async fn send_track_events(
|
||||
let url = format!("{base_url}/codex/analytics-events/events");
|
||||
let payload = TrackEventsRequest { events };
|
||||
|
||||
let mut request = create_client()
|
||||
let response = create_client()
|
||||
.post(&url)
|
||||
.timeout(ANALYTICS_EVENTS_TIMEOUT)
|
||||
.header("authorization", authorization_header_value)
|
||||
.bearer_auth(&access_token)
|
||||
.header("chatgpt-account-id", &account_id)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload);
|
||||
if auth.is_fedramp_account() {
|
||||
request = request.header("X-OpenAI-Fedramp", "true");
|
||||
}
|
||||
let response = request.send().await;
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) if response.status().is_success() => {}
|
||||
|
||||
@@ -286,9 +286,9 @@ use codex_login::run_login_server;
|
||||
use codex_mcp::McpRuntimeEnvironment;
|
||||
use codex_mcp::McpServerStatusSnapshot;
|
||||
use codex_mcp::McpSnapshotDetail;
|
||||
use codex_mcp::collect_mcp_server_status_snapshot_with_detail_and_authorization_header;
|
||||
use codex_mcp::collect_mcp_server_status_snapshot_with_detail;
|
||||
use codex_mcp::discover_supported_scopes;
|
||||
use codex_mcp::effective_mcp_servers_with_authorization_header;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
use codex_mcp::resolve_oauth_scopes;
|
||||
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -2012,28 +2012,12 @@ impl CodexMessageProcessor {
|
||||
});
|
||||
}
|
||||
|
||||
let authorization_header_value = self
|
||||
.auth_manager
|
||||
.chatgpt_authorization_header_for_auth(&auth)
|
||||
.await;
|
||||
let mut client = BackendClient::new(self.config.chatgpt_base_url.clone())
|
||||
.map(|client| {
|
||||
client.with_user_agent(codex_login::default_client::get_codex_user_agent())
|
||||
})
|
||||
let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth)
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to construct backend client: {err}"),
|
||||
data: None,
|
||||
})?;
|
||||
if let Some(authorization_header_value) = authorization_header_value {
|
||||
client = client.with_authorization_header_value(authorization_header_value);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
let snapshots = client
|
||||
.get_rate_limits_many()
|
||||
@@ -5671,8 +5655,7 @@ impl CodexMessageProcessor {
|
||||
let mcp_config = config
|
||||
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
|
||||
.await;
|
||||
let auth_manager = Arc::clone(&self.auth_manager);
|
||||
let auth = auth_manager.auth().await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let runtime_environment = match self.thread_manager.environment_manager().current().await {
|
||||
Ok(Some(environment)) => {
|
||||
// Status listing has no turn cwd. This fallback is used only
|
||||
@@ -5697,113 +5680,122 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) {
|
||||
McpServerStatusDetail::Full => McpSnapshotDetail::Full,
|
||||
McpServerStatusDetail::ToolsAndAuthOnly => McpSnapshotDetail::ToolsAndAuthOnly,
|
||||
};
|
||||
|
||||
let background_authorization_header_value = if let Some(auth) = auth.as_ref() {
|
||||
auth_manager
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let snapshot = collect_mcp_server_status_snapshot_with_detail_and_authorization_header(
|
||||
&mcp_config,
|
||||
auth.as_ref(),
|
||||
request.request_id.to_string(),
|
||||
Self::list_mcp_server_status_task(
|
||||
outgoing,
|
||||
request,
|
||||
params,
|
||||
config,
|
||||
mcp_config,
|
||||
auth,
|
||||
runtime_environment,
|
||||
detail,
|
||||
background_authorization_header_value.as_deref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let effective_servers = effective_mcp_servers_with_authorization_header(
|
||||
&mcp_config,
|
||||
auth.as_ref(),
|
||||
background_authorization_header_value.as_deref(),
|
||||
);
|
||||
let McpServerStatusSnapshot {
|
||||
tools_by_server,
|
||||
resources,
|
||||
resource_templates,
|
||||
auth_statuses,
|
||||
} = snapshot;
|
||||
|
||||
let mut server_names: Vec<String> = config
|
||||
.mcp_servers
|
||||
.keys()
|
||||
.cloned()
|
||||
// Include built-in/plugin MCP servers that are present in the
|
||||
// effective runtime config even when they are not user-declared in
|
||||
// `config.mcp_servers`.
|
||||
.chain(effective_servers.keys().cloned())
|
||||
.chain(auth_statuses.keys().cloned())
|
||||
.chain(resources.keys().cloned())
|
||||
.chain(resource_templates.keys().cloned())
|
||||
.collect();
|
||||
server_names.sort();
|
||||
server_names.dedup();
|
||||
|
||||
let total = server_names.len();
|
||||
let limit = params.limit.unwrap_or(total as u32).max(1) as usize;
|
||||
let effective_limit = limit.min(total);
|
||||
let start = match params.cursor {
|
||||
Some(cursor) => match cursor.parse::<usize>() {
|
||||
Ok(idx) => idx,
|
||||
Err(_) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid cursor: {cursor}"),
|
||||
data: None,
|
||||
};
|
||||
outgoing.send_error(request, error).await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
|
||||
if start > total {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("cursor {start} exceeds total MCP servers {total}"),
|
||||
data: None,
|
||||
};
|
||||
outgoing.send_error(request, error).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let end = start.saturating_add(effective_limit).min(total);
|
||||
|
||||
let data: Vec<McpServerStatus> = server_names[start..end]
|
||||
.iter()
|
||||
.map(|name| McpServerStatus {
|
||||
name: name.clone(),
|
||||
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
|
||||
resources: resources.get(name).cloned().unwrap_or_default(),
|
||||
resource_templates: resource_templates.get(name).cloned().unwrap_or_default(),
|
||||
auth_status: auth_statuses
|
||||
.get(name)
|
||||
.cloned()
|
||||
.unwrap_or(CoreMcpAuthStatus::Unsupported)
|
||||
.into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let next_cursor = if end < total {
|
||||
Some(end.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = ListMcpServerStatusResponse { data, next_cursor };
|
||||
|
||||
outgoing.send_response(request, response).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn list_mcp_server_status_task(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ListMcpServerStatusParams,
|
||||
config: Config,
|
||||
mcp_config: codex_mcp::McpConfig,
|
||||
auth: Option<CodexAuth>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
) {
|
||||
let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) {
|
||||
McpServerStatusDetail::Full => McpSnapshotDetail::Full,
|
||||
McpServerStatusDetail::ToolsAndAuthOnly => McpSnapshotDetail::ToolsAndAuthOnly,
|
||||
};
|
||||
|
||||
let snapshot = collect_mcp_server_status_snapshot_with_detail(
|
||||
&mcp_config,
|
||||
auth.as_ref(),
|
||||
request_id.request_id.to_string(),
|
||||
runtime_environment,
|
||||
detail,
|
||||
)
|
||||
.await;
|
||||
|
||||
let effective_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
|
||||
let McpServerStatusSnapshot {
|
||||
tools_by_server,
|
||||
resources,
|
||||
resource_templates,
|
||||
auth_statuses,
|
||||
} = snapshot;
|
||||
|
||||
let mut server_names: Vec<String> = config
|
||||
.mcp_servers
|
||||
.keys()
|
||||
.cloned()
|
||||
// Include built-in/plugin MCP servers that are present in the
|
||||
// effective runtime config even when they are not user-declared in
|
||||
// `config.mcp_servers`.
|
||||
.chain(effective_servers.keys().cloned())
|
||||
.chain(auth_statuses.keys().cloned())
|
||||
.chain(resources.keys().cloned())
|
||||
.chain(resource_templates.keys().cloned())
|
||||
.collect();
|
||||
server_names.sort();
|
||||
server_names.dedup();
|
||||
|
||||
let total = server_names.len();
|
||||
let limit = params.limit.unwrap_or(total as u32).max(1) as usize;
|
||||
let effective_limit = limit.min(total);
|
||||
let start = match params.cursor {
|
||||
Some(cursor) => match cursor.parse::<usize>() {
|
||||
Ok(idx) => idx,
|
||||
Err(_) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid cursor: {cursor}"),
|
||||
data: None,
|
||||
};
|
||||
outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => 0,
|
||||
};
|
||||
|
||||
if start > total {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("cursor {start} exceeds total MCP servers {total}"),
|
||||
data: None,
|
||||
};
|
||||
outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let end = start.saturating_add(effective_limit).min(total);
|
||||
|
||||
let data: Vec<McpServerStatus> = server_names[start..end]
|
||||
.iter()
|
||||
.map(|name| McpServerStatus {
|
||||
name: name.clone(),
|
||||
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
|
||||
resources: resources.get(name).cloned().unwrap_or_default(),
|
||||
resource_templates: resource_templates.get(name).cloned().unwrap_or_default(),
|
||||
auth_status: auth_statuses
|
||||
.get(name)
|
||||
.cloned()
|
||||
.unwrap_or(CoreMcpAuthStatus::Unsupported)
|
||||
.into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let next_cursor = if end < total {
|
||||
Some(end.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = ListMcpServerStatusResponse { data, next_cursor };
|
||||
|
||||
outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
|
||||
async fn read_mcp_resource(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
|
||||
@@ -27,11 +27,10 @@ use crate::outgoing_message::QueuedOutgoingMessage;
|
||||
use crate::transport::CHANNEL_CAPACITY;
|
||||
use crate::transport::ConnectionState;
|
||||
use crate::transport::OutboundConnectionState;
|
||||
use crate::transport::RemoteControlStartOptions;
|
||||
use crate::transport::TransportEvent;
|
||||
use crate::transport::auth::policy_from_settings;
|
||||
use crate::transport::route_outgoing_envelope;
|
||||
use crate::transport::start_remote_control_with_options;
|
||||
use crate::transport::start_remote_control;
|
||||
use crate::transport::start_stdio_connection;
|
||||
use crate::transport::start_websocket_acceptor;
|
||||
use codex_analytics::AppServerRpcTransport;
|
||||
@@ -576,17 +575,16 @@ pub async fn run_main_with_transport(
|
||||
));
|
||||
}
|
||||
|
||||
let (remote_control_accept_handle, remote_control_handle) =
|
||||
start_remote_control_with_options(RemoteControlStartOptions {
|
||||
remote_control_url: config.chatgpt_base_url.clone(),
|
||||
state_db: state_db.clone(),
|
||||
auth_manager: auth_manager.clone(),
|
||||
transport_event_tx: transport_event_tx.clone(),
|
||||
shutdown_token: transport_shutdown_token.clone(),
|
||||
app_server_client_name_rx,
|
||||
initial_enabled: remote_control_enabled,
|
||||
})
|
||||
.await?;
|
||||
let (remote_control_accept_handle, remote_control_handle) = start_remote_control(
|
||||
config.chatgpt_base_url.clone(),
|
||||
state_db.clone(),
|
||||
auth_manager.clone(),
|
||||
transport_event_tx.clone(),
|
||||
transport_shutdown_token.clone(),
|
||||
app_server_client_name_rx,
|
||||
remote_control_enabled,
|
||||
)
|
||||
.await?;
|
||||
transport_accept_handles.push(remote_control_accept_handle);
|
||||
|
||||
let outbound_handle = tokio::spawn(async move {
|
||||
|
||||
@@ -34,8 +34,7 @@ mod stdio;
|
||||
mod websocket;
|
||||
|
||||
pub(crate) use remote_control::RemoteControlHandle;
|
||||
pub(crate) use remote_control::RemoteControlStartOptions;
|
||||
pub(crate) use remote_control::start_remote_control_with_options;
|
||||
pub(crate) use remote_control::start_remote_control;
|
||||
pub(crate) use stdio::start_stdio_connection;
|
||||
pub(crate) use websocket::start_websocket_acceptor;
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ const REMOTE_CONTROL_RESPONSE_BODY_MAX_BYTES: usize = 4096;
|
||||
const REQUEST_ID_HEADER: &str = "x-request-id";
|
||||
const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id";
|
||||
const CF_RAY_HEADER: &str = "cf-ray";
|
||||
const REMOTE_CONTROL_FEDRAMP_HEADER: &str = "X-OpenAI-Fedramp";
|
||||
pub(super) const REMOTE_CONTROL_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -30,9 +29,8 @@ pub(super) struct RemoteControlEnrollment {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct RemoteControlConnectionAuth {
|
||||
pub(super) authorization_header_value: String,
|
||||
pub(super) bearer_token: String,
|
||||
pub(super) account_id: String,
|
||||
pub(super) is_fedramp_account: bool,
|
||||
}
|
||||
|
||||
pub(super) async fn load_persisted_remote_control_enrollment(
|
||||
@@ -201,15 +199,12 @@ pub(super) async fn enroll_remote_control_server(
|
||||
app_server_version: env!("CARGO_PKG_VERSION"),
|
||||
};
|
||||
let client = build_reqwest_client();
|
||||
let mut http_request = client
|
||||
let http_request = client
|
||||
.post(enroll_url)
|
||||
.timeout(REMOTE_CONTROL_ENROLL_TIMEOUT)
|
||||
.header("authorization", &auth.authorization_header_value)
|
||||
.header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id);
|
||||
if auth.is_fedramp_account {
|
||||
http_request = http_request.header(REMOTE_CONTROL_FEDRAMP_HEADER, "true");
|
||||
}
|
||||
let http_request = http_request.json(&request);
|
||||
.bearer_auth(&auth.bearer_token)
|
||||
.header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)
|
||||
.json(&request);
|
||||
|
||||
let response = http_request.send().await.map_err(|err| {
|
||||
io::Error::other(format!(
|
||||
@@ -450,9 +445,8 @@ mod tests {
|
||||
let err = enroll_remote_control_server(
|
||||
&remote_control_target,
|
||||
&RemoteControlConnectionAuth {
|
||||
authorization_header_value: "Bearer Access Token".to_string(),
|
||||
bearer_token: "Access Token".to_string(),
|
||||
account_id: "account_id".to_string(),
|
||||
is_fedramp_account: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -4,7 +4,6 @@ mod protocol;
|
||||
mod websocket;
|
||||
|
||||
use crate::transport::remote_control::websocket::RemoteControlWebsocket;
|
||||
use crate::transport::remote_control::websocket::RemoteControlWebsocketOptions;
|
||||
|
||||
pub use self::protocol::ClientId;
|
||||
use self::protocol::ServerEvent;
|
||||
@@ -45,17 +44,6 @@ impl RemoteControlHandle {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RemoteControlStartOptions {
|
||||
pub(crate) remote_control_url: String,
|
||||
pub(crate) state_db: Option<Arc<StateRuntime>>,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
pub(crate) transport_event_tx: mpsc::Sender<TransportEvent>,
|
||||
pub(crate) shutdown_token: CancellationToken,
|
||||
pub(crate) app_server_client_name_rx: Option<oneshot::Receiver<String>>,
|
||||
pub(crate) initial_enabled: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn start_remote_control(
|
||||
remote_control_url: String,
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
@@ -65,38 +53,15 @@ pub(crate) async fn start_remote_control(
|
||||
app_server_client_name_rx: Option<oneshot::Receiver<String>>,
|
||||
initial_enabled: bool,
|
||||
) -> io::Result<(JoinHandle<()>, RemoteControlHandle)> {
|
||||
start_remote_control_with_options(RemoteControlStartOptions {
|
||||
remote_control_url,
|
||||
state_db,
|
||||
auth_manager,
|
||||
transport_event_tx,
|
||||
shutdown_token,
|
||||
app_server_client_name_rx,
|
||||
initial_enabled,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn start_remote_control_with_options(
|
||||
options: RemoteControlStartOptions,
|
||||
) -> io::Result<(JoinHandle<()>, RemoteControlHandle)> {
|
||||
let RemoteControlStartOptions {
|
||||
remote_control_url,
|
||||
state_db,
|
||||
auth_manager,
|
||||
transport_event_tx,
|
||||
shutdown_token,
|
||||
app_server_client_name_rx,
|
||||
initial_enabled,
|
||||
} = options;
|
||||
let remote_control_target = if initial_enabled {
|
||||
Some(normalize_remote_control_url(&remote_control_url)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (enabled_tx, enabled_rx) = watch::channel(initial_enabled);
|
||||
let join_handle = tokio::spawn(async move {
|
||||
RemoteControlWebsocket::from_options(RemoteControlWebsocketOptions {
|
||||
RemoteControlWebsocket::new(
|
||||
remote_control_url,
|
||||
remote_control_target,
|
||||
state_db,
|
||||
@@ -104,7 +69,7 @@ pub(crate) async fn start_remote_control_with_options(
|
||||
transport_event_tx,
|
||||
shutdown_token,
|
||||
enabled_rx,
|
||||
})
|
||||
)
|
||||
.run(app_server_client_name_rx)
|
||||
.await;
|
||||
});
|
||||
|
||||
@@ -49,7 +49,6 @@ use tracing::warn;
|
||||
|
||||
pub(super) const REMOTE_CONTROL_PROTOCOL_VERSION: &str = "2";
|
||||
pub(super) const REMOTE_CONTROL_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id";
|
||||
const REMOTE_CONTROL_FEDRAMP_HEADER: &str = "X-OpenAI-Fedramp";
|
||||
const REMOTE_CONTROL_SUBSCRIBE_CURSOR_HEADER: &str = "x-codex-subscribe-cursor";
|
||||
const REMOTE_CONTROL_WEBSOCKET_PING_INTERVAL: std::time::Duration =
|
||||
std::time::Duration::from_secs(10);
|
||||
@@ -129,16 +128,6 @@ pub(crate) struct RemoteControlWebsocket {
|
||||
enabled_rx: watch::Receiver<bool>,
|
||||
}
|
||||
|
||||
pub(crate) struct RemoteControlWebsocketOptions {
|
||||
pub(crate) remote_control_url: String,
|
||||
pub(crate) remote_control_target: Option<RemoteControlTarget>,
|
||||
pub(crate) state_db: Option<Arc<StateRuntime>>,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
pub(crate) transport_event_tx: mpsc::Sender<TransportEvent>,
|
||||
pub(crate) shutdown_token: CancellationToken,
|
||||
pub(crate) enabled_rx: watch::Receiver<bool>,
|
||||
}
|
||||
|
||||
enum ConnectOutcome {
|
||||
Connected(Box<WebSocketStream<MaybeTlsStream<TcpStream>>>),
|
||||
Disabled,
|
||||
@@ -146,7 +135,6 @@ enum ConnectOutcome {
|
||||
}
|
||||
|
||||
impl RemoteControlWebsocket {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(
|
||||
remote_control_url: String,
|
||||
remote_control_target: Option<RemoteControlTarget>,
|
||||
@@ -156,27 +144,6 @@ impl RemoteControlWebsocket {
|
||||
shutdown_token: CancellationToken,
|
||||
enabled_rx: watch::Receiver<bool>,
|
||||
) -> Self {
|
||||
Self::from_options(RemoteControlWebsocketOptions {
|
||||
remote_control_url,
|
||||
remote_control_target,
|
||||
state_db,
|
||||
auth_manager,
|
||||
transport_event_tx,
|
||||
shutdown_token,
|
||||
enabled_rx,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn from_options(options: RemoteControlWebsocketOptions) -> Self {
|
||||
let RemoteControlWebsocketOptions {
|
||||
remote_control_url,
|
||||
remote_control_target,
|
||||
state_db,
|
||||
auth_manager,
|
||||
transport_event_tx,
|
||||
shutdown_token,
|
||||
enabled_rx,
|
||||
} = options;
|
||||
let shutdown_token = shutdown_token.child_token();
|
||||
let (server_event_tx, server_event_rx) = mpsc::channel(super::CHANNEL_CAPACITY);
|
||||
let client_tracker =
|
||||
@@ -304,16 +271,14 @@ impl RemoteControlWebsocket {
|
||||
}
|
||||
return ConnectOutcome::Disabled;
|
||||
}
|
||||
connect_result = connect_remote_control_websocket_with_options(
|
||||
ConnectRemoteControlWebsocketOptions {
|
||||
remote_control_target: &remote_control_target,
|
||||
state_db: self.state_db.as_deref(),
|
||||
auth_manager: &self.auth_manager,
|
||||
auth_recovery: &mut self.auth_recovery,
|
||||
enrollment: &mut self.enrollment,
|
||||
subscribe_cursor: subscribe_cursor.as_deref(),
|
||||
app_server_client_name,
|
||||
},
|
||||
connect_result = connect_remote_control_websocket(
|
||||
&remote_control_target,
|
||||
self.state_db.as_deref(),
|
||||
&self.auth_manager,
|
||||
&mut self.auth_recovery,
|
||||
&mut self.enrollment,
|
||||
subscribe_cursor.as_deref(),
|
||||
app_server_client_name,
|
||||
) => connect_result,
|
||||
};
|
||||
|
||||
@@ -703,11 +668,12 @@ fn build_remote_control_websocket_request(
|
||||
"x-codex-protocol-version",
|
||||
REMOTE_CONTROL_PROTOCOL_VERSION,
|
||||
)?;
|
||||
set_remote_control_header(headers, "authorization", &auth.authorization_header_value)?;
|
||||
set_remote_control_header(
|
||||
headers,
|
||||
"authorization",
|
||||
&format!("Bearer {}", auth.bearer_token),
|
||||
)?;
|
||||
set_remote_control_header(headers, REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)?;
|
||||
if auth.is_fedramp_account {
|
||||
set_remote_control_header(headers, REMOTE_CONTROL_FEDRAMP_HEADER, "true")?;
|
||||
}
|
||||
if let Some(subscribe_cursor) = subscribe_cursor {
|
||||
set_remote_control_header(
|
||||
headers,
|
||||
@@ -752,19 +718,8 @@ pub(crate) async fn load_remote_control_auth(
|
||||
));
|
||||
}
|
||||
|
||||
let authorization_header_value = auth_manager
|
||||
.chatgpt_authorization_header_for_auth(&auth)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
ErrorKind::PermissionDenied,
|
||||
"remote control requires ChatGPT authentication",
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(RemoteControlConnectionAuth {
|
||||
authorization_header_value,
|
||||
is_fedramp_account: auth.is_fedramp_account(),
|
||||
bearer_token: auth.get_token().map_err(io::Error::other)?,
|
||||
account_id: auth.get_account_id().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
ErrorKind::WouldBlock,
|
||||
@@ -774,7 +729,6 @@ pub(crate) async fn load_remote_control_auth(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) async fn connect_remote_control_websocket(
|
||||
remote_control_target: &RemoteControlTarget,
|
||||
state_db: Option<&StateRuntime>,
|
||||
@@ -787,44 +741,6 @@ pub(super) async fn connect_remote_control_websocket(
|
||||
WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
tungstenite::http::Response<()>,
|
||||
)> {
|
||||
connect_remote_control_websocket_with_options(ConnectRemoteControlWebsocketOptions {
|
||||
remote_control_target,
|
||||
state_db,
|
||||
auth_manager,
|
||||
auth_recovery,
|
||||
enrollment,
|
||||
subscribe_cursor,
|
||||
app_server_client_name,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
struct ConnectRemoteControlWebsocketOptions<'a> {
|
||||
remote_control_target: &'a RemoteControlTarget,
|
||||
state_db: Option<&'a StateRuntime>,
|
||||
auth_manager: &'a Arc<AuthManager>,
|
||||
auth_recovery: &'a mut UnauthorizedRecovery,
|
||||
enrollment: &'a mut Option<RemoteControlEnrollment>,
|
||||
subscribe_cursor: Option<&'a str>,
|
||||
app_server_client_name: Option<&'a str>,
|
||||
}
|
||||
|
||||
async fn connect_remote_control_websocket_with_options(
|
||||
options: ConnectRemoteControlWebsocketOptions<'_>,
|
||||
) -> io::Result<(
|
||||
WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
tungstenite::http::Response<()>,
|
||||
)> {
|
||||
let ConnectRemoteControlWebsocketOptions {
|
||||
remote_control_target,
|
||||
state_db,
|
||||
auth_manager,
|
||||
auth_recovery,
|
||||
enrollment,
|
||||
subscribe_cursor,
|
||||
app_server_client_name,
|
||||
} = options;
|
||||
|
||||
ensure_rustls_crypto_provider();
|
||||
|
||||
let auth = load_remote_control_auth(auth_manager).await?;
|
||||
@@ -1087,34 +1003,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_remote_control_websocket_request_includes_fedramp_header() {
|
||||
let request = build_remote_control_websocket_request(
|
||||
"ws://127.0.0.1/backend-api/wham/remote/control/server",
|
||||
&RemoteControlEnrollment {
|
||||
account_id: "account_id".to_string(),
|
||||
environment_id: "env_test".to_string(),
|
||||
server_id: "srv_e_test".to_string(),
|
||||
server_name: "test-server".to_string(),
|
||||
},
|
||||
&RemoteControlConnectionAuth {
|
||||
authorization_header_value: "AgentAssertion assertion".to_string(),
|
||||
account_id: "account_id".to_string(),
|
||||
is_fedramp_account: true,
|
||||
},
|
||||
/*subscribe_cursor*/ None,
|
||||
)
|
||||
.expect("request should build");
|
||||
|
||||
assert_eq!(
|
||||
request
|
||||
.headers()
|
||||
.get(REMOTE_CONTROL_FEDRAMP_HEADER)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("true")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect_remote_control_websocket_includes_http_error_details() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
|
||||
@@ -116,7 +116,7 @@ impl PathStyle {
|
||||
pub struct Client {
|
||||
base_url: String,
|
||||
http: reqwest::Client,
|
||||
authorization_header_value: Option<String>,
|
||||
bearer_token: Option<String>,
|
||||
user_agent: Option<HeaderValue>,
|
||||
chatgpt_account_id: Option<String>,
|
||||
chatgpt_account_is_fedramp: bool,
|
||||
@@ -142,7 +142,7 @@ impl Client {
|
||||
Ok(Self {
|
||||
base_url,
|
||||
http,
|
||||
authorization_header_value: None,
|
||||
bearer_token: None,
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
@@ -165,12 +165,7 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
|
||||
self.authorization_header_value = Some(format!("Bearer {}", token.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_authorization_header_value(mut self, value: impl Into<String>) -> Self {
|
||||
self.authorization_header_value = Some(value.into());
|
||||
self.bearer_token = Some(token.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -203,10 +198,11 @@ impl Client {
|
||||
} else {
|
||||
h.insert(USER_AGENT, HeaderValue::from_static("codex-cli"));
|
||||
}
|
||||
if let Some(value) = &self.authorization_header_value
|
||||
&& let Ok(hv) = HeaderValue::from_str(value)
|
||||
{
|
||||
h.insert(AUTHORIZATION, hv);
|
||||
if let Some(token) = &self.bearer_token {
|
||||
let value = format!("Bearer {token}");
|
||||
if let Ok(hv) = HeaderValue::from_str(&value) {
|
||||
h.insert(AUTHORIZATION, hv);
|
||||
}
|
||||
}
|
||||
if let Some(acc) = &self.chatgpt_account_id
|
||||
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
|
||||
@@ -820,7 +816,7 @@ mod tests {
|
||||
let codex_client = Client {
|
||||
base_url: "https://example.test".to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
authorization_header_value: None,
|
||||
bearer_token: None,
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
@@ -834,7 +830,7 @@ mod tests {
|
||||
let chatgpt_client = Client {
|
||||
base_url: "https://chatgpt.com/backend-api".to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
authorization_header_value: None,
|
||||
bearer_token: None,
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use codex_core::config::Config;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::default_client::create_client;
|
||||
|
||||
use crate::chatgpt_token::get_chatgpt_token_data;
|
||||
@@ -32,32 +31,16 @@ pub(crate) async fn chatgpt_get_request_with_timeout<T: DeserializeOwned>(
|
||||
|
||||
let token =
|
||||
get_chatgpt_token_data().ok_or_else(|| anyhow::anyhow!("ChatGPT token not available"))?;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false);
|
||||
let auth = auth_manager.auth().await;
|
||||
let is_fedramp_account = auth
|
||||
.as_ref()
|
||||
.is_some_and(codex_login::CodexAuth::is_fedramp_account);
|
||||
let authorization_header_value = match auth.as_ref() {
|
||||
Some(auth) if auth.is_chatgpt_auth() => auth_manager
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await
|
||||
.unwrap_or_else(|| format!("Bearer {}", token.access_token)),
|
||||
_ => format!("Bearer {}", token.access_token),
|
||||
};
|
||||
|
||||
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)
|
||||
.header("authorization", authorization_header_value)
|
||||
.header("chatgpt-account-id", account_id)
|
||||
.bearer_auth(&token.access_token)
|
||||
.header("chatgpt-account-id", account_id?)
|
||||
.header("Content-Type", "application/json");
|
||||
if is_fedramp_account {
|
||||
request = request.header("X-OpenAI-Fedramp", "true");
|
||||
}
|
||||
|
||||
if let Some(timeout) = timeout {
|
||||
request = request.timeout(timeout);
|
||||
|
||||
@@ -195,15 +195,11 @@ trait RequirementsFetcher: Send + Sync {
|
||||
|
||||
struct BackendRequirementsFetcher {
|
||||
base_url: String,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
}
|
||||
|
||||
impl BackendRequirementsFetcher {
|
||||
fn new(auth_manager: Arc<AuthManager>, base_url: String) -> Self {
|
||||
Self {
|
||||
base_url,
|
||||
auth_manager,
|
||||
}
|
||||
fn new(base_url: String) -> Self {
|
||||
Self { base_url }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,14 +209,7 @@ impl RequirementsFetcher for BackendRequirementsFetcher {
|
||||
&self,
|
||||
auth: &CodexAuth,
|
||||
) -> Result<Option<String>, FetchAttemptError> {
|
||||
let authorization_header_value = self
|
||||
.auth_manager
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await;
|
||||
let mut client = BackendClient::new(self.base_url.clone())
|
||||
.map(|client| {
|
||||
client.with_user_agent(codex_login::default_client::get_codex_user_agent())
|
||||
})
|
||||
let client = BackendClient::from_auth(self.base_url.clone(), auth)
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
@@ -228,15 +217,6 @@ impl RequirementsFetcher for BackendRequirementsFetcher {
|
||||
);
|
||||
})
|
||||
.map_err(|_| FetchAttemptError::Retryable(RetryableFailureKind::BackendClientInit))?;
|
||||
if let Some(authorization_header_value) = authorization_header_value {
|
||||
client = client.with_authorization_header_value(authorization_header_value);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
let response = client
|
||||
.get_config_requirements_file()
|
||||
@@ -713,11 +693,8 @@ pub fn cloud_requirements_loader(
|
||||
codex_home: PathBuf,
|
||||
) -> CloudRequirementsLoader {
|
||||
let service = CloudRequirementsService::new(
|
||||
auth_manager.clone(),
|
||||
Arc::new(BackendRequirementsFetcher::new(
|
||||
auth_manager,
|
||||
chatgpt_base_url,
|
||||
)),
|
||||
auth_manager,
|
||||
Arc::new(BackendRequirementsFetcher::new(chatgpt_base_url)),
|
||||
codex_home,
|
||||
CLOUD_REQUIREMENTS_TIMEOUT,
|
||||
);
|
||||
|
||||
@@ -37,11 +37,6 @@ impl HttpClient {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_authorization_header_value(mut self, value: impl Into<String>) -> Self {
|
||||
self.backend = self.backend.clone().with_authorization_header_value(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
|
||||
self.backend = self.backend.clone().with_user_agent(ua);
|
||||
self
|
||||
@@ -52,11 +47,6 @@ impl HttpClient {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_fedramp_routing_header(mut self) -> Self {
|
||||
self.backend = self.backend.clone().with_fedramp_routing_header();
|
||||
self
|
||||
}
|
||||
|
||||
fn tasks_api(&self) -> api::Tasks<'_> {
|
||||
api::Tasks::new(self)
|
||||
}
|
||||
|
||||
@@ -68,45 +68,43 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext>
|
||||
};
|
||||
append_error_log(format!("startup: base_url={base_url} path_style={style}"));
|
||||
|
||||
let Some(auth_manager) = util::load_auth_manager(Some(base_url.clone())).await else {
|
||||
eprintln!(
|
||||
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
);
|
||||
std::process::exit(1);
|
||||
let auth_manager = util::load_auth_manager().await;
|
||||
let auth = match auth_manager.as_ref() {
|
||||
Some(manager) => manager.auth().await,
|
||||
None => None,
|
||||
};
|
||||
let Some(auth) = auth_manager.auth().await else {
|
||||
eprintln!(
|
||||
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
);
|
||||
std::process::exit(1);
|
||||
let auth = match auth {
|
||||
Some(auth) => auth,
|
||||
None => {
|
||||
eprintln!(
|
||||
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(acc) = auth.get_account_id() {
|
||||
append_error_log(format!("auth: mode=ChatGPT account_id={acc}"));
|
||||
}
|
||||
|
||||
let authorization_header_value = auth_manager
|
||||
.chatgpt_authorization_header_for_auth(&auth)
|
||||
.await;
|
||||
let Some(authorization_header_value) = authorization_header_value else {
|
||||
eprintln!(
|
||||
"Not signed in. Please run 'codex login' to sign in with ChatGPT, then re-run 'codex cloud'."
|
||||
);
|
||||
std::process::exit(1);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
http = http.with_authorization_header_value(authorization_header_value);
|
||||
if let Some(acc) = auth.get_account_id().or_else(|| {
|
||||
auth.get_token()
|
||||
.ok()
|
||||
.and_then(|token| util::extract_chatgpt_account_id(&token))
|
||||
}) {
|
||||
http = http.with_bearer_token(token.clone());
|
||||
if let Some(acc) = auth
|
||||
.get_account_id()
|
||||
.or_else(|| util::extract_chatgpt_account_id(&token))
|
||||
{
|
||||
append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
|
||||
http = http.with_chatgpt_account_id(acc);
|
||||
}
|
||||
if auth.is_fedramp_account() {
|
||||
http = http.with_fedramp_routing_header();
|
||||
}
|
||||
|
||||
Ok(BackendContext {
|
||||
backend: Arc::new(http),
|
||||
|
||||
@@ -3,7 +3,6 @@ use chrono::DateTime;
|
||||
use chrono::Local;
|
||||
use chrono::Utc;
|
||||
use reqwest::header::HeaderMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_login::AuthManager;
|
||||
@@ -60,18 +59,18 @@ pub fn extract_chatgpt_account_id(token: &str) -> Option<String> {
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<Arc<AuthManager>> {
|
||||
pub async fn load_auth_manager() -> Option<AuthManager> {
|
||||
// TODO: pass in cli overrides once cloud tasks properly support them.
|
||||
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false);
|
||||
if let Some(chatgpt_base_url) = chatgpt_base_url {
|
||||
auth_manager.set_chatgpt_backend_base_url(Some(chatgpt_base_url));
|
||||
}
|
||||
Some(auth_manager)
|
||||
Some(AuthManager::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
))
|
||||
}
|
||||
|
||||
/// Build headers for ChatGPT-backed requests.
|
||||
/// 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;
|
||||
@@ -85,34 +84,23 @@ pub async fn build_chatgpt_headers() -> HeaderMap {
|
||||
USER_AGENT,
|
||||
HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")),
|
||||
);
|
||||
let base_url = normalize_base_url(
|
||||
&std::env::var("CODEX_CLOUD_TASKS_BASE_URL")
|
||||
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()),
|
||||
);
|
||||
if let Some(auth_manager) = load_auth_manager(Some(base_url)).await
|
||||
&& let Some(auth) = auth_manager.auth().await
|
||||
if let Some(am) = load_auth_manager().await
|
||||
&& let Some(auth) = am.auth().await
|
||||
&& let Ok(tok) = auth.get_token()
|
||||
&& !tok.is_empty()
|
||||
{
|
||||
if let Some(authorization_header_value) = auth_manager
|
||||
.chatgpt_authorization_header_for_auth(&auth)
|
||||
.await
|
||||
&& let Ok(hv) = HeaderValue::from_str(&authorization_header_value)
|
||||
{
|
||||
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(|| {
|
||||
auth.get_token()
|
||||
.ok()
|
||||
.and_then(|token| extract_chatgpt_account_id(&token))
|
||||
}) && let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
|
||||
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);
|
||||
}
|
||||
if auth.is_fedramp_account()
|
||||
&& let Ok(name) = HeaderName::from_bytes(b"X-OpenAI-Fedramp")
|
||||
{
|
||||
headers.insert(name, HeaderValue::from_static("true"));
|
||||
}
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
@@ -16,18 +16,15 @@ pub use mcp::ToolPluginProvenance;
|
||||
pub use mcp::canonical_mcp_server_key;
|
||||
pub use mcp::collect_mcp_server_status_snapshot;
|
||||
pub use mcp::collect_mcp_server_status_snapshot_with_detail;
|
||||
pub use mcp::collect_mcp_server_status_snapshot_with_detail_and_authorization_header;
|
||||
pub use mcp::collect_mcp_snapshot;
|
||||
pub use mcp::collect_mcp_snapshot_from_manager;
|
||||
pub use mcp::collect_mcp_snapshot_from_manager_with_detail;
|
||||
pub use mcp::collect_mcp_snapshot_with_detail;
|
||||
pub use mcp::collect_mcp_snapshot_with_detail_and_authorization_header;
|
||||
pub use mcp::collect_missing_mcp_dependencies;
|
||||
pub use mcp::compute_auth_statuses;
|
||||
pub use mcp::configured_mcp_servers;
|
||||
pub use mcp::discover_supported_scopes;
|
||||
pub use mcp::effective_mcp_servers;
|
||||
pub use mcp::effective_mcp_servers_with_authorization_header;
|
||||
pub use mcp::group_tools_by_server;
|
||||
pub use mcp::mcp_permission_prompt_is_auto_approved;
|
||||
pub use mcp::oauth_login_support;
|
||||
@@ -37,7 +34,6 @@ pub use mcp::should_retry_without_scopes;
|
||||
pub use mcp::split_qualified_tool_name;
|
||||
pub use mcp::tool_plugin_provenance;
|
||||
pub use mcp::with_codex_apps_mcp;
|
||||
pub use mcp::with_codex_apps_mcp_with_authorization_header;
|
||||
pub use mcp_connection_manager::CodexAppsToolsCacheKey;
|
||||
pub use mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT;
|
||||
pub use mcp_connection_manager::MCP_SANDBOX_STATE_META_CAPABILITY;
|
||||
|
||||
@@ -213,25 +213,14 @@ fn codex_apps_mcp_bearer_token(auth: Option<&CodexAuth>) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_apps_mcp_http_headers(
|
||||
auth: Option<&CodexAuth>,
|
||||
authorization_header_value: Option<&str>,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
fn codex_apps_mcp_http_headers(auth: Option<&CodexAuth>) -> Option<HashMap<String, String>> {
|
||||
let mut headers = HashMap::new();
|
||||
if let Some(authorization_header_value) = authorization_header_value {
|
||||
headers.insert(
|
||||
"Authorization".to_string(),
|
||||
authorization_header_value.to_string(),
|
||||
);
|
||||
} else if let Some(token) = codex_apps_mcp_bearer_token(auth) {
|
||||
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 auth.is_some_and(CodexAuth::is_fedramp_account) {
|
||||
headers.insert("X-OpenAI-Fedramp".to_string(), "true".to_string());
|
||||
}
|
||||
if headers.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -265,16 +254,12 @@ 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>,
|
||||
authorization_header_value: Option<&str>,
|
||||
) -> McpServerConfig {
|
||||
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, authorization_header_value)
|
||||
codex_apps_mcp_http_headers(auth)
|
||||
};
|
||||
let url = codex_apps_mcp_url(config);
|
||||
|
||||
@@ -302,25 +287,14 @@ fn codex_apps_mcp_server_config(
|
||||
}
|
||||
|
||||
pub fn with_codex_apps_mcp(
|
||||
servers: HashMap<String, McpServerConfig>,
|
||||
auth: Option<&CodexAuth>,
|
||||
config: &McpConfig,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
with_codex_apps_mcp_with_authorization_header(
|
||||
servers, auth, config, /*authorization_header_value*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_codex_apps_mcp_with_authorization_header(
|
||||
mut servers: HashMap<String, McpServerConfig>,
|
||||
auth: Option<&CodexAuth>,
|
||||
config: &McpConfig,
|
||||
authorization_header_value: Option<&str>,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
if config.apps_enabled && auth.is_some_and(CodexAuth::is_chatgpt_auth) {
|
||||
servers.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
codex_apps_mcp_server_config(config, auth, authorization_header_value),
|
||||
codex_apps_mcp_server_config(config, auth),
|
||||
);
|
||||
} else {
|
||||
servers.remove(CODEX_APPS_MCP_SERVER_NAME);
|
||||
@@ -335,19 +309,9 @@ pub fn configured_mcp_servers(config: &McpConfig) -> HashMap<String, McpServerCo
|
||||
pub fn effective_mcp_servers(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
effective_mcp_servers_with_authorization_header(
|
||||
config, auth, /*authorization_header_value*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn effective_mcp_servers_with_authorization_header(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
authorization_header_value: Option<&str>,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
let servers = configured_mcp_servers(config);
|
||||
with_codex_apps_mcp_with_authorization_header(servers, auth, config, authorization_header_value)
|
||||
with_codex_apps_mcp(servers, auth, config)
|
||||
}
|
||||
|
||||
pub fn tool_plugin_provenance(config: &McpConfig) -> ToolPluginProvenance {
|
||||
@@ -377,27 +341,7 @@ pub async fn collect_mcp_snapshot_with_detail(
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
detail: McpSnapshotDetail,
|
||||
) -> McpListToolsResponseEvent {
|
||||
collect_mcp_snapshot_with_detail_and_authorization_header(
|
||||
config,
|
||||
auth,
|
||||
submit_id,
|
||||
runtime_environment,
|
||||
detail,
|
||||
/*authorization_header_value*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn collect_mcp_snapshot_with_detail_and_authorization_header(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
submit_id: String,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
detail: McpSnapshotDetail,
|
||||
authorization_header_value: Option<&str>,
|
||||
) -> McpListToolsResponseEvent {
|
||||
let mcp_servers =
|
||||
effective_mcp_servers_with_authorization_header(config, auth, authorization_header_value);
|
||||
let mcp_servers = effective_mcp_servers(config, auth);
|
||||
let tool_plugin_provenance = tool_plugin_provenance(config);
|
||||
if mcp_servers.is_empty() {
|
||||
return McpListToolsResponseEvent {
|
||||
@@ -472,27 +416,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
detail: McpSnapshotDetail,
|
||||
) -> McpServerStatusSnapshot {
|
||||
collect_mcp_server_status_snapshot_with_detail_and_authorization_header(
|
||||
config,
|
||||
auth,
|
||||
submit_id,
|
||||
runtime_environment,
|
||||
detail,
|
||||
/*authorization_header_value*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn collect_mcp_server_status_snapshot_with_detail_and_authorization_header(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
submit_id: String,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
detail: McpSnapshotDetail,
|
||||
authorization_header_value: Option<&str>,
|
||||
) -> McpServerStatusSnapshot {
|
||||
let mcp_servers =
|
||||
effective_mcp_servers_with_authorization_header(config, auth, authorization_header_value);
|
||||
let mcp_servers = effective_mcp_servers(config, auth);
|
||||
let tool_plugin_provenance = tool_plugin_provenance(config);
|
||||
if mcp_servers.is_empty() {
|
||||
return McpServerStatusSnapshot {
|
||||
|
||||
@@ -6,9 +6,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_login::BackgroundAgentTaskAuthMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::cached_background_agent_task_authorization_header_value;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
|
||||
const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
@@ -114,15 +112,13 @@ pub async fn list_remote_skills(
|
||||
.get(&url)
|
||||
.timeout(REMOTE_SKILLS_API_TIMEOUT)
|
||||
.query(&query_params);
|
||||
let authorization_header_value = authorization_header_value_for_auth(auth)
|
||||
let token = auth
|
||||
.get_token()
|
||||
.context("Failed to read auth token for remote skills")?;
|
||||
request = request.header("authorization", authorization_header_value);
|
||||
request = request.bearer_auth(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
if auth.is_fedramp_account() {
|
||||
request = request.header("X-OpenAI-Fedramp", "true");
|
||||
}
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
@@ -161,15 +157,13 @@ pub async fn export_remote_skill(
|
||||
let url = format!("{base_url}/hazelnuts/{skill_id}/export");
|
||||
let mut request = client.get(&url).timeout(REMOTE_SKILLS_API_TIMEOUT);
|
||||
|
||||
let authorization_header_value = authorization_header_value_for_auth(auth)
|
||||
let token = auth
|
||||
.get_token()
|
||||
.context("Failed to read auth token for remote skills")?;
|
||||
request = request.header("authorization", authorization_header_value);
|
||||
request = request.bearer_auth(token);
|
||||
if let Some(account_id) = auth.get_account_id() {
|
||||
request = request.header("chatgpt-account-id", account_id);
|
||||
}
|
||||
if auth.is_fedramp_account() {
|
||||
request = request.header("X-OpenAI-Fedramp", "true");
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
@@ -207,19 +201,6 @@ pub async fn export_remote_skill(
|
||||
})
|
||||
}
|
||||
|
||||
fn authorization_header_value_for_auth(auth: &CodexAuth) -> std::io::Result<String> {
|
||||
if let Ok(Some(authorization_header_value)) =
|
||||
cached_background_agent_task_authorization_header_value(
|
||||
auth,
|
||||
BackgroundAgentTaskAuthMode::Disabled,
|
||||
)
|
||||
{
|
||||
Ok(authorization_header_value)
|
||||
} else {
|
||||
auth.get_token().map(|token| format!("Bearer {token}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn safe_join(base: &Path, name: &str) -> Result<PathBuf> {
|
||||
let path = Path::new(name);
|
||||
for component in path.components() {
|
||||
|
||||
@@ -412,7 +412,6 @@ impl StoredAgentIdentity {
|
||||
agent_runtime_id: self.agent_runtime_id.clone(),
|
||||
agent_private_key: self.private_key_pkcs8_base64.clone(),
|
||||
registered_at: self.registered_at.clone(),
|
||||
background_task_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,7 +672,6 @@ mod tests {
|
||||
agent_runtime_id: "agent_invalid".to_string(),
|
||||
agent_private_key: "not-valid-base64".to_string(),
|
||||
registered_at: "2026-01-01T00:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
})
|
||||
.expect("seed invalid identity");
|
||||
|
||||
@@ -713,7 +711,6 @@ mod tests {
|
||||
agent_runtime_id: "agent_old".to_string(),
|
||||
agent_private_key: stale_key.private_key_pkcs8_base64,
|
||||
registered_at: "2026-01-01T00:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
})
|
||||
.expect("seed stale identity");
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ use codex_config::types::UriBasedFileOpener;
|
||||
use codex_config::types::WindowsSandboxModeToml;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
pub use codex_features::Feature;
|
||||
use codex_features::Feature;
|
||||
use codex_features::FeatureConfigSource;
|
||||
use codex_features::FeatureOverrides;
|
||||
use codex_features::FeatureToml;
|
||||
@@ -58,7 +58,6 @@ use codex_features::FeaturesToml;
|
||||
use codex_features::MultiAgentV2ConfigToml;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_login::AuthManagerConfig;
|
||||
use codex_login::BackgroundAgentTaskAuthMode;
|
||||
use codex_mcp::McpConfig;
|
||||
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
@@ -630,16 +629,6 @@ impl AuthManagerConfig for Config {
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<String> {
|
||||
self.forced_chatgpt_workspace_id.clone()
|
||||
}
|
||||
|
||||
fn chatgpt_base_url(&self) -> Option<String> {
|
||||
Some(self.chatgpt_base_url.clone())
|
||||
}
|
||||
|
||||
fn background_agent_task_auth_mode(&self) -> BackgroundAgentTaskAuthMode {
|
||||
BackgroundAgentTaskAuthMode::from_feature_enabled(
|
||||
self.features.enabled(Feature::UseAgentIdentity),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
||||
@@ -43,7 +43,7 @@ use codex_mcp::ToolInfo;
|
||||
use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::codex_apps_tools_cache_key;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_mcp::with_codex_apps_mcp_with_authorization_header;
|
||||
use codex_mcp::with_codex_apps_mcp;
|
||||
|
||||
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
||||
const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
@@ -220,20 +220,8 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
});
|
||||
}
|
||||
|
||||
let background_authorization_header_value = if let Some(auth) = auth.as_ref() {
|
||||
auth_manager
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mcp_config = config.to_mcp_config(plugins_manager.as_ref()).await;
|
||||
let mcp_servers = with_codex_apps_mcp_with_authorization_header(
|
||||
HashMap::new(),
|
||||
auth.as_ref(),
|
||||
&mcp_config,
|
||||
background_authorization_header_value.as_deref(),
|
||||
);
|
||||
let mcp_servers = with_codex_apps_mcp(HashMap::new(), auth.as_ref(), &mcp_config);
|
||||
if mcp_servers.is_empty() {
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
connectors: Vec::new(),
|
||||
@@ -435,18 +423,6 @@ async fn list_directory_connectors_for_tool_suggest_with_auth(
|
||||
};
|
||||
let access_token = token_data.access_token.clone();
|
||||
let account_id = account_id.to_string();
|
||||
let is_fedramp_account = token_data.id_token.is_fedramp_account();
|
||||
let authorization_header_value = {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false);
|
||||
match auth {
|
||||
Some(auth) if auth.is_chatgpt_auth() => auth_manager
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await
|
||||
.unwrap_or_else(|| format!("Bearer {access_token}")),
|
||||
_ => format!("Bearer {access_token}"),
|
||||
}
|
||||
};
|
||||
let is_workspace_account = token_data.id_token.is_workspace_account();
|
||||
let cache_key = AllConnectorsCacheKey::new(
|
||||
config.chatgpt_base_url.clone(),
|
||||
@@ -460,15 +436,14 @@ async fn list_directory_connectors_for_tool_suggest_with_auth(
|
||||
is_workspace_account,
|
||||
/*force_refetch*/ false,
|
||||
|path| {
|
||||
let authorization_header_value = authorization_header_value.clone();
|
||||
let access_token = access_token.clone();
|
||||
let account_id = account_id.clone();
|
||||
async move {
|
||||
chatgpt_get_request_with_authorization_header::<DirectoryListResponse>(
|
||||
chatgpt_get_request_with_token::<DirectoryListResponse>(
|
||||
config,
|
||||
path,
|
||||
authorization_header_value.as_str(),
|
||||
access_token.as_str(),
|
||||
account_id.as_str(),
|
||||
is_fedramp_account,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -477,25 +452,23 @@ async fn list_directory_connectors_for_tool_suggest_with_auth(
|
||||
.await
|
||||
}
|
||||
|
||||
async fn chatgpt_get_request_with_authorization_header<T: DeserializeOwned>(
|
||||
async fn chatgpt_get_request_with_token<T: DeserializeOwned>(
|
||||
config: &Config,
|
||||
path: String,
|
||||
authorization_header_value: &str,
|
||||
access_token: &str,
|
||||
account_id: &str,
|
||||
is_fedramp_account: bool,
|
||||
) -> anyhow::Result<T> {
|
||||
let client = create_client();
|
||||
let url = format!("{}{}", config.chatgpt_base_url, path);
|
||||
let mut request = client
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("authorization", authorization_header_value)
|
||||
.bearer_auth(access_token)
|
||||
.header("chatgpt-account-id", account_id)
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(DIRECTORY_CONNECTORS_TIMEOUT);
|
||||
if is_fedramp_account {
|
||||
request = request.header("X-OpenAI-Fedramp", "true");
|
||||
}
|
||||
let response = request.send().await.context("failed to send request")?;
|
||||
.timeout(DIRECTORY_CONNECTORS_TIMEOUT)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to send request")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
response
|
||||
|
||||
@@ -7,7 +7,7 @@ use codex_config::McpServerConfig;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::configured_mcp_servers;
|
||||
use codex_mcp::effective_mcp_servers_with_authorization_header;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
use codex_mcp::tool_plugin_provenance as collect_tool_plugin_provenance;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -29,25 +29,9 @@ impl McpManager {
|
||||
&self,
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
self.effective_servers_with_authorization_header(
|
||||
config, auth, /*authorization_header_value*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn effective_servers_with_authorization_header(
|
||||
&self,
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
authorization_header_value: Option<&str>,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
||||
effective_mcp_servers_with_authorization_header(
|
||||
&mcp_config,
|
||||
auth,
|
||||
authorization_header_value,
|
||||
)
|
||||
effective_mcp_servers(&mcp_config, auth)
|
||||
}
|
||||
|
||||
pub async fn tool_plugin_provenance(&self, config: &Config) -> ToolPluginProvenance {
|
||||
|
||||
@@ -471,22 +471,10 @@ pub async fn reload_user_config(sess: &Arc<Session>) {
|
||||
pub async fn list_mcp_tools(sess: &Session, config: &Arc<Config>, sub_id: String) {
|
||||
let mcp_connection_manager = sess.services.mcp_connection_manager.read().await;
|
||||
let auth = sess.services.auth_manager.auth().await;
|
||||
let background_authorization_header_value = if let Some(auth) = auth.as_ref() {
|
||||
sess.services
|
||||
.auth_manager
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mcp_servers = sess
|
||||
.services
|
||||
.mcp_manager
|
||||
.effective_servers_with_authorization_header(
|
||||
config,
|
||||
auth.as_ref(),
|
||||
background_authorization_header_value.as_deref(),
|
||||
)
|
||||
.effective_servers(config, auth.as_ref())
|
||||
.await;
|
||||
let snapshot = collect_mcp_snapshot_from_manager(
|
||||
&mcp_connection_manager,
|
||||
|
||||
@@ -190,20 +190,7 @@ impl Session {
|
||||
.mcp_manager
|
||||
.tool_plugin_provenance(config.as_ref())
|
||||
.await;
|
||||
let background_authorization_header_value = if let Some(auth) = auth.as_ref() {
|
||||
self.services
|
||||
.auth_manager
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mcp_servers = with_codex_apps_mcp_with_authorization_header(
|
||||
mcp_servers,
|
||||
auth.as_ref(),
|
||||
&mcp_config,
|
||||
background_authorization_header_value.as_deref(),
|
||||
);
|
||||
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 mut guard = self.services.mcp_startup_cancellation_token.lock().await;
|
||||
|
||||
@@ -293,7 +293,7 @@ use crate::unified_exec::UnifiedExecProcessManager;
|
||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_git_utils::get_git_repo_root;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_mcp::with_codex_apps_mcp_with_authorization_header;
|
||||
use codex_mcp::with_codex_apps_mcp;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::THREAD_STARTED_METRIC;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
|
||||
@@ -333,20 +333,8 @@ impl Session {
|
||||
let mcp_manager_for_mcp = Arc::clone(&mcp_manager);
|
||||
let auth_and_mcp_fut = async move {
|
||||
let auth = auth_manager_clone.auth().await;
|
||||
let authorization_header_value = match auth.as_ref() {
|
||||
Some(auth) => {
|
||||
auth_manager_clone
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let mcp_servers = mcp_manager_for_mcp
|
||||
.effective_servers_with_authorization_header(
|
||||
&config_for_mcp,
|
||||
auth.as_ref(),
|
||||
authorization_header_value.as_deref(),
|
||||
)
|
||||
.effective_servers(&config_for_mcp, auth.as_ref())
|
||||
.await;
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
|
||||
@@ -4468,7 +4468,6 @@ fn seed_stored_identity(
|
||||
agent_runtime_id: stored_identity.agent_runtime_id.clone(),
|
||||
agent_private_key: stored_identity.private_key_pkcs8_base64.clone(),
|
||||
registered_at: stored_identity.registered_at.clone(),
|
||||
background_task_id: None,
|
||||
})
|
||||
.expect("store identity");
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-terminal-detection = { workspace = true }
|
||||
codex-utils-template = { workspace = true }
|
||||
crypto_box = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
os_info = { workspace = true }
|
||||
|
||||
@@ -1,834 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::SecondsFormat;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use crypto_box::SecretKey as Curve25519SecretKey;
|
||||
use ed25519_dalek::Signer as _;
|
||||
use ed25519_dalek::SigningKey;
|
||||
use ed25519_dalek::VerifyingKey;
|
||||
use ed25519_dalek::pkcs8::DecodePrivateKey;
|
||||
use ed25519_dalek::pkcs8::EncodePrivateKey;
|
||||
use rand::TryRngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sha2::Digest as _;
|
||||
use sha2::Sha512;
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing::debug;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::AgentIdentityAuthRecord;
|
||||
use crate::AuthManager;
|
||||
use crate::CodexAuth;
|
||||
use crate::default_client::create_client;
|
||||
|
||||
const AGENT_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const AGENT_TASK_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const AGENT_IDENTITY_BISCUIT_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BackgroundAgentTaskManager {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
chatgpt_base_url: String,
|
||||
auth_mode: BackgroundAgentTaskAuthMode,
|
||||
abom: AgentBillOfMaterials,
|
||||
ensure_lock: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BackgroundAgentTaskManager {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BackgroundAgentTaskManager")
|
||||
.field("chatgpt_base_url", &self.chatgpt_base_url)
|
||||
.field("auth_mode", &self.auth_mode)
|
||||
.field("abom", &self.abom)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum BackgroundAgentTaskAuthMode {
|
||||
Enabled,
|
||||
#[default]
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl BackgroundAgentTaskAuthMode {
|
||||
pub fn from_feature_enabled(enabled: bool) -> Self {
|
||||
if enabled {
|
||||
Self::Enabled
|
||||
} else {
|
||||
Self::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
fn is_enabled(self) -> bool {
|
||||
matches!(self, Self::Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct StoredAgentIdentity {
|
||||
binding_id: String,
|
||||
chatgpt_account_id: String,
|
||||
chatgpt_user_id: Option<String>,
|
||||
agent_runtime_id: String,
|
||||
private_key_pkcs8_base64: String,
|
||||
public_key_ssh: String,
|
||||
registered_at: String,
|
||||
background_task_id: Option<String>,
|
||||
abom: AgentBillOfMaterials,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct AgentBillOfMaterials {
|
||||
agent_version: String,
|
||||
agent_harness_id: String,
|
||||
running_location: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RegisterAgentRequest {
|
||||
abom: AgentBillOfMaterials,
|
||||
agent_public_key: String,
|
||||
capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RegisterAgentResponse {
|
||||
agent_runtime_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RegisterTaskRequest {
|
||||
signature: String,
|
||||
timestamp: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RegisterTaskResponse {
|
||||
encrypted_task_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct AgentIdentityBinding {
|
||||
binding_id: String,
|
||||
chatgpt_account_id: String,
|
||||
chatgpt_user_id: Option<String>,
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
struct GeneratedAgentKeyMaterial {
|
||||
private_key_pkcs8_base64: String,
|
||||
public_key_ssh: String,
|
||||
}
|
||||
|
||||
impl BackgroundAgentTaskManager {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(
|
||||
auth_manager: Arc<AuthManager>,
|
||||
chatgpt_base_url: String,
|
||||
session_source: SessionSource,
|
||||
) -> Self {
|
||||
Self::new_with_auth_mode(
|
||||
auth_manager,
|
||||
chatgpt_base_url,
|
||||
session_source,
|
||||
BackgroundAgentTaskAuthMode::Disabled,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_auth_mode(
|
||||
auth_manager: Arc<AuthManager>,
|
||||
chatgpt_base_url: String,
|
||||
session_source: SessionSource,
|
||||
auth_mode: BackgroundAgentTaskAuthMode,
|
||||
) -> Self {
|
||||
Self {
|
||||
auth_manager,
|
||||
chatgpt_base_url: normalize_chatgpt_base_url(&chatgpt_base_url),
|
||||
auth_mode,
|
||||
abom: build_abom(session_source),
|
||||
ensure_lock: Arc::new(Semaphore::new(/*permits*/ 1)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn authorization_header_value_for_auth(
|
||||
&self,
|
||||
auth: &CodexAuth,
|
||||
) -> Result<Option<String>> {
|
||||
if !self.auth_mode.is_enabled() {
|
||||
debug!("skipping background agent task auth because agent identity is disabled");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !supports_background_agent_task_auth(&self.chatgpt_base_url) {
|
||||
debug!(
|
||||
chatgpt_base_url = %self.chatgpt_base_url,
|
||||
"skipping background agent task auth for unsupported backend host"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(binding) =
|
||||
AgentIdentityBinding::from_auth(auth, self.auth_manager.forced_chatgpt_workspace_id())
|
||||
else {
|
||||
debug!("skipping background agent task auth because ChatGPT auth is unavailable");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let _guard = self
|
||||
.ensure_lock
|
||||
.acquire()
|
||||
.await
|
||||
.context("background agent task ensure semaphore closed")?;
|
||||
let mut stored_identity = self
|
||||
.ensure_registered_identity_for_binding(auth, &binding)
|
||||
.await?;
|
||||
let background_task_id = match stored_identity.background_task_id.clone() {
|
||||
Some(background_task_id) => background_task_id,
|
||||
_ => {
|
||||
let background_task_id = self
|
||||
.register_background_task_for_identity(&binding, &stored_identity)
|
||||
.await?;
|
||||
stored_identity.background_task_id = Some(background_task_id.clone());
|
||||
self.store_identity(auth, &stored_identity)?;
|
||||
background_task_id
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(authorization_header_for_task(
|
||||
&stored_identity,
|
||||
&background_task_id,
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) async fn authorization_header_value_or_bearer(
|
||||
&self,
|
||||
auth: &CodexAuth,
|
||||
) -> Option<String> {
|
||||
match self.authorization_header_value_for_auth(auth).await {
|
||||
Ok(Some(authorization_header_value)) => Some(authorization_header_value),
|
||||
Ok(None) => auth
|
||||
.get_token()
|
||||
.ok()
|
||||
.filter(|token| !token.is_empty())
|
||||
.map(|token| format!("Bearer {token}")),
|
||||
Err(error) => {
|
||||
warn!(
|
||||
error = %error,
|
||||
"falling back to bearer authorization because background agent task auth failed"
|
||||
);
|
||||
auth.get_token()
|
||||
.ok()
|
||||
.filter(|token| !token.is_empty())
|
||||
.map(|token| format!("Bearer {token}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_registered_identity_for_binding(
|
||||
&self,
|
||||
auth: &CodexAuth,
|
||||
binding: &AgentIdentityBinding,
|
||||
) -> Result<StoredAgentIdentity> {
|
||||
if let Some(stored_identity) = self.load_stored_identity(auth, binding)? {
|
||||
return Ok(stored_identity);
|
||||
}
|
||||
|
||||
let stored_identity = self.register_agent_identity(binding).await?;
|
||||
self.store_identity(auth, &stored_identity)?;
|
||||
Ok(stored_identity)
|
||||
}
|
||||
|
||||
async fn register_agent_identity(
|
||||
&self,
|
||||
binding: &AgentIdentityBinding,
|
||||
) -> Result<StoredAgentIdentity> {
|
||||
let key_material = generate_agent_key_material()?;
|
||||
let request_body = RegisterAgentRequest {
|
||||
abom: self.abom.clone(),
|
||||
agent_public_key: key_material.public_key_ssh.clone(),
|
||||
capabilities: Vec::new(),
|
||||
};
|
||||
|
||||
let url = agent_registration_url(&self.chatgpt_base_url);
|
||||
let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?;
|
||||
let client = create_client();
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("X-OpenAI-Authorization", human_biscuit)
|
||||
.json(&request_body)
|
||||
.timeout(AGENT_REGISTRATION_TIMEOUT)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to send agent identity registration request to {url}")
|
||||
})?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let response_body = response
|
||||
.json::<RegisterAgentResponse>()
|
||||
.await
|
||||
.with_context(|| format!("failed to parse agent identity response from {url}"))?;
|
||||
let stored_identity = StoredAgentIdentity {
|
||||
binding_id: binding.binding_id.clone(),
|
||||
chatgpt_account_id: binding.chatgpt_account_id.clone(),
|
||||
chatgpt_user_id: binding.chatgpt_user_id.clone(),
|
||||
agent_runtime_id: response_body.agent_runtime_id,
|
||||
private_key_pkcs8_base64: key_material.private_key_pkcs8_base64,
|
||||
public_key_ssh: key_material.public_key_ssh,
|
||||
registered_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
background_task_id: None,
|
||||
abom: self.abom.clone(),
|
||||
};
|
||||
info!(
|
||||
agent_runtime_id = %stored_identity.agent_runtime_id,
|
||||
binding_id = %binding.binding_id,
|
||||
"registered background agent identity"
|
||||
);
|
||||
return Ok(stored_identity);
|
||||
}
|
||||
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("agent identity registration failed with status {status} from {url}: {body}")
|
||||
}
|
||||
|
||||
async fn register_background_task_for_identity(
|
||||
&self,
|
||||
binding: &AgentIdentityBinding,
|
||||
stored_identity: &StoredAgentIdentity,
|
||||
) -> Result<String> {
|
||||
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
let request_body = RegisterTaskRequest {
|
||||
signature: sign_task_registration_payload(stored_identity, ×tamp)?,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
let client = create_client();
|
||||
let url =
|
||||
agent_task_registration_url(&self.chatgpt_base_url, &stored_identity.agent_runtime_id);
|
||||
let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?;
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("X-OpenAI-Authorization", human_biscuit)
|
||||
.json(&request_body)
|
||||
.timeout(AGENT_TASK_REGISTRATION_TIMEOUT)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("failed to send background agent task request to {url}"))?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let response_body = response
|
||||
.json::<RegisterTaskResponse>()
|
||||
.await
|
||||
.with_context(|| format!("failed to parse background task response from {url}"))?;
|
||||
let background_task_id =
|
||||
decrypt_task_id_response(stored_identity, &response_body.encrypted_task_id)?;
|
||||
info!(
|
||||
agent_runtime_id = %stored_identity.agent_runtime_id,
|
||||
task_id = %background_task_id,
|
||||
"registered background agent task"
|
||||
);
|
||||
return Ok(background_task_id);
|
||||
}
|
||||
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!(
|
||||
"background agent task registration failed with status {status} from {url}: {body}"
|
||||
)
|
||||
}
|
||||
|
||||
async fn mint_human_biscuit(
|
||||
&self,
|
||||
binding: &AgentIdentityBinding,
|
||||
target_method: &str,
|
||||
target_url: &str,
|
||||
) -> Result<String> {
|
||||
let url = agent_identity_biscuit_url(&self.chatgpt_base_url);
|
||||
let request_id = agent_identity_request_id()?;
|
||||
let client = create_client();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.bearer_auth(&binding.access_token)
|
||||
.header("X-Request-Id", request_id.clone())
|
||||
.header("X-Original-Method", target_method)
|
||||
.header("X-Original-Url", target_url)
|
||||
.timeout(AGENT_IDENTITY_BISCUIT_TIMEOUT)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("failed to send agent identity biscuit request to {url}"))?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let human_biscuit = response
|
||||
.headers()
|
||||
.get("x-openai-authorization")
|
||||
.context("agent identity biscuit response did not include x-openai-authorization")?
|
||||
.to_str()
|
||||
.context("agent identity biscuit response header was not valid UTF-8")?
|
||||
.to_string();
|
||||
info!(
|
||||
request_id = %request_id,
|
||||
"minted human biscuit for background agent task"
|
||||
);
|
||||
return Ok(human_biscuit);
|
||||
}
|
||||
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!(
|
||||
"agent identity biscuit minting failed with status {status} from {url}: {body}"
|
||||
)
|
||||
}
|
||||
|
||||
fn load_stored_identity(
|
||||
&self,
|
||||
auth: &CodexAuth,
|
||||
binding: &AgentIdentityBinding,
|
||||
) -> Result<Option<StoredAgentIdentity>> {
|
||||
let Some(record) = auth.get_agent_identity(&binding.chatgpt_account_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let stored_identity =
|
||||
match StoredAgentIdentity::from_auth_record(binding, record, self.abom.clone()) {
|
||||
Ok(stored_identity) => stored_identity,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
binding_id = %binding.binding_id,
|
||||
error = %error,
|
||||
"stored agent identity is invalid; deleting cached value"
|
||||
);
|
||||
auth.remove_agent_identity()?;
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
if !stored_identity.matches_binding(binding) {
|
||||
warn!(
|
||||
binding_id = %binding.binding_id,
|
||||
"stored agent identity binding no longer matches current auth; deleting cached value"
|
||||
);
|
||||
auth.remove_agent_identity()?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Err(error) = stored_identity.validate_key_material() {
|
||||
warn!(
|
||||
agent_runtime_id = %stored_identity.agent_runtime_id,
|
||||
binding_id = %binding.binding_id,
|
||||
error = %error,
|
||||
"stored agent identity key material is invalid; deleting cached value"
|
||||
);
|
||||
auth.remove_agent_identity()?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(stored_identity))
|
||||
}
|
||||
|
||||
fn store_identity(
|
||||
&self,
|
||||
auth: &CodexAuth,
|
||||
stored_identity: &StoredAgentIdentity,
|
||||
) -> Result<()> {
|
||||
auth.set_agent_identity(stored_identity.to_auth_record())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached_background_agent_task_authorization_header_value(
|
||||
auth: &CodexAuth,
|
||||
auth_mode: BackgroundAgentTaskAuthMode,
|
||||
) -> Result<Option<String>> {
|
||||
if !auth_mode.is_enabled() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(binding) = AgentIdentityBinding::from_auth(auth, /*forced_workspace_id*/ None) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(record) = auth.get_agent_identity(&binding.chatgpt_account_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let stored_identity =
|
||||
StoredAgentIdentity::from_auth_record(&binding, record, build_abom(SessionSource::Cli))?;
|
||||
if !stored_identity.matches_binding(&binding) {
|
||||
return Ok(None);
|
||||
}
|
||||
stored_identity.validate_key_material()?;
|
||||
let Some(background_task_id) = stored_identity.background_task_id.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
authorization_header_for_task(&stored_identity, background_task_id).map(Some)
|
||||
}
|
||||
|
||||
impl StoredAgentIdentity {
|
||||
fn from_auth_record(
|
||||
binding: &AgentIdentityBinding,
|
||||
record: AgentIdentityAuthRecord,
|
||||
abom: AgentBillOfMaterials,
|
||||
) -> Result<Self> {
|
||||
if record.workspace_id != binding.chatgpt_account_id {
|
||||
anyhow::bail!(
|
||||
"stored agent identity workspace {:?} does not match current workspace {:?}",
|
||||
record.workspace_id,
|
||||
binding.chatgpt_account_id
|
||||
);
|
||||
}
|
||||
let signing_key = signing_key_from_private_key_pkcs8_base64(&record.agent_private_key)?;
|
||||
Ok(Self {
|
||||
binding_id: binding.binding_id.clone(),
|
||||
chatgpt_account_id: binding.chatgpt_account_id.clone(),
|
||||
chatgpt_user_id: record.chatgpt_user_id,
|
||||
agent_runtime_id: record.agent_runtime_id.clone(),
|
||||
private_key_pkcs8_base64: record.agent_private_key,
|
||||
public_key_ssh: encode_ssh_ed25519_public_key(&signing_key.verifying_key()),
|
||||
registered_at: record.registered_at,
|
||||
background_task_id: record.background_task_id,
|
||||
abom,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_auth_record(&self) -> AgentIdentityAuthRecord {
|
||||
AgentIdentityAuthRecord {
|
||||
workspace_id: self.chatgpt_account_id.clone(),
|
||||
chatgpt_user_id: self.chatgpt_user_id.clone(),
|
||||
agent_runtime_id: self.agent_runtime_id.clone(),
|
||||
agent_private_key: self.private_key_pkcs8_base64.clone(),
|
||||
registered_at: self.registered_at.clone(),
|
||||
background_task_id: self.background_task_id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_binding(&self, binding: &AgentIdentityBinding) -> bool {
|
||||
binding.matches_parts(
|
||||
&self.binding_id,
|
||||
&self.chatgpt_account_id,
|
||||
self.chatgpt_user_id.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_key_material(&self) -> Result<()> {
|
||||
let signing_key = self.signing_key()?;
|
||||
let derived_public_key = encode_ssh_ed25519_public_key(&signing_key.verifying_key());
|
||||
anyhow::ensure!(
|
||||
self.public_key_ssh == derived_public_key,
|
||||
"stored public key does not match the private key"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn signing_key(&self) -> Result<SigningKey> {
|
||||
signing_key_from_private_key_pkcs8_base64(&self.private_key_pkcs8_base64)
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentIdentityBinding {
|
||||
fn matches_parts(
|
||||
&self,
|
||||
binding_id: &str,
|
||||
chatgpt_account_id: &str,
|
||||
chatgpt_user_id: Option<&str>,
|
||||
) -> bool {
|
||||
binding_id == self.binding_id
|
||||
&& chatgpt_account_id == self.chatgpt_account_id
|
||||
&& match self.chatgpt_user_id.as_deref() {
|
||||
Some(expected_user_id) => chatgpt_user_id == Some(expected_user_id),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_auth(auth: &CodexAuth, forced_workspace_id: Option<String>) -> Option<Self> {
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let token_data = auth.get_token_data().ok()?;
|
||||
let resolved_account_id =
|
||||
forced_workspace_id
|
||||
.filter(|value| !value.is_empty())
|
||||
.or(token_data
|
||||
.account_id
|
||||
.clone()
|
||||
.filter(|value| !value.is_empty()))?;
|
||||
|
||||
Some(Self {
|
||||
binding_id: format!("chatgpt-account-{resolved_account_id}"),
|
||||
chatgpt_account_id: resolved_account_id,
|
||||
chatgpt_user_id: token_data
|
||||
.id_token
|
||||
.chatgpt_user_id
|
||||
.filter(|value| !value.is_empty()),
|
||||
access_token: token_data.access_token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn authorization_header_for_task(
|
||||
stored_identity: &StoredAgentIdentity,
|
||||
background_task_id: &str,
|
||||
) -> Result<String> {
|
||||
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
let signature = sign_agent_assertion_payload(stored_identity, background_task_id, ×tamp)?;
|
||||
let payload = serde_json::to_vec(&BTreeMap::from([
|
||||
(
|
||||
"agent_runtime_id",
|
||||
stored_identity.agent_runtime_id.as_str(),
|
||||
),
|
||||
("signature", signature.as_str()),
|
||||
("task_id", background_task_id),
|
||||
("timestamp", timestamp.as_str()),
|
||||
]))
|
||||
.context("failed to serialize agent assertion envelope")?;
|
||||
Ok(format!(
|
||||
"AgentAssertion {}",
|
||||
URL_SAFE_NO_PAD.encode(payload)
|
||||
))
|
||||
}
|
||||
|
||||
fn sign_agent_assertion_payload(
|
||||
stored_identity: &StoredAgentIdentity,
|
||||
background_task_id: &str,
|
||||
timestamp: &str,
|
||||
) -> Result<String> {
|
||||
let signing_key = stored_identity.signing_key()?;
|
||||
let payload = format!(
|
||||
"{}:{background_task_id}:{timestamp}",
|
||||
stored_identity.agent_runtime_id
|
||||
);
|
||||
Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes()))
|
||||
}
|
||||
|
||||
fn sign_task_registration_payload(
|
||||
stored_identity: &StoredAgentIdentity,
|
||||
timestamp: &str,
|
||||
) -> Result<String> {
|
||||
let signing_key = stored_identity.signing_key()?;
|
||||
let payload = format!("{}:{timestamp}", stored_identity.agent_runtime_id);
|
||||
Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes()))
|
||||
}
|
||||
|
||||
fn decrypt_task_id_response(
|
||||
stored_identity: &StoredAgentIdentity,
|
||||
encrypted_task_id: &str,
|
||||
) -> Result<String> {
|
||||
let signing_key = stored_identity.signing_key()?;
|
||||
let ciphertext = BASE64_STANDARD
|
||||
.decode(encrypted_task_id)
|
||||
.context("encrypted task id is not valid base64")?;
|
||||
let plaintext = curve25519_secret_key_from_signing_key(&signing_key)
|
||||
.unseal(&ciphertext)
|
||||
.map_err(|_| anyhow::anyhow!("failed to decrypt encrypted task id"))?;
|
||||
String::from_utf8(plaintext).context("decrypted task id is not valid UTF-8")
|
||||
}
|
||||
|
||||
fn curve25519_secret_key_from_signing_key(signing_key: &SigningKey) -> Curve25519SecretKey {
|
||||
let digest = Sha512::digest(signing_key.to_bytes());
|
||||
let mut secret_key = [0u8; 32];
|
||||
secret_key.copy_from_slice(&digest[..32]);
|
||||
secret_key[0] &= 248;
|
||||
secret_key[31] &= 127;
|
||||
secret_key[31] |= 64;
|
||||
Curve25519SecretKey::from(secret_key)
|
||||
}
|
||||
|
||||
fn build_abom(session_source: SessionSource) -> AgentBillOfMaterials {
|
||||
AgentBillOfMaterials {
|
||||
agent_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
agent_harness_id: match &session_source {
|
||||
SessionSource::VSCode => "codex-app".to_string(),
|
||||
SessionSource::Cli
|
||||
| SessionSource::Exec
|
||||
| SessionSource::Mcp
|
||||
| SessionSource::Custom(_)
|
||||
| SessionSource::SubAgent(_)
|
||||
| SessionSource::Unknown => "codex-cli".to_string(),
|
||||
},
|
||||
running_location: format!("{}-{}", session_source, std::env::consts::OS),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_agent_key_material() -> Result<GeneratedAgentKeyMaterial> {
|
||||
let mut secret_key_bytes = [0u8; 32];
|
||||
OsRng
|
||||
.try_fill_bytes(&mut secret_key_bytes)
|
||||
.context("failed to generate agent identity private key bytes")?;
|
||||
let signing_key = SigningKey::from_bytes(&secret_key_bytes);
|
||||
let private_key_pkcs8 = signing_key
|
||||
.to_pkcs8_der()
|
||||
.context("failed to encode agent identity private key as PKCS#8")?;
|
||||
|
||||
Ok(GeneratedAgentKeyMaterial {
|
||||
private_key_pkcs8_base64: BASE64_STANDARD.encode(private_key_pkcs8.as_bytes()),
|
||||
public_key_ssh: encode_ssh_ed25519_public_key(&signing_key.verifying_key()),
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_ssh_ed25519_public_key(verifying_key: &VerifyingKey) -> String {
|
||||
let mut blob = Vec::with_capacity(4 + 11 + 4 + 32);
|
||||
append_ssh_string(&mut blob, b"ssh-ed25519");
|
||||
append_ssh_string(&mut blob, verifying_key.as_bytes());
|
||||
format!("ssh-ed25519 {}", BASE64_STANDARD.encode(blob))
|
||||
}
|
||||
|
||||
fn append_ssh_string(buf: &mut Vec<u8>, value: &[u8]) {
|
||||
buf.extend_from_slice(&(value.len() as u32).to_be_bytes());
|
||||
buf.extend_from_slice(value);
|
||||
}
|
||||
|
||||
fn signing_key_from_private_key_pkcs8_base64(private_key_pkcs8_base64: &str) -> Result<SigningKey> {
|
||||
let private_key = BASE64_STANDARD
|
||||
.decode(private_key_pkcs8_base64)
|
||||
.context("stored agent identity private key is not valid base64")?;
|
||||
SigningKey::from_pkcs8_der(&private_key)
|
||||
.context("stored agent identity private key is not valid PKCS#8")
|
||||
}
|
||||
|
||||
fn agent_registration_url(chatgpt_base_url: &str) -> String {
|
||||
let trimmed = chatgpt_base_url.trim_end_matches('/');
|
||||
format!("{trimmed}/v1/agent/register")
|
||||
}
|
||||
|
||||
fn agent_task_registration_url(chatgpt_base_url: &str, agent_runtime_id: &str) -> String {
|
||||
let trimmed = chatgpt_base_url.trim_end_matches('/');
|
||||
format!("{trimmed}/v1/agent/{agent_runtime_id}/task/register")
|
||||
}
|
||||
|
||||
fn agent_identity_biscuit_url(chatgpt_base_url: &str) -> String {
|
||||
let trimmed = chatgpt_base_url.trim_end_matches('/');
|
||||
format!("{trimmed}/authenticate_app_v2")
|
||||
}
|
||||
|
||||
fn agent_identity_request_id() -> Result<String> {
|
||||
let mut request_id_bytes = [0u8; 16];
|
||||
OsRng
|
||||
.try_fill_bytes(&mut request_id_bytes)
|
||||
.context("failed to generate agent identity request id")?;
|
||||
Ok(format!(
|
||||
"codex-agent-identity-{}",
|
||||
URL_SAFE_NO_PAD.encode(request_id_bytes)
|
||||
))
|
||||
}
|
||||
|
||||
fn normalize_chatgpt_base_url(chatgpt_base_url: &str) -> String {
|
||||
let mut base_url = chatgpt_base_url.trim_end_matches('/').to_string();
|
||||
for suffix in [
|
||||
"/wham/remote/control/server/enroll",
|
||||
"/wham/remote/control/server",
|
||||
] {
|
||||
if let Some(stripped) = base_url.strip_suffix(suffix) {
|
||||
base_url = stripped.to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (base_url.starts_with("https://chatgpt.com")
|
||||
|| base_url.starts_with("https://chat.openai.com"))
|
||||
&& !base_url.contains("/backend-api")
|
||||
{
|
||||
base_url = format!("{base_url}/backend-api");
|
||||
}
|
||||
if let Some(stripped) = base_url.strip_suffix("/codex") {
|
||||
stripped.to_string()
|
||||
} else {
|
||||
base_url
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_background_agent_task_auth(chatgpt_base_url: &str) -> bool {
|
||||
let Ok(url) = url::Url::parse(chatgpt_base_url) else {
|
||||
return false;
|
||||
};
|
||||
let Some(host) = url.host_str() else {
|
||||
return false;
|
||||
};
|
||||
host == "chatgpt.com"
|
||||
|| host == "chat.openai.com"
|
||||
|| host == "chatgpt-staging.com"
|
||||
|| host.ends_with(".chatgpt.com")
|
||||
|| host.ends_with(".chatgpt-staging.com")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_background_agent_task_auth_returns_none_for_supported_host() {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth.clone());
|
||||
let manager = BackgroundAgentTaskManager::new_with_auth_mode(
|
||||
auth_manager,
|
||||
"https://chatgpt.com/backend-api".to_string(),
|
||||
SessionSource::Cli,
|
||||
BackgroundAgentTaskAuthMode::Disabled,
|
||||
);
|
||||
|
||||
let authorization_header_value = manager
|
||||
.authorization_header_value_for_auth(&auth)
|
||||
.await
|
||||
.expect("disabled manager should not fail");
|
||||
|
||||
assert_eq!(None, authorization_header_value);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_background_agent_task_auth_returns_none_for_supported_host() {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth.clone());
|
||||
let manager = BackgroundAgentTaskManager::new(
|
||||
auth_manager,
|
||||
"https://chatgpt.com/backend-api".to_string(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
|
||||
let authorization_header_value = manager
|
||||
.authorization_header_value_for_auth(&auth)
|
||||
.await
|
||||
.expect("default manager should not fail");
|
||||
|
||||
assert_eq!(None, authorization_header_value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_background_agent_task_auth_honors_disabled_mode() {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let key_material = generate_agent_key_material().expect("generate key material");
|
||||
auth.set_agent_identity(AgentIdentityAuthRecord {
|
||||
workspace_id: "account_id".to_string(),
|
||||
chatgpt_user_id: None,
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: key_material.private_key_pkcs8_base64,
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: Some("task_123".to_string()),
|
||||
})
|
||||
.expect("set agent identity");
|
||||
|
||||
let disabled_authorization_header_value =
|
||||
cached_background_agent_task_authorization_header_value(
|
||||
&auth,
|
||||
BackgroundAgentTaskAuthMode::Disabled,
|
||||
)
|
||||
.expect("disabled cached auth should not fail");
|
||||
let enabled_authorization_header_value =
|
||||
cached_background_agent_task_authorization_header_value(
|
||||
&auth,
|
||||
BackgroundAgentTaskAuthMode::Enabled,
|
||||
)
|
||||
.expect("enabled cached auth should not fail");
|
||||
|
||||
assert_eq!(None, disabled_authorization_header_value);
|
||||
assert!(enabled_authorization_header_value.is_some());
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,6 @@ mod tests {
|
||||
agent_runtime_id: agent_runtime_id.to_string(),
|
||||
agent_private_key: BASE64_STANDARD.encode(private_key.as_bytes()),
|
||||
registered_at: "2026-03-23T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,6 @@ fn chatgpt_auth_persists_agent_identity_for_workspace() {
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: "pkcs8-base64".to_string(),
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
};
|
||||
|
||||
auth.set_agent_identity(record.clone())
|
||||
|
||||
@@ -15,7 +15,7 @@ use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
@@ -43,12 +43,9 @@ use codex_protocol::auth::KnownPlan as InternalKnownPlan;
|
||||
use codex_protocol::auth::PlanType as InternalPlanType;
|
||||
use codex_protocol::auth::RefreshTokenFailedError;
|
||||
use codex_protocol::auth::RefreshTokenFailedReason;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::agent_identity::BackgroundAgentTaskAuthMode;
|
||||
use crate::agent_identity::BackgroundAgentTaskManager;
|
||||
/// Authentication mechanism used by the current user.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CodexAuth {
|
||||
@@ -378,7 +375,7 @@ impl CodexAuth {
|
||||
.filter(|identity| identity.workspace_id == workspace_id)
|
||||
}
|
||||
|
||||
pub fn set_agent_identity(&self, mut record: AgentIdentityAuthRecord) -> std::io::Result<()> {
|
||||
pub fn set_agent_identity(&self, record: AgentIdentityAuthRecord) -> std::io::Result<()> {
|
||||
let (state, storage) = match self {
|
||||
Self::Chatgpt(auth) => (&auth.state, &auth.storage),
|
||||
Self::ChatgptAuthTokens(auth) => (&auth.state, &auth.storage),
|
||||
@@ -391,13 +388,6 @@ impl CodexAuth {
|
||||
let mut auth = guard
|
||||
.clone()
|
||||
.ok_or_else(|| std::io::Error::other("auth data is not available"))?;
|
||||
if record.background_task_id.is_none()
|
||||
&& let Some(existing) = auth.agent_identity.as_ref()
|
||||
&& existing.workspace_id == record.workspace_id
|
||||
&& existing.agent_runtime_id == record.agent_runtime_id
|
||||
{
|
||||
record.background_task_id = existing.background_task_id.clone();
|
||||
}
|
||||
auth.agent_identity = Some(record);
|
||||
storage.save(&auth)?;
|
||||
*guard = Some(auth);
|
||||
@@ -1198,9 +1188,7 @@ pub struct AuthManager {
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
forced_chatgpt_workspace_id: RwLock<Option<String>>,
|
||||
chatgpt_base_url: RwLock<Option<String>>,
|
||||
background_agent_task_auth_mode: RwLock<BackgroundAgentTaskAuthMode>,
|
||||
refresh_lock: Semaphore,
|
||||
refresh_lock: AsyncMutex<()>,
|
||||
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
|
||||
auth_state_tx: watch::Sender<()>,
|
||||
}
|
||||
@@ -1220,16 +1208,6 @@ pub trait AuthManagerConfig {
|
||||
|
||||
/// Returns the workspace ID that ChatGPT auth should be restricted to, if any.
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<String>;
|
||||
|
||||
/// Returns the ChatGPT backend base URL used for first-party backend authorization.
|
||||
fn chatgpt_base_url(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns whether default ChatGPT backend authorization may use background AgentAssertion.
|
||||
fn background_agent_task_auth_mode(&self) -> BackgroundAgentTaskAuthMode {
|
||||
BackgroundAgentTaskAuthMode::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AuthManager {
|
||||
@@ -1246,11 +1224,6 @@ impl Debug for AuthManager {
|
||||
"forced_chatgpt_workspace_id",
|
||||
&self.forced_chatgpt_workspace_id,
|
||||
)
|
||||
.field("chatgpt_base_url", &self.chatgpt_base_url)
|
||||
.field(
|
||||
"background_agent_task_auth_mode",
|
||||
&self.background_agent_task_auth_mode,
|
||||
)
|
||||
.field("has_external_auth", &self.has_external_auth())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
@@ -1283,9 +1256,7 @@ impl AuthManager {
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: RwLock::new(None),
|
||||
background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
refresh_lock: AsyncMutex::new(()),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_state_tx,
|
||||
}
|
||||
@@ -1305,9 +1276,7 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: RwLock::new(None),
|
||||
background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
refresh_lock: AsyncMutex::new(()),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_state_tx,
|
||||
})
|
||||
@@ -1326,9 +1295,7 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: RwLock::new(None),
|
||||
background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
refresh_lock: AsyncMutex::new(()),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_state_tx,
|
||||
})
|
||||
@@ -1345,9 +1312,7 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: RwLock::new(None),
|
||||
background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
refresh_lock: AsyncMutex::new(()),
|
||||
external_auth: RwLock::new(Some(
|
||||
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
|
||||
)),
|
||||
@@ -1565,90 +1530,6 @@ impl AuthManager {
|
||||
Ok(Some(record))
|
||||
}
|
||||
|
||||
pub fn set_chatgpt_backend_auth_config(
|
||||
&self,
|
||||
chatgpt_base_url: Option<String>,
|
||||
background_agent_task_auth_mode: BackgroundAgentTaskAuthMode,
|
||||
) {
|
||||
let mut changed = false;
|
||||
if let Ok(mut guard) = self.chatgpt_base_url.write()
|
||||
&& *guard != chatgpt_base_url
|
||||
{
|
||||
*guard = chatgpt_base_url;
|
||||
changed = true;
|
||||
}
|
||||
if let Ok(mut guard) = self.background_agent_task_auth_mode.write()
|
||||
&& *guard != background_agent_task_auth_mode
|
||||
{
|
||||
*guard = background_agent_task_auth_mode;
|
||||
changed = true;
|
||||
}
|
||||
if changed {
|
||||
self.auth_state_tx.send_replace(());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_chatgpt_backend_base_url(&self, chatgpt_base_url: Option<String>) {
|
||||
let (_, auth_mode) = self.chatgpt_backend_auth_config();
|
||||
self.set_chatgpt_backend_auth_config(chatgpt_base_url, auth_mode);
|
||||
}
|
||||
|
||||
fn chatgpt_backend_auth_config(&self) -> (Option<String>, BackgroundAgentTaskAuthMode) {
|
||||
let chatgpt_base_url = self
|
||||
.chatgpt_base_url
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|guard| guard.clone());
|
||||
let auth_mode = self
|
||||
.background_agent_task_auth_mode
|
||||
.read()
|
||||
.ok()
|
||||
.map(|guard| *guard)
|
||||
.unwrap_or_default();
|
||||
(chatgpt_base_url, auth_mode)
|
||||
}
|
||||
|
||||
/// Returns the default authorization header for ChatGPT backend requests.
|
||||
///
|
||||
/// This uses background AgentAssertion when configured and available, otherwise it falls back
|
||||
/// to the ChatGPT bearer token. Low-level bootstrap calls that must never use AgentAssertion
|
||||
/// should use [`Self::chatgpt_bearer_authorization_header_for_auth`] instead.
|
||||
pub async fn chatgpt_authorization_header(self: &Arc<Self>) -> Option<String> {
|
||||
let auth = self.auth().await?;
|
||||
self.chatgpt_authorization_header_for_auth(&auth).await
|
||||
}
|
||||
|
||||
pub async fn chatgpt_authorization_header_for_auth(
|
||||
self: &Arc<Self>,
|
||||
auth: &CodexAuth,
|
||||
) -> Option<String> {
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (chatgpt_base_url, auth_mode) = self.chatgpt_backend_auth_config();
|
||||
let Some(chatgpt_base_url) = chatgpt_base_url else {
|
||||
return Self::chatgpt_bearer_authorization_header_for_auth(auth);
|
||||
};
|
||||
|
||||
BackgroundAgentTaskManager::new_with_auth_mode(
|
||||
Arc::clone(self),
|
||||
chatgpt_base_url,
|
||||
SessionSource::Cli,
|
||||
auth_mode,
|
||||
)
|
||||
.authorization_header_value_or_bearer(auth)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn chatgpt_bearer_token_for_auth(auth: &CodexAuth) -> Option<String> {
|
||||
auth.get_token().ok().filter(|token| !token.is_empty())
|
||||
}
|
||||
|
||||
pub fn chatgpt_bearer_authorization_header_for_auth(auth: &CodexAuth) -> Option<String> {
|
||||
Self::chatgpt_bearer_token_for_auth(auth).map(|token| format!("Bearer {token}"))
|
||||
}
|
||||
|
||||
pub fn subscribe_auth_state(&self) -> watch::Receiver<()> {
|
||||
self.auth_state_tx.subscribe()
|
||||
}
|
||||
@@ -1691,10 +1572,6 @@ impl AuthManager {
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
);
|
||||
auth_manager.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id());
|
||||
auth_manager.set_chatgpt_backend_auth_config(
|
||||
config.chatgpt_base_url(),
|
||||
config.background_agent_task_auth_mode(),
|
||||
);
|
||||
auth_manager
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,6 @@ pub struct AgentIdentityAuthRecord {
|
||||
pub agent_runtime_id: String,
|
||||
pub agent_private_key: String,
|
||||
pub registered_at: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub background_task_id: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) fn get_auth_file(codex_home: &Path) -> PathBuf {
|
||||
|
||||
@@ -69,7 +69,6 @@ async fn file_storage_persists_agent_identity() -> anyhow::Result<()> {
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: "pkcs8-base64".to_string(),
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod agent_identity;
|
||||
pub mod auth;
|
||||
pub mod auth_env_telemetry;
|
||||
pub mod token_data;
|
||||
@@ -18,8 +17,6 @@ pub use server::ServerOptions;
|
||||
pub use server::ShutdownHandle;
|
||||
pub use server::run_login_server;
|
||||
|
||||
pub use agent_identity::BackgroundAgentTaskAuthMode;
|
||||
pub use agent_identity::cached_background_agent_task_authorization_header_value;
|
||||
pub use auth::AgentIdentityAuthRecord;
|
||||
pub use auth::AgentTaskAuthorizationTarget;
|
||||
pub use auth::AuthConfig;
|
||||
|
||||
@@ -17,7 +17,6 @@ use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::collect_auth_env_telemetry;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_model_provider::AuthorizationHeaderAuthProvider;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
@@ -454,23 +453,7 @@ impl ModelsManager {
|
||||
let auth = self.provider.auth().await;
|
||||
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);
|
||||
let api_provider = self.provider.api_provider().await?;
|
||||
let mut api_auth = self.provider.api_auth().await?;
|
||||
if let Some(auth_manager) = auth_manager.as_ref()
|
||||
&& let Some(auth) = auth.as_ref().filter(|auth| auth.is_chatgpt_auth())
|
||||
&& provider_uses_codex_login_auth(self.provider.info())
|
||||
&& let Some(authorization_header_value) = auth_manager
|
||||
.chatgpt_authorization_header_for_auth(auth)
|
||||
.await
|
||||
{
|
||||
let mut auth_provider = AuthorizationHeaderAuthProvider::new(
|
||||
Some(authorization_header_value),
|
||||
auth.get_account_id(),
|
||||
);
|
||||
if auth.is_fedramp_account() {
|
||||
auth_provider = auth_provider.with_fedramp_routing_header();
|
||||
}
|
||||
api_auth = Arc::new(auth_provider);
|
||||
}
|
||||
let api_auth = self.provider.api_auth().await?;
|
||||
let auth_env = collect_auth_env_telemetry(self.provider.info(), codex_api_key_env_enabled);
|
||||
let transport = ReqwestTransport::new(build_reqwest_client());
|
||||
let auth_telemetry = auth_header_telemetry(api_auth.as_ref());
|
||||
@@ -618,10 +601,6 @@ impl ModelsManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_uses_codex_login_auth(provider: &ModelProviderInfo) -> bool {
|
||||
provider.env_key.is_none() && provider.experimental_bearer_token.is_none()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "manager_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user