feat(mcp): enforce per-server tool allowlists across runtime, jobs, agents, and graph nodes
This commit is contained in:
@@ -686,6 +686,7 @@ pub async fn run_agent_for_graph(
|
||||
sync_agent_functions_to_ctx(&mut child_ctx)?;
|
||||
} else {
|
||||
populate_agent_mcp_runtime(&mut child_ctx, &agent_mcp_servers).await?;
|
||||
child_ctx.refresh_mcp_tool_filters();
|
||||
sync_agent_functions_to_ctx(&mut child_ctx)?;
|
||||
child_ctx.init_agent_shared_variables()?;
|
||||
}
|
||||
@@ -869,6 +870,7 @@ async fn handle_spawn(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
|
||||
sync_agent_functions_to_ctx(&mut child_ctx)?;
|
||||
} else {
|
||||
populate_agent_mcp_runtime(&mut child_ctx, &agent_mcp_servers).await?;
|
||||
child_ctx.refresh_mcp_tool_filters();
|
||||
sync_agent_functions_to_ctx(&mut child_ctx)?;
|
||||
child_ctx.init_agent_shared_variables()?;
|
||||
}
|
||||
@@ -1604,6 +1606,7 @@ mod tests {
|
||||
use crate::config::test_fixtures::{FixtureServer, fixture_runtime};
|
||||
use crate::config::{AgentConfig, AppState, WorkingMode};
|
||||
use crate::function::jobs::RingBuf;
|
||||
use crate::mcp::{McpServer, McpServersConfig, McpTransportType};
|
||||
use crate::supervisor::escalation::{EscalationQueue, EscalationRequest};
|
||||
use crate::supervisor::{JobHandle, JobResult, JobState, JobStatus};
|
||||
use parking_lot::Mutex;
|
||||
@@ -1791,6 +1794,61 @@ mod tests {
|
||||
assert!(!functions.contains("mcp_invoke_fixture"));
|
||||
}
|
||||
|
||||
fn app_state_with_fixture_mcp_config() -> Arc<AppState> {
|
||||
let mut state = AppState::test_default();
|
||||
state.mcp_config = Some(McpServersConfig {
|
||||
mcp_servers: [(
|
||||
"fixture".to_string(),
|
||||
McpServer {
|
||||
transport_type: McpTransportType::Stdio,
|
||||
command: Some("echo".to_string()),
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: None,
|
||||
oauth: None,
|
||||
allowed_tools: None,
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
});
|
||||
Arc::new(state)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawned_child_runtime_enforces_child_agent_filters() {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
let config = AgentConfig {
|
||||
mcp_tools: Some(IndexMap::from([(
|
||||
"fixture".to_string(),
|
||||
vec!["get_*".to_string()],
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let mut ctx = RequestContext::new(app_state_with_fixture_mcp_config(), WorkingMode::Cmd);
|
||||
ctx.agent = Some(Agent::test_new(config));
|
||||
let fixture = FixtureServer::default();
|
||||
let call_tool_calls = Arc::clone(&fixture.call_tool_calls);
|
||||
let (runtime, _server) = fixture_runtime(fixture).await;
|
||||
ctx.tool_scope.mcp_runtime = runtime;
|
||||
|
||||
populate_agent_mcp_runtime(&mut ctx, &[]).await.unwrap();
|
||||
ctx.refresh_mcp_tool_filters();
|
||||
|
||||
let err = ctx
|
||||
.tool_scope
|
||||
.mcp_runtime
|
||||
.invoke("fixture", "dup", json!({}))
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert_eq!(err, "dup not found in fixture MCP server catalog");
|
||||
assert_eq!(call_tool_calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_list_running_empty_supervisor() {
|
||||
let mut ctx = ctx_with_supervisor(4, 3);
|
||||
|
||||
@@ -457,6 +457,11 @@ async fn handle_start(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let mut mcp_runtime = McpRuntime::new();
|
||||
mcp_runtime.insert(server.clone(), Arc::clone(server_handle));
|
||||
if let Some(filter) = ctx.tool_scope.mcp_runtime.tool_filters.get(&server) {
|
||||
mcp_runtime
|
||||
.tool_filters
|
||||
.insert(server.clone(), filter.clone());
|
||||
}
|
||||
let job_ctx = JobCtx {
|
||||
mcp_runtime,
|
||||
current_depth: ctx.current_depth,
|
||||
@@ -1263,7 +1268,9 @@ fn tail_chars(text: &str, max_chars: usize) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::test_fixtures::{FixtureServer, fixture_runtime};
|
||||
use crate::config::{AppConfig, AppState, WorkingMode};
|
||||
use crate::config::{LayerSource, ToolFilter};
|
||||
use crate::function::agents::{
|
||||
GuardrailAction, check_pending_tasks_guardrail, handle_agent_tool,
|
||||
};
|
||||
@@ -1697,6 +1704,43 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_runtime_carries_the_servers_tool_filter() {
|
||||
run_async(async {
|
||||
let mut ctx = plain_ctx();
|
||||
ctx.declared_function_names
|
||||
.insert("mcp_invoke_fixture".into());
|
||||
let fixture = FixtureServer::default();
|
||||
let call_tool_calls = Arc::clone(&fixture.call_tool_calls);
|
||||
let (mut runtime, _server) = fixture_runtime(fixture).await;
|
||||
let mut filter = ToolFilter::default();
|
||||
filter.push_layer(LayerSource::Global, &[]);
|
||||
runtime.tool_filters.insert("fixture".to_string(), filter);
|
||||
ctx.tool_scope.mcp_runtime = runtime;
|
||||
|
||||
let started = handle_start(
|
||||
&mut ctx,
|
||||
&json!({"tool": "mcp_invoke_fixture", "arguments": {"tool": "dup"}}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let job_id = started["job_id"].as_str().unwrap().to_string();
|
||||
|
||||
let collected = handle_collect(&ctx, &json!({"id": job_id})).await.unwrap();
|
||||
|
||||
assert_eq!(collected["status"], "failed");
|
||||
assert!(
|
||||
collected["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("dup not found in fixture MCP server catalog"),
|
||||
"unexpected error: {}",
|
||||
collected["error"]
|
||||
);
|
||||
assert_eq!(call_tool_calls.load(std::sync::atomic::Ordering::SeqCst), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_start_rejects_unconnected_mcp_server() {
|
||||
let mut ctx = plain_ctx();
|
||||
|
||||
Reference in New Issue
Block a user