> ## Documentation Index
> Fetch the complete documentation index at: https://vibex.peatboy.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Remote v2 and Relay

> Protocol boundaries to follow when you implement pairing, authentication, sync, and self-hosted encrypted transport.

Remote v2 is the typed protocol between the authoritative runtime and clients. It keeps business authorization on the authority and treats the transport route (Direct, Tailnet, Relay, LAN) as replaceable infrastructure.

The complete wire protocol definition lives in the repository at `docs/remote/protocol-v2.md`, and `crates/core/src/remote_v2.rs` is the single source of truth at the wire level. This page covers only the boundaries you must respect in an implementation.

## Two pairing paths

Vibex has two pairing paths with different forms. Do not mix them:

| Path                          | Link form                                          | Validity                                 | Generated by                  |
| ----------------------------- | -------------------------------------------------- | ---------------------------------------- | ----------------------------- |
| Desktop SAS offer             | `vibex://open/<transport>#/pair/<base64url-offer>` | 60–120 seconds                           | The desktop (pairing a phone) |
| Headless runtime pairing code | `vibex://pair#/code/<base64url-payload>`           | 5 minutes by default, 30 minutes maximum | `vibex-server`                |

`<transport>` is one of `direct`, `tailnet`, or `self_hosted_relay`.

### The offer path

* The desktop identity is generated in the runtime home and stored with `0600` permissions on Unix.
* An offer is cancelable and single-use, and consuming it and creating the device grant happen in the **same SQLite transaction**.
* An offer contains route candidates, the desktop public key, a permission summary, an expiry, and a one-time challenge. It **does not contain** a long-lived grant, keys, or workspace data.
* Mobile parses the link locally and strips the fragment before processing it asynchronously.

### The pairing-code path

* Nine digits grouped as `NNN-NNN-NNN`, derived from a UUIDv4, with **only its SHA-256 hash persisted**.
* The client claims the code at `POST /api/v2/pairing/code/claim`. The code **appears only in the request body, never in the URL**, so it does not reach proxies or access logs.
* The endpoint shares per-peer rate limits and authentication-failure budgets with the other unauthenticated routes.
* A claim is single-use: reuse, a malformed code, or an expired code returns `remote_pairing_code_invalid` / `remote_pairing_code_expired` and writes a `pairing_code_rejected` audit record.
* A successful claim returns **exactly one** device grant, with the permission level fixed when the pairing code was created.

### Connection strings and certificate pinning

The payload of `vibex://pair#/code/<payload>` is a JSON object:

```json theme={null}
{
  "schemaVersion": "vibex-pairing-code.v1",
  "serverUrl": "https://192.168.1.10:8765",
  "pairingCode": "123-456-789",
  "tlsCertificateDer": "<base64 DER>"
}
```

`tlsCertificateDer` is optional. **The certificate travels out of band with the connection string** (from the server console, not over the network), and the client pins it before issuing the first TLS request, so a self-signed LAN setup needs no public CA. The fingerprint format is `sha256:<base64url(sha256(DER))>`.

A pinned link must use `https` and point to a numeric LAN address (loopback, RFC1918, or link-local / unique-local IPv6); otherwise it returns `remote_pairing_link_invalid`. This restriction is deliberate: pairing must not become a way to bypass the public CA system.

The server identity key and the TLS certificate are pinned **separately**; the former comes from `serverIdentityPublicKey` in `/api/v2/info`.

## Handshake and keys

The implementation uses mature primitives such as X25519, HKDF-SHA256, HMAC-SHA256, and ChaCha20-Poly1305.

* A connection starts with `control/hello` and completes the handshake on `control/server_info`; the protocol uses range negotiation and currently selects `2.0`.
* A WebSocket ticket is valid for 30 seconds and single-use, exchanged through a controlled subprotocol and **not placed in a URL**.
* The hello proof binds the ticket challenge, the full hello transcript, the server identity, the session epoch, the device identity, and the client ephemeral key.
* `server_info` returns the server ephemeral key and the session-key confirmation.

## Frame types

