feat(infrastructure,backend,app-tauri,frontend): tools MCP workspace natifs read-only idea_workspace_map/list/read/search, activés par défaut et configurables dans les permissions MCP et les templates — #341 (QA verte)
This commit is contained in:
@ -64,8 +64,8 @@ use infrastructure::orchestrator::mcp::{
|
||||
TicketToolProvider,
|
||||
};
|
||||
use infrastructure::{
|
||||
InMemoryConversationRegistry, InMemoryMailbox, McpServer, MediatedInbox, MemoryTransport,
|
||||
SystemMillisClock, ToolPolicyRegistry,
|
||||
FsWorkspaceToolProvider, InMemoryConversationRegistry, InMemoryMailbox, McpServer,
|
||||
MediatedInbox, MemoryTransport, SystemMillisClock, ToolPolicyRegistry,
|
||||
};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
@ -824,6 +824,11 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
// Public plugin administration tools.
|
||||
"idea_plugin_install_from_directory",
|
||||
"idea_plugin_reload",
|
||||
// Read-only project workspace tools (#341).
|
||||
"idea_workspace_map",
|
||||
"idea_workspace_list",
|
||||
"idea_workspace_read",
|
||||
"idea_workspace_search",
|
||||
] {
|
||||
assert!(
|
||||
names.contains(&expected),
|
||||
@ -833,8 +838,8 @@ async fn tools_list_advertises_the_idea_tools_with_schemas() {
|
||||
assert!(!names.contains(&"idea_reply"));
|
||||
assert_eq!(
|
||||
tools.len(),
|
||||
50,
|
||||
"exactly the fifty exposed idea_* tools; got {names:?}"
|
||||
61,
|
||||
"exactly the sixty-one exposed idea_* tools; got {names:?}"
|
||||
);
|
||||
|
||||
// Every tool advertises an object input schema.
|
||||
@ -940,6 +945,14 @@ async fn requester_with_durable_store_but_no_override_sees_read_only_tools() {
|
||||
assert!(names.contains(&"idea_ticket_list"));
|
||||
assert!(names.contains(&"idea_template_list"));
|
||||
assert!(names.contains(&"idea_template_read"));
|
||||
for tool in [
|
||||
"idea_workspace_map",
|
||||
"idea_workspace_list",
|
||||
"idea_workspace_read",
|
||||
"idea_workspace_search",
|
||||
] {
|
||||
assert!(names.contains(&tool), "read-only fallback omitted {tool}");
|
||||
}
|
||||
assert!(!names.contains(&"idea_memory_write"));
|
||||
assert!(!names.contains(&"idea_ask_agent"));
|
||||
assert!(!names.contains(&"idea_ticket_update_carnet"));
|
||||
@ -953,6 +966,76 @@ async fn requester_with_durable_store_but_no_override_sees_read_only_tools() {
|
||||
assert!(!names.contains(&"idea_project_cancel_agent"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_tools_call_the_dedicated_provider_and_obey_explicit_policy() {
|
||||
let root = std::env::temp_dir().join(format!("idea-mcp-workspace-{}", Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
std::fs::write(root.join("note.txt"), "alpha needle\nbeta\n").unwrap();
|
||||
let scoped_project = Project::new(
|
||||
ProjectId::from_uuid(Uuid::from_u128(1001)),
|
||||
"workspace",
|
||||
ProjectPath::new(root.to_string_lossy()).unwrap(),
|
||||
RemoteRef::local(),
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
let (service, _s) = build_service(FakeContexts::new());
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(341));
|
||||
let allowed = McpServer::new(Arc::clone(&service), scoped_project.clone())
|
||||
.with_workspace_tools(Arc::new(FsWorkspaceToolProvider::new()))
|
||||
.with_mcp_tool_permissions(Arc::new(FakeMcpToolPermissionStore::new(
|
||||
ProjectMcpToolPermissions::default(),
|
||||
)))
|
||||
.for_requester(agent.to_string());
|
||||
let response = allowed
|
||||
.handle_raw(&tools_call(
|
||||
341,
|
||||
"idea_workspace_search",
|
||||
json!({"query":"needle"}),
|
||||
))
|
||||
.await
|
||||
.expect("workspace reply");
|
||||
assert!(response.error.is_none());
|
||||
let result = response.result.unwrap();
|
||||
assert_eq!(result["isError"], false);
|
||||
let payload: Value = serde_json::from_str(result_text(&result)).unwrap();
|
||||
assert_eq!(payload["matches"][0]["path"], "note.txt");
|
||||
|
||||
let denied = McpServer::new(service, scoped_project)
|
||||
.with_workspace_tools(Arc::new(FsWorkspaceToolProvider::new()))
|
||||
.with_mcp_tool_permissions(Arc::new(FakeMcpToolPermissionStore::new(allow_doc(
|
||||
agent,
|
||||
&["idea_memory_read"],
|
||||
))))
|
||||
.for_requester(agent.to_string());
|
||||
let listed = denied
|
||||
.handle_raw(
|
||||
&serde_json::to_vec(&json!({
|
||||
"jsonrpc":"2.0", "id":342, "method":"tools/list"
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.result
|
||||
.unwrap();
|
||||
assert!(!listed["tools"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|tool| { tool["name"] == "idea_workspace_search" }));
|
||||
let rejected = denied
|
||||
.handle_raw(&tools_call(
|
||||
343,
|
||||
"idea_workspace_search",
|
||||
json!({"query":"needle"}),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(rejected.error.unwrap().message.contains("not permitted"));
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cross_project_actions_are_advertised_only_when_allowed_and_denial_is_actionable() {
|
||||
let (service, _s) = build_service(FakeContexts::new());
|
||||
|
||||
Reference in New Issue
Block a user