diff --git a/FEAT.md b/FEAT.md new file mode 100644 index 0000000..09d485c --- /dev/null +++ b/FEAT.md @@ -0,0 +1,268 @@ +# Yonexus — Feature Checklist + +## Project Direction + +Yonexus is a **two-plugin** cross-instance communication system for OpenClaw: +- `Yonexus.Server` +- `Yonexus.Client` + +This repository now targets the split-plugin architecture only. + +--- + +## 1. Yonexus.Server Features + +### 1.1 Server Runtime +- WebSocket server startup on OpenClaw gateway boot +- Configurable bind host / bind port +- Optional public WebSocket URL metadata +- Connection accept / close lifecycle handling +- One active authenticated connection per client identifier + +### 1.2 Client Registry +- In-memory active client session registry +- Persistent client trust registry keyed by `identifier` +- Store client public key +- Store shared secret +- Store pairing state and expiry +- Store pairing notification metadata +- Store heartbeat timestamps +- Store recent security windows (nonce / handshake attempts) +- Store liveness state (`online | unstable | offline`) + +### 1.3 Allowlist and Validation +- `followerIdentifiers` allowlist enforcement +- Reject unknown client identifiers +- Reject malformed builtin payloads +- Reject unsupported protocol versions + +### 1.4 Pairing Flow +- Generate pairing code +- Generate pairing expiry / TTL +- Start pending pairing session +- Never send pairing code over Yonexus WebSocket +- Send pairing code to human admin via Discord DM using `notifyBotToken` +- Include target client `identifier` in pairing DM +- Accept client-submitted pairing code via builtin protocol +- Fail pairing on invalid code / expired code / notification failure +- Issue shared secret after successful pairing +- Persist paired trust material + +### 1.5 Authentication +- Verify signed proof from client +- Validate stored secret +- Validate nonce format and uniqueness +- Validate timestamp drift `< 10s` +- Track recent handshake attempts +- Enforce `>10 attempts / 10s` unsafe threshold +- Trigger re-pair on unsafe condition +- Rotate or invalidate trust state when required + +### 1.6 Heartbeat and Status +- Receive heartbeat from authenticated clients +- Update `lastHeartbeatAt` +- Mark client `unstable` after 7 minutes without heartbeat +- Mark client `offline` after 11 minutes without heartbeat +- Close socket when client becomes offline +- Optional heartbeat acknowledgement +- Periodic server-side status sweep timer + +### 1.7 Messaging and Dispatch +- `sendMessageToClient(identifier, message)` API +- Rewrite inbound client messages to `${rule_identifier}::${sender_identifier}::${message_content}` +- Builtin message routing +- Rule registry for application messages +- First-match rule dispatch +- Reject reserved rule `builtin` +- Reject duplicate rule registration by default + +### 1.8 Operations and Safety +- Structured errors for pairing/auth/transport failures +- Redacted logging for sensitive values +- Restart-safe persistent storage for trust state +- Clear or safely rebuild rolling security windows on restart + +--- + +## 2. Yonexus.Client Features + +### 2.1 Client Runtime +- WebSocket client startup on OpenClaw gateway boot +- Connect to configured `mainHost` +- Disconnect / reconnect lifecycle handling +- Retry/backoff reconnect strategy + +### 2.2 Local Identity and Trust Material +- Persist local `identifier` +- Generate public/private keypair on first run +- Persist private key locally +- Persist server-issued secret locally +- Load existing trust material on restart + +### 2.3 Pairing Flow +- Send `hello` after connect +- Enter pairing mode when server requires pairing +- Receive pairing metadata without receiving code itself +- Accept human-provided pairing code on client side +- Send pairing confirmation to server +- Store secret after `pair_success` + +### 2.4 Authentication +- Build proof from `secret + nonce + timestamp` +- Prefer canonical serialized payload for signing +- Sign proof with local private key +- Send builtin `auth_request` +- Handle `auth_success` +- Handle `auth_failed` +- Handle `re_pair_required` + +### 2.5 Heartbeat +- Start heartbeat loop after authentication +- Send heartbeat every 5 minutes +- Stop heartbeat when disconnected / unauthenticated +- Handle optional heartbeat acknowledgement + +### 2.6 Messaging and Dispatch +- `sendMessageToServer(message)` API +- Builtin message routing +- Rule registry for application messages +- First-match rule dispatch +- Reject reserved rule `builtin` +- Reject duplicate rule registration by default + +--- + +## 3. Shared Protocol Features + +### 3.1 Builtin Wire Format +- `builtin::{json}` message format +- Standard builtin envelope with `type`, `requestId`, `timestamp`, `payload` +- UTC unix seconds as protocol timestamp unit + +### 3.2 Builtin Types +- `hello` +- `hello_ack` +- `pair_request` +- `pair_confirm` +- `pair_success` +- `pair_failed` +- `auth_request` +- `auth_success` +- `auth_failed` +- `re_pair_required` +- `heartbeat` +- `heartbeat_ack` +- `status_update` +- `disconnect_notice` +- `error` + +### 3.3 Security Constraints +- Pairing code must be delivered out-of-band only +- Pairing code must not travel over Yonexus WebSocket +- Nonce length fixed at 24 random characters +- Nonce replay detection window +- Timestamp freshness validation +- Rate-limit / unsafe reconnect detection + +### 3.4 Rule Message Format +- Application messages use `${rule_identifier}::${message_content}` +- Server rewrites inbound client messages before dispatch +- Rule matching is exact-match in v1 + +--- + +## 4. Configuration Features + +### 4.1 Yonexus.Server Config +- `followerIdentifiers: string[]` +- `notifyBotToken: string` +- `adminUserId: string` +- `listenHost?: string` +- `listenPort: number` +- `publicWsUrl?: string` + +### 4.2 Yonexus.Client Config +- `mainHost: string` +- `identifier: string` +- `notifyBotToken: string` +- `adminUserId: string` + +### 4.3 Validation +- Fail startup on missing required fields +- Fail startup on invalid config shape +- Validate required split-plugin semantics per side + +--- + +## 5. Docs and Deliverables + +### Required Planning / Spec Docs +- `PLAN.md` +- `PROTOCOL.md` +- `FEAT.md` + +### Next Implementation Deliverables +- server plugin manifest +- client plugin manifest +- README for dual-plugin architecture +- implementation task breakdown +- protocol test cases +- pairing/auth failure-path test matrix + +--- + +## 6. Suggested Delivery Order + +### Phase 0 — Planning +- [x] Rewrite project direction +- [x] Define split-plugin model +- [x] Write protocol draft +- [x] Write feature checklist + +### Phase 1 — Skeleton +- [ ] Create `Yonexus.Server` plugin scaffold +- [ ] Create `Yonexus.Client` plugin scaffold +- [ ] Add config schema / manifests +- [ ] Add minimal startup hooks + +### Phase 2 — Transport +- [ ] Implement WebSocket server +- [ ] Implement WebSocket client +- [ ] Implement hello / hello_ack flow +- [ ] Implement reconnect baseline + +### Phase 3 — Pairing and Auth +- [ ] Implement keypair generation +- [ ] Implement pairing creation +- [ ] Implement Discord DM notification +- [ ] Implement pairing confirmation +- [ ] Implement secret issuance +- [ ] Implement signed auth proof validation +- [ ] Implement nonce and rate-limit protection + +### Phase 4 — Heartbeat and Messaging +- [ ] Implement heartbeat loop +- [ ] Implement server status sweep +- [ ] Implement `sendMessageToServer` +- [ ] Implement `sendMessageToClient` +- [ ] Implement rule registry and dispatch + +### Phase 5 — Hardening +- [ ] Add persistence +- [ ] Add restart recovery behavior +- [ ] Add structured errors +- [ ] Add logging/redaction +- [ ] Add integration tests +- [ ] Add operator docs + +--- + +## 7. Non-Goals + +Not in initial scope unless explicitly added later: +- direct client-to-client sockets +- multi-server topology +- distributed consensus +- queueing guarantees for offline clients +- management UI +- advanced pattern matching for rules diff --git a/PLAN.md b/PLAN.md index f0cac13..4d53cd2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -2,133 +2,183 @@ ## 1. Goal -Yonexus is an OpenClaw plugin for **cross-instance communication** between multiple OpenClaw deployments. +Yonexus is a cross-instance communication system for OpenClaw, implemented as **two separate plugins**: + +- `Yonexus.Server` +- `Yonexus.Client` + +Together they provide: +- communication between multiple OpenClaw instances +- a central WebSocket hub model +- client pairing and authentication +- heartbeat-based client liveness tracking +- rule-based message dispatch +- out-of-band pairing notification to a human administrator via Discord DM +- TypeScript interfaces for higher-level plugin/runtime integrations + +This project is no longer a role-switched single plugin. It is now explicitly split into two installable plugins with distinct responsibilities. + +--- + +## 2. Plugin Split + +## 2.1 Yonexus.Server + +`Yonexus.Server` is installed only on the main OpenClaw instance. + +Responsibilities: +- start and maintain the WebSocket server +- accept incoming client connections +- maintain the client registry +- handle pairing flow +- verify authentication proofs +- track heartbeat and connection state +- route or relay messages to connected clients +- rewrite inbound client messages before rule dispatch +- send Discord DM pairing notifications to the human administrator + +## 2.2 Yonexus.Client + +`Yonexus.Client` is installed on follower OpenClaw instances. + +Responsibilities: +- connect to the configured Yonexus server +- generate and persist local keypair on first use +- persist local client identity and secret +- perform pairing confirmation +- perform authenticated reconnect +- send periodic heartbeats +- expose client-side messaging and rule registration APIs + +--- + +## 3. Deployment Model A Yonexus network contains: -- exactly one instance with role `main` -- one or more instances with role `follower` +- exactly one OpenClaw instance running `Yonexus.Server` +- one or more OpenClaw instances running `Yonexus.Client` -The plugin provides: -- a WebSocket-based communication layer between OpenClaw instances -- pairing and identity verification for followers -- persistent follower registry and trust state on the main node -- heartbeat-based follower status tracking -- a rule-based message dispatch mechanism -- TypeScript function interfaces for other plugin/runtime code - -This project is **not** an organization/identity management plugin anymore. All prior goals are discarded. +Topology rules: +- `Yonexus.Server` must be reachable via fixed IP/domain or otherwise stable addressable endpoint +- `Yonexus.Client` instances do not need stable public IP/domain +- all `Yonexus.Client` instances connect outbound to the `Yonexus.Server` WebSocket endpoint +- no direct client-to-client communication is required in v1 +- inter-client communication, if needed, is relayed by `Yonexus.Server` --- -## 2. High-Level Architecture +## 4. Configuration Model -### 2.1 Roles - -Each OpenClaw instance running Yonexus must be configured with a `role`: -- `main` -- `follower` - -Role semantics: -- `main` is the hub/server for all Yonexus communication -- `follower` connects outbound to the `main` instance - -### 2.2 Network Topology - -- The `main` instance must expose a fixed reachable IP/domain and run a WebSocket service. -- `follower` instances do not need fixed IP/domain. -- All `follower` instances connect to the `main` WebSocket endpoint. -- No direct follower-to-follower communication is required in v1. -- Messages between followers, if needed, are relayed by `main`. - -### 2.3 Runtime Lifecycle - -- On OpenClaw gateway startup: - - if role is `main`, Yonexus starts a WebSocket server through a hook - - if role is `follower`, Yonexus starts a WebSocket client and attempts to connect to `mainHost` - ---- - -## 3. Configuration Model - -## 3.1 Common Config +## 4.1 Yonexus.Server Config ```ts -role: "main" | "follower" +followerIdentifiers: string[] +notifyBotToken: string +adminUserId: string +listenHost?: string +listenPort: number +publicWsUrl?: string ``` -## 3.2 Follower Config +Semantics: +- `followerIdentifiers`: allowlist of client identifiers permitted to pair/connect +- `notifyBotToken`: Discord bot token used to send pairing notifications +- `adminUserId`: Discord user id of the human administrator who receives pairing codes by DM +- `listenHost`: local bind host for WebSocket server +- `listenPort`: local bind port for WebSocket server +- `publicWsUrl`: optional canonical external URL advertised/documented for clients -Required when `role === "follower"`: +## 4.2 Yonexus.Client Config ```ts mainHost: string identifier: string +notifyBotToken: string +adminUserId: string ``` Semantics: -- `mainHost`: WebSocket endpoint of the main instance (`ip:port` or full URL) -- `identifier`: unique follower identity inside the Yonexus network +- `mainHost`: WebSocket endpoint of `Yonexus.Server` +- `identifier`: unique identity of this client inside the Yonexus network +- `notifyBotToken`: kept aligned with shared config expectations if future client-side notification behaviors are needed +- `adminUserId`: human administrator identity reference shared with the Yonexus system -## 3.3 Main Config +## 4.3 Validation Rules -Required when `role === "main"`: - -```ts -followerIdentifiers: string[] -``` - -Semantics: -- `followerIdentifiers`: allowlist of follower identifiers that are permitted to pair/connect - -## 3.4 Validation Rules - -### Main -- must have `role = main` +### Yonexus.Server - must provide `followerIdentifiers` -- must expose a stable/reachable IP/domain outside the plugin itself +- must provide `notifyBotToken` +- must provide `adminUserId` +- must provide `listenPort` +- must be deployed on a reachable/stable endpoint -### Follower -- must have `role = follower` +### Yonexus.Client - must provide `mainHost` - must provide `identifier` +- must provide `notifyBotToken` +- must provide `adminUserId` ### Shared -- invalid or missing role-specific fields must fail plugin initialization -- unknown follower identifiers must be rejected by `main` +- invalid or missing required fields must fail plugin initialization +- unknown client identifiers must be rejected by `Yonexus.Server` --- -## 4. Main Responsibilities +## 5. Runtime Lifecycle -The `main` instance must maintain a registry keyed by follower `identifier`. +## 5.1 Yonexus.Server Startup -Each follower record contains at minimum: +On OpenClaw gateway startup: +- initialize persistent client registry +- start WebSocket server +- register builtin protocol handlers +- register application rule registry +- start heartbeat/status sweep timer + +## 5.2 Yonexus.Client Startup + +On OpenClaw gateway startup: +- load local persisted identity, private key, and secret state +- generate keypair if absent +- connect to `mainHost` +- perform pairing or authentication flow depending on local state +- start heartbeat schedule after successful authentication +- attempt reconnect when disconnected + +--- + +## 6. Server Registry and Persistence + +`Yonexus.Server` must maintain a registry keyed by client `identifier`. + +Each client record contains at minimum: - `identifier` - `publicKey` - `secret` - pairing state - pairing expiration data +- pairing notification metadata - connection status - security counters/window data - heartbeat timestamps -- last known connection/session metadata +- last known session metadata The registry must use: -- in-memory runtime state for active operations -- persistent on-disk storage for restart survival +- in-memory runtime state for active connections and recent security windows +- persistent on-disk storage for durable trust state -### 4.1 Persistent Main Registry Model - -Proposed shape: +### 6.1 Proposed Server Record Shape ```ts -interface FollowerRecord { +interface ClientRecord { identifier: string; publicKey?: string; secret?: string; pairingStatus: "unpaired" | "pending" | "paired" | "revoked"; pairingCode?: string; pairingExpiresAt?: number; + pairingNotifiedAt?: number; + pairingNotifyStatus?: "pending" | "sent" | "failed"; status: "online" | "offline" | "unstable"; lastHeartbeatAt?: number; lastAuthenticatedAt?: number; @@ -142,87 +192,101 @@ interface FollowerRecord { } ``` -Notes: -- `recentNonces` stores only the recent nonce window needed for replay detection -- `recentHandshakeAttempts` stores timestamps for rate-limiting / unsafe reconnect detection -- actual field names can change during implementation, but these semantics must remain - --- -## 5. Pairing and Authentication Flow +## 7. Pairing and Authentication -## 5.1 First Connection: Key Generation +## 7.1 First Connection and Key Generation -When a follower connects to main for the first time: -- the follower generates a public/private key pair locally -- the private key remains only on the follower -- the public key is sent to `main` during handshake +When a client connects to the server for the first time: +- `Yonexus.Client` generates a public/private key pair locally +- the private key remains only on the client instance +- the public key is sent to `Yonexus.Server` during handshake -If `main` sees that: -- the follower identifier is allowed, and -- no valid `secret` is currently associated with that identifier +If the server sees that: +- the client identifier is allowed, and +- there is no valid `secret` currently associated with that identifier -then `main` must enter pairing flow. +then the server must enter pairing flow. -## 5.2 Pairing Flow +## 7.2 Pairing Flow -### Step A: Pairing Request -`main` responds with a pairing request containing: +### Step A: Pairing Request Creation +`Yonexus.Server` generates: - a random pairing string - an expiration time +The pairing string must **not** be sent to the client over WebSocket. + +Instead, `Yonexus.Server` uses `notifyBotToken` to send a Discord DM to `adminUserId` containing: +- the client `identifier` +- the generated `pairingCode` +- the expiration time + ### Step B: Pairing Confirmation -If the follower sends that random pairing string back to `main` before expiration: +The client must provide the pairing code back to the server before expiration. + +How the client operator obtains the pairing code is intentionally out-of-band from the Yonexus WebSocket channel. The server only trusts that the code came through some human-mediated path. + +If the client sends the correct pairing code before expiration: - pairing succeeds ### Step C: Secret Issuance After successful pairing: -- `main` generates a random `secret` -- `main` returns that `secret` to the follower -- `main` stores follower `publicKey` + `secret` -- `follower` stores private key + secret locally +- `Yonexus.Server` generates a random `secret` +- `Yonexus.Server` returns that `secret` to the client +- `Yonexus.Server` stores client `publicKey` + `secret` +- `Yonexus.Client` stores private key + secret locally + +If Discord DM delivery fails: +- pairing must not proceed +- server should mark the pairing attempt as failed or pending-error +- client must not receive a usable pairing code through the protocol channel If pairing expires before confirmation: - pairing fails -- follower must restart the pairing process +- the client must restart the pairing process -## 5.3 Reconnection Authentication Flow +## 7.3 Reconnect Authentication Flow -After pairing is complete, future follower authentication must use: +After pairing is complete, future client authentication must use: - the stored `secret` - a 24-character random nonce - current UTC Unix timestamp -The follower builds a plaintext proof payload from: +The client builds a proof payload from: - `secret` - `nonce` - `timestamp` -Concatenation order: +Logical concatenation order: ```text secret + nonce + timestamp ``` -The follower encrypts/signs this payload using its private key and sends it to `main`. +Implementation recommendation: +- use a canonical serialized object and sign its bytes rather than naive string concatenation in code -`main` verifies: -1. the follower identifier is known and paired -2. the public key matches stored state -3. decrypted/verified payload contains the correct `secret` -4. timestamp difference from current UTC time is less than 10 seconds +The client signs the proof using its private key and sends it to the server. + +The server verifies: +1. identifier is known and paired +2. public key matches stored state +3. proof contains the correct `secret` +4. timestamp difference from current time is less than 10 seconds 5. nonce does not collide with the recent nonce window 6. handshake attempts in the last 10 seconds do not exceed 10 If all checks pass: - authentication succeeds -- follower is considered authenticated for the connection/session +- the client is considered authenticated for the session If any check fails: - authentication fails -- main may downgrade/revoke trust state +- server may downgrade or revoke trust state -## 5.4 Unsafe Condition Handling +## 7.4 Unsafe Condition Handling The connection is considered unsafe and must return to pairing flow if either is true: - more than 10 handshake attempts occur within 10 seconds @@ -230,55 +294,55 @@ The connection is considered unsafe and must return to pairing flow if either is When unsafe: - existing trust state must no longer be accepted for authentication -- the follower must re-pair -- main should clear or rotate the stored `secret` -- main should reset security windows as part of re-pairing +- the client must re-pair +- server should clear or rotate the stored `secret` +- server should reset security windows as part of re-pairing --- -## 6. Heartbeat and Follower Status +## 8. Heartbeat and Client Status -The main instance must track each follower’s liveness state: +The server must track each client’s liveness state: - `online` - `unstable` - `offline` -## 6.1 Heartbeat Rules +## 8.1 Heartbeat Rules -Each follower must send a heartbeat to main every 5 minutes. +Each client must send a heartbeat to the server every 5 minutes. -## 6.2 Status Transitions +## 8.2 Status Transitions ### online -A follower is `online` when: +A client is `online` when: - it has an active authenticated WebSocket connection, and -- main has received a recent heartbeat +- the server has received a recent heartbeat ### unstable -A follower becomes `unstable` when: +A client becomes `unstable` when: - no heartbeat has been received for 7 minutes ### offline -A follower becomes `offline` when: +A client becomes `offline` when: - no heartbeat has been received for 11 minutes -When follower becomes `offline`: -- main must close/terminate the WebSocket connection for that follower +When a client becomes `offline`: +- the server must close/terminate the WebSocket connection for that client -## 6.3 Status Evaluation Strategy +## 8.3 Status Evaluation Strategy -Main should run a periodic status sweep timer to evaluate heartbeat freshness. +The server should run a periodic status sweep timer. -Recommended initial interval: +Recommended interval: - every 30 to 60 seconds --- -## 7. Messaging Model +## 9. Messaging Model Yonexus provides rule-based message dispatch over WebSocket. -## 7.1 Base Message Format +## 9.1 Base Message Format All application messages must use the format: @@ -286,21 +350,17 @@ All application messages must use the format: ${rule_identifier}::${message_content} ``` -Constraints: -- `rule_identifier` is a string token -- `message_content` is the remainder payload as string +## 9.2 Server-Side Rewriting -## 7.2 Main-Side Rewriting - -When `main` receives a message from a follower, before rule matching it must rewrite the message into: +When `Yonexus.Server` receives a message from a client, before rule matching it must rewrite the message into: ```text ${rule_identifier}::${sender_identifier}::${message_content} ``` -This ensures rule processors on `main` can identify which follower sent the message. +This ensures server-side processors can identify which client sent the message. -## 7.3 Builtin Rule Namespace +## 9.3 Builtin Rule Namespace The reserved rule identifier is: @@ -318,250 +378,202 @@ User code must not be allowed to register handlers for `builtin`. --- -## 8. Rule Registration and Dispatch +## 10. TypeScript API Surface -## 8.1 Public API +## 10.1 Yonexus.Client API ```ts -registerRule(rule: string, processor: (message: string) => unknown): void -``` - -## 8.2 Rule Format - -`rule` must use the format: - -```text -${rule_identifier} -``` - -Validation rules: -- must be non-empty -- must not contain the message delimiter sequence in invalid ways -- must not equal `builtin` - -## 8.3 Dispatch Rules - -When Yonexus receives a message over WebSocket: -- it iterates registered rules in registration order -- it finds the first matching rule -- it invokes the corresponding processor -- only the first match is used - -Clarification for implementation: -- matching should initially be exact match on `rule_identifier` -- if pattern-based matching is desired later, that must be explicitly added in a future phase - -If no rule matches: -- the message is ignored or logged as unhandled, depending on runtime policy - ---- - -## 9. TypeScript API Surface - -## 9.1 sendMessageToMain - -```ts -sendMessageToMain(message: string): Promise +sendMessageToServer(message: string): Promise ``` Rules: -- allowed only on `follower` -- calling from `main` must throw an error -- sends message to connected `main` +- sends message to connected `Yonexus.Server` - message must already conform to `${rule_identifier}::${message_content}` -## 9.2 sendMessageToFollower - -```ts -sendMessageToFollower(identifier: string, message: string): Promise -``` - -Rules: -- allowed only on `main` -- calling from `follower` must throw an error -- target follower must be known and currently connected/authenticated -- message must already conform to `${rule_identifier}::${message_content}` - -## 9.3 registerRule - ```ts registerRule(rule: string, processor: (message: string) => unknown): void ``` Rules: - rejects `builtin` -- rejects duplicate rule registration unless an explicit override mode is added later -- processors are invoked with the final received string after any main-side rewrite +- rejects duplicate rule registration unless explicit override support is added later + +## 10.2 Yonexus.Server API + +```ts +sendMessageToClient(identifier: string, message: string): Promise +``` + +Rules: +- target client must be known and currently connected/authenticated +- message must already conform to `${rule_identifier}::${message_content}` + +```ts +registerRule(rule: string, processor: (message: string) => unknown): void +``` + +Rules: +- rejects `builtin` +- rejects duplicate rule registration unless explicit override support is added later +- processors are invoked with the final received string after any server-side rewrite --- -## 10. Hooks and Runtime Integration +## 11. Hooks and Integration -## 10.1 Main Hook +## 11.1 Yonexus.Server Hooking -The plugin must register a hook so that when OpenClaw gateway starts: -- Yonexus initializes internal state -- Yonexus starts a WebSocket server -- Yonexus begins follower status sweep tasks +`Yonexus.Server` must register hooks so that when OpenClaw gateway starts: +- the WebSocket server is started +- the server registry is initialized +- builtin protocol handling is enabled +- heartbeat sweep begins -## 10.2 Follower Runtime Behavior +## 11.2 Yonexus.Client Behavior -On startup, follower should: -- load local identity/secret/private key state -- connect to `mainHost` -- perform pairing or authentication flow -- start periodic heartbeats when authenticated -- attempt reconnect when disconnected +`Yonexus.Client` must: +- connect outbound to `mainHost` +- manage local trust material +- handle pairing/authentication transitions +- emit heartbeats after authentication +- reconnect after disconnect with retry/backoff behavior -## 10.3 Persistence Requirements +--- -### Main persists: -- follower registry -- public keys -- secrets -- pairing state -- security/rate-limit windows if needed across restart, or resets them safely +## 12. Storage Strategy -### Follower persists: +## 12.1 Yonexus.Server Storage + +Server persists at minimum: - identifier -- private key -- current secret -- minimal pairing/auth state needed for reconnect +- public key +- secret +- trust state +- pairing code + expiry if pairing is pending +- pairing notification metadata +- last known status +- metadata timestamps ---- +May persist or reset on restart: +- recent nonces +- recent handshake attempts -## 11. Storage Strategy +Recommended v1: +- clear rolling security windows on restart +- keep long-lived trust records -## 11.1 Main Storage +## 12.2 Yonexus.Client Storage -Main needs a local data file for follower registry persistence. - -Suggested persisted sections: -- trusted followers -- pairing pending records -- last known status metadata -- security-related rolling records when persistence is desirable - -## 11.2 Follower Storage - -Follower needs a local secure data file for: +Client persists at minimum: +- identifier - private key - secret -- identifier -- optional last successful connection metadata +- optional last successful pair/auth metadata -## 11.3 Security Notes - -- private key must never be sent to main +Security notes: +- private key must never be sent to the server - secret must be treated as sensitive material -- storage format should support future encryption-at-rest, but plaintext local file may be acceptable in initial implementation if clearly documented as a limitation +- encryption-at-rest can be a future enhancement, but any plaintext local storage must be documented as a limitation if used initially --- -## 12. Error Handling +## 13. Error Handling -The plugin should define structured errors for at least: +Structured errors should exist for at least: - invalid configuration -- invalid role usage - unauthorized identifier - pairing required - pairing expired -- handshake verification failed +- pairing notification failure +- handshake verification failure - replay/nonce collision detected -- rate limit / unsafe handshake detected -- follower not connected +- unsafe handshake rate detected +- target client not connected - duplicate rule registration - reserved rule registration - malformed message --- -## 13. Initial Implementation Phases +## 14. Initial Implementation Phases ## Phase 0 — Protocol and Skeleton -- finalize config schema -- define persisted data models -- define protocol message types for builtin traffic -- define hook startup behavior +- finalize split-plugin configuration schema +- define persistent data models +- define builtin protocol messages +- define startup hooks for both plugins - define rule registry behavior +- define Discord DM notification flow -## Phase 1 — Main/Follower Transport MVP -- main WebSocket server startup -- follower WebSocket client startup +## Phase 1 — Transport MVP +- Yonexus.Server WebSocket server startup +- Yonexus.Client WebSocket client startup - reconnect logic -- basic builtin protocol channel -- persistent registry scaffolding +- builtin protocol channel +- persistent registry/state scaffolding ## Phase 2 — Pairing and Authentication -- follower keypair generation -- pairing request/confirmation flow +- client keypair generation +- pairing request creation +- Discord DM notification to admin user +- pairing confirmation flow - secret issuance and persistence -- signed/encrypted handshake proof verification +- signed proof verification - nonce/replay protection - unsafe-condition reset to pairing ## Phase 3 — Heartbeat and Status Tracking -- follower heartbeat sender -- main heartbeat receiver +- client heartbeat sender +- server heartbeat receiver - periodic sweep - status transitions: online / unstable / offline - forced disconnect on offline -## Phase 4 — Public APIs and Message Dispatch -- `sendMessageToMain` -- `sendMessageToFollower` +## Phase 4 — Public APIs and Dispatch +- `sendMessageToServer` +- `sendMessageToClient` - `registerRule` - first-match dispatch -- main-side sender rewrite behavior +- server-side sender rewrite behavior ## Phase 5 — Hardening and Docs - integration tests - failure-path coverage - restart recovery checks - protocol docs -- operator setup docs for main/follower deployment +- operator setup docs for server/client deployment --- -## 14. Non-Goals for Initial Version +## 15. Non-Goals for Initial Version Not required in the first version unless explicitly added later: -- direct follower-to-follower sockets -- multi-main clustering +- direct client-to-client sockets +- multi-server clustering - distributed consensus - message ordering guarantees across reconnects -- end-to-end application payload encryption beyond the handshake/authentication requirements -- UI management panel +- end-to-end payload encryption beyond the pairing/authentication requirements +- management UI --- -## 15. Open Questions To Confirm Later +## 16. Open Questions To Confirm Later -These should be resolved before implementation starts: - -1. Is the handshake primitive meant to be: - - asymmetric encryption with private/public key, or - - digital signature with verification by public key? - - Recommended: **signature**, not “private-key encryption” wording. - -2. Should `mainHost` accept only full WebSocket URLs (`ws://` / `wss://`) or also raw `ip:port` strings? - -3. Should pairing require explicit operator approval on main, or is allowlist membership enough for automatic pairing? - -4. On unsafe condition, should the old public key be retained or must the follower generate a brand-new keypair? - -5. Should offline followers be allowed queued outbound messages from main, or should send fail immediately? - -6. Are rule identifiers exact strings only, or should regex/prefix matching exist in future? +1. Exact signing algorithm: + - Ed25519 is a strong default candidate +2. Should `mainHost` accept only full WebSocket URLs or also raw `ip:port` strings? +3. Is human code relay sufficient for v1 pairing, or should admin approve/deny controls be added later? +4. On unsafe condition, should the old public key be retained or should the client generate a new keypair? +5. Should offline clients support queued outbound messages from server, or should sends fail immediately? +6. Are rule identifiers exact strings only, or should regex/prefix matching exist later? --- -## 16. Immediate Next Deliverables +## 17. Immediate Next Deliverables After this plan, the next files to create should be: - `FEAT.md` — feature checklist derived from this plan -- `README.md` — concise operator/developer overview -- `plugin.json` — plugin config schema and entry declaration -- protocol notes for builtin messages -- implementation task breakdown \ No newline at end of file +- `README.md` — concise system overview for both plugins +- `plugin.server.json` or equivalent server plugin manifest +- `plugin.client.json` or equivalent client plugin manifest +- implementation task breakdown diff --git a/PROTOCOL.md b/PROTOCOL.md index 078ed21..4103dd7 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -1,76 +1,71 @@ # Yonexus Protocol Specification -Version: draft v0.1 +Version: draft v0.3 Status: planning --- ## 1. Purpose -This document defines the **Yonexus built-in communication protocol** used between OpenClaw instances. +This document defines the built-in Yonexus communication protocol used between: +- `Yonexus.Server` +- `Yonexus.Client` -Yonexus has two roles: -- `main`: the central WebSocket server/hub -- `follower`: an outbound WebSocket client connected to `main` - -This protocol covers only the **built-in/system channel** required for: +The protocol covers: - connection setup - pairing - authentication - heartbeat - status/lifecycle events - protocol-level errors +- transport of application rule messages over the same WebSocket channel -Application-level business messages are transported separately through the Yonexus rule dispatch layer, but they still use the same WebSocket connection. +Important security rule: +- pairing codes must **not** be delivered to `Yonexus.Client` over the Yonexus WebSocket channel +- pairing codes must be delivered out-of-band to a human administrator via Discord DM --- ## 2. Transport -## 2.1 Protocol Transport - Transport is WebSocket. -- `main` listens as WebSocket server -- `follower` connects as WebSocket client -- all protocol frames are UTF-8 text messages in the first version +- `Yonexus.Server` listens as WebSocket server +- `Yonexus.Client` connects as WebSocket client +- protocol frames are UTF-8 text in v1 +- binary frames are not required in v1 -Binary frames are not required in v1. +Client connects to configured `mainHost`, which may be: +- `ws://host:port/path` +- `wss://host:port/path` +- or raw `host:port` if normalized by implementation -## 2.2 Endpoint - -The `follower` connects to `mainHost`, which may be configured as: -- full URL: `ws://host:port/path` or `wss://host:port/path` -- or raw `host:port` if implementation normalizes it - -Recommended canonical configuration for docs and production: +Recommended canonical config: - prefer full WebSocket URL --- ## 3. Message Categories -Yonexus messages over WebSocket are split into two categories: +## 3.1 Builtin Protocol Messages -### 3.1 Builtin Protocol Messages - -Builtin/system messages always use the reserved rule identifier: +Builtin messages always use: ```text builtin::${json_payload} ``` -User code must never register a handler for `builtin`. +`builtin` is reserved and must not be registered by user code. -### 3.2 Application Rule Messages +## 3.2 Application Rule Messages -Application messages use the normal format: +Application messages use: ```text ${rule_identifier}::${message_content} ``` -When received by `main` from a follower, before dispatch they are rewritten to: +When `Yonexus.Server` receives a rule message from a client, it must rewrite it before dispatch to: ```text ${rule_identifier}::${sender_identifier}::${message_content} @@ -78,56 +73,43 @@ ${rule_identifier}::${sender_identifier}::${message_content} --- -## 4. Builtin Message Envelope +## 4. Builtin Envelope -Builtin messages use this wire format: +Builtin wire format: ```text builtin::{JSON} ``` -Where the JSON payload has this envelope shape: +Canonical envelope: ```ts interface BuiltinEnvelope { type: string; requestId?: string; - timestamp?: number; + timestamp?: number; // UTC unix seconds payload?: Record; } ``` -Field semantics: -- `type`: builtin message type -- `requestId`: optional correlation id for request/response pairs -- `timestamp`: sender-side UTC unix timestamp in seconds or milliseconds (must be standardized in implementation) -- `payload`: type-specific object - -## 4.1 Timestamp Unit - -To avoid ambiguity, protocol implementation should standardize on: - -```text -UTC Unix timestamp in seconds -``` - -If milliseconds are ever used internally, they must not leak into protocol payloads without explicit versioning. +Rules: +- `timestamp` uses UTC unix seconds +- `requestId` is used for correlation where needed +- `payload` content depends on `type` --- ## 5. Builtin Message Types -The first protocol version should define these builtin message types. - -## 5.1 Session / Connection +## 5.1 Session Setup ### `hello` -Sent by follower immediately after WebSocket connection opens. +Sent by `Yonexus.Client` immediately after WebSocket connection opens. Purpose: -- identify follower intent - declare identifier -- advertise available auth material/state +- advertise current auth material state +- announce protocol version Example: @@ -137,7 +119,7 @@ builtin::{ "requestId":"req_001", "timestamp":1711886400, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "hasSecret":true, "hasKeyPair":true, "publicKey":"", @@ -146,22 +128,14 @@ builtin::{ } ``` -Notes: -- `publicKey` may be included during first connection and may also be re-sent during re-pair -- `hasSecret` indicates whether follower believes it already holds a valid secret -- `hasKeyPair` indicates whether follower has generated a key pair locally - ### `hello_ack` -Sent by main in response to `hello`. +Sent by `Yonexus.Server` in response to `hello`. -Purpose: -- acknowledge identifier -- indicate required next step - -Possible next actions: +Possible `nextAction` values: - `pair_required` - `auth_required` - `rejected` +- `waiting_pair_confirm` Example: @@ -171,7 +145,7 @@ builtin::{ "requestId":"req_001", "timestamp":1711886401, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "nextAction":"pair_required" } } @@ -181,12 +155,47 @@ builtin::{ ## 5.2 Pairing Flow +## 5.2.1 Pairing Design Rule + +`Yonexus.Server` must never send the actual `pairingCode` to `Yonexus.Client` through the Yonexus WebSocket channel. + +The pairing code must be delivered to the configured human administrator using: +- `notifyBotToken` +- `adminUserId` + +Specifically: +- `Yonexus.Server` sends a Discord DM to the configured admin user +- the DM contains the client identifier and pairing code +- the human relays the code to the client side by some trusted out-of-band path + +## 5.2.2 Pairing Request Creation + +When pairing is required, `Yonexus.Server` generates: +- `pairingCode` +- `expiresAt` +- `ttlSeconds` + +The admin DM must include at minimum: +- `identifier` +- `pairingCode` +- `expiresAt` or TTL + +Example DM body: + +```text +Yonexus pairing request +identifier: client-a +pairingCode: ABCD-1234-XYZ +expiresAt: 1711886702 +``` + ### `pair_request` -Sent by main when follower needs pairing. +Sent by `Yonexus.Server` to `Yonexus.Client` after pairing starts. Purpose: -- start pairing challenge -- deliver pairing code and expiry +- indicate that pairing has started +- indicate whether admin notification succeeded +- provide expiry metadata without revealing the code Example: @@ -196,20 +205,26 @@ builtin::{ "requestId":"req_002", "timestamp":1711886402, "payload":{ - "identifier":"follower-a", - "pairingCode":"ABCD-1234-XYZ", + "identifier":"client-a", "expiresAt":1711886702, "ttlSeconds":300, - "publicKeyAccepted":true + "adminNotification":"sent", + "codeDelivery":"out_of_band" } } ``` +Allowed `adminNotification` values: +- `sent` +- `failed` + +If notification failed, pairing must not proceed until retried successfully. + ### `pair_confirm` -Sent by follower to confirm pairing. +Sent by `Yonexus.Client` to confirm pairing. Purpose: -- prove receipt of pairing code before expiry +- submit the pairing code obtained out-of-band Example: @@ -219,14 +234,14 @@ builtin::{ "requestId":"req_002", "timestamp":1711886410, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "pairingCode":"ABCD-1234-XYZ" } } ``` ### `pair_success` -Sent by main after successful pairing. +Sent by `Yonexus.Server` after successful pairing. Purpose: - return generated secret @@ -240,7 +255,7 @@ builtin::{ "requestId":"req_002", "timestamp":1711886411, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "secret":"", "pairedAt":1711886411 } @@ -248,13 +263,14 @@ builtin::{ ``` ### `pair_failed` -Sent by main if pairing fails. +Sent by `Yonexus.Server` when pairing fails. -Example reasons: -- expired -- invalid_code -- identifier_not_allowed -- internal_error +Typical reasons: +- `expired` +- `invalid_code` +- `identifier_not_allowed` +- `admin_notification_failed` +- `internal_error` Example: @@ -264,7 +280,7 @@ builtin::{ "requestId":"req_002", "timestamp":1711886710, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "reason":"expired" } } @@ -274,25 +290,24 @@ builtin::{ ## 5.3 Authentication Flow -After pairing, reconnect authentication uses the stored `secret`, nonce, timestamp, and follower private key. +After pairing, reconnect authentication uses: +- stored `secret` +- 24-character random nonce +- current UTC unix timestamp +- client private key -## 5.3.1 Authentication Payload Construction +## 5.3.1 Proof Construction -Follower constructs plaintext proof data as: +Logical proof content: ```text secret + nonce + timestamp ``` -Where: -- `secret`: the current shared secret issued by main -- `nonce`: 24 random characters -- `timestamp`: current UTC Unix timestamp in seconds +Implementation recommendation: +- use canonical serialized object bytes for signing -Recommended future improvement: -- use a canonical delimiter or JSON encoding before signing to avoid ambiguity - -For v1 planning, the required logical content is unchanged, but implementation should prefer a canonical serialized object like: +Recommended logical form: ```json { @@ -302,20 +317,14 @@ For v1 planning, the required logical content is unchanged, but implementation s } ``` -and then sign the serialized bytes. - ## 5.3.2 Signature Primitive -The requirement says the follower uses the private key to encrypt/sign the proof. - -For implementation, the recommended primitive is: -- **digital signature using the private key** -- verified by main using the stored public key - -This should replace any literal “private-key encryption” wording in implementation docs. +Recommended primitive: +- digital signature using client private key +- verification using stored client public key on server ### `auth_request` -Sent by follower to authenticate after pairing. +Sent by `Yonexus.Client` after pairing or on reconnect. Example: @@ -325,7 +334,7 @@ builtin::{ "requestId":"req_003", "timestamp":1711886500, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "nonce":"RANDOM24CHARACTERSTRINGX", "proofTimestamp":1711886500, "signature":"", @@ -334,19 +343,19 @@ builtin::{ } ``` -Validation performed by main: +Server validation: 1. identifier is allowlisted 2. identifier exists in registry -3. follower is in paired state +3. client is in paired state 4. public key matches expected key if provided -5. signature verifies successfully against canonical proof payload +5. signature verifies successfully 6. proof contains correct secret 7. `abs(now - proofTimestamp) < 10` 8. nonce has not appeared in recent nonce window 9. handshake attempts in last 10 seconds do not exceed 10 ### `auth_success` -Sent by main on successful authentication. +Sent by `Yonexus.Server` on success. Example: @@ -356,7 +365,7 @@ builtin::{ "requestId":"req_003", "timestamp":1711886501, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "authenticatedAt":1711886501, "status":"online" } @@ -364,36 +373,21 @@ builtin::{ ``` ### `auth_failed` -Sent by main if authentication fails. +Sent by `Yonexus.Server` on auth failure. -Allowed failure reasons include: -- unknown_identifier -- not_paired -- invalid_signature -- invalid_secret -- stale_timestamp -- future_timestamp -- nonce_collision -- rate_limited -- re_pair_required - -Example: - -```text -builtin::{ - "type":"auth_failed", - "requestId":"req_003", - "timestamp":1711886501, - "payload":{ - "identifier":"follower-a", - "reason":"stale_timestamp", - "rePairRequired":false - } -} -``` +Allowed reasons include: +- `unknown_identifier` +- `not_paired` +- `invalid_signature` +- `invalid_secret` +- `stale_timestamp` +- `future_timestamp` +- `nonce_collision` +- `rate_limited` +- `re_pair_required` ### `re_pair_required` -Sent by main when unsafe conditions or trust reset require full pairing again. +Sent by `Yonexus.Server` when trust state must be reset. Example: @@ -403,7 +397,7 @@ builtin::{ "requestId":"req_004", "timestamp":1711886510, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "reason":"nonce_collision" } } @@ -414,7 +408,7 @@ builtin::{ ## 5.4 Heartbeat ### `heartbeat` -Sent by follower every 5 minutes after authentication. +Sent by `Yonexus.Client` every 5 minutes after authentication. Example: @@ -423,18 +417,14 @@ builtin::{ "type":"heartbeat", "timestamp":1711886800, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "status":"alive" } } ``` ### `heartbeat_ack` -Optional response by main. - -Purpose: -- confirm receipt -- provide server time or status hints if desired +Optional response by `Yonexus.Server`. Example: @@ -443,7 +433,7 @@ builtin::{ "type":"heartbeat_ack", "timestamp":1711886801, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "status":"online" } } @@ -454,7 +444,7 @@ builtin::{ ## 5.5 Status / Lifecycle Notifications ### `status_update` -Sent by main when follower state changes. +Sent by `Yonexus.Server` when client state changes. Example: @@ -463,7 +453,7 @@ builtin::{ "type":"status_update", "timestamp":1711887220, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "status":"unstable", "reason":"heartbeat_timeout_7m" } @@ -471,7 +461,7 @@ builtin::{ ``` ### `disconnect_notice` -Sent before main deliberately closes a follower connection. +Sent by `Yonexus.Server` before deliberate close. Example: @@ -480,7 +470,7 @@ builtin::{ "type":"disconnect_notice", "timestamp":1711887460, "payload":{ - "identifier":"follower-a", + "identifier":"client-a", "reason":"heartbeat_timeout_11m" } } @@ -491,21 +481,7 @@ builtin::{ ## 5.6 Errors ### `error` -Generic protocol-level error envelope. - -Example: - -```text -builtin::{ - "type":"error", - "requestId":"req_999", - "timestamp":1711887000, - "payload":{ - "code":"MALFORMED_MESSAGE", - "message":"missing type field" - } -} -``` +Generic protocol-level error. Recommended builtin error codes: - `MALFORMED_MESSAGE` @@ -513,20 +489,21 @@ Recommended builtin error codes: - `IDENTIFIER_NOT_ALLOWED` - `PAIRING_REQUIRED` - `PAIRING_EXPIRED` +- `ADMIN_NOTIFICATION_FAILED` - `AUTH_FAILED` - `NONCE_COLLISION` - `RATE_LIMITED` - `RE_PAIR_REQUIRED` -- `FOLLOWER_OFFLINE` +- `CLIENT_OFFLINE` - `INTERNAL_ERROR` --- -## 6. Builtin State Machines +## 6. State Machines -## 6.1 Follower State Machine +## 6.1 Client State Machine -Suggested follower states: +Suggested `Yonexus.Client` states: - `idle` - `connecting` - `connected` @@ -553,7 +530,7 @@ pairing_required -> authenticated authenticated - -> reconnecting (on socket close) + -> reconnecting -> connecting ``` @@ -563,25 +540,19 @@ On `re_pair_required`: authenticated | authenticating -> pairing_required ``` -## 6.2 Main-Side Follower Trust State +## 6.2 Server-Side Client State -Per follower trust state: +Per client trust state: - `unpaired` - `pending` - `paired` - `revoked` -Per follower liveness state: +Per client liveness state: - `online` - `unstable` - `offline` -These are related but not identical. - -Example: -- follower may be `paired` + `offline` -- follower may be `pending` + `offline` - --- ## 7. Security Windows and Replay Protection @@ -590,31 +561,31 @@ Example: Nonce rules: - exactly 24 random characters -- generated fresh for each auth attempt -- must not repeat within the recent security window +- fresh per auth attempt +- must not repeat within recent security window ## 7.2 Recent Nonce Window -Main stores for each follower: +Server stores for each client: - the last 10 nonces seen within the recent validity window -If a nonce collides with the recent stored set: -- authentication must fail -- main must mark the situation unsafe -- follower must re-pair +If a nonce collides: +- authentication fails +- server marks condition unsafe +- client must re-pair ## 7.3 Handshake Attempt Window -Main stores recent handshake attempt timestamps for each follower. +Server stores recent handshake attempt timestamps. If more than 10 handshake attempts occur within 10 seconds: -- authentication must fail -- main must mark situation unsafe -- follower must re-pair +- authentication fails +- server marks condition unsafe +- client must re-pair ## 7.4 Time Drift Validation -Main validates: +Server validates: ```text abs(current_utc_unix_time - proofTimestamp) < 10 @@ -622,16 +593,11 @@ abs(current_utc_unix_time - proofTimestamp) < 10 If validation fails: - auth fails -- no successful session is established - -Implementation note: -- use server time only on main as source of truth +- no session is established --- -## 8. Rule Message Dispatch Semantics - -## 8.1 Message Format +## 8. Rule Message Dispatch All non-builtin messages use: @@ -639,24 +605,18 @@ All non-builtin messages use: ${rule_identifier}::${message_content} ``` -### Examples - -Follower to main: +Client to server example: ```text chat_sync::{"conversationId":"abc","body":"hello"} ``` -Main rewrites before matching: +Server rewrites before matching: ```text -chat_sync::follower-a::{"conversationId":"abc","body":"hello"} +chat_sync::client-a::{"conversationId":"abc","body":"hello"} ``` -## 8.2 Rule Matching - -Initial rule matching should be exact string match against `rule_identifier`. - Dispatch algorithm: 1. parse first delimiter section as `rule_identifier` 2. if `rule_identifier === builtin`, route to builtin protocol handler @@ -664,52 +624,37 @@ Dispatch algorithm: 4. invoke the first exact match 5. ignore/log if no match is found -## 8.3 Processor Input - -Processor receives the final message string exactly as seen after protocol-level rewrite. - -This means: -- on follower, processor input remains `${rule_identifier}::${message_content}` -- on main for follower-originated messages, processor input becomes `${rule_identifier}::${sender_identifier}::${message_content}` +Processor input: +- on client: `${rule_identifier}::${message_content}` +- on server for client-originated messages: `${rule_identifier}::${sender_identifier}::${message_content}` --- ## 9. Connection Rules -## 9.1 Allowed Connection Policy - -Main should reject connection attempts when: +Server should reject connection attempts when: - identifier is absent -- identifier is not in configured `followerIdentifiers` +- identifier is not in configured allowlist - protocol version is unsupported -- builtin hello/auth payload is malformed - -## 9.2 One Active Connection Per Identifier +- hello/auth payload is malformed Recommended v1 policy: -- only one active authenticated connection per follower identifier - -If a second connection for the same identifier appears: -- main may reject the new one, or -- terminate the previous one and accept the new one - -This behavior must be chosen explicitly in implementation. - -Recommended default: -- terminate old, accept new after successful auth +- only one active authenticated connection per client identifier +- terminate old connection and accept new one after successful auth --- ## 10. Persistence Semantics -## 10.1 Main Persists +## 10.1 Yonexus.Server Persists At minimum: - identifier - public key - secret - trust state -- pairing code + expiry if pairing is pending +- pairing code + expiry if pending +- pairing notification metadata - last known liveness status - metadata timestamps @@ -721,36 +666,29 @@ Recommended v1: - clear rolling security windows on restart - keep long-lived trust records -## 10.2 Follower Persists +## 10.2 Yonexus.Client Persists At minimum: - identifier - private key - secret -- protocol version if useful -- last successful pair/auth metadata if useful +- optional last successful pair/auth metadata --- ## 11. Versioning -Protocol payloads should include a protocol version during `hello`. +Protocol version is advertised during `hello`. -Initial protocol version: +Initial version: ```text 1 ``` -If main does not support the provided version, it should respond with: -- `error` -- code: `UNSUPPORTED_PROTOCOL_VERSION` - --- -## 12. Recommended Canonical JSON Shapes - -To reduce ambiguity during implementation, these payload models are recommended. +## 12. Canonical JSON Shapes ```ts interface HelloPayload { @@ -763,10 +701,10 @@ interface HelloPayload { interface PairRequestPayload { identifier: string; - pairingCode: string; expiresAt: number; ttlSeconds: number; - publicKeyAccepted: boolean; + adminNotification: "sent" | "failed"; + codeDelivery: "out_of_band"; } interface PairConfirmPayload { @@ -796,52 +734,55 @@ interface HeartbeatPayload { --- -## 13. Example End-to-End Flows +## 13. Example Flows ## 13.1 First-Time Pairing Flow ```text -Follower connects WS -Follower -> builtin::hello -Main -> builtin::hello_ack(nextAction=pair_required) -Main -> builtin::pair_request(pairingCode, expiresAt) -Follower -> builtin::pair_confirm(pairingCode) -Main -> builtin::pair_success(secret) -Follower stores secret -Follower -> builtin::auth_request(signature over secret+nonce+timestamp) -Main -> builtin::auth_success -Follower enters authenticated state +Client connects WS +Client -> builtin::hello +Server sends Discord DM to configured admin with identifier + pairingCode +Server -> builtin::hello_ack(nextAction=pair_required) +Server -> builtin::pair_request(expiresAt, adminNotification=sent, codeDelivery=out_of_band) +Human reads DM and relays pairingCode to client side +Client -> builtin::pair_confirm(pairingCode) +Server -> builtin::pair_success(secret) +Client stores secret +Client -> builtin::auth_request(signature over secret+nonce+timestamp) +Server -> builtin::auth_success +Client enters authenticated state ``` ## 13.2 Normal Reconnect Flow ```text -Follower connects WS -Follower -> builtin::hello(hasSecret=true) -Main -> builtin::hello_ack(nextAction=auth_required) -Follower -> builtin::auth_request(...) -Main -> builtin::auth_success -Follower begins heartbeat schedule +Client connects WS +Client -> builtin::hello(hasSecret=true) +Server -> builtin::hello_ack(nextAction=auth_required) +Client -> builtin::auth_request(...) +Server -> builtin::auth_success +Client begins heartbeat schedule ``` ## 13.3 Unsafe Replay / Collision Flow ```text -Follower -> builtin::auth_request(nonce collision) -Main detects unsafe condition -Main -> builtin::re_pair_required(reason=nonce_collision) -Main invalidates existing secret/session trust -Follower returns to pairing_required state +Client -> builtin::auth_request(nonce collision) +Server detects unsafe condition +Server -> builtin::re_pair_required(reason=nonce_collision) +Server invalidates existing secret/session trust +Server sends fresh Discord DM pairing notification on next pairing start +Client returns to pairing_required state ``` ## 13.4 Heartbeat Timeout Flow ```text -Follower authenticated -No heartbeat for 7 min -> main marks unstable -No heartbeat for 11 min -> main marks offline -Main -> builtin::disconnect_notice -Main closes WS connection +Client authenticated +No heartbeat for 7 min -> server marks unstable +No heartbeat for 11 min -> server marks offline +Server -> builtin::disconnect_notice +Server closes WS connection ``` --- @@ -850,55 +791,41 @@ Main closes WS connection ## 14.1 Parsing -Because the top-level wire format is string-based with `::` delimiters: +Because the wire format is string-based with `::` delimiters: - only the first delimiter split should determine the `rule_identifier` -- for `builtin`, the remainder should be treated as JSON string and parsed once +- for `builtin`, the remainder is parsed as JSON once - message content itself may contain `::`, so avoid naive full split logic -## 14.2 Payload Encoding +## 14.2 Discord DM Notification -Recommended message content for application rules: -- JSON string payloads where applicable -- but Yonexus itself only requires string content +When pairing starts on `Yonexus.Server`: +- use configured `notifyBotToken` +- send DM to `adminUserId` +- include only required pairing data +- if DM send fails, surface pairing notification failure -## 14.3 Logging - -Main should log at least: -- connection open/close -- hello received -- pairing created/succeeded/expired/failed -- auth success/failure -- nonce collisions -- handshake rate-limit triggers -- status transitions -- unhandled rule messages - -Sensitive values must never be logged in plaintext: +Sensitive values that must never be logged in plaintext: - `secret` - private key - raw proof material -- full signature verification internals unless safely redacted --- ## 15. Open Clarifications -Before implementation, these should be finalized: - -1. Exact signing algorithm: - - Ed25519 is a strong default candidate +1. Exact signing algorithm: Ed25519 is a strong default candidate 2. Secret length and generation requirements 3. Pairing code format and length -4. Whether `pair_request` should require operator confirmation or stay automatic for allowlisted identifiers -5. Whether `heartbeat_ack` is mandatory or optional -6. Whether follower should auto-reconnect with backoff strategy after disconnect -7. Whether duplicate active connections should replace old sessions or be rejected +4. Is human code relay enough for v1, or should admin approve/deny controls be added later? +5. Should `heartbeat_ack` be mandatory or optional? +6. Should client reconnect use exponential backoff? +7. Should duplicate active connections replace old sessions or be rejected in stricter modes? --- -## 16. Summary of Reserved Builtin Types +## 16. Reserved Builtin Types -Current reserved builtin `type` values: +Reserved builtin `type` values: - `hello` - `hello_ack` - `pair_request` @@ -915,4 +842,4 @@ Current reserved builtin `type` values: - `disconnect_notice` - `error` -These names are reserved by Yonexus and must not be repurposed by user rules. \ No newline at end of file +These names are reserved by Yonexus and must not be repurposed by user rules.