| Type                                | Purpose                                                                                                                                                   |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| JSON `control`                      | hello, ping/pong, subscribe, attach/detach, resync, close.                                                                                                |
| JSON `rpc_request` / `rpc_response` | Carry a request and a correlation id. An RPC timeout ends only that request and does not close the socket; each connection has a bounded in-flight limit. |
| JSON `event`                        | Carries the domain generation and a monotonic sequence number.                                                                                            |
| Binary frames                       | Start with `VBX2`, followed by a big-endian JSON header length, a typed header, and the raw payload. Terminal bytes are **never** converted to UTF-8.     |

Unknown client, control, JSON message, attachment, binary frame, timeout, close, and transport enum values all decode to `unknown`; an unknown activity message closes with a structured protocol reason instead of panicking. The legacy `0.4` HTTP and `/ws` routes are compatibility endpoints; new clients use `/ws/v2`.

## RPC, events, and sync

Every business operation passes device authentication, permission, and workspace authorization. Mutations use bounded idempotency keys; file operations additionally use content revisions / CAS.

When a client detects a sequence-number or cursor gap, it returns `resync_required` and points at an authoritative operation to re-fetch. Attachment streams re-authenticate and re-authorize their domain before they start. A terminal attachment carries workspace scope, and terminal input requires generation checks, authorization, and auditing, and **keeps no raw bytes**.

After a device grant is revoked, every active connection for that device immediately receives `device_revoked`. When the runtime shuts down, it sends `server_shutdown`, drains listeners, and releases sockets.

## Permission model

`RemoteDevicePermissionLevel` has three values, serialized as `read_only` / `approve_only` / `full_control`:

| Level          | Allowed operation classes                                                                                                                                                                             |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read_only`    | `ReadProject`, `ReadAgentSession`, `ReadProviderSettings`; everything else is denied and audited as `permission_denied`.                                                                              |
| `approve_only` | The reads above, plus `ResolvePermission`, `ResolveElicitation`.                                                                                                                                      |
| `full_control` | All 13 classes, including `MutateAgentSession`, `MutateAgentAuthentication`, `MutateFile`, `MutateGit`, `MutateTerminal`, `MutateProviderSettings`, `ReadDeviceManagement`, `MutateDeviceManagement`. |

The level is fixed on the authority when the offer / pairing code is created; a client cannot raise its own privileges.

## Gateway endpoints

| Endpoint                                     | Description                                                      |
| -------------------------------------------- | ---------------------------------------------------------------- |
| `/api/v2/info`                               | Capabilities, `serverIdentityPublicKey`, `pairingCodeClaimPath`. |
| `/api/v2/pairing/claim`                      | Offer claim.                                                     |
| `/api/v2/pairing/code/claim`                 | Pairing-code claim.                                              |
| `/api/v2/pairing/lan`, `/request`, `/status` | LAN pairing.                                                     |
| `/api/v2/ws-ticket`                          | Exchange for a WebSocket ticket.                                 |
| `/ws/v2`                                     | The main Remote v2 channel.                                      |

## Relay constraints

Relay is a **zero-knowledge room router**: it only forwards opaque encrypted frames. It may expose the routing metadata that `/health`, `/api/info`, and the WebSocket bridge need, but it does **not** perform business authorization, read or write workspaces, or store provider profiles or timelines.

The endpoints on Relay are `/health`, `/api/info`, `/ws`, `/api/rooms/{room_id}/pair|command`, and two `/api/push/*` routes. **There is no `/api/v2/info` on Relay** — that is an endpoint of the authoritative runtime.

Local checks:

```bash theme={null}
pnpm smoke:relay:local
cargo test -p vibex-remote-client --test relay_smoke --locked -- --nocapture
```

## Connection strategy

The client can probe and fall back between Direct, Tailnet, Relay, and LAN. After a route recovers, probe again and switch, **without pairing again**; if the device has been revoked, every route must fail.

## Deployment

```bash theme={null}
docker compose -f deploy/relay/docker-compose.yml up --build -d relay-server
curl -fsS http://127.0.0.1:9700/health
```

Before you publish to the public internet, configure HTTPS/WSS, Host/Origin validation, and connection and room limits, and read `docs/smoke/relay-nat.md` and `docs/smoke/remote-lan.md` in the repository.

## Logging and evidence requirements

Do not record the following in Relay logs or test evidence: tokens, pairing codes, private keys, prompts, file paths, terminal content, Git diffs, provider settings, raw ciphertext, or nonces.

Relay logs should contain routing, counts, and timing metadata only.
