# Yonexus.Client — Project Plan ## 1. Goal `Yonexus.Client` is the OpenClaw plugin that acts as a client in a Yonexus network. This repository references `Yonexus.Protocol` as a submodule at `protocol/`. It is responsible for: - connecting to `Yonexus.Server` - managing local keypair and secret - completing the out-of-band pairing confirmation - authenticating on each reconnect - sending periodic heartbeats - exposing a TypeScript API for client-side plugins and integrations - sending and receiving application messages through the server --- ## 2. Configuration ```ts interface YonexusClientConfig { mainHost: string; identifier: string; notifyBotToken: string; adminUserId: string; } ``` Field semantics: - `mainHost`: WebSocket endpoint of `Yonexus.Server` (`ws://host:port/path` or `wss://`) - `identifier`: unique identity of this client within the Yonexus network - `notifyBotToken`: Discord bot token (kept for potential future client-side notification needs) - `adminUserId`: Discord user id of the human administrator (reference/shared with Yonexus system) Validation: - missing required fields must fail plugin initialization - `mainHost` must be a valid WebSocket URL - `identifier` must be non-empty --- ## 3. Runtime Lifecycle ### 3.1 Startup On OpenClaw gateway startup: 1. load and validate config 2. load local identity, keypair, and secret from persistent storage 3. if no keypair exists, generate one 4. connect to `mainHost` 5. start pairing or authentication flow depending on local state 6. after authentication, start heartbeat schedule 7. handle incoming messages and rule dispatch ### 3.2 Reconnect On disconnect: 1. stop heartbeat 2. enter `reconnecting` state 3. attempt reconnect with backoff 4. after reconnect, perform authentication or pairing again --- ## 4. Local Identity and Storage ### 4.1 Keypair `Yonexus.Client` must generate a cryptographic keypair on first run. Recommended algorithm: Ed25519 - private key: never transmitted to server - public key: transmitted to server during pairing and optionally during auth ### 4.2 Local Persistence Model ```ts interface LocalClientState { identifier: string; privateKey: string; // encoded private key, stored locally publicKey: string; // encoded public key secret?: string; // shared secret issued by server after pairing pairingStatus: "unpaired" | "pending" | "paired" | "revoked"; pairedAt?: number; lastConnectedAt?: number; } ``` ### 4.3 Security Notes - private key must never leave the local instance - storage format should make encryption-at-rest a future option - initial implementation may use plaintext local file if documented as a limitation --- ## 5. Pairing Flow ### 5.1 Entry Condition Pairing starts when: - the server responds to `hello` with `hello_ack(nextAction: "pair_required")` - OR the server sends `pair_request` builtin message ### 5.2 Step A — Receive Notification Client receives `pair_request` from server via WebSocket. This message contains: - `expiresAt` - `ttlSeconds` - `adminNotification` - `codeDelivery: "out_of_band"` ### 5.3 Step B — Human-Mediated Code Acquisition The pairing code is delivered to a human administrator via Discord DM from the server. The client operator must obtain this code from the human through some out-of-band trusted path. This is intentionally not automated — a human must relay the code. ### 5.4 Step C — Submit Confirmation Client sends `pair_confirm` builtin message to server: ```json { "type": "pair_confirm", "payload": { "identifier": "", "pairingCode": "" } } ``` ### 5.5 Step D — Receive Secret On success, server sends `pair_success` containing: - `identifier` - `secret` - `pairedAt` Client must: - store `secret` locally - update `pairingStatus` to `paired` - store `pairedAt` ### 5.6 Failure Handling If server responds with `pair_failed`: - log the reason - return to `pairing_required` state - wait for human to provide a new code and retry --- ## 6. Authentication ### 6.1 Entry Condition Authentication starts when: - client connects and already has a stored `secret` - server responds to `hello` with `hello_ack(nextAction: "auth_required")` ### 6.2 Proof Construction Client constructs a proof payload from: - `secret`: stored shared secret - `nonce`: exactly 24 random characters, generated fresh each attempt - `timestamp`: current UTC unix seconds Logical concatenation: ```text secret + nonce + timestamp ``` Implementation recommendation: - use a canonical serialized object, then sign its canonical bytes - avoid naive string concatenation in code Example canonical JSON: ```json { "secret": "...", "nonce": "RANDOM24CHARACTERSTRINGX", "timestamp": 1711886500 } ``` ### 6.3 Signing Client signs the canonical proof bytes using its private key. ### 6.4 Sending Auth Request Client sends `auth_request` builtin message: ```json { "type": "auth_request", "payload": { "identifier": "", "nonce": "<24-char-nonce>", "proofTimestamp": 1711886500, "signature": "" } } ``` ### 6.5 Response Handling On `auth_success`: - record `lastAuthenticatedAt` - transition to `authenticated` state - start heartbeat loop On `auth_failed`: - log the reason - handle according to reason: - `stale_timestamp` / `future_timestamp`: retry with fresh timestamp - `nonce_collision`: retry with fresh nonce - others: escalate to re-pairing if needed On `re_pair_required`: - clear stored `secret` and public key trust - transition to `pairing_required` - begin pairing flow again --- ## 7. Heartbeat ### 7.1 Sending After successful authentication, client starts a heartbeat loop. Interval: every 5 minutes (300 seconds) Message: `heartbeat` builtin message with `status: "alive"`. ### 7.2 Handling heartbeat_ack If server responds with `heartbeat_ack`, client may: - update local connection metadata - optionally log `heartbeat_ack` is optional on the server side; client should not fail if it is absent. ### 7.3 On Disconnect When WebSocket connection is lost: - stop heartbeat loop immediately - enter `reconnecting` state - attempt reconnect with backoff --- ## 8. Messaging and Rule Dispatch ### 8.1 Outbound: sendMessageToServer ```ts async function sendMessageToServer(message: string): Promise ``` Constraints: - client must be authenticated / connected - message must already conform to `${rule_identifier}::${message_content}` ### 8.2 Inbound: Rule Dispatch Client receives messages from server formatted as: ``` ${rule_identifier}::${message_content} ``` Client maintains its own rule registry (separate from server's). Dispatch algorithm: 1. parse first `::` segment as `rule_identifier` 2. if `rule_identifier === builtin`, route to builtin protocol handler 3. iterate registered rules in registration order 4. invoke first exact match 5. if no match, ignore or log as unhandled ### 8.3 registerRule API ```ts function registerRule(rule: string, processor: (message: string) => unknown): void ``` Validation: - must reject `builtin` - must reject duplicate rule unless explicit override mode is added later --- ## 9. WebSocket Client ### 9.1 Connection Client connects to `mainHost` on startup and on each reconnect. ### 9.2 First Message: hello Immediately after connection opens, client sends `hello`: ```json { "type": "hello", "payload": { "identifier": "", "hasSecret": true, "hasKeyPair": true, "publicKey": "", "protocolVersion": "1" } } ``` Fields: - `hasSecret`: whether a secret is stored locally - `hasKeyPair`: always `true` after first key generation - `publicKey`: current public key (required after keypair generation) ### 9.3 Reconnect Backoff Strategy Recommended initial strategy: - first retry: 1 second - subsequent retries: multiply by 2, cap at 60 seconds - jitter: add random 0–1s to avoid thundering herd --- ## 10. Error Handling Structured errors required for at minimum: - `INVALID_CONFIG` — missing required config fields - `CONNECTION_FAILED` — cannot connect to `mainHost` - `PAIRING_FAILED` — server rejected pairing - `AUTH_FAILED` — server rejected auth proof - `RE_PAIR_REQUIRED` — server demands re-pairing - `RULE_ALREADY_REGISTERED` — duplicate rule registration - `RESERVED_RULE` — attempted to register `builtin` - `MALFORMED_MESSAGE` — malformed builtin/application message received - `NOT_AUTHENTICATED` — attempted to send before auth --- ## 11. Implementation Phases ### Phase 0 — Skeleton - plugin manifest and entry point - config loading and validation - basic OpenClaw hook registration - minimal logging/error scaffolding ### Phase 1 — WebSocket Client - WebSocket client connection - hello message - reconnect with backoff - connection close lifecycle ### Phase 2 — Local Identity - keypair generation (Ed25519) - local state persistence - state load on startup ### Phase 3 — Pairing - handle `pair_request` - accept human-provided pairing code - submit `pair_confirm` - store `secret` on `pair_success` - handle `pair_failed` ### Phase 4 — Authentication - proof construction - signing - send `auth_request` - handle `auth_success` - handle `auth_failed` - handle `re_pair_required` ### Phase 5 — Heartbeat - heartbeat loop (5 min interval) - heartbeat message construction - handle `heartbeat_ack` - stop loop on disconnect ### Phase 6 — Rule Dispatch and APIs - rule registry - inbound message dispatch - `registerRule` API - `sendMessageToServer` API ### Phase 7 — Hardening - structured error definitions - redacted logging for sensitive values - integration test coverage - failure-path coverage --- ## 12. Open Questions for Yonexus.Client These should be resolved before or during implementation: 1. What cryptographic library will be used for Ed25519 keypair and signing? 2. Should `notifyBotToken` and `adminUserId` be retained in client config, or are they purely server-side concerns? 3. Should reconnect backoff be configurable or fixed? 4. Should the client maintain a local log of recent sent/received messages for debugging? 5. Should `sendMessageToServer` throw if called before authentication, or queue the message?