mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Summary Introduces a single background/control-plane agent task for ChatGPT backend requests that do not have a thread-scoped task, with `AuthManager` owning the default ChatGPT backend authorization decision. Callers now ask `AuthManager` for the default ChatGPT backend authorization header. `AuthManager` decides whether that is bearer or background AgentAssertion based on config/internal state, while low-level bootstrap paths can explicitly request bearer-only auth. This PR is stacked on PR4 and focuses on the shared background task auth plumbing plus the first tranche of backend/control-plane consumers. The remaining callsite wiring is split into PR4.2 to keep review size down. ## Stack - PR1: https://github.com/openai/codex/pull/17385 - add `features.use_agent_identity` - PR2: https://github.com/openai/codex/pull/17386 - register agent identities when enabled - PR3: https://github.com/openai/codex/pull/17387 - register agent tasks when enabled - PR3.1: https://github.com/openai/codex/pull/17978 - persist and prewarm registered tasks per thread - PR4: https://github.com/openai/codex/pull/17980 - use task-scoped `AgentAssertion` for downstream calls - PR4.1: this PR - introduce AuthManager-owned background/control-plane `AgentAssertion` auth - PR4.2: https://github.com/openai/codex/pull/18260 - use background task auth for additional backend/control-plane calls ## What Changed - add background task registration and assertion minting inside `codex-login` - persist `agent_identity.background_task_id` separately from per-session task state - make `BackgroundAgentTaskManager` private to `codex-login`; call sites do not instantiate or pass it around - teach `AuthManager` the ChatGPT backend base URL and feature-derived background auth mode from resolved config - expose bearer-only helpers for bootstrap/registration/refresh-style paths that must not use AgentAssertion - wire `AuthManager` default ChatGPT authorization through app listing, connector directory listing, remote plugins, MCP status/listing, analytics, and core-skills remote calls - preserve bearer fallback when the feature is disabled, the backend host is unsupported, or background task registration is not available ## Validation - `just fmt` - `cargo check -p codex-core -p codex-login -p codex-analytics -p codex-app-server -p codex-cloud-requirements -p codex-cloud-tasks -p codex-models-manager -p codex-chatgpt -p codex-model-provider -p codex-mcp -p codex-core-skills` - `cargo test -p codex-login agent_identity` - `cargo test -p codex-model-provider bearer_auth_provider` - `cargo test -p codex-core agent_assertion` - `cargo test -p codex-app-server remote_control` - `cargo test -p codex-cloud-requirements fetch_cloud_requirements` - `cargo test -p codex-models-manager manager::tests` - `cargo test -p codex-chatgpt` - `cargo test -p codex-cloud-tasks` - `just fix -p codex-core -p codex-login -p codex-analytics -p codex-app-server -p codex-cloud-requirements -p codex-cloud-tasks -p codex-models-manager -p codex-chatgpt -p codex-model-provider -p codex-mcp -p codex-core-skills` - `just fix -p codex-app-server` - `git diff --check`
305 lines
9.6 KiB
Rust
305 lines
9.6 KiB
Rust
use super::*;
|
|
|
|
impl Session {
|
|
pub async fn request_mcp_server_elicitation(
|
|
&self,
|
|
turn_context: &TurnContext,
|
|
request_id: RequestId,
|
|
params: McpServerElicitationRequestParams,
|
|
) -> Option<ElicitationResponse> {
|
|
let server_name = params.server_name.clone();
|
|
let request = match params.request {
|
|
McpServerElicitationRequest::Form {
|
|
meta,
|
|
message,
|
|
requested_schema,
|
|
} => {
|
|
let requested_schema = match serde_json::to_value(requested_schema) {
|
|
Ok(requested_schema) => requested_schema,
|
|
Err(err) => {
|
|
warn!(
|
|
"failed to serialize MCP elicitation schema for server_name: {server_name}, request_id: {request_id}: {err:#}"
|
|
);
|
|
return None;
|
|
}
|
|
};
|
|
codex_protocol::approvals::ElicitationRequest::Form {
|
|
meta,
|
|
message,
|
|
requested_schema,
|
|
}
|
|
}
|
|
McpServerElicitationRequest::Url {
|
|
meta,
|
|
message,
|
|
url,
|
|
elicitation_id,
|
|
} => codex_protocol::approvals::ElicitationRequest::Url {
|
|
meta,
|
|
message,
|
|
url,
|
|
elicitation_id,
|
|
},
|
|
};
|
|
|
|
let (tx_response, rx_response) = oneshot::channel();
|
|
let prev_entry = {
|
|
let mut active = self.active_turn.lock().await;
|
|
match active.as_mut() {
|
|
Some(at) => {
|
|
let mut ts = at.turn_state.lock().await;
|
|
ts.insert_pending_elicitation(
|
|
server_name.clone(),
|
|
request_id.clone(),
|
|
tx_response,
|
|
)
|
|
}
|
|
None => None,
|
|
}
|
|
};
|
|
if prev_entry.is_some() {
|
|
warn!(
|
|
"Overwriting existing pending elicitation for server_name: {server_name}, request_id: {request_id}"
|
|
);
|
|
}
|
|
let id = match request_id {
|
|
rmcp::model::NumberOrString::String(value) => {
|
|
codex_protocol::mcp::RequestId::String(value.to_string())
|
|
}
|
|
rmcp::model::NumberOrString::Number(value) => {
|
|
codex_protocol::mcp::RequestId::Integer(value)
|
|
}
|
|
};
|
|
let event = EventMsg::ElicitationRequest(ElicitationRequestEvent {
|
|
turn_id: params.turn_id,
|
|
server_name,
|
|
id,
|
|
request,
|
|
});
|
|
self.send_event(turn_context, event).await;
|
|
rx_response.await.ok()
|
|
}
|
|
|
|
pub async fn resolve_elicitation(
|
|
&self,
|
|
server_name: String,
|
|
id: RequestId,
|
|
response: ElicitationResponse,
|
|
) -> anyhow::Result<()> {
|
|
let entry = {
|
|
let mut active = self.active_turn.lock().await;
|
|
match active.as_mut() {
|
|
Some(at) => {
|
|
let mut ts = at.turn_state.lock().await;
|
|
ts.remove_pending_elicitation(&server_name, &id)
|
|
}
|
|
None => None,
|
|
}
|
|
};
|
|
if let Some(tx_response) = entry {
|
|
tx_response
|
|
.send(response)
|
|
.map_err(|e| anyhow::anyhow!("failed to send elicitation response: {e:?}"))?;
|
|
return Ok(());
|
|
}
|
|
|
|
self.services
|
|
.mcp_connection_manager
|
|
.read()
|
|
.await
|
|
.resolve_elicitation(server_name, id, response)
|
|
.await
|
|
}
|
|
|
|
pub async fn list_resources(
|
|
&self,
|
|
server: &str,
|
|
params: Option<PaginatedRequestParams>,
|
|
) -> anyhow::Result<ListResourcesResult> {
|
|
self.services
|
|
.mcp_connection_manager
|
|
.read()
|
|
.await
|
|
.list_resources(server, params)
|
|
.await
|
|
}
|
|
|
|
pub async fn list_resource_templates(
|
|
&self,
|
|
server: &str,
|
|
params: Option<PaginatedRequestParams>,
|
|
) -> anyhow::Result<ListResourceTemplatesResult> {
|
|
self.services
|
|
.mcp_connection_manager
|
|
.read()
|
|
.await
|
|
.list_resource_templates(server, params)
|
|
.await
|
|
}
|
|
|
|
pub async fn read_resource(
|
|
&self,
|
|
server: &str,
|
|
params: ReadResourceRequestParams,
|
|
) -> anyhow::Result<ReadResourceResult> {
|
|
self.services
|
|
.mcp_connection_manager
|
|
.read()
|
|
.await
|
|
.read_resource(server, params)
|
|
.await
|
|
}
|
|
|
|
pub async fn call_tool(
|
|
&self,
|
|
server: &str,
|
|
tool: &str,
|
|
arguments: Option<serde_json::Value>,
|
|
meta: Option<serde_json::Value>,
|
|
) -> anyhow::Result<CallToolResult> {
|
|
self.services
|
|
.mcp_connection_manager
|
|
.read()
|
|
.await
|
|
.call_tool(server, tool, arguments, meta)
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn resolve_mcp_tool_info(&self, tool_name: &ToolName) -> Option<ToolInfo> {
|
|
self.services
|
|
.mcp_connection_manager
|
|
.read()
|
|
.await
|
|
.resolve_tool_info(tool_name)
|
|
.await
|
|
}
|
|
|
|
async fn refresh_mcp_servers_inner(
|
|
&self,
|
|
turn_context: &TurnContext,
|
|
mcp_servers: HashMap<String, McpServerConfig>,
|
|
store_mode: OAuthCredentialsStoreMode,
|
|
) {
|
|
let auth = self.services.auth_manager.auth().await;
|
|
let config = self.get_config().await;
|
|
let mcp_config = config
|
|
.to_mcp_config(self.services.plugins_manager.as_ref())
|
|
.await;
|
|
let tool_plugin_provenance = self
|
|
.services
|
|
.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 auth_statuses = compute_auth_statuses(mcp_servers.iter(), store_mode).await;
|
|
{
|
|
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
|
|
guard.cancel();
|
|
*guard = CancellationToken::new();
|
|
}
|
|
let (refreshed_manager, cancel_token) = McpConnectionManager::new(
|
|
&mcp_servers,
|
|
store_mode,
|
|
auth_statuses,
|
|
&turn_context.config.permissions.approval_policy,
|
|
turn_context.sub_id.clone(),
|
|
self.get_tx_event(),
|
|
turn_context.sandbox_policy.get().clone(),
|
|
McpRuntimeEnvironment::new(
|
|
turn_context
|
|
.environment
|
|
.clone()
|
|
.unwrap_or_else(|| Arc::new(Environment::default())),
|
|
turn_context.cwd.to_path_buf(),
|
|
),
|
|
config.codex_home.to_path_buf(),
|
|
codex_apps_tools_cache_key(auth.as_ref()),
|
|
tool_plugin_provenance,
|
|
)
|
|
.await;
|
|
{
|
|
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
|
|
if guard.is_cancelled() {
|
|
cancel_token.cancel();
|
|
}
|
|
*guard = cancel_token;
|
|
}
|
|
|
|
let mut manager = self.services.mcp_connection_manager.write().await;
|
|
*manager = refreshed_manager;
|
|
}
|
|
|
|
pub(crate) async fn refresh_mcp_servers_if_requested(&self, turn_context: &TurnContext) {
|
|
let refresh_config = { self.pending_mcp_server_refresh_config.lock().await.take() };
|
|
let Some(refresh_config) = refresh_config else {
|
|
return;
|
|
};
|
|
|
|
let McpServerRefreshConfig {
|
|
mcp_servers,
|
|
mcp_oauth_credentials_store_mode,
|
|
} = refresh_config;
|
|
|
|
let mcp_servers =
|
|
match serde_json::from_value::<HashMap<String, McpServerConfig>>(mcp_servers) {
|
|
Ok(servers) => servers,
|
|
Err(err) => {
|
|
warn!("failed to parse MCP server refresh config: {err}");
|
|
return;
|
|
}
|
|
};
|
|
let store_mode = match serde_json::from_value::<OAuthCredentialsStoreMode>(
|
|
mcp_oauth_credentials_store_mode,
|
|
) {
|
|
Ok(mode) => mode,
|
|
Err(err) => {
|
|
warn!("failed to parse MCP OAuth refresh config: {err}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
self.refresh_mcp_servers_inner(turn_context, mcp_servers, store_mode)
|
|
.await;
|
|
}
|
|
|
|
pub(crate) async fn refresh_mcp_servers_now(
|
|
&self,
|
|
turn_context: &TurnContext,
|
|
mcp_servers: HashMap<String, McpServerConfig>,
|
|
store_mode: OAuthCredentialsStoreMode,
|
|
) {
|
|
self.refresh_mcp_servers_inner(turn_context, mcp_servers, store_mode)
|
|
.await;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) async fn mcp_startup_cancellation_token(&self) -> CancellationToken {
|
|
self.services
|
|
.mcp_startup_cancellation_token
|
|
.lock()
|
|
.await
|
|
.clone()
|
|
}
|
|
|
|
pub(crate) async fn cancel_mcp_startup(&self) {
|
|
self.services
|
|
.mcp_startup_cancellation_token
|
|
.lock()
|
|
.await
|
|
.cancel();
|
|
}
|
|
}
|