mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
fix: forward originator to Codex Apps MCP (#31481)
## Summary - Forward Codexs canonical `originator` header on ChatGPT-hosted Apps and plugin-runtime MCP requests. - Preserve the configured `X-OpenAI-Product-Sku` header. - Cover originator-only and originator-plus-SKU configurations. ## Why Sites project creation is logged downstream of Apps MCP. Production validation found `CODEX_UNKNOWN_DEFAULT` project-created threads that matched `codex_surface=desktop_app` and `originator=Codex Desktop` in `fact_codex_cli` ([query](https://kepler.gateway.data-1.internal.api.openai.org/permalink/H_mVoVPqLQ0)). The hosted Apps MCP configuration forwarded the product SKU but not Codexs canonical originator, so codex-backend could not derive `CODEX_DESKTOP_APP` for those tool calls. ## Validation - `just fmt` - `CARGO_HOME=/private/tmp/codex-cargo-home CARGO_TARGET_DIR=/private/tmp/codex-target just test -p codex-mcp` (106 passed) - `CARGO_HOME=/private/tmp/codex-cargo-home CARGO_TARGET_DIR=/private/tmp/codex-target just fix -p codex-mcp`
This commit is contained in:
1 parent
cf6d8ad921
commit
b58952b0fa
13 files changed
+208
-20
No files matched your search
@@ -493,10 +493,12 @@ fn codex_apps_mcp_url_for_base_url(base_url: &str) -> String {
|
||||
pub fn codex_apps_mcp_server_config(
|
||||
chatgpt_base_url: &str,
|
||||
apps_mcp_product_sku: Option<&str>,
|
||||
originator: Option<&str>,
|
||||
) -> McpServerConfig {
|
||||
mcp_server_config_for_url(
|
||||
codex_apps_mcp_url_for_base_url(chatgpt_base_url),
|
||||
apps_mcp_product_sku,
|
||||
originator,
|
||||
McpServerAuth::ChatGpt,
|
||||
)
|
||||
}
|
||||
@@ -505,6 +507,7 @@ pub fn codex_apps_mcp_server_config(
|
||||
pub fn hosted_plugin_runtime_mcp_server_config(
|
||||
chatgpt_base_url: &str,
|
||||
apps_mcp_product_sku: Option<&str>,
|
||||
originator: Option<&str>,
|
||||
) -> McpServerConfig {
|
||||
let base_url = normalize_codex_apps_base_url(chatgpt_base_url);
|
||||
let base_url = if base_url.contains("/backend-api") || base_url.contains("/api/codex") {
|
||||
@@ -515,6 +518,7 @@ pub fn hosted_plugin_runtime_mcp_server_config(
|
||||
mcp_server_config_for_url(
|
||||
format!("{base_url}/ps/mcp"),
|
||||
apps_mcp_product_sku,
|
||||
originator,
|
||||
McpServerAuth::ChatGpt,
|
||||
)
|
||||
}
|
||||
@@ -522,19 +526,21 @@ pub fn hosted_plugin_runtime_mcp_server_config(
|
||||
fn mcp_server_config_for_url(
|
||||
url: String,
|
||||
apps_mcp_product_sku: Option<&str>,
|
||||
originator: Option<&str>,
|
||||
auth_mode: McpServerAuth,
|
||||
) -> McpServerConfig {
|
||||
let product_sku = apps_mcp_product_sku.unwrap_or(DEFAULT_CODEX_APPS_MCP_PRODUCT_SKU);
|
||||
let http_headers = Some(HashMap::from([(
|
||||
"X-OpenAI-Product-Sku".to_string(),
|
||||
product_sku.to_string(),
|
||||
)]));
|
||||
let mut http_headers =
|
||||
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())]);
|
||||
if let Some(originator) = originator {
|
||||
http_headers.insert("originator".to_string(), originator.to_string());
|
||||
}
|
||||
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(),
|
||||
http_headers,
|
||||
http_headers: Some(http_headers),
|
||||
env_http_headers: None,
|
||||
},
|
||||
auth: auth_mode,
|
||||
|
||||
@@ -130,7 +130,11 @@ fn tool_plugin_provenance_collects_app_and_mcp_sources() {
|
||||
"alpha".to_string(),
|
||||
McpPluginAttribution::new("alpha@test".to_string(), "alpha-plugin".to_string()),
|
||||
/*plugin_order*/ 0,
|
||||
codex_apps_mcp_server_config("https://alpha.example", /*apps_mcp_product_sku*/ None),
|
||||
codex_apps_mcp_server_config(
|
||||
"https://alpha.example",
|
||||
/*apps_mcp_product_sku*/ None,
|
||||
/*originator*/ None,
|
||||
),
|
||||
));
|
||||
config.mcp_server_catalog = catalog.build();
|
||||
config.connector_snapshot =
|
||||
@@ -197,7 +201,11 @@ fn selected_mcp_attribution_does_not_join_an_unrelated_local_summary() {
|
||||
"Executor GitHub".to_string(),
|
||||
),
|
||||
/*selection_order*/ 0,
|
||||
codex_apps_mcp_server_config("https://github.example", /*apps_mcp_product_sku*/ None),
|
||||
codex_apps_mcp_server_config(
|
||||
"https://github.example",
|
||||
/*apps_mcp_product_sku*/ None,
|
||||
/*originator*/ None,
|
||||
),
|
||||
));
|
||||
config.mcp_server_catalog = catalog.build();
|
||||
config.connector_snapshot =
|
||||
@@ -252,8 +260,11 @@ fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() {
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_config_uses_legacy_codex_apps_path() {
|
||||
let config =
|
||||
codex_apps_mcp_server_config("https://chatgpt.com", /*apps_mcp_product_sku*/ None);
|
||||
let config = codex_apps_mcp_server_config(
|
||||
"https://chatgpt.com",
|
||||
/*apps_mcp_product_sku*/ None,
|
||||
/*originator*/ None,
|
||||
);
|
||||
let url = match &config.transport {
|
||||
McpServerTransportConfig::StreamableHttp { url, .. } => url,
|
||||
_ => panic!("expected streamable http transport for codex apps"),
|
||||
@@ -262,10 +273,41 @@ 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_forwards_thread_originator_header() {
|
||||
let config = codex_apps_mcp_server_config(
|
||||
"https://chatgpt.com",
|
||||
/*apps_mcp_product_sku*/ None,
|
||||
Some("thread_originator"),
|
||||
);
|
||||
|
||||
match &config.transport {
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(
|
||||
http_headers,
|
||||
&Some(HashMap::from([
|
||||
("originator".to_string(), "thread_originator".to_string()),
|
||||
("X-OpenAI-Product-Sku".to_string(), "codex".to_string()),
|
||||
]))
|
||||
);
|
||||
assert!(env_http_headers.is_none());
|
||||
}
|
||||
other => panic!("expected streamable http transport, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_config_sets_product_sku_header() {
|
||||
for (configured_product_sku, expected_product_sku) in [(None, "codex"), (Some("tpp"), "tpp")] {
|
||||
let config = codex_apps_mcp_server_config("https://chatgpt.com", configured_product_sku);
|
||||
let config = codex_apps_mcp_server_config(
|
||||
"https://chatgpt.com",
|
||||
configured_product_sku,
|
||||
/*originator*/ None,
|
||||
);
|
||||
|
||||
match &config.transport {
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
@@ -287,6 +329,33 @@ fn codex_apps_server_config_sets_product_sku_header() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_config_forwards_originator_and_configured_product_sku_headers() {
|
||||
let config = codex_apps_mcp_server_config(
|
||||
"https://chatgpt.com",
|
||||
Some("tpp"),
|
||||
Some("thread_originator"),
|
||||
);
|
||||
|
||||
match &config.transport {
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(
|
||||
http_headers,
|
||||
&Some(HashMap::from([
|
||||
("originator".to_string(), "thread_originator".to_string()),
|
||||
("X-OpenAI-Product-Sku".to_string(), "tpp".to_string()),
|
||||
]))
|
||||
);
|
||||
assert!(env_http_headers.is_none());
|
||||
}
|
||||
other => panic!("expected streamable http transport, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn effective_mcp_servers_preserve_runtime_servers() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
@@ -352,6 +421,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
|
||||
codex_apps_mcp_server_config(
|
||||
&config.chatgpt_base_url,
|
||||
config.apps_mcp_product_sku.as_deref(),
|
||||
/*originator*/ None,
|
||||
),
|
||||
));
|
||||
config.mcp_server_catalog = catalog.build();
|
||||
|
||||
Reference in new issue
Block a user