Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Augani/kael/llms.txt

Use this file to discover all available pages before exploring further.

Kael isolates every plugin in its own OS process and routes all communication through a typed, versioned IPC channel. The design has three independent layers that you compose together: a low-level framed transport, a typed RPC protocol built on top of it, and a process supervisor that keeps child processes healthy. A capability-based permission broker sits across all three layers, deciding what each process is allowed to do before any message is acted on.

IPC transport layer

The Transport trait

All transport implementations share a single interface:
pub trait Transport: Send {
    fn send_frame(&mut self, data: &[u8]) -> Result<()>;
    fn recv_frame(&mut self) -> Result<Vec<u8>>;
    fn close(&mut self) -> Result<()>;
}
The trait is object-safe and Send, so you can box it and move it across threads.

Frame encoding

Messages are length-prefixed with a 4-byte big-endian header:
/// Encode a payload as a length-prefixed frame.
pub fn encode_frame(payload: &[u8]) -> Vec<u8> {
    let len = payload.len() as u32;
    let mut frame = Vec::with_capacity(4 + payload.len());
    frame.extend_from_slice(&len.to_be_bytes());
    frame.extend_from_slice(payload);
    frame
}

/// Decode a length-prefixed frame.
/// Returns the payload and the number of bytes consumed, or None if the
/// buffer is incomplete.
pub fn decode_frame(buf: &[u8]) -> Result<Option<(Vec<u8>, usize)>> {
    if buf.len() < 4 { return Ok(None); }
    let len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
    if buf.len() < 4 + len { return Ok(None); }
    let payload = buf[4..4 + len].to_vec();
    Ok(Some((payload, 4 + len)))
}
You generally do not call these functions directly — ExtensionTransport handles framing for you.

Socket path resolution

pub fn ipc_socket_path(app_id: &str, process_name: &str) -> PathBuf
ipc_socket_path returns a platform-appropriate path:
PlatformPath format
macOS / LinuxResolved via platform socket helper
Windows\\.\pipe\<name> named pipe
Other Unix$TMPDIR/<app_id>-<process_name>.sock

Concrete transport implementations

Used on macOS and Linux for extension communication:
use kael::ipc_transport::UnixDomainSocketTransport;

// Connect to an existing socket (plugin side)
let transport = UnixDomainSocketTransport::connect("/tmp/my-app-my-plugin.sock")?;

// Listen and accept one connection (host side)
let transport = UnixDomainSocketTransport::listen("/tmp/my-app-my-plugin.sock")?;

// Create a connected pair for testing
let (host_side, plugin_side) = UnixDomainSocketTransport::pair()?;

RPC protocol

IpcMessage

The base protocol type carries requests, responses, progress updates, and cancellations over a shared correlation id:
pub enum IpcMessage<Request, Response, Progress, Error> {
    Request  { id: u64, body: Request },
    Response { id: u64, result: Result<Response, Error> },
    Progress { id: u64, body: Progress },
    Cancel   { id: u64 },
}
Extension-specific messages use concrete types for all four type parameters.

Extension message types

ExtensionRequest is sent from the host to the plugin:
pub enum ExtensionRequest {
    Activate,
    Deactivate,
    ExecuteCommand { command_id: String, args: Option<serde_json::Value> },
    GetContributions,
    Shutdown,
}
ExtensionResponse is sent from the plugin back to the host:
pub enum ExtensionResponse {
    Ack,
    Contributions(Contributions),
    Error(String),
}
ExtensionNotification is a one-way fire-and-forget from the plugin to the host:
pub enum ExtensionNotification {
    CommandExecuted { command_id: String, result: Option<serde_json::Value> },
    PanelUpdated    { panel_id: String, state: Option<serde_json::Value> },
    SettingsChanged { key: String, value: serde_json::Value },
}
All three are unified in ExtensionMessage:
pub enum ExtensionMessage {
    Rpc(IpcMessage<ExtensionRequest, ExtensionResponse, (), String>),
    Notification(ExtensionNotification),
    Handshake(ExtensionHandshake),
}

Handshake

