Compare commits

...

1 Commits

Author SHA1 Message Date
colby-oai
417bb835f8 for tool calls using fileparams metadata, insert information into our safety monitors 2026-04-28 15:42:17 -04:00
5 changed files with 205 additions and 0 deletions

View File

@@ -61,6 +61,7 @@ pub(crate) enum GuardianApprovalRequest {
server: String,
tool_name: String,
arguments: Option<Value>,
file_content_sharing: Option<GuardianFileContentSharing>,
connector_id: Option<String>,
connector_name: Option<String>,
connector_description: Option<String>,
@@ -102,6 +103,18 @@ pub(crate) struct GuardianMcpAnnotations {
pub(crate) read_only_hint: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct GuardianFileContentSharing {
pub(crate) summary: &'static str,
pub(crate) arguments: Vec<GuardianFileContentSharingArgument>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct GuardianFileContentSharingArgument {
pub(crate) name: String,
pub(crate) local_paths: Vec<String>,
}
#[derive(Serialize)]
struct CommandApprovalAction<'a> {
tool: &'a str,
@@ -135,6 +148,8 @@ struct McpToolCallApprovalAction<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
arguments: Option<&'a Value>,
#[serde(skip_serializing_if = "Option::is_none")]
file_content_sharing: Option<&'a GuardianFileContentSharing>,
#[serde(skip_serializing_if = "Option::is_none")]
connector_id: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
connector_name: Option<&'a String>,
@@ -340,6 +355,7 @@ pub(crate) fn guardian_approval_request_to_json(
server,
tool_name,
arguments,
file_content_sharing,
connector_id,
connector_name,
connector_description,
@@ -351,6 +367,7 @@ pub(crate) fn guardian_approval_request_to_json(
server,
tool_name,
arguments: arguments.as_ref(),
file_content_sharing: file_content_sharing.as_ref(),
connector_id: connector_id.as_ref(),
connector_name: connector_name.as_ref(),
connector_description: connector_description.as_ref(),

View File

@@ -24,6 +24,8 @@ use serde::Deserialize;
use serde::Serialize;
pub(crate) use approval_request::GuardianApprovalRequest;
pub(crate) use approval_request::GuardianFileContentSharing;
pub(crate) use approval_request::GuardianFileContentSharingArgument;
pub(crate) use approval_request::GuardianMcpAnnotations;
pub(crate) use approval_request::GuardianNetworkAccessTrigger;
pub(crate) use approval_request::guardian_approval_request_to_json;

View File

@@ -724,6 +724,7 @@ fn guardian_approval_request_to_json_renders_mcp_tool_call_shape() -> serde_json
arguments: Some(serde_json::json!({
"url": "https://example.com",
})),
file_content_sharing: None,
connector_id: None,
connector_name: Some("Playwright".to_string()),
connector_description: None,
@@ -756,6 +757,55 @@ fn guardian_approval_request_to_json_renders_mcp_tool_call_shape() -> serde_json
Ok(())
}
#[test]
fn guardian_approval_request_to_json_renders_file_content_sharing() -> serde_json::Result<()> {
let action = GuardianApprovalRequest::McpToolCall {
id: "call-1".to_string(),
server: "codex_apps".to_string(),
tool_name: "gmail_send_email".to_string(),
arguments: Some(serde_json::json!({
"attachment_files": ["/Users/alice/report.pdf"],
})),
file_content_sharing: Some(GuardianFileContentSharing {
summary: "Local file contents from the listed paths will be shared with the downstream MCP server.",
arguments: vec![GuardianFileContentSharingArgument {
name: "attachment_files".to_string(),
local_paths: vec!["/Users/alice/report.pdf".to_string()],
}],
}),
connector_id: None,
connector_name: Some("Gmail".to_string()),
connector_description: None,
tool_title: Some("send_email".to_string()),
tool_description: None,
annotations: None,
};
assert_eq!(
guardian_approval_request_to_json(&action)?,
serde_json::json!({
"tool": "mcp_tool_call",
"server": "codex_apps",
"tool_name": "gmail_send_email",
"arguments": {
"attachment_files": ["/Users/alice/report.pdf"],
},
"file_content_sharing": {
"summary": "Local file contents from the listed paths will be shared with the downstream MCP server.",
"arguments": [
{
"name": "attachment_files",
"local_paths": ["/Users/alice/report.pdf"],
},
],
},
"connector_name": "Gmail",
"tool_title": "send_email",
})
);
Ok(())
}
#[test]
fn guardian_approval_request_to_json_renders_network_access_trigger() -> serde_json::Result<()> {
let cwd = test_path_buf("/repo").abs();

View File

@@ -18,6 +18,8 @@ use crate::config::edit::ConfigEditsBuilder;
use crate::config::load_global_mcp_servers;
use crate::connectors;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::GuardianFileContentSharing;
use crate::guardian::GuardianFileContentSharingArgument;
use crate::guardian::GuardianMcpAnnotations;
use crate::guardian::guardian_approval_request_to_json;
use crate::guardian::guardian_rejection_message;
@@ -1141,6 +1143,10 @@ pub(crate) fn build_guardian_mcp_tool_review_request(
server: invocation.server.clone(),
tool_name: invocation.tool.clone(),
arguments: invocation.arguments.clone(),
file_content_sharing: guardian_file_content_sharing_for_openai_file_params(
invocation.arguments.as_ref(),
metadata.and_then(|metadata| metadata.openai_file_input_params.as_deref()),
),
connector_id: metadata.and_then(|metadata| metadata.connector_id.clone()),
connector_name: metadata.and_then(|metadata| metadata.connector_name.clone()),
connector_description: metadata.and_then(|metadata| metadata.connector_description.clone()),
@@ -1156,6 +1162,39 @@ pub(crate) fn build_guardian_mcp_tool_review_request(
}
}
fn guardian_file_content_sharing_for_openai_file_params(
arguments: Option<&serde_json::Value>,
openai_file_input_params: Option<&[String]>,
) -> Option<GuardianFileContentSharing> {
let arguments = arguments?.as_object()?;
let shared_arguments = openai_file_input_params?
.iter()
.filter_map(|field_name| {
let value = arguments.get(field_name)?;
let local_paths = match value {
serde_json::Value::String(path) => vec![path.clone()],
serde_json::Value::Array(values) => values
.iter()
.map(serde_json::Value::as_str)
.collect::<Option<Vec<_>>>()?
.into_iter()
.map(str::to_string)
.collect(),
_ => return None,
};
(!local_paths.is_empty()).then(|| GuardianFileContentSharingArgument {
name: field_name.clone(),
local_paths,
})
})
.collect::<Vec<_>>();
(!shared_arguments.is_empty()).then_some(GuardianFileContentSharing {
summary: "Local file contents from the listed paths will be shared with the downstream MCP server.",
arguments: shared_arguments,
})
}
async fn mcp_tool_approval_decision_from_guardian(
sess: &Session,
review_id: &str,

View File

@@ -1073,6 +1073,7 @@ fn guardian_mcp_review_request_includes_invocation_metadata() {
arguments: Some(serde_json::json!({
"url": "https://example.com",
})),
file_content_sharing: None,
connector_id: Some("playwright".to_string()),
connector_name: Some("Playwright".to_string()),
connector_description: Some("Browser automation".to_string()),
@@ -1083,6 +1084,56 @@ fn guardian_mcp_review_request_includes_invocation_metadata() {
);
}
#[test]
fn guardian_mcp_review_request_includes_file_content_sharing_for_openai_file_params() {
let invocation = McpInvocation {
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool: "gmail_send_email".to_string(),
arguments: Some(serde_json::json!({
"to": "alice@example.com",
"attachment_files": ["/Users/alice/report.pdf"],
})),
};
let mut metadata = approval_metadata(
Some("gmail"),
Some("Gmail"),
Some("Find and reference emails from your inbox."),
Some("send_email"),
Some("Send an email from the authenticated Gmail account."),
);
metadata.openai_file_input_params = Some(vec!["attachment_files".to_string()]);
let request = build_guardian_mcp_tool_review_request("call-1", &invocation, Some(&metadata));
assert_eq!(
request,
GuardianApprovalRequest::McpToolCall {
id: "call-1".to_string(),
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "gmail_send_email".to_string(),
arguments: Some(serde_json::json!({
"to": "alice@example.com",
"attachment_files": ["/Users/alice/report.pdf"],
})),
file_content_sharing: Some(GuardianFileContentSharing {
summary: "Local file contents from the listed paths will be shared with the downstream MCP server.",
arguments: vec![GuardianFileContentSharingArgument {
name: "attachment_files".to_string(),
local_paths: vec!["/Users/alice/report.pdf".to_string()],
}],
}),
connector_id: Some("gmail".to_string()),
connector_name: Some("Gmail".to_string()),
connector_description: Some("Find and reference emails from your inbox.".to_string()),
tool_title: Some("send_email".to_string()),
tool_description: Some(
"Send an email from the authenticated Gmail account.".to_string()
),
annotations: None,
}
);
}
#[test]
fn guardian_mcp_review_request_includes_annotations_when_present() {
let invocation = McpInvocation {
@@ -1111,6 +1162,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
server: "custom_server".to_string(),
tool_name: "dangerous_tool".to_string(),
arguments: None,
file_content_sharing: None,
connector_id: None,
connector_name: None,
connector_description: None,
@@ -1161,6 +1213,51 @@ fn prepare_arc_request_action_serializes_mcp_tool_call_shape() {
);
}
#[test]
fn prepare_arc_request_action_includes_file_content_sharing() {
let invocation = McpInvocation {
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool: "gmail_send_email".to_string(),
arguments: Some(serde_json::json!({
"attachment_files": "/Users/alice/report.pdf",
})),
};
let mut metadata = approval_metadata(
Some("gmail"),
Some("Gmail"),
/*connector_description*/ None,
Some("send_email"),
/*tool_description*/ None,
);
metadata.openai_file_input_params = Some(vec!["attachment_files".to_string()]);
let action = prepare_arc_request_action(&invocation, Some(&metadata));
assert_eq!(
action,
serde_json::json!({
"tool": "mcp_tool_call",
"server": CODEX_APPS_MCP_SERVER_NAME,
"tool_name": "gmail_send_email",
"arguments": {
"attachment_files": "/Users/alice/report.pdf",
},
"file_content_sharing": {
"summary": "Local file contents from the listed paths will be shared with the downstream MCP server.",
"arguments": [
{
"name": "attachment_files",
"local_paths": ["/Users/alice/report.pdf"],
},
],
},
"connector_id": "gmail",
"connector_name": "Gmail",
"tool_title": "send_email",
})
);
}
#[tokio::test(flavor = "current_thread")]
async fn guardian_review_decision_maps_to_mcp_tool_decision() {
let (session, _) = make_session_and_context().await;