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

419 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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": "<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 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?