From 9232aa7c1755adda6990a5a2f6c7c1a114285a73 Mon Sep 17 00:00:00 2001 From: nav Date: Wed, 1 Apr 2026 01:19:22 +0000 Subject: [PATCH] add protocol specification --- PROTOCOL.md | 845 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 30 ++ 2 files changed, 875 insertions(+) create mode 100644 PROTOCOL.md diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..e4cdcbe --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,845 @@ +# Yonexus Protocol Specification + +Version: draft v0.3 +Status: planning + +--- + +## 1. Purpose + +This document defines the built-in Yonexus communication protocol used between: +- `Yonexus.Server` +- `Yonexus.Client` + +The protocol covers: +- connection setup +- pairing +- authentication +- heartbeat +- status/lifecycle events +- protocol-level errors +- transport of application rule messages over the same WebSocket channel + +Important security rule: +- pairing codes must **not** be delivered to `Yonexus.Client` over the Yonexus WebSocket channel +- pairing codes must be delivered out-of-band to a human administrator via Discord DM + +--- + +## 2. Transport + +Transport is WebSocket. + +- `Yonexus.Server` listens as WebSocket server +- `Yonexus.Client` connects as WebSocket client +- protocol frames are UTF-8 text in v1 +- binary frames are not required in v1 + +Client connects to configured `mainHost`, which may be: +- `ws://host:port/path` +- `wss://host:port/path` +- or raw `host:port` if normalized by implementation + +Recommended canonical config: +- prefer full WebSocket URL + +--- + +## 3. Message Categories + +## 3.1 Builtin Protocol Messages + +Builtin messages always use: + +```text +builtin::${json_payload} +``` + +`builtin` is reserved and must not be registered by user code. + +## 3.2 Application Rule Messages + +Application messages use: + +```text +${rule_identifier}::${message_content} +``` + +When `Yonexus.Server` receives a rule message from a client, it must rewrite it before dispatch to: + +```text +${rule_identifier}::${sender_identifier}::${message_content} +``` + +--- + +## 4. Builtin Envelope + +Builtin wire format: + +```text +builtin::{JSON} +``` + +Canonical envelope: + +```ts +interface BuiltinEnvelope { + type: string; + requestId?: string; + timestamp?: number; // UTC unix seconds + payload?: Record; +} +``` + +Rules: +- `timestamp` uses UTC unix seconds +- `requestId` is used for correlation where needed +- `payload` content depends on `type` + +--- + +## 5. Builtin Message Types + +## 5.1 Session Setup + +### `hello` +Sent by `Yonexus.Client` immediately after WebSocket connection opens. + +Purpose: +- declare identifier +- advertise current auth material state +- announce protocol version + +Example: + +```text +builtin::{ + "type":"hello", + "requestId":"req_001", + "timestamp":1711886400, + "payload":{ + "identifier":"client-a", + "hasSecret":true, + "hasKeyPair":true, + "publicKey":"", + "protocolVersion":"1" + } +} +``` + +### `hello_ack` +Sent by `Yonexus.Server` in response to `hello`. + +Possible `nextAction` values: +- `pair_required` +- `auth_required` +- `rejected` +- `waiting_pair_confirm` + +Example: + +```text +builtin::{ + "type":"hello_ack", + "requestId":"req_001", + "timestamp":1711886401, + "payload":{ + "identifier":"client-a", + "nextAction":"pair_required" + } +} +``` + +--- + +## 5.2 Pairing Flow + +## 5.2.1 Pairing Design Rule + +`Yonexus.Server` must never send the actual `pairingCode` to `Yonexus.Client` through the Yonexus WebSocket channel. + +The pairing code must be delivered to the configured human administrator using: +- `notifyBotToken` +- `adminUserId` + +Specifically: +- `Yonexus.Server` sends a Discord DM to the configured admin user +- the DM contains the client identifier and pairing code +- the human relays the code to the client side by some trusted out-of-band path + +## 5.2.2 Pairing Request Creation + +When pairing is required, `Yonexus.Server` generates: +- `pairingCode` +- `expiresAt` +- `ttlSeconds` + +The admin DM must include at minimum: +- `identifier` +- `pairingCode` +- `expiresAt` or TTL + +Example DM body: + +``` +Yonexus pairing request +identifier: client-a +pairingCode: ABCD-1234-XYZ +expiresAt: 1711886702 +``` + +### `pair_request` +Sent by `Yonexus.Server` to `Yonexus.Client` after pairing starts. + +Purpose: +- indicate that pairing has started +- indicate whether admin notification succeeded +- provide expiry metadata without revealing the code + +Example: + +```text +builtin::{ + "type":"pair_request", + "requestId":"req_002", + "timestamp":1711886402, + "payload":{ + "identifier":"client-a", + "expiresAt":1711886702, + "ttlSeconds":300, + "adminNotification":"sent", + "codeDelivery":"out_of_band" + } +} +``` + +Allowed `adminNotification` values: +- `sent` +- `failed` + +If notification failed, pairing must not proceed until retried successfully. + +### `pair_confirm` +Sent by `Yonexus.Client` to confirm pairing. + +Purpose: +- submit the pairing code obtained out-of-band + +Example: + +```text +builtin::{ + "type":"pair_confirm", + "requestId":"req_002", + "timestamp":1711886410, + "payload":{ + "identifier":"client-a", + "pairingCode":"ABCD-1234-XYZ" + } +} +``` + +### `pair_success` +Sent by `Yonexus.Server` after successful pairing. + +Purpose: +- return generated secret +- confirm trusted pairing state + +Example: + +```text +builtin::{ + "type":"pair_success", + "requestId":"req_002", + "timestamp":1711886411, + "payload":{ + "identifier":"client-a", + "secret":"", + "pairedAt":1711886411 + } +} +``` + +### `pair_failed` +Sent by `Yonexus.Server` when pairing fails. + +Typical reasons: +- `expired` +- `invalid_code` +- `identifier_not_allowed` +- `admin_notification_failed` +- `internal_error` + +Example: + +```text +builtin::{ + "type":"pair_failed", + "requestId":"req_002", + "timestamp":1711886710, + "payload":{ + "identifier":"client-a", + "reason":"expired" + } +} +``` + +--- + +## 5.3 Authentication Flow + +After pairing, reconnect authentication uses: +- stored `secret` +- 24-character random nonce +- current UTC unix timestamp +- client private key + +## 5.3.1 Proof Construction + +Logical proof content: + +```text +secret + nonce + timestamp +``` + +Implementation recommendation: +- use canonical serialized object bytes for signing + +Recommended logical form: + +```json +{ + "secret":"...", + "nonce":"...", + "timestamp":1711886500 +} +``` + +## 5.3.2 Signature Primitive + +Recommended primitive: +- digital signature using client private key +- verification using stored client public key on server + +### `auth_request` +Sent by `Yonexus.Client` after pairing or on reconnect. + +Example: + +```text +builtin::{ + "type":"auth_request", + "requestId":"req_003", + "timestamp":1711886500, + "payload":{ + "identifier":"client-a", + "nonce":"RANDOM24CHARACTERSTRINGX", + "proofTimestamp":1711886500, + "signature":"", + "publicKey":"" + } +} +``` + +Server validation: +1. identifier is allowlisted +2. identifier exists in registry +3. client is in paired state +4. public key matches expected key if provided +5. signature verifies successfully +6. proof contains correct secret +7. `abs(now - proofTimestamp) < 10` +8. nonce has not appeared in recent nonce window +9. handshake attempts in last 10 seconds do not exceed 10 + +### `auth_success` +Sent by `Yonexus.Server` on success. + +Example: + +```text +builtin::{ + "type":"auth_success", + "requestId":"req_003", + "timestamp":1711886501, + "payload":{ + "identifier":"client-a", + "authenticatedAt":1711886501, + "status":"online" + } +} +``` + +### `auth_failed` +Sent by `Yonexus.Server` on auth failure. + +Allowed reasons include: +- `unknown_identifier` +- `not_paired` +- `invalid_signature` +- `invalid_secret` +- `stale_timestamp` +- `future_timestamp` +- `nonce_collision` +- `rate_limited` +- `re_pair_required` + +### `re_pair_required` +Sent by `Yonexus.Server` when trust state must be reset. + +Example: + +```text +builtin::{ + "type":"re_pair_required", + "requestId":"req_004", + "timestamp":1711886510, + "payload":{ + "identifier":"client-a", + "reason":"nonce_collision" + } +} +``` + +--- + +## 5.4 Heartbeat + +### `heartbeat` +Sent by `Yonexus.Client` every 5 minutes after authentication. + +Example: + +```text +builtin::{ + "type":"heartbeat", + "timestamp":1711886800, + "payload":{ + "identifier":"client-a", + "status":"alive" + } +} +``` + +### `heartbeat_ack` +Optional response by `Yonexus.Server`. + +Example: + +```text +builtin::{ + "type":"heartbeat_ack", + "timestamp":1711886801, + "payload":{ + "identifier":"client-a", + "status":"online" + } +} +``` + +--- + +## 5.5 Status / Lifecycle Notifications + +### `status_update` +Sent by `Yonexus.Server` when client state changes. + +Example: + +```text +builtin::{ + "type":"status_update", + "timestamp":1711887220, + "payload":{ + "identifier":"client-a", + "status":"unstable", + "reason":"heartbeat_timeout_7m" + } +} +``` + +### `disconnect_notice` +Sent by `Yonexus.Server` before deliberate close. + +Example: + +```text +builtin::{ + "type":"disconnect_notice", + "timestamp":1711887460, + "payload":{ + "identifier":"client-a", + "reason":"heartbeat_timeout_11m" + } +} +``` + +--- + +## 5.6 Errors + +### `error` +Generic protocol-level error. + +Recommended builtin error codes: +- `MALFORMED_MESSAGE` +- `UNSUPPORTED_PROTOCOL_VERSION` +- `IDENTIFIER_NOT_ALLOWED` +- `PAIRING_REQUIRED` +- `PAIRING_EXPIRED` +- `ADMIN_NOTIFICATION_FAILED` +- `AUTH_FAILED` +- `NONCE_COLLISION` +- `RATE_LIMITED` +- `RE_PAIR_REQUIRED` +- `CLIENT_OFFLINE` +- `INTERNAL_ERROR` + +--- + +## 6. State Machines + +## 6.1 Client State Machine + +Suggested `Yonexus.Client` states: +- `idle` +- `connecting` +- `connected` +- `pairing_required` +- `pairing_pending` +- `paired` +- `authenticating` +- `authenticated` +- `reconnecting` +- `error` + +Typical transitions: + +``` +idle + -> connecting + -> connected + -> (pairing_required | authenticating) + +pairing_required + -> pairing_pending + -> paired + -> authenticating + -> authenticated + +authenticated + -> reconnecting + -> connecting +``` + +On `re_pair_required`: + +``` +authenticated | authenticating -> pairing_required +``` + +## 6.2 Server-Side Client State + +Per client trust state: +- `unpaired` +- `pending` +- `paired` +- `revoked` + +Per client liveness state: +- `online` +- `unstable` +- `offline` + +--- + +## 7. Security Windows and Replay Protection + +## 7.1 Nonce Requirements + +Nonce rules: +- exactly 24 random characters +- fresh per auth attempt +- must not repeat within recent security window + +## 7.2 Recent Nonce Window + +Server stores for each client: +- the last 10 nonces seen within the recent validity window + +If a nonce collides: +- authentication fails +- server marks condition unsafe +- client must re-pair + +## 7.3 Handshake Attempt Window + +Server stores recent handshake attempt timestamps. + +If more than 10 handshake attempts occur within 10 seconds: +- authentication fails +- server marks condition unsafe +- client must re-pair + +## 7.4 Time Drift Validation + +Server validates: + +```text +abs(current_utc_unix_time - proofTimestamp) < 10 +``` + +If validation fails: +- auth fails +- no session is established + +--- + +## 8. Rule Message Dispatch + +All non-builtin messages use: + +```text +${rule_identifier}::${message_content} +``` + +Client to server example: + +```text +chat_sync::{"conversationId":"abc","body":"hello"} +``` + +Server rewrites before matching: + +```text +chat_sync::client-a::{"conversationId":"abc","body":"hello"} +``` + +Dispatch algorithm: +1. parse first delimiter section as `rule_identifier` +2. if `rule_identifier === builtin`, route to builtin protocol handler +3. otherwise iterate registered rules in registration order +4. invoke the first exact match +5. ignore/log if no match is found + +Processor input: +- on client: `${rule_identifier}::${message_content}` +- on server for client-originated messages: `${rule_identifier}::${sender_identifier}::${message_content}` + +--- + +## 9. Connection Rules + +Server should reject connection attempts when: +- identifier is absent +- identifier is not in configured allowlist +- protocol version is unsupported +- hello/auth payload is malformed + +Recommended v1 policy: +- only one active authenticated connection per client identifier +- terminate old connection and accept new one after successful auth + +--- + +## 10. Persistence Semantics + +## 10.1 Yonexus.Server Persists + +At minimum: +- identifier +- public key +- secret +- trust state +- pairing code + expiry if pending +- pairing notification metadata +- last known liveness status +- metadata timestamps + +May persist or reset on restart: +- recent nonces +- recent handshake attempts + +Recommended v1: +- clear rolling security windows on restart +- keep long-lived trust records + +## 10.2 Yonexus.Client Persists + +At minimum: +- identifier +- private key +- secret +- optional last successful pair/auth metadata + +--- + +## 11. Versioning + +Protocol version is advertised during `hello`. + +Initial version: + +```text +1 +``` + +--- + +## 12. Canonical JSON Shapes + +```ts +interface HelloPayload { + identifier: string; + hasSecret: boolean; + hasKeyPair: boolean; + publicKey?: string; + protocolVersion: string; +} + +interface PairRequestPayload { + identifier: string; + expiresAt: number; + ttlSeconds: number; + adminNotification: "sent" | "failed"; + codeDelivery: "out_of_band"; +} + +interface PairConfirmPayload { + identifier: string; + pairingCode: string; +} + +interface PairSuccessPayload { + identifier: string; + secret: string; + pairedAt: number; +} + +interface AuthRequestPayload { + identifier: string; + nonce: string; + proofTimestamp: number; + signature: string; + publicKey?: string; +} + +interface HeartbeatPayload { + identifier: string; + status: "alive"; +} +``` + +--- + +## 13. Example Flows + +## 13.1 First-Time Pairing Flow + +``` +Client connects WS +Client -> builtin::hello +Server sends Discord DM to configured admin with identifier + pairingCode +Server -> builtin::hello_ack(nextAction=pair_required) +Server -> builtin::pair_request(expiresAt, adminNotification=sent, codeDelivery=out_of_band) +Human reads DM and relays pairingCode to client side +Client -> builtin::pair_confirm(pairingCode) +Server -> builtin::pair_success(secret) +Client stores secret +Client -> builtin::auth_request(signature over secret+nonce+timestamp) +Server -> builtin::auth_success +Client enters authenticated state +``` + +## 13.2 Normal Reconnect Flow + +``` +Client connects WS +Client -> builtin::hello(hasSecret=true) +Server -> builtin::hello_ack(nextAction=auth_required) +Client -> builtin::auth_request(...) +Server -> builtin::auth_success +Client begins heartbeat schedule +``` + +## 13.3 Unsafe Replay / Collision Flow + +``` +Client -> builtin::auth_request(nonce collision) +Server detects unsafe condition +Server -> builtin::re_pair_required(reason=nonce_collision) +Server invalidates existing secret/session trust +Server sends fresh Discord DM pairing notification on next pairing start +Client returns to pairing_required state +``` + +## 13.4 Heartbeat Timeout Flow + +``` +Client authenticated +No heartbeat for 7 min -> server marks unstable +No heartbeat for 11 min -> server marks offline +Server -> builtin::disconnect_notice +Server closes WS connection +``` + +--- + +## 14. Implementation Notes + +## 14.1 Parsing + +Because the wire format is string-based with `::` delimiters: +- only the first delimiter split should determine the `rule_identifier` +- for `builtin`, the remainder is parsed as JSON once +- message content itself may contain `::`, so avoid naive full split logic + +## 14.2 Discord DM Notification + +When pairing starts on `Yonexus.Server`: +- use configured `notifyBotToken` +- send DM to `adminUserId` +- include only required pairing data +- if DM send fails, surface pairing notification failure + +Sensitive values that must never be logged in plaintext: +- `secret` +- private key +- raw proof material + +--- + +## 15. Open Clarifications + +1. Exact signing algorithm: Ed25519 is a strong default candidate +2. Secret length and generation requirements +3. Pairing code format and length +4. Is human code relay enough for v1, or should admin approve/deny controls be added later? +5. Should `heartbeat_ack` be mandatory or optional? +6. Should client reconnect use exponential backoff? +7. Should duplicate active connections replace old sessions or be rejected in stricter modes? + +--- + +## 16. Reserved Builtin Types + +Reserved builtin `type` values: +- `hello` +- `hello_ack` +- `pair_request` +- `pair_confirm` +- `pair_success` +- `pair_failed` +- `auth_request` +- `auth_success` +- `auth_failed` +- `re_pair_required` +- `heartbeat` +- `heartbeat_ack` +- `status_update` +- `disconnect_notice` +- `error` + +These names are reserved by Yonexus and must not be repurposed by user rules. diff --git a/README.md b/README.md index e69de29..7350721 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,30 @@ +# Yonexus.Protocol + +This repository contains the **shared communication protocol specification** for the Yonexus system. + +It is referenced as a git submodule by both `Yonexus.Server` and `Yonexus.Client`. + +## Contents + +- `PROTOCOL.md` — full protocol specification +- TypeScript type definitions (planned: `src/types.ts`) +- Canonical JSON shape references + +## Purpose + +Yonexus is a cross-instance communication system for OpenClaw, split into: +- `Yonexus.Server` — the central hub plugin +- `Yonexus.Client` — the client plugin +- `Yonexus.Protocol` — the shared protocol definition + +Both server and client implementations must conform to the protocol defined in this repository. + +## Version + +Current protocol version: **draft v0.3** + +## References + +- [Yonexus umbrella repository](https://git.hangman-lab.top/nav/Yonexus) +- [Yonexus.Server](https://git.hangman-lab.top/nav/Yonexus.Server) +- [Yonexus.Client](https://git.hangman-lab.top/nav/Yonexus.Client)