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

@ -49,6 +49,11 @@ use crate::model_server::{
HfModelRef, LocalModelServerConfig, ModelPath, ModelServerEndpoint, ModelServerStatus,
};
use crate::permission::ProjectPermissions;
use crate::plugin::{
ContentHash, PluginBundleUrl, PluginId, PluginManifest, PluginMcpServerSpec,
PluginMcpStatusSet, PluginPackageRef, PluginRegistry, RelativePath, RemovalOutcome,
StagedPluginPackage,
};
use crate::profile::{AgentProfile, EmbedderProfile};
use crate::project::{Project, ProjectPath};
use crate::remote::RemoteKind;
@ -279,6 +284,166 @@ impl RemotePath {
}
}
/// Opaque local path supplied by a driving adapter.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LocalPath(pub String);
impl LocalPath {
/// Wraps a raw path.
#[must_use]
pub fn new(p: impl Into<String>) -> Self {
Self(p.into())
}
/// Returns the path as a string slice.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
/// Raw manifest bytes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginManifestBytes {
/// Bytes of `idea-plugin.json`.
pub bytes: Vec<u8>,
}
/// Plugin package store errors.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PluginStoreError {
/// Package was not found.
#[error("plugin package not found")]
NotFound,
/// Invalid input/source.
#[error("invalid plugin package: {0}")]
Invalid(String),
/// I/O error.
#[error("plugin package I/O error: {0}")]
Io(String),
/// Serialization or archive error.
#[error("plugin package format error: {0}")]
Format(String),
}
/// Plugin registry errors.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PluginRegistryError {
/// I/O error.
#[error("plugin registry I/O error: {0}")]
Io(String),
/// Serialization error.
#[error("plugin registry serialization error: {0}")]
Serialization(String),
}
/// Manifest validation errors.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PluginManifestError {
/// Invalid JSON.
#[error("invalid plugin manifest JSON: {0}")]
Json(String),
/// Invalid manifest data.
#[error("invalid plugin manifest: {0}")]
Invalid(String),
/// Incompatible engine.
#[error("incompatible IdeA engine: {0}")]
IncompatibleEngine(String),
}
/// Plugin MCP errors.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PluginMcpError {
/// Process/supervisor failure.
#[error("plugin MCP error: {0}")]
Process(String),
}
/// Store for installed plugin packages under the global app data directory.
#[async_trait]
pub trait PluginPackageStore: Send + Sync {
/// Lists committed package roots.
async fn list_installed(&self) -> Result<Vec<PluginPackageRef>, PluginStoreError>;
/// Reads `idea-plugin.json` from a package root.
async fn read_manifest(
&self,
package: &PluginPackageRef,
) -> Result<PluginManifestBytes, PluginStoreError>;
/// Stages an archive for validation.
async fn install_from_archive(
&self,
archive: &LocalPath,
) -> Result<StagedPluginPackage, PluginStoreError>;
/// Stages a directory snapshot for validation.
async fn install_from_directory(
&self,
dir: &LocalPath,
) -> Result<StagedPluginPackage, PluginStoreError>;
/// Commits a staged install into `installed/<pluginId>`.
async fn commit_install(
&self,
staged: StagedPluginPackage,
plugin_id: &PluginId,
) -> Result<PluginPackageRef, PluginStoreError>;
/// Removes a committed package.
async fn remove_package(
&self,
plugin_id: &PluginId,
) -> Result<RemovalOutcome, PluginStoreError>;
/// Builds a protocol URL for a bundle/asset.
fn bundle_url(
&self,
plugin_id: &PluginId,
entry: &RelativePath,
hash: &ContentHash,
) -> Result<PluginBundleUrl, PluginStoreError>;
/// Returns the global application data directory label when the adapter can
/// expose it for manifest variable substitution.
fn app_data_dir_label(&self) -> Option<String> {
None
}
}
/// Store for the global plugin registry.
#[async_trait]
pub trait PluginRegistryStore: Send + Sync {
/// Loads the registry, returning an empty document if missing.
async fn load_registry(&self) -> Result<PluginRegistry, PluginRegistryError>;
/// Persists the registry.
async fn save_registry(&self, registry: &PluginRegistry) -> Result<(), PluginRegistryError>;
}
/// Manifest parser and validator.
pub trait PluginManifestValidator: Send + Sync {
/// Validates raw manifest bytes.
fn validate(
&self,
bytes: &[u8],
package_root: &PluginPackageRef,
) -> Result<PluginManifest, PluginManifestError>;
}
/// Supervisor for external MCP servers declared by plugins.
#[async_trait]
pub trait PluginMcpSupervisor: Send + Sync {
/// Reconciles the desired active server set.
async fn reconcile(
&self,
active_servers: Vec<PluginMcpServerSpec>,
) -> Result<PluginMcpStatusSet, PluginMcpError>;
/// Stops every server owned by one plugin.
async fn stop_plugin(&self, plugin_id: &PluginId) -> Result<(), PluginMcpError>;
}
/// A single directory entry returned by [`FileSystem::list`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirEntry {