# IPC wire protocol — v1.0 (frozen at v1.0.0)
> **Status**: frozen at ezpn v1.0.0. SemVer applies. Breaking changes
> require a `proto_major` bump (and a major version bump of ezpn itself).
> Additive changes (new tags in the reserved range, new optional fields,
> new capability strings) bump `proto_minor`.
This document is the authoritative reference for the binary protocol that
the `ezpn` client speaks to the `ezpn` daemon over a UNIX domain socket.
It is the source-of-truth for the schema-guard CI job (issue #97). Every
field marked **frozen** below cannot be removed or have its type changed
without a major bump.
The implementation lives in [`src/protocol.rs`](../../src/protocol.rs).
## 1. Transport
* **Socket type**: `AF_UNIX` `SOCK_STREAM`.
* **Path**: `${XDG_RUNTIME_DIR:-/tmp}/ezpn-<session>.sock`. The daemon
binds with `umask 0o077` and re-asserts permissions to `0o600` after
bind (#65). Cross-UID `connect()` is rejected at accept time.
* **Connection lifecycle**: one client, one socket. The client opens a
fresh socket for each `attach`/`ls`/`kill`/`ezpn-ctl` invocation.
## 2. Frame format (frozen)
Every message on the socket is a single frame:
```
+--------+---------------------+----------------+
1B 4B 0..=2^24-1 B
```
* `tag`: identifies the message kind (see §3).
* `length`: payload length in bytes, big-endian, unsigned 32-bit.
* `payload`: opaque bytes whose meaning depends on `tag`.
**Hard limit**: `MAX_PAYLOAD = 16 MiB` (16777216 bytes). Frames larger
than this are rejected with `io::ErrorKind::InvalidData`. This cap is
**frozen**.
Empty payloads (`length == 0`) are valid and carry signal-only meaning
(e.g. `C_DETACH`, `C_KILL`).
## 3. Tag space (frozen)
Tags are 1 byte. The space is partitioned:
| `0x01..=0x0F` | Client→Server normal-traffic tags (`C_*`) |
| `0x10..=0x1F` | Version negotiation / handshake (both sides) |
| `0x20..=0x7F` | Reserved for additive `C_*` tags (proto_minor) |
| `0x80..=0x8F` | Server→Client normal-traffic tags (`S_*`) |
| `0x90..=0xFE` | Reserved for additive `S_*` tags (proto_minor) |
| `0xFF` | Reserved (sentinel; never emit) |
### 3.1 Assigned client tags (frozen)
| `0x01` | `C_EVENT` | crossterm event as JSON | Keystrokes, mouse events. |
| `0x02` | `C_DETACH` | empty | Client requests detach. |
| `0x03` | `C_RESIZE` | `[u16 cols BE][u16 rows BE]` (4 B) | Terminal resize. |
| `0x04` | `C_KILL` | empty | Sent by `ezpn kill`. |
| `0x05` | `C_PING` | empty | Liveness probe by `ezpn ls`. No side effects. |
| `0x06` | `C_ATTACH` | JSON `AttachRequest` (see §4.1) | Replaces legacy v0.5 first-frame attach. |
| `0x07`–`0x0F` | _reserved_ | — | Future `C_*` tags. |
### 3.2 Assigned server tags (frozen)
| `0x81` | `S_OUTPUT` | raw rendered terminal bytes | Frame bytes for the client to write to its TTY. |
| `0x82` | `S_DETACHED` | empty | Server acknowledges detach. |
| `0x83` | `S_EXIT` | empty | Server is shutting down. |
| `0x84` | `S_PONG` | empty | Pong response to `C_PING`. |
| `0x85`–`0x8F` | _reserved_ | — | Future `S_*` tags. |
### 3.3 Negotiation tags (`0x10..=0x1F`, frozen)
| `0x10` | `S_VERSION` | JSON `ServerHello` (§4.2) | S→C | First frame on every connection. |
| `0x11` | `C_HELLO` | JSON `ClientHello` (§4.3) | C→S | Reply to `S_VERSION`. |
| `0x12` | `S_INCOMPAT` | JSON `IncompatNotice` (§4.4) | S→C | Sent on major mismatch or legacy first byte. Server closes immediately after. |
| `0x13`–`0x1F` | _reserved_ | — | both | Future negotiation extensions (auth, capability re-negotiation). |
## 4. Payload schemas (frozen)
All JSON payloads are UTF-8. Unknown additive fields **MUST** be
tolerated by both sides — adding a new optional field is a `proto_minor`
bump, not a major bump. Removing or retyping a field below requires a
major bump.
### 4.1 `AttachRequest` (`C_ATTACH` payload)
```json
{
"cols": 120,
"rows": 40,
```
| `cols` | u16 | yes | Initial terminal width. |
| `rows` | u16 | yes | Initial terminal height. |
| `mode` | enum string | no (default `steal`) | See `AttachMode`. |
`AttachMode` values: `steal` (default — detach existing client),
`shared` (multi-client), `readonly` (observe only, no input forwarded).
### 4.2 `ServerHello` (`S_VERSION` payload)
```json
{
"proto_major": 1,
"proto_minor": 0,
"build": "ezpn 1.0.0 (rev abc1234)"
}
```
| `proto_major` | u16 | yes | `1` for this spec. |
| `proto_minor` | u16 | yes | Bumped for every additive change post-1.0. |
| `build` | string | yes | Human-readable. Matches `^ezpn \d+\.\d+\.\d+ \(rev .+\)$` in canonical form. |
### 4.3 `ClientHello` (`C_HELLO` payload)
```json
{
"proto_major": 1,
"proto_minor": 0,
"client_build": "ezpn 1.0.0 (rev abc1234)",
"supported_features": ["scrollback-v3", "kitty-kbd-stack", "osc-52-confirm"]
}
```
| `proto_major` | u16 | yes | Client's max supported major. |
| `proto_minor` | u16 | yes | Client's max supported minor. |
| `client_build` | string | yes | Same canonical form as `ServerHello.build`. |
| `supported_features` | string[] | yes | See §5. May be empty. |
### 4.4 `IncompatNotice` (`S_INCOMPAT` payload)
```json
{
"server_proto": "1.0",
"client_proto": "2.0" | "unknown",
"message": "client v2.0 cannot attach to server v1.0 — restart the daemon with 'ezpn kill <name>' to upgrade."
}
```
| `server_proto` | string | yes | `"<major>.<minor>"` of the daemon. |
| `client_proto` | string | yes | `"<major>.<minor>"` of the client, or `"unknown"` for legacy v0.5 clients that never sent `C_HELLO`. |
| `message` | string | yes | Human-readable, suitable for direct display. |
## 5. Capability strings (frozen)
Capability strings are stable identifiers a client advertises in
`ClientHello.supported_features`. The server gates optional output
formats on these flags. **Adding a new capability is additive** and
does NOT require a `proto_minor` bump unless the new flag is mandatory
(in which case it MUST land in a `proto_minor` bump and the server MUST
fall back gracefully when the flag is absent until the bump).
| `scrollback-v3` | Client understands the v3 scrollback snapshot envelope (#69, #70). Daemons may stream extended snapshot frames only when this is present. |
| `kitty-kbd-stack` | Client understands the Kitty keyboard protocol stack semantics (#74). Daemons may forward unmodified `CSI u` sequences. |
| `osc-52-confirm` | Client understands the multiplexer-side OSC 52 confirm prompt (#79). Daemons may park sequences awaiting user decision. |
The current binary advertises all three.
## 6. Handshake state machine (frozen)
```
client server
| | S_VERSION (0x10)
|◀──────────────────|
| C_HELLO (0x11) |
|──────────────────▶|
| |
| normal traffic |
|◀═════════════════▶|
```
### 6.1 Mismatch handling
* **Major mismatch (`server.proto_major != client.proto_major`)**: the
side that detects the mismatch first emits `S_INCOMPAT` with a
user-readable message and closes the connection. The client MUST NOT
send `C_HELLO` if it sees a major mismatch in `S_VERSION`.
* **Minor mismatch (`server.proto_minor != client.proto_minor`,
same major)**: tolerated, silently. Both sides operate at
`min(server.proto_minor, client.proto_minor)` semantics.
* **Legacy client (first byte is `{` or `[`, no `C_HELLO`)**: the
daemon emits `S_INCOMPAT` with `client_proto = "unknown"` and closes.
This catches v0.5.x clients that dumped `AttachRequest` JSON without
a tag byte.
* **Unknown first byte (not in tag space, not legacy)**: protocol
violation. Connection is closed without `S_INCOMPAT`.
The client-side handshake helper is
[`protocol::client_handshake`](../../src/protocol.rs); the server-side
heuristic is [`protocol::classify_first_byte`](../../src/protocol.rs).
## 7. Versioning rules (frozen)
| Add a new tag in the reserved range (`0x07..=0x0F`, etc.) | `proto_minor` | yes |
| Add a new optional field to an existing JSON payload | `proto_minor` | yes |
| Add a new capability string | none | yes (purely additive) |
| Promote a capability from optional to mandatory | `proto_minor` | yes (with grace window)|
| Change the wire encoding of an existing field | `proto_major` | NO until v2.0 |
| Remove an existing field | `proto_major` | NO until v2.0 |
| Repurpose an assigned tag | `proto_major` | NO until v2.0 |
| Change the frame format | `proto_major` | NO until v2.0 |
| Change `MAX_PAYLOAD` | `proto_major` | NO until v2.0 |
## 8. Compatibility matrix
ezpn supports clients **N-1.x ≤ client ≤ N.x** for daemon version `N.x`.
Older clients are rejected with `S_INCOMPAT`. The matrix:
| 1.0.x | 1.0 |
| 1.1.x | 1.0, 1.1 |
| 1.2.x | 1.1, 1.2 |
| 2.0.x | 1.x (until 2.1), 2.0 |
| 2.1.x | 2.0, 2.1 |
A daemon that accepts an older minor MUST gracefully avoid emitting
features the client did not advertise (e.g. omit `scrollback-v3`
extended frames if the client did not list that capability).
## 9. Schema-guard CI
A schema-guard job runs on every PR. It diffs the structures listed in
this document (frozen surface) against the previous tag and fails unless
the commit message contains one of:
* `[proto-additive]` — for additive changes (new tag, new optional
field, new capability). The change MUST also touch `proto_minor`.
* `[proto-break]` — for breaking changes. The change MUST also touch
`proto_major` AND the ezpn major version.
The guard inspects:
1. Tag-byte assignments in [`src/protocol.rs`](../../src/protocol.rs)
(`pub const C_*` / `pub const S_*`).
2. Public field lists on `AttachRequest`, `ServerHello`, `ClientHello`,
`IncompatNotice`.
3. The set of strings in `CLIENT_FEATURES`.
## 10. Surface NOT frozen by this document
The following surfaces ship their own freeze documents and SHOULD NOT
be conflated with the wire protocol:
* Internal action vocabulary (command palette / keymap) — see
`docs/actions/v1.md` (TBD, tracked by #84).
* `ezpn-ctl ls --json` schema — see [`docs/scripting.md`](../scripting.md)
for the v1 freeze.
* `.ezpn.toml` and `config.toml` schema — see
[`docs/configuration.md`](../configuration.md) for the v1 freeze.
* Snapshot file format — see issue #70 for the freeze plan.
* Event bus JSON vocabulary — defined as v1 in
[`src/events.rs`](../../src/events.rs) and documented in
[`docs/scripting.md`](../scripting.md).
## 11. Reference test vectors
```
S_VERSION frame (proto 1.0):
10 00 00 00 4b
7b 22 70 72 6f 74 6f 5f 6d 61 6a 6f 72 22 3a 31
2c 22 70 72 6f 74 6f 5f 6d 69 6e 6f 72 22 3a 30
2c 22 62 75 69 6c 64 22 3a 22 65 7a 70 6e 20 31
2e 30 2e 30 20 28 72 65 76 20 75 6e 6b 6e 6f 77
6e 29 22 7d
```
The test suite in `src/protocol.rs` (`mod tests`) is the executable
counterpart of this document. Anything diverging between this file and
the tests is a bug in the document.