Merge branch 'dev/winston/mitm-sandbox-readable-carvebacks' into dev/winston/mitm-child-env-snapshots

This commit is contained in:
Winston Howes
2026-06-18 15:05:15 -07:00
78 changed files with 2494 additions and 437 deletions
+6
View File
@@ -692,6 +692,9 @@
"unified_exec_zsh_fork": {
"type": "boolean"
},
"use_agent_identity": {
"type": "boolean"
},
"use_legacy_landlock": {
"type": "boolean"
},
@@ -4932,6 +4935,9 @@
"unified_exec_zsh_fork": {
"type": "boolean"
},
"use_agent_identity": {
"type": "boolean"
},
"use_legacy_landlock": {
"type": "boolean"
},
+25 -21
View File
@@ -31,8 +31,8 @@ use codex_core_plugins::PluginsManager;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::default_client::originator;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY;
use codex_mcp::McpConnectionManager;
use codex_mcp::McpRuntimeContext;
use codex_mcp::ToolInfo;
@@ -112,7 +112,6 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
directory_connectors,
accessible_connectors,
&connector_ids,
originator().value.as_str(),
)
.into_iter()
.map(DiscoverableTool::from);
@@ -143,12 +142,7 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools(
return Some(Vec::new());
}
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
read_cached_accessible_connectors(&cache_key).map(|connectors| {
codex_connectors::filter::filter_disallowed_connectors(
connectors,
originator().value.as_str(),
)
})
read_cached_accessible_connectors(&cache_key)
}
pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
@@ -161,10 +155,7 @@ pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
}
let cache_key = accessible_connectors_cache_key(config, auth);
let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors(
accessible_connectors_from_mcp_tools(mcp_tools),
originator().value.as_str(),
);
let accessible_connectors = accessible_connectors_for_app_list_from_mcp_tools(mcp_tools);
write_cached_accessible_connectors(cache_key, &accessible_connectors);
}
@@ -240,10 +231,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
let tool_plugin_provenance = tool_plugin_provenance(&mcp_config);
if !force_refetch && let Some(cached_connectors) = read_cached_accessible_connectors(&cache_key)
{
let cached_connectors = codex_connectors::filter::filter_disallowed_connectors(
cached_connectors,
originator().value.as_str(),
);
let cached_connectors = with_app_plugin_sources(cached_connectors, &tool_plugin_provenance);
return Ok(AccessibleConnectorsStatus {
connectors: cached_connectors,
@@ -353,10 +340,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
cancel_token.cancel();
}
let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors(
accessible_connectors_from_mcp_tools(&tools),
originator().value.as_str(),
);
let accessible_connectors = accessible_connectors_for_app_list_from_mcp_tools(&tools);
if codex_apps_ready || !accessible_connectors.is_empty() {
write_cached_accessible_connectors(cache_key, &accessible_connectors);
}
@@ -486,9 +470,15 @@ async fn cached_directory_connectors_for_tool_suggest_with_auth(
}
pub(crate) fn accessible_connectors_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Vec<AppInfo> {
collect_accessible_connectors_from_mcp_tools(mcp_tools.iter())
}
fn collect_accessible_connectors_from_mcp_tools<'a>(
mcp_tools: impl Iterator<Item = &'a ToolInfo>,
) -> Vec<AppInfo> {
// 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.iter().filter_map(|tool| {
let tools = mcp_tools.filter_map(|tool| {
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
return None;
}
@@ -503,6 +493,20 @@ pub(crate) fn accessible_connectors_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Ve
codex_connectors::accessible::collect_accessible_connectors(tools)
}
fn accessible_connectors_for_app_list_from_mcp_tools(mcp_tools: &[ToolInfo]) -> Vec<AppInfo> {
let non_synthetic_tools = mcp_tools.iter().filter(|tool| {
tool.tool
.meta
.as_deref()
.and_then(|meta| meta.get(MCP_TOOL_CODEX_APPS_META_KEY))
.and_then(serde_json::Value::as_object)
.and_then(|meta| meta.get("synthetic_link"))
.and_then(serde_json::Value::as_bool)
!= Some(true)
});
collect_accessible_connectors_from_mcp_tools(non_synthetic_tools)
}
pub fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) -> Vec<AppInfo> {
let user_apps_config = apps_config_from_layer_stack(&config.config_layer_stack);
let requirements_apps_config = config.config_layer_stack.requirements_toml().apps.as_ref();
+64
View File
@@ -17,6 +17,7 @@ use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo;
use pretty_assertions::assert_eq;
use rmcp::model::JsonObject;
use rmcp::model::Meta;
use rmcp::model::Tool;
use std::collections::BTreeMap;
use std::collections::HashSet;
@@ -140,6 +141,69 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
);
}
#[test]
fn synthetic_links_are_exposed_to_the_agent_but_not_accessible_in_app_list() {
let mut synthetic_tool = codex_app_tool("gmail_batch_read_email", "gmail", Some("Gmail"), &[]);
synthetic_tool.tool.meta = Some(Meta(
serde_json::json!({
"resource_name": "gmail.batch_read_email",
"_codex_apps": {
"resource_uri": "/connector/gmail/batch_read_email",
"contains_mcp_source": false,
"synthetic_link": true
}
})
.as_object()
.expect("meta should be an object")
.clone(),
));
let tools = vec![
synthetic_tool,
codex_app_tool("calendar_list_events", "calendar", Some("Calendar"), &[]),
];
let calendar = AppInfo {
id: "calendar".to_string(),
name: "Calendar".to_string(),
description: None,
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
install_url: Some(connector_install_url("Calendar", "calendar")),
branding: None,
app_metadata: None,
labels: None,
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
};
assert_eq!(
accessible_connectors_for_app_list_from_mcp_tools(&tools),
vec![calendar.clone()]
);
assert_eq!(
accessible_connectors_from_mcp_tools(&tools),
vec![
calendar,
AppInfo {
id: "gmail".to_string(),
name: "Gmail".to_string(),
description: None,
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
install_url: Some(connector_install_url("Gmail", "gmail")),
branding: None,
app_metadata: None,
labels: None,
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
}
]
);
}
#[tokio::test]
async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_installed_apps() {
let codex_home = tempdir().expect("tempdir should succeed");
+35 -9
View File
@@ -143,14 +143,7 @@ pub(crate) async fn handle_mcp_tool_call(
let metadata =
lookup_mcp_tool_metadata(sess.as_ref(), turn_context.as_ref(), &server, &tool_name).await;
let item_metadata = McpToolCallItemMetadata {
mcp_app_resource_uri: metadata
.as_ref()
.and_then(|metadata| metadata.mcp_app_resource_uri.clone()),
plugin_id: metadata
.as_ref()
.and_then(|metadata| metadata.plugin_id.clone()),
};
let item_metadata = McpToolCallItemMetadata::from_tool_metadata(&server, metadata.as_ref());
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
let annotations = metadata
.as_ref()
@@ -312,12 +305,32 @@ pub(crate) struct HandledMcpToolCall {
pub(crate) tool_input: JsonValue,
}
#[derive(Clone)]
#[derive(Clone, Debug, PartialEq, Eq)]
struct McpToolCallItemMetadata {
connector_id: Option<String>,
link_id: Option<String>,
mcp_app_resource_uri: Option<String>,
plugin_id: Option<String>,
}
impl McpToolCallItemMetadata {
fn from_tool_metadata(server: &str, metadata: Option<&McpToolApprovalMetadata>) -> Self {
let trusted_mcp_app_metadata = if server == CODEX_APPS_MCP_SERVER_NAME {
metadata
} else {
None
};
Self {
connector_id: trusted_mcp_app_metadata
.and_then(|metadata| metadata.connector_id.clone()),
link_id: trusted_mcp_app_metadata.and_then(|metadata| metadata.link_id.clone()),
mcp_app_resource_uri: metadata
.and_then(|metadata| metadata.mcp_app_resource_uri.clone()),
plugin_id: metadata.and_then(|metadata| metadata.plugin_id.clone()),
}
}
}
async fn handle_approved_mcp_tool_call(
sess: &Session,
turn_context: &TurnContext,
@@ -892,7 +905,9 @@ async fn notify_mcp_tool_call_started(
server,
tool,
arguments: arguments.unwrap_or(JsonValue::Null),
connector_id: item_metadata.connector_id,
mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,
link_id: item_metadata.link_id,
plugin_id: item_metadata.plugin_id,
status: McpToolCallStatus::InProgress,
result: None,
@@ -932,7 +947,9 @@ async fn notify_mcp_tool_call_completed(
server,
tool,
arguments: arguments.unwrap_or(JsonValue::Null),
connector_id: item_metadata.connector_id,
mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,
link_id: item_metadata.link_id,
plugin_id: item_metadata.plugin_id,
status,
result,
@@ -998,6 +1015,7 @@ enum McpToolApprovalDecision {
pub(crate) struct McpToolApprovalMetadata {
annotations: Option<ToolAnnotations>,
connector_id: Option<String>,
link_id: Option<String>,
connector_name: Option<String>,
connector_description: Option<String>,
plugin_id: Option<String>,
@@ -1010,6 +1028,7 @@ pub(crate) struct McpToolApprovalMetadata {
const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate";
const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri";
const MCP_TOOL_LINK_ID_META_KEY: &str = "link_id";
const MCP_TOOL_PLUGIN_ID_META_KEY: &str = "plugin_id";
const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
@@ -1493,6 +1512,13 @@ pub(crate) async fn lookup_mcp_tool_metadata(
Some(McpToolApprovalMetadata {
annotations: tool_info.tool.annotations,
connector_id: tool_info.connector_id,
link_id: tool_info
.tool
.meta
.as_ref()
.and_then(|meta| meta.get(MCP_TOOL_LINK_ID_META_KEY))
.and_then(serde_json::Value::as_str)
.map(str::to_string),
connector_name: tool_info.connector_name,
connector_description,
plugin_id,
+54 -2
View File
@@ -78,6 +78,7 @@ fn approval_metadata(
McpToolApprovalMetadata {
annotations: None,
connector_id: connector_id.map(str::to_string),
link_id: None,
connector_name: connector_name.map(str::to_string),
connector_description: connector_description.map(str::to_string),
plugin_id: None,
@@ -1149,8 +1150,39 @@ async fn plugin_mcp_tool_call_request_meta_includes_plugin_id() {
);
}
#[test]
fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() {
let mut metadata = approval_metadata(
Some("asdk_app_0123456789abcdef0123456789abcdef"),
/*connector_name*/ None,
/*connector_description*/ None,
/*tool_title*/ None,
/*tool_description*/ None,
);
metadata.link_id = Some("link_fedcba9876543210fedcba9876543210".to_string());
assert_eq!(
McpToolCallItemMetadata::from_tool_metadata(CODEX_APPS_MCP_SERVER_NAME, Some(&metadata),),
McpToolCallItemMetadata {
connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()),
link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()),
mcp_app_resource_uri: None,
plugin_id: None,
}
);
assert_eq!(
McpToolCallItemMetadata::from_tool_metadata("custom_server", Some(&metadata)),
McpToolCallItemMetadata {
connector_id: None,
link_id: None,
mcp_app_resource_uri: None,
plugin_id: None,
}
);
}
#[tokio::test]
async fn mcp_tool_call_item_includes_plugin_id() {
async fn mcp_tool_call_item_includes_app_identity() {
let (session, turn_context, rx_event) = make_session_and_context_with_rx().await;
notify_mcp_tool_call_started(
@@ -1158,11 +1190,13 @@ async fn mcp_tool_call_item_includes_plugin_id() {
&turn_context,
"call-plugin",
McpInvocation {
server: "sample".to_string(),
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool: "echo".to_string(),
arguments: None,
},
McpToolCallItemMetadata {
connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()),
link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()),
mcp_app_resource_uri: None,
plugin_id: Some("sample@test".to_string()),
},
@@ -1180,6 +1214,14 @@ async fn mcp_tool_call_item_includes_plugin_id() {
panic!("expected MCP tool call item");
};
assert_eq!(
item.connector_id.as_deref(),
Some("asdk_app_0123456789abcdef0123456789abcdef")
);
assert_eq!(
item.link_id.as_deref(),
Some("link_fedcba9876543210fedcba9876543210")
);
assert_eq!(item.plugin_id.as_deref(), Some("sample@test"));
}
@@ -1193,6 +1235,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
let metadata = McpToolApprovalMetadata {
annotations: None,
connector_id: Some("calendar".to_string()),
link_id: None,
connector_name: Some("Calendar".to_string()),
connector_description: Some("Manage events".to_string()),
plugin_id: None,
@@ -1661,6 +1704,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
let metadata = McpToolApprovalMetadata {
annotations: Some(annotations(Some(false), Some(true), Some(true))),
connector_id: None,
link_id: None,
connector_name: None,
connector_description: None,
plugin_id: None,
@@ -2326,6 +2370,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
/*open_world*/ None,
)),
connector_id: None,
link_id: None,
connector_name: None,
connector_description: None,
plugin_id: None,
@@ -2400,6 +2445,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
/*open_world*/ None,
)),
connector_id: None,
link_id: None,
connector_name: None,
connector_description: None,
plugin_id: None,
@@ -2457,6 +2503,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
/*open_world*/ None,
)),
connector_id: None,
link_id: None,
connector_name: None,
connector_description: None,
plugin_id: None,
@@ -2593,6 +2640,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
/*open_world*/ None,
)),
connector_id: None,
link_id: None,
connector_name: None,
connector_description: None,
plugin_id: None,
@@ -2680,6 +2728,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
let metadata = McpToolApprovalMetadata {
annotations: Some(annotations(Some(false), Some(true), Some(true))),
connector_id: None,
link_id: None,
connector_name: None,
connector_description: None,
plugin_id: None,
@@ -2734,6 +2783,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
/*open_world*/ None,
)),
connector_id: None,
link_id: None,
connector_name: None,
connector_description: None,
plugin_id: None,
@@ -2789,6 +2839,7 @@ async fn full_access_mode_skips_mcp_tool_approval_for_all_approval_modes() {
let metadata = McpToolApprovalMetadata {
annotations: Some(annotations(Some(false), Some(true), Some(true))),
connector_id: Some("calendar".to_string()),
link_id: None,
connector_name: Some("Calendar".to_string()),
connector_description: Some("Manage events".to_string()),
plugin_id: None,
@@ -2842,6 +2893,7 @@ async fn approve_mode_skips_guardian_in_every_permission_mode() {
let metadata = McpToolApprovalMetadata {
annotations: Some(annotations(Some(false), Some(true), Some(true))),
connector_id: Some("calendar".to_string()),
link_id: None,
connector_name: Some("Calendar".to_string()),
connector_description: Some("Manage events".to_string()),
plugin_id: None,
@@ -203,7 +203,9 @@ async fn emit_tool_call_begin(
server,
tool,
arguments: arguments.unwrap_or(Value::Null),
connector_id: None,
mcp_app_resource_uri: None,
link_id: None,
plugin_id: None,
status: McpToolCallStatus::InProgress,
result: None,
@@ -242,7 +244,9 @@ async fn emit_tool_call_end(
server,
tool,
arguments: arguments.unwrap_or(Value::Null),
connector_id: None,
mcp_app_resource_uri: None,
link_id: None,
plugin_id: None,
status,
result,
@@ -17,6 +17,7 @@ use wiremock::matchers::path;
use wiremock::matchers::path_regex;
const CONNECTOR_ID: &str = "calendar";
pub const LINK_ID: &str = "link_calendar";
const CONNECTOR_NAME: &str = "Calendar";
const DISCOVERABLE_CALENDAR_ID: &str = "connector_2128aebfecb84f64a069897515042a44";
const DISCOVERABLE_GMAIL_ID: &str = "connector_68df038e0ba48191908c8434991bbac2";
@@ -352,6 +353,7 @@ impl Respond for CodexAppsJsonRpcResponder {
},
"_meta": {
"connector_id": CONNECTOR_ID,
"link_id": LINK_ID,
"connector_name": self.connector_name.clone(),
"connector_description": self.connector_description.clone(),
"openai/outputTemplate": CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI,
@@ -378,6 +380,7 @@ impl Respond for CodexAppsJsonRpcResponder {
},
"_meta": {
"connector_id": CONNECTOR_ID,
"link_id": LINK_ID,
"connector_name": self.connector_name.clone(),
"connector_description": self.connector_description.clone(),
"_codex_apps": {
@@ -410,6 +413,7 @@ impl Respond for CodexAppsJsonRpcResponder {
},
"_meta": {
"connector_id": CONNECTOR_ID,
"link_id": LINK_ID,
"connector_name": self.connector_name.clone(),
"connector_description": self.connector_description.clone(),
"openai/fileParams": ["file"],
+2
View File
@@ -1337,7 +1337,9 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
tool: "image".to_string(),
arguments: Some(json!({})),
},
connector_id: None,
mcp_app_resource_uri: None,
link_id: None,
plugin_id: None,
},
);
+3
View File
@@ -25,6 +25,7 @@ use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_
use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_RESOURCE_URI;
use core_test_support::apps_test_server::DIRECT_CALENDAR_CREATE_EVENT_TOOL as CALENDAR_CREATE_TOOL;
use core_test_support::apps_test_server::DIRECT_CALENDAR_LIST_EVENTS_TOOL as CALENDAR_LIST_TOOL;
use core_test_support::apps_test_server::LINK_ID;
use core_test_support::apps_test_server::SEARCH_CALENDAR_APP_ONLY_TOOL;
use core_test_support::apps_test_server::SEARCH_CALENDAR_CREATE_TOOL;
use core_test_support::apps_test_server::SEARCH_CALENDAR_LIST_TOOL;
@@ -559,10 +560,12 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
unreachable!("event guard guarantees McpToolCallEnd");
};
assert_eq!(end.call_id, "calendar-call-1");
assert_eq!(end.connector_id.as_deref(), Some("calendar"));
assert_eq!(
end.mcp_app_resource_uri.as_deref(),
Some(CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI)
);
assert_eq!(end.link_id.as_deref(), Some(LINK_ID));
assert_eq!(
end.invocation,
McpInvocation {