Enforce Agent Plugin runtime boundaries (#37027)

## What changed

- Track Agent Plugin manifests through plugin, skill, and MCP loading so their capabilities use format-specific behavior without changing legacy plugins.
- Discover only direct-child skills, exclude app and hook capabilities, isolate MCP data, and reject MCP configuration files that are non-regular or resolve outside the plugin root.
- Bound model-visible skill instructions, plugin instructions, MCP descriptions, schemas, individual tools, and the aggregate Agent Plugin MCP tool set.
- Stop MCP and OAuth redirects when Agent Plugins send configured or authorization headers, while retaining existing redirect behavior for legacy MCP servers.

## Testing

- Add coverage for capability filtering, skill discovery, isolated MCP data and reserved-path expansion, unsafe MCP configuration files, context limits, and redirect handling.

GitOrigin-RevId: c9af66b051269f3226628ca280a58d32c808c38f
This commit is contained in:
jacobzhou-oai authored and copyberry committed 2026-08-05 04:54:33 +00:00
1 parent f21dc46388
commit 56b82e676c
63 files changed
+1830 -106

No files matched your search

+190 -35
View File
@@ -3,10 +3,11 @@ use crate::app_mcp_routing::apps_route_available;
use crate::command_migration::migrated_command_skills_root;
use crate::is_openai_curated_marketplace_name;
use crate::manifest::PluginManifest;
use crate::manifest::PluginManifestFormat;
use crate::manifest::PluginManifestHooks;
use crate::manifest::PluginManifestMcpServers;
use crate::manifest::PluginManifestPaths;
use crate::manifest::load_plugin_manifest;
use crate::manifest::load_plugin_manifest_with_format;
use crate::marketplace::MarketplacePluginSource;
use crate::marketplace::find_marketplace_plugin;
use crate::marketplace::list_marketplaces_with_home;
@@ -23,6 +24,7 @@ use crate::store::plugin_version_for_source_with_fallback_manifest;
use codex_config::ConfigLayerStack;
use codex_config::HooksFile;
use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig;
use codex_config::types::PluginConfig;
use codex_config::types::PluginMcpServerConfig;
use codex_connectors::parse_plugin_app_config;
@@ -33,6 +35,7 @@ use codex_core_skills::config_rules::skill_config_rules_from_stack;
use codex_core_skills::loader::SkillRoot;
use codex_core_skills::loader::load_skills_from_roots;
use codex_exec_server::LOCAL_FS;
use codex_mcp::parse_agent_plugin_mcp_config;
use codex_mcp::parse_plugin_mcp_config;
use codex_plugin::AppDeclaration;
use codex_plugin::LoadedPlugin;
@@ -831,6 +834,7 @@ async fn load_plugin(
root,
enabled: plugin.enabled,
skill_roots: Vec::new(),
skill_discovery_mode: SkillDiscoveryMode::Recursive,
disabled_skill_paths: HashSet::new(),
has_enabled_skills: false,
mcp_servers: HashMap::new(),
@@ -873,12 +877,19 @@ async fn load_plugin(
return loaded_plugin;
}
let Some(manifest) = load_plugin_manifest(plugin_root.as_path()) else {
let Some(loaded_manifest) = load_plugin_manifest_with_format(plugin_root.as_path()) else {
loaded_plugin.error = Some("missing or invalid plugin.json".to_string());
return loaded_plugin;
};
loaded_plugin.skill_discovery_mode = match loaded_manifest.format {
PluginManifestFormat::Legacy => SkillDiscoveryMode::Recursive,
PluginManifestFormat::AgentPlugin => SkillDiscoveryMode::DirectChildren,
};
let manifest = loaded_manifest.manifest;
let manifest_paths = &manifest.paths;
let plugin_data_root = store.plugin_data_root(&loaded_plugin_id);
let mcp_plugin_data_root = store.mcp_data_root(&loaded_plugin_id, loaded_manifest.format);
loaded_plugin.plugin_namespace = Some(manifest.name.clone());
match scope {
PluginLoadScope::AllCapabilities {
@@ -890,40 +901,51 @@ async fn load_plugin(
} => {
loaded_plugin.manifest_name = Some(manifest.display_name().to_string());
loaded_plugin.manifest_description = manifest.description.clone();
loaded_plugin.skill_roots = plugin_skill_roots(&plugin_root, manifest_paths);
loaded_plugin.skill_roots =
plugin_skill_roots(&plugin_root, manifest_paths, loaded_manifest.format);
let plugin_identity = PluginIdentity {
plugin_id: loaded_plugin_id.as_key(),
remote_plugin_id: loaded_plugin.remote_plugin_id.clone(),
};
let resolved_skills = load_plugin_skills_with_identity(
let resolved_skills = load_plugin_skill_inventory(
&plugin_root,
&plugin_identity,
&manifest,
loaded_manifest.format,
*restriction_product,
skill_config_rules,
*plugin_skill_snapshots,
Arc::clone(root_scan_slots),
)
.await;
.await
.resolve(skill_config_rules);
let has_enabled_skills = resolved_skills.has_enabled_skills();
loaded_plugin.disabled_skill_paths = resolved_skills.disabled_skill_paths;
loaded_plugin.has_enabled_skills = has_enabled_skills;
loaded_plugin.mcp_servers = load_plugin_mcp_servers_from_manifest(
loaded_plugin.mcp_servers = load_plugin_mcp_servers_from_manifest_with_format(
plugin_root.as_path(),
manifest_paths,
Some(&plugin.mcp_servers),
Some(mcp_plugin_data_root.as_path()),
loaded_manifest.format,
)
.await;
loaded_plugin.apps = load_plugin_apps(plugin_root.as_path()).await;
if loaded_manifest.format == PluginManifestFormat::Legacy {
loaded_plugin.apps = load_plugin_apps(plugin_root.as_path()).await;
}
}
PluginLoadScope::HooksOnly => {}
}
let (hook_sources, hook_load_warnings) = load_plugin_hooks(
&plugin_root,
&loaded_plugin_id,
&store.plugin_data_root(&loaded_plugin_id),
manifest_paths,
);
let (hook_sources, hook_load_warnings) =
if loaded_manifest.format == PluginManifestFormat::AgentPlugin {
(Vec::new(), Vec::new())
} else {
load_plugin_hooks(
&plugin_root,
&loaded_plugin_id,
&plugin_data_root,
manifest_paths,
)
};
loaded_plugin.hook_sources = hook_sources;
loaded_plugin.hook_load_warnings = hook_load_warnings;
loaded_plugin
@@ -961,7 +983,7 @@ impl PluginSkillInventory {
)
}
fn resolve(self, skill_config_rules: &SkillConfigRules) -> ResolvedPluginSkills {
pub(crate) fn resolve(self, skill_config_rules: &SkillConfigRules) -> ResolvedPluginSkills {
let disabled_skill_paths = resolve_disabled_skill_paths(&self.skills, skill_config_rules);
ResolvedPluginSkills {
skills: self.skills,
@@ -1031,6 +1053,7 @@ pub(crate) async fn load_plugin_skills_with_identity(
plugin_root,
plugin_identity,
manifest,
PluginManifestFormat::Legacy,
restriction_product,
plugin_skill_snapshots,
root_scan_slots,
@@ -1043,11 +1066,16 @@ pub(crate) async fn load_plugin_skill_inventory(
plugin_root: &AbsolutePathBuf,
plugin_identity: &PluginIdentity,
manifest: &PluginManifest,
manifest_format: PluginManifestFormat,
restriction_product: Option<Product>,
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
root_scan_slots: Arc<Semaphore>,
) -> PluginSkillInventory {
let roots = plugin_skill_roots(plugin_root, &manifest.paths)
let discovery_mode = match manifest_format {
PluginManifestFormat::Legacy => SkillDiscoveryMode::Recursive,
PluginManifestFormat::AgentPlugin => SkillDiscoveryMode::DirectChildren,
};
let roots = plugin_skill_roots(plugin_root, &manifest.paths, manifest_format)
.into_iter()
.map(|path| SkillRoot {
path,
@@ -1056,7 +1084,7 @@ pub(crate) async fn load_plugin_skill_inventory(
plugin_identity: Some(plugin_identity.clone()),
plugin_namespace: Some(manifest.name.clone()),
plugin_root: Some(plugin_root.clone()),
discovery_mode: SkillDiscoveryMode::Recursive,
discovery_mode,
})
.collect::<Vec<_>>();
let outcome = load_skills_from_roots(roots, plugin_skill_snapshots, root_scan_slots).await;
@@ -1098,15 +1126,18 @@ pub(crate) async fn load_plugin_skill_inventory(
fn plugin_skill_roots(
plugin_root: &AbsolutePathBuf,
manifest_paths: &PluginManifestPaths,
manifest_format: PluginManifestFormat,
) -> Vec<AbsolutePathBuf> {
let mut paths = if manifest_paths.skills.is_empty() {
default_skill_roots(plugin_root)
} else {
manifest_paths.skills.clone()
};
let migrated_command_skills = migrated_command_skills_root(plugin_root);
if migrated_command_skills.is_dir() {
paths.push(migrated_command_skills);
if manifest_format == PluginManifestFormat::Legacy {
let migrated_command_skills = migrated_command_skills_root(plugin_root);
if migrated_command_skills.is_dir() {
paths.push(migrated_command_skills);
}
}
paths.sort_unstable();
paths.dedup();
@@ -1146,8 +1177,11 @@ fn default_mcp_config_paths(plugin_root: &Path) -> Vec<AbsolutePathBuf> {
}
pub async fn load_plugin_apps(plugin_root: &Path) -> Vec<AppDeclaration> {
if let Some(manifest) = load_plugin_manifest(plugin_root) {
return load_plugin_apps_from_manifest(plugin_root, &manifest.paths).await;
if let Some(loaded_manifest) = load_plugin_manifest_with_format(plugin_root) {
if loaded_manifest.format == PluginManifestFormat::AgentPlugin {
return Vec::new();
}
return load_plugin_apps_from_manifest(plugin_root, &loaded_manifest.manifest.paths).await;
}
load_apps_from_paths(plugin_root, default_app_config_paths(plugin_root)).await
}
@@ -1345,14 +1379,40 @@ pub async fn plugin_capability_summary_from_root(
plugin_id: &PluginId,
plugin_root: &AbsolutePathBuf,
) -> Option<PluginCapabilitySummary> {
let manifest = load_plugin_manifest(plugin_root.as_path())?;
let loaded_manifest = load_plugin_manifest_with_format(plugin_root.as_path())?;
let manifest_format = loaded_manifest.format;
let manifest = loaded_manifest.manifest;
let plugin_identity = PluginIdentity {
plugin_id: plugin_id.as_key(),
remote_plugin_id: None,
};
let manifest_paths = &manifest.paths;
let has_skills = !plugin_skill_roots(plugin_root, manifest_paths).is_empty();
let mut mcp_server_names = load_plugin_mcp_servers_from_manifest(
let has_skills = match manifest_format {
PluginManifestFormat::Legacy => {
!plugin_skill_roots(plugin_root, manifest_paths, manifest_format).is_empty()
}
PluginManifestFormat::AgentPlugin => {
!load_plugin_skill_inventory(
plugin_root,
&plugin_identity,
&manifest,
manifest_format,
/*restriction_product*/ None,
/*plugin_skill_snapshots*/ None,
Arc::new(Semaphore::new(1)),
)
.await
.skills
.is_empty()
}
};
let mut mcp_server_names = load_plugin_mcp_servers_from_manifest_with_format(
plugin_root.as_path(),
manifest_paths,
/*plugin_policy*/ None,
/*plugin_data_root*/ None,
manifest_format,
)
.await
.into_keys()
@@ -1360,16 +1420,17 @@ pub async fn plugin_capability_summary_from_root(
mcp_server_names.sort_unstable();
mcp_server_names.dedup();
let app_declarations = load_apps_from_paths(
plugin_root.as_path(),
plugin_app_config_paths(plugin_root.as_path(), manifest_paths),
)
.await;
let app_declarations = if manifest_format == PluginManifestFormat::AgentPlugin {
Vec::new()
} else {
load_plugin_apps_from_manifest(plugin_root.as_path(), manifest_paths).await
};
let app_connector_ids = app_connector_ids_from_declarations(&app_declarations);
Some(PluginCapabilitySummary {
config_name: plugin_id.as_key(),
display_name: plugin_id.plugin_name.clone(),
plugin_namespace: Some(manifest.name.clone()),
description: None,
has_skills,
mcp_server_names,
@@ -1426,17 +1487,26 @@ async fn load_declared_plugin_mcp_servers(
plugin_root: &Path,
plugin_policy: Option<&HashMap<String, PluginMcpServerConfig>>,
) -> HashMap<String, McpServerConfig> {
let Some(manifest) = load_plugin_manifest(plugin_root) else {
let Some(loaded_manifest) = load_plugin_manifest_with_format(plugin_root) else {
return HashMap::new();
};
load_plugin_mcp_servers_from_manifest(plugin_root, &manifest.paths, plugin_policy).await
load_plugin_mcp_servers_from_manifest_with_format(
plugin_root,
&loaded_manifest.manifest.paths,
plugin_policy,
/*plugin_data_root*/ None,
loaded_manifest.format,
)
.await
}
pub(crate) async fn load_plugin_mcp_servers_from_manifest(
pub(crate) async fn load_plugin_mcp_servers_from_manifest_with_format(
plugin_root: &Path,
manifest_paths: &PluginManifestPaths,
plugin_policy: Option<&HashMap<String, PluginMcpServerConfig>>,
plugin_data_root: Option<&Path>,
manifest_format: PluginManifestFormat,
) -> HashMap<String, McpServerConfig> {
let mut mcp_servers = HashMap::new();
match &manifest_paths.mcp_servers {
@@ -1457,7 +1527,13 @@ pub(crate) async fn load_plugin_mcp_servers_from_manifest(
}
Some(PluginManifestMcpServers::Path(_)) | None => {
for mcp_config_path in plugin_mcp_config_paths(plugin_root, manifest_paths) {
let plugin_mcp = load_mcp_servers_from_file(plugin_root, &mcp_config_path).await;
let plugin_mcp = load_mcp_servers_from_file(
plugin_root,
plugin_data_root,
manifest_format,
&mcp_config_path,
)
.await;
for (name, mut config) in plugin_mcp.mcp_servers {
if let Some(policy) = plugin_policy.and_then(|policy| policy.get(&name)) {
apply_plugin_mcp_server_policy(&mut config, policy);
@@ -1480,12 +1556,74 @@ pub(crate) async fn load_plugin_mcp_servers_from_manifest(
async fn load_mcp_servers_from_file(
plugin_root: &Path,
plugin_data_root: Option<&Path>,
manifest_format: PluginManifestFormat,
mcp_config_path: &AbsolutePathBuf,
) -> PluginMcpDiscovery {
let is_agent_plugin_mcp = manifest_format == PluginManifestFormat::AgentPlugin;
if is_agent_plugin_mcp {
match tokio::fs::symlink_metadata(mcp_config_path.as_path()).await {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
warn!(
path = %mcp_config_path.display(),
"Agent Plugins MCP config is not a regular file; disabling MCP"
);
return PluginMcpDiscovery::default();
}
Ok(_) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return PluginMcpDiscovery::default();
}
Err(err) => {
warn!(
path = %mcp_config_path.display(),
"failed to inspect Agent Plugins MCP config; disabling MCP: {err}"
);
return PluginMcpDiscovery::default();
}
}
let resolved_root = match tokio::fs::canonicalize(plugin_root).await {
Ok(path) => path,
Err(err) => {
warn!(
plugin = %plugin_root.display(),
"failed to resolve Agent Plugins root; disabling MCP: {err}"
);
return PluginMcpDiscovery::default();
}
};
let resolved_config = match tokio::fs::canonicalize(mcp_config_path.as_path()).await {
Ok(path) => path,
Err(err) => {
warn!(
path = %mcp_config_path.display(),
"failed to resolve Agent Plugins MCP config; disabling MCP: {err}"
);
return PluginMcpDiscovery::default();
}
};
if !resolved_config.starts_with(&resolved_root) {
warn!(
plugin = %plugin_root.display(),
path = %mcp_config_path.display(),
"Agent Plugins MCP config resolves outside the plugin root; disabling MCP"
);
return PluginMcpDiscovery::default();
}
}
let Ok(contents) = tokio::fs::read_to_string(mcp_config_path.as_path()).await else {
return PluginMcpDiscovery::default();
};
let parsed = match parse_plugin_mcp_config(plugin_root, &contents) {
let fallback_data_root = plugin_root.join(".plugin-data");
let mut parsed = match if is_agent_plugin_mcp {
parse_agent_plugin_mcp_config(
plugin_root,
plugin_data_root.unwrap_or(&fallback_data_root),
&contents,
)
} else {
parse_plugin_mcp_config(plugin_root, &contents)
} {
Ok(parsed) => parsed,
Err(err) => {
warn!(
@@ -1495,6 +1633,23 @@ async fn load_mcp_servers_from_file(
return PluginMcpDiscovery::default();
}
};
if is_agent_plugin_mcp
&& let Some(plugin_data_root) = plugin_data_root
&& parsed
.servers
.values()
.any(|server| matches!(&server.transport, McpServerTransportConfig::Stdio { .. }))
&& let Err(err) = tokio::fs::create_dir_all(plugin_data_root).await
{
warn!(
plugin = %plugin_root.display(),
path = %plugin_data_root.display(),
"failed to create Agent Plugins data directory; disabling stdio MCP servers: {err}"
);
parsed.servers.retain(|_, server| {
!matches!(&server.transport, McpServerTransportConfig::Stdio { .. })
});
}
for error in parsed.errors {
warn!(
plugin = %plugin_root.display(),