fix(orchestrator,plugins): contact cross-project corrélé + découvrabilité assignation plugin par agent — #284 (backend: registre des demandes actives clé projet/agent appelant + correlationId, refus corrélation vide/dupliquée, cancel routé après correspondance exacte sans redirection vers une autre cible, appelant projeté Waiting→Done dans le live-state de son propre projet avec correlationId exposé par idea_workstate_read, gardes RAII réconcilient l'abandon du futur, tools ask/cancel WriteAction deny-by-default dont le refus indique où activer la permission; frontend: action Attribuer aux agents depuis Settings projet > Plugins ouvrant le panneau Agents flottant/ancré/détaché, section Capabilities de plugins — {agent} explicitant attribution skills et tools MCP des plugins activés; QA verte: orchestrator_service 79/79, mcp_server 40/40, domain live_state 15/15, correlated_update_is_preserved_in_the_lean_projection 1/1, frontend ProjectPluginSettings 3 tests, npm run build frontend, cargo fmt --check, git diff --check; AppImage IdeA_0.3.0_amd64.AppImage rebuildée workflow build-appimage, validation live cross-project restant après remplacement/relance manuels; réserves: registre corrélation en mémoire non persisté, surface web unsupported, catalogue dynamique tools MCP = réserve #282)
This commit is contained in:
@ -443,9 +443,14 @@ impl McpServer {
|
||||
} else {
|
||||
&self.requester
|
||||
};
|
||||
let guidance = if matches!(name, "idea_project_ask_agent" | "idea_project_cancel_agent") {
|
||||
"; enable this cross-project action in the requester's project MCP tool permissions"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
Err(JsonRpcError::new(
|
||||
error_codes::INVALID_PARAMS,
|
||||
format!("MCP tool `{name}` is not permitted for requester {requester}"),
|
||||
format!("MCP tool `{name}` is not permitted for requester {requester}{guidance}"),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -228,7 +228,7 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_project_ask_agent",
|
||||
description: "Ask a cross-project contactable agent by projectId and agentId and wait for its final answer.",
|
||||
description: "Ask a cross-project contactable agent by projectId and agentId and wait for its final answer. Requires explicit permission in the requester's project MCP tool settings.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -243,7 +243,7 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_project_cancel_agent",
|
||||
description: "Cancel/stop a cross-project contactable agent by projectId, agentId and correlationId.",
|
||||
description: "Cancel an active cross-project request by the same projectId, agentId and correlationId used for the ask. Requires explicit permission in the requester's project MCP tool settings.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@ -928,6 +928,60 @@ async fn requester_with_durable_store_but_no_override_sees_read_only_tools() {
|
||||
assert!(!names.contains(&"idea_template_delete"));
|
||||
assert!(!names.contains(&"idea_plugin_install_from_directory"));
|
||||
assert!(!names.contains(&"idea_plugin_reload"));
|
||||
assert!(!names.contains(&"idea_project_ask_agent"));
|
||||
assert!(!names.contains(&"idea_project_cancel_agent"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cross_project_actions_are_advertised_only_when_allowed_and_denial_is_actionable() {
|
||||
let (service, _s) = build_service(FakeContexts::new());
|
||||
let agent = AgentId::from_uuid(Uuid::from_u128(284));
|
||||
let allowed = server_with_mcp_permissions(
|
||||
Arc::clone(&service),
|
||||
allow_doc(
|
||||
agent,
|
||||
&["idea_project_ask_agent", "idea_project_cancel_agent"],
|
||||
),
|
||||
)
|
||||
.for_requester(agent.to_string());
|
||||
|
||||
let listed = allowed
|
||||
.handle_raw(
|
||||
&serde_json::to_vec(&json!({
|
||||
"jsonrpc": "2.0", "id": 284, "method": "tools/list"
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.expect("tools/list reply");
|
||||
let listed_result = listed.result.unwrap();
|
||||
let names: Vec<&str> = listed_result["tools"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|tool| tool["name"].as_str().unwrap())
|
||||
.collect();
|
||||
assert!(names.contains(&"idea_project_ask_agent"));
|
||||
assert!(names.contains(&"idea_project_cancel_agent"));
|
||||
|
||||
let denied = server_with_mcp_permissions(service, ProjectMcpToolPermissions::default())
|
||||
.for_requester(agent.to_string());
|
||||
let response = denied
|
||||
.handle_raw(&tools_call(
|
||||
285,
|
||||
"idea_project_ask_agent",
|
||||
json!({
|
||||
"projectId": Uuid::from_u128(2),
|
||||
"agentId": Uuid::from_u128(3),
|
||||
"correlationId": "corr-denied",
|
||||
"task": "work"
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.expect("denied call reply");
|
||||
let error = response.error.expect("permission error");
|
||||
assert!(error.message.contains("not permitted"));
|
||||
assert!(error.message.contains("project MCP tool permissions"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user