# How tarpc RPC Protocol Works in OpenLogi: Wire-Format Rules and Versioning Explained

> Explore how OpenLogi implements the tarpc RPC protocol. Understand its wire-format rules, bincode serialization, and versioning for seamless client agent communication.

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

---

**OpenLogi uses the tarpc RPC framework with bincode serialization over length-delimited local sockets, enforcing strict append-only method ordering and a hardcoded `PROTOCOL_VERSION` constant to guarantee wire-format compatibility between GUI clients and the background agent.**

The **tarpc RPC protocol** in OpenLogi powers all communication between the graphical interface and the system agent. This architecture is implemented in the `openlogi-ipc` crate, where a single `#[tarpc::service]` trait defines the complete contract. Understanding this protocol requires examining how method ordering, binary encoding, and versioning interact to produce a stable, evolvable interface.

## Service Definition and Method Ordering Rules

The RPC contract centers on the `Agent` trait declared in [[`ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/ipc.rs). This trait uses tarpc's procedural macro to generate client stubs, server handlers, and the wire-format enums:

```rust
#[tarpc::service]
pub trait Agent {
    async fn protocol_version() -> u32;
    async fn status() -> AgentStatus;
    async fn inventory() -> Vec<DeviceInventory>;
    // … additional methods …
    async fn observe(since: u64) -> Observation;
}

```

**Method order is immutable** in this design. Tarpc generates a Rust enum where each method becomes a variant, and **bincode encodes the variant index (discriminant) directly into the wire format**. The first method, `protocol_version`, occupies index 0. Every new RPC must be **appended at the end** of the trait. Inserting a method in the middle would shift subsequent discriminants, breaking compatibility with existing clients. This constraint is documented in the source comments at lines 41–45 of [`ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs).

## Wire-Format Encoding and Framing

The tarpc RPC protocol in OpenLogi relies on **bincode** for binary serialization with specific framing rules:

| Aspect | Implementation |
|--------|----------------|
| **Serialization** | `bincode` — compact, binary, index-based enum encoding |
| **Framing** | Length-delimited via `tarpc::serde_transport::Transport` |
| **Transport** | OS-local sockets (Unix domain sockets on Unix, named pipes on Windows) |
| **Message structure** | `[length prefix][variant index][payload fields]` |

The transport layer in [[`transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/transport.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/transport.rs) wraps the raw socket:

```rust
pub fn wrap<Item, SinkItem>(stream: Stream) -> Transport<Stream, Item, SinkItem, Bincode<Item, SinkItem>>
where
    Item: DeserializeOwned,
    SinkItem: Serialize,
{
    Transport::from((stream, Bincode::default()))
}

```

This `wrap` function constructs a `Transport` that automatically handles length-prefix framing and bincode serialization for both directions. The `interprocess` crate provides the cross-platform local socket implementation.

### Enum Encoding Constraints

All types crossing the RPC boundary—`AgentRequest`, `AgentResponse`, `InventoryHealth`, `PairingPhase`, and others—follow **append-only enum semantics**. Bincode serializes enums as a variant index followed by the variant's fields. Consequently:

- **Never reorder existing variants**
- **Never remove existing variants**
- **Only add new variants at the end**

Violating these rules changes the binary representation and breaks wire compatibility.

## Protocol Versioning and Compatibility

Version management in OpenLogi's tarpc implementation uses a **hardcoded constant with golden-file testing**:

```rust
// In ipc.rs, line 64
pub const PROTOCOL_VERSION: u32 = 29;

```

The `protocol_version` RPC (variant index 0) must return this value. On connection, the GUI client queries this endpoint; a mismatch aborts the handshake immediately.

### Version Bump Policy

A protocol version increment is mandatory when:

- **Adding, removing, or reordering methods** in the `Agent` trait
- **Changing the shape of any bincode-serialized type** (adding fields, new enum variants)
- **Any modification that alters the binary representation** of existing requests or responses

The enforcement mechanism resides in [[`wire_format.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/wire_format.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/tests/wire_format.rs). These **golden-wire-format tests** assert that each request and response serializes to an exact, stored hex string:

```rust
assert_wire(&AgentRequest::ProtocolVersion {}, "00");

```

Changing this test output without bumping `PROTOCOL_VERSION` constitutes a protocol violation. The test at line 104 explicitly validates version negotiation.

## Concrete Wire Format Example

A request to `protocol_version` produces the following bytes on the wire:

```

01          // length prefix: 1 byte payload follows
00          // variant index 0: ProtocolVersion

```

A `DeclareClient` request (variant 0x19 / 25 decimal), which identifies the client type during handshake, encodes accordingly with its discriminant fixed in the wire format.

### Complete Client Connection Flow

```rust
use openlogi_ipc::{client, transport};
use tarpc::context;

// Establish local socket connection
let stream = transport::connect().await?;
let transport = transport::wrap(stream);

// Instantiate tarpc client with current context
let client = client::new(transport, context::current());

// Execute RPC: returns u32 = 29
let version = client.protocol_version().await?;
assert_eq!(version, openlogi_ipc::PROTOCOL_VERSION);

```

The `DeclareClient` request (index 0x19) precedes other operations, informing the agent whether the peer is a GUI, CLI, or Overlay client.

## Key Source Files

| File | Role in tarpc RPC Protocol |
|------|---------------------------|
| [[`ipc.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/ipc.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/ipc.rs) | `Agent` trait definition, `PROTOCOL_VERSION` constant, all RPC data structures |
| [[`transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/transport.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/transport.rs) | Local socket establishment, `Transport` wrapping for length-delimited + bincode framing |
| [[`wire_format.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/wire_format.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/tests/wire_format.rs) | Golden-file tests locking binary encoding; version bump enforcement |
| [[`client.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/client.rs)](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/client.rs) | Client-side convenience wrapper around generated `AgentClient` |
| [`server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/server.rs) in `openlogi-agent` | Server-side `Agent` implementation binding |

## Summary

- **tarpc generates RPC enums from the `Agent` trait**, with bincode encoding variant indices directly to the wire
- **Method order is append-only**; new RPCs extend the trait end, preserving discriminant stability
- **Length-delimited framing** via `tarpc::serde_transport::Transport` over `interprocess` local sockets
- **`PROTOCOL_VERSION` (currently 29)** guards compatibility; mismatches abort connection
- **Golden-wire-format tests** in [`wire_format.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/wire_format.rs) enforce binary stability, requiring version bumps on any format change

## Frequently Asked Questions

### What happens if I reorder methods in the `Agent` trait?

Reordering methods shifts bincode variant indices, corrupting the wire format. Existing clients would send requests that the server misinterprets, or vice versa. The golden tests in [`wire_format.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/wire_format.rs) would fail, signaling a required `PROTOCOL_VERSION` increment.

### How does OpenLogi detect incompatible protocol versions?

The GUI calls `Agent::protocol_version()` immediately after connection and compares the returned `u32` against its compiled `PROTOCOL_VERSION` constant. A mismatch triggers an immediate disconnect, as verified in [`wire_format.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/wire_format.rs) line 104.

### Why choose bincode over JSON or Protocol Buffers for tarpc?

Bincode provides **zero-overhead, compact binary encoding** with deterministic variant indices—critical for the enum-based method dispatch that tarpc generates. The tradeoff is strict schema evolution rules (append-only) that OpenLogi enforces through version constants and golden tests.

### Can the RPC protocol work over network sockets instead of local sockets?

The current implementation in [`transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/transport.rs) uses `interprocess` for local sockets, but the `tarpc::serde_transport::Transport` abstraction supports any `AsyncRead + AsyncWrite` stream. Replacing the connection establishment would enable network transport while preserving the identical wire format and versioning semantics.