417 lines
10 KiB
Markdown
417 lines
10 KiB
Markdown
# 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
|
||
|
||
```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": "<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:
|
||
|
||
```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": "<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
|
||
|
||
```ts
|
||
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
|
||
|
||
```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": "<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 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?
|