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:
@ -92,6 +92,25 @@ impl OrchestrationCapabilitySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
impl AssignedSkillSnapshot {
|
||||
/// Stable reference accepted by `idea_skill_read(ref=...)`.
|
||||
#[must_use]
|
||||
pub fn stable_ref(&self) -> String {
|
||||
if let Some(plugin) = &self.plugin {
|
||||
return format!(
|
||||
"plugin:{}:{}",
|
||||
plugin.plugin_id.as_str(),
|
||||
plugin.skill_id.as_str()
|
||||
);
|
||||
}
|
||||
let scope = match self.scope {
|
||||
SkillScope::Global => "global",
|
||||
SkillScope::Project => "project",
|
||||
};
|
||||
format!("skill:{scope}:{}", self.skill_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors raised while validating a raw [`OrchestratorRequest`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum OrchestratorError {
|
||||
@ -164,6 +183,10 @@ pub struct OrchestratorRequest {
|
||||
/// Target agent display name (required by every v1 action).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
/// Stable assigned-skill reference for `skill.read`. Preferred over `name`;
|
||||
/// kept separate so legacy name-based callers remain compatible.
|
||||
#[serde(default, rename = "ref", skip_serializing_if = "Option::is_none")]
|
||||
pub skill_ref: Option<String>,
|
||||
/// V2 target agent display name (`agent.run`/`agent.stop`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_agent: Option<String>,
|
||||
@ -411,13 +434,15 @@ pub enum OrchestratorCommand {
|
||||
/// The reading party (handshake identity).
|
||||
requester: ConversationParty,
|
||||
},
|
||||
/// Read a reusable skill's Markdown body **by name** (`idea_skill_read`).
|
||||
/// Read a reusable skill's Markdown body by stable ref, or by legacy name.
|
||||
/// The application layer authorizes the read against the requester's assigned
|
||||
/// runtime skill snapshot; unassigned skills are not readable. Read-only — no
|
||||
/// [`crate::fileguard::FileGuard`] lease (skills are not mutated through this path).
|
||||
ReadSkill {
|
||||
/// Skill display name to resolve (case-insensitive).
|
||||
name: String,
|
||||
/// Stable reference (`plugin:<pluginId>:<skillId>` for plugin skills).
|
||||
reference: Option<String>,
|
||||
/// Legacy display-name lookup (case-insensitive).
|
||||
name: Option<String>,
|
||||
/// The party that issued the read (handshake identity). Must be an agent
|
||||
/// requester for authorization against assigned skills.
|
||||
requester: ConversationParty,
|
||||
@ -651,10 +676,29 @@ impl OrchestratorRequest {
|
||||
slug: self.optional_slug(),
|
||||
requester: self.requester_party(),
|
||||
}),
|
||||
"skill.read" => Ok(OrchestratorCommand::ReadSkill {
|
||||
name: self.require_name(action)?,
|
||||
requester: self.requester_party(),
|
||||
}),
|
||||
"skill.read" => {
|
||||
let reference = self
|
||||
.skill_ref
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty());
|
||||
let name = self
|
||||
.name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty());
|
||||
if reference.is_none() && name.is_none() {
|
||||
return Err(OrchestratorError::MissingField {
|
||||
action: action.to_owned(),
|
||||
field: "ref or name".to_owned(),
|
||||
});
|
||||
}
|
||||
Ok(OrchestratorCommand::ReadSkill {
|
||||
reference: reference.map(str::to_owned),
|
||||
name: name.map(str::to_owned),
|
||||
requester: self.requester_party(),
|
||||
})
|
||||
}
|
||||
"memory.write" => Ok(OrchestratorCommand::WriteMemory {
|
||||
slug: self.require("slug", action, self.slug.as_deref())?,
|
||||
content: self.require("content", action, self.content.as_deref())?,
|
||||
|
||||
@ -924,6 +924,12 @@ pub struct PluginMcpServerSpec {
|
||||
pub cwd: String,
|
||||
/// Transport.
|
||||
pub transport: String,
|
||||
/// Exact MCP tool names authorized for the target agent.
|
||||
///
|
||||
/// This allowlist is carried with the launch spec so provider adapters never
|
||||
/// have to infer authorization from the server's advertised catalogue.
|
||||
#[serde(default)]
|
||||
pub authorized_tools: Vec<String>,
|
||||
}
|
||||
|
||||
impl PluginMcpServerSpec {
|
||||
@ -1044,6 +1050,7 @@ mod tests {
|
||||
env: Vec::new(),
|
||||
cwd: String::new(),
|
||||
transport: "stdio".to_owned(),
|
||||
authorized_tools: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user