feat(domain,application,infrastructure,backend,app-tauri,frontend): lot utilisateur #350 — animation d'écartement des onglets au drag/reorder, icônes projet N&B colorisées derrière les messages, tools/skills plugin utilisables par les agents autorisés en headless, refs stables des skills assignés (QA verte)
- Tabs (shared/ui): cible de drop matérialisée par écartement before/after pendant le drag/reorder, reset via rAF sans reliquat, clavier et annonces préservés. - projectIcons: icônes projet en noir et blanc par défaut, colorisées avec la couleur du projet, occupant la cellule et visibles derrière les messages rendus légèrement transparents. - Permissions plugin headless: PluginMcpServerSpec transporte l'allowlist exacte authorized_tools regroupée par (pluginId, serverId) depuis les PluginToolRef; projection Codex enabled_tools + default_tools_approval_mode="approve" — une permission déjà accordée au niveau projet/template ne déclenche pas de seconde approbation, un tool non assigné reste hors surface appelable; réserves orchestrator/mcp server+tools alignées, dto backend + dto_agents app-tauri. - Skills: ref stable plugin:<pluginId>:<skillId> exposée dans le contexte agent, idea_skill_read(ref=...) canonique via read_assigned_by_ref, erreur structurée ambiguousSkillName avec candidates; SkillsPanel dédoublonné (android-build-install); agents-dev/L12-skills.md à jour. - Docs: ARCHITECTURE.md section 22.3 (projection des autorisations agent vers les runtimes plugin). - QA verte #350 reconfirmée avant commit.
This commit is contained in:
@ -759,7 +759,17 @@ impl McpServer {
|
||||
// (`isError: true`) rather than a protocol error: the agent can read
|
||||
// it and decide, and the connection stays healthy.
|
||||
Err(e) => {
|
||||
let detail = e.to_string();
|
||||
let detail = match &e {
|
||||
application::AppError::AmbiguousSkillName { candidates, .. } => {
|
||||
serde_json::json!({
|
||||
"code": e.code(),
|
||||
"message": e.to_string(),
|
||||
"candidates": candidates,
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
_ => e.to_string(),
|
||||
};
|
||||
application::diag!(
|
||||
"[mcp] tools_call end tool={name} requester={requester_label} \
|
||||
target={arg_target} ok=false is_error=true result_len={} elapsed_ms={}",
|
||||
|
||||
@ -521,16 +521,21 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_skill_read",
|
||||
description: "Read the full body of an IdeA skill assigned to you, by name — call this \
|
||||
description: "Read the full body of an IdeA skill assigned to you, preferably by its stable ref. \
|
||||
Name lookup remains available for compatibility and fails when ambiguous — call this \
|
||||
to actually execute/follow a skill listed in your « Skills disponibles » \
|
||||
section. Resolves project scope first, then global. Returns the skill's \
|
||||
section. Returns the skill's \
|
||||
Markdown inline.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "Skill name (as listed in your context)." }
|
||||
"ref": { "type": "string", "description": "Stable skill ref (for example plugin:<pluginId>:<skillId>)." },
|
||||
"name": { "type": "string", "description": "Legacy skill name lookup (must be unambiguous)." }
|
||||
},
|
||||
"required": ["name"],
|
||||
"anyOf": [
|
||||
{ "required": ["ref"] },
|
||||
{ "required": ["name"] }
|
||||
],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
@ -767,6 +772,7 @@ pub fn map_tool_call(
|
||||
// The requester (handshake identity) is carried for symmetry with the
|
||||
// other read tools; `validate` derives the requester party from it.
|
||||
requested_by: Some(requester.to_owned()),
|
||||
skill_ref: s("ref"),
|
||||
name: s("name"),
|
||||
..base()
|
||||
},
|
||||
@ -809,6 +815,7 @@ fn base() -> OrchestratorRequest {
|
||||
request_type: None,
|
||||
requested_by: None,
|
||||
name: None,
|
||||
skill_ref: None,
|
||||
target_agent: None,
|
||||
profile: None,
|
||||
context: None,
|
||||
@ -1450,6 +1457,48 @@ mod tests {
|
||||
assert!(matches!(err, Err(ToolMapError::Invalid(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_read_prefers_stable_ref_and_keeps_name_compatibility() {
|
||||
let requester = domain::ConversationParty::agent(domain::AgentId::from_uuid(
|
||||
uuid::Uuid::parse_str(REQ).unwrap(),
|
||||
));
|
||||
assert_eq!(
|
||||
map_tool_call(
|
||||
"idea_skill_read",
|
||||
&json!({ "ref": "plugin:dev.idea.android:build-install" }),
|
||||
REQ,
|
||||
)
|
||||
.unwrap(),
|
||||
OrchestratorCommand::ReadSkill {
|
||||
reference: Some("plugin:dev.idea.android:build-install".to_owned()),
|
||||
name: None,
|
||||
requester: requester.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
map_tool_call("idea_skill_read", &json!({ "name": "build" }), REQ).unwrap(),
|
||||
OrchestratorCommand::ReadSkill {
|
||||
reference: None,
|
||||
name: Some("build".to_owned()),
|
||||
requester,
|
||||
}
|
||||
);
|
||||
assert!(matches!(
|
||||
map_tool_call("idea_skill_read", &json!({}), REQ),
|
||||
Err(ToolMapError::Invalid(_))
|
||||
));
|
||||
|
||||
let tool = catalogue()
|
||||
.into_iter()
|
||||
.find(|tool| tool.name == "idea_skill_read")
|
||||
.unwrap();
|
||||
assert!(tool.input_schema["properties"].get("ref").is_some());
|
||||
assert_eq!(
|
||||
tool.input_schema["anyOf"],
|
||||
json!([{ "required": ["ref"] }, { "required": ["name"] }])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ask_agent_is_an_inline_reply_tool_but_reply_is_not_exposed() {
|
||||
assert!(tool_returns_reply("idea_ask_agent"));
|
||||
|
||||
@ -890,6 +890,7 @@ mod tests {
|
||||
env: vec![("PLUGIN_ROOT".to_owned(), "/plugin".to_owned())],
|
||||
cwd: "/plugin".to_owned(),
|
||||
transport: "stdio".to_owned(),
|
||||
authorized_tools: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user