OpenLogi IPC Types and Payload Constraints: A Complete Guide
OpenLogi uses bincode-serialized structs and append-only enums for all inter-process communication between its GUI client and agent, with strict versioning rules that prevent breaking changes.
The OpenLogi project's IPC protocol defines how the GUI, CLI, and overlay clients communicate with the system agent. Every payload crossing this boundary is defined in crates/openlogi-ipc/src/ipc.rs and serialized using bincode, a compact binary format that encodes enum variant indexes directly. This design choice imposes specific constraints that every contributor must understand when extending the protocol.
Core IPC Types in OpenLogi
The OpenLogi IPC layer organizes data into three categories: status reporting, pairing workflows, and action ring interactions. Each category uses dedicated structs and enums with documented stability guarantees.
Agent Status and Health Types
The AgentStatus struct serves as the primary heartbeat from agent to client. It combines system permissions, version information, and inventory health in a single DTO:
// Key fields in AgentStatus (from crates/openlogi-ipc/src/ipc.rs)
pub struct AgentStatus {
pub accessibility_granted: bool,
pub hook_installed: bool,
pub launch_at_login: bool,
pub inventory: InventoryHealth, // Enum: append-only constraint
pub protocol_version: u32, // Must match PROTOCOL_VERSION constant
pub agent_version: String,
pub input_monitoring_granted: bool,
pub hid_open_failures: u32,
}
The nested InventoryHealth enum demonstrates the append-only rule:
pub enum InventoryHealth {
Scanning, // index 0
Ready, // index 1
Unavailable, // index 2
// New variants MUST be added here, never reordered
}
Because bincode encodes Scanning as 0, Ready as 1, and Unavailable as 2, any reordering or removal would desynchronize the client and agent. This constraint is explicitly documented in the source: /// bincode encodes the variant *index*, so variants are append-only.
Snapshot and Observation Types
The AgentSnapshot struct aggregates complete state to prevent data mismatches:
pub struct AgentSnapshot {
pub status: AgentStatus,
pub inventory: Vec<DeviceInventory>,
pub standalone: Vec<StandaloneDevice>,
pub camera_active: bool,
pub pairing: Option<PairingPhase>, // None when not pairing
pub foreground: ForegroundApps,
}
The Observation struct wraps this snapshot with a generation counter for level-triggered hold semantics:
pub struct Observation {
pub generation: u64,
pub snapshot: AgentSnapshot,
}
The client passes back generation to implement OBSERVE_HOLD, allowing the agent to skip updates when no state change occurred.
Pairing Workflow Types
Pairing uses a state machine represented by PairingPhase and streamed updates via PairingUpdate:
| Type | Purpose | Key Variants |
|---|---|---|
PairingPhase |
Current pairing state | Searching, Found(Vec<FoundDevice>), Pairing, Passkey(PasskeyMethod), Paired { slot: u8 }, Failed(PairingFailure) |
PairingUpdate |
Streamed events to GUI | Searching, DeviceFound(FoundDevice), Passkey(PasskeyMethod), Paired { slot }, Failed(PairingFailure) |
PairingFailure |
Typed error details | Hid, ReceiverNotFound, Register, Timeout, Device, Cancelled, ReceiverBusy, WatcherUnavailable, AgentRestarted, ReceiverAccessUnavailable, AlreadyActive, UnknownDevice, NoActiveSession |
PairingCommandError |
Immediate command rejection | AlreadyActive, ReceiverBusy, WatcherUnavailable, UnknownDevice, NoActiveSession |
Both enums follow the append-only constraint. New failure reasons or pairing phases must be added at the end, preserving existing indexes.
Action Ring and Overlay Types
The overlay receives presentation data only, never executable actions:
pub struct ActionRingPresentation {
pub label: String,
pub literal: String,
pub icon: Icon, // Renderable icon identifier
}
pub struct ActionRingInvocation {
pub session_id: u64, // Correlates hover/activate/cancel commands
pub slots: BTreeMap<ActionRingSlot, ActionRingPresentation>,
pub language: String,
}
pub struct RingObservation {
pub generation: u64,
pub invocation: Option<ActionRingInvocation>, // None when ring hidden
}
The session_id field ensures that user interactions correlate to the correct invocation, preventing race conditions between ring updates and user input.
Event and Client Identification Types
The MonitorEvent enum carries live input events:
pub enum MonitorEvent {
Button { button: Button, pressed: bool },
Scroll { delta_x: i32, delta_y: i32 },
CaptureInterrupted, // Emitted when secure input blocks capture
}
The ClientKind enum identifies connecting clients after the version handshake:
pub enum ClientKind {
Gui, // index 0
Cli, // index 1
Overlay, // index 2
}
Critical Constraints on OpenLogi IPC Payloads
Append-Only Enums (Hard Constraint)
Every enum in ipc.rs carries the same stability requirement. The bincode serializer writes variant indexes as raw integers—no field names, no schema versioning. Reordering InventoryHealth::Ready to index 0 would cause a client expecting Scanning to deserialize it as Ready instead.
This pattern appears repeatedly in the source comments between lines 85-98 of ipc.rs, warning that variants are append-only.
Versioned Protocol Handshake
The constant PROTOCOL_VERSION (currently 29) guards connection establishment:
// From crates/openlogi-ipc/src/ipc.rs lines 64-71
pub const PROTOCOL_VERSION: u32 = 29;
Both client and agent verify this value immediately after connecting. A mismatch aborts the handshake rather than risk misinterpreted payloads.
The Agent tarpc trait also respects ordering constraints—new methods must append to the end, not insert before existing ones, as method dispatch relies on stable indexes.
Whole-State Transfer Design
Methods like snapshot(), observe(), and observe_action_ring() return complete state rather than deltas. This enables connection recovery without replay history: a reconnecting client simply requests current state and continues from there.
The trade-off is higher bandwidth for simpler client logic and stronger consistency guarantees.
Working with OpenLogi IPC in Code
Serializing a Pairing Update
use openlogi_ipc::ipc::{PairingUpdate, FoundDevice};
use bincode;
// Construct a device discovery event
let payload = PairingUpdate::DeviceFound(FoundDevice {
address: [0x00, 0x1A, 0x7D, 0x12, 0x34, 0x56],
name: "MX Master 3S".into(),
});
// Serialize to wire format
let bytes = bincode::serialize(&payload)
.expect("bincode serialization infallible for IPC types");
// Deserialize on receiving side
let received: PairingUpdate = bincode::deserialize(&bytes)
.expect("version mismatch caught at handshake");
assert!(matches!(received, PairingUpdate::DeviceFound(_)));
Requesting and Inspecting Agent State
use openlogi_ipc::client::Connection;
use openlogi_ipc::ipc::AgentClient;
async fn monitor_inventory(mut conn: Connection) -> Result<(), Box<dyn std::error::Error>> {
let client = AgentClient::new(conn);
// `snapshot()` returns complete AgentSnapshot
let snap = client.snapshot().await?;
match snap.status.inventory {
InventoryHealth::Ready => println!("Devices enumerated: {}", snap.inventory.len()),
InventoryHealth::Scanning => println!("Enumeration in progress..."),
InventoryHealth::Unavailable => eprintln!("Device access failed"),
}
// Check for active pairing session
if let Some(phase) = snap.pairing {
println!("Pairing active: {:?}", std::mem::discriminant(&phase));
}
Ok(())
}
Key Source Files
Understanding the OpenLogi IPC system requires familiarity with four primary files:
crates/openlogi-ipc/src/ipc.rs– All payload types, theAgentservice contract, and protocol version constantcrates/openlogi-ipc/src/client.rs–AgentClientimplementation and connection managementcrates/openlogi-ipc/src/transport.rs– Bincode framing and low-level I/Ocrates/openlogi-ipc/tests/wire_format.rs– Format stability tests across protocol versions
Summary
- Bincode serialization determines all IPC constraints—compact but index-sensitive
- Append-only enums prevent breaking changes; new variants extend, never reorder
- Protocol version constant (
29) enforces client-agent compatibility at handshake - Whole-state snapshots simplify recovery but require complete DTO design
- Typed error enums (
PairingFailure,PairingCommandError) replace string errors for structured handling
Frequently Asked Questions
What happens if I reorder enum variants in the OpenLogi IPC definitions?
Deserialization will silently produce incorrect values. If you swap InventoryHealth::Scanning (index 0) with Ready (index 1), a client receiving index 0 will interpret it as Ready instead of Scanning. The version handshake cannot catch this because both sides run the same protocol version. Always append new variants at the end.
How does OpenLogi handle client-agent version mismatches?
The PROTOCOL_VERSION constant (defined in ipc.rs lines 64-71) is exchanged immediately after connection. Either side aborts if the values differ. This prevents scenarios where new fields are unknown or enum variants misalign. Upgrade both client and agent together.
Why does OpenLogi use whole-state snapshots instead of deltas?
Complete snapshots in AgentSnapshot and Observation eliminate the need for change history and replay logic. A reconnecting client requests current state and resumes operation. This trades bandwidth for reliability, which matches OpenLogi's use case where state size remains modest but connection stability varies.
Can I add new fields to existing IPC structs?
Yes, with caution. Bincode encodes structs field-by-field in declaration order. Adding a field at the end preserves compatibility if the receiving side ignores trailing data (older clients) or uses default values (new clients reading old data). The safer pattern is creating new method variants in the Agent trait, appended after existing ones.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →