# OpenLogi IPC Contract: Tarpc Service Definition for GUI-Agent Communication

> Explore the OpenLogi IPC contract, a Tarpc service definition enabling GUI-agent communication. Learn how it handles state queries, device pairing, and update polling via Unix-domain sockets.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: api-reference
- Published: 2026-09-08

---

**OpenLogi uses a Tarpc service contract defined in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs) that exposes versioned RPC methods over a Unix-domain socket, enabling the GUI client to query agent state, manage device pairing, and long-poll for updates using bincode serialization.**

The AprilNEA/OpenLogi repository implements a local-first device management system where a background agent process handles hardware communication while a separate GUI process provides the user interface. The **IPC contract used between OpenLogi processes** is defined as a Rust trait annotated with `#[tarpc::service]` in the `openlogi-ipc` crate, establishing a strict binary protocol over Unix sockets with explicit versioning and append-only data structures.

## Tarpc Service Contract Architecture

The inter-process communication protocol centers on the `Agent` trait located in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs). This trait defines every request the GUI client can make to the agent server and strictly governs the binary wire format.

Each method in the trait is assigned an index based on its declaration order. The `protocol_version()` method must remain the first method in the trait definition because Tarpc encodes method order into the binary protocol. Reordering methods would break backward compatibility between the GUI and agent binaries.

The contract declares a constant that governs compatibility:

```rust
const PROTOCOL_VERSION: u32 = 29;  // Line 64 in ipc.rs

```

Both processes exchange this version during the initial handshake to ensure they speak the same protocol revision.

## Core IPC Methods and State Management

The service contract follows a state-driven design using append-only data structures to guarantee backward compatibility when adding new device states or events.

### Version Handshake and Client Declaration

Before performing operations, the client must declare its kind and verify protocol alignment:

- **`protocol_version()`**: Returns the `PROTOCOL_VERSION` constant (29). This method must remain the first method in the trait to ensure it receives index 0 in the Tarpc dispatch table.
- **`declare_client(kind: ClientKind)`**: Identifies the connection as a GUI, CLI, or overlay helper. This influences the agent's dormancy behavior and resource management policies.

### State Observation and Long-Polling

Because the protocol lacks server-push semantics, the contract implements long-polling for reactive updates:

- **`observe(since: Generation) -> Observation`**: Blocks until the observable state changes or a timeout occurs, returning the entire state snapshot along with a new generation counter.
- **`observe_action_ring(...)`**: Similar long-poll mechanism for action-specific event rings.

The client passes the last seen generation; the server holds the request until the generation increments, enabling efficient state synchronization without persistent server-side connections.

### Device Management and Pairing

The contract exposes hardware control methods:

- **`status()`**, **`inventory()`**, **`snapshot()`**: Query the agent's health, enumerate connected devices, or retrieve a combined status/inventory view.
- **Pairing RPCs**: `start_pairing(selector: ReceiverSelector)`, `pair_device(...)`, `cancel_pairing()`, and `next_pairing()` manage the device pairing flow. The `next_pairing()` method returns streaming updates during the pairing process.

All state enums (e.g., `AgentStatus`, `PairingPhase`, `MonitorEvent`) use append-only variants, ensuring new states do not break older client binaries.

## Wire Format and Transport Layer

Tarpc serializes RPC calls using **bincode** over the `interprocess` crate's Unix-domain socket transport. This produces a compact binary representation suitable for local communication without network overhead.

The transport layer guarantees message framing and ordering, but does not provide server-initiated broadcasts. All "push-like" behavior derives from the long-polling `observe` methods, where the client initiates requests that the server deliberately delays until state changes occur.

## Implementing the IPC Client in Rust

To interact with the agent process, instantiate the Tarpc client and invoke the contract methods:

```rust
use openlogi_ipc::AgentClient;          // Generated by tarpc
use tarpc::client::Config;
use std::net::UnixStream;

async fn check_version() -> anyhow::Result<()> {
    // Connect to the agent's Unix socket (path defined by the app)
    let transport = UnixStream::connect("/tmp/openlogi.sock")?;
    let client = AgentClient::new(Config::default(), transport).await?;
    let version = client.protocol_version().await?;
    println!("Agent protocol version: {}", version);
    Ok(())
}

```

For reactive state monitoring, implement the long-poll loop:

```rust
use openlogi_ipc::{AgentClient, Observation, Generation};

async fn watch_state() -> anyhow::Result<()> {
    let client = /* create client as above */;
    let mut gen: Generation = 0; // Start with "nothing seen"
    loop {
        let obs: Observation = client.observe(gen).await?;
        println!("New state generation {}: {:#?}", obs.generation, obs.snapshot);
        gen = obs.generation; // Next call blocks until a later change
    }
}

```

To initiate device pairing:

```rust
use openlogi_core::hid::ReceiverSelector;
use openlogi_ipc::AgentClient;

async fn start_pairing_example() -> anyhow::Result<()> {
    let client = /* create client */;
    let selector = ReceiverSelector::Bolt; // or Unifying, etc.
    client.start_pairing(selector).await?;
    // Then poll for updates:
    while let Some(update) = client.next_pairing().await? {
        println!("Pairing update: {:#?}", update);
    }
    Ok(())
}

```

## Summary

- The **IPC contract** lives in [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs) as a `#[tarpc::service]` trait named `Agent`.
- **Protocol version 29** is hardcoded and exchanged via the `protocol_version()` method, which must remain the first method in the trait definition to preserve binary compatibility.
- Communication occurs over **Unix-domain sockets** using **bincode** serialization via the `interprocess` transport.
- State changes propagate through **long-polling** (`observe`, `observe_action_ring`) rather than server push, with the client providing a generation counter to synchronize state.
- The design uses **append-only enums** and strict method ordering to maintain backward compatibility across GUI and agent updates.

## Frequently Asked Questions

### What protocol version does OpenLogi IPC use?

The current protocol version is **29**, defined as the constant `PROTOCOL_VERSION: u32 = 29` on line 64 of [`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs). The GUI client must check this version immediately after connecting by calling `protocol_version()` to ensure binary compatibility with the running agent process.

### How does OpenLogi handle backward compatibility in IPC messages?

OpenLogi uses **append-only enums** for all state types, including `AgentStatus`, `PairingPhase`, and `MonitorEvent`. New variants are added to the end of the enum definition, allowing older clients to ignore unrecognized states without crashing. Additionally, the method order in the `Agent` trait is immutable because Tarpc encodes method indices into the wire format.

### Why does OpenLogi use long-polling instead of server push?

The Tarpc framework over Unix sockets does not provide native server-push semantics. OpenLogi implements reactive updates via the **`observe(since: Generation)`** method, where the server holds the RPC request open until the state generation counter exceeds the client's provided value. This creates an efficient, event-driven flow while maintaining a simple request-response protocol.

### Where is the IPC contract defined in the OpenLogi source code?

The complete service definition resides in **[`crates/openlogi-ipc/src/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-ipc/src/ipc.rs)**. This file contains the `Agent` trait annotated with `#[tarpc::service]`, the `PROTOCOL_VERSION` constant, and all data structures exchanged between the GUI client and agent server. The desktop GUI implements the client side in [`crates/openlogi-desktop/src/services/ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/services/ipc.rs).