bmux_plugin 0.0.1-alpha.1

Plugin system for bmux terminal multiplexer
//! Host-side service caller trait and `HostRuntimeApi`.
//!
//! These traits require the host-side service dispatch implementation
//! (provided via `bmux_ipc`) and are therefore defined here rather than
//! in the SDK.
//!
//! `HostRuntimeApi` carries only truly generic primitives. Plugins
//! that speak in session/context/pane/client domain terms either use
//! [`ServiceCaller::execute_kernel_request`] for foundational access
//! to core IPC, or typed BPDL services (`call_service`) for
//! cross-plugin orchestration.

use bmux_plugin_sdk::{
    CORE_CLI_COMMAND_CAPABILITY, CORE_CLI_COMMAND_INTERFACE_V1,
    CORE_CLI_COMMAND_RUN_PATH_OPERATION_V1, CORE_CLI_COMMAND_RUN_PLUGIN_OPERATION_V1,
    CoreCliCommandRequest, CoreCliCommandResponse, LogWriteRequest, PluginCliCommandRequest,
    PluginCliCommandResponse, RecordingWriteEventRequest, RecordingWriteEventResponse, Result,
    ServiceKind, StorageGetRequest, StorageGetResponse, StorageSetRequest,
};
use serde::{Deserialize, Serialize, de::DeserializeOwned};

const RECORDING_WRITE_CAPABILITY: &str = "bmux.recording.write";
const RECORDING_COMMANDS_INTERFACE: &str = "recording-commands";
const RECORDING_WRITE_CUSTOM_EVENT_OPERATION: &str = "write-custom-event";

#[derive(Serialize)]
struct RecordingWriteCustomEventWireRequest {
    session_id: Option<uuid::Uuid>,
    pane_id: Option<uuid::Uuid>,
    source: String,
    name: String,
    #[serde(with = "bmux_codec::serde_bytes_vec")]
    payload: Vec<u8>,
}

#[derive(Deserialize)]
enum RecordingWriteCustomEventWireError {
    NoActive,
    Unavailable,
    Failed { reason: String },
}

impl RecordingWriteCustomEventWireError {
    fn into_reason(self) -> Option<String> {
        match self {
            Self::NoActive | Self::Unavailable => None,
            Self::Failed { reason } => Some(reason),
        }
    }
}

/// Trait for types that can dispatch cross-plugin service calls.
///
/// The three context types ([`NativeCommandContext`](crate::NativeCommandContext),
/// [`NativeLifecycleContext`](crate::NativeLifecycleContext),
/// [`NativeServiceContext`](crate::NativeServiceContext)) and the
/// long-lived [`TypedServiceCaller`](crate::TypedServiceCaller)
/// implement this trait.
pub trait ServiceCaller {
    /// Dispatch a raw service call with a binary payload.
    ///
    /// # Errors
    ///
    /// Returns an error when the capability is not accessible, the service
    /// is not registered, or the provider returns a transport-level error.
    fn call_service_raw(
        &self,
        capability: &str,
        kind: ServiceKind,
        interface_id: &str,
        operation: &str,
        payload: Vec<u8>,
    ) -> Result<Vec<u8>>;

    /// Dispatch a typed service call, serializing the request and
    /// deserializing the response.
    ///
    /// # Errors
    ///
    /// Returns an error when the capability is not accessible, the service
    /// is not registered, or the provider returns a transport-level error.
    fn call_service<Request, Response>(
        &self,
        capability: &str,
        kind: ServiceKind,
        interface_id: &str,
        operation: &str,
        request: &Request,
    ) -> Result<Response>
    where
        Request: Serialize,
        Response: DeserializeOwned,
    {
        let payload = bmux_plugin_sdk::encode_service_message(request)?;
        let response = self.call_service_raw(capability, kind, interface_id, operation, payload)?;
        bmux_plugin_sdk::decode_service_message(&response)
    }

    /// Dispatch an IPC request directly to the host kernel.
    ///
    /// Used by domain plugins (sessions, contexts, clients, windows)
    /// that own typed services backed by core state the host kernel
    /// exposes over IPC. The typed service layer would otherwise cycle
    /// back into the plugin's own handlers.
    ///
    /// # Errors
    ///
    /// Returns an error when the host kernel bridge is missing
    /// (typical in tests), or the server returns an error response.
    fn execute_kernel_request(
        &self,
        request: bmux_ipc::Request,
    ) -> Result<bmux_ipc::ResponsePayload>;
}

