10 KiB
Yonexus.Client — Project Plan
1. Goal
Yonexus.Client is the OpenClaw plugin that acts as a client in a Yonexus network.
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
interface YonexusClientConfig {
mainHost: string;
identifier: string;
notifyBotToken: string;
adminUserId: string;
}
Field semantics:
mainHost: WebSocket endpoint ofYonexus.Server(ws://host:port/pathorwss://)identifier: unique identity of this client within the Yonexus networknotifyBotToken: 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
mainHostmust be a valid WebSocket URLidentifiermust be non-empty
3. Runtime Lifecycle
3.1 Startup
On OpenClaw gateway startup:
- load and validate config
- load local identity, keypair, and secret from persistent storage
- if no keypair exists, generate one
- connect to
mainHost - start pairing or authentication flow depending on local state
- after authentication, start heartbeat schedule
- handle incoming messages and rule dispatch
3.2 Reconnect
On disconnect:
- stop heartbeat
- enter
reconnectingstate - attempt reconnect with backoff
- 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
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
hellowithhello_ack(nextAction: "pair_required") - OR the server sends
pair_requestbuiltin message
5.2 Step A — Receive Notification
Client receives pair_request from server via WebSocket.
This message contains:
expiresAtttlSecondsadminNotificationcodeDelivery: "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:
{
"type": "pair_confirm",
"payload": {
"identifier": "<this-client-identifier>",
"pairingCode": "<human-provided-code>"
}
}
5.5 Step D — Receive Secret
On success, server sends pair_success containing:
identifiersecretpairedAt
Client must:
- store
secretlocally - update
pairingStatustopaired - store
pairedAt
5.6 Failure Handling
If server responds with pair_failed:
- log the reason
- return to
pairing_requiredstate - 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
hellowithhello_ack(nextAction: "auth_required")
6.2 Proof Construction
Client constructs a proof payload from:
secret: stored shared secretnonce: exactly 24 random characters, generated fresh each attempttimestamp: current UTC unix seconds
Logical concatenation:
secret + nonce + timestamp
Implementation recommendation:
- use a canonical serialized object, then sign its canonical bytes
- avoid naive string concatenation in code
Example canonical 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:
{
"type": "auth_request",
"payload": {
"identifier": "<this-client-identifier>",
"nonce": "<24-char-nonce>",
"proofTimestamp": 1711886500,
"signature": "<base64-signature>"
}
}
6.5 Response Handling
On auth_success:
- record
lastAuthenticatedAt - transition to
authenticatedstate - start heartbeat loop
On auth_failed:
- log the reason
- handle according to reason:
stale_timestamp/future_timestamp: retry with fresh timestampnonce_collision: retry with fresh nonce- others: escalate to re-pairing if needed
On re_pair_required:
- clear stored
secretand 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
reconnectingstate - attempt reconnect with backoff
8. Messaging and Rule Dispatch
8.1 Outbound: sendMessageToServer
async function sendMessageToServer(message: string): Promise<void>
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:
- parse first
::segment asrule_identifier - if
rule_identifier === builtin, route to builtin protocol handler - iterate registered rules in registration order
- invoke first exact match
- if no match, ignore or log as unhandled
8.3 registerRule API
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:
{
"type": "hello",
"payload": {
"identifier": "<this-client-identifier>",
"hasSecret": true,
"hasKeyPair": true,
"publicKey": "<current-public-key>",
"protocolVersion": "1"
}
}
Fields:
hasSecret: whether a secret is stored locallyhasKeyPair: alwaystrueafter first key generationpublicKey: 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 fieldsCONNECTION_FAILED— cannot connect tomainHostPAIRING_FAILED— server rejected pairingAUTH_FAILED— server rejected auth proofRE_PAIR_REQUIRED— server demands re-pairingRULE_ALREADY_REGISTERED— duplicate rule registrationRESERVED_RULE— attempted to registerbuiltinMALFORMED_MESSAGE— malformed builtin/application message receivedNOT_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
secretonpair_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
registerRuleAPIsendMessageToServerAPI
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:
- What cryptographic library will be used for Ed25519 keypair and signing?
- Should
notifyBotTokenandadminUserIdbe retained in client config, or are they purely server-side concerns? - Should reconnect backoff be configurable or fixed?
- Should the client maintain a local log of recent sent/received messages for debugging?
- Should
sendMessageToServerthrow if called before authentication, or queue the message?