Files
Yonexus.Client/PLAN.md
2026-04-01 01:21:33 +00:00

10 KiB
Raw Blame History

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

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

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:

{
  "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:

  • 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:

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 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

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:

  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

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 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 01s 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?