/// Generic host runtime API.
///
/// Carries only domain-agnostic primitives: core-CLI command dispatch,
/// plugin-command dispatch, key-value storage, logging, and recording.
/// Every blanket-implemented method is a thin wrapper over a well-known
/// host-provided service (`bmux.storage`, `bmux.logs.write`, etc.).
///
/// Domain-specific conveniences (session/context/pane/client helpers)
/// are not part of this trait. Foundational plugins reach core IPC via
/// [`ServiceCaller::execute_kernel_request`]; non-foundational plugins
/// use typed BPDL services through [`ServiceCaller::call_service`].
pub trait HostRuntimeApi: ServiceCaller {
    /// Run a core built-in CLI command path in-process.
    ///
    /// # Errors
    ///
    /// Returns an error when the service call fails.
    fn core_cli_command_run_path(
        &self,
        request: &CoreCliCommandRequest,
    ) -> Result<CoreCliCommandResponse> {
        self.call_service(
            CORE_CLI_COMMAND_CAPABILITY,
            ServiceKind::Command,
            CORE_CLI_COMMAND_INTERFACE_V1,
            CORE_CLI_COMMAND_RUN_PATH_OPERATION_V1,
            request,
        )
    }

    /// Run a plugin command in-process.
    ///
    /// # Errors
    ///
    /// Returns an error when the service call fails.
    fn plugin_command_run(
        &self,
        request: &PluginCliCommandRequest,
    ) -> Result<PluginCliCommandResponse> {
        self.call_service(
            CORE_CLI_COMMAND_CAPABILITY,
            ServiceKind::Command,
            CORE_CLI_COMMAND_INTERFACE_V1,
            CORE_CLI_COMMAND_RUN_PLUGIN_OPERATION_V1,
            request,
        )
    }

    /// Get a value from plugin storage.
    ///
    /// # Errors
    ///
    /// Returns an error when the service call fails.
    fn storage_get(&self, request: &StorageGetRequest) -> Result<StorageGetResponse> {
        self.call_service(
            "bmux.storage",
            ServiceKind::Query,
            "storage-query/v1",
            "get",
            request,
        )
    }

    /// Set a value in plugin storage.
    ///
    /// # Errors
    ///
    /// Returns an error when the service call fails.
    fn storage_set(&self, request: &StorageSetRequest) -> Result<()> {
        self.call_service(
            "bmux.storage",
            ServiceKind::Command,
            "storage-command/v1",
            "set",
            request,
        )
    }

    /// Write a log message.
    ///
    /// # Errors
    ///
    /// Returns an error when the service call fails.
    fn log_write(&self, request: &LogWriteRequest) -> Result<()> {
        self.call_service(
            "bmux.logs.write",
            ServiceKind::Command,
            "logging-command/v1",
            "write",
            request,
        )
    }

    /// Write a custom recording event.
    ///
    /// # Errors
    ///
    /// Returns an error when the service call fails.
    fn recording_write_event(
        &self,
        request: &RecordingWriteEventRequest,
    ) -> Result<RecordingWriteEventResponse> {
        let recording_request = RecordingWriteCustomEventWireRequest {
            session_id: recording_attribute_uuid(request, "bmux.session_id"),
            pane_id: recording_attribute_uuid(request, "bmux.pane_id"),
            // `HostRuntimeApi` doesn't know which plugin is calling, so
            // callers that need source identity should dispatch directly
            // via the recording plugin's generated command client.
            source: String::new(),
            name: request.name.clone(),
            payload: serde_json::to_vec(&request.payload).unwrap_or_default(),
        };
        let response: std::result::Result<(), RecordingWriteCustomEventWireError> = self
            .call_service(
                RECORDING_WRITE_CAPABILITY,
                ServiceKind::Command,
                RECORDING_COMMANDS_INTERFACE,
                RECORDING_WRITE_CUSTOM_EVENT_OPERATION,
                &recording_request,
            )?;
        match response {
            Ok(()) => Ok(RecordingWriteEventResponse { accepted: true }),
            Err(err) => {
                let _ = err.into_reason();
                Ok(RecordingWriteEventResponse { accepted: false })
            }
        }
    }
}

impl<T> HostRuntimeApi for T where T: ServiceCaller + ?Sized {}

fn recording_attribute_uuid(request: &RecordingWriteEventRequest, key: &str) -> Option<uuid::Uuid> {
    request
        .attributes
        .get(key)
        .and_then(|value| uuid::Uuid::parse_str(value).ok())
}