use crate::{
HostConnectionInfo, HostMetadata, PluginError, RegisteredService, Result, ServiceRequest,
decode_service_message, encode_service_message,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
mod toml_value_option {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[allow(clippy::ref_option)] pub fn serialize<S: Serializer>(
value: &Option<toml::Value>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let text: Option<String> = value
.as_ref()
.map(serde_json::to_string)
.transpose()
.map_err(serde::ser::Error::custom)?;
text.serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<toml::Value>, D::Error> {
let text: Option<String> = Option::deserialize(deserializer)?;
text.map(|s| serde_json::from_str(&s))
.transpose()
.map_err(serde::de::Error::custom)
}
}
mod toml_value_map {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
pub fn serialize<S: Serializer>(
map: &BTreeMap<String, toml::Value>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let text_map: BTreeMap<String, String> = map
.iter()
.map(|(k, v)| serde_json::to_string(v).map(|s| (k.clone(), s)))
.collect::<Result<_, _>>()
.map_err(serde::ser::Error::custom)?;
text_map.serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<BTreeMap<String, toml::Value>, D::Error> {
let text_map: BTreeMap<String, String> = BTreeMap::deserialize(deserializer)?;
text_map
.into_iter()
.map(|(k, s)| {
serde_json::from_str(&s)
.map(|v| (k, v))
.map_err(serde::de::Error::custom)
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegisteredPluginInfo {
pub id: String,
pub display_name: String,
pub version: String,
pub bundled_static: bool,
pub required_capabilities: Vec<String>,
pub provided_capabilities: Vec<String>,
pub commands: Vec<String>,
#[serde(default)]
pub command_schemas: Vec<crate::PluginCommand>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActiveKeybinding {
pub scope: String,
pub chord: String,
pub action: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NativeLifecycleContext {
pub plugin_id: String,
#[serde(default)]
pub required_capabilities: Vec<String>,
#[serde(default)]
pub provided_capabilities: Vec<String>,
#[serde(default)]
pub services: Vec<RegisteredService>,
#[serde(default)]
pub available_capabilities: Vec<String>,
#[serde(default)]
pub enabled_plugins: Vec<String>,
#[serde(default)]
pub plugin_search_roots: Vec<String>,
#[serde(default)]
pub registered_plugins: Vec<RegisteredPluginInfo>,
pub host: HostMetadata,
pub connection: HostConnectionInfo,
#[serde(default, with = "toml_value_option")]
pub settings: Option<toml::Value>,
#[serde(default, with = "toml_value_map")]
pub plugin_settings_map: BTreeMap<String, toml::Value>,
#[serde(default)]
pub host_kernel_bridge: Option<HostKernelBridge>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NativeCommandContext {
pub plugin_id: String,
pub command: String,
pub arguments: Vec<String>,
#[serde(default)]
pub required_capabilities: Vec<String>,
#[serde(default)]
pub provided_capabilities: Vec<String>,
#[serde(default)]
pub services: Vec<RegisteredService>,
#[serde(default)]
pub available_capabilities: Vec<String>,
#[serde(default)]
pub enabled_plugins: Vec<String>,
#[serde(default)]
pub plugin_search_roots: Vec<String>,
#[serde(default)]
pub registered_plugins: Vec<RegisteredPluginInfo>,
#[serde(default)]
pub active_keybindings: Vec<ActiveKeybinding>,
pub host: HostMetadata,
pub connection: HostConnectionInfo,
#[serde(default, with = "toml_value_option")]
pub settings: Option<toml::Value>,
#[serde(default, with = "toml_value_map")]
pub plugin_settings_map: BTreeMap<String, toml::Value>,
#[serde(default)]
pub caller_client_id: Option<uuid::Uuid>,
#[serde(default)]
pub invocation_source: NativeCommandInvocationSource,
#[serde(default)]
pub host_kernel_bridge: Option<HostKernelBridge>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum NativeCommandInvocationSource {
#[default]
Unknown,
Cli,
AttachKeybinding,
Internal,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NativeServiceContext {
pub plugin_id: String,
pub request: ServiceRequest,
#[serde(default)]
pub required_capabilities: Vec<String>,
#[serde(default)]
pub provided_capabilities: Vec<String>,
#[serde(default)]
pub services: Vec<RegisteredService>,
#[serde(default)]
pub available_capabilities: Vec<String>,
#[serde(default)]
pub enabled_plugins: Vec<String>,
#[serde(default)]
pub plugin_search_roots: Vec<String>,
pub host: HostMetadata,
pub connection: HostConnectionInfo,
#[serde(default, with = "toml_value_option")]
pub settings: Option<toml::Value>,
#[serde(default, with = "toml_value_map")]
pub plugin_settings_map: BTreeMap<String, toml::Value>,
#[serde(default)]
pub caller_client_id: Option<uuid::Uuid>,
#[serde(default)]
pub host_kernel_bridge: Option<HostKernelBridge>,
}
type HostKernelBridgeFn = unsafe extern "C" fn(
input_ptr: *const u8,
input_len: usize,
output_ptr: *mut u8,
output_capacity: usize,
output_len: *mut usize,
) -> i32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct HostKernelBridge(u64);
impl HostKernelBridge {
#[must_use]
pub fn from_fn(pointer: HostKernelBridgeFn) -> Self {
Self(pointer as usize as u64)
}
pub fn invoke(
self,
input_ptr: *const u8,
input_len: usize,
output_ptr: *mut u8,
output_capacity: usize,
output_len: *mut usize,
) -> i32 {
#[allow(clippy::cast_possible_truncation)]
let bridge: HostKernelBridgeFn = unsafe { std::mem::transmute(self.0 as usize) };
unsafe {
bridge(
input_ptr,
input_len,
output_ptr,
output_capacity,
output_len,
)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostKernelBridgeRequest {
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostKernelBridgeResponse {
pub payload: Vec<u8>,
}
pub const CORE_CLI_COMMAND_CAPABILITY: &str = "bmux.commands";
pub const CORE_CLI_COMMAND_INTERFACE_V1: &str = "cli-command/v1";
pub const CORE_CLI_COMMAND_RUN_PATH_OPERATION_V1: &str = "run_path";
pub const CORE_CLI_COMMAND_RUN_PLUGIN_OPERATION_V1: &str = "run_plugin";
pub const CORE_CLI_BRIDGE_MAGIC_V1: &[u8] = b"BMUXCMD1";
pub const PLUGIN_CLI_BRIDGE_MAGIC_V1: &[u8] = b"BMUXPLG1";
pub const CORE_CLI_BRIDGE_PROTOCOL_V1: u16 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CoreCliCommandRequest {
pub protocol_version: u16,
pub command_path: Vec<String>,
pub arguments: Vec<String>,
}
impl CoreCliCommandRequest {
#[must_use]
pub const fn new(command_path: Vec<String>, arguments: Vec<String>) -> Self {
Self {
protocol_version: CORE_CLI_BRIDGE_PROTOCOL_V1,
command_path,
arguments,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CoreCliCommandResponse {
pub protocol_version: u16,
pub exit_code: i32,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginCliCommandRequest {
pub protocol_version: u16,
pub plugin_id: String,
pub command_name: String,
pub arguments: Vec<String>,
}
impl PluginCliCommandRequest {
#[must_use]
pub const fn new(plugin_id: String, command_name: String, arguments: Vec<String>) -> Self {
Self {
protocol_version: CORE_CLI_BRIDGE_PROTOCOL_V1,
plugin_id,
command_name,
arguments,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginCliCommandResponse {
pub protocol_version: u16,
pub exit_code: i32,
pub error: Option<String>,
}
impl PluginCliCommandResponse {
#[must_use]
pub const fn new(exit_code: i32) -> Self {
Self {
protocol_version: CORE_CLI_BRIDGE_PROTOCOL_V1,
exit_code,
error: None,
}
}
#[must_use]
pub const fn failed(exit_code: i32, error: String) -> Self {
Self {
protocol_version: CORE_CLI_BRIDGE_PROTOCOL_V1,
exit_code,
error: Some(error),
}
}
}
impl CoreCliCommandResponse {
#[must_use]
pub const fn new(exit_code: i32) -> Self {
Self {
protocol_version: CORE_CLI_BRIDGE_PROTOCOL_V1,
exit_code,
error: None,
}
}
#[must_use]
pub const fn failed(exit_code: i32, error: String) -> Self {
Self {
protocol_version: CORE_CLI_BRIDGE_PROTOCOL_V1,
exit_code,
error: Some(error),
}
}
}
pub fn encode_host_kernel_bridge_cli_command_payload(
request: &CoreCliCommandRequest,
) -> Result<Vec<u8>> {
let mut payload = CORE_CLI_BRIDGE_MAGIC_V1.to_vec();
payload.extend(encode_service_message(request)?);
Ok(payload)
}
pub fn decode_host_kernel_bridge_cli_command_payload(
payload: &[u8],
) -> Result<Option<CoreCliCommandRequest>> {
if !payload.starts_with(CORE_CLI_BRIDGE_MAGIC_V1) {
return Ok(None);
}
let encoded = &payload[CORE_CLI_BRIDGE_MAGIC_V1.len()..];
let request: CoreCliCommandRequest = decode_service_message(encoded)?;
if request.protocol_version != CORE_CLI_BRIDGE_PROTOCOL_V1 {
return Err(PluginError::ServiceProtocol {
details: format!(
"unsupported core CLI bridge request protocol version: {}",
request.protocol_version
),
});
}
Ok(Some(request))
}
pub fn encode_host_kernel_bridge_plugin_command_payload(
request: &PluginCliCommandRequest,
) -> Result<Vec<u8>> {
let mut payload = PLUGIN_CLI_BRIDGE_MAGIC_V1.to_vec();
payload.extend(encode_service_message(request)?);
Ok(payload)
}
pub fn decode_host_kernel_bridge_plugin_command_payload(
payload: &[u8],
) -> Result<Option<PluginCliCommandRequest>> {
if !payload.starts_with(PLUGIN_CLI_BRIDGE_MAGIC_V1) {
return Ok(None);
}
let encoded = &payload[PLUGIN_CLI_BRIDGE_MAGIC_V1.len()..];
let request: PluginCliCommandRequest = decode_service_message(encoded)?;
if request.protocol_version != CORE_CLI_BRIDGE_PROTOCOL_V1 {
return Err(PluginError::ServiceProtocol {
details: format!(
"unsupported plugin CLI bridge request protocol version: {}",
request.protocol_version
),
});
}
Ok(Some(request))
}
#[cfg(test)]
mod tests {
use super::{
CORE_CLI_BRIDGE_PROTOCOL_V1, CoreCliCommandRequest, PluginCliCommandRequest,
decode_host_kernel_bridge_cli_command_payload,
decode_host_kernel_bridge_plugin_command_payload,
encode_host_kernel_bridge_cli_command_payload,
encode_host_kernel_bridge_plugin_command_payload,
};
#[test]
fn cli_bridge_payload_round_trip_preserves_request() {
let request = CoreCliCommandRequest::new(
vec!["logs".to_string(), "path".to_string()],
vec!["--json".to_string()],
);
let encoded =
encode_host_kernel_bridge_cli_command_payload(&request).expect("request should encode");
let decoded = decode_host_kernel_bridge_cli_command_payload(&encoded)
.expect("payload should decode")
.expect("payload should be recognized");
assert_eq!(decoded, request);
}
#[test]
fn cli_bridge_payload_ignores_unknown_prefix() {
let decoded = decode_host_kernel_bridge_cli_command_payload(b"not-a-cli-bridge-payload")
.expect("decode should succeed");
assert!(decoded.is_none());
}
#[test]
fn cli_bridge_payload_rejects_unsupported_protocol_version() {
let mut request = CoreCliCommandRequest::new(Vec::new(), Vec::new());
request.protocol_version = CORE_CLI_BRIDGE_PROTOCOL_V1 + 1;
let encoded =
encode_host_kernel_bridge_cli_command_payload(&request).expect("request should encode");
let error = decode_host_kernel_bridge_cli_command_payload(&encoded)
.expect_err("decode should fail for unsupported protocol version");
assert!(
error
.to_string()
.contains("unsupported core CLI bridge request protocol version")
);
}
#[test]
fn core_cli_response_failed_carries_error() {
let response = super::CoreCliCommandResponse::failed(7, "boom".to_string());
let encoded = super::encode_service_message(&response).expect("response should encode");
let decoded: super::CoreCliCommandResponse =
super::decode_service_message(&encoded).expect("response should decode");
assert_eq!(decoded.exit_code, 7);
assert_eq!(decoded.error.as_deref(), Some("boom"));
}
#[test]
fn plugin_cli_bridge_payload_round_trip_preserves_request() {
let request = PluginCliCommandRequest::new(
"bmux.windows".to_string(),
"new-window".to_string(),
vec!["--name".to_string(), "work".to_string()],
);
let encoded = encode_host_kernel_bridge_plugin_command_payload(&request)
.expect("request should encode");
let decoded = decode_host_kernel_bridge_plugin_command_payload(&encoded)
.expect("payload should decode")
.expect("payload should be recognized");
assert_eq!(decoded, request);
}
#[test]
fn plugin_cli_bridge_payload_ignores_unknown_prefix() {
let decoded =
decode_host_kernel_bridge_plugin_command_payload(b"not-a-plugin-bridge-payload")
.expect("decode should succeed");
assert!(decoded.is_none());
}
#[test]
fn plugin_cli_bridge_payload_rejects_unsupported_protocol_version() {
let mut request = PluginCliCommandRequest::new(
"bmux.windows".to_string(),
"new-window".to_string(),
Vec::new(),
);
request.protocol_version = CORE_CLI_BRIDGE_PROTOCOL_V1 + 1;
let encoded = encode_host_kernel_bridge_plugin_command_payload(&request)
.expect("request should encode");
let error = decode_host_kernel_bridge_plugin_command_payload(&encoded)
.expect_err("decode should fail for unsupported protocol version");
assert!(
error
.to_string()
.contains("unsupported plugin CLI bridge request protocol version")
);
}
}