feat(backend): système de plugins — domaine, application, infrastructure (#43)

Lots B1-B4 : modèle de domaine des plugins (manifeste, capacités menus/layouts/MCP),
port et registre applicatif, chargement/validation en infrastructure, exposition DTO
et commandes Tauri. Tests cargo verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 07:37:03 +02:00
parent 47aacc6da9
commit bb35641715
24 changed files with 4304 additions and 41 deletions

View File

@ -20,6 +20,253 @@ use domain::{AgentBusyState, PageCursor, PageDirection, Project, ProjectId, Turn
pub use crate::ticket_dto::*;
// ---------------------------------------------------------------------------
// Plugins (#43)
// ---------------------------------------------------------------------------
/// Plugin contribution summary DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginContributionSummaryDto {
/// Top-level menus count.
pub top_level_menus: usize,
/// Menu items count.
pub menu_items: usize,
/// Layouts count.
pub layouts: usize,
/// MCP servers count.
pub mcp_servers: usize,
}
impl From<application::PluginContributionSummary> for PluginContributionSummaryDto {
fn from(value: application::PluginContributionSummary) -> Self {
Self {
top_level_menus: value.top_level_menus,
menu_items: value.menu_items,
layouts: value.layouts,
mcp_servers: value.mcp_servers,
}
}
}
/// Admin plugin DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginAdminDto {
/// Plugin id.
pub id: String,
/// Display name.
pub display_name: String,
/// Publisher.
pub publisher: Option<String>,
/// Version.
pub version: String,
/// Description.
pub description: Option<String>,
/// Icon URL.
pub icon_url: Option<String>,
/// Source kind.
pub source_kind: String,
/// Source label.
pub source_label: Option<String>,
/// Lifecycle state.
pub lifecycle_state: domain::PluginLifecycleState,
/// Enabled flag.
pub enabled: bool,
/// Pending enable state.
pub pending_enable_state: Option<bool>,
/// Pending uninstall flag.
pub pending_uninstall: bool,
/// Restart required flag.
pub restart_required: bool,
/// Trust level.
pub trust_level: domain::PluginTrustLevel,
/// Contribution summary.
pub contribution_summary: PluginContributionSummaryDto,
/// Optional error.
pub error: Option<String>,
}
impl From<application::PluginAdmin> for PluginAdminDto {
fn from(value: application::PluginAdmin) -> Self {
Self {
id: value.id,
display_name: value.display_name,
publisher: value.publisher,
version: value.version,
description: value.description,
icon_url: value.icon_url,
source_kind: value.source_kind,
source_label: value.source_label,
lifecycle_state: value.lifecycle_state,
enabled: value.enabled,
pending_enable_state: value.pending_enable_state,
pending_uninstall: value.pending_uninstall,
restart_required: value.restart_required,
trust_level: value.trust_level,
contribution_summary: value.contribution_summary.into(),
error: value.error,
}
}
}
/// Review request DTO.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReviewPluginPackageDto {
/// Source kind: `archive` or `directory`.
pub source_kind: String,
/// Local source path.
pub path: String,
}
/// Plugin review DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginReviewDto {
/// Manifest id.
pub id: String,
/// Display name.
pub display_name: String,
/// Publisher.
pub publisher: Option<String>,
/// Version.
pub version: String,
/// Description.
pub description: Option<String>,
/// Source kind.
pub source_kind: String,
/// Source label.
pub source_label: Option<String>,
/// Content hash.
pub content_hash: String,
/// Trust level.
pub trust_level: domain::PluginTrustLevel,
/// Summary.
pub contribution_summary: PluginContributionSummaryDto,
/// Manifest contributions.
pub contributes: domain::PluginContributionSet,
}
impl From<application::PluginReview> for PluginReviewDto {
fn from(value: application::PluginReview) -> Self {
Self {
id: value.manifest.id.as_str().to_owned(),
display_name: value.manifest.display_name,
publisher: value.manifest.publisher,
version: value.manifest.version.as_str().to_owned(),
description: value.manifest.description,
source_kind: value.source.kind().to_owned(),
source_label: Some(value.source.label().to_owned()),
content_hash: value.content_hash,
trust_level: value.trust_level,
contribution_summary: value.contribution_summary.into(),
contributes: value.manifest.contributes,
}
}
}
/// Plugin install result DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginInstallResultDto {
/// Installed plugin.
pub plugin: PluginAdminDto,
/// Review.
pub review: PluginReviewDto,
/// Restart required.
pub restart_required: bool,
}
impl From<application::PluginInstallResult> for PluginInstallResultDto {
fn from(value: application::PluginInstallResult) -> Self {
Self {
plugin: value.plugin.into(),
review: value.review.into(),
restart_required: value.restart_required,
}
}
}
/// Plugin uninstall result DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginUninstallResultDto {
/// Plugin id.
pub plugin_id: String,
/// Removal outcome.
pub removal_outcome: domain::RemovalOutcome,
/// Restart required.
pub restart_required: bool,
}
impl From<application::UninstallPluginResult> for PluginUninstallResultDto {
fn from(value: application::UninstallPluginResult) -> Self {
Self {
plugin_id: value.plugin_id,
removal_outcome: value.removal_outcome,
restart_required: value.restart_required,
}
}
}
/// Runtime catalog DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginRuntimeContributionCatalogDto {
/// Runtime plugins.
pub plugins: Vec<PluginRuntimePluginDto>,
}
/// Runtime plugin DTO.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginRuntimePluginDto {
/// Plugin id.
pub id: String,
/// Display name.
pub display_name: String,
/// Publisher.
pub publisher: Option<String>,
/// Version.
pub version: String,
/// Bundle URL.
pub bundle_url: String,
/// Icon URL.
pub icon_url: Option<String>,
/// Content hash.
pub content_hash: String,
/// Contributions.
pub contributes: domain::PluginContributionSet,
}
impl From<application::PluginRuntimeCatalog> for PluginRuntimeContributionCatalogDto {
fn from(value: application::PluginRuntimeCatalog) -> Self {
Self {
plugins: value
.plugins
.into_iter()
.map(PluginRuntimePluginDto::from)
.collect(),
}
}
}
impl From<application::PluginRuntimePlugin> for PluginRuntimePluginDto {
fn from(value: application::PluginRuntimePlugin) -> Self {
Self {
id: value.id,
display_name: value.display_name,
publisher: value.publisher,
version: value.version,
bundle_url: value.bundle_url,
icon_url: value.icon_url,
content_hash: value.content_hash,
contributes: value.contributes,
}
}
}
/// Request DTO for the `health` command.
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]