add project plan

This commit is contained in:
root
2026-04-01 01:08:12 +00:00
parent 2c5f4c6002
commit c5330bb9f9

416
PLAN.md Normal file
View File

@@ -0,0 +1,416 @@
# 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 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?