add project plan
This commit is contained in:
416
PLAN.md
Normal file
416
PLAN.md
Normal 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 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?
|
||||||
Reference in New Issue
Block a user