ezpn 0.13.1

Dead simple terminal pane splitting — ezpn 2 3 gives you a 2x3 grid of shells
# 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:

```
+--------+---------------------+----------------+
| u8 tag | u32 length (BE)     | payload bytes  |
+--------+---------------------+----------------+
   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:

| Range          | Reserved for                                   |
|----------------|------------------------------------------------|
| `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)

| Tag       | Name        | Payload                              | Notes |
|-----------|-------------|--------------------------------------|-------|
| `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)

| Tag       | Name         | Payload                          | Notes |
|-----------|--------------|----------------------------------|-------|
| `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)

| Tag    | Name          | Payload                       | Direction | Notes |
|--------|---------------|-------------------------------|-----------|-------|
| `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,
  "mode": "steal" | "shared" | "readonly"
}
```

| Field   | Type        | Required | Notes |
|---------|-------------|----------|-------|
| `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)"
}
```

| Field         | Type   | Required | Notes |
|---------------|--------|----------|-------|
| `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"]
}
```

| Field                | Type      | Required | Notes |
|----------------------|-----------|----------|-------|
| `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."
}
```

| Field          | Type   | Required | Notes |
|----------------|--------|----------|-------|
| `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).

| Capability         | Meaning |
|--------------------|---------|
| `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
  |  connect()        |
  | ─────────────────▶|
  |                   |  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)

| Change                                                       | Bump          | Allowed in v1.x?       |
|--------------------------------------------------------------|---------------|------------------------|
| 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:

| Daemon | Accepts client (proto_major.proto_minor) |
|--------|------------------------------------------|
| 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.