Freeze plugin roots in MCP tool attribution (#41117)

## Why

Plugin MCP trust decisions must describe the exact host-discovered server
registration. Looking up a plugin again by ID can resolve a different root.

## What changed

- Retain the host plugin root in MCP catalog attribution and connection identity.
- Reconnect an MCP server when its attributed plugin root changes.
- Pass the retained root through tool lifecycle attribution and use it directly
  when building trusted tool context.

## Testing

Add coverage for catalog identity, connection reconciliation, lifecycle
attribution, and rejecting a different root associated with the same plugin ID.

GitOrigin-RevId: bc92743dd40ab44065a07c3ea972b4956ef8351f
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-08-27 11:27:50 +00:00
1 parent 6c59264b14
commit aa89cf62bb
11 files changed
+279 -38

No files matched your search

@@ -68,13 +68,13 @@ pub(crate) async fn trusted_tool_context(
) -> Option<GuardianTrustedToolFragment> {
let codex_home = config.codex_home.as_path().canonicalize().ok()?;
let plugins = match source {
McpToolSource::Connector | McpToolSource::Plugin { .. } => Some(
McpToolSource::Connector => Some(
manager
.plugins_manager()
.plugins_for_config(&config.plugins_config_input())
.await,
),
McpToolSource::Config => None,
McpToolSource::Config | McpToolSource::Plugin { .. } => None,
McpToolSource::SelectedPlugin | McpToolSource::Other => return None,
};
@@ -98,18 +98,16 @@ pub(crate) async fn trusted_tool_context(
trusted_user_config_source(config, "apps", connector_id, &codex_home)?
}
}
McpToolSource::Plugin { id: plugin_id } => {
let plugin = plugins.as_ref()?.plugins().iter().find(|plugin| {
plugin.is_active()
&& plugin.config_name == plugin_id.as_str()
&& plugin.mcp_servers.contains_key(&tool.server_name)
&& is_home_owned_plugin_capability(
plugin.root.as_path(),
&codex_home,
PluginCapability::Mcp,
)
})?;
plugin.root.as_path().display().to_string()
McpToolSource::Plugin { root, .. } => {
let plugin_root = root.to_abs_path().ok()?;
if !is_home_owned_plugin_capability(
plugin_root.as_path(),
&codex_home,
PluginCapability::Mcp,
) {
return None;
}
plugin_root.as_path().display().to_string()
}
McpToolSource::Config => {
trusted_user_config_source(config, "mcp_servers", &tool.server_name, &codex_home)?
@@ -149,6 +149,11 @@ async fn trusts_connector_declared_by_home_owned_plugin() -> Result<()> {
&mcp,
&McpToolSource::Plugin {
id: "trusted@test".to_string(),
root: test
.config
.codex_home
.join("plugins/cache/test/trusted/local")
.into(),
},
&test.thread_manager,
&test.config,
@@ -156,6 +161,20 @@ async fn trusts_connector_declared_by_home_owned_plugin() -> Result<()> {
.await
.expect("home-owned plugin MCP server should be trusted");
assert_eq!(mcp_context.metadata, expected_context(&mcp, &plugin_root));
// A trusted cached plugin with the same ID must not replace the frozen outside-home root.
assert_eq!(
trusted_tool_context(
&mcp,
&McpToolSource::Plugin {
id: "trusted@test".to_string(),
root: test.config.cwd.clone().into(),
},
&test.thread_manager,
&test.config,
)
.await,
None,
);
assert_eq!(
trusted_tool_context(
&mcp,