use anyhow::{Context, Result};
use bmux_config::{BmuxConfig, ConfigPaths};
use bmux_plugin::{
NativeServiceBufferConfig, PluginManifest, PluginRegistry, PluginRuntime,
load_registered_plugin_with_native_service_buffer_config as load_native_registered_plugin,
};
use bmux_plugin_sdk::{
CURRENT_PLUGIN_ABI_VERSION, CURRENT_PLUGIN_API_VERSION, HostConnectionInfo, HostMetadata,
HostScope, NativeCommandContext, NativeLifecycleContext, PluginCommandOutcome, PluginEvent,
PluginEventKind, RegisteredService,
perf_telemetry::{PhaseChannel, PhasePayload, emit as emit_phase_timing},
};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::{debug, instrument, warn};
use super::{
KernelClientFactory, available_capability_providers, available_service_descriptors,
connect_raw, core_provided_capabilities, enter_host_kernel_client_factory,
enter_host_kernel_connection, host_kernel_bridge, map_cli_client_error,
plugin_commands::PluginCommandRegistry, plugin_host, server_event_name,
service_descriptors_from_declarations,
};
#[derive(Debug, Clone)]
pub(super) struct RuntimeCommandState {
pub(super) config: BmuxConfig,
pub(super) paths: ConfigPaths,
pub(super) registry: Arc<PluginRegistry>,
pub(super) enabled_plugins: Vec<String>,
pub(super) available_capability_providers: BTreeMap<HostScope, bmux_plugin::CapabilityProvider>,
pub(super) available_capability_names: Vec<String>,
pub(super) available_services: Vec<RegisteredService>,
pub(super) plugin_search_roots: Vec<String>,
pub(super) registered_plugin_infos: Vec<bmux_plugin_sdk::RegisteredPluginInfo>,
}
thread_local! {
static RUNTIME_COMMAND_STATE_CACHE: RefCell<Option<RuntimeCommandState>> = const { RefCell::new(None) };
static LOADED_PLUGIN_CACHE: RefCell<BTreeMap<String, Rc<bmux_plugin::LoadedPlugin>>> = const { RefCell::new(BTreeMap::new()) };
}
pub(super) fn plugin_host_metadata() -> HostMetadata {
HostMetadata {
product_name: "bmux".to_string(),
product_version: env!("CARGO_PKG_VERSION").to_string(),
plugin_api_version: CURRENT_PLUGIN_API_VERSION,
plugin_abi_version: CURRENT_PLUGIN_ABI_VERSION,
}
}
pub(super) fn plugin_host_for_declaration(
declaration: &bmux_plugin::PluginDeclaration,
paths: &ConfigPaths,
config: &BmuxConfig,
available_services: Vec<RegisteredService>,
) -> plugin_host::CliPluginHost {
plugin_host::CliPluginHost::for_plugin(
declaration.id.as_str(),
plugin_host_metadata(),
paths,
config.clone(),
declaration.required_capabilities.clone(),
declaration.provided_capabilities.clone(),
available_services,
)
.with_typed_services(typed_service_registry_snapshot())
}
#[cfg(test)]
pub(super) fn validate_configured_plugins(config: &BmuxConfig, paths: &ConfigPaths) -> Result<()> {
let registry = scan_available_plugins(config, paths)?;
validate_enabled_plugins(config, ®istry)
}
macro_rules! declare_bundled_plugins {
($(
feature = $feature:literal,
id = $plugin_id:literal,
manifest = $manifest:expr,
plugin_type = $ty:ty;
)*) => {
#[allow(unused_variables, clippy::missing_const_for_fn)]
fn register_static_bundled_plugins(registry: &mut PluginRegistry) {
$(
#[cfg(feature = $feature)]
{
let vtable = bmux_plugin_sdk::bundled_plugin_vtable!($ty, $manifest);
let declared_services = (vtable.declared_services)().unwrap_or_else(|error| {
tracing::warn!(
"failed collecting generated service declarations for bundled plugin '{}': {error}",
$plugin_id,
);
Vec::new()
});
if let Err(e) = registry.register_bundled_manifest_with_services($manifest, declared_services) {
let plugin_id = bmux_plugin::PluginManifest::from_toml_str($manifest)
.ok()
.map(|parsed| parsed.id)
.unwrap_or_else(|| "<unknown-plugin-id>".to_string());
tracing::warn!("failed to register bundled plugin '{plugin_id}': {e}");
}
bmux_plugin::register_static_vtable($plugin_id, vtable);
}
)*
}
};
}
declare_bundled_plugins! {
feature = "bundled-plugin-clients",
id = "bmux.clients",
manifest = include_str!("../../../../plugins/clients-plugin/plugin.toml"),
plugin_type = bmux_clients_plugin::ClientsPlugin;
feature = "bundled-plugin-clipboard",
id = "bmux.clipboard",
manifest = include_str!("../../../../plugins/clipboard-plugin/plugin.toml"),
plugin_type = bmux_clipboard_plugin::ClipboardPlugin;
feature = "bundled-plugin-cluster",
id = "bmux.cluster",
manifest = include_str!("../../../../plugins/cluster-plugin/plugin.toml"),
plugin_type = bmux_cluster_plugin::ClusterPlugin;
feature = "bundled-plugin-command-palette",
id = "bmux.command_palette",
manifest = include_str!("../../../../plugins/command-palette-plugin/plugin.toml"),
plugin_type = bmux_command_palette_plugin::CommandPalettePlugin;
feature = "bundled-plugin-contexts",
id = "bmux.contexts",
manifest = include_str!("../../../../plugins/contexts-plugin/plugin.toml"),
plugin_type = bmux_contexts_plugin::ContextsPlugin;
feature = "bundled-plugin-control-catalog",
id = "bmux.control_catalog",
manifest = include_str!("../../../../plugins/control-catalog-plugin/plugin.toml"),
plugin_type = bmux_control_catalog_plugin::ControlCatalogPlugin;
feature = "bundled-plugin-performance",
id = "bmux.performance",
manifest = include_str!("../../../../plugins/performance-plugin/plugin.toml"),
plugin_type = bmux_performance_plugin::PerformancePlugin;
feature = "bundled-plugin-permissions",
id = "bmux.permissions",
manifest = include_str!("../../../../plugins/permissions-plugin/plugin.toml"),
plugin_type = bmux_permissions_plugin::PermissionsPlugin;
feature = "bundled-plugin-cli",
id = "bmux.plugin_cli",
manifest = include_str!("../../../../plugins/plugin-cli-plugin/plugin.toml"),
plugin_type = bmux_plugin_cli_plugin::PluginCliPlugin;
feature = "bundled-plugin-prompted-actions",
id = "bmux.prompted_actions",
manifest = include_str!("../../../../plugins/prompted-actions-plugin/plugin.toml"),
plugin_type = bmux_prompted_actions_plugin::PromptedActionsPlugin;
feature = "bundled-plugin-recording",
id = "bmux.recording",
manifest = include_str!("../../../../plugins/recording-plugin/plugin.toml"),
plugin_type = bmux_recording_plugin::RecordingPlugin;
feature = "bundled-plugin-sessions",
id = "bmux.sessions",
manifest = include_str!("../../../../plugins/sessions-plugin/plugin.toml"),
plugin_type = bmux_sessions_plugin::SessionsPlugin;
feature = "bundled-plugin-pane-runtime",
id = "bmux.pane_runtime",
manifest = include_str!("../../../../plugins/pane-runtime-plugin/plugin.toml"),
plugin_type = bmux_pane_runtime_plugin::PaneRuntimePlugin;
feature = "bundled-plugin-snapshot",
id = "bmux.snapshot",
manifest = include_str!("../../../../plugins/snapshot-plugin/plugin.toml"),
plugin_type = bmux_snapshot_plugin::SnapshotPlugin;
feature = "bundled-plugin-theme",
id = "bmux.theme",
manifest = include_str!("../../../../plugins/theme-plugin/plugin.toml"),
plugin_type = bmux_theme_plugin::ThemePlugin;
feature = "bundled-plugin-windows",
id = "bmux.windows",
manifest = include_str!("../../../../plugins/windows-plugin/plugin.toml"),
plugin_type = bmux_windows_plugin::WindowsPlugin;
feature = "bundled-plugin-decoration",
id = "bmux.decoration",
manifest = include_str!("../../../../plugins/decoration-plugin/plugin.toml"),
plugin_type = bmux_decoration_plugin::DecorationPlugin;
}
#[allow(clippy::result_large_err)] pub(super) fn load_plugin(
plugin: &bmux_plugin::RegisteredPlugin,
host: &HostMetadata,
available_capabilities: &std::collections::BTreeMap<HostScope, bmux_plugin::CapabilityProvider>,
config: &BmuxConfig,
) -> bmux_plugin_sdk::Result<bmux_plugin::LoadedPlugin> {
let native_service_buffers = native_service_buffer_config(config);
if plugin.bundled_static {
let vtable =
bmux_plugin::static_vtable(plugin.declaration.id.as_str()).ok_or_else(|| {
bmux_plugin_sdk::PluginError::MissingStaticVtable {
plugin_id: plugin.declaration.id.as_str().to_string(),
}
})?;
bmux_plugin::load_trusted_static_plugin_with_native_service_buffer_config(
plugin,
vtable,
host,
available_capabilities,
native_service_buffers,
)
} else {
load_native_registered_plugin(plugin, host, available_capabilities, native_service_buffers)
}
}
const fn native_service_buffer_config(config: &BmuxConfig) -> NativeServiceBufferConfig {
NativeServiceBufferConfig {
initial_response_bytes: config.plugins.native_service.initial_response_bytes,
max_response_bytes: config.plugins.native_service.max_response_bytes,
buffer_resize_attempts: config.plugins.native_service.buffer_resize_attempts,
}
}
pub fn scan_available_plugins(config: &BmuxConfig, paths: &ConfigPaths) -> Result<PluginRegistry> {
let total_started = Instant::now();
let workspace_bundled_root = workspace_bundled_plugin_root();
let search_paths = resolve_plugin_search_paths(config, paths)?;
let search_root_count = search_paths.len();
let discover_started = Instant::now();
let reports = bmux_plugin::discover_plugin_manifests_in_roots(&search_paths)?;
let discover_us = discover_started.elapsed().as_micros();
let manifest_count = reports
.iter()
.map(|report| report.manifest_paths.len())
.sum::<usize>();
let mut registry = PluginRegistry::new();
let static_started = Instant::now();
register_static_bundled_plugins(&mut registry);
let static_register_us = static_started.elapsed().as_micros();
let static_bundled_count = registry
.iter()
.filter(|plugin| plugin.bundled_static)
.count();
let filesystem_started = Instant::now();
let mut filesystem_registered_count = 0_usize;
for report in reports {
for manifest_path in report.manifest_paths {
if workspace_bundled_root
.as_ref()
.is_some_and(|root| report.search_root == *root)
&& static_bundled_workspace_manifest_is_registered(&manifest_path)
{
continue;
}
match PluginManifest::from_path(&manifest_path) {
Ok(mut manifest) => {
if let Some(entry_path) = manifest.resolve_entry_path(
manifest_path
.parent()
.unwrap_or_else(|| std::path::Path::new(".")),
) && !entry_path.exists()
&& workspace_bundled_root
.as_ref()
.is_some_and(|root| report.search_root == *root)
&& let Ok(executable) = std::env::current_exe()
&& let Some(executable_dir) = executable.parent()
&& let Some(entry) = manifest.entry.as_ref()
{
let executable_candidate = executable_dir.join(entry);
if executable_candidate.exists() {
manifest.entry = Some(executable_candidate);
}
}
if let Err(error) = registry.register_manifest_from_root(
&report.search_root,
&manifest_path,
manifest,
) {
if matches!(
error,
bmux_plugin_sdk::PluginError::DuplicatePluginId { .. }
) {
debug!(
"skipping filesystem plugin {} (duplicate of static-bundled plugin)",
manifest_path.display()
);
} else {
warn!(
"skipping plugin manifest {} during enabled-plugin scan: {error}",
manifest_path.display()
);
}
} else {
filesystem_registered_count += 1;
}
}
Err(error) => {
warn!(
"skipping unreadable plugin manifest {} during enabled-plugin scan: {error}",
manifest_path.display()
);
}
}
}
}
let filesystem_register_us = filesystem_started.elapsed().as_micros();
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.registry_scan")
.field("search_root_count", search_root_count)
.field("manifest_count", manifest_count)
.field("static_bundled_count", static_bundled_count)
.field("filesystem_registered_count", filesystem_registered_count)
.field("discover_us", discover_us)
.field("static_register_us", static_register_us)
.field("filesystem_register_us", filesystem_register_us)
.field("total_us", total_started.elapsed().as_micros())
.finish(),
);
Ok(registry)
}
fn static_bundled_workspace_manifest_is_registered(manifest_path: &Path) -> bool {
if manifest_path.file_name().and_then(|name| name.to_str()) != Some("plugin.toml") {
return false;
}
let Some(dir_name) = manifest_path
.parent()
.and_then(Path::file_name)
.and_then(|name| name.to_str())
else {
return false;
};
static_bundled_workspace_plugin_dirs().contains(&dir_name)
}
const fn static_bundled_workspace_plugin_dirs() -> &'static [&'static str] {
&[
#[cfg(feature = "bundled-plugin-clients")]
"clients-plugin",
#[cfg(feature = "bundled-plugin-clipboard")]
"clipboard-plugin",
#[cfg(feature = "bundled-plugin-cluster")]
"cluster-plugin",
#[cfg(feature = "bundled-plugin-command-palette")]
"command-palette-plugin",
#[cfg(feature = "bundled-plugin-contexts")]
"contexts-plugin",
#[cfg(feature = "bundled-plugin-control-catalog")]
"control-catalog-plugin",
#[cfg(feature = "bundled-plugin-performance")]
"performance-plugin",
#[cfg(feature = "bundled-plugin-permissions")]
"permissions-plugin",
#[cfg(feature = "bundled-plugin-cli")]
"plugin-cli-plugin",
#[cfg(feature = "bundled-plugin-prompted-actions")]
"prompted-actions-plugin",
#[cfg(feature = "bundled-plugin-recording")]
"recording-plugin",
#[cfg(feature = "bundled-plugin-sessions")]
"sessions-plugin",
#[cfg(feature = "bundled-plugin-pane-runtime")]
"pane-runtime-plugin",
#[cfg(feature = "bundled-plugin-snapshot")]
"snapshot-plugin",
#[cfg(feature = "bundled-plugin-theme")]
"theme-plugin",
#[cfg(feature = "bundled-plugin-windows")]
"windows-plugin",
#[cfg(feature = "bundled-plugin-decoration")]
"decoration-plugin",
]
}
fn runtime_command_state() -> Result<RuntimeCommandState> {
RUNTIME_COMMAND_STATE_CACHE.with(|slot| {
if let Some(state) = slot.borrow().clone() {
return Ok(state);
}
let config_started = Instant::now();
let config = BmuxConfig::load()?;
let paths = ConfigPaths::default();
let config_us = config_started.elapsed().as_micros();
let scan_started = Instant::now();
let registry = Arc::new(scan_available_plugins(&config, &paths)?);
let scan_us = scan_started.elapsed().as_micros();
let build_started = Instant::now();
let state = build_runtime_command_state(config, paths, registry)?;
emit_runtime_state_phase_timing(config_us, scan_us, build_started.elapsed().as_micros());
*slot.borrow_mut() = Some(state.clone());
Ok(state)
})
}
pub(super) fn build_runtime_command_state(
config: BmuxConfig,
paths: ConfigPaths,
registry: Arc<PluginRegistry>,
) -> Result<RuntimeCommandState> {
let enabled_plugins = effective_enabled_plugins(&config, ®istry);
let available_capability_providers = available_capability_providers(&config, ®istry)?;
let available_capability_names = available_capability_providers
.keys()
.map(ToString::to_string)
.collect::<Vec<_>>();
let available_services = available_service_descriptors(&config, ®istry)?;
let plugin_search_roots = resolve_plugin_search_paths(&config, &paths)?
.into_iter()
.map(|path| path.to_string_lossy().into_owned())
.collect::<Vec<_>>();
let registered_plugin_infos = registered_plugin_infos_from_registry(®istry);
let state = RuntimeCommandState {
config,
paths,
registry,
enabled_plugins,
available_capability_providers,
available_capability_names,
available_services,
plugin_search_roots,
registered_plugin_infos,
};
mark_runtime_command_state_remote(&state);
Ok(state)
}
fn mark_runtime_command_state_remote(state: &RuntimeCommandState) {
let locations = bmux_plugin::global_service_locations();
for plugin_id in &state.enabled_plugins {
locations.mark_remote(plugin_id);
}
}
fn emit_runtime_state_phase_timing(config_us: u128, scan_us: u128, build_us: u128) {
let payload = PhasePayload::new("runtime_command_state")
.field("config_us", config_us)
.field("scan_us", scan_us)
.field("build_us", build_us)
.field("total_us", config_us + scan_us + build_us)
.finish();
emit_phase_timing(PhaseChannel::Plugin, &payload);
}
fn emit_plugin_runtime_phase_timing(payload: &serde_json::Value) {
emit_phase_timing(PhaseChannel::Plugin, payload);
}
pub(super) fn resolve_plugin_search_paths(
config: &BmuxConfig,
paths: &ConfigPaths,
) -> Result<Vec<PathBuf>> {
let mut resolved = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for bundled in bundled_plugin_roots() {
if seen.insert(bundled.clone()) {
resolved.push(bundled);
}
}
let user_plugins = paths.plugins_dir();
if seen.insert(user_plugins.clone()) {
resolved.push(user_plugins);
}
for search_path in &config.plugins.search_paths {
let absolute = if search_path.is_absolute() {
search_path.clone()
} else {
std::env::current_dir()
.context("failed resolving current directory for plugin search path")?
.join(search_path)
};
if seen.insert(absolute.clone()) {
resolved.push(absolute);
}
}
Ok(resolved)
}
pub(super) fn command_accepts_repeat(plugin_id: &str, command_name: &str) -> bool {
runtime_command_state().ok().is_some_and(|state| {
state
.registry
.command_accepts_repeat(plugin_id, command_name)
})
}
pub(super) fn bundled_plugin_root() -> Option<PathBuf> {
let executable = std::env::current_exe().ok()?;
let parent = executable.parent()?;
Some(parent.join("plugins"))
}
pub(super) fn workspace_bundled_plugin_root() -> Option<PathBuf> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let root = manifest_dir.parent()?.parent()?;
let plugins = root.join("plugins");
plugins.exists().then_some(plugins)
}
pub fn bundled_plugin_roots() -> Vec<PathBuf> {
let mut roots = Vec::new();
let mut seen = std::collections::BTreeSet::new();
if let Some(root) = bundled_plugin_root()
&& seen.insert(root.clone())
{
roots.push(root);
}
if let Some(root) = workspace_bundled_plugin_root()
&& seen.insert(root.clone())
{
roots.push(root);
}
roots
}
pub fn registered_plugin_entry_exists(plugin: &bmux_plugin::RegisteredPlugin) -> bool {
plugin
.manifest
.resolve_entry_path(
plugin
.manifest_path
.parent()
.unwrap_or_else(|| std::path::Path::new(".")),
)
.is_some_and(|path| path.exists())
}
pub fn discover_bundled_plugin_ids() -> Vec<String> {
let config = BmuxConfig::default();
let paths = bmux_config::ConfigPaths::default();
let roots = bundled_plugin_roots();
match scan_available_plugins(&config, &paths) {
Ok(registry) => registry
.iter()
.filter(|plugin| {
roots.contains(&plugin.search_root) && registered_plugin_entry_exists(plugin)
})
.map(|plugin| plugin.declaration.id.as_str().to_string())
.collect(),
Err(e) => {
tracing::warn!("failed to discover bundled plugins, using empty list: {e:#}");
Vec::new()
}
}
}
pub(super) fn effective_enabled_plugins(
config: &BmuxConfig,
registry: &PluginRegistry,
) -> Vec<String> {
let disabled = config
.plugins
.disabled
.iter()
.map(String::as_str)
.collect::<std::collections::BTreeSet<_>>();
let bundled_roots = bundled_plugin_roots()
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
let mut enabled = Vec::new();
let mut seen = std::collections::BTreeSet::new();
let mut static_bundled = registry
.iter()
.filter(|&plugin| plugin.bundled_static)
.map(|plugin| plugin.declaration.id.as_str().to_string())
.collect::<Vec<_>>();
static_bundled.sort();
for plugin_id in static_bundled {
if disabled.contains(plugin_id.as_str()) {
continue;
}
if seen.insert(plugin_id.clone()) {
enabled.push(plugin_id);
}
}
let mut bundled_defaults = registry
.iter()
.filter(|&plugin| {
!plugin.bundled_static
&& bundled_roots.contains(&plugin.search_root)
&& registered_plugin_entry_exists(plugin)
})
.map(|plugin| plugin.declaration.id.as_str().to_string())
.collect::<Vec<_>>();
bundled_defaults.sort();
for plugin_id in bundled_defaults {
if disabled.contains(plugin_id.as_str()) {
continue;
}
if seen.insert(plugin_id.clone()) {
enabled.push(plugin_id);
}
}
for plugin_id in &config.plugins.enabled {
if disabled.contains(plugin_id.as_str()) {
continue;
}
if seen.insert(plugin_id.clone()) {
enabled.push(plugin_id.clone());
}
}
enabled
}
pub(super) fn validate_enabled_plugins(
config: &BmuxConfig,
registry: &PluginRegistry,
) -> Result<()> {
let disabled = config
.plugins
.disabled
.iter()
.map(String::as_str)
.collect::<std::collections::BTreeSet<_>>();
let enabled_plugins = effective_enabled_plugins(config, registry);
if enabled_plugins.is_empty() {
validate_plugin_routing_policy(config, registry)?;
return Ok(());
}
for plugin_id in &config.plugins.enabled {
if disabled.contains(plugin_id.as_str()) {
continue;
}
let _ = registry.get(plugin_id).with_context(|| {
let available = registry.plugin_ids();
if available.is_empty() {
format!(
"enabled plugin '{plugin_id}' was not found in the configured plugins directory"
)
} else {
format!(
"enabled plugin '{plugin_id}' was not found in the configured plugins directory (available: {})",
available.join(", ")
)
}
})?;
}
let _ = registry
.activation_order_for(&enabled_plugins)
.context("enabled plugin dependency graph is invalid")?;
let mut command_config = config.clone();
command_config.plugins.enabled = enabled_plugins;
PluginCommandRegistry::build(&command_config, registry)
.context("failed building plugin CLI command registry")?;
validate_plugin_routing_policy(config, registry)?;
Ok(())
}
fn validate_plugin_routing_policy(config: &BmuxConfig, registry: &PluginRegistry) -> Result<()> {
let mut command_config = config.clone();
command_config.plugins.enabled = effective_enabled_plugins(config, registry);
let command_registry = PluginCommandRegistry::build(&command_config, registry)
.context("failed building plugin CLI command registry")?;
match config.plugins.routing.conflict_mode {
bmux_config::PluginRoutingConflictMode::FailStartup => {}
}
for claim in &config.plugins.routing.required_namespaces {
validate_required_namespace_claim(claim, &command_registry)?;
}
for claim in &config.plugins.routing.required_paths {
validate_required_path_claim(claim, &command_registry)?;
}
Ok(())
}
fn validate_required_namespace_claim(
claim: &bmux_config::RequiredNamespaceClaim,
command_registry: &PluginCommandRegistry,
) -> Result<()> {
let namespace = claim.namespace.trim();
if namespace.is_empty() {
anyhow::bail!("plugins.routing.required_namespaces.namespace must not be empty");
}
let owner = command_registry
.owner_for_namespace(namespace)
.ok_or_else(|| {
anyhow::anyhow!(
"required namespace claim '{namespace}' is not owned by any enabled plugin"
)
})?;
if let Some(expected_owner) = claim.owner.as_deref()
&& owner != expected_owner
{
anyhow::bail!(
"required namespace claim '{namespace}' must be owned by plugin '{expected_owner}' (actual owner '{owner}')"
);
}
Ok(())
}
fn validate_required_path_claim(
claim: &bmux_config::RequiredPathClaim,
command_registry: &PluginCommandRegistry,
) -> Result<()> {
if claim.path.is_empty() {
anyhow::bail!("plugins.routing.required_paths.path must not be empty");
}
if claim.path.iter().any(|segment| segment.trim().is_empty()) {
anyhow::bail!(
"plugins.routing.required_paths.path must not contain empty command segments"
);
}
let owner = command_registry
.owner_for_path(&claim.path)
.ok_or_else(|| {
anyhow::anyhow!(
"required path claim '{}' is not owned by any enabled plugin",
claim.path.join(" ")
)
})?;
if let Some(expected_owner) = claim.owner.as_deref()
&& owner != expected_owner
{
anyhow::bail!(
"required path claim '{}' must be owned by plugin '{}' (actual owner '{}')",
claim.path.join(" "),
expected_owner,
owner
);
}
Ok(())
}
pub(super) fn load_enabled_plugins(
config: &BmuxConfig,
registry: &PluginRegistry,
) -> Result<Vec<bmux_plugin::LoadedPlugin>> {
let total_started = Instant::now();
let enabled_plugins = effective_enabled_plugins(config, registry);
if enabled_plugins.is_empty() {
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.load_all")
.field("enabled_count", 0_usize)
.field("loaded_count", 0_usize)
.field("total_us", total_started.elapsed().as_micros())
.finish(),
);
return Ok(Vec::new());
}
let disabled = config
.plugins
.disabled
.iter()
.map(String::as_str)
.collect::<std::collections::BTreeSet<_>>();
let explicitly_enabled = config
.plugins
.enabled
.iter()
.filter(|plugin_id| !disabled.contains(plugin_id.as_str()))
.cloned()
.collect::<std::collections::BTreeSet<_>>();
for plugin_id in &enabled_plugins {
if registry.get(plugin_id).is_some() {
continue;
}
if explicitly_enabled.contains(plugin_id) {
anyhow::bail!("enabled plugin '{plugin_id}' disappeared during native load");
}
warn!("skipping bundled plugin '{plugin_id}' because it is no longer discoverable");
}
let host = plugin_host_metadata();
let available_capabilities = available_capability_providers(config, registry)?;
let ordered_plugins = registry
.activation_order_for(&enabled_plugins)
.context("enabled plugin dependency graph is invalid")?;
let mut loaded_plugins = Vec::with_capacity(ordered_plugins.len());
for plugin in ordered_plugins {
let plugin_id = plugin.declaration.id.as_str();
let load_started = Instant::now();
let backend = if plugin.bundled_static {
"static"
} else if plugin.manifest.runtime == PluginRuntime::Process {
"process"
} else {
"dynamic"
};
let loaded = match load_plugin(plugin, &host, &available_capabilities, config) {
Ok(loaded) => {
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.load")
.field("plugin_id", plugin_id)
.field("backend", backend)
.field("bundled_static", plugin.bundled_static)
.field("total_us", load_started.elapsed().as_micros())
.finish(),
);
loaded
}
Err(error) => {
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.load")
.field("plugin_id", plugin_id)
.field("backend", backend)
.field("bundled_static", plugin.bundled_static)
.field("error", error.to_string())
.field("total_us", load_started.elapsed().as_micros())
.finish(),
);
if explicitly_enabled.contains(plugin_id) {
return Err(error)
.with_context(|| format!("failed loading enabled plugin '{plugin_id}'"));
}
warn!("skipping bundled plugin '{plugin_id}': {error}");
continue;
}
};
loaded_plugins.push(loaded);
}
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.load_all")
.field("enabled_count", enabled_plugins.len())
.field("loaded_count", loaded_plugins.len())
.field("total_us", total_started.elapsed().as_micros())
.finish(),
);
Ok(loaded_plugins)
}
pub(super) fn registered_plugin_infos_from_loaded(
loaded_plugins: &[bmux_plugin::LoadedPlugin],
) -> Vec<bmux_plugin_sdk::RegisteredPluginInfo> {
loaded_plugins
.iter()
.map(|plugin| bmux_plugin_sdk::RegisteredPluginInfo {
id: plugin.declaration.id.as_str().to_string(),
display_name: plugin.declaration.display_name.clone(),
version: plugin.declaration.plugin_version.clone(),
bundled_static: plugin.registered.bundled_static,
required_capabilities: plugin
.declaration
.required_capabilities
.iter()
.map(ToString::to_string)
.collect(),
provided_capabilities: plugin
.declaration
.provided_capabilities
.iter()
.map(ToString::to_string)
.collect(),
commands: plugin
.declaration
.commands
.iter()
.map(|c| c.name.clone())
.collect(),
command_schemas: plugin.declaration.commands.clone(),
})
.collect()
}
pub(super) fn registered_plugin_infos_from_registry(
registry: &PluginRegistry,
) -> Vec<bmux_plugin_sdk::RegisteredPluginInfo> {
registry
.iter()
.map(|plugin| bmux_plugin_sdk::RegisteredPluginInfo {
id: plugin.declaration.id.as_str().to_string(),
display_name: plugin.declaration.display_name.clone(),
version: plugin.declaration.plugin_version.clone(),
bundled_static: plugin.bundled_static,
required_capabilities: plugin
.declaration
.required_capabilities
.iter()
.map(ToString::to_string)
.collect(),
provided_capabilities: plugin
.declaration
.provided_capabilities
.iter()
.map(ToString::to_string)
.collect(),
commands: plugin
.declaration
.commands
.iter()
.map(|c| c.name.clone())
.collect(),
command_schemas: plugin.declaration.commands.clone(),
})
.collect()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn plugin_lifecycle_context(
config: &BmuxConfig,
paths: &ConfigPaths,
declaration: &bmux_plugin::PluginDeclaration,
available_services: Vec<RegisteredService>,
available_capabilities: Vec<String>,
enabled_plugins: Vec<String>,
plugin_search_roots: Vec<String>,
registered_plugins: Vec<bmux_plugin_sdk::RegisteredPluginInfo>,
) -> NativeLifecycleContext {
let host = plugin_host_for_declaration(declaration, paths, config, available_services.clone());
NativeLifecycleContext {
plugin_id: declaration.id.as_str().to_string(),
required_capabilities: declaration
.required_capabilities
.iter()
.map(ToString::to_string)
.collect(),
provided_capabilities: declaration
.provided_capabilities
.iter()
.map(ToString::to_string)
.collect(),
services: available_services,
available_capabilities,
enabled_plugins,
plugin_search_roots,
registered_plugins,
host: plugin_host_metadata(),
connection: bmux_plugin_sdk::PluginHost::connection(&host).clone(),
settings: config
.plugins
.settings
.get(declaration.id.as_str())
.cloned(),
plugin_settings_map: config.plugins.settings.clone(),
host_kernel_bridge: Some(bmux_plugin_sdk::HostKernelBridge::from_fn(
host_kernel_bridge,
)),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn plugin_command_context(
config: &BmuxConfig,
paths: &ConfigPaths,
declaration: &bmux_plugin::PluginDeclaration,
command: &str,
arguments: &[String],
available_services: Vec<RegisteredService>,
available_capabilities: Vec<String>,
enabled_plugins: Vec<String>,
plugin_search_roots: Vec<String>,
registered_plugins: Vec<bmux_plugin_sdk::RegisteredPluginInfo>,
active_keybindings: Vec<bmux_plugin_sdk::ActiveKeybinding>,
caller_client_id: Option<uuid::Uuid>,
invocation_source: bmux_plugin_sdk::NativeCommandInvocationSource,
) -> NativeCommandContext {
let host = plugin_host_for_declaration(declaration, paths, config, available_services.clone());
NativeCommandContext {
plugin_id: declaration.id.as_str().to_string(),
command: command.to_string(),
arguments: arguments.to_vec(),
required_capabilities: declaration
.required_capabilities
.iter()
.map(ToString::to_string)
.collect(),
provided_capabilities: declaration
.provided_capabilities
.iter()
.map(ToString::to_string)
.collect(),
services: available_services,
available_capabilities,
enabled_plugins,
plugin_search_roots,
registered_plugins,
active_keybindings,
host: plugin_host_metadata(),
connection: bmux_plugin_sdk::PluginHost::connection(&host).clone(),
settings: config
.plugins
.settings
.get(declaration.id.as_str())
.cloned(),
plugin_settings_map: config.plugins.settings.clone(),
caller_client_id,
invocation_source,
host_kernel_bridge: Some(bmux_plugin_sdk::HostKernelBridge::from_fn(
host_kernel_bridge,
)),
}
}
pub(super) fn plugin_system_event(name: &str) -> PluginEvent {
PluginEvent {
kind: PluginEventKind::from_owned(format!("bmux.core/{name}")),
payload: serde_json::json!({
"product": "bmux",
"version": env!("CARGO_PKG_VERSION"),
}),
}
}
thread_local! {
static TYPED_SERVICE_REGISTRY: std::cell::RefCell<
std::sync::Arc<
std::collections::BTreeMap<
bmux_plugin_sdk::TypedServiceKey,
bmux_plugin_sdk::TypedServiceHandle,
>,
>,
> = std::cell::RefCell::new(std::sync::Arc::new(std::collections::BTreeMap::new()));
static READY_TRACKER: std::cell::RefCell<bmux_plugin_sdk::ReadyTracker> =
std::cell::RefCell::new(bmux_plugin_sdk::ReadyTracker::new());
}
#[allow(clippy::too_many_lines)]
fn install_typed_service_registry(
loaded_plugins: &[bmux_plugin::LoadedPlugin],
config: &BmuxConfig,
paths: &ConfigPaths,
) -> Result<()> {
let total_started = Instant::now();
let bridge = bmux_plugin_sdk::HostKernelBridge::from_fn(host_kernel_bridge);
let available_capabilities = core_provided_capabilities()
.into_iter()
.chain(
loaded_plugins
.iter()
.flat_map(|plugin| plugin.declaration.provided_capabilities.iter().cloned()),
)
.map(|capability| capability.to_string())
.collect::<Vec<_>>();
let available_services = service_descriptors_from_declarations(
loaded_plugins.iter().map(|plugin| &plugin.declaration),
);
let enabled_plugins = loaded_plugins
.iter()
.map(|plugin| plugin.declaration.id.as_str().to_string())
.collect::<Vec<_>>();
let search_roots = resolve_plugin_search_paths(config, paths)
.map(|paths| {
paths
.into_iter()
.map(|p| p.to_string_lossy().into_owned())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let host_metadata = plugin_host_metadata();
let host_connection = bmux_plugin_sdk::HostConnectionInfo {
config_dir: paths.config_dir.to_string_lossy().into_owned(),
config_dir_candidates: paths
.config_dir_candidates()
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect(),
runtime_dir: paths.runtime_dir.to_string_lossy().into_owned(),
data_dir: paths.data_dir.to_string_lossy().into_owned(),
state_dir: paths.state_dir.to_string_lossy().into_owned(),
};
let plugin_settings_map: std::collections::BTreeMap<String, toml::Value> =
config.plugins.settings.clone();
let mut map: std::collections::BTreeMap<
bmux_plugin_sdk::TypedServiceKey,
bmux_plugin_sdk::TypedServiceHandle,
> = std::collections::BTreeMap::new();
for plugin in loaded_plugins {
let collect_started = Instant::now();
let required_caps: Vec<String> = plugin
.declaration
.required_capabilities
.iter()
.map(ToString::to_string)
.collect();
let provided_caps: Vec<String> = plugin
.declaration
.provided_capabilities
.iter()
.map(ToString::to_string)
.collect();
let context = bmux_plugin_sdk::TypedServiceRegistrationContext {
plugin_id: plugin.declaration.id.as_str(),
host_kernel_bridge: Some(&bridge),
required_capabilities: &required_caps,
provided_capabilities: &provided_caps,
services: &available_services,
available_capabilities: &available_capabilities,
enabled_plugins: &enabled_plugins,
plugin_search_roots: &search_roots,
host: &host_metadata,
connection: &host_connection,
plugin_settings_map: &plugin_settings_map,
};
let entries = plugin.collect_typed_services(context).into_entries();
let service_count = entries.len();
for (key, handle) in entries {
ensure_typed_service_declared(plugin, &key)?;
map.insert(key, handle);
}
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.typed_services.collect")
.field("plugin_id", plugin.declaration.id.as_str())
.field("service_count", service_count)
.field("total_us", collect_started.elapsed().as_micros())
.finish(),
);
}
let total_service_count = map.len();
TYPED_SERVICE_REGISTRY.with(|cell| {
*cell.borrow_mut() = std::sync::Arc::new(map);
});
READY_TRACKER.with(|cell| {
let tracker = cell.borrow();
for plugin in loaded_plugins {
tracker.declare(
plugin.declaration.id.as_str(),
&plugin.declaration.ready_signals,
);
}
});
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.typed_services.collect_all")
.field("plugin_count", loaded_plugins.len())
.field("service_count", total_service_count)
.field("total_us", total_started.elapsed().as_micros())
.finish(),
);
Ok(())
}
fn ensure_typed_service_declared(
plugin: &bmux_plugin::LoadedPlugin,
key: &bmux_plugin_sdk::TypedServiceKey,
) -> Result<()> {
let (capability, kind, interface_id) = key;
if plugin.declaration.services.iter().any(|service| {
service.capability == *capability
&& service.kind == *kind
&& service.interface_id == interface_id.as_str()
}) {
return Ok(());
}
anyhow::bail!(
"plugin '{}' registered typed service capability='{}' kind='{kind:?}' interface='{}' but its effective declaration does not expose it; return the BPDL-generated service from RustPlugin::declared_services or declare an explicit manifest service",
plugin.declaration.id.as_str(),
capability.as_str(),
interface_id.as_str(),
);
}
fn mark_plugin_ready_signals(plugin: &bmux_plugin::LoadedPlugin) {
READY_TRACKER.with(|cell| {
let tracker = cell.borrow();
for signal in &plugin.declaration.ready_signals {
tracker.mark_ready(plugin.declaration.id.as_str(), &signal.name);
}
});
}
#[must_use]
pub(super) fn typed_service_registry_snapshot() -> std::sync::Arc<
std::collections::BTreeMap<
bmux_plugin_sdk::TypedServiceKey,
bmux_plugin_sdk::TypedServiceHandle,
>,
> {
TYPED_SERVICE_REGISTRY.with(|cell| std::sync::Arc::clone(&cell.borrow()))
}
#[must_use]
#[allow(dead_code)] pub(super) fn resolve_windows_commands_service() -> Option<
std::sync::Arc<
dyn bmux_windows_plugin_api::windows_commands::WindowsCommandsService + Send + Sync,
>,
> {
let write_cap = bmux_plugin_sdk::HostScope::new("bmux.windows.write").ok()?;
let registry = typed_service_registry_snapshot();
let handle = registry.get(&(
write_cap,
bmux_plugin_sdk::ServiceKind::Command,
bmux_windows_plugin_api::windows_commands::INTERFACE_ID,
))?;
handle
.provider_as_trait::<
dyn bmux_windows_plugin_api::windows_commands::WindowsCommandsService + Send + Sync,
>()
.ok()
}
#[must_use]
#[allow(dead_code)] pub(super) fn resolve_windows_state_service() -> Option<
std::sync::Arc<dyn bmux_windows_plugin_api::windows_state::WindowsStateService + Send + Sync>,
> {
let read_cap = bmux_plugin_sdk::HostScope::new("bmux.windows.read").ok()?;
let registry = typed_service_registry_snapshot();
let handle = registry.get(&(
read_cap,
bmux_plugin_sdk::ServiceKind::Query,
bmux_windows_plugin_api::windows_state::INTERFACE_ID,
))?;
handle
.provider_as_trait::<
dyn bmux_windows_plugin_api::windows_state::WindowsStateService + Send + Sync,
>()
.ok()
}
#[must_use]
#[allow(dead_code)] pub(super) fn ready_tracker_snapshot() -> bmux_plugin_sdk::ReadyTracker {
READY_TRACKER.with(|cell| cell.borrow().clone())
}
#[allow(dead_code)] fn block_on_future<T>(
fut: std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + '_>>,
) -> T {
use std::sync::Arc;
use std::task::{Context, Poll, Wake, Waker};
struct NoopWake;
impl Wake for NoopWake {
fn wake(self: Arc<Self>) {}
}
let waker = Waker::from(Arc::new(NoopWake));
let mut cx = Context::from_waker(&waker);
let mut pinned = fut;
loop {
match pinned.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
#[allow(clippy::too_many_lines)]
pub(super) fn activate_loaded_plugins(
loaded_plugins: &[bmux_plugin::LoadedPlugin],
config: &BmuxConfig,
paths: &ConfigPaths,
) -> Result<()> {
let total_started = Instant::now();
install_typed_service_registry(loaded_plugins, config, paths)?;
let mut activated: Vec<&bmux_plugin::LoadedPlugin> = Vec::new();
let connection_info = HostConnectionInfo {
config_dir: paths.config_dir.to_string_lossy().into_owned(),
config_dir_candidates: paths
.config_dir_candidates()
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect(),
runtime_dir: paths.runtime_dir.to_string_lossy().into_owned(),
data_dir: paths.data_dir.to_string_lossy().into_owned(),
state_dir: paths.state_dir.to_string_lossy().into_owned(),
};
let plugin_search_roots = resolve_plugin_search_paths(config, paths)?
.into_iter()
.map(|path| path.to_string_lossy().into_owned())
.collect::<Vec<_>>();
let available_capabilities = core_provided_capabilities()
.into_iter()
.chain(
loaded_plugins
.iter()
.flat_map(|plugin| plugin.declaration.provided_capabilities.iter().cloned()),
)
.map(|capability| capability.to_string())
.collect::<Vec<_>>();
let available_services = service_descriptors_from_declarations(
loaded_plugins.iter().map(|plugin| &plugin.declaration),
);
let enabled_plugins = loaded_plugins
.iter()
.map(|plugin| plugin.declaration.id.as_str().to_string())
.collect::<Vec<_>>();
let registered_plugins = registered_plugin_infos_from_loaded(loaded_plugins);
let mut activated_count = 0_usize;
for plugin in loaded_plugins {
if !plugin.declaration.lifecycle.activate_on_startup {
continue;
}
let activate_started = Instant::now();
let context = plugin_lifecycle_context(
config,
paths,
&plugin.declaration,
available_services.clone(),
available_capabilities.clone(),
enabled_plugins.clone(),
plugin_search_roots.clone(),
registered_plugins.clone(),
);
let _host_kernel_connection_guard = enter_host_kernel_connection(connection_info.clone());
#[allow(clippy::option_if_let_else)]
let activate_result = match bmux_plugin_sdk::HostAsyncHandle::try_current() {
Ok(async_handle) => plugin.activate_with_async(&context, async_handle),
Err(_) => plugin.activate(&context),
};
if let Err(error) = activate_result {
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.lifecycle.activate")
.field("plugin_id", plugin.declaration.id.as_str())
.field("ready_signal_count", plugin.declaration.ready_signals.len())
.field("error", error.to_string())
.field("total_us", activate_started.elapsed().as_micros())
.finish(),
);
for activated_plugin in activated.into_iter().rev() {
let context = plugin_lifecycle_context(
config,
paths,
&activated_plugin.declaration,
available_services.clone(),
available_capabilities.clone(),
enabled_plugins.clone(),
plugin_search_roots.clone(),
registered_plugins.clone(),
);
let _host_kernel_connection_guard =
enter_host_kernel_connection(connection_info.clone());
if let Err(deactivate_error) = activated_plugin.deactivate(&context) {
warn!(
"failed rolling back plugin activation for {}: {deactivate_error}",
activated_plugin.declaration.id.as_str()
);
}
}
return Err(error).with_context(|| {
format!(
"failed activating plugin '{}'",
plugin.declaration.id.as_str()
)
});
}
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.lifecycle.activate")
.field("plugin_id", plugin.declaration.id.as_str())
.field("ready_signal_count", plugin.declaration.ready_signals.len())
.field("status", 0_i32)
.field("total_us", activate_started.elapsed().as_micros())
.finish(),
);
activated_count += 1;
activated.push(plugin);
mark_plugin_ready_signals(plugin);
bmux_plugin::global_service_locations().mark_local(plugin.declaration.id.as_str());
}
emit_plugin_runtime_phase_timing(
&PhasePayload::new("plugin.lifecycle.activate_all")
.field("plugin_count", loaded_plugins.len())
.field("activated_count", activated_count)
.field("total_us", total_started.elapsed().as_micros())
.finish(),
);
Ok(())
}
pub(super) fn deactivate_loaded_plugins(
loaded_plugins: &[bmux_plugin::LoadedPlugin],
config: &BmuxConfig,
paths: &ConfigPaths,
) -> Result<()> {
let connection_info = HostConnectionInfo {
config_dir: paths.config_dir.to_string_lossy().into_owned(),
config_dir_candidates: paths
.config_dir_candidates()
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect(),
runtime_dir: paths.runtime_dir.to_string_lossy().into_owned(),
data_dir: paths.data_dir.to_string_lossy().into_owned(),
state_dir: paths.state_dir.to_string_lossy().into_owned(),
};
let plugin_search_roots = resolve_plugin_search_paths(config, paths)?
.into_iter()
.map(|path| path.to_string_lossy().into_owned())
.collect::<Vec<_>>();
let available_capabilities = core_provided_capabilities()
.into_iter()
.chain(
loaded_plugins
.iter()
.flat_map(|plugin| plugin.declaration.provided_capabilities.iter().cloned()),
)
.map(|capability| capability.to_string())
.collect::<Vec<_>>();
let available_services = service_descriptors_from_declarations(
loaded_plugins.iter().map(|plugin| &plugin.declaration),
);
let enabled_plugins = loaded_plugins
.iter()
.map(|plugin| plugin.declaration.id.as_str().to_string())
.collect::<Vec<_>>();
let registered_plugins = registered_plugin_infos_from_loaded(loaded_plugins);
for plugin in loaded_plugins.iter().rev() {
if !plugin.declaration.lifecycle.activate_on_startup {
continue;
}
let context = plugin_lifecycle_context(
config,
paths,
&plugin.declaration,
available_services.clone(),
available_capabilities.clone(),
enabled_plugins.clone(),
plugin_search_roots.clone(),
registered_plugins.clone(),
);
let _host_kernel_connection_guard = enter_host_kernel_connection(connection_info.clone());
let _ = plugin.deactivate(&context).with_context(|| {
format!(
"failed deactivating plugin '{}'",
plugin.declaration.id.as_str()
)
})?;
}
Ok(())
}
pub(super) fn dispatch_loaded_plugin_event(
loaded_plugins: &[bmux_plugin::LoadedPlugin],
event: &PluginEvent,
) -> Result<()> {
for plugin in loaded_plugins {
let _ = plugin.dispatch_event(event).with_context(|| {
format!(
"failed dispatching plugin event '{}' to '{}'",
event.kind.as_str(),
plugin.declaration.id.as_str()
)
})?;
}
Ok(())
}
pub(super) async fn plugin_event_bridge_loop(
loaded_plugins: &[bmux_plugin::LoadedPlugin],
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
) -> Result<()> {
if loaded_plugins.is_empty() {
return Ok(());
}
let client = loop {
if *shutdown_rx.borrow() {
return Ok(());
}
match connect_raw("bmux-plugin-event-bridge").await {
Ok(client) => break client,
Err(_) => {
tokio::select! {
_ = shutdown_rx.changed() => {
if *shutdown_rx.borrow() {
return Ok(());
}
}
() = tokio::time::sleep(Duration::from_millis(100)) => {}
}
}
}
};
let Ok(mut streaming_client) = bmux_client::StreamingBmuxClient::from_client(client) else {
return Ok(());
};
streaming_client
.subscribe_events()
.await
.map_err(map_cli_client_error)?;
streaming_client
.enable_event_push()
.await
.map_err(map_cli_client_error)?;
loop {
tokio::select! {
_ = shutdown_rx.changed() => {
if *shutdown_rx.borrow() {
return Ok(());
}
}
event = streaming_client.event_receiver().recv() => {
let Some(event) = event else {
return Ok(()); };
if is_pane_output_plugin_event(&event) {
continue;
}
dispatch_loaded_plugin_event(loaded_plugins, &plugin_event_from_server_event(&event)?)?;
}
}
}
}
fn is_pane_output_plugin_event(event: &bmux_client::ServerEvent) -> bool {
let bmux_client::ServerEvent::PluginBusEvent { kind, payload } = event else {
return false;
};
if kind != bmux_pane_runtime_plugin_api::pane_runtime_events::EVENT_KIND.as_str() {
return false;
}
serde_json::from_slice::<bmux_pane_runtime_plugin_api::pane_runtime_events::PaneEvent>(payload)
.is_ok_and(|event| {
matches!(
event,
bmux_pane_runtime_plugin_api::pane_runtime_events::PaneEvent::OutputAvailable { .. }
)
})
}
pub(super) fn plugin_event_from_server_event(
event: &bmux_client::ServerEvent,
) -> Result<PluginEvent> {
Ok(PluginEvent {
kind: plugin_event_kind_from_server_event(event),
payload: serde_json::to_value(event).context("failed encoding server event payload")?,
})
}
pub(super) fn plugin_event_kind_from_server_event(
event: &bmux_client::ServerEvent,
) -> PluginEventKind {
PluginEventKind::from_owned(format!("bmux.core/{}", server_event_name(event)))
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub(super) async fn run_plugin_command(
plugin_id: &str,
command_name: &str,
args: &[String],
) -> Result<u8> {
let execution = run_plugin_command_internal(
plugin_id,
command_name,
args,
None,
None,
Vec::new(),
bmux_plugin_sdk::NativeCommandInvocationSource::Cli,
)?;
if execution.status != 0
&& let Some(error) = execution.outcome.error_message.as_deref()
{
eprintln!("{error}");
}
Ok(execution.status.clamp(0, i32::from(u8::MAX)) as u8)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub(super) async fn run_plugin_command_with_state(
state: &RuntimeCommandState,
plugin_id: &str,
command_name: &str,
args: &[String],
) -> Result<u8> {
let execution = run_plugin_command_internal_with_state(
Some(state),
plugin_id,
command_name,
args,
None,
None,
Vec::new(),
bmux_plugin_sdk::NativeCommandInvocationSource::Cli,
)?;
if execution.status != 0
&& let Some(error) = execution.outcome.error_message.as_deref()
{
eprintln!("{error}");
}
Ok(execution.status.clamp(0, i32::from(u8::MAX)) as u8)
}
pub(super) fn run_plugin_keybinding_command(
plugin_id: &str,
command_name: &str,
args: &[String],
kernel_client_factory: Option<&KernelClientFactory>,
caller_client_id: Option<uuid::Uuid>,
) -> Result<PluginCommandExecution> {
run_plugin_keybinding_command_with_active_bindings(
plugin_id,
command_name,
args,
kernel_client_factory,
caller_client_id,
Vec::new(),
)
}
pub(super) fn run_plugin_keybinding_command_with_active_bindings(
plugin_id: &str,
command_name: &str,
args: &[String],
kernel_client_factory: Option<&KernelClientFactory>,
caller_client_id: Option<uuid::Uuid>,
active_keybindings: Vec<bmux_plugin_sdk::ActiveKeybinding>,
) -> Result<PluginCommandExecution> {
run_plugin_command_internal(
plugin_id,
command_name,
args,
kernel_client_factory,
caller_client_id,
active_keybindings,
bmux_plugin_sdk::NativeCommandInvocationSource::AttachKeybinding,
)
}
pub(super) fn run_plugin_keybinding_command_with_state(
state: &RuntimeCommandState,
plugin_id: &str,
command_name: &str,
args: &[String],
kernel_client_factory: Option<&KernelClientFactory>,
caller_client_id: Option<uuid::Uuid>,
) -> Result<PluginCommandExecution> {
run_plugin_command_internal_with_state(
Some(state),
plugin_id,
command_name,
args,
kernel_client_factory,
caller_client_id,
Vec::new(),
bmux_plugin_sdk::NativeCommandInvocationSource::AttachKeybinding,
)
}
#[derive(Debug, Clone)]
pub(super) struct PluginCommandPolicyHints {
pub(super) execution: bmux_plugin_sdk::CommandExecutionKind,
pub(super) execution_class: bmux_plugin::PluginExecutionClass,
pub(super) required_capabilities: Vec<HostScope>,
}
pub(super) fn plugin_command_policy_hints(
plugin_id: &str,
command_name: &str,
) -> Result<PluginCommandPolicyHints> {
let state = runtime_command_state()?;
let registry = &state.registry;
let available = registry.plugin_ids();
let plugin = registry
.get(plugin_id)
.with_context(|| format_plugin_not_found_message(plugin_id, &available))?;
if !state
.enabled_plugins
.iter()
.any(|enabled| enabled == plugin_id)
{
anyhow::bail!(format_plugin_not_enabled_message(plugin_id));
}
let command = plugin
.declaration
.commands
.iter()
.find(|entry| entry.name == command_name)
.ok_or_else(|| {
anyhow::anyhow!("plugin '{plugin_id}' does not declare command '{command_name}'")
})?;
Ok(PluginCommandPolicyHints {
execution: command.execution.clone(),
execution_class: plugin.declaration.execution_class,
required_capabilities: plugin
.declaration
.required_capabilities
.iter()
.cloned()
.collect(),
})
}
pub(super) struct PluginCommandExecution {
pub(super) status: i32,
pub(super) outcome: PluginCommandOutcome,
}
#[instrument(
level = "debug",
target = "bmux_cli::runtime::plugin_command",
skip_all,
fields(
plugin_id = %plugin_id,
command_name = %command_name,
invocation_source = ?invocation_source,
args_count = args.len(),
status = tracing::field::Empty,
error = tracing::field::Empty,
),
)]
pub(super) fn run_plugin_command_internal(
plugin_id: &str,
command_name: &str,
args: &[String],
kernel_client_factory: Option<&KernelClientFactory>,
caller_client_id: Option<uuid::Uuid>,
active_keybindings: Vec<bmux_plugin_sdk::ActiveKeybinding>,
invocation_source: bmux_plugin_sdk::NativeCommandInvocationSource,
) -> Result<PluginCommandExecution> {
run_plugin_command_internal_with_state(
None,
plugin_id,
command_name,
args,
kernel_client_factory,
caller_client_id,
active_keybindings,
invocation_source,
)
}
#[allow(clippy::too_many_arguments)] fn run_plugin_command_internal_with_state(
provided_state: Option<&RuntimeCommandState>,
plugin_id: &str,
command_name: &str,
args: &[String],
kernel_client_factory: Option<&KernelClientFactory>,
caller_client_id: Option<uuid::Uuid>,
active_keybindings: Vec<bmux_plugin_sdk::ActiveKeybinding>,
invocation_source: bmux_plugin_sdk::NativeCommandInvocationSource,
) -> Result<PluginCommandExecution> {
let total_started = Instant::now();
let runtime_state_started = Instant::now();
let cached_state;
let state = if let Some(state) = provided_state {
state
} else {
cached_state = runtime_command_state()?;
&cached_state
};
let runtime_state_us = runtime_state_started.elapsed().as_micros();
let lookup_started = Instant::now();
let config = &state.config;
let paths = &state.paths;
let registry = &state.registry;
let available = registry.plugin_ids();
let plugin = registry
.get(plugin_id)
.with_context(|| format_plugin_not_found_message(plugin_id, &available))?;
let enabled_plugins = state.enabled_plugins.clone();
if !enabled_plugins.iter().any(|enabled| enabled == plugin_id) {
anyhow::bail!(format_plugin_not_enabled_message(plugin_id));
}
let lookup_us = lookup_started.elapsed().as_micros();
let load_started = Instant::now();
let loaded = load_cached_plugin(plugin, state)?;
let load_us = load_started.elapsed().as_micros();
let context_started = Instant::now();
let context = plugin_command_context(
config,
paths,
&plugin.declaration,
command_name,
args,
state.available_services.clone(),
state.available_capability_names.clone(),
enabled_plugins,
state.plugin_search_roots.clone(),
state.registered_plugin_infos.clone(),
active_keybindings,
caller_client_id,
invocation_source,
);
let context_us = context_started.elapsed().as_micros();
let _host_kernel_connection_guard = enter_host_kernel_connection(context.connection.clone());
let _host_kernel_factory_guard =
kernel_client_factory.map(|f| enter_host_kernel_client_factory(Arc::clone(f)));
let run_started = Instant::now();
let run_result =
loaded.run_command_with_context_and_outcome(command_name, args, Some(&context));
let run_us = run_started.elapsed().as_micros();
let (status, outcome) = run_result.map_err(|error| {
tracing::Span::current().record("error", tracing::field::display(&error));
anyhow::anyhow!(format_plugin_command_run_error(
plugin_id,
command_name,
&error
))
})?;
tracing::Span::current().record("status", status);
if let Some(msg) = outcome.error_message.as_deref() {
tracing::Span::current().record("error", msg);
}
emit_plugin_phase_timing(
plugin_id,
command_name,
invocation_source,
runtime_state_us,
lookup_us,
load_us,
context_us,
run_us,
total_started.elapsed().as_micros(),
);
Ok(PluginCommandExecution { status, outcome })
}
#[allow(clippy::too_many_arguments)]
fn emit_plugin_phase_timing(
plugin_id: &str,
command_name: &str,
invocation_source: bmux_plugin_sdk::NativeCommandInvocationSource,
runtime_state_us: u128,
lookup_us: u128,
load_us: u128,
context_us: u128,
run_us: u128,
total_us: u128,
) {
let payload = PhasePayload::new("plugin.command")
.field("plugin_id", plugin_id)
.field("command_name", command_name)
.field("invocation_source", format!("{invocation_source:?}"))
.field("runtime_state_us", runtime_state_us)
.field("lookup_us", lookup_us)
.field("load_us", load_us)
.field("context_us", context_us)
.field("run_us", run_us)
.field("total_us", total_us)
.finish();
emit_phase_timing(PhaseChannel::Plugin, &payload);
}
fn load_cached_plugin(
plugin: &bmux_plugin::RegisteredPlugin,
state: &RuntimeCommandState,
) -> Result<Rc<bmux_plugin::LoadedPlugin>> {
let plugin_id = plugin.declaration.id.as_str().to_string();
LOADED_PLUGIN_CACHE.with(|slot| {
if let Some(existing) = slot.borrow().get(&plugin_id) {
return Ok(Rc::clone(existing));
}
let loaded = load_plugin(
plugin,
&plugin_host_metadata(),
&state.available_capability_providers,
&state.config,
)
.with_context(|| format!("failed loading enabled plugin '{plugin_id}'"))?;
let loaded = Rc::new(loaded);
slot.borrow_mut().insert(plugin_id, Rc::clone(&loaded));
Ok(loaded)
})
}
pub(super) fn format_plugin_command_run_error(
plugin_id: &str,
command_name: &str,
error: &dyn std::fmt::Display,
) -> String {
let base = format!("failed running plugin command '{plugin_id}:{command_name}': {error}");
if base.contains("session policy denied for this operation") {
format!(
"{base}\nHint: operation denied by an active policy provider. Verify policy state or run with an authorized principal."
)
} else {
base
}
}
pub(super) fn format_plugin_not_found_message<S: AsRef<str>>(
plugin_id: &str,
available: &[S],
) -> String {
if available.is_empty() {
format!("plugin '{plugin_id}' was not found")
} else {
let available = available
.iter()
.map(std::convert::AsRef::as_ref)
.collect::<Vec<_>>();
format!(
"plugin '{plugin_id}' was not found (available: {})",
available.join(", ")
)
}
}
pub(super) fn format_plugin_not_enabled_message(plugin_id: &str) -> String {
format!(
"plugin '{plugin_id}' is not enabled; remove it from plugins.disabled or add it under plugins.enabled to run commands"
)
}
pub(super) fn unknown_external_command_message(args: &[String]) -> String {
format!(
"unknown command '{}'; run 'bmux plugin list' to inspect available plugins",
args.join(" ")
)
}
pub(super) fn format_plugin_argument_validation_error(
command_path: &[String],
error: &dyn std::fmt::Display,
) -> String {
let base = format!(
"failed validating plugin command arguments for '{}': {error}",
command_path.join(" ")
);
if base.contains("missing required") {
format!("{base}\nHint: run '<command> --help' to inspect required plugin options.")
} else {
base
}
}
pub(super) async fn run_external_plugin_command(args: &[String]) -> Result<u8> {
let total_started = Instant::now();
let config_started = Instant::now();
let config = BmuxConfig::load()?;
let paths = ConfigPaths::default();
let config_us = config_started.elapsed().as_micros();
let scan_started = Instant::now();
let registry = scan_available_plugins(&config, &paths)?;
let scan_us = scan_started.elapsed().as_micros();
let registry_started = Instant::now();
let mut command_config = config.clone();
command_config.plugins.enabled = effective_enabled_plugins(&config, ®istry);
let command_registry = PluginCommandRegistry::build(&command_config, ®istry)
.context("failed building plugin CLI command registry")?;
let registry_us = registry_started.elapsed().as_micros();
let resolve_started = Instant::now();
let resolved = command_registry
.resolve(args)
.with_context(|| unknown_external_command_message(args))?;
let validated_arguments =
PluginCommandRegistry::validate_arguments(&resolved.schema, &resolved.arguments).map_err(
|error| anyhow::anyhow!(format_plugin_argument_validation_error(args, &error)),
)?;
let resolve_us = resolve_started.elapsed().as_micros();
let dispatch_started = Instant::now();
let status = run_plugin_command(
&resolved.plugin_id,
&resolved.command_name,
&validated_arguments,
)
.await?;
emit_external_plugin_phase_timing(
args,
config_us,
scan_us,
registry_us,
resolve_us,
dispatch_started.elapsed().as_micros(),
total_started.elapsed().as_micros(),
);
Ok(status)
}
fn emit_external_plugin_phase_timing(
args: &[String],
config_us: u128,
scan_us: u128,
registry_us: u128,
resolve_us: u128,
dispatch_us: u128,
total_us: u128,
) {
let payload = PhasePayload::new("plugin.external_command")
.field("command_path", args)
.field("config_us", config_us)
.field("scan_us", scan_us)
.field("registry_us", registry_us)
.field("resolve_us", resolve_us)
.field("dispatch_us", dispatch_us)
.field("total_us", total_us)
.finish();
emit_phase_timing(PhaseChannel::Plugin, &payload);
}
#[cfg(test)]
mod tests {
static TEMP_DIR_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn temp_dir() -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time should be monotonic for test")
.as_nanos();
let id = TEMP_DIR_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("bmux-cli-plugin-test-{nanos}-{id}"));
std::fs::create_dir_all(&dir).expect("temp dir should be created");
dir
}
#[allow(clippy::wildcard_imports)]
use super::*;
use crate::runtime::attach::runtime::{
plugin_fallback_new_context_id, plugin_fallback_retarget_context_id,
};
use crate::runtime::built_in_commands::BuiltInHandlerId;
use crate::runtime::cli_parse::{ParsedRuntimeCli, parse_runtime_cli_with_registry};
use crate::runtime::dispatch::built_in_handler_for_command;
use crate::runtime::session_cli::format_destructive_op_error;
use crate::runtime::terminal_doctor::{filter_trace_events, merged_runtime_keybindings};
use crate::runtime::terminal_protocol::{ProtocolDirection, ProtocolTraceEvent};
use bmux_cli_schema::{Command, TraceFamily};
use bmux_client::ClientError;
use bmux_config::{BmuxConfig, ConfigPaths};
use bmux_ipc::ErrorCode;
use bmux_plugin::{PluginManifest, PluginRegistry};
use std::ffi::OsString;
use std::fs;
use uuid::Uuid;
fn plugin_manifest(id: &str, entry: &str) -> PluginManifest {
plugin_manifest_with_commands(id, entry, "")
}
fn plugin_manifest_with_commands(id: &str, entry: &str, commands: &str) -> PluginManifest {
let manifest = format!(
"id = \"{id}\"\nname = \"{id}\"\nversion = \"0.1.0\"\nentry = \"{entry}\"\n{commands}\n"
);
PluginManifest::from_toml_str(&manifest).expect("manifest should parse")
}
#[test]
fn validate_enabled_plugins_accepts_registered_plugin() {
let dir = temp_dir();
let plugin_dir = dir.join("example");
fs::create_dir_all(&plugin_dir).expect("plugin dir should exist");
fs::write(plugin_dir.join("example.dylib"), []).expect("entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest(
&plugin_dir.join("plugin.toml"),
plugin_manifest("example.plugin", "example.dylib"),
)
.expect("plugin should register");
let mut config = BmuxConfig::default();
config.plugins.enabled.push("example.plugin".to_string());
assert!(validate_enabled_plugins(&config, ®istry).is_ok());
}
#[test]
fn validate_enabled_plugins_rejects_when_required_namespace_is_unowned() {
let dir = temp_dir();
let plugin_dir = dir.join("plugin-cli");
fs::create_dir_all(&plugin_dir).expect("plugin dir should exist");
fs::write(plugin_dir.join("plugin-cli.dylib"), []).expect("entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest(
&plugin_dir.join("plugin.toml"),
plugin_manifest_with_commands(
"bmux.plugin_cli",
"plugin-cli.dylib",
"owns_namespaces=['plugin']\n[[commands]]\nname='list'\npath=['plugin','list']\nsummary='list'\nexecution='provider_exec'\nexpose_in_cli=true\n",
),
)
.expect("plugin should register");
let mut config = BmuxConfig::default();
config.plugins.disabled.push("bmux.plugin_cli".to_string());
config
.plugins
.routing
.required_namespaces
.push(bmux_config::RequiredNamespaceClaim {
namespace: "plugin".to_string(),
owner: None,
});
let error = validate_enabled_plugins(&config, ®istry)
.expect_err("required namespace claim should fail when unowned");
assert!(
error
.to_string()
.contains("required namespace claim 'plugin' is not owned")
);
}
#[test]
fn validate_enabled_plugins_rejects_when_required_namespace_owner_mismatches() {
let dir = temp_dir();
let plugin_dir = dir.join("plugin-cli");
fs::create_dir_all(&plugin_dir).expect("plugin dir should exist");
fs::write(plugin_dir.join("plugin-cli.dylib"), []).expect("entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest(
&plugin_dir.join("plugin.toml"),
plugin_manifest_with_commands(
"bmux.plugin_cli",
"plugin-cli.dylib",
"owns_namespaces=['plugin']\n[[commands]]\nname='list'\npath=['plugin','list']\nsummary='list'\nexecution='provider_exec'\nexpose_in_cli=true\n",
),
)
.expect("plugin should register");
let mut config = BmuxConfig::default();
config
.plugins
.routing
.required_namespaces
.push(bmux_config::RequiredNamespaceClaim {
namespace: "plugin".to_string(),
owner: Some("third.party".to_string()),
});
let error = validate_enabled_plugins(&config, ®istry)
.expect_err("owner mismatch should fail startup validation");
assert!(
error
.to_string()
.contains("required namespace claim 'plugin'")
);
}
#[test]
fn effective_enabled_plugins_includes_bundled_plugins_by_default() {
let Some(bundled_root) = bundled_plugin_root() else {
return;
};
let dir = temp_dir();
fs::write(dir.join("windows.dylib"), []).expect("entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest_from_root(
&bundled_root,
&dir.join("plugin.toml"),
plugin_manifest("bmux.windows", "windows.dylib"),
)
.expect("bundled plugin should register");
let config = BmuxConfig::default();
let enabled = effective_enabled_plugins(&config, ®istry);
assert!(enabled.iter().any(|plugin_id| plugin_id == "bmux.windows"));
}
#[test]
fn windows_manifest_marks_navigation_commands_repeat_safe() {
let mut registry = PluginRegistry::new();
registry
.register_bundled_manifest(include_str!(
"../../../../plugins/windows-plugin/plugin.toml"
))
.expect("windows manifest should register");
assert!(registry.command_accepts_repeat("bmux.windows", "focus-pane-in-direction"));
assert!(registry.command_accepts_repeat("bmux.windows", "resize-pane"));
assert!(registry.command_accepts_repeat("bmux.windows", "next-window"));
assert!(registry.command_accepts_repeat("bmux.windows", "prev-window"));
assert!(!registry.command_accepts_repeat("bmux.windows", "close-active-pane"));
}
#[test]
fn static_bundled_load_uses_registered_vtable() {
let config = BmuxConfig::default();
let paths = ConfigPaths::new(
std::path::PathBuf::from("/config"),
std::path::PathBuf::from("/runtime"),
std::path::PathBuf::from("/data"),
std::path::PathBuf::from("/state"),
);
let registry = scan_available_plugins(&config, &paths).expect("plugin scan should succeed");
let plugin = registry
.get("bmux.theme")
.expect("theme plugin should be registered");
assert!(plugin.bundled_static);
assert!(
bmux_plugin::static_vtable("bmux.theme").is_some(),
"scan should register the process-wide static vtable used by load_plugin"
);
let enabled = effective_enabled_plugins(&config, ®istry);
let providers = registry
.capability_providers_for(&enabled, &core_provided_capabilities())
.expect("providers should resolve");
let loaded = load_plugin(
plugin,
&plugin_host_metadata(),
&providers,
&BmuxConfig::default(),
)
.expect("registered static vtable should load bundled plugin");
assert_eq!(loaded.declaration.id.as_str(), "bmux.theme");
}
#[test]
fn effective_enabled_plugins_include_windows_and_permissions_by_default() {
let Some(bundled_root) = bundled_plugin_root() else {
return;
};
let dir = temp_dir();
fs::write(dir.join("windows.dylib"), []).expect("windows entry should be written");
fs::write(dir.join("permissions.dylib"), []).expect("permissions entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest_from_root(
&bundled_root,
&dir.join("windows.toml"),
plugin_manifest("bmux.windows", "windows.dylib"),
)
.expect("windows plugin should register");
registry
.register_manifest_from_root(
&bundled_root,
&dir.join("permissions.toml"),
plugin_manifest("bmux.permissions", "permissions.dylib"),
)
.expect("permissions plugin should register");
let config = BmuxConfig::default();
let enabled = effective_enabled_plugins(&config, ®istry);
assert!(enabled.iter().any(|plugin_id| plugin_id == "bmux.windows"));
assert!(
enabled
.iter()
.any(|plugin_id| plugin_id == "bmux.permissions")
);
}
#[test]
fn effective_enabled_plugins_honors_disabled_overrides() {
let Some(bundled_root) = bundled_plugin_root() else {
return;
};
let dir = temp_dir();
fs::write(dir.join("windows.dylib"), []).expect("entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest_from_root(
&bundled_root,
&dir.join("plugin.toml"),
plugin_manifest("bmux.windows", "windows.dylib"),
)
.expect("bundled plugin should register");
let mut config = BmuxConfig::default();
config.plugins.disabled.push("bmux.windows".to_string());
let enabled = effective_enabled_plugins(&config, ®istry);
assert!(!enabled.iter().any(|plugin_id| plugin_id == "bmux.windows"));
}
#[test]
fn effective_enabled_plugins_skips_bundled_plugins_with_missing_entry() {
let Some(bundled_root) = bundled_plugin_root() else {
return;
};
let dir = temp_dir();
let mut registry = PluginRegistry::new();
registry
.register_manifest_from_root(
&bundled_root,
&dir.join("plugin.toml"),
plugin_manifest("bmux.windows", "windows.dylib"),
)
.expect("bundled plugin should register");
let config = BmuxConfig::default();
let enabled = effective_enabled_plugins(&config, ®istry);
assert!(!enabled.iter().any(|plugin_id| plugin_id == "bmux.windows"));
}
#[test]
fn validate_enabled_plugins_accepts_plugin_provided_capabilities() {
let dir = temp_dir();
let provider_dir = dir.join("provider");
let dependent_dir = dir.join("consumer");
fs::create_dir_all(&provider_dir).expect("provider dir should exist");
fs::create_dir_all(&dependent_dir).expect("dependent dir should exist");
fs::write(provider_dir.join("provider.dylib"), []).expect("provider entry should exist");
fs::write(dependent_dir.join("consumer.dylib"), []).expect("dependent entry should exist");
let mut registry = PluginRegistry::new();
registry
.register_manifest(
&provider_dir.join("plugin.toml"),
PluginManifest::from_toml_str(
"id='provider.plugin'\nname='Provider'\nversion='0.1.0'\nentry='provider.dylib'\nrequired_capabilities=['bmux.commands']\nprovided_capabilities=['example.cap.read','example.cap.write']\n[plugin_api]\nminimum='1.0'\n[native_abi]\nminimum='1.0'\n",
)
.expect("provider manifest should parse"),
)
.expect("provider should register");
registry
.register_manifest(
&dependent_dir.join("plugin.toml"),
PluginManifest::from_toml_str(
"id='consumer.plugin'\nname='Consumer'\nversion='0.1.0'\nentry='consumer.dylib'\nrequired_capabilities=['example.cap.read']\n[[dependencies]]\nplugin_id='provider.plugin'\nversion_req='^0.1'\n[plugin_api]\nminimum='1.0'\n[native_abi]\nminimum='1.0'\n",
)
.expect("dependent manifest should parse"),
)
.expect("dependent should register");
let mut config = BmuxConfig::default();
config.plugins.enabled.push("provider.plugin".to_string());
config.plugins.enabled.push("consumer.plugin".to_string());
assert!(validate_enabled_plugins(&config, ®istry).is_ok());
}
#[test]
fn validate_enabled_plugins_rejects_missing_plugin() {
let mut config = BmuxConfig::default();
config.plugins.enabled.push("missing.plugin".to_string());
let error = validate_enabled_plugins(&config, &PluginRegistry::new())
.expect_err("validation should fail");
assert!(error.to_string().contains("missing.plugin"));
}
#[test]
fn validate_configured_plugins_discovers_plugins_from_default_layout() {
let dir = temp_dir();
let plugin_dir = dir.join("data").join("plugins").join("example");
fs::create_dir_all(&plugin_dir).expect("plugin dir should exist");
fs::write(plugin_dir.join("example.dylib"), []).expect("entry should be written");
fs::write(
plugin_dir.join("plugin.toml"),
"id = 'example.plugin'\nname = 'Example'\nversion='0.1.0'\nentry='example.dylib'\nrequired_capabilities=['bmux.commands']\n[plugin_api]\nminimum='1.0'\n[native_abi]\nminimum='1.0'\n",
)
.expect("manifest should be written");
let mut config = BmuxConfig::default();
config.plugins.enabled.push("example.plugin".to_string());
let paths = ConfigPaths::new(
dir.join("config"),
dir.join("runtime"),
dir.join("data"),
dir.join("state"),
);
assert!(validate_configured_plugins(&config, &paths).is_ok());
}
#[test]
fn runtime_cli_prefers_dynamic_session_plugin_aliases_over_static_cli_rejection() {
let dir = temp_dir();
let plugin_dir = dir.join("policy");
fs::create_dir_all(&plugin_dir).expect("plugin dir should exist");
fs::write(plugin_dir.join("policy.dylib"), []).expect("entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest(
&plugin_dir.join("plugin.toml"),
plugin_manifest_with_commands(
"policy.plugin",
"policy.dylib",
"owns_namespaces=['roles','session']\n[[commands]]\nname='roles'\npath=['roles']\naliases=[[\"session\",\"roles\"]]\nsummary='list'\nexecution='provider_exec'\nexpose_in_cli=true\n[[commands.arguments]]\nname='session'\nkind='string'\nlong='session'\nrequired=true\n",
),
)
.expect("plugin should register");
let mut config = BmuxConfig::default();
config.plugins.enabled.push("policy.plugin".to_string());
let argv = vec![
OsString::from("bmux"),
OsString::from("session"),
OsString::from("roles"),
OsString::from("--session"),
OsString::from("dev"),
];
let parsed = parse_runtime_cli_with_registry(
&argv,
&config,
®istry,
None,
bmux_config::ConfigLoadOverrides::default(),
)
.expect("runtime CLI should parse plugin alias under session namespace");
match parsed {
ParsedRuntimeCli::Plugin {
plugin_id,
command_name,
arguments,
..
} => {
assert_eq!(plugin_id, "policy.plugin");
assert_eq!(command_name, "roles");
assert_eq!(arguments, vec!["--session".to_string(), "dev".to_string()]);
}
other => panic!("expected plugin runtime parse, got {other:?}"),
}
}
#[test]
fn runtime_cli_allows_plugin_owned_plugin_namespace_commands() {
let dir = temp_dir();
let plugin_dir = dir.join("plugin-cli");
fs::create_dir_all(&plugin_dir).expect("plugin dir should exist");
fs::write(plugin_dir.join("plugin-cli.dylib"), []).expect("entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest(
&plugin_dir.join("plugin.toml"),
plugin_manifest_with_commands(
"bmux.plugin_cli",
"plugin-cli.dylib",
"owns_namespaces=['plugin']\n[[commands]]\nname='list'\npath=['plugin','list']\nsummary='list'\nexecution='provider_exec'\nexpose_in_cli=true\n",
),
)
.expect("plugin should register");
let mut config = BmuxConfig::default();
config.plugins.enabled.push("bmux.plugin_cli".to_string());
let argv = vec![
OsString::from("bmux"),
OsString::from("plugin"),
OsString::from("list"),
];
let parsed = parse_runtime_cli_with_registry(
&argv,
&config,
®istry,
None,
bmux_config::ConfigLoadOverrides::default(),
)
.expect("runtime CLI should parse plugin-owned plugin namespace command");
match parsed {
ParsedRuntimeCli::Plugin {
plugin_id,
command_name,
arguments,
..
} => {
assert_eq!(plugin_id, "bmux.plugin_cli");
assert_eq!(command_name, "list");
assert!(arguments.is_empty());
}
other => panic!("expected plugin runtime parse, got {other:?}"),
}
}
#[test]
fn runtime_cli_parses_bundled_plugin_command_without_explicit_enable() {
let Some(bundled_root) = bundled_plugin_root() else {
return;
};
let dir = temp_dir();
fs::write(dir.join("windows.dylib"), []).expect("entry should be written");
let mut registry = PluginRegistry::new();
registry
.register_manifest_from_root(
&bundled_root,
&dir.join("plugin.toml"),
plugin_manifest_with_commands(
"bmux.windows",
"windows.dylib",
"owns_namespaces=['new-window']\n[[commands]]\nname='new-window'\npath=['new-window']\nsummary='new'\nexecution='provider_exec'\nexpose_in_cli=true\n",
),
)
.expect("plugin should register");
let config = BmuxConfig::default();
let argv = vec![OsString::from("bmux"), OsString::from("new-window")];
let parsed = parse_runtime_cli_with_registry(
&argv,
&config,
®istry,
None,
bmux_config::ConfigLoadOverrides::default(),
)
.expect("runtime CLI should parse bundled plugin command");
match parsed {
ParsedRuntimeCli::Plugin { plugin_id, .. } => {
assert_eq!(plugin_id, "bmux.windows");
}
other => panic!("expected plugin runtime parse, got {other:?}"),
}
}
#[test]
fn runtime_cli_attach_remains_builtin_without_windows_plugin() {
let config = BmuxConfig::default();
let registry = PluginRegistry::new();
let argv = vec![
OsString::from("bmux"),
OsString::from("attach"),
OsString::from("dev"),
];
let parsed = parse_runtime_cli_with_registry(
&argv,
&config,
®istry,
None,
bmux_config::ConfigLoadOverrides::default(),
)
.expect("runtime CLI should parse built-in attach command");
match parsed {
ParsedRuntimeCli::BuiltIn { cli, .. } => {
assert!(matches!(
cli.command,
Some(Command::Attach {
target: Some(ref target),
follow: None,
global: false,
}) if target == "dev"
));
}
other => panic!("expected built-in CLI parse, got {other:?}"),
}
}
#[test]
#[allow(clippy::too_many_lines)]
fn plugin_lifecycle_context_uses_plugin_specific_settings() {
let mut config = BmuxConfig::default();
config
.plugins
.settings
.insert("example.plugin".to_string(), "configured".into());
let paths = ConfigPaths::new(
std::path::PathBuf::from("/config"),
std::path::PathBuf::from("/runtime"),
std::path::PathBuf::from("/data"),
std::path::PathBuf::from("/state"),
);
let declaration = bmux_plugin::PluginDeclaration {
id: bmux_plugin::PluginId::new("example.plugin").expect("id should parse"),
display_name: "Example".to_string(),
plugin_version: "0.1.0".to_string(),
plugin_api: bmux_plugin_sdk::VersionRange::at_least(bmux_plugin_sdk::ApiVersion::new(
1, 0,
)),
native_abi: bmux_plugin_sdk::VersionRange::at_least(bmux_plugin_sdk::ApiVersion::new(
1, 0,
)),
entrypoint: bmux_plugin::PluginEntrypoint::Native {
symbol: bmux_plugin_sdk::DEFAULT_NATIVE_ENTRY_SYMBOL.to_string(),
},
description: None,
homepage: None,
provider_priority: 0,
execution_class: bmux_plugin::PluginExecutionClass::NativeStandard,
owns_namespaces: std::collections::BTreeSet::new(),
owns_paths: std::collections::BTreeSet::new(),
required_capabilities: std::collections::BTreeSet::from([
bmux_plugin_sdk::HostScope::new("bmux.commands").expect("capability should parse"),
]),
provided_capabilities: std::collections::BTreeSet::from([
bmux_plugin_sdk::HostScope::new("example.provider.write")
.expect("capability should parse"),
]),
provided_features: std::collections::BTreeSet::new(),
services: vec![bmux_plugin_sdk::PluginService {
capability: bmux_plugin_sdk::HostScope::new("example.provider.write")
.expect("capability should parse"),
kind: bmux_plugin_sdk::ServiceKind::Command,
interface_id: "provider-command/v1".to_string(),
}],
commands: Vec::new(),
event_subscriptions: Vec::new(),
event_publications: Vec::new(),
dependencies: Vec::new(),
lifecycle: bmux_plugin::PluginLifecycle::default(),
ready_signals: Vec::new(),
};
let context = plugin_lifecycle_context(
&config,
&paths,
&declaration,
service_descriptors_from_declarations([&declaration]),
vec![
"bmux.commands".to_string(),
"example.provider.write".to_string(),
],
vec!["example.plugin".to_string()],
vec!["/plugins".to_string()],
Vec::new(),
);
assert_eq!(context.plugin_id, "example.plugin");
assert_eq!(context.connection.data_dir, "/data");
assert_eq!(
context.required_capabilities,
vec!["bmux.commands".to_string()]
);
assert_eq!(
context.provided_capabilities,
vec!["example.provider.write".to_string()]
);
assert_eq!(context.services.len(), 8);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "config-query/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "storage-query/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "storage-command/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "volatile-state-query/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "volatile-state-command/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "logging-command/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "cli-command/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "provider-command/v1")
);
assert_eq!(
context.settings.as_ref().and_then(|value| value.as_str()),
Some("configured")
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn plugin_command_context_includes_capability_sets() {
let config = BmuxConfig::default();
let paths = ConfigPaths::new(
std::path::PathBuf::from("/config"),
std::path::PathBuf::from("/runtime"),
std::path::PathBuf::from("/data"),
std::path::PathBuf::from("/state"),
);
let declaration = bmux_plugin::PluginDeclaration {
id: bmux_plugin::PluginId::new("provider.plugin").expect("id should parse"),
display_name: "Provider".to_string(),
plugin_version: "0.1.0".to_string(),
plugin_api: bmux_plugin_sdk::VersionRange::at_least(bmux_plugin_sdk::ApiVersion::new(
1, 0,
)),
native_abi: bmux_plugin_sdk::VersionRange::at_least(bmux_plugin_sdk::ApiVersion::new(
1, 0,
)),
entrypoint: bmux_plugin::PluginEntrypoint::Native {
symbol: bmux_plugin_sdk::DEFAULT_NATIVE_ENTRY_SYMBOL.to_string(),
},
description: None,
homepage: None,
provider_priority: 0,
execution_class: bmux_plugin::PluginExecutionClass::NativeStandard,
owns_namespaces: std::collections::BTreeSet::new(),
owns_paths: std::collections::BTreeSet::new(),
required_capabilities: std::collections::BTreeSet::from([
bmux_plugin_sdk::HostScope::new("bmux.commands").expect("capability should parse"),
bmux_plugin_sdk::HostScope::new("example.base.read")
.expect("capability should parse"),
]),
provided_capabilities: std::collections::BTreeSet::from([
bmux_plugin_sdk::HostScope::new("example.provider.read")
.expect("capability should parse"),
bmux_plugin_sdk::HostScope::new("example.provider.write")
.expect("capability should parse"),
]),
provided_features: std::collections::BTreeSet::new(),
services: vec![
bmux_plugin_sdk::PluginService {
capability: bmux_plugin_sdk::HostScope::new("example.provider.read")
.expect("capability should parse"),
kind: bmux_plugin_sdk::ServiceKind::Query,
interface_id: "provider-query/v1".to_string(),
},
bmux_plugin_sdk::PluginService {
capability: bmux_plugin_sdk::HostScope::new("example.provider.write")
.expect("capability should parse"),
kind: bmux_plugin_sdk::ServiceKind::Command,
interface_id: "provider-command/v1".to_string(),
},
],
commands: Vec::new(),
event_subscriptions: Vec::new(),
event_publications: Vec::new(),
dependencies: Vec::new(),
lifecycle: bmux_plugin::PluginLifecycle::default(),
ready_signals: Vec::new(),
};
let context = plugin_command_context(
&config,
&paths,
&declaration,
"run-action",
&["--name".to_string(), "editor".to_string()],
service_descriptors_from_declarations([&declaration]),
vec![
"bmux.commands".to_string(),
"example.base.read".to_string(),
"example.provider.read".to_string(),
"example.provider.write".to_string(),
],
vec!["provider.plugin".to_string()],
vec!["/plugins".to_string()],
Vec::new(),
Vec::new(),
None,
bmux_plugin_sdk::NativeCommandInvocationSource::Unknown,
);
assert_eq!(context.plugin_id, "provider.plugin");
assert_eq!(context.command, "run-action");
assert_eq!(
context.required_capabilities,
vec!["bmux.commands".to_string(), "example.base.read".to_string()]
);
assert_eq!(
context.provided_capabilities,
vec![
"example.provider.read".to_string(),
"example.provider.write".to_string()
]
);
assert_eq!(context.services.len(), 9);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "config-query/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "storage-query/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "storage-command/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "logging-command/v1")
);
assert!(
context
.services
.iter()
.any(|service| service.interface_id == "cli-command/v1")
);
}
#[test]
fn plugin_system_event_uses_system_kind_and_name() {
let event = plugin_system_event("server_started");
assert_eq!(event.kind.as_str(), "bmux.core/server_started");
assert_eq!(
event
.payload
.get("product")
.and_then(serde_json::Value::as_str),
Some("bmux")
);
}
#[test]
fn plugin_event_from_server_event_maps_kind_and_payload() {
let server_event = bmux_client::ServerEvent::ServerStarted;
let event =
plugin_event_from_server_event(&server_event).expect("plugin event should build");
assert_eq!(event.kind.as_str(), "bmux.core/server_started");
assert_eq!(
event.payload,
serde_json::to_value(server_event).expect("server event should encode")
);
}
#[test]
fn built_in_handler_mapping_stays_in_sync_for_core_native_commands() {
let command = Command::KillSession {
target: "dev".to_string(),
force_local: false,
};
assert_eq!(
built_in_handler_for_command(&command),
BuiltInHandlerId::KillSession
);
}
#[test]
fn runtime_keybindings_deep_merge_defaults_and_overrides() {
let mut config = BmuxConfig::default();
config.keybindings.runtime.clear();
config
.keybindings
.runtime
.insert("o".to_string(), "quit".to_string());
let (runtime, _global, _scroll) = merged_runtime_keybindings(&config);
assert_eq!(runtime.get("o"), Some(&"quit".to_string()));
assert_eq!(
runtime.get("%"),
Some(&"plugin:bmux.windows:split-pane --direction vertical".to_string())
);
assert_eq!(runtime.get("["), Some(&"enter_scroll_mode".to_string()));
}
#[test]
fn trace_filtering_applies_family_and_pane_constraints() {
let events = vec![
ProtocolTraceEvent {
timestamp_ms: 1,
pane_id: Some(1),
profile: "xterm".to_string(),
family: "csi".to_string(),
name: "csi_primary_da".to_string(),
direction: ProtocolDirection::Query,
raw_hex: "1b5b63".to_string(),
decoded: "\u{1b}[c".to_string(),
},
ProtocolTraceEvent {
timestamp_ms: 2,
pane_id: Some(2),
profile: "xterm".to_string(),
family: "osc".to_string(),
name: "osc_color_query".to_string(),
direction: ProtocolDirection::Reply,
raw_hex: "1b5d31303b3f".to_string(),
decoded: "...".to_string(),
},
ProtocolTraceEvent {
timestamp_ms: 3,
pane_id: Some(2),
profile: "xterm".to_string(),
family: "csi".to_string(),
name: "csi_primary_da".to_string(),
direction: ProtocolDirection::Reply,
raw_hex: "1b5b3f313b3263".to_string(),
decoded: "...".to_string(),
},
];
let by_family = filter_trace_events(&events, Some(TraceFamily::Csi), None, 50);
assert_eq!(by_family.len(), 2);
let by_pane = filter_trace_events(&events, None, Some(2), 50);
assert_eq!(by_pane.len(), 2);
let both = filter_trace_events(&events, Some(TraceFamily::Csi), Some(2), 50);
assert_eq!(both.len(), 1);
assert_eq!(both[0].timestamp_ms, 3);
}
#[test]
fn destructive_op_error_formats_session_policy_guidance() {
let message = format_destructive_op_error(
"session",
ClientError::ServerError {
code: ErrorCode::InvalidRequest,
message: "session policy denied for this operation".to_string(),
},
false,
);
assert!(message.contains("not permitted by current session policy"));
}
#[test]
fn destructive_op_error_formats_force_local_guidance() {
let message = format_destructive_op_error(
"window",
ClientError::ServerError {
code: ErrorCode::InvalidRequest,
message: "force-local is only allowed for the server control principal".to_string(),
},
true,
);
assert!(message.contains("--force-local"));
assert!(message.contains("bmux server whoami-principal"));
}
#[test]
fn format_plugin_command_run_error_adds_policy_hint_when_denied() {
let error = anyhow::anyhow!("session policy denied for this operation");
let message = format_plugin_command_run_error("bmux.windows", "kill", &error);
assert!(message.contains("failed running plugin command 'bmux.windows:kill'"));
assert!(message.contains("operation denied by an active policy provider"));
assert!(message.contains("authorized principal"));
}
#[test]
fn format_plugin_command_run_error_keeps_generic_failures_without_hint() {
let error = anyhow::anyhow!("unsupported service operation");
let message = format_plugin_command_run_error("bmux.permissions", "grant", &error);
assert!(message.contains("failed running plugin command 'bmux.permissions:grant'"));
assert!(!message.contains("operation denied by session policy"));
}
#[test]
fn unknown_external_command_message_points_to_plugin_list_help() {
let message =
unknown_external_command_message(&["session".to_string(), "roles".to_string()]);
assert!(message.contains("unknown command 'session roles'"));
assert!(message.contains("bmux plugin list"));
}
#[test]
fn format_plugin_not_found_message_lists_available_plugins() {
let message = format_plugin_not_found_message(
"missing.plugin",
&["bmux.windows".to_string(), "bmux.permissions".to_string()],
);
assert!(message.contains("plugin 'missing.plugin' was not found"));
assert!(message.contains("bmux.windows, bmux.permissions"));
}
#[test]
fn format_plugin_not_found_message_handles_empty_registry() {
let empty: [&str; 0] = [];
let message = format_plugin_not_found_message("missing.plugin", &empty);
assert_eq!(message, "plugin 'missing.plugin' was not found");
}
#[test]
fn format_plugin_not_enabled_message_points_to_plugins_enabled() {
let message = format_plugin_not_enabled_message("bmux.windows");
assert!(message.contains("plugin 'bmux.windows' is not enabled"));
assert!(message.contains("plugins.disabled"));
assert!(message.contains("plugins.enabled"));
}
#[test]
fn format_plugin_argument_validation_error_adds_help_hint_for_missing_required() {
let error = anyhow::anyhow!("missing required option '--session'");
let message = format_plugin_argument_validation_error(
&["session".to_string(), "roles".to_string()],
&error,
);
assert!(message.contains("failed validating plugin command arguments for 'session roles'"));
assert!(message.contains("missing required option '--session'"));
assert!(message.contains("--help"));
}
#[test]
fn format_plugin_argument_validation_error_keeps_non_required_errors_without_hint() {
let error = anyhow::anyhow!("unknown option '--wat'");
let message = format_plugin_argument_validation_error(
&["session".to_string(), "roles".to_string()],
&error,
);
assert!(message.contains("failed validating plugin command arguments for 'session roles'"));
assert!(message.contains("unknown option '--wat'"));
assert!(!message.contains("--help"));
}
#[test]
fn plugin_fallback_retarget_context_id_returns_changed_context_when_no_effect_applied() {
let before = Some(Uuid::from_u128(1));
let after = Some(Uuid::from_u128(2));
let attached = Some(Uuid::from_u128(1));
assert_eq!(
plugin_fallback_retarget_context_id(before, after, attached, false),
after
);
}
#[test]
fn plugin_fallback_retarget_context_id_ignores_when_outcome_already_applied() {
let before = Some(Uuid::from_u128(1));
let after = Some(Uuid::from_u128(2));
let attached = Some(Uuid::from_u128(2));
assert_eq!(
plugin_fallback_retarget_context_id(before, after, attached, true),
None
);
}
#[test]
#[allow(clippy::iter_on_single_items)]
fn plugin_fallback_new_context_id_returns_single_new_context() {
let before = [Uuid::from_u128(1)]
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
let after = [Uuid::from_u128(1), Uuid::from_u128(2)]
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
plugin_fallback_new_context_id(
Some(&before),
Some(&after),
Some(Uuid::from_u128(1)),
Some(Uuid::from_u128(1)),
false,
),
Some(Uuid::from_u128(2))
);
}
#[test]
#[allow(clippy::iter_on_single_items)]
fn plugin_fallback_new_context_id_prefers_after_context_when_multiple_new() {
let before = [Uuid::from_u128(1)]
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
let after = [Uuid::from_u128(1), Uuid::from_u128(2), Uuid::from_u128(3)]
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
plugin_fallback_new_context_id(
Some(&before),
Some(&after),
Some(Uuid::from_u128(1)),
Some(Uuid::from_u128(3)),
false,
),
Some(Uuid::from_u128(3))
);
}
#[test]
#[allow(clippy::iter_on_single_items)]
fn plugin_fallback_new_context_id_ignores_when_outcome_applied() {
let before = [Uuid::from_u128(1)]
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
let after = [Uuid::from_u128(1), Uuid::from_u128(2)]
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
plugin_fallback_new_context_id(
Some(&before),
Some(&after),
Some(Uuid::from_u128(1)),
Some(Uuid::from_u128(2)),
true,
),
None
);
}
}