When the host spawns an extension process, it immediately sends a Handshake::Host message. The extension must respond with Handshake::Extension before any other RPC traffic is sent:
pub enum ExtensionHandshake {
    Host {
        version: u32,                      // EXTENSION_RPC_VERSION (currently 1)
        capabilities: Vec<serde_json::Value>,
    },
    Extension {
        version: u32,
        accepted: bool,
    },
}

ExtensionTransport

ExtensionTransport wraps any Box<dyn Transport> and provides typed send/receive methods:
pub struct ExtensionTransport {
    inner: Box<dyn Transport>,
}

impl ExtensionTransport {
    pub fn new(inner: Box<dyn Transport>) -> Self;
    pub fn send_request(&mut self, id: u64, body: ExtensionRequest) -> Result<()>;
    pub fn send_response(&mut self, id: u64, result: Result<ExtensionResponse, String>) -> Result<()>;
    pub fn send_notification(&mut self, notification: ExtensionNotification) -> Result<()>;
    pub fn send_handshake(&mut self, handshake: ExtensionHandshake) -> Result<()>;
    pub fn recv_message(&mut self) -> Result<ExtensionMessage>;
}

Typical plugin main loop

use kael::{
    ExtensionHandshake, ExtensionMessage, ExtensionRequest, ExtensionResponse,
    ExtensionTransport, EXTENSION_RPC_VERSION, UnixDomainSocketTransport,
};

fn main() {
    let socket = std::env::var("KAEL_EXTENSION_SOCKET").unwrap();
    let raw = UnixDomainSocketTransport::connect(&socket).unwrap();
    let mut transport = ExtensionTransport::new(Box::new(raw));

    // Complete handshake
    if let Ok(ExtensionMessage::Handshake(ExtensionHandshake::Host { .. })) =
        transport.recv_message()
    {
        transport
            .send_handshake(ExtensionHandshake::Extension {
                version: EXTENSION_RPC_VERSION,
                accepted: true,
            })
            .unwrap();
    }

    // Process requests
    loop {
        match transport.recv_message() {
            Ok(ExtensionMessage::Rpc(kael::process_model::IpcMessage::Request { id, body })) => {
                match body {
                    ExtensionRequest::Shutdown => {
                        transport.send_response(id, Ok(ExtensionResponse::Ack)).unwrap();
                        break;
                    }
                    ExtensionRequest::GetContributions => {
                        let contributions = build_contributions();
                        transport
                            .send_response(id, Ok(ExtensionResponse::Contributions(contributions)))
                            .unwrap();
                    }
                    _ => {
                        transport.send_response(id, Ok(ExtensionResponse::Ack)).unwrap();
                    }
                }
            }
            Err(_) => break,
            _ => {}
        }
    }
}

Process model

ProcessId and ProcessInfo

Every supervised child process is identified by a ProcessId(u64) and described by a ProcessInfo:
pub struct ProcessInfo {
    pub id: ProcessId,
    pub class: ProcessClass,
    pub name: String,
    pub executable: PathBuf,
    pub args: Vec<String>,
    pub env: HashMap<String, String>,
    pub working_dir: Option<PathBuf>,
}
ProcessClass describes the role of the process:
pub enum ProcessClass {
    Ui,        // main application process
    Worker,    // background CPU/IO worker
    Media,     // media/capture pipeline
    Extension, // plugin host process
}
Use the convenience constructors to build a ProcessInfo:
let info = ProcessInfo::extension(ProcessId(1), "com.example.my-plugin")
    .executable("/app/extensions/my-plugin/my-plugin")
    .env("KAEL_EXTENSION_ID", "com.example.my-plugin")
    .env("KAEL_API_VERSION", "1.0.0");

RestartPolicy

RestartPolicy tells the ProcessSupervisor what to do when a child exits:
pub enum RestartPolicy {
    Never,
    OnFailure {
        max_restarts: u32,
        backoff: Duration,
    },
    Always {
        backoff: Duration,
    },
}
The process is not restarted after it exits or crashes. Use this for plugins that should only run once.
let opts = ProcessSpawnOptions {
    restart_policy: RestartPolicy::Never,
    ..Default::default()
};

ProcessSupervisor

