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")]

View File

@ -119,6 +119,60 @@ pub enum DomainEventDto {
/// Project id (UUID string).
project_id: String,
},
/// A plugin was installed.
#[serde(rename_all = "camelCase")]
PluginInstalled {
/// Plugin id.
plugin_id: String,
/// Installed version.
version: String,
},
/// A plugin was enabled.
#[serde(rename_all = "camelCase")]
PluginEnabled {
/// Plugin id.
plugin_id: String,
},
/// A plugin was disabled.
#[serde(rename_all = "camelCase")]
PluginDisabled {
/// Plugin id.
plugin_id: String,
/// Whether a restart is needed for full JS purge.
restart_required: bool,
},
/// A plugin was uninstalled.
#[serde(rename_all = "camelCase")]
PluginUninstalled {
/// Plugin id.
plugin_id: String,
/// Whether a restart is needed for full JS purge.
restart_required: bool,
},
/// A plugin failed to load.
#[serde(rename_all = "camelCase")]
PluginLoadFailed {
/// Plugin id.
plugin_id: String,
/// Failure reason.
reason: String,
},
/// A plugin MCP server started.
#[serde(rename_all = "camelCase")]
PluginMcpServerStarted {
/// Plugin id.
plugin_id: String,
/// Server id.
server_id: String,
},
/// A plugin MCP server stopped.
#[serde(rename_all = "camelCase")]
PluginMcpServerStopped {
/// Plugin id.
plugin_id: String,
/// Server id.
server_id: String,
},
/// An agent was launched.
#[serde(rename_all = "camelCase")]
AgentLaunched {
@ -677,6 +731,45 @@ impl From<&DomainEvent> for DomainEventDto {
DomainEvent::ProjectCreated { project_id } => Self::ProjectCreated {
project_id: project_id.to_string(),
},
DomainEvent::PluginInstalled { plugin_id, version } => Self::PluginInstalled {
plugin_id: plugin_id.to_string(),
version: version.as_str().to_owned(),
},
DomainEvent::PluginEnabled { plugin_id } => Self::PluginEnabled {
plugin_id: plugin_id.to_string(),
},
DomainEvent::PluginDisabled {
plugin_id,
restart_required,
} => Self::PluginDisabled {
plugin_id: plugin_id.to_string(),
restart_required: *restart_required,
},
DomainEvent::PluginUninstalled {
plugin_id,
restart_required,
} => Self::PluginUninstalled {
plugin_id: plugin_id.to_string(),
restart_required: *restart_required,
},
DomainEvent::PluginLoadFailed { plugin_id, reason } => Self::PluginLoadFailed {
plugin_id: plugin_id.to_string(),
reason: reason.clone(),
},
DomainEvent::PluginMcpServerStarted {
plugin_id,
server_id,
} => Self::PluginMcpServerStarted {
plugin_id: plugin_id.to_string(),
server_id: server_id.as_str().to_owned(),
},
DomainEvent::PluginMcpServerStopped {
plugin_id,
server_id,
} => Self::PluginMcpServerStopped {
plugin_id: plugin_id.to_string(),
server_id: server_id.as_str().to_owned(),
},
DomainEvent::AgentLaunched {
agent_id,
session_id,

View File

@ -24,27 +24,30 @@ use application::{
EnsureLocalModelServer, FirstRunState, GetAppExitWorkGuardState, GetLiveStateLean, GetMemory,
GetProjectPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph,
GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase,
InspectConversation, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput,
ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers,
ListProfiles, ListProjects, ListResumableAgents, ListSkills, ListSprints, ListTemplates,
LiveAgentRegistry, LiveSessions, LiveStateLeanProvider, LiveStateProvider,
LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue, MoveTabToNewWindow,
MutateLayout, OnnxModelView, OpenProject, OpenTerminal, OpenTicketAssistant,
OrchestratorService, PairAttemptLimiter, PairDevice, PermissionProjectorRegistry,
ProposeContext, ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue,
ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex, ReadProjectContext,
ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts, ReconcileLiveState,
ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameDevice,
RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentPermissions,
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, RevokeAllDevices, RevokeDevice,
InspectConversation, InstallPluginFromArchive, InstallPluginFromDirectory,
JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
ListAgentsInput, ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories,
ListModelServers, ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects,
ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions,
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime,
McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
OpenTerminal, OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
ReconcileLiveState, ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn,
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks,
RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage, RevokeAllDevices, RevokeDevice,
RotateConversationLog, SaveEmbedderProfile, SaveModelServer, SaveProfile, SessionLimitService,
SetActiveLayout, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand,
StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession,
SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent,
UnassignTicketFromSprint, UnlinkIssues, UpdateAgentContext, UpdateAgentMcpToolPermissions,
UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory,
UpdateProjectContext, UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateSkill,
UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
SetActiveLayout, SetPluginEnabled, SnapshotOpenWindows, SnapshotRunningAgents,
SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode, StructuredSessions,
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
UnassignSkillFromAgent, UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues,
UpdateAgentContext, UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateIssue,
UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateSkill, UpdateTemplate,
WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
};
use async_trait::async_trait;
use domain::ports::{
@ -53,9 +56,10 @@ use domain::ports::{
BackgroundTaskStore, Clock, DeviceSessionStore, Embedder, EmbedderEnvInspector,
EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator,
IssueNumberAllocator, IssueStore, McpToolPermissionStore, MemoryRecall, MemoryStore,
PermissionStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, ScheduledTask, Scheduler,
SkillStore, SprintStore, StructuredSessionEnvironmentPreparer, TemplateStore, ToolInvoker,
WakeError, WakeReason, WindowStateStore,
PermissionStore, PluginManifestValidator, PluginMcpSupervisor, PluginPackageStore,
PluginRegistryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, ScheduledTask,
Scheduler, SkillStore, SprintStore, StructuredSessionEnvironmentPreparer, TemplateStore,
ToolInvoker, WakeError, WakeReason, WindowStateStore,
};
use domain::profile::{
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
@ -73,10 +77,11 @@ use infrastructure::{
embedder_from_profile, AdaptiveMemoryRecall, BackgroundCompletionSink,
BackgroundTaskReadyToDeliver, ClaudePermissionProjector, ClaudeTranscriptInspector,
CliAgentRuntime, CodexPermissionProjector, CommandBackgroundRunner, EmbedderEnvProbe,
FsAssistantContextStore, FsBackgroundTaskStore, FsConversationLog, FsDeviceSessionStore,
FsEmbedderProfileStore, FsEmbedderPromptStore, FsHandoffStore, FsIssueNumberAllocator,
FsIssueStore, FsLiveStateStore, FsMcpToolPermissionStore, FsMemoryStore, FsModelServerRegistry,
FsOrchestratorWatcher, FsPermissionStore, FsProfileStore, FsProjectStore,
ExternalMcpPluginSupervisor, FsAssistantContextStore, FsBackgroundTaskStore, FsConversationLog,
FsDeviceSessionStore, FsEmbedderProfileStore, FsEmbedderPromptStore, FsHandoffStore,
FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsMcpToolPermissionStore,
FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher, FsPermissionStore,
FsPluginPackageStore, FsPluginRegistryStore, FsProfileStore, FsProjectStore,
FsProviderSessionStore, FsSkillStore, FsSprintStore, FsTemplateStore, FsWindowStateStore,
Git2Repository, HeuristicHandoffSummarizer, HfModelArtifactDownloader,
HttpOpenAiCompatibleProbe, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
@ -1062,6 +1067,29 @@ pub struct BackendCore {
pub retry_background_task: Arc<RetryBackgroundTask>,
/// Store handle used by `list_background_tasks` to read the task read-model.
pub background_task_store: Arc<dyn BackgroundTaskStore>,
// --- Plugins (#43) ---
/// Review a local plugin package before install.
pub review_plugin_package: Arc<ReviewPluginPackage>,
/// Install a plugin archive.
pub install_plugin_from_archive: Arc<InstallPluginFromArchive>,
/// Install a plugin directory snapshot.
pub install_plugin_from_directory: Arc<InstallPluginFromDirectory>,
/// List installed plugins for admin UI.
pub list_plugins: Arc<ListPlugins>,
/// Enable or disable a plugin.
pub set_plugin_enabled: Arc<SetPluginEnabled>,
/// Uninstall a plugin.
pub uninstall_plugin: Arc<UninstallPlugin>,
/// List runtime contributions for UI bootstrap.
pub list_plugin_runtime_contributions: Arc<ListPluginRuntimeContributions>,
/// Reconcile external MCP plugin servers.
pub reconcile_plugin_mcp_servers: Arc<ReconcilePluginMcpServers>,
/// Package store exposed for the Tauri asset protocol adapter.
pub plugin_package_store: Arc<FsPluginPackageStore>,
/// Registry store exposed for the Tauri asset protocol adapter.
pub plugin_registry_store: Arc<dyn PluginRegistryStore>,
/// Manifest validator exposed for the Tauri asset protocol adapter.
pub plugin_manifest_validator: Arc<dyn PluginManifestValidator>,
// --- Templates & sync (L7) ---
/// Create a template in the global store.
pub create_template: Arc<CreateTemplate>,
@ -1233,6 +1261,17 @@ impl BackendCore {
let pair_attempt_limiter_port =
Arc::clone(&pair_attempt_limiter) as Arc<dyn PairAttemptLimiter>;
let events_port = Arc::clone(&event_bus) as Arc<dyn EventBus>;
let plugin_packages = Arc::new(FsPluginPackageStore::new(app_data_dir.clone()));
let plugin_registry = Arc::new(FsPluginRegistryStore::new(app_data_dir.clone()));
let plugin_validator =
Arc::new(JsonPluginManifestValidator::new(env!("CARGO_PKG_VERSION")));
let plugin_mcp_supervisor = Arc::new(ExternalMcpPluginSupervisor::new());
let plugin_package_store = Arc::clone(&plugin_packages) as Arc<dyn PluginPackageStore>;
let plugin_registry_store = Arc::clone(&plugin_registry) as Arc<dyn PluginRegistryStore>;
let plugin_manifest_validator =
Arc::clone(&plugin_validator) as Arc<dyn PluginManifestValidator>;
let plugin_mcp_supervisor_port =
Arc::clone(&plugin_mcp_supervisor) as Arc<dyn PluginMcpSupervisor>;
// --- Use cases (ports injected as Arc<dyn Port>) ---
let health = Arc::new(HealthUseCase::new(
@ -2089,6 +2128,53 @@ impl BackendCore {
Arc::clone(&background_runner) as Arc<dyn BackgroundCommandArchive>,
Arc::clone(&spawn_background_command),
));
let review_plugin_package = Arc::new(ReviewPluginPackage::new(
Arc::clone(&plugin_package_store),
Arc::clone(&plugin_manifest_validator),
));
let list_plugins = Arc::new(ListPlugins::new(
Arc::clone(&plugin_package_store),
Arc::clone(&plugin_registry_store),
Arc::clone(&plugin_manifest_validator),
));
let install_plugin_from_archive = Arc::new(InstallPluginFromArchive::new(
Arc::clone(&plugin_package_store),
Arc::clone(&plugin_registry_store),
Arc::clone(&plugin_manifest_validator),
Arc::clone(&events_port),
Arc::clone(&plugin_mcp_supervisor_port),
));
let install_plugin_from_directory = Arc::new(InstallPluginFromDirectory::new(
Arc::clone(&plugin_package_store),
Arc::clone(&plugin_registry_store),
Arc::clone(&plugin_manifest_validator),
Arc::clone(&events_port),
Arc::clone(&plugin_mcp_supervisor_port),
));
let set_plugin_enabled = Arc::new(SetPluginEnabled::new(
Arc::clone(&plugin_package_store),
Arc::clone(&plugin_registry_store),
Arc::clone(&plugin_manifest_validator),
Arc::clone(&events_port),
Arc::clone(&plugin_mcp_supervisor_port),
));
let uninstall_plugin = Arc::new(UninstallPlugin::new(
Arc::clone(&plugin_package_store),
Arc::clone(&plugin_registry_store),
Arc::clone(&events_port),
Arc::clone(&plugin_mcp_supervisor_port),
));
let list_plugin_runtime_contributions = Arc::new(ListPluginRuntimeContributions::new(
Arc::clone(&plugin_package_store),
Arc::clone(&plugin_registry_store),
Arc::clone(&plugin_manifest_validator),
));
let reconcile_plugin_mcp_servers = Arc::new(ReconcilePluginMcpServers::new(
Arc::clone(&plugin_package_store),
Arc::clone(&plugin_registry_store),
Arc::clone(&plugin_manifest_validator),
Arc::clone(&plugin_mcp_supervisor_port),
));
let background_wake = Arc::new(AgentWakeService::new(
Arc::clone(&mediated_inbox) as Arc<dyn AgentInbox>,
Arc::clone(&input_mediator),
@ -2613,6 +2699,17 @@ impl BackendCore {
cancel_background_task,
retry_background_task,
background_task_store: Arc::clone(&background_tasks_port),
review_plugin_package,
install_plugin_from_archive,
install_plugin_from_directory,
list_plugins,
set_plugin_enabled,
uninstall_plugin,
list_plugin_runtime_contributions,
reconcile_plugin_mcp_servers,
plugin_package_store: Arc::clone(&plugin_packages),
plugin_registry_store: Arc::clone(&plugin_registry_store),
plugin_manifest_validator: Arc::clone(&plugin_manifest_validator),
ticket_tool_binder,
template_tool_binder,
}