mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
f8937b7d86
## 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.
149 lines
4.5 KiB
Rust
149 lines
4.5 KiB
Rust
use super::*;
|
|
use crate::McpServerOAuthConfig;
|
|
use crate::McpServerToolConfig;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::HashMap;
|
|
use std::time::SystemTime;
|
|
use std::time::UNIX_EPOCH;
|
|
|
|
#[tokio::test]
|
|
async fn replace_mcp_servers_serializes_per_tool_approval_overrides() -> anyhow::Result<()> {
|
|
let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
|
let codex_home = std::env::temp_dir().join(format!(
|
|
"codex-config-mcp-edit-test-{}-{unique_suffix}",
|
|
std::process::id()
|
|
));
|
|
let servers = BTreeMap::from([(
|
|
"docs".to_string(),
|
|
McpServerConfig {
|
|
auth: Default::default(),
|
|
transport: McpServerTransportConfig::Stdio {
|
|
command: "docs-server".to_string(),
|
|
args: Vec::new(),
|
|
env: None,
|
|
env_vars: Vec::new(),
|
|
cwd: None,
|
|
},
|
|
environment_id: crate::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
|
enabled: true,
|
|
required: false,
|
|
supports_parallel_tool_calls: true,
|
|
disabled_reason: None,
|
|
startup_timeout_sec: None,
|
|
tool_timeout_sec: None,
|
|
default_tools_approval_mode: Some(AppToolApproval::Auto),
|
|
enabled_tools: None,
|
|
disabled_tools: None,
|
|
scopes: None,
|
|
oauth: None,
|
|
oauth_resource: None,
|
|
tools: HashMap::from([
|
|
(
|
|
"search".to_string(),
|
|
McpServerToolConfig {
|
|
approval_mode: Some(AppToolApproval::Approve),
|
|
},
|
|
),
|
|
(
|
|
"read".to_string(),
|
|
McpServerToolConfig {
|
|
approval_mode: Some(AppToolApproval::Prompt),
|
|
},
|
|
),
|
|
]),
|
|
},
|
|
)]);
|
|
|
|
ConfigEditsBuilder::new(&codex_home)
|
|
.replace_mcp_servers(&servers)
|
|
.apply()
|
|
.await?;
|
|
|
|
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
|
let serialized = std::fs::read_to_string(&config_path)?;
|
|
assert_eq!(
|
|
serialized,
|
|
r#"[mcp_servers.docs]
|
|
command = "docs-server"
|
|
supports_parallel_tool_calls = true
|
|
default_tools_approval_mode = "auto"
|
|
|
|
[mcp_servers.docs.tools]
|
|
|
|
[mcp_servers.docs.tools.read]
|
|
approval_mode = "prompt"
|
|
|
|
[mcp_servers.docs.tools.search]
|
|
approval_mode = "approve"
|
|
"#
|
|
);
|
|
|
|
let loaded = load_global_mcp_servers(&codex_home).await?;
|
|
assert_eq!(loaded, servers);
|
|
|
|
std::fs::remove_dir_all(&codex_home)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn replace_mcp_servers_serializes_oauth_client_id() -> anyhow::Result<()> {
|
|
let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
|
let codex_home = std::env::temp_dir().join(format!(
|
|
"codex-config-mcp-oauth-edit-test-{}-{unique_suffix}",
|
|
std::process::id()
|
|
));
|
|
let servers = BTreeMap::from([(
|
|
"maas_outlook".to_string(),
|
|
McpServerConfig {
|
|
auth: Default::default(),
|
|
transport: McpServerTransportConfig::StreamableHttp {
|
|
url: "https://example.com/mcp".to_string(),
|
|
bearer_token_env_var: None,
|
|
http_headers: None,
|
|
env_http_headers: None,
|
|
},
|
|
environment_id: crate::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
|
|
enabled: true,
|
|
required: false,
|
|
supports_parallel_tool_calls: false,
|
|
disabled_reason: None,
|
|
startup_timeout_sec: None,
|
|
tool_timeout_sec: None,
|
|
default_tools_approval_mode: None,
|
|
enabled_tools: None,
|
|
disabled_tools: None,
|
|
scopes: None,
|
|
oauth: Some(McpServerOAuthConfig {
|
|
client_id: Some("eci-prd-pub-codex-123".to_string()),
|
|
}),
|
|
oauth_resource: None,
|
|
tools: HashMap::new(),
|
|
},
|
|
)]);
|
|
|
|
ConfigEditsBuilder::new(&codex_home)
|
|
.replace_mcp_servers(&servers)
|
|
.apply()
|
|
.await?;
|
|
|
|
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
|
let serialized = std::fs::read_to_string(&config_path)?;
|
|
assert_eq!(
|
|
serialized,
|
|
r#"[mcp_servers.maas_outlook]
|
|
url = "https://example.com/mcp"
|
|
|
|
[mcp_servers.maas_outlook.oauth]
|
|
client_id = "eci-prd-pub-codex-123"
|
|
"#
|
|
);
|
|
|
|
let loaded = load_global_mcp_servers(&codex_home).await?;
|
|
assert_eq!(loaded, servers);
|
|
|
|
std::fs::remove_dir_all(&codex_home)?;
|
|
|
|
Ok(())
|
|
}
|