ProcessSupervisor is the runtime engine that spawns, monitors, and restarts child processes. ExtensionHostRuntime holds one internally. When you call ExtensionHostRuntime::new, a supervisor is created automatically:
pub struct ExtensionHostRuntime {
    host: ExtensionHost,
    supervisor: ProcessSupervisor,
    // ...
}

impl ExtensionHostRuntime {
    pub fn new(extensions_dir: impl AsRef<Path>, app_id: impl Into<String>) -> Self {
        Self {
            host: ExtensionHost::new(),
            supervisor: ProcessSupervisor::new(),
            // ...
        }
    }
}

Security model

The Capability enum

Capabilities are explicit grants for dangerous actions. The default for every process is deny:
pub enum Capability {
    OpenExternalUrl,
    FilesystemRead  { scope: PathScope },
    FilesystemWrite { scope: PathScope },
    ShellExecute,
    ClipboardRead,
    ClipboardWrite,
    Notification,
    Network { hosts: Vec<String> },
    Microphone,
    Camera,
    ScreenCapture,
}
High-risk capabilities (those where capability.is_high_risk() returns true) are: ShellExecute, ClipboardRead, Network, Camera, ScreenCapture, FilesystemRead { scope: Any }, and FilesystemWrite { scope: Any }.

PermissionBroker

PermissionBroker is the central authority for capability decisions. You configure it at application startup and pass it to the host runtime:
pub struct PermissionBroker {
    grants: HashMap<ProcessId, HashSet<Capability>>,
    process_classes: HashMap<ProcessId, ProcessClass>,
    class_defaults: HashMap<ProcessClass, HashSet<Capability>>,
    prompt_handler: Option<PromptHandler>,
}

Configuring the broker

1

Create the broker

use kael::security::PermissionBroker;

let mut broker = PermissionBroker::new();
2

Register processes and set class defaults

use kael::process_model::{ProcessClass, ProcessId};
use kael::security::Capability;

broker.register_process(ProcessId(1), ProcessClass::Extension);

broker.set_default_capabilities(
    ProcessClass::Extension,
    [Capability::Notification, Capability::ClipboardWrite],
);
3

Grant per-process capabilities

broker.grant(ProcessId(1), Capability::Network {
    hosts: vec!["api.example.com".to_string()],
});
4

Check capabilities at runtime

use kael::security::PermissionResult;

match broker.check(ProcessId(1), &Capability::Notification) {
    PermissionResult::Granted => { /* proceed */ }
    PermissionResult::Denied  => { /* block */ }
    PermissionResult::Prompt  => { /* show UI prompt */ }
}
5

Install a prompt handler (optional)

A prompt handler intercepts capabilities that are not pre-granted and lets you show a user-facing dialog:
broker.set_prompt_handler(|process_id, capability| {
    // show a dialog, return the user's choice
    PermissionResult::Granted
});

Checking multiple capabilities

// Returns Granted if any of the listed capabilities is granted
let result = broker.check_any(ProcessId(1), &[
    Capability::ClipboardRead,
    Capability::ClipboardWrite,
]);

// Inspect all capabilities held by a process
let caps: Vec<Capability> = broker.capabilities(ProcessId(1));

Revoking access

// Revoke a single capability
broker.revoke(ProcessId(1), &Capability::Notification);

// Revoke everything for a process
broker.revoke_all(ProcessId(1));

// Clean up when the process exits
broker.unregister_process(ProcessId(1));
Always call unregister_process when a child process exits. Leaving stale entries in the broker can grant capabilities to a recycled ProcessId if the same numeric identifier is reused.

How the layers connect

Plugin process                    Host process
──────────────                    ────────────
main()
  connect(KAEL_EXTENSION_SOCKET) ──── UnixDomainSocketTransport::listen()
  ExtensionTransport::new(...)         ExtensionTransport::new(...)
  recv_message()  ◄──── Handshake::Host ────  send_handshake()
  send_handshake() ──── Handshake::Extension ──►
  loop:
    recv_message()  ◄──── Request { Activate } ──  send_request()
    send_response() ──── Response { Ack }       ──►
                                                   broker.check(pid, &cap)
The host’s PermissionBroker is consulted before the host acts on any response that exercises a guarded resource. The plugin itself never touches the broker; it simply declares capabilities in its manifest and the host enforces them.

Build docs developers (and LLMs) love