mirror of
https://github.com/openai/codex.git
synced 2026-05-07 21:06:39 +00:00
Compare commits
12 Commits
fcoury/wor
...
dev/mzeng/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1498d5e8d | ||
|
|
aba93e61f4 | ||
|
|
3c0c775143 | ||
|
|
0fcebd8d8f | ||
|
|
0bb1719363 | ||
|
|
b22bcab083 | ||
|
|
5a9d9fdf86 | ||
|
|
24c953304d | ||
|
|
a67d0dc430 | ||
|
|
d2723793b2 | ||
|
|
2c7611434b | ||
|
|
6b3013938c |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -2986,6 +2986,7 @@ dependencies = [
|
||||
"async-channel",
|
||||
"codex-api",
|
||||
"codex-async-utils",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
"codex-login",
|
||||
|
||||
@@ -303,6 +303,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -16,6 +16,7 @@ anyhow = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
codex-async-utils = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
|
||||
@@ -9,10 +9,10 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::tools::MCP_TOOLS_CACHE_WRITE_DURATION_METRIC;
|
||||
use crate::tools::ToolInfo;
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_utils_plugins::mcp_connector::is_connector_id_allowed;
|
||||
use codex_utils_plugins::mcp_connector::sanitize_name;
|
||||
@@ -43,7 +43,7 @@ pub fn filter_non_codex_apps_mcp_tools_only(
|
||||
) -> HashMap<String, ToolInfo> {
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| tool.server_name != CODEX_APPS_MCP_SERVER_NAME)
|
||||
.filter(|(_, tool)| !tool.is_host_owned_codex_apps())
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect()
|
||||
}
|
||||
@@ -71,11 +71,11 @@ pub(crate) enum CachedCodexAppsToolsLoad {
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_codex_apps_tool_title(
|
||||
server_name: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
connector_name: Option<&str>,
|
||||
value: &str,
|
||||
) -> String {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
if !is_host_owned_codex_apps_server {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
@@ -97,12 +97,12 @@ pub(crate) fn normalize_codex_apps_tool_title(
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_codex_apps_callable_name(
|
||||
server_name: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
tool_name: &str,
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
) -> String {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
if !is_host_owned_codex_apps_server {
|
||||
return tool_name.to_string();
|
||||
}
|
||||
|
||||
@@ -133,11 +133,10 @@ pub(crate) fn normalize_codex_apps_callable_name(
|
||||
|
||||
pub(crate) fn normalize_codex_apps_callable_namespace(
|
||||
server_name: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
connector_name: Option<&str>,
|
||||
) -> String {
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
&& let Some(connector_name) = connector_name
|
||||
{
|
||||
if is_host_owned_codex_apps_server && let Some(connector_name) = connector_name {
|
||||
format!("mcp__{}__{}", server_name, sanitize_name(connector_name))
|
||||
} else {
|
||||
format!("mcp__{server_name}__")
|
||||
@@ -145,11 +144,11 @@ pub(crate) fn normalize_codex_apps_callable_namespace(
|
||||
}
|
||||
|
||||
pub(crate) fn write_cached_codex_apps_tools_if_needed(
|
||||
server_name: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
tools: &[ToolInfo],
|
||||
) {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
if !is_host_owned_codex_apps_server {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,10 +164,10 @@ pub(crate) fn write_cached_codex_apps_tools_if_needed(
|
||||
}
|
||||
|
||||
pub(crate) fn load_startup_cached_codex_apps_tools_snapshot(
|
||||
server_name: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
) -> Option<Vec<ToolInfo>> {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
if !is_host_owned_codex_apps_server {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -208,7 +207,11 @@ pub(crate) fn load_cached_codex_apps_tools(
|
||||
if cache.schema_version != CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION {
|
||||
return CachedCodexAppsToolsLoad::Invalid;
|
||||
}
|
||||
CachedCodexAppsToolsLoad::Hit(filter_disallowed_codex_apps_tools(cache.tools))
|
||||
let mut tools = filter_disallowed_codex_apps_tools(cache.tools);
|
||||
for tool in &mut tools {
|
||||
tool.server_provenance = McpServerProvenance::HostOwnedCodexApps;
|
||||
}
|
||||
CachedCodexAppsToolsLoad::Hit(tools)
|
||||
}
|
||||
|
||||
pub(crate) fn write_cached_codex_apps_tools(
|
||||
|
||||
@@ -17,7 +17,6 @@ use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::CodexAppsToolsCacheKey;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
use crate::rmcp_client::AsyncManagedClient;
|
||||
use crate::rmcp_client::DEFAULT_STARTUP_TIMEOUT;
|
||||
@@ -38,6 +37,7 @@ use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -116,6 +116,12 @@ impl McpConnectionManager {
|
||||
self.server_origins.get(server_name).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {
|
||||
self.clients
|
||||
.get(server_name)
|
||||
.is_some_and(|client| client.provenance == McpServerProvenance::HostOwnedCodexApps)
|
||||
}
|
||||
|
||||
pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {
|
||||
if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() {
|
||||
*policy = approval_policy.value();
|
||||
@@ -156,6 +162,9 @@ impl McpConnectionManager {
|
||||
.map(codex_model_provider::auth_provider_from_auth);
|
||||
let mcp_servers = mcp_servers.clone();
|
||||
for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) {
|
||||
let provenance = cfg.provenance;
|
||||
let is_host_owned_codex_apps_server =
|
||||
provenance == McpServerProvenance::HostOwnedCodexApps;
|
||||
if let Some(origin) = transport_origin(&cfg.transport) {
|
||||
server_origins.insert(server_name.clone(), origin);
|
||||
}
|
||||
@@ -169,7 +178,7 @@ impl McpConnectionManager {
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let codex_apps_tools_cache_context = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let codex_apps_tools_cache_context = if is_host_owned_codex_apps_server {
|
||||
Some(CodexAppsToolsCacheContext {
|
||||
codex_home: codex_home.clone(),
|
||||
user_key: codex_apps_tools_cache_key.clone(),
|
||||
@@ -184,12 +193,12 @@ impl McpConnectionManager {
|
||||
} => bearer_token_env_var.is_some(),
|
||||
McpServerTransportConfig::Stdio { .. } => false,
|
||||
};
|
||||
let runtime_auth_provider =
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME && !uses_env_bearer_token {
|
||||
codex_apps_auth_provider.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let runtime_auth_provider = if is_host_owned_codex_apps_server && !uses_env_bearer_token
|
||||
{
|
||||
codex_apps_auth_provider.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let async_managed_client = AsyncManagedClient::new(
|
||||
server_name.clone(),
|
||||
cfg,
|
||||
@@ -201,6 +210,7 @@ impl McpConnectionManager {
|
||||
Arc::clone(&tool_plugin_provenance),
|
||||
runtime_environment.clone(),
|
||||
runtime_auth_provider,
|
||||
provenance,
|
||||
);
|
||||
clients.insert(server_name.clone(), async_managed_client.clone());
|
||||
let tx_event = tx_event.clone();
|
||||
@@ -335,10 +345,12 @@ impl McpConnectionManager {
|
||||
/// latest filtered tool map is returned directly to the caller. On
|
||||
/// failure, the existing cache remains unchanged.
|
||||
pub async fn hard_refresh_codex_apps_tools_cache(&self) -> Result<HashMap<String, ToolInfo>> {
|
||||
let managed_client = self
|
||||
let (server_name, async_managed_client) = self
|
||||
.clients
|
||||
.get(CODEX_APPS_MCP_SERVER_NAME)
|
||||
.ok_or_else(|| anyhow!("unknown MCP server '{CODEX_APPS_MCP_SERVER_NAME}'"))?
|
||||
.iter()
|
||||
.find(|(_, client)| client.provenance == McpServerProvenance::HostOwnedCodexApps)
|
||||
.ok_or_else(|| anyhow!("host-owned Codex Apps MCP server is not available"))?;
|
||||
let managed_client = async_managed_client
|
||||
.client()
|
||||
.await
|
||||
.context("failed to get client")?;
|
||||
@@ -346,15 +358,14 @@ impl McpConnectionManager {
|
||||
let list_start = Instant::now();
|
||||
let fetch_start = Instant::now();
|
||||
let tools = list_tools_for_client_uncached(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
server_name,
|
||||
async_managed_client.provenance,
|
||||
&managed_client.client,
|
||||
managed_client.tool_timeout,
|
||||
managed_client.server_instructions.as_deref(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to refresh tools for MCP server '{CODEX_APPS_MCP_SERVER_NAME}'")
|
||||
})?;
|
||||
.with_context(|| format!("failed to refresh tools for MCP server '{server_name}'"))?;
|
||||
emit_duration(
|
||||
MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC,
|
||||
fetch_start.elapsed(),
|
||||
@@ -362,7 +373,7 @@ impl McpConnectionManager {
|
||||
);
|
||||
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
async_managed_client.provenance == McpServerProvenance::HostOwnedCodexApps,
|
||||
managed_client.codex_apps_tools_cache_context.as_ref(),
|
||||
&tools,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::codex_apps::CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION;
|
||||
use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot;
|
||||
@@ -17,6 +18,7 @@ use crate::tools::filter_tools;
|
||||
use crate::tools::qualify_tools;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
@@ -38,6 +40,7 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
|
||||
let tool_namespace = format!("mcp__{server_name}__");
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
server_provenance: Default::default(),
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: tool_namespace,
|
||||
namespace_description: None,
|
||||
@@ -630,13 +633,17 @@ fn startup_cached_codex_apps_tools_loads_from_disk_cache() {
|
||||
write_cached_codex_apps_tools(&cache_context, &cached_tools);
|
||||
|
||||
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
Some(&cache_context),
|
||||
);
|
||||
let startup_tools = startup_snapshot.expect("expected startup snapshot to load from cache");
|
||||
|
||||
assert_eq!(startup_tools.len(), 1);
|
||||
assert_eq!(startup_tools[0].server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(
|
||||
startup_tools[0].server_provenance,
|
||||
McpServerProvenance::HostOwnedCodexApps
|
||||
);
|
||||
assert_eq!(startup_tools[0].callable_name, "calendar_search");
|
||||
}
|
||||
|
||||
@@ -661,6 +668,7 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
provenance: McpServerProvenance::HostOwnedCodexApps,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -690,6 +698,7 @@ async fn resolve_tool_info_accepts_canonical_namespaced_tool_names() {
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
provenance: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -727,6 +736,7 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
provenance: McpServerProvenance::HostOwnedCodexApps,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -752,6 +762,7 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
provenance: McpServerProvenance::HostOwnedCodexApps,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -787,6 +798,7 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
startup_complete,
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
provenance: McpServerProvenance::HostOwnedCodexApps,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -798,6 +810,42 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_owned_codex_apps_server_is_identified_by_client_provenance() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager =
|
||||
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client.clone(),
|
||||
startup_snapshot: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
provenance: McpServerProvenance::UserConfigured,
|
||||
},
|
||||
);
|
||||
manager.clients.insert(
|
||||
"host_apps".to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
startup_snapshot: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
provenance: McpServerProvenance::HostOwnedCodexApps,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!manager.is_host_owned_codex_apps_server(CODEX_APPS_MCP_SERVER_NAME));
|
||||
assert!(manager.is_host_owned_codex_apps_server("host_apps"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elicitation_capability_uses_2025_06_18_shape_for_all_servers() {
|
||||
for server_name in [CODEX_APPS_MCP_SERVER_NAME, "custom_mcp"] {
|
||||
@@ -826,6 +874,7 @@ fn mcp_init_error_display_prompts_for_github_pat() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -878,6 +927,7 @@ fn mcp_init_error_display_reports_generic_errors() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -12,8 +13,6 @@ use codex_rmcp_client::discover_streamable_http_oauth;
|
||||
use futures::future::join_all;
|
||||
use tracing::warn;
|
||||
|
||||
use super::CODEX_APPS_MCP_SERVER_NAME;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct McpOAuthLoginConfig {
|
||||
pub url: String,
|
||||
@@ -136,7 +135,7 @@ where
|
||||
let futures = servers.into_iter().map(|(name, config)| {
|
||||
let name = name.clone();
|
||||
let config = config.clone();
|
||||
let has_runtime_auth = name == CODEX_APPS_MCP_SERVER_NAME
|
||||
let has_runtime_auth = config.provenance == McpServerProvenance::HostOwnedCodexApps
|
||||
&& auth.is_some_and(CodexAuth::uses_codex_backend)
|
||||
&& matches!(
|
||||
&config.transport,
|
||||
|
||||
149
codex-rs/codex-mcp/src/mcp/codex_apps_endpoint.rs
Normal file
149
codex-rs/codex-mcp/src/mcp/codex_apps_endpoint.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use codex_client::is_allowed_chatgpt_host;
|
||||
use codex_config::McpServerProvenance;
|
||||
use url::Host;
|
||||
use url::Url;
|
||||
|
||||
use super::McpConfig;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct HostOwnedCodexAppsMcpEndpoint {
|
||||
url: trusted_codex_apps_mcp_url::TrustedCodexAppsMcpUrl,
|
||||
provenance: McpServerProvenance,
|
||||
}
|
||||
|
||||
impl HostOwnedCodexAppsMcpEndpoint {
|
||||
fn new(url: trusted_codex_apps_mcp_url::TrustedCodexAppsMcpUrl) -> Self {
|
||||
Self {
|
||||
url,
|
||||
provenance: McpServerProvenance::HostOwnedCodexApps,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn into_parts(self) -> (String, McpServerProvenance) {
|
||||
(self.url.into_string(), self.provenance)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn url(&self) -> &str {
|
||||
self.url.as_str()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn provenance(&self) -> McpServerProvenance {
|
||||
self.provenance
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the only endpoint allowed to receive host-owned Codex Apps provenance.
|
||||
pub(super) fn host_owned_codex_apps_mcp_endpoint(
|
||||
config: &McpConfig,
|
||||
) -> Result<HostOwnedCodexAppsMcpEndpoint, String> {
|
||||
// HostOwnedCodexApps gates first-party connector behavior, including
|
||||
// privileged file upload handling. Keep the trusted URL check and the
|
||||
// provenance grant together so a config-derived URL cannot receive the
|
||||
// host-owned marker without passing this audit point.
|
||||
let url = trusted_codex_apps_mcp_url::from_base_url(
|
||||
&config.chatgpt_base_url,
|
||||
config.apps_mcp_path_override.as_deref(),
|
||||
)?;
|
||||
Ok(HostOwnedCodexAppsMcpEndpoint::new(url))
|
||||
}
|
||||
|
||||
mod trusted_codex_apps_mcp_url {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct TrustedCodexAppsMcpUrl(String);
|
||||
|
||||
impl TrustedCodexAppsMcpUrl {
|
||||
fn new(url: String) -> Result<Self, String> {
|
||||
let parsed_url = Url::parse(&url)
|
||||
.map_err(|err| format!("invalid Codex Apps MCP URL `{url}`: {err}"))?;
|
||||
validate_url(&parsed_url, &url, "URL")?;
|
||||
Ok(Self(url))
|
||||
}
|
||||
|
||||
pub(super) fn into_string(self) -> String {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_base_url(
|
||||
base_url: &str,
|
||||
apps_mcp_path_override: Option<&str>,
|
||||
) -> Result<TrustedCodexAppsMcpUrl, String> {
|
||||
let base_url = base_url.trim_end_matches('/');
|
||||
let parsed_base_url = Url::parse(base_url)
|
||||
.map_err(|err| format!("invalid Codex Apps MCP base URL `{base_url}`: {err}"))?;
|
||||
validate_url(&parsed_base_url, base_url, "base URL")?;
|
||||
|
||||
let mut base_url = base_url.to_string();
|
||||
if is_allowed_chatgpt_host_url(&parsed_base_url.host())
|
||||
&& !parsed_base_url.path().contains("/backend-api")
|
||||
{
|
||||
base_url = format!("{base_url}/backend-api");
|
||||
}
|
||||
let (base_url, default_path) = if base_url.contains("/backend-api") {
|
||||
(base_url, "wham/apps")
|
||||
} else if base_url.contains("/api/codex") {
|
||||
(base_url, "apps")
|
||||
} else {
|
||||
(format!("{base_url}/api/codex"), "apps")
|
||||
};
|
||||
let path = apps_mcp_path_override
|
||||
.unwrap_or(default_path)
|
||||
.trim_start_matches('/');
|
||||
TrustedCodexAppsMcpUrl::new(format!("{base_url}/{path}"))
|
||||
}
|
||||
|
||||
fn validate_url(url: &Url, original_url: &str, label: &str) -> Result<(), String> {
|
||||
if !url.username().is_empty()
|
||||
|| url.password().is_some()
|
||||
|| url.query().is_some()
|
||||
|| url.fragment().is_some()
|
||||
{
|
||||
return Err(format!(
|
||||
"invalid Codex Apps MCP {label} `{original_url}`; expected a URL without credentials, query, or fragment"
|
||||
));
|
||||
}
|
||||
|
||||
let scheme = url.scheme();
|
||||
let host = url.host();
|
||||
let valid_first_party_url =
|
||||
scheme == "https" && url.port().is_none() && is_allowed_chatgpt_host_url(&host);
|
||||
let valid_local_url =
|
||||
cfg!(debug_assertions) && matches!(scheme, "http" | "https") && is_localhost(&host);
|
||||
if valid_first_party_url || valid_local_url {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"invalid Codex Apps MCP {label} `{original_url}`; expected an HTTPS URL for chatgpt.com, chat.openai.com, or chatgpt-staging.com"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_allowed_chatgpt_host_url(host: &Option<Host<&str>>) -> bool {
|
||||
let Some(Host::Domain(host)) = host else {
|
||||
return false;
|
||||
};
|
||||
is_allowed_chatgpt_host(host)
|
||||
}
|
||||
|
||||
fn is_localhost(host: &Option<Host<&str>>) -> bool {
|
||||
match host {
|
||||
Some(Host::Domain(host)) => *host == "localhost",
|
||||
Some(Host::Ipv4(ip)) => ip.is_loopback(),
|
||||
Some(Host::Ipv6(ip)) => ip.is_loopback(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "codex_apps_endpoint_tests.rs"]
|
||||
mod tests;
|
||||
96
codex-rs/codex-mcp/src/mcp/codex_apps_endpoint_tests.rs
Normal file
96
codex-rs/codex-mcp/src/mcp/codex_apps_endpoint_tests.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn trusted_url_keeps_existing_paths() {
|
||||
assert_eq!(
|
||||
trusted_codex_apps_mcp_url::from_base_url(
|
||||
"https://chatgpt.com/backend-api",
|
||||
/*apps_mcp_path_override*/ None,
|
||||
)
|
||||
.expect("trusted ChatGPT URL should build")
|
||||
.as_str(),
|
||||
"https://chatgpt.com/backend-api/wham/apps"
|
||||
);
|
||||
assert_eq!(
|
||||
trusted_codex_apps_mcp_url::from_base_url(
|
||||
"https://chat.openai.com",
|
||||
/*apps_mcp_path_override*/ None,
|
||||
)
|
||||
.expect("trusted legacy ChatGPT URL should build")
|
||||
.as_str(),
|
||||
"https://chat.openai.com/backend-api/wham/apps"
|
||||
);
|
||||
assert_eq!(
|
||||
trusted_codex_apps_mcp_url::from_base_url(
|
||||
"http://localhost:8080/api/codex",
|
||||
/*apps_mcp_path_override*/ None,
|
||||
)
|
||||
.expect("local debug URL should build")
|
||||
.as_str(),
|
||||
"http://localhost:8080/api/codex/apps"
|
||||
);
|
||||
assert_eq!(
|
||||
trusted_codex_apps_mcp_url::from_base_url(
|
||||
"http://localhost:8080",
|
||||
/*apps_mcp_path_override*/ None,
|
||||
)
|
||||
.expect("local debug URL should build")
|
||||
.as_str(),
|
||||
"http://localhost:8080/api/codex/apps"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_url_rejects_untrusted_base_urls() {
|
||||
for base_url in [
|
||||
"http://chatgpt.com/backend-api",
|
||||
"https://example.com/backend-api",
|
||||
"https://chatgpt.com.evil.example/backend-api",
|
||||
"https://evilchatgpt.com/backend-api",
|
||||
"https://foo.chat.openai.com/backend-api",
|
||||
"https://chatgpt.com:4443/backend-api",
|
||||
"https://user:pass@chatgpt.com/backend-api",
|
||||
"https://chatgpt.com/backend-api?token=secret",
|
||||
] {
|
||||
let err = trusted_codex_apps_mcp_url::from_base_url(
|
||||
base_url, /*apps_mcp_path_override*/ None,
|
||||
)
|
||||
.expect_err("untrusted URL should be rejected");
|
||||
|
||||
assert!(
|
||||
err.starts_with("invalid Codex Apps MCP base URL"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_url_rejects_override_that_adds_query_or_fragment() {
|
||||
for path_override in ["custom/mcp?token=secret", "custom/mcp#fragment"] {
|
||||
let err =
|
||||
trusted_codex_apps_mcp_url::from_base_url("https://chatgpt.com", Some(path_override))
|
||||
.expect_err("untrusted final URL should be rejected");
|
||||
|
||||
assert!(
|
||||
err.starts_with("invalid Codex Apps MCP URL"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_pairs_trusted_url_with_provenance() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let config = crate::mcp::tests::test_mcp_config(codex_home.path().to_path_buf());
|
||||
let endpoint =
|
||||
host_owned_codex_apps_mcp_endpoint(&config).expect("trusted ChatGPT URL should build");
|
||||
|
||||
assert_eq!(
|
||||
(endpoint.url(), endpoint.provenance()),
|
||||
(
|
||||
"https://chatgpt.com/backend-api/wham/apps",
|
||||
McpServerProvenance::HostOwnedCodexApps,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub use auth::resolve_oauth_scopes;
|
||||
pub use auth::should_retry_without_scopes;
|
||||
|
||||
pub(crate) mod auth;
|
||||
mod codex_apps_endpoint;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
@@ -39,6 +40,7 @@ use serde_json::Value;
|
||||
use crate::codex_apps::codex_apps_tools_cache_key;
|
||||
use crate::connection_manager::McpConnectionManager;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
use codex_apps_endpoint::host_owned_codex_apps_mcp_endpoint;
|
||||
|
||||
pub const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
|
||||
const MCP_TOOL_NAME_PREFIX: &str = "mcp";
|
||||
@@ -198,19 +200,32 @@ pub fn with_codex_apps_mcp(
|
||||
auth: Option<&CodexAuth>,
|
||||
config: &McpConfig,
|
||||
) -> HashMap<String, McpServerConfig> {
|
||||
remove_reserved_codex_apps_server(&mut servers);
|
||||
if config.apps_enabled && auth.is_some_and(CodexAuth::uses_codex_backend) {
|
||||
servers.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
codex_apps_mcp_server_config(config),
|
||||
);
|
||||
} else {
|
||||
servers.remove(CODEX_APPS_MCP_SERVER_NAME);
|
||||
match codex_apps_mcp_server_config(config) {
|
||||
Ok(server_config) => {
|
||||
servers.insert(CODEX_APPS_MCP_SERVER_NAME.to_string(), server_config);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Skipping host-owned Codex Apps MCP server: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
servers
|
||||
}
|
||||
|
||||
pub fn configured_mcp_servers(config: &McpConfig) -> HashMap<String, McpServerConfig> {
|
||||
config.configured_mcp_servers.clone()
|
||||
let mut servers = config.configured_mcp_servers.clone();
|
||||
remove_reserved_codex_apps_server(&mut servers);
|
||||
servers
|
||||
}
|
||||
|
||||
fn remove_reserved_codex_apps_server(servers: &mut HashMap<String, McpServerConfig>) {
|
||||
if servers.remove(CODEX_APPS_MCP_SERVER_NAME).is_some() {
|
||||
tracing::warn!(
|
||||
"Ignoring configured MCP server with reserved name `{CODEX_APPS_MCP_SERVER_NAME}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn effective_mcp_servers(
|
||||
@@ -347,13 +362,6 @@ pub async fn collect_mcp_snapshot_from_manager(
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) fn codex_apps_mcp_url(config: &McpConfig) -> String {
|
||||
codex_apps_mcp_url_for_base_url(
|
||||
&config.chatgpt_base_url,
|
||||
config.apps_mcp_path_override.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The Responses API requires tool names to match `^[a-zA-Z0-9_-]+$`.
|
||||
/// MCP server/tool names are user-controlled, so sanitize the fully-qualified
|
||||
/// name we expose to the model by replacing any disallowed character with `_`.
|
||||
@@ -383,36 +391,11 @@ fn codex_apps_mcp_bearer_token_env_var() -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_codex_apps_base_url(base_url: &str) -> String {
|
||||
let mut base_url = base_url.trim_end_matches('/').to_string();
|
||||
if (base_url.starts_with("https://chatgpt.com")
|
||||
|| base_url.starts_with("https://chat.openai.com"))
|
||||
&& !base_url.contains("/backend-api")
|
||||
{
|
||||
base_url = format!("{base_url}/backend-api");
|
||||
}
|
||||
base_url
|
||||
}
|
||||
fn codex_apps_mcp_server_config(config: &McpConfig) -> Result<McpServerConfig, String> {
|
||||
let endpoint = host_owned_codex_apps_mcp_endpoint(config)?;
|
||||
let (url, provenance) = endpoint.into_parts();
|
||||
|
||||
fn codex_apps_mcp_url_for_base_url(base_url: &str, apps_mcp_path_override: Option<&str>) -> String {
|
||||
let base_url = normalize_codex_apps_base_url(base_url);
|
||||
let (base_url, default_path) = if base_url.contains("/backend-api") {
|
||||
(base_url, "wham/apps")
|
||||
} else if base_url.contains("/api/codex") {
|
||||
(base_url, "apps")
|
||||
} else {
|
||||
(format!("{base_url}/api/codex"), "apps")
|
||||
};
|
||||
let path = apps_mcp_path_override
|
||||
.unwrap_or(default_path)
|
||||
.trim_start_matches('/');
|
||||
format!("{base_url}/{path}")
|
||||
}
|
||||
|
||||
fn codex_apps_mcp_server_config(config: &McpConfig) -> McpServerConfig {
|
||||
let url = codex_apps_mcp_url(config);
|
||||
|
||||
McpServerConfig {
|
||||
Ok(McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(),
|
||||
@@ -424,6 +407,7 @@ fn codex_apps_mcp_server_config(config: &McpConfig) -> McpServerConfig {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance,
|
||||
startup_timeout_sec: Some(Duration::from_secs(30)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -432,7 +416,7 @@ fn codex_apps_mcp_server_config(config: &McpConfig) -> McpServerConfig {
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn protocol_tool_from_rmcp_tool(name: &str, tool: &rmcp::model::Tool) -> Option<Tool> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -14,7 +15,7 @@ use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
pub(super) fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
McpConfig {
|
||||
chatgpt_base_url: "https://chatgpt.com".to_string(),
|
||||
apps_mcp_path_override: None,
|
||||
@@ -161,51 +162,21 @@ fn tool_plugin_provenance_collects_app_and_mcp_sources() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() {
|
||||
assert_eq!(
|
||||
codex_apps_mcp_url_for_base_url(
|
||||
"https://chatgpt.com/backend-api",
|
||||
/*apps_mcp_path_override*/ None,
|
||||
),
|
||||
"https://chatgpt.com/backend-api/wham/apps"
|
||||
);
|
||||
assert_eq!(
|
||||
codex_apps_mcp_url_for_base_url(
|
||||
"https://chat.openai.com",
|
||||
/*apps_mcp_path_override*/ None,
|
||||
),
|
||||
"https://chat.openai.com/backend-api/wham/apps"
|
||||
);
|
||||
assert_eq!(
|
||||
codex_apps_mcp_url_for_base_url(
|
||||
"http://localhost:8080/api/codex",
|
||||
/*apps_mcp_path_override*/ None,
|
||||
),
|
||||
"http://localhost:8080/api/codex/apps"
|
||||
);
|
||||
assert_eq!(
|
||||
codex_apps_mcp_url_for_base_url(
|
||||
"http://localhost:8080",
|
||||
/*apps_mcp_path_override*/ None,
|
||||
),
|
||||
"http://localhost:8080/api/codex/apps"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_mcp_url_uses_legacy_codex_apps_path() {
|
||||
let config = test_mcp_config(PathBuf::from("/tmp"));
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let config = test_mcp_config(codex_home.path().to_path_buf());
|
||||
|
||||
assert_eq!(
|
||||
codex_apps_mcp_url(&config),
|
||||
"https://chatgpt.com/backend-api/wham/apps"
|
||||
);
|
||||
let endpoint =
|
||||
host_owned_codex_apps_mcp_endpoint(&config).expect("trusted ChatGPT URL should build");
|
||||
|
||||
assert_eq!(endpoint.url(), "https://chatgpt.com/backend-api/wham/apps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_config_uses_legacy_codex_apps_path() {
|
||||
let mut config = test_mcp_config(PathBuf::from("/tmp"));
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let mut config = test_mcp_config(codex_home.path().to_path_buf());
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
|
||||
let mut servers = with_codex_apps_mcp(HashMap::new(), /*auth*/ None, &config);
|
||||
@@ -225,9 +196,23 @@ fn codex_apps_server_config_uses_legacy_codex_apps_path() {
|
||||
assert_eq!(url, "https://chatgpt.com/backend-api/wham/apps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_config_is_marked_host_owned() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let config = test_mcp_config(codex_home.path().to_path_buf());
|
||||
|
||||
assert_eq!(
|
||||
codex_apps_mcp_server_config(&config)
|
||||
.expect("trusted ChatGPT URL should build")
|
||||
.provenance,
|
||||
McpServerProvenance::HostOwnedCodexApps
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_config_uses_configured_apps_mcp_path_override() {
|
||||
let mut config = test_mcp_config(PathBuf::from("/tmp"));
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let mut config = test_mcp_config(codex_home.path().to_path_buf());
|
||||
config.apps_mcp_path_override = Some("/custom/mcp".to_string());
|
||||
config.apps_enabled = true;
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
@@ -244,6 +229,52 @@ fn codex_apps_server_config_uses_configured_apps_mcp_path_override() {
|
||||
assert_eq!(url, "https://chatgpt.com/backend-api/custom/mcp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_codex_apps_mcp_does_not_trust_untrusted_base_url() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let mut config = test_mcp_config(codex_home.path().to_path_buf());
|
||||
config.apps_enabled = true;
|
||||
config.chatgpt_base_url = "https://chatgpt.com.evil.example/backend-api".to_string();
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
|
||||
let servers = with_codex_apps_mcp(HashMap::new(), Some(&auth), &config);
|
||||
|
||||
assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_mcp_servers_ignore_reserved_codex_apps_name() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let mut config = test_mcp_config(codex_home.path().to_path_buf());
|
||||
config.configured_mcp_servers.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://user.example/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!configured_mcp_servers(&config).contains_key(CODEX_APPS_MCP_SERVER_NAME));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn effective_mcp_servers_preserve_user_servers_and_add_codex_apps() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
@@ -265,6 +296,7 @@ async fn effective_mcp_servers_preserve_user_servers_and_add_codex_apps() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -289,6 +321,7 @@ async fn effective_mcp_servers_preserve_user_servers_and_add_codex_apps() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -26,7 +26,6 @@ use crate::codex_apps::normalize_codex_apps_callable_namespace;
|
||||
use crate::codex_apps::normalize_codex_apps_tool_title;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
use crate::runtime::emit_duration;
|
||||
@@ -41,6 +40,7 @@ use codex_api::SharedAuthProvider;
|
||||
use codex_async_utils::CancelErr;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_exec_server::HttpClient;
|
||||
@@ -125,6 +125,7 @@ pub(crate) struct AsyncManagedClient {
|
||||
pub(crate) startup_complete: Arc<AtomicBool>,
|
||||
pub(crate) tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
pub(crate) cancel_token: CancellationToken,
|
||||
pub(crate) provenance: McpServerProvenance,
|
||||
}
|
||||
|
||||
impl AsyncManagedClient {
|
||||
@@ -142,10 +143,12 @@ impl AsyncManagedClient {
|
||||
tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
provenance: McpServerProvenance,
|
||||
) -> Self {
|
||||
let tool_filter = ToolFilter::from_config(&config);
|
||||
let is_host_owned_codex_apps_server = provenance == McpServerProvenance::HostOwnedCodexApps;
|
||||
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
&server_name,
|
||||
is_host_owned_codex_apps_server,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
)
|
||||
.map(|tools| filter_tools(tools, &tool_filter));
|
||||
@@ -181,6 +184,7 @@ impl AsyncManagedClient {
|
||||
tx_event,
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context,
|
||||
provenance,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -209,6 +213,7 @@ impl AsyncManagedClient {
|
||||
startup_complete,
|
||||
tool_plugin_provenance,
|
||||
cancel_token,
|
||||
provenance,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +243,8 @@ impl AsyncManagedClient {
|
||||
let annotate_tools = |tools: Vec<ToolInfo>| {
|
||||
let mut tools = tools;
|
||||
for tool in &mut tools {
|
||||
if tool.server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
tool.server_provenance = self.provenance;
|
||||
if tool.is_host_owned_codex_apps() {
|
||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
||||
}
|
||||
|
||||
@@ -327,10 +333,13 @@ pub(crate) fn elicitation_capability_for_server(
|
||||
|
||||
pub(crate) async fn list_tools_for_client_uncached(
|
||||
server_name: &str,
|
||||
server_provenance: McpServerProvenance,
|
||||
client: &Arc<RmcpClient>,
|
||||
timeout: Option<Duration>,
|
||||
server_instructions: Option<&str>,
|
||||
) -> Result<Vec<ToolInfo>> {
|
||||
let is_host_owned_codex_apps_server =
|
||||
server_provenance == McpServerProvenance::HostOwnedCodexApps;
|
||||
let resp = client
|
||||
.list_tools_with_connector_ids(/*params*/ None, timeout)
|
||||
.await?;
|
||||
@@ -341,23 +350,29 @@ pub(crate) async fn list_tools_for_client_uncached(
|
||||
let mut tool_def = tool.tool;
|
||||
let (connector_id, connector_name, connector_description) =
|
||||
sanitize_tool_connector_metadata(
|
||||
server_name,
|
||||
is_host_owned_codex_apps_server,
|
||||
&mut tool_def,
|
||||
tool.connector_id,
|
||||
tool.connector_name,
|
||||
tool.connector_description,
|
||||
);
|
||||
let callable_name = normalize_codex_apps_callable_name(
|
||||
server_name,
|
||||
is_host_owned_codex_apps_server,
|
||||
&tool_def.name,
|
||||
connector_id.as_deref(),
|
||||
connector_name.as_deref(),
|
||||
);
|
||||
let callable_namespace =
|
||||
normalize_codex_apps_callable_namespace(server_name, connector_name.as_deref());
|
||||
let callable_namespace = normalize_codex_apps_callable_namespace(
|
||||
server_name,
|
||||
is_host_owned_codex_apps_server,
|
||||
connector_name.as_deref(),
|
||||
);
|
||||
if let Some(title) = tool_def.title.as_deref() {
|
||||
let normalized_title =
|
||||
normalize_codex_apps_tool_title(server_name, connector_name.as_deref(), title);
|
||||
let normalized_title = normalize_codex_apps_tool_title(
|
||||
is_host_owned_codex_apps_server,
|
||||
connector_name.as_deref(),
|
||||
title,
|
||||
);
|
||||
if tool_def.title.as_deref() != Some(normalized_title.as_str()) {
|
||||
tool_def.title = Some(normalized_title);
|
||||
}
|
||||
@@ -372,6 +387,7 @@ pub(crate) async fn list_tools_for_client_uncached(
|
||||
};
|
||||
ToolInfo {
|
||||
server_name: server_name.to_owned(),
|
||||
server_provenance,
|
||||
callable_name,
|
||||
callable_namespace,
|
||||
namespace_description,
|
||||
@@ -382,20 +398,20 @@ pub(crate) async fn list_tools_for_client_uncached(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
if is_host_owned_codex_apps_server {
|
||||
return Ok(filter_disallowed_codex_apps_tools(tools));
|
||||
}
|
||||
Ok(tools)
|
||||
}
|
||||
|
||||
fn sanitize_tool_connector_metadata(
|
||||
server_name: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
tool: &mut RmcpTool,
|
||||
connector_id: Option<String>,
|
||||
connector_name: Option<String>,
|
||||
connector_description: Option<String>,
|
||||
) -> (Option<String>, Option<String>, Option<String>) {
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
if is_host_owned_codex_apps_server {
|
||||
return (connector_id, connector_name, connector_description);
|
||||
}
|
||||
|
||||
@@ -463,9 +479,10 @@ async fn start_server_task(
|
||||
tx_event,
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context,
|
||||
provenance,
|
||||
} = params;
|
||||
let elicitation = elicitation_capability_for_server(&server_name);
|
||||
let params = InitializeRequestParams {
|
||||
let initialize_params = InitializeRequestParams {
|
||||
meta: None,
|
||||
capabilities: ClientCapabilities {
|
||||
experimental: None,
|
||||
@@ -489,7 +506,7 @@ async fn start_server_task(
|
||||
let send_elicitation = elicitation_requests.make_sender(server_name.clone(), tx_event);
|
||||
|
||||
let initialize_result = client
|
||||
.initialize(params, startup_timeout, send_elicitation)
|
||||
.initialize(initialize_params, startup_timeout, send_elicitation)
|
||||
.await
|
||||
.map_err(StartupOutcomeError::from)?;
|
||||
|
||||
@@ -503,6 +520,7 @@ async fn start_server_task(
|
||||
let fetch_start = Instant::now();
|
||||
let tools = list_tools_for_client_uncached(
|
||||
&server_name,
|
||||
provenance,
|
||||
&client,
|
||||
startup_timeout,
|
||||
initialize_result.instructions.as_deref(),
|
||||
@@ -515,11 +533,11 @@ async fn start_server_task(
|
||||
&[],
|
||||
);
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
&server_name,
|
||||
provenance == McpServerProvenance::HostOwnedCodexApps,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
&tools,
|
||||
);
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
if provenance == McpServerProvenance::HostOwnedCodexApps {
|
||||
emit_duration(
|
||||
MCP_TOOLS_LIST_DURATION_METRIC,
|
||||
list_start.elapsed(),
|
||||
@@ -548,6 +566,7 @@ struct StartServerTaskParams {
|
||||
tx_event: Sender<Event>,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
|
||||
provenance: McpServerProvenance,
|
||||
}
|
||||
|
||||
async fn make_rmcp_client(
|
||||
@@ -685,7 +704,7 @@ mod tests {
|
||||
|
||||
let (connector_id, connector_name, connector_description) =
|
||||
sanitize_tool_connector_metadata(
|
||||
"minimaltest",
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
&mut tool,
|
||||
Some("connector_gmail".to_string()),
|
||||
Some("Gmail".to_string()),
|
||||
@@ -721,7 +740,7 @@ mod tests {
|
||||
|
||||
let (connector_id, connector_name, connector_description) =
|
||||
sanitize_tool_connector_metadata(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
&mut tool,
|
||||
Some("connector_gmail".to_string()),
|
||||
Some("Gmail".to_string()),
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_protocol::ToolName;
|
||||
use rmcp::model::Tool;
|
||||
use serde::Deserialize;
|
||||
@@ -29,6 +30,9 @@ pub(crate) const MCP_TOOLS_CACHE_WRITE_DURATION_METRIC: &str =
|
||||
pub struct ToolInfo {
|
||||
/// Raw MCP server name used for routing the tool call.
|
||||
pub server_name: String,
|
||||
/// Runtime-only provenance for the server that exposed this tool.
|
||||
#[serde(skip)]
|
||||
pub server_provenance: McpServerProvenance,
|
||||
/// Model-visible tool name used in Responses API tool declarations.
|
||||
#[serde(rename = "tool_name", alias = "callable_name")]
|
||||
pub callable_name: String,
|
||||
@@ -51,6 +55,10 @@ impl ToolInfo {
|
||||
pub fn canonical_tool_name(&self) -> ToolName {
|
||||
ToolName::namespaced(self.callable_namespace.clone(), self.callable_name.clone())
|
||||
}
|
||||
|
||||
pub fn is_host_owned_codex_apps(&self) -> bool {
|
||||
self.server_provenance == McpServerProvenance::HostOwnedCodexApps
|
||||
}
|
||||
}
|
||||
|
||||
pub fn declared_openai_file_input_param_names(
|
||||
|
||||
@@ -91,6 +91,7 @@ pub use mcp_types::AppToolApproval;
|
||||
pub use mcp_types::McpServerConfig;
|
||||
pub use mcp_types::McpServerDisabledReason;
|
||||
pub use mcp_types::McpServerEnvVar;
|
||||
pub use mcp_types::McpServerProvenance;
|
||||
pub use mcp_types::McpServerToolConfig;
|
||||
pub use mcp_types::McpServerTransportConfig;
|
||||
pub use mcp_types::RawMcpServerConfig;
|
||||
|
||||
@@ -27,6 +27,7 @@ async fn replace_mcp_servers_serializes_per_tool_approval_overrides() -> anyhow:
|
||||
required: false,
|
||||
supports_parallel_tool_calls: true,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::Auto),
|
||||
|
||||
@@ -36,6 +36,14 @@ pub enum McpServerDisabledReason {
|
||||
Requirements { source: RequirementSource },
|
||||
}
|
||||
|
||||
/// Runtime-only origin marker for an MCP server instance.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum McpServerProvenance {
|
||||
#[default]
|
||||
UserConfigured,
|
||||
HostOwnedCodexApps,
|
||||
}
|
||||
|
||||
impl fmt::Display for McpServerDisabledReason {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -139,6 +147,10 @@ pub struct McpServerConfig {
|
||||
#[serde(skip)]
|
||||
pub disabled_reason: Option<McpServerDisabledReason>,
|
||||
|
||||
/// Internal provenance marker for server configs created by Codex itself.
|
||||
#[serde(skip)]
|
||||
pub provenance: McpServerProvenance,
|
||||
|
||||
/// Startup timeout in seconds for initializing MCP server & initially listing tools.
|
||||
#[serde(
|
||||
default,
|
||||
@@ -334,6 +346,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
|
||||
required: required.unwrap_or_default(),
|
||||
supports_parallel_tool_calls: supports_parallel_tool_calls.unwrap_or_default(),
|
||||
disabled_reason: None,
|
||||
provenance: McpServerProvenance::UserConfigured,
|
||||
default_tools_approval_mode,
|
||||
enabled_tools,
|
||||
disabled_tools,
|
||||
|
||||
@@ -387,6 +387,7 @@ fn deserialize_ignores_unknown_server_fields() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -7,6 +7,7 @@ pub use crate::mcp_types::AppToolApproval;
|
||||
pub use crate::mcp_types::McpServerConfig;
|
||||
pub use crate::mcp_types::McpServerDisabledReason;
|
||||
pub use crate::mcp_types::McpServerEnvVar;
|
||||
pub use crate::mcp_types::McpServerProvenance;
|
||||
pub use crate::mcp_types::McpServerToolConfig;
|
||||
pub use crate::mcp_types::McpServerTransportConfig;
|
||||
pub use crate::mcp_types::RawMcpServerConfig;
|
||||
|
||||
@@ -223,6 +223,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -688,6 +689,7 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -799,6 +801,7 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -960,6 +963,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -104,6 +104,7 @@ fn stdio_mcp(command: &str) -> McpServerConfig {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -128,6 +129,7 @@ fn http_mcp(url: &str) -> McpServerConfig {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -3499,6 +3501,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(3)),
|
||||
tool_timeout_sec: Some(Duration::from_secs(5)),
|
||||
default_tools_approval_mode: None,
|
||||
@@ -3762,6 +3765,7 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -3838,6 +3842,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -3899,6 +3904,7 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()>
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -3950,6 +3956,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4004,6 +4011,7 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(2)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4074,6 +4082,7 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(2)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4156,6 +4165,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(2)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4191,6 +4201,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4261,6 +4272,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(2)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4286,6 +4298,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4374,6 +4387,7 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4424,6 +4438,7 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
|
||||
required: true,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4474,6 +4489,7 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -4528,6 +4544,7 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -869,6 +869,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: true,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -898,6 +899,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(std::time::Duration::from_secs(5)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -964,6 +966,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::Prompt),
|
||||
@@ -1028,6 +1031,7 @@ foo = { command = "cmd" }
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -1082,6 +1086,7 @@ foo = { command = "cmd" } # keep me
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -1135,6 +1140,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -1189,6 +1195,7 @@ foo = { command = "cmd" }
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -518,7 +518,7 @@ pub(crate) fn accessible_connectors_from_mcp_tools(
|
||||
// ToolInfo already carries plugin provenance, so app-level plugin sources
|
||||
// can be derived here instead of requiring a separate enrichment pass.
|
||||
let tools = mcp_tools.values().filter_map(|tool| {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
if !tool.is_host_owned_codex_apps() {
|
||||
return None;
|
||||
}
|
||||
let connector_id = tool.connector_id.as_deref()?;
|
||||
@@ -588,7 +588,7 @@ pub(crate) fn app_tool_policy(
|
||||
}
|
||||
|
||||
pub(crate) fn codex_app_tool_is_enabled(config: &Config, tool_info: &ToolInfo) -> bool {
|
||||
if tool_info.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
if !tool_info.is_host_owned_codex_apps() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_config::types::AppConfig;
|
||||
use codex_config::types::AppToolConfig;
|
||||
use codex_config::types::AppToolsConfig;
|
||||
@@ -117,6 +118,7 @@ fn codex_app_tool(
|
||||
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
server_provenance: McpServerProvenance::HostOwnedCodexApps,
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: tool_namespace,
|
||||
namespace_description: None,
|
||||
@@ -195,6 +197,7 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
|
||||
"mcp__sample__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "sample".to_string(),
|
||||
server_provenance: Default::default(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "sample".to_string(),
|
||||
namespace_description: None,
|
||||
@@ -319,6 +322,7 @@ fn accessible_connectors_from_mcp_tools_preserves_description() {
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
server_provenance: McpServerProvenance::HostOwnedCodexApps,
|
||||
callable_name: "calendar_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: Some("Plan events".to_string()),
|
||||
|
||||
@@ -355,6 +355,7 @@ fn mcp_dependency_to_server_config(
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -384,6 +385,7 @@ fn mcp_dependency_to_server_config(
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -39,7 +39,6 @@ use codex_analytics::build_track_events_context;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_features::Feature;
|
||||
use codex_hooks::PermissionRequestDecision;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::McpPermissionPromptAutoApproveContext;
|
||||
use codex_mcp::SandboxState;
|
||||
use codex_mcp::declared_openai_file_input_param_names;
|
||||
@@ -121,13 +120,19 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
tool: tool_name.clone(),
|
||||
arguments: arguments_value.clone(),
|
||||
};
|
||||
let is_host_owned_codex_apps_server = sess
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.read()
|
||||
.await
|
||||
.is_host_owned_codex_apps_server(&server);
|
||||
|
||||
let metadata =
|
||||
lookup_mcp_tool_metadata(sess.as_ref(), turn_context.as_ref(), &server, &tool_name).await;
|
||||
let mcp_app_resource_uri = metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.mcp_app_resource_uri.clone());
|
||||
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let app_tool_policy = if is_host_owned_codex_apps_server {
|
||||
connectors::app_tool_policy(
|
||||
&turn_context.config,
|
||||
metadata
|
||||
@@ -144,14 +149,14 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
} else {
|
||||
connectors::AppToolPolicy::default()
|
||||
};
|
||||
let approval_mode = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let approval_mode = if is_host_owned_codex_apps_server {
|
||||
app_tool_policy.approval
|
||||
} else {
|
||||
custom_mcp_tool_approval_mode(sess.as_ref(), turn_context.as_ref(), &server, &tool_name)
|
||||
.await
|
||||
};
|
||||
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME && !app_tool_policy.enabled {
|
||||
if is_host_owned_codex_apps_server && !app_tool_policy.enabled {
|
||||
let result = notify_mcp_tool_call_skip(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
@@ -176,7 +181,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
}
|
||||
let request_meta = build_mcp_tool_call_request_meta(
|
||||
turn_context.as_ref(),
|
||||
&server,
|
||||
is_host_owned_codex_apps_server,
|
||||
&call_id,
|
||||
metadata.as_ref(),
|
||||
);
|
||||
@@ -308,6 +313,12 @@ async fn handle_approved_mcp_tool_call(
|
||||
maybe_mark_thread_memory_mode_polluted(sess, turn_context).await;
|
||||
|
||||
let server = invocation.server.clone();
|
||||
let is_host_owned_codex_apps_server = sess
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.read()
|
||||
.await
|
||||
.is_host_owned_codex_apps_server(&server);
|
||||
let tool_name = invocation.tool.clone();
|
||||
let arguments_value = invocation.arguments.clone();
|
||||
let connector_id = metadata.and_then(|metadata| metadata.connector_id.as_deref());
|
||||
@@ -375,7 +386,14 @@ async fn handle_approved_mcp_tool_call(
|
||||
truncate_mcp_tool_result_for_event(&result),
|
||||
)
|
||||
.await;
|
||||
maybe_track_codex_app_used(sess, turn_context, &server, &tool_name).await;
|
||||
maybe_track_codex_app_used(
|
||||
sess,
|
||||
turn_context,
|
||||
&server,
|
||||
&tool_name,
|
||||
is_host_owned_codex_apps_server,
|
||||
)
|
||||
.await;
|
||||
|
||||
let status = if result.is_ok() { "ok" } else { "error" };
|
||||
emit_mcp_call_metrics(
|
||||
@@ -777,8 +795,9 @@ async fn maybe_track_codex_app_used(
|
||||
turn_context: &TurnContext,
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
) {
|
||||
if server != CODEX_APPS_MCP_SERVER_NAME {
|
||||
if !is_host_owned_codex_apps_server {
|
||||
return;
|
||||
}
|
||||
let metadata = lookup_mcp_app_usage_metadata(sess, server, tool_name).await;
|
||||
@@ -889,7 +908,7 @@ async fn custom_mcp_tool_approval_mode(
|
||||
|
||||
fn build_mcp_tool_call_request_meta(
|
||||
turn_context: &TurnContext,
|
||||
server: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
call_id: &str,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
) -> Option<serde_json::Value> {
|
||||
@@ -902,7 +921,7 @@ fn build_mcp_tool_call_request_meta(
|
||||
);
|
||||
}
|
||||
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
if is_host_owned_codex_apps_server {
|
||||
let mut codex_apps_meta = metadata
|
||||
.and_then(|metadata| metadata.codex_apps_meta.clone())
|
||||
.unwrap_or_default();
|
||||
@@ -951,6 +970,7 @@ struct McpToolApprovalPromptOptions {
|
||||
|
||||
struct McpToolApprovalElicitationRequest<'a> {
|
||||
server: &'a str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
metadata: Option<&'a McpToolApprovalMetadata>,
|
||||
tool_params: Option<&'a serde_json::Value>,
|
||||
tool_params_display: Option<&'a [RenderedMcpToolApprovalParam]>,
|
||||
@@ -995,6 +1015,7 @@ pub(crate) fn is_mcp_tool_approval_question_id(question_id: &str) -> bool {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
struct McpToolApprovalKey {
|
||||
server: String,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
connector_id: Option<String>,
|
||||
tool_name: String,
|
||||
}
|
||||
@@ -1020,6 +1041,12 @@ async fn maybe_request_mcp_tool_approval(
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> Option<McpToolApprovalDecision> {
|
||||
let is_host_owned_codex_apps_server = sess
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.read()
|
||||
.await
|
||||
.is_host_owned_codex_apps_server(&invocation.server);
|
||||
if mcp_permission_prompt_is_auto_approved(
|
||||
turn_context.approval_policy.value(),
|
||||
&turn_context.permission_profile(),
|
||||
@@ -1062,9 +1089,18 @@ async fn maybe_request_mcp_tool_approval(
|
||||
}
|
||||
}
|
||||
|
||||
let session_approval_key = session_mcp_tool_approval_key(invocation, metadata, approval_mode);
|
||||
let persistent_approval_key =
|
||||
persistent_mcp_tool_approval_key(invocation, metadata, approval_mode);
|
||||
let session_approval_key = session_mcp_tool_approval_key(
|
||||
invocation,
|
||||
metadata,
|
||||
approval_mode,
|
||||
is_host_owned_codex_apps_server,
|
||||
);
|
||||
let persistent_approval_key = persistent_mcp_tool_approval_key(
|
||||
invocation,
|
||||
metadata,
|
||||
approval_mode,
|
||||
is_host_owned_codex_apps_server,
|
||||
);
|
||||
if let Some(key) = session_approval_key.as_ref()
|
||||
&& mcp_tool_approval_is_remembered(sess, key).await
|
||||
{
|
||||
@@ -1143,6 +1179,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
let mut question = build_mcp_tool_approval_question(
|
||||
question_id.clone(),
|
||||
&invocation.server,
|
||||
is_host_owned_codex_apps_server,
|
||||
&invocation.tool,
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
prompt_options,
|
||||
@@ -1161,6 +1198,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
turn_context.as_ref(),
|
||||
McpToolApprovalElicitationRequest {
|
||||
server: &invocation.server,
|
||||
is_host_owned_codex_apps_server,
|
||||
metadata,
|
||||
tool_params: rendered_template
|
||||
.as_ref()
|
||||
@@ -1249,18 +1287,20 @@ fn session_mcp_tool_approval_key(
|
||||
invocation: &McpInvocation,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
) -> Option<McpToolApprovalKey> {
|
||||
if approval_mode != AppToolApproval::Auto {
|
||||
return None;
|
||||
}
|
||||
|
||||
let connector_id = metadata.and_then(|metadata| metadata.connector_id.clone());
|
||||
if invocation.server == CODEX_APPS_MCP_SERVER_NAME && connector_id.is_none() {
|
||||
if is_host_owned_codex_apps_server && connector_id.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(McpToolApprovalKey {
|
||||
server: invocation.server.clone(),
|
||||
is_host_owned_codex_apps_server,
|
||||
connector_id,
|
||||
tool_name: invocation.tool.clone(),
|
||||
})
|
||||
@@ -1270,8 +1310,14 @@ fn persistent_mcp_tool_approval_key(
|
||||
invocation: &McpInvocation,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
) -> Option<McpToolApprovalKey> {
|
||||
session_mcp_tool_approval_key(invocation, metadata, approval_mode)
|
||||
session_mcp_tool_approval_key(
|
||||
invocation,
|
||||
metadata,
|
||||
approval_mode,
|
||||
is_host_owned_codex_apps_server,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_guardian_mcp_tool_review_request(
|
||||
@@ -1341,6 +1387,12 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
) -> Option<McpToolApprovalMetadata> {
|
||||
let is_host_owned_codex_apps_server = sess
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.read()
|
||||
.await
|
||||
.is_host_owned_codex_apps_server(server);
|
||||
let tools = sess
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
@@ -1351,7 +1403,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
let tool_info = tools
|
||||
.into_values()
|
||||
.find(|tool_info| tool_info.server_name == server && tool_info.tool.name == tool_name)?;
|
||||
let connector_description = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let connector_description = if is_host_owned_codex_apps_server {
|
||||
let connectors = match connectors::list_cached_accessible_connectors_from_mcp_tools(
|
||||
turn_context.config.as_ref(),
|
||||
)
|
||||
@@ -1392,17 +1444,17 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
.cloned(),
|
||||
// Disallow custom MCPs from uploading files via fileParams.
|
||||
openai_file_input_params: openai_file_input_params_for_server(
|
||||
server,
|
||||
is_host_owned_codex_apps_server,
|
||||
tool_info.tool.meta.as_deref(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn openai_file_input_params_for_server(
|
||||
server: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
meta: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||
) -> Option<Vec<String>> {
|
||||
(server == CODEX_APPS_MCP_SERVER_NAME)
|
||||
is_host_owned_codex_apps_server
|
||||
.then_some(declared_openai_file_input_param_names(meta))
|
||||
.filter(|params| !params.is_empty())
|
||||
}
|
||||
@@ -1459,6 +1511,7 @@ async fn lookup_mcp_app_usage_metadata(
|
||||
fn build_mcp_tool_approval_question(
|
||||
question_id: String,
|
||||
server: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
tool_name: &str,
|
||||
connector_name: Option<&str>,
|
||||
prompt_options: McpToolApprovalPromptOptions,
|
||||
@@ -1467,7 +1520,12 @@ fn build_mcp_tool_approval_question(
|
||||
let question = question_override
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| {
|
||||
build_mcp_tool_approval_fallback_message(server, tool_name, connector_name)
|
||||
build_mcp_tool_approval_fallback_message(
|
||||
server,
|
||||
is_host_owned_codex_apps_server,
|
||||
tool_name,
|
||||
connector_name,
|
||||
)
|
||||
});
|
||||
let question = format!("{}?", question.trim_end_matches('?'));
|
||||
|
||||
@@ -1504,6 +1562,7 @@ fn build_mcp_tool_approval_question(
|
||||
|
||||
fn build_mcp_tool_approval_fallback_message(
|
||||
server: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
tool_name: &str,
|
||||
connector_name: Option<&str>,
|
||||
) -> String {
|
||||
@@ -1512,7 +1571,7 @@ fn build_mcp_tool_approval_fallback_message(
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| {
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
if is_host_owned_codex_apps_server {
|
||||
"this app".to_string()
|
||||
} else {
|
||||
format!("the {server} MCP server")
|
||||
@@ -1555,7 +1614,7 @@ fn build_mcp_tool_approval_elicitation_request(
|
||||
server_name: request.server.to_string(),
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: build_mcp_tool_approval_elicitation_meta(
|
||||
request.server,
|
||||
request.is_host_owned_codex_apps_server,
|
||||
request.metadata,
|
||||
request.tool_params,
|
||||
request.tool_params_display,
|
||||
@@ -1573,7 +1632,7 @@ fn build_mcp_tool_approval_elicitation_request(
|
||||
}
|
||||
|
||||
fn build_mcp_tool_approval_elicitation_meta(
|
||||
server: &str,
|
||||
is_host_owned_codex_apps_server: bool,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
tool_params: Option<&serde_json::Value>,
|
||||
tool_params_display: Option<&[RenderedMcpToolApprovalParam]>,
|
||||
@@ -1624,7 +1683,7 @@ fn build_mcp_tool_approval_elicitation_meta(
|
||||
serde_json::Value::String(tool_description.clone()),
|
||||
);
|
||||
}
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME
|
||||
if is_host_owned_codex_apps_server
|
||||
&& (metadata.connector_id.is_some()
|
||||
|| metadata.connector_name.is_some()
|
||||
|| metadata.connector_description.is_some())
|
||||
@@ -1852,7 +1911,7 @@ async fn maybe_persist_mcp_tool_approval(
|
||||
) {
|
||||
let tool_name = key.tool_name.clone();
|
||||
|
||||
let persist_result = if key.server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let persist_result = if key.is_host_owned_codex_apps_server {
|
||||
let Some(connector_id) = key.connector_id.clone() else {
|
||||
remember_mcp_tool_approval(sess, key).await;
|
||||
return;
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerToolConfig;
|
||||
use codex_hooks::Hooks;
|
||||
use codex_hooks::HooksConfig;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -219,11 +220,11 @@ fn openai_file_params_are_only_honored_for_codex_apps() {
|
||||
let meta = meta.as_object();
|
||||
|
||||
assert_eq!(
|
||||
openai_file_input_params_for_server(CODEX_APPS_MCP_SERVER_NAME, meta),
|
||||
openai_file_input_params_for_server(/*is_host_owned_codex_apps_server*/ true, meta),
|
||||
Some(vec!["file".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
openai_file_input_params_for_server("minimaltest", meta),
|
||||
openai_file_input_params_for_server(/*is_host_owned_codex_apps_server*/ false, meta),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -483,6 +484,7 @@ async fn approval_elicitation_request_uses_message_override_and_preserves_tool_p
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
"create_event",
|
||||
Some("Calendar"),
|
||||
prompt_options(
|
||||
@@ -496,6 +498,7 @@ async fn approval_elicitation_request_uses_message_override_and_preserves_tool_p
|
||||
&turn_context,
|
||||
McpToolApprovalElicitationRequest {
|
||||
server: CODEX_APPS_MCP_SERVER_NAME,
|
||||
is_host_owned_codex_apps_server: true,
|
||||
metadata: Some(&approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
@@ -580,6 +583,7 @@ fn custom_mcp_tool_question_mentions_server_name() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
"custom_server",
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
"run_action",
|
||||
/*connector_name*/ None,
|
||||
prompt_options(
|
||||
@@ -608,6 +612,7 @@ fn codex_apps_tool_question_uses_fallback_app_label() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
"run_action",
|
||||
/*connector_name*/ None,
|
||||
prompt_options(
|
||||
@@ -622,11 +627,32 @@ fn codex_apps_tool_question_uses_fallback_app_label() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spoofed_codex_apps_server_name_uses_custom_server_fallback() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
"run_action",
|
||||
/*connector_name*/ None,
|
||||
prompt_options(
|
||||
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
|
||||
),
|
||||
/*question_override*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
question.question,
|
||||
"Allow the codex_apps MCP server to run tool \"run_action\"?"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_codex_apps_tool_question_offers_always_allow() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
"run_action",
|
||||
Some("Calendar"),
|
||||
prompt_options(
|
||||
@@ -662,6 +688,7 @@ fn trusted_codex_apps_tool_question_offers_always_allow() {
|
||||
fn codex_apps_tool_question_without_elicitation_omits_always_allow() {
|
||||
let session_key = McpToolApprovalKey {
|
||||
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
is_host_owned_codex_apps_server: true,
|
||||
connector_id: Some("calendar".to_string()),
|
||||
tool_name: "run_action".to_string(),
|
||||
};
|
||||
@@ -669,6 +696,7 @@ fn codex_apps_tool_question_without_elicitation_omits_always_allow() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
"run_action",
|
||||
Some("Calendar"),
|
||||
mcp_tool_approval_prompt_options(
|
||||
@@ -699,6 +727,7 @@ fn custom_mcp_tool_question_offers_session_remember_and_always_allow() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
"custom_server",
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
"run_action",
|
||||
/*connector_name*/ None,
|
||||
prompt_options(
|
||||
@@ -732,19 +761,26 @@ fn custom_servers_support_session_and_persistent_approval() {
|
||||
};
|
||||
let expected = McpToolApprovalKey {
|
||||
server: "custom_server".to_string(),
|
||||
is_host_owned_codex_apps_server: false,
|
||||
connector_id: None,
|
||||
tool_name: "run_action".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
session_mcp_tool_approval_key(&invocation, /*metadata*/ None, AppToolApproval::Auto),
|
||||
session_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
/*metadata*/ None,
|
||||
AppToolApproval::Auto,
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
),
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
/*metadata*/ None,
|
||||
AppToolApproval::Auto
|
||||
AppToolApproval::Auto,
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
),
|
||||
Some(expected)
|
||||
);
|
||||
@@ -766,16 +802,27 @@ fn codex_apps_connectors_support_persistent_approval() {
|
||||
);
|
||||
let expected = McpToolApprovalKey {
|
||||
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
is_host_owned_codex_apps_server: true,
|
||||
connector_id: Some("calendar".to_string()),
|
||||
tool_name: "calendar/list_events".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
session_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto),
|
||||
session_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
),
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto),
|
||||
persistent_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
@@ -918,7 +965,7 @@ async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() {
|
||||
|
||||
let meta = build_mcp_tool_call_request_meta(
|
||||
&turn_context,
|
||||
"custom_server",
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
"call-custom",
|
||||
/*metadata*/ None,
|
||||
)
|
||||
@@ -941,7 +988,7 @@ async fn mcp_tool_call_request_meta_includes_turn_started_at_unix_ms() {
|
||||
|
||||
let meta = build_mcp_tool_call_request_meta(
|
||||
&turn_context,
|
||||
"custom_server",
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
"call-custom",
|
||||
/*metadata*/ None,
|
||||
)
|
||||
@@ -992,7 +1039,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
|
||||
assert_eq!(
|
||||
build_mcp_tool_call_request_meta(
|
||||
&turn_context,
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
"call_abc123xyz789",
|
||||
Some(&metadata),
|
||||
),
|
||||
@@ -1022,7 +1069,7 @@ async fn codex_apps_tool_call_request_meta_includes_call_id_without_existing_cod
|
||||
assert_eq!(
|
||||
build_mcp_tool_call_request_meta(
|
||||
&turn_context,
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
"call_abc123xyz789",
|
||||
/*metadata*/ None,
|
||||
),
|
||||
@@ -1089,7 +1136,7 @@ fn accepted_elicitation_content_converts_to_request_user_input_response() {
|
||||
fn approval_elicitation_meta_marks_tool_approvals() {
|
||||
assert_eq!(
|
||||
build_mcp_tool_approval_elicitation_meta(
|
||||
"custom_server",
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
/*metadata*/ None,
|
||||
/*tool_params*/ None,
|
||||
/*tool_params_display*/ None,
|
||||
@@ -1107,7 +1154,7 @@ fn approval_elicitation_meta_marks_tool_approvals() {
|
||||
fn approval_elicitation_meta_merges_session_and_always_persist_for_custom_servers() {
|
||||
assert_eq!(
|
||||
build_mcp_tool_approval_elicitation_meta(
|
||||
"custom_server",
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
Some(&approval_metadata(
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
@@ -1319,7 +1366,7 @@ async fn guardian_review_decision_maps_to_mcp_tool_decision() {
|
||||
fn approval_elicitation_meta_includes_connector_source_for_codex_apps() {
|
||||
assert_eq!(
|
||||
build_mcp_tool_approval_elicitation_meta(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
Some(&approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
@@ -1354,7 +1401,7 @@ fn approval_elicitation_meta_includes_connector_source_for_codex_apps() {
|
||||
fn approval_elicitation_meta_merges_session_and_always_persist_with_connector_source() {
|
||||
assert_eq!(
|
||||
build_mcp_tool_approval_elicitation_meta(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_host_owned_codex_apps_server*/ true,
|
||||
Some(&approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
@@ -1697,6 +1744,7 @@ async fn maybe_persist_mcp_tool_approval_reloads_session_config() {
|
||||
std::fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
let key = McpToolApprovalKey {
|
||||
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
is_host_owned_codex_apps_server: true,
|
||||
connector_id: Some("calendar".to_string()),
|
||||
tool_name: "calendar/list_events".to_string(),
|
||||
};
|
||||
@@ -1741,6 +1789,7 @@ async fn maybe_persist_mcp_tool_approval_reloads_session_config_for_custom_serve
|
||||
.expect("seed config");
|
||||
let key = McpToolApprovalKey {
|
||||
server: "docs".to_string(),
|
||||
is_host_owned_codex_apps_server: false,
|
||||
connector_id: None,
|
||||
tool_name: "search".to_string(),
|
||||
};
|
||||
@@ -1796,6 +1845,7 @@ enabled = true
|
||||
session.services.plugins_manager.clear_cache();
|
||||
let key = McpToolApprovalKey {
|
||||
server: "sample".to_string(),
|
||||
is_host_owned_codex_apps_server: false,
|
||||
connector_id: None,
|
||||
tool_name: "search".to_string(),
|
||||
};
|
||||
@@ -1852,6 +1902,7 @@ async fn maybe_persist_mcp_tool_approval_writes_project_config_for_project_serve
|
||||
turn_context.config = Arc::new(config);
|
||||
let key = McpToolApprovalKey {
|
||||
server: "docs".to_string(),
|
||||
is_host_owned_codex_apps_server: false,
|
||||
connector_id: None,
|
||||
tool_name: "search".to_string(),
|
||||
};
|
||||
@@ -2162,9 +2213,13 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
let remembered_key =
|
||||
session_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto)
|
||||
.expect("memory MCP tool should support session approval");
|
||||
let remembered_key = session_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
Some(&metadata),
|
||||
AppToolApproval::Auto,
|
||||
/*is_host_owned_codex_apps_server*/ false,
|
||||
)
|
||||
.expect("memory MCP tool should support session approval");
|
||||
remember_mcp_tool_approval(&session, remembered_key).await;
|
||||
|
||||
let session = Arc::new(session);
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo as McpToolInfo;
|
||||
use codex_mcp::filter_non_codex_apps_mcp_tools_only;
|
||||
use codex_tools::ToolsConfig;
|
||||
@@ -71,7 +70,7 @@ fn filter_codex_apps_mcp_tools(
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
if !tool.is_host_owned_codex_apps() {
|
||||
return false;
|
||||
}
|
||||
let Some(connector_id) = tool.connector_id.as_deref() else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_config::McpServerProvenance;
|
||||
use codex_connectors::metadata::sanitize_name;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
@@ -56,6 +57,11 @@ fn make_mcp_tool(
|
||||
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
server_provenance: if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
McpServerProvenance::HostOwnedCodexApps
|
||||
} else {
|
||||
McpServerProvenance::UserConfigured
|
||||
},
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: tool_namespace,
|
||||
namespace_description: None,
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::context::ContextualUserFragment;
|
||||
use crate::context::PluginInstructions;
|
||||
use crate::plugins::PluginCapabilitySummary;
|
||||
use crate::plugins::render_explicit_plugin_instructions;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ToolInfo;
|
||||
|
||||
pub(crate) fn build_plugin_injections(
|
||||
@@ -29,7 +28,7 @@ pub(crate) fn build_plugin_injections(
|
||||
let available_mcp_servers = mcp_tools
|
||||
.values()
|
||||
.filter(|tool| {
|
||||
tool.server_name != CODEX_APPS_MCP_SERVER_NAME
|
||||
!tool.is_host_owned_codex_apps()
|
||||
&& tool
|
||||
.plugin_display_names
|
||||
.iter()
|
||||
|
||||
@@ -390,6 +390,7 @@ mod tests {
|
||||
fn tool_info(server_name: &str, tool_name: &str, description_prefix: &str) -> ToolInfo {
|
||||
ToolInfo {
|
||||
server_name: server_name.to_string(),
|
||||
server_provenance: Default::default(),
|
||||
callable_name: tool_name.to_string(),
|
||||
callable_namespace: format!("mcp__{server_name}__"),
|
||||
namespace_description: None,
|
||||
|
||||
@@ -60,6 +60,7 @@ fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> r
|
||||
fn mcp_tool_info(tool: rmcp::model::Tool) -> ToolInfo {
|
||||
ToolInfo {
|
||||
server_name: "test_server".to_string(),
|
||||
server_provenance: Default::default(),
|
||||
callable_name: tool.name.to_string(),
|
||||
callable_namespace: "mcp__test_server__".to_string(),
|
||||
namespace_description: None,
|
||||
@@ -78,6 +79,7 @@ fn mcp_tool_info_with_display_name(display_name: &str, tool: rmcp::model::Tool)
|
||||
|
||||
ToolInfo {
|
||||
server_name: "test_server".to_string(),
|
||||
server_provenance: Default::default(),
|
||||
callable_name,
|
||||
callable_namespace,
|
||||
namespace_description: None,
|
||||
@@ -894,6 +896,7 @@ async fn search_tool_description_falls_back_to_connector_name_without_descriptio
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
server_provenance: codex_config::McpServerProvenance::HostOwnedCodexApps,
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
@@ -945,6 +948,7 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
server_provenance: codex_config::McpServerProvenance::HostOwnedCodexApps,
|
||||
callable_name: "_create_event".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
@@ -962,6 +966,7 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
server_provenance: codex_config::McpServerProvenance::HostOwnedCodexApps,
|
||||
callable_name: "_list_events".to_string(),
|
||||
callable_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
namespace_description: None,
|
||||
@@ -979,6 +984,7 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
"mcp__rmcp__echo".to_string(),
|
||||
ToolInfo {
|
||||
server_name: "rmcp".to_string(),
|
||||
server_provenance: Default::default(),
|
||||
callable_name: "echo".to_string(),
|
||||
callable_namespace: "mcp__rmcp__".to_string(),
|
||||
namespace_description: None,
|
||||
|
||||
@@ -238,6 +238,7 @@ async fn run_code_mode_turn_with_rmcp_config(
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -143,6 +143,7 @@ fn insert_rmcp_test_server(config: &mut Config, command: String, approval_mode:
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: Some(approval_mode),
|
||||
|
||||
@@ -315,6 +315,7 @@ fn insert_mcp_server(
|
||||
required: false,
|
||||
supports_parallel_tool_calls: options.supports_parallel_tool_calls,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: options.tool_timeout_sec,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -960,6 +960,7 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> {
|
||||
enabled: true,
|
||||
required: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -1078,6 +1079,7 @@ async fn tool_search_uses_non_app_mcp_server_instructions_as_namespace_descripti
|
||||
enabled: true,
|
||||
required: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -379,6 +379,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result<
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -250,6 +250,7 @@ async fn historical_unavailable_mcp_call_is_exposed_as_placeholder_tool() -> Res
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
@@ -392,6 +392,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()>
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(std::time::Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -491,6 +492,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
@@ -773,6 +775,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
provenance: Default::default(),
|
||||
startup_timeout_sec: Some(std::time::Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
|
||||
Reference in New Issue
Block a user