mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Represent MCP authentication with an enum (#29924)
## Why MCP authentication has distinct OAuth and ChatGPT-session flows. Representing that choice as `use_chatgpt_auth` makes one flow implicit and allows the configuration model to express the distinction only through a boolean. ChatGPT credential forwarding also needs a first-party trust boundary. A configurable `chatgpt_base_url` controls routing, but must not grant an MCP server permission to receive session credentials. This change builds on #29733, where the boolean was introduced. ## What changed - Replace `use_chatgpt_auth` with an `auth` field backed by the exhaustive `McpServerAuth` enum. - Support `auth = "oauth"` and `auth = "chatgpt"`, with OAuth remaining the default. - Trust only the origin derived from the existing hardcoded `CHATGPT_CODEX_BASE_URL` when granting ChatGPT auth to an MCP server. - Keep configured bearer tokens and authorization headers ahead of the selected authentication flow. - Update config writers, schema output, fixtures, and integration-test setup to use the enum. ## Verification Integration coverage exercises the complete streamable HTTP startup path in two independent configurations: - A directly constructed MCP configuration verifies that matching an overridden `chatgpt_base_url` does not grant ChatGPT auth. - A persisted `config.toml` containing an attacker-controlled `chatgpt_base_url` and `auth = "chatgpt"` verifies the same boundary through normal config parsing. Both tests complete MCP initialization and tool listing and assert that the full captured request sequence contains no authorization headers. Separate integration coverage verifies that configured authorization takes precedence over ChatGPT auth.
This commit is contained in:
committed by
GitHub
Unverified
parent
6801941cfe
commit
f8937b7d86
@@ -345,7 +345,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
};
|
||||
|
||||
let new_entry = McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: transport.clone(),
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
|
||||
@@ -17,7 +17,7 @@ use super::ResolvedMcpCatalog;
|
||||
|
||||
fn server(url: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -43,6 +43,7 @@ use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerAuth;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
@@ -872,7 +873,7 @@ fn chatgpt_auth_provider_for_server(
|
||||
) -> Option<SharedAuthProvider> {
|
||||
if !server
|
||||
.configured_config()
|
||||
.is_some_and(|config| config.use_chatgpt_auth)
|
||||
.is_some_and(|config| matches!(&config.auth, McpServerAuth::ChatGpt))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1272,7 +1272,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
(
|
||||
"stdio".to_string(),
|
||||
EffectiveMcpServer::configured(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1299,7 +1299,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
(
|
||||
"http".to_string(),
|
||||
EffectiveMcpServer::configured(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "http://127.0.0.1:1".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -1407,7 +1407,7 @@ fn mcp_init_error_display_prompts_for_github_pat() {
|
||||
let server_name = "github";
|
||||
let entry = McpAuthStatusEntry {
|
||||
config: Some(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://api.githubcopilot.com/mcp/".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -1461,7 +1461,7 @@ fn mcp_init_error_display_reports_generic_errors() {
|
||||
let server_name = "custom";
|
||||
let entry = McpAuthStatusEntry {
|
||||
config: Some(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com".to_string(),
|
||||
bearer_token_env_var: Some("TOKEN".to_string()),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_config::McpServerAuth;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
@@ -141,7 +142,7 @@ where
|
||||
let config = server.configured_config().cloned();
|
||||
let has_runtime_auth = config
|
||||
.as_ref()
|
||||
.is_some_and(|config| config.use_chatgpt_auth)
|
||||
.is_some_and(|config| matches!(&config.auth, McpServerAuth::ChatGpt))
|
||||
&& auth.is_some_and(CodexAuth::uses_codex_backend)
|
||||
&& config.as_ref().is_some_and(|config| {
|
||||
matches!(
|
||||
|
||||
@@ -19,6 +19,7 @@ use std::time::Duration;
|
||||
|
||||
use async_channel::unbounded;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerAuth;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AppToolApproval;
|
||||
@@ -26,6 +27,7 @@ use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_connectors::ConnectorSnapshot;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider::CHATGPT_CODEX_BASE_URL;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::mcp::Resource;
|
||||
use codex_protocol::mcp::ResourceTemplate;
|
||||
@@ -257,23 +259,28 @@ pub fn effective_mcp_servers_from_configured(
|
||||
config: &McpConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<String, EffectiveMcpServer> {
|
||||
let chatgpt_origin = url::Url::parse(&config.chatgpt_base_url)
|
||||
let chatgpt_origin = url::Url::parse(CHATGPT_CODEX_BASE_URL)
|
||||
.ok()
|
||||
.filter(|url| matches!(url.scheme(), "http" | "https"))
|
||||
.map(|url| url.origin());
|
||||
let mut servers = configured_servers
|
||||
.into_iter()
|
||||
.map(|(name, mut server)| {
|
||||
if server.use_chatgpt_auth {
|
||||
let server_origin = match &server.transport {
|
||||
McpServerTransportConfig::StreamableHttp { url, .. } => url::Url::parse(url)
|
||||
.ok()
|
||||
.filter(|url| matches!(url.scheme(), "http" | "https"))
|
||||
.map(|url| url.origin()),
|
||||
McpServerTransportConfig::Stdio { .. } => None,
|
||||
};
|
||||
server.use_chatgpt_auth =
|
||||
server_origin.is_some() && server_origin.as_ref() == chatgpt_origin.as_ref();
|
||||
match server.auth.clone() {
|
||||
McpServerAuth::ChatGpt => {
|
||||
let server_origin = match &server.transport {
|
||||
McpServerTransportConfig::StreamableHttp { url, .. } => {
|
||||
url::Url::parse(url)
|
||||
.ok()
|
||||
.filter(|url| matches!(url.scheme(), "http" | "https"))
|
||||
.map(|url| url.origin())
|
||||
}
|
||||
McpServerTransportConfig::Stdio { .. } => None,
|
||||
};
|
||||
if server_origin.as_ref() != chatgpt_origin.as_ref() {
|
||||
server.auth = McpServerAuth::OAuth;
|
||||
}
|
||||
}
|
||||
McpServerAuth::OAuth => {}
|
||||
}
|
||||
(name, EffectiveMcpServer::configured(server))
|
||||
})
|
||||
@@ -474,7 +481,7 @@ pub fn codex_apps_mcp_server_config(
|
||||
mcp_server_config_for_url(
|
||||
codex_apps_mcp_url_for_base_url(chatgpt_base_url),
|
||||
apps_mcp_product_sku,
|
||||
/*use_chatgpt_auth*/ true,
|
||||
McpServerAuth::ChatGpt,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -492,14 +499,14 @@ pub fn hosted_plugin_runtime_mcp_server_config(
|
||||
mcp_server_config_for_url(
|
||||
format!("{base_url}/ps/mcp"),
|
||||
apps_mcp_product_sku,
|
||||
/*use_chatgpt_auth*/ true,
|
||||
McpServerAuth::ChatGpt,
|
||||
)
|
||||
}
|
||||
|
||||
fn mcp_server_config_for_url(
|
||||
url: String,
|
||||
apps_mcp_product_sku: Option<&str>,
|
||||
use_chatgpt_auth: bool,
|
||||
auth_mode: McpServerAuth,
|
||||
) -> McpServerConfig {
|
||||
let http_headers = apps_mcp_product_sku.map(|product_sku| {
|
||||
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())])
|
||||
@@ -512,7 +519,7 @@ fn mcp_server_config_for_url(
|
||||
http_headers,
|
||||
env_http_headers: None,
|
||||
},
|
||||
use_chatgpt_auth,
|
||||
auth: auth_mode,
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
|
||||
@@ -296,7 +296,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
|
||||
catalog.register(McpServerRegistration::from_config(
|
||||
"sample".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://user.example/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -322,7 +322,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
|
||||
catalog.register(McpServerRegistration::from_config(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://docs.example/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -32,7 +32,7 @@ fn stdio_server(
|
||||
env_vars: Vec<McpServerEnvVar>,
|
||||
) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: command.to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -67,7 +67,7 @@ fn declared_placement_preserves_local_plugin_normalization() {
|
||||
Vec::new(),
|
||||
);
|
||||
let expected_http = McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -104,7 +104,7 @@ mod tests {
|
||||
|
||||
fn stdio_server(environment_id: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -131,7 +131,7 @@ mod tests {
|
||||
|
||||
fn http_server(environment_id: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "http://127.0.0.1:1".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -115,6 +115,7 @@ pub use mcp_edit::ConfigEditsBuilder;
|
||||
pub use mcp_edit::load_global_mcp_servers;
|
||||
pub use mcp_types::AppToolApproval;
|
||||
pub use mcp_types::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
||||
pub use mcp_types::McpServerAuth;
|
||||
pub use mcp_types::McpServerConfig;
|
||||
pub use mcp_types::McpServerDisabledReason;
|
||||
pub use mcp_types::McpServerEnvVar;
|
||||
|
||||
@@ -13,6 +13,7 @@ use toml_edit::value;
|
||||
|
||||
use crate::AppToolApproval;
|
||||
use crate::CONFIG_TOML_FILE;
|
||||
use crate::McpServerAuth;
|
||||
use crate::McpServerConfig;
|
||||
use crate::McpServerEnvVar;
|
||||
use crate::McpServerTransportConfig;
|
||||
@@ -172,8 +173,8 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem {
|
||||
}
|
||||
}
|
||||
|
||||
if config.use_chatgpt_auth {
|
||||
entry["use_chatgpt_auth"] = value(true);
|
||||
if matches!(&config.auth, McpServerAuth::ChatGpt) {
|
||||
entry["auth"] = value("chatgpt");
|
||||
}
|
||||
if !config.enabled {
|
||||
entry["enabled"] = value(false);
|
||||
|
||||
@@ -16,7 +16,7 @@ async fn replace_mcp_servers_serializes_per_tool_approval_overrides() -> anyhow:
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -96,7 +96,7 @@ async fn replace_mcp_servers_serializes_oauth_client_id() -> anyhow::Result<()>
|
||||
let servers = BTreeMap::from([(
|
||||
"maas_outlook".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -126,16 +126,40 @@ pub struct McpServerOAuthConfig {
|
||||
pub client_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Authentication flow Codex attempts after resolving an HTTP MCP server's
|
||||
/// configured bearer token and authorization headers, which always take
|
||||
/// precedence. ChatGPT authentication falls back to stored OAuth credentials
|
||||
/// when its session provider is unavailable; both modes ultimately fall back
|
||||
/// to an unauthenticated connection.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum McpServerAuth {
|
||||
/// Use stored MCP OAuth credentials when available. Starting an OAuth login
|
||||
/// is a separate operation.
|
||||
#[default]
|
||||
#[serde(rename = "oauth")]
|
||||
OAuth,
|
||||
/// Use the current ChatGPT session for servers on the trusted first-party
|
||||
/// ChatGPT origin. If no ChatGPT session provider is available, startup can
|
||||
/// still fall back to stored OAuth credentials.
|
||||
#[serde(rename = "chatgpt")]
|
||||
ChatGpt,
|
||||
}
|
||||
|
||||
impl McpServerAuth {
|
||||
fn is_default(&self) -> bool {
|
||||
self == &Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct McpServerConfig {
|
||||
#[serde(flatten)]
|
||||
pub transport: McpServerTransportConfig,
|
||||
|
||||
/// When `true`, request authentication with the current ChatGPT session. Codex honors this
|
||||
/// only when the server URL has the same origin as the configured ChatGPT base URL and no
|
||||
/// configured bearer token or authorization header resolves.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub use_chatgpt_auth: bool,
|
||||
/// Authentication flow to use when no configured authorization resolves.
|
||||
#[serde(default, skip_serializing_if = "McpServerAuth::is_default")]
|
||||
pub auth: McpServerAuth,
|
||||
|
||||
/// Effective environment id for where Codex should start this MCP server.
|
||||
pub environment_id: String,
|
||||
@@ -245,7 +269,7 @@ pub struct RawMcpServerConfig {
|
||||
#[serde(default)]
|
||||
pub environment_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub use_chatgpt_auth: Option<bool>,
|
||||
pub auth: Option<McpServerAuth>,
|
||||
#[serde(default)]
|
||||
pub startup_timeout_sec: Option<f64>,
|
||||
#[serde(default)]
|
||||
@@ -294,7 +318,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
|
||||
bearer_token,
|
||||
bearer_token_env_var,
|
||||
environment_id,
|
||||
use_chatgpt_auth,
|
||||
auth,
|
||||
startup_timeout_sec,
|
||||
startup_timeout_ms,
|
||||
tool_timeout_sec,
|
||||
@@ -338,7 +362,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
|
||||
throw_if_set("stdio", "env_http_headers", env_http_headers.as_ref())?;
|
||||
throw_if_set("stdio", "oauth", oauth.as_ref())?;
|
||||
throw_if_set("stdio", "oauth_resource", oauth_resource.as_ref())?;
|
||||
throw_if_set("stdio", "use_chatgpt_auth", use_chatgpt_auth.as_ref())?;
|
||||
throw_if_set("stdio", "auth", auth.as_ref())?;
|
||||
let env_vars = env_vars.unwrap_or_default();
|
||||
for env_var in &env_vars {
|
||||
env_var.validate_source()?;
|
||||
@@ -371,7 +395,7 @@ impl TryFrom<RawMcpServerConfig> for McpServerConfig {
|
||||
|
||||
Ok(Self {
|
||||
transport,
|
||||
use_chatgpt_auth: use_chatgpt_auth.unwrap_or_default(),
|
||||
auth: auth.unwrap_or_default(),
|
||||
environment_id,
|
||||
startup_timeout_sec,
|
||||
tool_timeout_sec,
|
||||
|
||||
@@ -426,7 +426,7 @@ fn deserialize_ignores_unknown_server_fields() {
|
||||
assert_eq!(
|
||||
cfg,
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: vec![],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// definitions that do not contain business logic.
|
||||
|
||||
pub use crate::mcp_types::AppToolApproval;
|
||||
pub use crate::mcp_types::McpServerAuth;
|
||||
pub use crate::mcp_types::McpServerConfig;
|
||||
pub use crate::mcp_types::McpServerDisabledReason;
|
||||
pub use crate::mcp_types::McpServerEnvVar;
|
||||
|
||||
@@ -677,7 +677,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
mcp_servers: HashMap::from([(
|
||||
"sample".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://sample.example/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -769,7 +769,7 @@ enabled = true
|
||||
HashMap::from([(
|
||||
"counter".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://sample.example/counter/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -1664,7 +1664,7 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
|
||||
HashMap::from([(
|
||||
"custom".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://custom.example/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -1845,7 +1845,7 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
|
||||
HashMap::from([(
|
||||
"default".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://default.example/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -2096,7 +2096,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
let connector = |id: &str| AppConnectorId(id.to_string());
|
||||
let app = |name: &str, connector_id: &str| app_declaration(name, connector_id);
|
||||
let http_server = |url: &str| McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -1384,6 +1384,25 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerAuth": {
|
||||
"description": "Authentication flow Codex attempts after resolving an HTTP MCP server's configured bearer token and authorization headers, which always take precedence. ChatGPT authentication falls back to stored OAuth credentials when its session provider is unavailable; both modes ultimately fall back to an unauthenticated connection.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Use stored MCP OAuth credentials when available. Starting an OAuth login is a separate operation.",
|
||||
"enum": [
|
||||
"oauth"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Use the current ChatGPT session for servers on the trusted first-party ChatGPT origin. If no ChatGPT session provider is available, startup can still fall back to stored OAuth credentials.",
|
||||
"enum": [
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpServerEnvVar": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -2427,6 +2446,14 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"auth": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerAuth"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"bearer_token_env_var": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2555,10 +2582,6 @@
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_chatgpt_auth": {
|
||||
"default": null,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -114,7 +114,7 @@ use tempfile::TempDir;
|
||||
|
||||
fn stdio_mcp(command: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: command.to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -141,7 +141,7 @@ fn stdio_mcp(command: &str) -> McpServerConfig {
|
||||
|
||||
fn http_mcp(url: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -5568,7 +5568,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
|
||||
servers.insert(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: vec!["hello".to_string()],
|
||||
@@ -5926,7 +5926,7 @@ async fn replace_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: vec!["--verbose".to_string()],
|
||||
@@ -6006,7 +6006,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6062,7 +6062,7 @@ async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()>
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6121,7 +6121,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6177,7 +6177,7 @@ async fn replace_mcp_servers_streamable_http_serializes_bearer_token() -> anyhow
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
|
||||
@@ -6245,7 +6245,7 @@ async fn replace_mcp_servers_streamable_http_serializes_custom_headers() -> anyh
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
|
||||
@@ -6328,7 +6328,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
let mut servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
|
||||
@@ -6367,7 +6367,7 @@ async fn replace_mcp_servers_streamable_http_removes_optional_sections() -> anyh
|
||||
servers.insert(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -6435,7 +6435,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
|
||||
@@ -6464,7 +6464,7 @@ async fn replace_mcp_servers_streamable_http_isolates_headers_between_servers()
|
||||
(
|
||||
"logs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "logs-server".to_string(),
|
||||
args: vec!["--follow".to_string()],
|
||||
@@ -6553,7 +6553,7 @@ async fn replace_mcp_servers_serializes_disabled_flag() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6604,7 +6604,7 @@ async fn replace_mcp_servers_serializes_required_flag() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6655,7 +6655,7 @@ async fn replace_mcp_servers_serializes_tool_filters() -> anyhow::Result<()> {
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -6711,7 +6711,7 @@ async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyh
|
||||
let servers = BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com/mcp".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::McpServerAuth;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_config::types::McpServerToolConfig;
|
||||
@@ -95,8 +96,8 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
|
||||
}
|
||||
}
|
||||
|
||||
if config.use_chatgpt_auth {
|
||||
entry["use_chatgpt_auth"] = value(true);
|
||||
if matches!(&config.auth, McpServerAuth::ChatGpt) {
|
||||
entry["auth"] = value("chatgpt");
|
||||
}
|
||||
if !config.enabled {
|
||||
entry["enabled"] = value(false);
|
||||
|
||||
@@ -873,7 +873,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
servers.insert(
|
||||
"stdio".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: vec!["--flag".to_string()],
|
||||
@@ -908,7 +908,7 @@ fn blocking_replace_mcp_servers_round_trips() {
|
||||
servers.insert(
|
||||
"http".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "https://example.com".to_string(),
|
||||
bearer_token_env_var: Some("TOKEN".to_string()),
|
||||
@@ -983,7 +983,7 @@ fn blocking_replace_mcp_servers_serializes_tool_approval_overrides() {
|
||||
servers.insert(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1044,7 +1044,7 @@ foo = { command = "cmd" }
|
||||
servers.insert(
|
||||
"foo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1095,7 +1095,7 @@ foo = { command = "cmd" } # keep me
|
||||
servers.insert(
|
||||
"foo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1145,7 +1145,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
|
||||
servers.insert(
|
||||
"foo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -1196,7 +1196,7 @@ foo = { command = "cmd" }
|
||||
servers.insert(
|
||||
"foo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "cmd".to_string(),
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -357,7 +357,7 @@ fn mcp_dependency_to_server_config(
|
||||
.as_ref()
|
||||
.ok_or_else(|| "missing url for streamable_http dependency".to_string())?;
|
||||
return Ok(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.clone(),
|
||||
bearer_token_env_var: None,
|
||||
@@ -387,7 +387,7 @@ fn mcp_dependency_to_server_config(
|
||||
.as_ref()
|
||||
.ok_or_else(|| "missing command for stdio dependency".to_string())?;
|
||||
return Ok(McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: command.clone(),
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -395,7 +395,7 @@ async fn run_code_mode_turn_with_rmcp_config(
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -184,7 +184,7 @@ fn insert_rmcp_test_server(config: &mut Config, command: String, approval_mode:
|
||||
servers.insert(
|
||||
RMCP_SERVER.to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn refresh_shuts_down_superseded_mcp_stdio_server() -> anyhow::Result<()>
|
||||
servers.insert(
|
||||
"refresh_cleanup".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -17,6 +17,7 @@ use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use codex_config::types::McpServerAuth;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
@@ -268,7 +269,7 @@ fn copy_binary_to_remote_env(
|
||||
|
||||
struct TestMcpServerOptions {
|
||||
environment_id: String,
|
||||
use_chatgpt_auth: bool,
|
||||
auth: McpServerAuth,
|
||||
supports_parallel_tool_calls: bool,
|
||||
tool_timeout_sec: Option<Duration>,
|
||||
}
|
||||
@@ -277,7 +278,7 @@ impl Default for TestMcpServerOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
||||
use_chatgpt_auth: false,
|
||||
auth: McpServerAuth::default(),
|
||||
supports_parallel_tool_calls: false,
|
||||
tool_timeout_sec: None,
|
||||
}
|
||||
@@ -318,7 +319,7 @@ fn insert_mcp_server(
|
||||
server_name.to_string(),
|
||||
McpServerConfig {
|
||||
transport,
|
||||
use_chatgpt_auth: options.use_chatgpt_auth,
|
||||
auth: options.auth,
|
||||
environment_id: options.environment_id,
|
||||
enabled: true,
|
||||
required: false,
|
||||
@@ -1231,7 +1232,7 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res
|
||||
stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()),
|
||||
TestMcpServerOptions {
|
||||
environment_id: remote_aware_environment_id(),
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
supports_parallel_tool_calls: true,
|
||||
tool_timeout_sec: Some(Duration::from_secs(2)),
|
||||
},
|
||||
@@ -2304,62 +2305,20 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn streamable_http_chatgpt_auth_respects_configured_authorization() -> anyhow::Result<()> {
|
||||
async fn streamable_http_configured_auth_precedes_chatgpt_auth() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let Some(chatgpt_auth_server) =
|
||||
start_streamable_http_test_server("chatgpt-auth", Some("Access Token")).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let chatgpt_auth_url = chatgpt_auth_server.url().to_string();
|
||||
let chatgpt_base_url = chatgpt_auth_url
|
||||
.strip_suffix("/mcp")
|
||||
.expect("test MCP URL should end in /mcp")
|
||||
.to_string();
|
||||
|
||||
let chatgpt_auth_fixture = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
config.chatgpt_base_url = chatgpt_base_url;
|
||||
insert_mcp_server(
|
||||
config,
|
||||
"chatgpt_auth",
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
url: chatgpt_auth_url,
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
TestMcpServerOptions {
|
||||
environment_id: remote_aware_environment_id(),
|
||||
use_chatgpt_auth: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
})
|
||||
.build_with_remote_env(&server)
|
||||
.await?;
|
||||
wait_for_mcp_server(&chatgpt_auth_fixture.codex, "chatgpt_auth").await?;
|
||||
drop(chatgpt_auth_fixture);
|
||||
chatgpt_auth_server.shutdown().await;
|
||||
|
||||
let Some(configured_auth_server) =
|
||||
start_streamable_http_test_server("configured-auth", Some("configured-token")).await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let configured_auth_url = configured_auth_server.url().to_string();
|
||||
let configured_auth_base_url = configured_auth_url
|
||||
.strip_suffix("/mcp")
|
||||
.expect("test MCP URL should end in /mcp")
|
||||
.to_string();
|
||||
|
||||
let configured_auth_fixture = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
config.chatgpt_base_url = configured_auth_base_url;
|
||||
insert_mcp_server(
|
||||
config,
|
||||
"configured_auth",
|
||||
@@ -2374,12 +2333,12 @@ async fn streamable_http_chatgpt_auth_respects_configured_authorization() -> any
|
||||
},
|
||||
TestMcpServerOptions {
|
||||
environment_id: remote_aware_environment_id(),
|
||||
use_chatgpt_auth: true,
|
||||
auth: McpServerAuth::ChatGpt,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
})
|
||||
.build_with_remote_env(&server)
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
|
||||
wait_for_mcp_server(&configured_auth_fixture.codex, "configured_auth").await?;
|
||||
@@ -2390,19 +2349,19 @@ async fn streamable_http_chatgpt_auth_respects_configured_authorization() -> any
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn streamable_http_chatgpt_auth_is_not_sent_to_another_origin() -> anyhow::Result<()> {
|
||||
async fn streamable_http_chatgpt_auth_is_not_sent_to_configured_origin() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let untrusted_server = MockServer::start().await;
|
||||
let untrusted_apps = AppsTestServer::mount(&untrusted_server).await?;
|
||||
let untrusted_mcp_url = format!("{}/api/codex/apps", untrusted_apps.chatgpt_base_url);
|
||||
let trusted_chatgpt_base_url = server.uri();
|
||||
let untrusted_chatgpt_base_url = untrusted_apps.chatgpt_base_url;
|
||||
|
||||
let fixture = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
config.chatgpt_base_url = trusted_chatgpt_base_url;
|
||||
config.chatgpt_base_url = untrusted_chatgpt_base_url;
|
||||
insert_mcp_server(
|
||||
config,
|
||||
"untrusted_origin",
|
||||
@@ -2413,7 +2372,7 @@ async fn streamable_http_chatgpt_auth_is_not_sent_to_another_origin() -> anyhow:
|
||||
env_http_headers: None,
|
||||
},
|
||||
TestMcpServerOptions {
|
||||
use_chatgpt_auth: true,
|
||||
auth: McpServerAuth::ChatGpt,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -2452,6 +2411,67 @@ async fn streamable_http_chatgpt_auth_is_not_sent_to_another_origin() -> anyhow:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn configured_chatgpt_base_url_does_not_grant_mcp_chatgpt_auth() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let untrusted_server = MockServer::start().await;
|
||||
let untrusted_apps = AppsTestServer::mount(&untrusted_server).await?;
|
||||
let untrusted_mcp_url = format!("{}/api/codex/apps", untrusted_apps.chatgpt_base_url);
|
||||
let untrusted_chatgpt_base_url = untrusted_apps.chatgpt_base_url;
|
||||
|
||||
let fixture = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_pre_build_hook(move |codex_home| {
|
||||
fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
chatgpt_base_url = "{untrusted_chatgpt_base_url}"
|
||||
|
||||
[mcp_servers.untrusted_origin]
|
||||
url = "{untrusted_mcp_url}"
|
||||
auth = "chatgpt"
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.expect("write attacker-controlled MCP config");
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
wait_for_mcp_server(&fixture.codex, "untrusted_origin").await?;
|
||||
let observed_requests = untrusted_server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should capture MCP startup requests")
|
||||
.into_iter()
|
||||
.filter(|request| request.url.path() == "/api/codex/apps")
|
||||
.filter_map(|request| {
|
||||
let body: Value = serde_json::from_slice(&request.body).ok()?;
|
||||
let method = body.get("method")?.as_str()?.to_string();
|
||||
let authorization = request
|
||||
.headers
|
||||
.get("authorization")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::to_string);
|
||||
Some((method, authorization))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
observed_requests,
|
||||
vec![
|
||||
("initialize".to_string(), None),
|
||||
("notifications/initialized".to_string(), None),
|
||||
("tools/list".to_string(), None),
|
||||
],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This test writes to a fallback credentials file in CODEX_HOME.
|
||||
/// Ideally, we wouldn't need to serialize the test but it's much more cumbersome to wire CODEX_HOME through the code.
|
||||
#[test]
|
||||
|
||||
@@ -1090,7 +1090,7 @@ async fn tool_search_indexes_only_enabled_non_app_mcp_tools() -> Result<()> {
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
@@ -1217,7 +1217,7 @@ async fn tool_search_surfaced_mcp_tool_errors_are_returned_to_model() -> Result<
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
@@ -1366,7 +1366,7 @@ async fn tool_search_uses_non_app_mcp_server_instructions_as_namespace_descripti
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -657,7 +657,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result<
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -276,7 +276,7 @@ async fn token_budget_context_injects_plain_thread_hint_text() -> Result<()> {
|
||||
servers.insert(
|
||||
"notes".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -383,7 +383,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()>
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
codex_config::types::McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: codex_config::types::McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
@@ -482,7 +482,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
@@ -775,7 +775,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> {
|
||||
servers.insert(
|
||||
server_name.to_string(),
|
||||
codex_config::types::McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: codex_config::types::McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -166,7 +166,7 @@ async fn reads_declared_config_only_through_executor_file_system() {
|
||||
vec![(
|
||||
"demo".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "demo-mcp".to_string(),
|
||||
args: Vec::new(),
|
||||
@@ -222,7 +222,7 @@ async fn reads_manifest_object_config_without_executor_file_system_access() {
|
||||
vec![(
|
||||
"counter".to_string(),
|
||||
McpServerConfig {
|
||||
use_chatgpt_auth: false,
|
||||
auth: Default::default(),
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "counter-mcp".to_string(),
|
||||
args: Vec::new(),
|
||||
|
||||
@@ -8,6 +8,7 @@ pub use auth::auth_provider_from_auth;
|
||||
pub use auth::unauthenticated_auth_provider;
|
||||
pub use bearer_auth_provider::BearerAuthProvider;
|
||||
pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider;
|
||||
pub use codex_model_provider_info::CHATGPT_CODEX_BASE_URL;
|
||||
pub use codex_protocol::account::ProviderAccount;
|
||||
pub use provider::ModelProvider;
|
||||
pub use provider::ModelProviderFuture;
|
||||
|
||||
Reference in New Issue
Block a user