10 KiB
Yonexus.Server — Project Plan
1. Goal
Yonexus.Server is the OpenClaw plugin that acts as the central communication hub in a Yonexus network.
It is responsible for:
- accepting WebSocket connections from clients
- maintaining the client registry and trust state
- handling pairing initiation and Discord DM notification
- verifying client authentication proofs
- tracking client liveness via heartbeat
- routing and dispatching application messages
- exposing a TypeScript API for server-side plugins and integrations
2. Configuration
interface YonexusServerConfig {
followerIdentifiers: string[];
notifyBotToken: string;
adminUserId: string;
listenHost?: string;
listenPort: number;
publicWsUrl?: string;
}
Field semantics:
followerIdentifiers: allowlist of identifiers permitted to pair/connectnotifyBotToken: Discord bot token for sending pairing code DM to adminadminUserId: Discord user id of the human administratorlistenHost: local bind address (default:0.0.0.0)listenPort: local bind port (required)publicWsUrl: optional canonical external WebSocket URL advertised to clients
Validation:
- missing required fields must fail plugin initialization
- identifiers not in
followerIdentifiersmust be rejected at connection time
3. Runtime Lifecycle
3.1 Startup
On OpenClaw gateway startup:
- load and validate config
- initialize persistent client registry
- register builtin protocol handlers
- register application rule registry
- start WebSocket server on configured host/port
- start heartbeat/status sweep timer
3.2 Shutdown
On shutdown:
- close all client WebSocket connections gracefully
- persist client registry state
- stop sweep timers
4. Client Registry
4.1 Data Model
interface ClientRecord {
identifier: string;
publicKey?: string;
secret?: string;
pairingStatus: "unpaired" | "pending" | "paired" | "revoked";
pairingCode?: string;
pairingExpiresAt?: number;
pairingNotifiedAt?: number;
pairingNotifyStatus?: "pending" | "sent" | "failed";
status: "online" | "offline" | "unstable";
lastHeartbeatAt?: number;
lastAuthenticatedAt?: number;
recentNonces: Array<{ nonce: string; timestamp: number }>;
recentHandshakeAttempts: number[];
createdAt: number;
updatedAt: number;
}
4.2 Persistence
Registry must survive server restarts:
- all trust-related fields must be persisted
- security rolling windows should be reset on restart or kept safely
- on-disk storage format should support future encryption-at-rest
5. Pairing Flow
5.1 Entry Condition
Pairing starts when a client connects and:
- its identifier is in
followerIdentifiers - it has no valid
secretstored
5.2 Step A — Generate Pairing Code
Server generates:
- a random
pairingCode expiresAt(UTC unix seconds)ttlSeconds
5.3 Step B — Discord DM to Admin
Server must use notifyBotToken to DM adminUserId.
DM body must contain:
identifierpairingCodeexpiresAtor TTL
DM delivery must succeed before protocol continues.
5.4 Step C — Protocol Notification to Client
Server sends hello_ack with nextAction: "pair_required".
Server then sends pair_request builtin message containing:
identifierexpiresAtttlSecondsadminNotification: "sent" | "failed"codeDelivery: "out_of_band"
5.5 Step D — Accept Confirmation
Server accepts a pair_confirm builtin message from client containing the pairing code.
Validation:
- code must match stored pending code
- current time must be before
pairingExpiresAt
5.6 Step E — Issue Secret
On successful confirmation:
- generate a random
secret - store
publicKeyandsecret - mark
pairingStatusaspaired - send
pair_successbuiltin message to client with the secret
On failure:
- send
pair_failedbuiltin message - optionally retry or leave for client to reconnect
6. Authentication
6.1 Entry Condition
Authentication starts when a connected client sends a hello with hasSecret: true.
6.2 Proof Validation
Client sends auth_request containing:
identifiernonce(24 random characters)proofTimestamp(UTC unix seconds)signature(signed proof payload)- optionally a new
publicKeyif rotating
Server validates:
- identifier is allowlisted and paired
- public key matches stored key (if not rotating)
- signature verifies correctly
- decrypted proof contains the correct
secret abs(now - proofTimestamp) < 10- nonce is not in recent nonce window
- handshake attempts in last 10s ≤ 10
6.3 Nonce Window
Store last 10 nonces per client with their timestamps.
When a nonce is presented:
- if it matches any in the window, reject with
nonce_collision - add the new nonce to the window
- trim window to most recent 10 entries
6.4 Handshake Rate Limit
Track recent handshake attempt timestamps per client.
If >10 attempts appear in the last 10 seconds:
- reject with
rate_limited - trigger
re_pair_required - mark pairing status as
revoked
6.5 Success / Failure Responses
On success:
- send
auth_successwithstatus: "online" - record
lastAuthenticatedAt
On failure:
- send
auth_failedwith reason - if reason is unsafe, also send
re_pair_required
7. Heartbeat and Liveness
7.1 Heartbeat Reception
Clients send heartbeat builtin messages every 5 minutes.
On receiving a heartbeat:
- update
lastHeartbeatAt - if client was
offlineorunstable, transition toonline
7.2 Status Sweep
Server runs a periodic sweep (recommended: every 30–60s).
For each registered client:
- if no heartbeat for 7 min → mark
unstable - if no heartbeat for 11 min → mark
offline, close socket, senddisconnect_noticefirst
7.3 Status Transitions
Allowed transitions:
online→unstable(7 min timeout)unstable→online(heartbeat received)unstable→offline(11 min timeout)offline→ (removed from active registry or marked offline permanently)
8. Messaging and Rule Dispatch
8.1 Message Rewrite
When server receives an application rule message from a client, before rule dispatch it rewrites:
${rule_identifier}::${message_content}
Into:
${rule_identifier}::${sender_identifier}::${message_content}
8.2 Rule Registry
Server maintains a registry of (rule_identifier → processor) pairs.
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 Processor Function Signature
type RuleProcessor = (message: string) => unknown;
registerRule(rule: string, processor: RuleProcessor): void;
Validation:
- must reject
builtin - must reject duplicate rule unless explicit override mode is added later
8.4 API: sendMessageToClient
async function sendMessageToClient(identifier: string, message: string): Promise<void>
Constraints:
- identifier must be currently connected and authenticated
- message must already conform to
${rule_identifier}::${message_content} - throws if identifier is not online
9. WebSocket Server
9.1 Connection Accept
On new WebSocket connection:
- read initial
hellomessage - validate identifier is in allowlist
- check if paired/authenticated or requires pairing
- proceed accordingly
9.2 Connection Close
On client disconnect:
- mark client as offline immediately
- stop heartbeat tracking for that session
- keep persistent registry state intact
9.3 One Active Session Per Identifier
Recommended v1 policy:
- if a new authenticated connection appears for an already-authenticated identifier, terminate the old connection and accept the new one
10. Error Handling
Structured errors required for at minimum:
INVALID_CONFIG— missing required config fieldsIDENTIFIER_NOT_ALLOWED— identifier not in allowlistPAIRING_NOTIFICATION_FAILED— Discord DM send failedPAIRING_EXPIRED— pairing code expiredAUTH_FAILED— proof verification failedNONCE_COLLISION— replay detectedRATE_LIMITED— unsafe handshake rateRE_PAIR_REQUIRED— trust must be resetCLIENT_OFFLINE— attempted to send to offline clientRULE_ALREADY_REGISTERED— duplicate rule registrationRESERVED_RULE— attempted to registerbuiltinMALFORMED_MESSAGE— malformed builtin/application message
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 Server
- WebSocket server startup
- connection accept / close lifecycle
- hello / hello_ack flow
- per-connection state tracking
Phase 2 — Registry and Persistence
- in-memory client registry
- on-disk persistence (JSON or equivalent)
- restart recovery
- basic CRUD for client records
Phase 3 — Pairing
- pairing code generation
- Discord DM via bot token
- pair_request / pair_confirm / pair_success / pair_failed
- pairing state transitions
Phase 4 — Authentication
- auth_request verification
- signature verification
- nonce window tracking
- handshake rate limiting
- re_pair_required flow
Phase 5 — Heartbeat and Status
- heartbeat receiver
- status sweep timer
- online / unstable / offline transitions
- disconnect_notice before socket close
Phase 6 — Rule Dispatch and APIs
- rule registry
- message rewrite on inbound
- first-match dispatch
registerRuleAPIsendMessageToClientAPI
Phase 7 — Hardening
- structured error definitions
- redacted logging for sensitive values
- integration test coverage
- failure-path coverage
12. Open Questions for Yonexus.Server
These should be resolved before or during implementation:
- What Discord library/module will be used to send DM? (direct Discord API / discord.js / etc.)
- Should the WebSocket server also expose an optional TLS listener?
- Should the sweep timer interval be configurable or fixed?
- What is the maximum supported number of concurrent connected clients?
- Should server-side rule processors run in isolated contexts?
- Should
sendMessageToClientqueue messages for briefly offline clients, or fail immediately?