Backport Agent Plugin runtime boundaries (#37027, #36967)

Adapt the Agent Plugin runtime boundary enforcement to the alpha.6.4 APIs and skip symlinks while installing plugins.

(cherry picked from commit 56b82e676c)

(cherry picked from commit 720c9d68e1)
This commit is contained in:
jacobzhou-oai
2026-08-05 17:37:06 +00:00
committed by Jacob Zhou
parent 3044612e13
commit 8ac86822a1
63 changed files with 1839 additions and 130 deletions
+2
View File
@@ -38,6 +38,7 @@ pub use json_schema::JsonSchemaType;
pub use json_schema::parse_tool_input_schema;
pub use json_schema::parse_tool_input_schema_without_compaction;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::parse_agent_plugin_mcp_tool;
pub use mcp_tool::parse_mcp_tool;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE;
@@ -56,6 +57,7 @@ pub use responses_api::LoadableToolSpec;
pub use responses_api::ResponsesApiNamespace;
pub use responses_api::ResponsesApiNamespaceTool;
pub use responses_api::ResponsesApiTool;
pub use responses_api::agent_plugin_mcp_tool_to_responses_api_tool;
pub use responses_api::coalesce_loadable_tool_specs;
pub use responses_api::default_namespace_description;
pub use responses_api::dynamic_tool_to_responses_api_tool;
+24 -1
View File
@@ -1,9 +1,25 @@
use crate::ToolDefinition;
use crate::parse_tool_input_schema;
use codex_utils_string::take_bytes_at_char_boundary;
use serde_json::Value as JsonValue;
use serde_json::json;
const MAX_MCP_TOOL_DESCRIPTION_BYTES: usize = 1_000;
pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_json::Error> {
parse_mcp_tool_with_description_limit(tool, /*description_limit*/ None)
}
pub fn parse_agent_plugin_mcp_tool(
tool: &rmcp::model::Tool,
) -> Result<ToolDefinition, serde_json::Error> {
parse_mcp_tool_with_description_limit(tool, Some(MAX_MCP_TOOL_DESCRIPTION_BYTES))
}
fn parse_mcp_tool_with_description_limit(
tool: &rmcp::model::Tool,
description_limit: Option<usize>,
) -> Result<ToolDefinition, serde_json::Error> {
let mut serialized_input_schema = serde_json::Value::Object(tool.input_schema.as_ref().clone());
// OpenAI models mandate the "properties" field in the schema. Some MCP
@@ -27,7 +43,14 @@ pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_
Ok(ToolDefinition {
name: tool.name.to_string(),
description: tool.description.clone().map(Into::into).unwrap_or_default(),
description: tool
.description
.as_deref()
.map(|description| match description_limit {
Some(limit) => take_bytes_at_char_boundary(description, limit).to_string(),
None => description.to_string(),
})
.unwrap_or_default(),
input_schema,
output_schema: Some(mcp_call_tool_result_output_schema(
structured_content_schema,
+35
View File
@@ -1,4 +1,5 @@
use super::mcp_call_tool_result_output_schema;
use super::parse_agent_plugin_mcp_tool;
use super::parse_mcp_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
@@ -39,6 +40,40 @@ fn parse_mcp_tool_inserts_empty_properties() {
);
}
#[test]
fn agent_plugin_mcp_tool_bounds_model_visible_description() {
let description = format!("{}é", "a".repeat(super::MAX_MCP_TOOL_DESCRIPTION_BYTES - 1));
let tool = mcp_tool(
"bounded_description",
&description,
serde_json::json!({"type": "object"}),
);
let parsed = parse_agent_plugin_mcp_tool(&tool).expect("parse Agent Plugin MCP tool");
assert_eq!(
parsed.description.len(),
super::MAX_MCP_TOOL_DESCRIPTION_BYTES - 1
);
}
#[test]
fn legacy_mcp_tool_preserves_long_description() {
let description = "a".repeat(super::MAX_MCP_TOOL_DESCRIPTION_BYTES + 100);
let tool = mcp_tool(
"legacy_description",
&description,
serde_json::json!({"type": "object"}),
);
assert_eq!(
parse_mcp_tool(&tool)
.expect("parse legacy MCP tool")
.description,
description
);
}
#[test]
fn parse_mcp_tool_preserves_top_level_output_schema() {
let mut tool = mcp_tool(
+20
View File
@@ -1,6 +1,7 @@
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolName;
use crate::parse_agent_plugin_mcp_tool;
use crate::parse_dynamic_tool;
use crate::parse_mcp_tool;
use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
@@ -8,6 +9,8 @@ use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
const MAX_SERIALIZED_MCP_TOOL_BYTES: usize = 8_000;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FreeformTool {
pub name: String,
@@ -118,6 +121,23 @@ pub fn mcp_tool_to_responses_api_tool(
))
}
pub fn agent_plugin_mcp_tool_to_responses_api_tool(
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
let mut tool = tool_definition_to_responses_api_tool(
parse_agent_plugin_mcp_tool(tool)?.renamed(tool_name.name.clone()),
);
if serde_json::to_vec(&tool)?.len() > MAX_SERIALIZED_MCP_TOOL_BYTES {
tool.parameters = JsonSchema::object(
Default::default(),
/*required*/ None,
Some(true.into()),
);
}
Ok(tool)
}
pub fn mcp_tool_to_deferred_responses_api_tool(
tool_name: &ToolName,
tool: &rmcp::model::Tool,
+41
View File
@@ -3,6 +3,7 @@ use super::LoadableToolSpec;
use super::ResponsesApiNamespace;
use super::ResponsesApiNamespaceTool;
use super::ResponsesApiTool;
use super::agent_plugin_mcp_tool_to_responses_api_tool;
use super::dynamic_tool_to_responses_api_tool;
use super::mcp_tool_to_deferred_responses_api_tool;
use super::tool_definition_to_responses_api_tool;
@@ -153,6 +154,46 @@ fn mcp_tool_to_deferred_responses_api_tool_sets_defer_loading() {
);
}
#[test]
fn agent_plugin_mcp_tool_uses_fallback_for_oversized_schema() {
let properties: serde_json::Map<String, serde_json::Value> = (0..1_024)
.map(|index| (format!("property_{index}"), json!({"type": "string"})))
.collect();
let tool = rmcp::model::Tool::new(
"oversized",
"Large schema",
std::sync::Arc::new(rmcp::model::object(json!({
"type": "object",
"properties": properties,
}))),
);
assert_eq!(
agent_plugin_mcp_tool_to_responses_api_tool(&ToolName::from("oversized"), &tool)
.expect("Agent Plugin MCP tool should use a fallback schema")
.parameters,
JsonSchema::object(BTreeMap::new(), /*required*/ None, Some(true.into()))
);
}
#[test]
fn legacy_mcp_tool_accepts_oversized_schema() {
let properties: serde_json::Map<String, serde_json::Value> = (0..1_024)
.map(|index| (format!("property_{index}"), json!({"type": "string"})))
.collect();
let tool = rmcp::model::Tool::new(
"oversized",
"Large legacy schema",
std::sync::Arc::new(rmcp::model::object(json!({
"type": "object",
"properties": properties,
}))),
);
super::mcp_tool_to_responses_api_tool(&ToolName::from("oversized"), &tool)
.expect("legacy MCP conversion must preserve existing acceptance");
}
#[test]
fn loadable_tool_spec_namespace_serializes_with_deferred_child_tools() {
let namespace = LoadableToolSpec::Namespace(ResponsesApiNamespace {