Compare commits
14 Commits
220ec85e6f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d01ba40b0 | |||
| 2309425cf6 | |||
| f830012dbb | |||
| 26135a9659 | |||
| 60a11686cc | |||
| 2a60031ad5 | |||
| eba5f7535d | |||
| 64dac56e37 | |||
| 95dc06453a | |||
| eb7a6248f8 | |||
| 673d1bcb69 | |||
| 488fc29a89 | |||
| 1d270110b0 | |||
| 83e02829e7 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
data/*.json
|
||||
!data/.gitkeep
|
||||
9
.gitmodules
vendored
Normal file
9
.gitmodules
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
[submodule "Yonexus.Server"]
|
||||
path = Yonexus.Server
|
||||
url = https://git.hangman-lab.top/nav/Yonexus.Server.git
|
||||
[submodule "Yonexus.Client"]
|
||||
path = Yonexus.Client
|
||||
url = https://git.hangman-lab.top/nav/Yonexus.Client.git
|
||||
[submodule "Yonexus.Protocol"]
|
||||
path = Yonexus.Protocol
|
||||
url = https://git.hangman-lab.top/nav/Yonexus.Protocol.git
|
||||
@@ -1,54 +0,0 @@
|
||||
# Yonexus — AGENT_TASKS
|
||||
|
||||
> 目标:将插件拆解为可执行任务(按阶段/优先级)。
|
||||
|
||||
## Phase 0 — 基础准备(P0)
|
||||
- [x] 明确插件运行环境/依赖(OpenClaw 版本、Node 版本)
|
||||
- [x] 定义最终配置文件格式(schema + permissions + registrars)
|
||||
- [x] 统一 ID 规则(org/dept/team/agent)
|
||||
|
||||
## Phase 1 — MVP 核心(P0)
|
||||
### 数据与存储
|
||||
- [x] 设计数据模型(Org/Dept/Team/Agent/Identity/Supervisor)
|
||||
- [x] 实现 in-memory store + JSON 持久化
|
||||
- [x] 定义 CRUD API
|
||||
|
||||
### 权限系统
|
||||
- [x] 实现权限角色(Org Admin / Dept Admin / Team Lead / Agent)
|
||||
- [x] 实现权限校验函数 authorize(action, actor, scope)
|
||||
- [x] 实现 registrars 白名单(禁止自注册)
|
||||
|
||||
### 工具/API
|
||||
- [x] create_department
|
||||
- [x] create_team
|
||||
- [x] register_agent
|
||||
- [x] assign_identity
|
||||
- [x] set_supervisor
|
||||
- [x] whoami
|
||||
- [x] query_agents
|
||||
|
||||
### Query DSL
|
||||
- [x] filters/op 解析(eq / contains / regex)
|
||||
- [x] schema queryable 字段约束
|
||||
- [x] pagination(limit/offset)
|
||||
|
||||
### Scope Memory
|
||||
- [x] scope_memory.put(scopeId, text, metadata)
|
||||
- [x] scope_memory.search(scopeId, query, limit)
|
||||
- [x] 兼容 memory-lancedb-pro
|
||||
|
||||
## Phase 2 — v1 增强(P1)
|
||||
- [x] 模糊/正则性能优化(索引/缓存)
|
||||
- [x] 管理命令与校验(重命名/删除/迁移)
|
||||
- [x] 完善错误码与审计日志
|
||||
- [x] 增加导入/导出工具
|
||||
|
||||
## Phase 3 — 体验与文档(P1)
|
||||
- [x] README(安装/配置/示例)
|
||||
- [x] 示例数据集与演示脚本
|
||||
- [x] 安装脚本完善(build + copy 到 dist/yonexus)
|
||||
|
||||
## Risk & Notes
|
||||
- 结构数据不进 memory_store(只做 scope 共享记忆)
|
||||
- queryable 字段必须严格按 schema 控制
|
||||
- supervisor 关系不隐含权限
|
||||
351
ARCHITECTURE.md
Normal file
351
ARCHITECTURE.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# Yonexus — Architecture Overview
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Yonexus is a cross-instance communication system for OpenClaw.
|
||||
|
||||
The repository `Yonexus` is the **umbrella/specification repository** for the system. It contains:
|
||||
- high-level planning
|
||||
- architecture documents
|
||||
- references to implementation repositories as git submodules
|
||||
|
||||
Yonexus is implemented as three repositories:
|
||||
- `Yonexus.Server` — central hub plugin
|
||||
- `Yonexus.Client` — client plugin
|
||||
- `Yonexus.Protocol` — shared protocol specification, referenced as a submodule by both
|
||||
|
||||
---
|
||||
|
||||
## 2. Repository Roles
|
||||
|
||||
## 2.1 `Yonexus` (umbrella repo)
|
||||
|
||||
Purpose:
|
||||
- system-level planning
|
||||
- architecture documents
|
||||
- cross-cutting decisions that apply to both server and client
|
||||
- coordination of sub-repositories via git submodules
|
||||
|
||||
This repository should contain:
|
||||
- top-level planning docs
|
||||
- architecture overview
|
||||
- feature checklists
|
||||
- cross-cutting design rationale
|
||||
|
||||
It references:
|
||||
- `Yonexus.Server` (submodule)
|
||||
- `Yonexus.Client` (submodule)
|
||||
- `Yonexus.Protocol` (submodule)
|
||||
|
||||
## 2.2 `Yonexus.Protocol`
|
||||
|
||||
Purpose:
|
||||
- protocol specification (PROTOCOL.md)
|
||||
- canonical JSON shape references
|
||||
- shared type definitions (planned)
|
||||
|
||||
Referenced as a submodule by:
|
||||
- `Yonexus.Server/protocol`
|
||||
- `Yonexus.Client/protocol`
|
||||
|
||||
This is the **single source of truth** for the Yonexus protocol. Both server and client implementations must conform to the protocol defined here.
|
||||
|
||||
## 2.3 `Yonexus.Server`
|
||||
|
||||
Purpose:
|
||||
- implementation of the central hub/server plugin
|
||||
- server-side connection management
|
||||
- server-side pairing/authentication/state tracking
|
||||
- server-side dispatch and routing behavior
|
||||
|
||||
Contains:
|
||||
- `protocol/` — submodule pointing to `Yonexus.Protocol`
|
||||
- `PLAN.md`
|
||||
- implementation code
|
||||
|
||||
## 2.4 `Yonexus.Client`
|
||||
|
||||
Purpose:
|
||||
- implementation of the client plugin
|
||||
- outbound connection to `Yonexus.Server`
|
||||
- local identity/keypair/secret management
|
||||
- client-side pairing confirmation and authenticated reconnect
|
||||
- client-side heartbeat and message sending
|
||||
|
||||
Contains:
|
||||
- `protocol/` — submodule pointing to `Yonexus.Protocol`
|
||||
- `PLAN.md`
|
||||
- implementation code
|
||||
|
||||
---
|
||||
|
||||
## 3. Repository Graph
|
||||
|
||||
```
|
||||
Yonexus (umbrella)
|
||||
├── Yonexus.Protocol (submodule)
|
||||
├── Yonexus.Server (submodule)
|
||||
│ └── protocol/ (nested submodule -> Yonexus.Protocol)
|
||||
└── Yonexus.Client (submodule)
|
||||
└── protocol/ (nested submodule -> Yonexus.Protocol)
|
||||
```
|
||||
|
||||
Policy:
|
||||
- protocol changes are always committed to `Yonexus.Protocol` first
|
||||
- `Yonexus.Server` and `Yonexus.Client` update their `protocol/` submodule ref after protocol version is stable
|
||||
- umbrella `Yonexus` updates its submodule refs after server/client have stable versions
|
||||
|
||||
---
|
||||
|
||||
## 4. System Topology
|
||||
|
||||
A Yonexus deployment contains:
|
||||
- exactly one `Yonexus.Server` instance
|
||||
- one or more `Yonexus.Client` instances
|
||||
|
||||
Topology assumptions:
|
||||
- `Yonexus.Server` runs on an OpenClaw instance reachable at a stable address
|
||||
- each `Yonexus.Client` connects outbound to the server
|
||||
- clients do not directly connect to each other in v1
|
||||
- cross-client coordination is relayed through the server
|
||||
|
||||
Visual model:
|
||||
|
||||
```
|
||||
Yonexus.Client A --->
|
||||
\
|
||||
Yonexus.Client B ----> Yonexus.Server
|
||||
/
|
||||
Yonexus.Client C --->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Shared vs Split Responsibilities
|
||||
|
||||
## 5.1 Yonexus.Protocol — Shared
|
||||
|
||||
These belong to the protocol repo and apply to both plugins:
|
||||
- protocol format and message categories
|
||||
- builtin message types and their semantics
|
||||
- pairing security model
|
||||
- nonce/timestamp validation rules
|
||||
- heartbeat timing rules
|
||||
- message rewrite rules
|
||||
- reserved rule namespace (`builtin`)
|
||||
- canonical JSON shapes
|
||||
- naming and terminology
|
||||
|
||||
## 5.2 Server-Only Concerns (Yonexus.Server)
|
||||
|
||||
These belong in `Yonexus.Server`:
|
||||
- WebSocket server startup
|
||||
- listen host/port config
|
||||
- client registry persistence
|
||||
- public key / secret storage
|
||||
- pairing code generation
|
||||
- Discord DM notification to admin
|
||||
- auth proof verification
|
||||
- liveness status tracking
|
||||
- client message rewriting and dispatch on server side
|
||||
- sending messages to connected clients
|
||||
|
||||
## 5.3 Client-Only Concerns (Yonexus.Client)
|
||||
|
||||
These belong in `Yonexus.Client`:
|
||||
- WebSocket client connection management
|
||||
- reconnect/backoff logic
|
||||
- local keypair generation
|
||||
- local secret persistence
|
||||
- pairing code submission
|
||||
- auth proof construction/signing
|
||||
- heartbeat sending
|
||||
- sending messages to server
|
||||
- receiving server messages and local dispatch
|
||||
|
||||
---
|
||||
|
||||
## 6. Communication Model
|
||||
|
||||
## 6.1 Transport
|
||||
|
||||
Transport is WebSocket.
|
||||
|
||||
- `Yonexus.Server` acts as server
|
||||
- `Yonexus.Client` acts as client
|
||||
|
||||
## 6.2 Message Categories
|
||||
|
||||
Two message categories exist on the same transport:
|
||||
|
||||
### Builtin protocol messages
|
||||
Used for:
|
||||
- hello/session setup
|
||||
- pairing
|
||||
- authentication
|
||||
- heartbeat
|
||||
- lifecycle/status
|
||||
- protocol errors
|
||||
|
||||
Format:
|
||||
|
||||
```text
|
||||
builtin::{json}
|
||||
```
|
||||
|
||||
### Application rule messages
|
||||
Used for higher-level cross-instance communication.
|
||||
|
||||
Format:
|
||||
|
||||
```text
|
||||
${rule_identifier}::${message_content}
|
||||
```
|
||||
|
||||
Server rewrite rule:
|
||||
|
||||
When server receives a message from a client, before dispatch it rewrites:
|
||||
|
||||
```text
|
||||
${rule_identifier}::${sender_identifier}::${message_content}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Security Architecture
|
||||
|
||||
## 7.1 Pairing Model
|
||||
|
||||
Pairing is intentionally out-of-band.
|
||||
|
||||
When a new client needs pairing:
|
||||
- server generates a pairing code
|
||||
- server sends that code to a human administrator via Discord DM
|
||||
- server does **not** send the code over the Yonexus WebSocket channel
|
||||
- human relays the code to the client side manually
|
||||
- client submits the code back to the server
|
||||
|
||||
This preserves a basic human-mediated trust step.
|
||||
|
||||
## 7.2 Post-Pairing Authentication
|
||||
|
||||
After pairing:
|
||||
- server issues a shared secret
|
||||
- client stores secret locally
|
||||
- client already has a private key
|
||||
- reconnect auth uses signed proof derived from:
|
||||
- secret
|
||||
- nonce
|
||||
- timestamp
|
||||
|
||||
## 7.3 Replay Protection
|
||||
|
||||
Server enforces:
|
||||
- timestamp freshness (`< 10s` drift)
|
||||
- nonce collision detection
|
||||
- handshake rate threshold (`>10 attempts in 10s` is unsafe)
|
||||
- re-pair requirement after unsafe conditions
|
||||
|
||||
---
|
||||
|
||||
## 8. State Ownership
|
||||
|
||||
## 8.1 Server-Owned State
|
||||
|
||||
Canonical server-owned state includes:
|
||||
- allowed client identifiers
|
||||
- trust state for each client
|
||||
- client public key
|
||||
- client secret
|
||||
- pairing state
|
||||
- pairing notification state
|
||||
- recent nonce window
|
||||
- recent handshake attempt window
|
||||
- client liveness state
|
||||
|
||||
## 8.2 Client-Owned State
|
||||
|
||||
Canonical client-owned state includes:
|
||||
- client identifier
|
||||
- client private key
|
||||
- client public key
|
||||
- current shared secret
|
||||
- last successful local trust metadata if needed
|
||||
|
||||
---
|
||||
|
||||
## 9. Plugin API Boundaries
|
||||
|
||||
## 9.1 Yonexus.Server API
|
||||
|
||||
Planned public API:
|
||||
- `sendMessageToClient(identifier, message)`
|
||||
- `registerRule(rule, processor)`
|
||||
|
||||
## 9.2 Yonexus.Client API
|
||||
|
||||
Planned public API:
|
||||
- `sendMessageToServer(message)`
|
||||
- `registerRule(rule, processor)`
|
||||
|
||||
The protocol defines semantics; implementation details belong in each submodule.
|
||||
|
||||
---
|
||||
|
||||
## 10. Documentation Ownership
|
||||
|
||||
## 10.1 Umbrella Repo Docs
|
||||
|
||||
Should contain:
|
||||
- system architecture
|
||||
- cross-cutting feature list
|
||||
- global design rationale
|
||||
- cross-repo coordination notes
|
||||
|
||||
## 10.2 Protocol Repo Docs
|
||||
|
||||
Must contain:
|
||||
- protocol specification (PROTOCOL.md)
|
||||
- canonical message shapes
|
||||
- protocol versioning notes
|
||||
|
||||
## 10.3 Server Repo Docs
|
||||
|
||||
Should contain:
|
||||
- server setup
|
||||
- server config reference
|
||||
- server persistence model
|
||||
- server operational behavior
|
||||
- implementation tasks
|
||||
|
||||
## 10.4 Client Repo Docs
|
||||
|
||||
Should contain:
|
||||
- client setup
|
||||
- client config reference
|
||||
- client local storage model
|
||||
- client reconnect/heartbeat behavior
|
||||
- implementation tasks
|
||||
|
||||
---
|
||||
|
||||
## 11. Development Flow
|
||||
|
||||
Recommended flow:
|
||||
1. define cross-cutting behavior in `Yonexus` umbrella
|
||||
2. finalize protocol in `Yonexus.Protocol`
|
||||
3. update submodule refs in `Yonexus.Server` and `Yonexus.Client`
|
||||
4. implement server-side protocol handling in `Yonexus.Server`
|
||||
5. implement client-side protocol handling in `Yonexus.Client`
|
||||
6. keep protocol changes synchronized back into umbrella docs
|
||||
|
||||
---
|
||||
|
||||
## 12. Non-Goals of the Umbrella Repo
|
||||
|
||||
The umbrella repo should avoid becoming:
|
||||
- the place where all implementation code lives
|
||||
- a dumping ground for server-only or client-only details
|
||||
- a duplicate of submodule READMEs without system-level value
|
||||
|
||||
Its job is coordination, not code concentration.
|
||||
330
FEAT.md
330
FEAT.md
@@ -1,86 +1,268 @@
|
||||
# FEAT — Yonexus Feature List
|
||||
# Yonexus — Feature Checklist
|
||||
|
||||
## Existing Features
|
||||
## Project Direction
|
||||
|
||||
### Core Model & Storage
|
||||
- Organization / Department / Team / Agent / Identity / Supervisor data model
|
||||
- In-memory runtime with JSON persistence (`data/org.json`)
|
||||
- Import/export of structure data
|
||||
Yonexus is a **two-plugin** cross-instance communication system for OpenClaw:
|
||||
- `Yonexus.Server`
|
||||
- `Yonexus.Client`
|
||||
|
||||
### Authorization
|
||||
- Role model: `org_admin`, `dept_admin`, `team_lead`, `agent`
|
||||
- `authorize(action, actor, scope)` permission check
|
||||
- Registrar whitelist (`registrars`) and bootstrap registration support
|
||||
|
||||
### Core APIs
|
||||
- `createOrganization(actor, name)`
|
||||
- `createDepartment(actor, name, orgId)`
|
||||
- `createTeam(actor, name, deptId)`
|
||||
- `registerAgent(actor, agentId, name, roles?)`
|
||||
- `assignIdentity(actor, agentId, deptId, teamId, meta)`
|
||||
- `setSupervisor(actor, agentId, supervisorId, deptId?)`
|
||||
- `whoami(agentId)`
|
||||
- `queryAgents(actor, scope, query)`
|
||||
|
||||
### Query & Search
|
||||
- Query ops: `eq`, `contains`, `regex`
|
||||
- Schema `queryable` whitelist enforcement
|
||||
- Pagination (`limit`, `offset`)
|
||||
- Basic query performance optimization (regex cache + ordered filter eval)
|
||||
|
||||
### Management & Audit
|
||||
- `renameDepartment`, `renameTeam`, `migrateTeam`, `deleteDepartment`, `deleteTeam`
|
||||
- Structured errors via `YonexusError`
|
||||
- In-memory audit log (`listAuditLogs`)
|
||||
|
||||
### Scope Memory
|
||||
- Scope memory adapter:
|
||||
- `scope_memory.put(scopeId, text, metadata)`
|
||||
- `scope_memory.search(scopeId, query, limit)`
|
||||
|
||||
### Developer Experience
|
||||
- `README.md` + `README.zh.md`
|
||||
- Example data (`examples/sample-data.json`)
|
||||
- Demo script (`scripts/demo.ts`)
|
||||
- Smoke test (`tests/smoke.ts`)
|
||||
This repository now targets the split-plugin architecture only.
|
||||
|
||||
---
|
||||
|
||||
## New Features (from NEW_FEAT)
|
||||
## 1. Yonexus.Server Features
|
||||
|
||||
### 1) Filesystem Resource Layout
|
||||
Data-only filesystem tree under:
|
||||
- `${openclaw dir}/yonexus/organizations/<org-name>/...`
|
||||
### 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
|
||||
|
||||
Auto-create (idempotent):
|
||||
- On `createOrganization`:
|
||||
- `teams/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/`
|
||||
- On `createTeam`:
|
||||
- `teams/<team-name>/agents/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/`
|
||||
- On `assignIdentity`:
|
||||
- `teams/<team-name>/agents/<agent-id>/docs|notes|knowledge|rules|lessons|workflows`
|
||||
### 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`)
|
||||
|
||||
### 2) Document Query Tool
|
||||
New API:
|
||||
- `getDocs(scope, topic, keyword)`
|
||||
### 1.3 Allowlist and Validation
|
||||
- `followerIdentifiers` allowlist enforcement
|
||||
- Reject unknown client identifiers
|
||||
- Reject malformed builtin payloads
|
||||
- Reject unsupported protocol versions
|
||||
|
||||
Parameters:
|
||||
- `scope`: `organization | department | team | agent`
|
||||
- `topic`: `docs | notes | knowledge | rules | lessons | workflows`
|
||||
- `keyword`: regex string
|
||||
### 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
|
||||
|
||||
Behavior:
|
||||
- Read-only search by filename regex under filesystem resources
|
||||
- Structured output:
|
||||
- `----ORG`
|
||||
- `----DEPT`
|
||||
- `----TEAM`
|
||||
- `----AGENT`
|
||||
- Invalid regex returns structured error (`YonexusError: VALIDATION_ERROR`)
|
||||
### 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
|
||||
|
||||
## Notes
|
||||
- `${openclaw dir}` resolution order:
|
||||
1. `YonexusOptions.openclawDir`
|
||||
2. `OPENCLAW_DIR` env
|
||||
3. `${HOME}/.openclaw`
|
||||
- Plugin code is not written into `${openclaw dir}/yonexus`; only data folders/files are used there.
|
||||
### 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
|
||||
|
||||
672
PLAN.md
672
PLAN.md
@@ -1,127 +1,579 @@
|
||||
# Yonexus — Project Plan
|
||||
|
||||
## 1) Goal
|
||||
Build an OpenClaw plugin that models organization hierarchy and agent identities, supports supervisor relationships, provides query tools for agents, and uses shared memory per scope (org/department/team).
|
||||
## 1. Goal
|
||||
|
||||
## 2) Core Concepts
|
||||
- **Hierarchy**: Organization → Department → Team → Agent
|
||||
- **Supervisor**: each agent may have exactly one supervisor
|
||||
- **Identity**: an agent can hold multiple identities across teams/departments
|
||||
- **Schema-driven metadata**: configurable fields with per-field queryability
|
||||
- **Scope memory**: shared memory for org/department/team (using `memory_store`, compatible with memory-lancedb-pro)
|
||||
Yonexus is a cross-instance communication system for OpenClaw, implemented as **two separate plugins**:
|
||||
|
||||
## 3) Storage Strategy
|
||||
- **Structure & identity data**: in-memory + JSON persistence (no memory_store)
|
||||
- **Shared memory**: memory_store keyed by scope (`org:{id}`, `dept:{id}`, `team:{id}`)
|
||||
- **Filesystem resources** (OpenClaw install dir `${openclaw dir}`):
|
||||
- Create a data-only folder at `${openclaw dir}/yonexus` (no plugin code here)
|
||||
- `yonexus/organizations/<org-name>/` contains: `teams/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/`
|
||||
- On **create_organization**: create `<org-name>` folder and its subfolders
|
||||
- On **create_team**: create `organizations/<org-name>/teams/<team-name>/` with `agents/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/`
|
||||
- On **assign_identity**: create `organizations/<org-name>/teams/<team-name>/agents/<agent-id>/` with `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/`
|
||||
- `Yonexus.Server`
|
||||
- `Yonexus.Client`
|
||||
|
||||
## 4) Permissions Model (B)
|
||||
Roles:
|
||||
- Org Admin
|
||||
- Dept Admin
|
||||
- Team Lead
|
||||
- Agent
|
||||
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 OpenClaw instance running `Yonexus.Server`
|
||||
- one or more OpenClaw instances running `Yonexus.Client`
|
||||
|
||||
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`
|
||||
|
||||
---
|
||||
|
||||
## 4. Configuration Model
|
||||
|
||||
## 4.1 Yonexus.Server Config
|
||||
|
||||
```ts
|
||||
followerIdentifiers: string[]
|
||||
notifyBotToken: string
|
||||
adminUserId: string
|
||||
listenHost?: string
|
||||
listenPort: number
|
||||
publicWsUrl?: string
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
## 4.2 Yonexus.Client Config
|
||||
|
||||
```ts
|
||||
mainHost: string
|
||||
identifier: string
|
||||
notifyBotToken: string
|
||||
adminUserId: string
|
||||
```
|
||||
|
||||
Semantics:
|
||||
- `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
|
||||
|
||||
## 4.3 Validation Rules
|
||||
|
||||
### Yonexus.Server
|
||||
- must provide `followerIdentifiers`
|
||||
- must provide `notifyBotToken`
|
||||
- must provide `adminUserId`
|
||||
- must provide `listenPort`
|
||||
- must be deployed on a reachable/stable endpoint
|
||||
|
||||
### Yonexus.Client
|
||||
- must provide `mainHost`
|
||||
- must provide `identifier`
|
||||
- must provide `notifyBotToken`
|
||||
- must provide `adminUserId`
|
||||
|
||||
### Shared
|
||||
- invalid or missing required fields must fail plugin initialization
|
||||
- unknown client identifiers must be rejected by `Yonexus.Server`
|
||||
|
||||
---
|
||||
|
||||
## 5. Runtime Lifecycle
|
||||
|
||||
## 5.1 Yonexus.Server Startup
|
||||
|
||||
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 session metadata
|
||||
|
||||
The registry must use:
|
||||
- in-memory runtime state for active connections and recent security windows
|
||||
- persistent on-disk storage for durable trust state
|
||||
|
||||
### 6.1 Proposed Server Record Shape
|
||||
|
||||
```ts
|
||||
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;
|
||||
recentNonces: Array<{
|
||||
nonce: string;
|
||||
timestamp: number;
|
||||
}>;
|
||||
recentHandshakeAttempts: number[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Pairing and Authentication
|
||||
|
||||
## 7.1 First Connection and Key Generation
|
||||
|
||||
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 the server sees that:
|
||||
- the client identifier is allowed, and
|
||||
- there is no valid `secret` currently associated with that identifier
|
||||
|
||||
then the server must enter pairing flow.
|
||||
|
||||
## 7.2 Pairing Flow
|
||||
|
||||
### 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
|
||||
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:
|
||||
- `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
|
||||
- the client must restart the pairing process
|
||||
|
||||
## 7.3 Reconnect Authentication Flow
|
||||
|
||||
After pairing is complete, future client authentication must use:
|
||||
- the stored `secret`
|
||||
- a 24-character random nonce
|
||||
- current UTC Unix timestamp
|
||||
|
||||
The client builds a proof payload from:
|
||||
- `secret`
|
||||
- `nonce`
|
||||
- `timestamp`
|
||||
|
||||
Logical concatenation order:
|
||||
|
||||
```text
|
||||
secret + nonce + timestamp
|
||||
```
|
||||
|
||||
Implementation recommendation:
|
||||
- use a canonical serialized object and sign its bytes rather than naive string concatenation in code
|
||||
|
||||
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
|
||||
- the client is considered authenticated for the session
|
||||
|
||||
If any check fails:
|
||||
- authentication fails
|
||||
- server may downgrade or revoke trust state
|
||||
|
||||
## 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
|
||||
- the presented nonce collides with one of the last 10 nonces observed within the recent window
|
||||
|
||||
When unsafe:
|
||||
- existing trust state must no longer be accepted for authentication
|
||||
- the client must re-pair
|
||||
- server should clear or rotate the stored `secret`
|
||||
- server should reset security windows as part of re-pairing
|
||||
|
||||
---
|
||||
|
||||
## 8. Heartbeat and Client Status
|
||||
|
||||
The server must track each client’s liveness state:
|
||||
- `online`
|
||||
- `unstable`
|
||||
- `offline`
|
||||
|
||||
## 8.1 Heartbeat Rules
|
||||
|
||||
Each client must send a heartbeat to the server every 5 minutes.
|
||||
|
||||
## 8.2 Status Transitions
|
||||
|
||||
### online
|
||||
A client is `online` when:
|
||||
- it has an active authenticated WebSocket connection, and
|
||||
- the server has received a recent heartbeat
|
||||
|
||||
### unstable
|
||||
A client becomes `unstable` when:
|
||||
- no heartbeat has been received for 7 minutes
|
||||
|
||||
### offline
|
||||
A client becomes `offline` when:
|
||||
- no heartbeat has been received for 11 minutes
|
||||
|
||||
When a client becomes `offline`:
|
||||
- the server must close/terminate the WebSocket connection for that client
|
||||
|
||||
## 8.3 Status Evaluation Strategy
|
||||
|
||||
The server should run a periodic status sweep timer.
|
||||
|
||||
Recommended interval:
|
||||
- every 30 to 60 seconds
|
||||
|
||||
---
|
||||
|
||||
## 9. Messaging Model
|
||||
|
||||
Yonexus provides rule-based message dispatch over WebSocket.
|
||||
|
||||
## 9.1 Base Message Format
|
||||
|
||||
All application messages must use the format:
|
||||
|
||||
```text
|
||||
${rule_identifier}::${message_content}
|
||||
```
|
||||
|
||||
## 9.2 Server-Side Rewriting
|
||||
|
||||
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 server-side processors can identify which client sent the message.
|
||||
|
||||
## 9.3 Builtin Rule Namespace
|
||||
|
||||
The reserved rule identifier is:
|
||||
|
||||
```text
|
||||
builtin
|
||||
```
|
||||
|
||||
It is used internally for:
|
||||
- handshake
|
||||
- pairing
|
||||
- heartbeat
|
||||
- protocol/system messages
|
||||
|
||||
User code must not be allowed to register handlers for `builtin`.
|
||||
|
||||
---
|
||||
|
||||
## 10. TypeScript API Surface
|
||||
|
||||
## 10.1 Yonexus.Client API
|
||||
|
||||
```ts
|
||||
sendMessageToServer(message: string): Promise<void>
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Supervisor is **not** a role (no inherent permissions)
|
||||
- Registration **not** self-service
|
||||
- only configured agent list or human via slash command
|
||||
- sends message to connected `Yonexus.Server`
|
||||
- message must already conform to `${rule_identifier}::${message_content}`
|
||||
|
||||
Permission matrix (recommended):
|
||||
- create_department → Org Admin
|
||||
- create_team → Org Admin, Dept Admin (same dept)
|
||||
- assign_identity → Org Admin, Dept Admin (same dept), Team Lead (same team)
|
||||
- register_agent → Org Admin, Dept Admin, Team Lead (scope-limited)
|
||||
- set_supervisor → Org Admin, Dept Admin (same dept)
|
||||
- query → all roles, but only schema fields with `queryable: true`
|
||||
|
||||
## 5) Schema Configuration (example)
|
||||
```json
|
||||
{
|
||||
"position": { "type": "string", "queryable": true },
|
||||
"discord_user_id": { "type": "string", "queryable": true },
|
||||
"git_user_name": { "type": "string", "queryable": true },
|
||||
"department": { "type": "string", "queryable": false },
|
||||
"team": { "type": "string", "queryable": false }
|
||||
}
|
||||
```ts
|
||||
registerRule(rule: string, processor: (message: string) => unknown): void
|
||||
```
|
||||
|
||||
## 6) Tool/API Surface (MVP)
|
||||
- `create_organization(name)`
|
||||
- `create_department(name, orgId)`
|
||||
- `create_team(name, deptId)`
|
||||
- `register_agent(agentId, name)`
|
||||
- `assign_identity(agentId, deptId, teamId, meta)`
|
||||
- `set_supervisor(actor, agentId, supervisorId)`
|
||||
- `whoami(agentId)` → identities + supervisor + roles
|
||||
- `query_agents(filters, options)` → list; supports `eq | contains | regex`
|
||||
Rules:
|
||||
- rejects `builtin`
|
||||
- rejects duplicate rule registration unless explicit override support is added later
|
||||
|
||||
Query example:
|
||||
```json
|
||||
{
|
||||
"filters": [
|
||||
{"field":"discord_user_id","op":"eq","value":"123"},
|
||||
{"field":"git_user_name","op":"regex","value":"^hang"}
|
||||
],
|
||||
"options": {"limit": 20, "offset": 0}
|
||||
}
|
||||
## 10.2 Yonexus.Server API
|
||||
|
||||
```ts
|
||||
sendMessageToClient(identifier: string, message: string): Promise<void>
|
||||
```
|
||||
|
||||
## 7) Data Model (MVP)
|
||||
- Organization { id, name }
|
||||
- Department { id, name, orgId }
|
||||
- Team { id, name, deptId }
|
||||
- Agent { id, name, roles[] }
|
||||
- Identity { id, agentId, deptId, teamId, meta }
|
||||
- Supervisor { agentId, supervisorId }
|
||||
Rules:
|
||||
- target client must be known and currently connected/authenticated
|
||||
- message must already conform to `${rule_identifier}::${message_content}`
|
||||
|
||||
## 8) Milestones
|
||||
**Phase 0 (Design)**
|
||||
- finalize schema
|
||||
- confirm permission rules
|
||||
|
||||
**Phase 1 (MVP)**
|
||||
- storage + JSON persistence
|
||||
- core models + tools
|
||||
- query DSL
|
||||
- scope memory adapter
|
||||
|
||||
**Phase 2 (v1)**
|
||||
- policy refinements
|
||||
- better query pagination & filtering
|
||||
- management commands & validation
|
||||
|
||||
## 9) Project Structure (recommended)
|
||||
```
|
||||
openclaw-plugin-yonexus/
|
||||
├─ plugin.json
|
||||
├─ src/
|
||||
│ ├─ index.ts
|
||||
│ ├─ store/ # in-memory + JSON persistence
|
||||
│ ├─ models/
|
||||
│ ├─ permissions/
|
||||
│ ├─ tools/
|
||||
│ ├─ memory/
|
||||
│ └─ utils/
|
||||
├─ scripts/
|
||||
│ └─ install.sh
|
||||
├─ dist/
|
||||
│ └─ yonexus/ # build output target
|
||||
└─ data/
|
||||
└─ org.json
|
||||
```ts
|
||||
registerRule(rule: string, processor: (message: string) => unknown): void
|
||||
```
|
||||
|
||||
## 10) Install Script Requirement
|
||||
- Provide `scripts/install.sh`
|
||||
- It should register the OpenClaw plugin name as **`yonexus`**
|
||||
- Build artifacts must be placed into **`dist/yonexus`**
|
||||
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
|
||||
|
||||
## 11) Notes & Decisions
|
||||
- Structure data is not stored in memory_store.
|
||||
- Shared memory uses memory_store (compatible with memory-lancedb-pro).
|
||||
- Queryable fields are whitelisted via schema.
|
||||
---
|
||||
|
||||
## 11. Hooks and Integration
|
||||
|
||||
## 11.1 Yonexus.Server Hooking
|
||||
|
||||
`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
|
||||
|
||||
## 11.2 Yonexus.Client Behavior
|
||||
|
||||
`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
|
||||
|
||||
---
|
||||
|
||||
## 12. Storage Strategy
|
||||
|
||||
## 12.1 Yonexus.Server Storage
|
||||
|
||||
Server persists at minimum:
|
||||
- identifier
|
||||
- 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
|
||||
|
||||
Recommended v1:
|
||||
- clear rolling security windows on restart
|
||||
- keep long-lived trust records
|
||||
|
||||
## 12.2 Yonexus.Client Storage
|
||||
|
||||
Client persists at minimum:
|
||||
- identifier
|
||||
- private key
|
||||
- secret
|
||||
- optional last successful pair/auth metadata
|
||||
|
||||
Security notes:
|
||||
- private key must never be sent to the server
|
||||
- secret must be treated as sensitive material
|
||||
- encryption-at-rest can be a future enhancement, but any plaintext local storage must be documented as a limitation if used initially
|
||||
|
||||
---
|
||||
|
||||
## 13. Error Handling
|
||||
|
||||
Structured errors should exist for at least:
|
||||
- invalid configuration
|
||||
- unauthorized identifier
|
||||
- pairing required
|
||||
- pairing expired
|
||||
- pairing notification failure
|
||||
- handshake verification failure
|
||||
- replay/nonce collision detected
|
||||
- unsafe handshake rate detected
|
||||
- target client not connected
|
||||
- duplicate rule registration
|
||||
- reserved rule registration
|
||||
- malformed message
|
||||
|
||||
---
|
||||
|
||||
## 14. Initial Implementation Phases
|
||||
|
||||
## Phase 0 — Protocol and Skeleton
|
||||
- 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 — Transport MVP
|
||||
- Yonexus.Server WebSocket server startup
|
||||
- Yonexus.Client WebSocket client startup
|
||||
- reconnect logic
|
||||
- builtin protocol channel
|
||||
- persistent registry/state scaffolding
|
||||
|
||||
## Phase 2 — Pairing and Authentication
|
||||
- client keypair generation
|
||||
- pairing request creation
|
||||
- Discord DM notification to admin user
|
||||
- pairing confirmation flow
|
||||
- secret issuance and persistence
|
||||
- signed proof verification
|
||||
- nonce/replay protection
|
||||
- unsafe-condition reset to pairing
|
||||
|
||||
## Phase 3 — Heartbeat and Status Tracking
|
||||
- client heartbeat sender
|
||||
- server heartbeat receiver
|
||||
- periodic sweep
|
||||
- status transitions: online / unstable / offline
|
||||
- forced disconnect on offline
|
||||
|
||||
## Phase 4 — Public APIs and Dispatch
|
||||
- `sendMessageToServer`
|
||||
- `sendMessageToClient`
|
||||
- `registerRule`
|
||||
- first-match dispatch
|
||||
- server-side sender rewrite behavior
|
||||
|
||||
## Phase 5 — Hardening and Docs
|
||||
- integration tests
|
||||
- failure-path coverage
|
||||
- restart recovery checks
|
||||
- protocol docs
|
||||
- operator setup docs for server/client deployment
|
||||
|
||||
---
|
||||
|
||||
## 15. Non-Goals for Initial Version
|
||||
|
||||
Not required in the first version unless explicitly added later:
|
||||
- direct client-to-client sockets
|
||||
- multi-server clustering
|
||||
- distributed consensus
|
||||
- message ordering guarantees across reconnects
|
||||
- end-to-end payload encryption beyond the pairing/authentication requirements
|
||||
- management UI
|
||||
|
||||
---
|
||||
|
||||
## 16. Open Questions To Confirm Later
|
||||
|
||||
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?
|
||||
|
||||
---
|
||||
|
||||
## 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 system overview for both plugins
|
||||
- `plugin.server.json` or equivalent server plugin manifest
|
||||
- `plugin.client.json` or equivalent client plugin manifest
|
||||
- implementation task breakdown
|
||||
|
||||
845
PROTOCOL.md
Normal file
845
PROTOCOL.md
Normal file
@@ -0,0 +1,845 @@
|
||||
# Yonexus Protocol Specification
|
||||
|
||||
Version: draft v0.3
|
||||
Status: planning
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document defines the built-in Yonexus communication protocol used between:
|
||||
- `Yonexus.Server`
|
||||
- `Yonexus.Client`
|
||||
|
||||
The protocol covers:
|
||||
- connection setup
|
||||
- pairing
|
||||
- authentication
|
||||
- heartbeat
|
||||
- status/lifecycle events
|
||||
- protocol-level errors
|
||||
- transport of application rule messages over the same WebSocket channel
|
||||
|
||||
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
|
||||
|
||||
Transport is WebSocket.
|
||||
|
||||
- `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
|
||||
|
||||
Client connects to configured `mainHost`, which may be:
|
||||
- `ws://host:port/path`
|
||||
- `wss://host:port/path`
|
||||
- or raw `host:port` if normalized by implementation
|
||||
|
||||
Recommended canonical config:
|
||||
- prefer full WebSocket URL
|
||||
|
||||
---
|
||||
|
||||
## 3. Message Categories
|
||||
|
||||
## 3.1 Builtin Protocol Messages
|
||||
|
||||
Builtin messages always use:
|
||||
|
||||
```text
|
||||
builtin::${json_payload}
|
||||
```
|
||||
|
||||
`builtin` is reserved and must not be registered by user code.
|
||||
|
||||
## 3.2 Application Rule Messages
|
||||
|
||||
Application messages use:
|
||||
|
||||
```text
|
||||
${rule_identifier}::${message_content}
|
||||
```
|
||||
|
||||
When `Yonexus.Server` receives a rule message from a client, it must rewrite it before dispatch to:
|
||||
|
||||
```text
|
||||
${rule_identifier}::${sender_identifier}::${message_content}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Builtin Envelope
|
||||
|
||||
Builtin wire format:
|
||||
|
||||
```text
|
||||
builtin::{JSON}
|
||||
```
|
||||
|
||||
Canonical envelope:
|
||||
|
||||
```ts
|
||||
interface BuiltinEnvelope {
|
||||
type: string;
|
||||
requestId?: string;
|
||||
timestamp?: number; // UTC unix seconds
|
||||
payload?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- `timestamp` uses UTC unix seconds
|
||||
- `requestId` is used for correlation where needed
|
||||
- `payload` content depends on `type`
|
||||
|
||||
---
|
||||
|
||||
## 5. Builtin Message Types
|
||||
|
||||
## 5.1 Session Setup
|
||||
|
||||
### `hello`
|
||||
Sent by `Yonexus.Client` immediately after WebSocket connection opens.
|
||||
|
||||
Purpose:
|
||||
- declare identifier
|
||||
- advertise current auth material state
|
||||
- announce protocol version
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"hello",
|
||||
"requestId":"req_001",
|
||||
"timestamp":1711886400,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"hasSecret":true,
|
||||
"hasKeyPair":true,
|
||||
"publicKey":"<optional-public-key>",
|
||||
"protocolVersion":"1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `hello_ack`
|
||||
Sent by `Yonexus.Server` in response to `hello`.
|
||||
|
||||
Possible `nextAction` values:
|
||||
- `pair_required`
|
||||
- `auth_required`
|
||||
- `rejected`
|
||||
- `waiting_pair_confirm`
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"hello_ack",
|
||||
"requestId":"req_001",
|
||||
"timestamp":1711886401,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"nextAction":"pair_required"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 `Yonexus.Server` to `Yonexus.Client` after pairing starts.
|
||||
|
||||
Purpose:
|
||||
- indicate that pairing has started
|
||||
- indicate whether admin notification succeeded
|
||||
- provide expiry metadata without revealing the code
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"pair_request",
|
||||
"requestId":"req_002",
|
||||
"timestamp":1711886402,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"expiresAt":1711886702,
|
||||
"ttlSeconds":300,
|
||||
"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 `Yonexus.Client` to confirm pairing.
|
||||
|
||||
Purpose:
|
||||
- submit the pairing code obtained out-of-band
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"pair_confirm",
|
||||
"requestId":"req_002",
|
||||
"timestamp":1711886410,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"pairingCode":"ABCD-1234-XYZ"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `pair_success`
|
||||
Sent by `Yonexus.Server` after successful pairing.
|
||||
|
||||
Purpose:
|
||||
- return generated secret
|
||||
- confirm trusted pairing state
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"pair_success",
|
||||
"requestId":"req_002",
|
||||
"timestamp":1711886411,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"secret":"<random-secret>",
|
||||
"pairedAt":1711886411
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `pair_failed`
|
||||
Sent by `Yonexus.Server` when pairing fails.
|
||||
|
||||
Typical reasons:
|
||||
- `expired`
|
||||
- `invalid_code`
|
||||
- `identifier_not_allowed`
|
||||
- `admin_notification_failed`
|
||||
- `internal_error`
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"pair_failed",
|
||||
"requestId":"req_002",
|
||||
"timestamp":1711886710,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"reason":"expired"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.3 Authentication Flow
|
||||
|
||||
After pairing, reconnect authentication uses:
|
||||
- stored `secret`
|
||||
- 24-character random nonce
|
||||
- current UTC unix timestamp
|
||||
- client private key
|
||||
|
||||
## 5.3.1 Proof Construction
|
||||
|
||||
Logical proof content:
|
||||
|
||||
```text
|
||||
secret + nonce + timestamp
|
||||
```
|
||||
|
||||
Implementation recommendation:
|
||||
- use canonical serialized object bytes for signing
|
||||
|
||||
Recommended logical form:
|
||||
|
||||
```json
|
||||
{
|
||||
"secret":"...",
|
||||
"nonce":"...",
|
||||
"timestamp":1711886500
|
||||
}
|
||||
```
|
||||
|
||||
## 5.3.2 Signature Primitive
|
||||
|
||||
Recommended primitive:
|
||||
- digital signature using client private key
|
||||
- verification using stored client public key on server
|
||||
|
||||
### `auth_request`
|
||||
Sent by `Yonexus.Client` after pairing or on reconnect.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"auth_request",
|
||||
"requestId":"req_003",
|
||||
"timestamp":1711886500,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"nonce":"RANDOM24CHARACTERSTRINGX",
|
||||
"proofTimestamp":1711886500,
|
||||
"signature":"<base64-signature>",
|
||||
"publicKey":"<optional-public-key-if-rotating>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Server validation:
|
||||
1. identifier is allowlisted
|
||||
2. identifier exists in registry
|
||||
3. client is in paired state
|
||||
4. public key matches expected key if provided
|
||||
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 `Yonexus.Server` on success.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"auth_success",
|
||||
"requestId":"req_003",
|
||||
"timestamp":1711886501,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"authenticatedAt":1711886501,
|
||||
"status":"online"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `auth_failed`
|
||||
Sent by `Yonexus.Server` on auth failure.
|
||||
|
||||
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 `Yonexus.Server` when trust state must be reset.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"re_pair_required",
|
||||
"requestId":"req_004",
|
||||
"timestamp":1711886510,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"reason":"nonce_collision"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.4 Heartbeat
|
||||
|
||||
### `heartbeat`
|
||||
Sent by `Yonexus.Client` every 5 minutes after authentication.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"heartbeat",
|
||||
"timestamp":1711886800,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"status":"alive"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `heartbeat_ack`
|
||||
Optional response by `Yonexus.Server`.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"heartbeat_ack",
|
||||
"timestamp":1711886801,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"status":"online"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.5 Status / Lifecycle Notifications
|
||||
|
||||
### `status_update`
|
||||
Sent by `Yonexus.Server` when client state changes.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"status_update",
|
||||
"timestamp":1711887220,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"status":"unstable",
|
||||
"reason":"heartbeat_timeout_7m"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `disconnect_notice`
|
||||
Sent by `Yonexus.Server` before deliberate close.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
builtin::{
|
||||
"type":"disconnect_notice",
|
||||
"timestamp":1711887460,
|
||||
"payload":{
|
||||
"identifier":"client-a",
|
||||
"reason":"heartbeat_timeout_11m"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.6 Errors
|
||||
|
||||
### `error`
|
||||
Generic protocol-level error.
|
||||
|
||||
Recommended builtin error codes:
|
||||
- `MALFORMED_MESSAGE`
|
||||
- `UNSUPPORTED_PROTOCOL_VERSION`
|
||||
- `IDENTIFIER_NOT_ALLOWED`
|
||||
- `PAIRING_REQUIRED`
|
||||
- `PAIRING_EXPIRED`
|
||||
- `ADMIN_NOTIFICATION_FAILED`
|
||||
- `AUTH_FAILED`
|
||||
- `NONCE_COLLISION`
|
||||
- `RATE_LIMITED`
|
||||
- `RE_PAIR_REQUIRED`
|
||||
- `CLIENT_OFFLINE`
|
||||
- `INTERNAL_ERROR`
|
||||
|
||||
---
|
||||
|
||||
## 6. State Machines
|
||||
|
||||
## 6.1 Client State Machine
|
||||
|
||||
Suggested `Yonexus.Client` states:
|
||||
- `idle`
|
||||
- `connecting`
|
||||
- `connected`
|
||||
- `pairing_required`
|
||||
- `pairing_pending`
|
||||
- `paired`
|
||||
- `authenticating`
|
||||
- `authenticated`
|
||||
- `reconnecting`
|
||||
- `error`
|
||||
|
||||
Typical transitions:
|
||||
|
||||
```text
|
||||
idle
|
||||
-> connecting
|
||||
-> connected
|
||||
-> (pairing_required | authenticating)
|
||||
|
||||
pairing_required
|
||||
-> pairing_pending
|
||||
-> paired
|
||||
-> authenticating
|
||||
-> authenticated
|
||||
|
||||
authenticated
|
||||
-> reconnecting
|
||||
-> connecting
|
||||
```
|
||||
|
||||
On `re_pair_required`:
|
||||
|
||||
```text
|
||||
authenticated | authenticating -> pairing_required
|
||||
```
|
||||
|
||||
## 6.2 Server-Side Client State
|
||||
|
||||
Per client trust state:
|
||||
- `unpaired`
|
||||
- `pending`
|
||||
- `paired`
|
||||
- `revoked`
|
||||
|
||||
Per client liveness state:
|
||||
- `online`
|
||||
- `unstable`
|
||||
- `offline`
|
||||
|
||||
---
|
||||
|
||||
## 7. Security Windows and Replay Protection
|
||||
|
||||
## 7.1 Nonce Requirements
|
||||
|
||||
Nonce rules:
|
||||
- exactly 24 random characters
|
||||
- fresh per auth attempt
|
||||
- must not repeat within recent security window
|
||||
|
||||
## 7.2 Recent Nonce Window
|
||||
|
||||
Server stores for each client:
|
||||
- the last 10 nonces seen within the recent validity window
|
||||
|
||||
If a nonce collides:
|
||||
- authentication fails
|
||||
- server marks condition unsafe
|
||||
- client must re-pair
|
||||
|
||||
## 7.3 Handshake Attempt Window
|
||||
|
||||
Server stores recent handshake attempt timestamps.
|
||||
|
||||
If more than 10 handshake attempts occur within 10 seconds:
|
||||
- authentication fails
|
||||
- server marks condition unsafe
|
||||
- client must re-pair
|
||||
|
||||
## 7.4 Time Drift Validation
|
||||
|
||||
Server validates:
|
||||
|
||||
```text
|
||||
abs(current_utc_unix_time - proofTimestamp) < 10
|
||||
```
|
||||
|
||||
If validation fails:
|
||||
- auth fails
|
||||
- no session is established
|
||||
|
||||
---
|
||||
|
||||
## 8. Rule Message Dispatch
|
||||
|
||||
All non-builtin messages use:
|
||||
|
||||
```text
|
||||
${rule_identifier}::${message_content}
|
||||
```
|
||||
|
||||
Client to server example:
|
||||
|
||||
```text
|
||||
chat_sync::{"conversationId":"abc","body":"hello"}
|
||||
```
|
||||
|
||||
Server rewrites before matching:
|
||||
|
||||
```text
|
||||
chat_sync::client-a::{"conversationId":"abc","body":"hello"}
|
||||
```
|
||||
|
||||
Dispatch algorithm:
|
||||
1. parse first delimiter section as `rule_identifier`
|
||||
2. if `rule_identifier === builtin`, route to builtin protocol handler
|
||||
3. otherwise iterate registered rules in registration order
|
||||
4. invoke the first exact match
|
||||
5. ignore/log if no match is found
|
||||
|
||||
Processor input:
|
||||
- on client: `${rule_identifier}::${message_content}`
|
||||
- on server for client-originated messages: `${rule_identifier}::${sender_identifier}::${message_content}`
|
||||
|
||||
---
|
||||
|
||||
## 9. Connection Rules
|
||||
|
||||
Server should reject connection attempts when:
|
||||
- identifier is absent
|
||||
- identifier is not in configured allowlist
|
||||
- protocol version is unsupported
|
||||
- hello/auth payload is malformed
|
||||
|
||||
Recommended v1 policy:
|
||||
- only one active authenticated connection per client identifier
|
||||
- terminate old connection and accept new one after successful auth
|
||||
|
||||
---
|
||||
|
||||
## 10. Persistence Semantics
|
||||
|
||||
## 10.1 Yonexus.Server Persists
|
||||
|
||||
At minimum:
|
||||
- identifier
|
||||
- public key
|
||||
- secret
|
||||
- trust state
|
||||
- pairing code + expiry if pending
|
||||
- pairing notification metadata
|
||||
- last known liveness status
|
||||
- metadata timestamps
|
||||
|
||||
May persist or reset on restart:
|
||||
- recent nonces
|
||||
- recent handshake attempts
|
||||
|
||||
Recommended v1:
|
||||
- clear rolling security windows on restart
|
||||
- keep long-lived trust records
|
||||
|
||||
## 10.2 Yonexus.Client Persists
|
||||
|
||||
At minimum:
|
||||
- identifier
|
||||
- private key
|
||||
- secret
|
||||
- optional last successful pair/auth metadata
|
||||
|
||||
---
|
||||
|
||||
## 11. Versioning
|
||||
|
||||
Protocol version is advertised during `hello`.
|
||||
|
||||
Initial version:
|
||||
|
||||
```text
|
||||
1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Canonical JSON Shapes
|
||||
|
||||
```ts
|
||||
interface HelloPayload {
|
||||
identifier: string;
|
||||
hasSecret: boolean;
|
||||
hasKeyPair: boolean;
|
||||
publicKey?: string;
|
||||
protocolVersion: string;
|
||||
}
|
||||
|
||||
interface PairRequestPayload {
|
||||
identifier: string;
|
||||
expiresAt: number;
|
||||
ttlSeconds: number;
|
||||
adminNotification: "sent" | "failed";
|
||||
codeDelivery: "out_of_band";
|
||||
}
|
||||
|
||||
interface PairConfirmPayload {
|
||||
identifier: string;
|
||||
pairingCode: string;
|
||||
}
|
||||
|
||||
interface PairSuccessPayload {
|
||||
identifier: string;
|
||||
secret: string;
|
||||
pairedAt: number;
|
||||
}
|
||||
|
||||
interface AuthRequestPayload {
|
||||
identifier: string;
|
||||
nonce: string;
|
||||
proofTimestamp: number;
|
||||
signature: string;
|
||||
publicKey?: string;
|
||||
}
|
||||
|
||||
interface HeartbeatPayload {
|
||||
identifier: string;
|
||||
status: "alive";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Example Flows
|
||||
|
||||
## 13.1 First-Time Pairing Flow
|
||||
|
||||
```text
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Implementation Notes
|
||||
|
||||
## 14.1 Parsing
|
||||
|
||||
Because the wire format is string-based with `::` delimiters:
|
||||
- only the first delimiter split should determine the `rule_identifier`
|
||||
- for `builtin`, the remainder is parsed as JSON once
|
||||
- message content itself may contain `::`, so avoid naive full split logic
|
||||
|
||||
## 14.2 Discord DM Notification
|
||||
|
||||
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
|
||||
|
||||
Sensitive values that must never be logged in plaintext:
|
||||
- `secret`
|
||||
- private key
|
||||
- raw proof material
|
||||
|
||||
---
|
||||
|
||||
## 15. Open Clarifications
|
||||
|
||||
1. Exact signing algorithm: Ed25519 is a strong default candidate
|
||||
2. Secret length and generation requirements
|
||||
3. Pairing code format and length
|
||||
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. Reserved Builtin Types
|
||||
|
||||
Reserved builtin `type` values:
|
||||
- `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`
|
||||
|
||||
These names are reserved by Yonexus and must not be repurposed by user rules.
|
||||
243
README.md
243
README.md
@@ -1,112 +1,171 @@
|
||||
[English](./README.md) | [中文](./README.zh.md)
|
||||
# Yonexus
|
||||
|
||||
Yonexus is a cross-instance communication system for OpenClaw built as **three separate repositories**:
|
||||
|
||||
| Repository | Role |
|
||||
|---|---|
|
||||
| `Yonexus` | Umbrella — architecture, planning, and coordination |
|
||||
| `Yonexus.Server` | Central hub plugin — accepts client connections, handles pairing/authentication |
|
||||
| `Yonexus.Client` | Client plugin — connects to server, manages local identity |
|
||||
| `Yonexus.Protocol` | Shared protocol specification — referenced as a submodule by both Server and Client |
|
||||
|
||||
## Overview
|
||||
|
||||
### Yonexus.Server
|
||||
Installed on the central OpenClaw instance.
|
||||
|
||||
Responsibilities:
|
||||
- run the WebSocket server
|
||||
- maintain the client registry
|
||||
- handle pairing and authentication
|
||||
- track heartbeat and liveness state
|
||||
- relay messages to connected clients
|
||||
- rewrite inbound client messages before rule dispatch
|
||||
- notify a human administrator of pairing requests via Discord DM
|
||||
|
||||
### Yonexus.Client
|
||||
Installed on follower OpenClaw instances.
|
||||
|
||||
Responsibilities:
|
||||
- connect to `Yonexus.Server`
|
||||
- manage local keypair and shared secret
|
||||
- complete pairing with out-of-band pairing code
|
||||
- authenticate on reconnect
|
||||
- send periodic heartbeat
|
||||
- send messages to server
|
||||
- receive messages from server via rule dispatch
|
||||
|
||||
### Yonexus.Protocol
|
||||
Shared protocol specification repository. Both `Yonexus.Server` and `Yonexus.Client` reference this as a submodule at `protocol/`.
|
||||
|
||||
---
|
||||
|
||||
# Yonexus
|
||||
## Repository Structure
|
||||
|
||||
Yonexus is an OpenClaw plugin for organization hierarchy and agent identity management.
|
||||
```
|
||||
Yonexus (umbrella)
|
||||
├── Yonexus.Protocol ← shared protocol submodule
|
||||
├── Yonexus.Server ← server plugin submodule
|
||||
│ └── protocol/ ← points to Yonexus.Protocol
|
||||
└── Yonexus.Client ← client plugin submodule
|
||||
└── protocol/ ← points to Yonexus.Protocol
|
||||
```
|
||||
|
||||
## Features
|
||||
---
|
||||
|
||||
- Organization hierarchy: `Organization -> Department -> Team -> Agent`
|
||||
- Filesystem-backed resource layout under `${openclaw dir}/yonexus`
|
||||
- Agent registration and multi-identity assignment
|
||||
- Supervisor relationship mapping (does **not** imply permissions)
|
||||
- Role-based authorization
|
||||
- Query DSL: `eq | contains | regex`
|
||||
- Queryable field whitelist via schema (`queryable: true`)
|
||||
- Scope shared memory adapter (`org/dept/team`)
|
||||
- JSON persistence for structure data
|
||||
- Audit logs and structured error codes
|
||||
- Import / export support
|
||||
## Architecture
|
||||
|
||||
## Project Layout
|
||||
A Yonexus network contains:
|
||||
- exactly one OpenClaw instance running `Yonexus.Server`
|
||||
- one or more OpenClaw instances running `Yonexus.Client`
|
||||
|
||||
Topology rules:
|
||||
- `Yonexus.Server` must be reachable via a stable address
|
||||
- `Yonexus.Client` instances connect outbound to the server
|
||||
- direct client-to-client sockets are not required in v1
|
||||
- client-to-client communication, if needed, is relayed by the server
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
|
||||
Pairing is intentionally **out-of-band**.
|
||||
|
||||
When a new client needs pairing:
|
||||
- the server generates a pairing code
|
||||
- the server sends that pairing code by Discord DM to a configured human admin
|
||||
- the pairing code is **not** sent over the Yonexus WebSocket connection
|
||||
- the human relays the code to the client side manually
|
||||
- the client submits the code back through the protocol
|
||||
|
||||
After pairing:
|
||||
- the server issues a shared secret
|
||||
- the client stores its private key and secret locally
|
||||
- reconnect authentication uses signed proof derived from `secret + nonce + timestamp`
|
||||
|
||||
---
|
||||
|
||||
## Current Repository Spec Files
|
||||
|
||||
### Umbrella (`Yonexus`)
|
||||
- `PLAN.md` — project plan and architecture
|
||||
- `ARCHITECTURE.md` — architecture overview and repository graph
|
||||
- `FEAT.md` — implementation feature checklist
|
||||
|
||||
### Protocol (`Yonexus.Protocol`)
|
||||
- `PROTOCOL.md` — shared communication protocol specification
|
||||
|
||||
### Server (`Yonexus.Server`)
|
||||
- `PLAN.md` — server-specific implementation plan
|
||||
- `protocol/` — submodule pointing to `Yonexus.Protocol`
|
||||
|
||||
### Client (`Yonexus.Client`)
|
||||
- `PLAN.md` — client-specific implementation plan
|
||||
- `protocol/` — submodule pointing to `Yonexus.Protocol`
|
||||
|
||||
---
|
||||
|
||||
## Planned TypeScript APIs
|
||||
|
||||
### Yonexus.Server
|
||||
```ts
|
||||
sendMessageToClient(identifier: string, message: string): Promise<void>
|
||||
registerRule(rule: string, processor: (message: string) => unknown): void
|
||||
```
|
||||
|
||||
### Yonexus.Client
|
||||
```ts
|
||||
sendMessageToServer(message: string): Promise<void>
|
||||
registerRule(rule: string, processor: (message: string) => unknown): void
|
||||
```
|
||||
|
||||
Message format:
|
||||
```text
|
||||
.
|
||||
├─ plugin.json
|
||||
├─ src/
|
||||
│ ├─ index.ts
|
||||
│ ├─ models/
|
||||
│ ├─ permissions/
|
||||
│ ├─ store/
|
||||
│ ├─ tools/
|
||||
│ ├─ memory/
|
||||
│ └─ utils/
|
||||
├─ scripts/
|
||||
│ ├─ install.sh
|
||||
│ └─ demo.ts
|
||||
├─ tests/
|
||||
│ └─ smoke.ts
|
||||
├─ examples/
|
||||
│ └─ sample-data.json
|
||||
└─ dist/
|
||||
└─ yonexus/
|
||||
${rule_identifier}::${message_content}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
Reserved rule: `builtin`
|
||||
|
||||
- Node.js 22+
|
||||
- npm 10+
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Planned Server Config
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
bash scripts/install.sh
|
||||
npm run test:smoke
|
||||
npm run demo
|
||||
```json
|
||||
{
|
||||
"followerIdentifiers": ["client-a", "client-b"],
|
||||
"notifyBotToken": "<discord-bot-token>",
|
||||
"adminUserId": "123456789012345678",
|
||||
"listenHost": "0.0.0.0",
|
||||
"listenPort": 8787,
|
||||
"publicWsUrl": "wss://example.com/yonexus"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
## Planned Client Config
|
||||
|
||||
`plugin.json` includes default config:
|
||||
|
||||
- `name`: `yonexus`
|
||||
- `entry`: `dist/yonexus/index.js`
|
||||
- `config.dataFile`: `./data/org.json`
|
||||
- `config.registrars`: whitelist for registrar agents
|
||||
- `config.schema`: metadata field schema and queryability
|
||||
|
||||
## Implemented APIs
|
||||
|
||||
Core:
|
||||
- `createOrganization(actor, name)`
|
||||
- `createDepartment(actor, name, orgId)`
|
||||
- `createTeam(actor, name, deptId)`
|
||||
- `registerAgent(actor, agentId, name, roles?)`
|
||||
- `assignIdentity(actor, agentId, deptId, teamId, meta)`
|
||||
- `setSupervisor(actor, agentId, supervisorId, deptId?)`
|
||||
- `whoami(agentId)`
|
||||
- `queryAgents(actor, scope, query)`
|
||||
|
||||
Management:
|
||||
- `renameDepartment(actor, deptId, newName)`
|
||||
- `renameTeam(actor, teamId, newName, deptId?)`
|
||||
- `migrateTeam(actor, teamId, newDeptId)`
|
||||
- `deleteDepartment(actor, deptId)`
|
||||
- `deleteTeam(actor, teamId, deptId?)`
|
||||
|
||||
Docs:
|
||||
- `getDocs(scope, topic, keyword)`
|
||||
|
||||
Data & audit:
|
||||
- `exportData(actor)`
|
||||
- `importData(actor, state)`
|
||||
- `listAuditLogs(limit?, offset?)`
|
||||
|
||||
## Testing
|
||||
|
||||
Smoke test:
|
||||
|
||||
```bash
|
||||
npm run test:smoke
|
||||
```json
|
||||
{
|
||||
"mainHost": "wss://example.com/yonexus",
|
||||
"identifier": "client-a",
|
||||
"notifyBotToken": "<discord-bot-token>",
|
||||
"adminUserId": "123456789012345678"
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
---
|
||||
|
||||
- Structure data is persisted in JSON, not memory_store.
|
||||
- Shared scope memory is handled via the scope memory adapter.
|
||||
- Unknown metadata fields are dropped during identity assignment.
|
||||
- `queryAgents` enforces schema queryable constraints.
|
||||
## Status
|
||||
|
||||
- planning/specification stage
|
||||
- split-plugin architecture defined
|
||||
- protocol draft defined in `Yonexus.Protocol`
|
||||
- implementation not started yet
|
||||
|
||||
---
|
||||
|
||||
## Repository URLs
|
||||
|
||||
- [Yonexus (umbrella)](https://git.hangman-lab.top/nav/Yonexus)
|
||||
- [Yonexus.Server](https://git.hangman-lab.top/nav/Yonexus.Server)
|
||||
- [Yonexus.Client](https://git.hangman-lab.top/nav/Yonexus.Client)
|
||||
- [Yonexus.Protocol](https://git.hangman-lab.top/nav/Yonexus.Protocol)
|
||||
|
||||
112
README.zh.md
112
README.zh.md
@@ -1,112 +0,0 @@
|
||||
[English](./README.md) | [中文](./README.zh.md)
|
||||
|
||||
---
|
||||
|
||||
# Yonexus
|
||||
|
||||
Yonexus 是一个用于 OpenClaw 的组织结构与 Agent 身份管理插件。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 组织层级:`Organization -> Department -> Team -> Agent`
|
||||
- 基于文件系统的资源目录:`${openclaw dir}/yonexus`
|
||||
- Agent 注册与多身份(Identity)管理
|
||||
- 上下级关系(Supervisor,**不自动赋权**)
|
||||
- 基于角色的权限控制
|
||||
- Query DSL:`eq | contains | regex`
|
||||
- 基于 schema 的可查询字段白名单(`queryable: true`)
|
||||
- scope 共享记忆适配(org/dept/team)
|
||||
- 结构化数据 JSON 持久化
|
||||
- 审计日志与结构化错误码
|
||||
- 导入 / 导出能力
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
.
|
||||
├─ plugin.json
|
||||
├─ src/
|
||||
│ ├─ index.ts
|
||||
│ ├─ models/
|
||||
│ ├─ permissions/
|
||||
│ ├─ store/
|
||||
│ ├─ tools/
|
||||
│ ├─ memory/
|
||||
│ └─ utils/
|
||||
├─ scripts/
|
||||
│ ├─ install.sh
|
||||
│ └─ demo.ts
|
||||
├─ tests/
|
||||
│ └─ smoke.ts
|
||||
├─ examples/
|
||||
│ └─ sample-data.json
|
||||
└─ dist/
|
||||
└─ yonexus/
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js 22+
|
||||
- npm 10+
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
bash scripts/install.sh
|
||||
npm run test:smoke
|
||||
npm run demo
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
`plugin.json` 默认包含以下配置:
|
||||
|
||||
- `name`: `yonexus`
|
||||
- `entry`: `dist/yonexus/index.js`
|
||||
- `config.dataFile`: `./data/org.json`
|
||||
- `config.registrars`: 注册人白名单
|
||||
- `config.schema`: 元数据字段定义与可查询性
|
||||
|
||||
## 已实现 API
|
||||
|
||||
核心 API:
|
||||
- `createOrganization(actor, name)`
|
||||
- `createDepartment(actor, name, orgId)`
|
||||
- `createTeam(actor, name, deptId)`
|
||||
- `registerAgent(actor, agentId, name, roles?)`
|
||||
- `assignIdentity(actor, agentId, deptId, teamId, meta)`
|
||||
- `setSupervisor(actor, agentId, supervisorId, deptId?)`
|
||||
- `whoami(agentId)`
|
||||
- `queryAgents(actor, scope, query)`
|
||||
|
||||
管理 API:
|
||||
- `renameDepartment(actor, deptId, newName)`
|
||||
- `renameTeam(actor, teamId, newName, deptId?)`
|
||||
- `migrateTeam(actor, teamId, newDeptId)`
|
||||
- `deleteDepartment(actor, deptId)`
|
||||
- `deleteTeam(actor, teamId, deptId?)`
|
||||
|
||||
文档检索:
|
||||
- `getDocs(scope, topic, keyword)`
|
||||
|
||||
数据与审计:
|
||||
- `exportData(actor)`
|
||||
- `importData(actor, state)`
|
||||
- `listAuditLogs(limit?, offset?)`
|
||||
|
||||
## 测试
|
||||
|
||||
冒烟测试:
|
||||
|
||||
```bash
|
||||
npm run test:smoke
|
||||
```
|
||||
|
||||
## 说明
|
||||
|
||||
- 结构数据保存在 JSON 文件,不进入 memory_store。
|
||||
- 共享记忆通过 scope memory 适配器处理。
|
||||
- 分配 identity 时,未知 meta 字段会被丢弃。
|
||||
- `queryAgents` 会严格校验字段是否在 schema 中标记为可查询。
|
||||
811
TASKLIST.md
Normal file
811
TASKLIST.md
Normal file
@@ -0,0 +1,811 @@
|
||||
# Yonexus — TASKLIST
|
||||
|
||||
基于 `FEAT.md`、`PLAN.md`、`PROTOCOL.md`、`README.md` 整理。
|
||||
|
||||
目标:把 Yonexus 从“规划完成、实现未开始”的状态,拆成可逐项交付、可验收、尽量低耦合的小任务列表。
|
||||
|
||||
---
|
||||
|
||||
## 0. 任务拆分原则
|
||||
|
||||
- 先把**协议、边界、脚手架**固定,再做运行时逻辑
|
||||
- 先做**可启动、可连接**,再做**配对/认证**
|
||||
- 先做**核心 happy path**,再做**失败路径和安全硬化**
|
||||
- 先做**Server / Client 各自最小闭环**,再做联调
|
||||
- 每个任务尽量满足:
|
||||
- 范围明确
|
||||
- 可以单独提交
|
||||
- 有清晰验收标准
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — 仓库与规范对齐
|
||||
|
||||
### YNX-0001 统一仓库定位与术语
|
||||
**目标**
|
||||
- 统一文档中对 Yonexus、Yonexus.Server、Yonexus.Client、Yonexus.Protocol 的描述
|
||||
- 明确这是“umbrella + 三个独立仓库/子模块”的模型
|
||||
|
||||
**子任务**
|
||||
- 检查 `README.md`、`PLAN.md`、`FEAT.md`、`PROTOCOL.md` 术语是否一致
|
||||
- 明确 `builtin`、`rule_identifier`、`identifier`、`secret`、`pairingCode` 等词汇定义
|
||||
- 记录 v1 非目标,避免实现漂移
|
||||
|
||||
**验收标准**
|
||||
- 核心文档不存在角色冲突或架构冲突描述
|
||||
- 协议字段名称在文档中保持一致
|
||||
|
||||
---
|
||||
|
||||
### YNX-0002 定义 v1 实现边界
|
||||
**目标**
|
||||
- 把“必须做”和“以后再说”彻底分开
|
||||
|
||||
**子任务**
|
||||
- 固化 v1 必做项:WebSocket、pairing、auth、heartbeat、rule dispatch、持久化
|
||||
- 固化 v1 不做项:多服务器、离线消息队列、复杂规则匹配、管理 UI
|
||||
- 明确心跳 ack 是否默认开启
|
||||
- 明确断线重连采用指数退避还是固定退避
|
||||
|
||||
**验收标准**
|
||||
- 有一份可执行的 v1 scope 列表
|
||||
- 实现阶段不再反复讨论边界
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — 协议落地与共享契约
|
||||
|
||||
### YNX-0101 固化 builtin 协议信封与类型枚举
|
||||
**目标**
|
||||
- 把 `PROTOCOL.md` 里的 builtin envelope 转成共享 TypeScript 类型
|
||||
|
||||
**子任务**
|
||||
- 定义 `BuiltinEnvelope`
|
||||
- 定义所有 builtin `type` 的联合类型
|
||||
- 定义 payload 接口:`HelloPayload`、`PairRequestPayload`、`PairConfirmPayload`、`PairSuccessPayload`、`AuthRequestPayload`、`HeartbeatPayload` 等
|
||||
- 统一 `timestamp` 使用 UTC unix seconds
|
||||
|
||||
**产出**
|
||||
- `Yonexus.Protocol` 中的共享类型文件
|
||||
|
||||
**验收标准**
|
||||
- Server / Client 都能直接复用类型
|
||||
- 所有 builtin 消息都可被类型系统约束
|
||||
|
||||
---
|
||||
|
||||
### YNX-0102 实现协议编解码工具
|
||||
**目标**
|
||||
- 提供统一的字符串协议解析与序列化能力
|
||||
|
||||
**子任务**
|
||||
- 实现 `encodeBuiltin(envelope)`
|
||||
- 实现 `decodeBuiltin(raw)`
|
||||
- 实现 rule message 的首段分隔解析
|
||||
- 确保 message content 中包含 `::` 时不会被错误切碎
|
||||
- 为 malformed message 提供标准错误
|
||||
|
||||
**验收标准**
|
||||
- `builtin::{json}` 可稳定双向转换
|
||||
- `rule::content` 和 `rule::sender::content` 都能正确解析
|
||||
|
||||
---
|
||||
|
||||
### YNX-0103 定义协议错误码与错误对象
|
||||
**目标**
|
||||
- 统一错误语义,避免 Server / Client 各自发明错误格式
|
||||
|
||||
**子任务**
|
||||
- 定义协议错误码枚举
|
||||
- 定义错误 envelope payload 结构
|
||||
- 区分:配置错误、协议错误、认证错误、配对错误、运行时错误
|
||||
|
||||
**验收标准**
|
||||
- 所有失败路径都能落到有限集合的错误码
|
||||
- 文档与代码错误码一致
|
||||
|
||||
---
|
||||
|
||||
### YNX-0104 编写协议级测试样例
|
||||
**目标**
|
||||
- 在实现运行时前,先锁定协议行为
|
||||
|
||||
**子任务**
|
||||
- 为 hello / pair / auth / heartbeat / error 编写样例用例
|
||||
- 为 malformed、缺字段、错误 rule、保留字冲突编写反例
|
||||
- 为 `::` 分隔边界编写测试
|
||||
|
||||
**验收标准**
|
||||
- 协议测试能独立运行
|
||||
- 后续实现可直接拿这些样例做回归
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Server 插件脚手架
|
||||
|
||||
### YNX-0201 创建 Yonexus.Server 最小插件骨架
|
||||
**目标**
|
||||
- 让 Server 插件可被 OpenClaw 加载
|
||||
|
||||
**子任务**
|
||||
- 创建插件目录结构
|
||||
- 创建 `package.json`
|
||||
- 创建 `openclaw.plugin.json`
|
||||
- 创建入口文件 `index.ts`
|
||||
- 添加基础构建配置
|
||||
|
||||
**验收标准**
|
||||
- 插件能被识别
|
||||
- 启动时至少能加载但不报致命错误
|
||||
|
||||
---
|
||||
|
||||
### YNX-0202 定义 Yonexus.Server 配置 schema
|
||||
**目标**
|
||||
- 让 Server 配置在启动前就能被校验
|
||||
|
||||
**子任务**
|
||||
- 校验 `followerIdentifiers`
|
||||
- 校验 `notifyBotToken`
|
||||
- 校验 `adminUserId`
|
||||
- 校验 `listenPort`
|
||||
- 校验 `listenHost` / `publicWsUrl` 可选字段
|
||||
- 启动失败时返回明确错误
|
||||
|
||||
**验收标准**
|
||||
- 缺字段和非法字段会 fail fast
|
||||
- 错误信息足够定位问题
|
||||
|
||||
---
|
||||
|
||||
### YNX-0203 实现 Server 生命周期 wiring
|
||||
**目标**
|
||||
- 在 gateway 启动时初始化 Server 内部组件
|
||||
|
||||
**子任务**
|
||||
- 初始化配置
|
||||
- 初始化 registry
|
||||
- 初始化持久化层
|
||||
- 初始化 rule registry
|
||||
- 注册 shutdown 清理逻辑
|
||||
|
||||
**验收标准**
|
||||
- 启停流程完整
|
||||
- 不会留下悬挂 timer / socket
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Client 插件脚手架
|
||||
|
||||
### YNX-0301 创建 Yonexus.Client 最小插件骨架
|
||||
**目标**
|
||||
- 让 Client 插件可被 OpenClaw 加载
|
||||
|
||||
**子任务**
|
||||
- 创建插件目录结构
|
||||
- 创建 `package.json`
|
||||
- 创建 `openclaw.plugin.json`
|
||||
- 创建入口文件 `index.ts`
|
||||
- 添加基础构建配置
|
||||
|
||||
**验收标准**
|
||||
- 插件能被识别并加载
|
||||
|
||||
---
|
||||
|
||||
### YNX-0302 定义 Yonexus.Client 配置 schema
|
||||
**目标**
|
||||
- 保证 Client 配置可启动前校验
|
||||
|
||||
**子任务**
|
||||
- 校验 `mainHost`
|
||||
- 校验 `identifier`
|
||||
- 校验 `notifyBotToken`
|
||||
- 校验 `adminUserId`
|
||||
- 规范 `mainHost` 是否允许裸 `host:port`
|
||||
|
||||
**验收标准**
|
||||
- 配置错误可在启动时直接发现
|
||||
|
||||
---
|
||||
|
||||
### YNX-0303 实现 Client 生命周期 wiring
|
||||
**目标**
|
||||
- 在 gateway 启动时初始化 Client 运行时
|
||||
|
||||
**子任务**
|
||||
- 加载本地状态
|
||||
- 初始化连接管理器
|
||||
- 初始化规则注册器
|
||||
- 注册 shutdown 清理逻辑
|
||||
|
||||
**验收标准**
|
||||
- Client 可启动并在未连接状态下稳定运行
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — 持久化与状态模型
|
||||
|
||||
### YNX-0401 定义 Server 持久化记录结构
|
||||
**目标**
|
||||
- 把 `PLAN.md` 中的 `ClientRecord` 落到代码
|
||||
|
||||
**子任务**
|
||||
- 定义 paired/unpaired/revoked/pending 状态
|
||||
- 定义 heartbeat/liveness 字段
|
||||
- 定义 pairing 通知字段
|
||||
- 定义 recent nonce / recent attempts 窗口结构
|
||||
- 定义 `createdAt` / `updatedAt`
|
||||
|
||||
**验收标准**
|
||||
- 持久化结构足以支撑 pairing/auth/heartbeat
|
||||
|
||||
---
|
||||
|
||||
### YNX-0402 实现 Server 状态存储
|
||||
**目标**
|
||||
- 让信任状态在重启后仍可恢复
|
||||
|
||||
**子任务**
|
||||
- 选择 JSON 文件或等价轻量持久化形式
|
||||
- 实现加载、保存、原子写入
|
||||
- 区分持久数据与内存态数据
|
||||
- 明确 restart 后是否清空 rolling windows
|
||||
|
||||
**验收标准**
|
||||
- 重启后 paired client 不丢失
|
||||
- 损坏文件时有可恢复行为
|
||||
|
||||
---
|
||||
|
||||
### YNX-0403 实现 Client 本地信任材料存储
|
||||
**目标**
|
||||
- 持久化 Client 的 `identifier`、私钥、secret
|
||||
|
||||
**子任务**
|
||||
- 定义本地 state 文件结构
|
||||
- 实现加载 / 初始化 / 保存
|
||||
- 缺失文件时自动初始化最小状态
|
||||
- 标记敏感字段
|
||||
|
||||
**验收标准**
|
||||
- Client 重启后可恢复身份与 secret
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Transport 最小闭环
|
||||
|
||||
### YNX-0501 实现 Server WebSocket 启动与连接接入
|
||||
**目标**
|
||||
- Server 能监听并接受连接
|
||||
|
||||
**子任务**
|
||||
- 启动 WebSocket server
|
||||
- 处理 connect / message / close / error
|
||||
- 记录连接与 identifier 绑定前的临时会话
|
||||
|
||||
**验收标准**
|
||||
- 可看到客户端连接进入
|
||||
- 无协议时也不会崩
|
||||
|
||||
---
|
||||
|
||||
### YNX-0502 实现 Client WebSocket 连接器
|
||||
**目标**
|
||||
- Client 能主动连到 Server
|
||||
|
||||
**子任务**
|
||||
- 建立 outbound 连接
|
||||
- 处理 open / message / close / error
|
||||
- 提供连接状态机
|
||||
|
||||
**验收标准**
|
||||
- Client 能连上可用的 Server
|
||||
- Server 不可用时不会死循环刷日志
|
||||
|
||||
---
|
||||
|
||||
### YNX-0503 实现 hello / hello_ack
|
||||
**目标**
|
||||
- 完成连接后的第一段协议握手
|
||||
|
||||
**子任务**
|
||||
- Client 连接后发送 `hello`
|
||||
- Server 验证 `identifier`、协议版本、payload 形状
|
||||
- Server 返回 `hello_ack`
|
||||
- 根据状态决定 `nextAction`
|
||||
|
||||
**验收标准**
|
||||
- 未配对客户端收到 `pair_required`
|
||||
- 已配对客户端收到 `auth_required`
|
||||
- 非 allowlist 客户端被拒绝
|
||||
|
||||
---
|
||||
|
||||
### YNX-0504 实现基础重连策略
|
||||
**目标**
|
||||
- Client 断线后可恢复连接
|
||||
|
||||
**子任务**
|
||||
- 设计退避策略
|
||||
- 避免并发多次重连
|
||||
- 在成功连接后重置退避计数
|
||||
|
||||
**验收标准**
|
||||
- 断线可自动恢复
|
||||
- 不会形成高频重连风暴
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Pairing 主流程
|
||||
|
||||
### YNX-0601 实现 Client 首次密钥生成
|
||||
**目标**
|
||||
- Client 首次运行自动生成本地公私钥
|
||||
|
||||
**子任务**
|
||||
- 选定签名算法(建议 Ed25519)
|
||||
- 生成 keypair
|
||||
- 保存私钥
|
||||
- 导出公钥供握手使用
|
||||
|
||||
**验收标准**
|
||||
- 首次启动可生成并持久化 keypair
|
||||
- 重启不会重复生成
|
||||
|
||||
---
|
||||
|
||||
### YNX-0602 实现 Server pairing request 创建
|
||||
**目标**
|
||||
- Server 可为待配对客户端创建 pairing 会话
|
||||
|
||||
**子任务**
|
||||
- 生成 pairing code
|
||||
- 生成 expiresAt / ttl
|
||||
- 将状态写入 registry
|
||||
- 更新 pairing 状态为 pending
|
||||
|
||||
**验收标准**
|
||||
- 每次 pairing 会话有可验证的过期时间
|
||||
- pairing code 不会通过 Yonexus WS 下发
|
||||
|
||||
---
|
||||
|
||||
### YNX-0603 实现 Discord DM 配对通知
|
||||
**目标**
|
||||
- Server 通过 `notifyBotToken` 向 `adminUserId` 发送 pairing code
|
||||
|
||||
**子任务**
|
||||
- 封装 Discord DM 发送逻辑
|
||||
- DM 内容包含 identifier、pairingCode、expiresAt/TTL
|
||||
- 处理发送失败
|
||||
- 记录 pairing notification metadata
|
||||
|
||||
**验收标准**
|
||||
- 通知成功时 Client 才能进入可确认状态
|
||||
- 通知失败时不会继续配对成功路径
|
||||
|
||||
---
|
||||
|
||||
### YNX-0604 实现 pair_request / pair_confirm / pair_success
|
||||
**目标**
|
||||
- 打通完整配对流程
|
||||
|
||||
**子任务**
|
||||
- Server 下发不含 code 的 `pair_request`
|
||||
- Client 接收并进入 pairing pending
|
||||
- Client 提交 `pair_confirm`
|
||||
- Server 校验 code 和 expiry
|
||||
- 成功后生成 secret
|
||||
- Server 返回 `pair_success`
|
||||
- Client 保存 secret
|
||||
|
||||
**验收标准**
|
||||
- 正确 code 可完成配对
|
||||
- 错误 code / 过期 code 会失败
|
||||
|
||||
---
|
||||
|
||||
### YNX-0605 实现配对失败路径
|
||||
**目标**
|
||||
- 补齐 pairing 相关失败逻辑
|
||||
|
||||
**子任务**
|
||||
- `invalid_code`
|
||||
- `expired`
|
||||
- `identifier_not_allowed`
|
||||
- `admin_notification_failed`
|
||||
- `internal_error`
|
||||
- 配对失败后的状态恢复策略
|
||||
|
||||
**验收标准**
|
||||
- 失败后不会留下脏状态导致后续无法重试
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Authentication 主流程
|
||||
|
||||
### YNX-0701 固化 proof 构造与签名规范
|
||||
**目标**
|
||||
- 明确签名输入,避免 Server / Client 不一致
|
||||
|
||||
**子任务**
|
||||
- 决定 canonical serialization 方案
|
||||
- 明确签名字段:secret、nonce、timestamp
|
||||
- 定义签名编码格式(如 base64)
|
||||
|
||||
**验收标准**
|
||||
- 同一输入在 Client 与 Server 上验证一致
|
||||
|
||||
---
|
||||
|
||||
### YNX-0702 实现 Client auth_request
|
||||
**目标**
|
||||
- Client 能基于本地 secret 与私钥发起认证
|
||||
|
||||
**子任务**
|
||||
- 生成 24 字符 nonce
|
||||
- 生成 proofTimestamp
|
||||
- 构造签名
|
||||
- 发送 `auth_request`
|
||||
|
||||
**验收标准**
|
||||
- 报文字段完整
|
||||
- nonce 格式符合协议要求
|
||||
|
||||
---
|
||||
|
||||
### YNX-0703 实现 Server 认证校验
|
||||
**目标**
|
||||
- Server 能验证 auth_request 真伪
|
||||
|
||||
**子任务**
|
||||
- 校验 identifier allowlist
|
||||
- 校验 paired 状态
|
||||
- 校验 publicKey 匹配
|
||||
- 校验 signature
|
||||
- 校验 secret
|
||||
- 校验 timestamp 漂移
|
||||
- 校验 nonce 重放
|
||||
- 校验 attempts 频率
|
||||
|
||||
**验收标准**
|
||||
- 正确认证返回 `auth_success`
|
||||
- 各类失败返回对应 `auth_failed`
|
||||
|
||||
---
|
||||
|
||||
### YNX-0704 实现 re_pair_required 机制
|
||||
**目标**
|
||||
- 在不安全条件下强制重新配对
|
||||
|
||||
**子任务**
|
||||
- 检测 nonce collision
|
||||
- 检测 `>10 attempts / 10s`
|
||||
- 失效旧 secret 或标记 trust revoked
|
||||
- 返回 `re_pair_required`
|
||||
- Client 收到后切回 pairing_required
|
||||
|
||||
**验收标准**
|
||||
- 不安全状态下不会继续接受旧信任材料
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Heartbeat 与在线状态
|
||||
|
||||
### YNX-0801 实现 Client heartbeat loop
|
||||
**目标**
|
||||
- Client 在认证后按周期发心跳
|
||||
|
||||
**子任务**
|
||||
- 认证成功后启动 heartbeat timer
|
||||
- 断线或未认证时停止 timer
|
||||
- 发送 `heartbeat`
|
||||
|
||||
**验收标准**
|
||||
- 默认每 5 分钟发送一次
|
||||
- 状态切换时 timer 无泄漏
|
||||
|
||||
---
|
||||
|
||||
### YNX-0802 实现 Server heartbeat 接收与记录
|
||||
**目标**
|
||||
- Server 能更新客户端最近存活时间
|
||||
|
||||
**子任务**
|
||||
- 接收 heartbeat
|
||||
- 校验来源已认证
|
||||
- 更新 `lastHeartbeatAt`
|
||||
- 可选返回 `heartbeat_ack`
|
||||
|
||||
**验收标准**
|
||||
- 收到心跳后客户端状态可维持在线
|
||||
|
||||
---
|
||||
|
||||
### YNX-0803 实现 Server liveness sweep
|
||||
**目标**
|
||||
- Server 能周期性评估 online / unstable / offline
|
||||
|
||||
**子任务**
|
||||
- 增加 30~60s sweep timer
|
||||
- 7 分钟无心跳标记 unstable
|
||||
- 11 分钟无心跳标记 offline
|
||||
- offline 时发送 `disconnect_notice` 并断开连接
|
||||
|
||||
**验收标准**
|
||||
- 状态转移符合文档定义
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — 规则消息与 API
|
||||
|
||||
### YNX-0901 实现 Client rule registry
|
||||
**目标**
|
||||
- Client 侧支持应用层规则分发
|
||||
|
||||
**子任务**
|
||||
- 实现 `registerRule(rule, processor)`
|
||||
- 禁止 `builtin`
|
||||
- 默认禁止重复注册
|
||||
- 按 exact match 分发
|
||||
|
||||
**验收标准**
|
||||
- 命中规则时正确调用处理器
|
||||
- 非法注册被拒绝
|
||||
|
||||
---
|
||||
|
||||
### YNX-0902 实现 Server rule registry
|
||||
**目标**
|
||||
- Server 侧支持应用层规则分发
|
||||
|
||||
**子任务**
|
||||
- 实现 `registerRule(rule, processor)`
|
||||
- 禁止 `builtin`
|
||||
- 默认禁止重复注册
|
||||
- 按 exact match 分发
|
||||
|
||||
**验收标准**
|
||||
- 规则注册与调用行为一致
|
||||
|
||||
---
|
||||
|
||||
### YNX-0903 实现 `sendMessageToServer(message)`
|
||||
**目标**
|
||||
- 暴露 Client 到 Server 的发送 API
|
||||
|
||||
**子任务**
|
||||
- 校验连接/认证状态
|
||||
- 发送 `${rule_identifier}::${message_content}`
|
||||
- 未连接时返回明确错误
|
||||
|
||||
**验收标准**
|
||||
- 上层插件可直接调用
|
||||
|
||||
---
|
||||
|
||||
### YNX-0904 实现 `sendMessageToClient(identifier, message)`
|
||||
**目标**
|
||||
- 暴露 Server 到指定 Client 的发送 API
|
||||
|
||||
**子任务**
|
||||
- 校验目标 client 已知且在线
|
||||
- 发送 `${rule_identifier}::${message_content}`
|
||||
- 离线时返回明确错误
|
||||
|
||||
**验收标准**
|
||||
- 上层插件可向指定客户端投递消息
|
||||
|
||||
---
|
||||
|
||||
### YNX-0905 实现 Server 入站消息重写
|
||||
**目标**
|
||||
- 把客户端来的消息重写成带 sender 标识的形式
|
||||
|
||||
**子任务**
|
||||
- 解析 `${rule_identifier}::${message_content}`
|
||||
- 重写为 `${rule_identifier}::${sender_identifier}::${message_content}`
|
||||
- 再进入 rule dispatch
|
||||
|
||||
**验收标准**
|
||||
- Server 侧处理器能可靠识别消息来源
|
||||
|
||||
---
|
||||
|
||||
## Phase 10 — 安全与鲁棒性
|
||||
|
||||
### YNX-1001 实现敏感信息脱敏日志
|
||||
**目标**
|
||||
- 避免 secret、私钥、proof 原文进入日志
|
||||
|
||||
**子任务**
|
||||
- 定义 redaction 工具
|
||||
- 覆盖 pairing、auth、error 日志路径
|
||||
- 对外只保留必要上下文
|
||||
|
||||
**验收标准**
|
||||
- 关键敏感值不会明文打印
|
||||
|
||||
---
|
||||
|
||||
### YNX-1002 实现 malformed / unsupported / unauthorized 防御
|
||||
**目标**
|
||||
- 所有非法输入都能被可控拒绝
|
||||
|
||||
**子任务**
|
||||
- malformed json
|
||||
- 缺字段 payload
|
||||
- unsupported protocol version
|
||||
- unauthorized identifier
|
||||
- 非法 builtin type
|
||||
|
||||
**验收标准**
|
||||
- 错误不会导致进程崩溃
|
||||
- 客户端能收到明确错误反馈或断开
|
||||
|
||||
---
|
||||
|
||||
### YNX-1003 实现单 identifier 单活跃连接策略
|
||||
**目标**
|
||||
- 同一 client identifier 只允许一个活跃认证连接
|
||||
|
||||
**子任务**
|
||||
- 定义旧连接替换策略
|
||||
- 新连接成功认证后踢掉旧连接
|
||||
- 避免竞态导致双在线
|
||||
|
||||
**验收标准**
|
||||
- 任意时刻同一 identifier 只有一个有效 session
|
||||
|
||||
---
|
||||
|
||||
### YNX-1004 实现重启恢复策略
|
||||
**目标**
|
||||
- 重启后行为可预期且文档一致
|
||||
|
||||
**子任务**
|
||||
- 恢复 durable trust records
|
||||
- 清理或重建 rolling windows
|
||||
- 明确 pending pairing 如何恢复
|
||||
- 明确 active connection 状态如何重建
|
||||
|
||||
**验收标准**
|
||||
- 重启后的认证/配对行为稳定可解释
|
||||
|
||||
---
|
||||
|
||||
## Phase 11 — 测试与联调
|
||||
|
||||
### YNX-1101 编写协议单元测试
|
||||
**目标**
|
||||
- 覆盖编解码、字段校验、错误码
|
||||
|
||||
### YNX-1102 编写 Server 单元测试
|
||||
**目标**
|
||||
- 覆盖 registry、pairing、auth、heartbeat sweep
|
||||
|
||||
### YNX-1103 编写 Client 单元测试
|
||||
**目标**
|
||||
- 覆盖状态机、keypair、auth 构造、heartbeat timer
|
||||
|
||||
### YNX-1104 编写 Server-Client 集成测试
|
||||
**目标**
|
||||
- 覆盖首次配对、正常重连、认证失败、心跳超时、re-pair
|
||||
|
||||
### YNX-1105 编写失败路径测试矩阵
|
||||
**目标**
|
||||
- 系统性覆盖 pairing/auth 失败路径
|
||||
|
||||
**重点场景**
|
||||
- pairing code 错误
|
||||
- pairing 过期
|
||||
- DM 发送失败
|
||||
- stale timestamp
|
||||
- future timestamp
|
||||
- nonce collision
|
||||
- handshake rate limit
|
||||
- unknown identifier
|
||||
- duplicate connection
|
||||
|
||||
**验收标准**
|
||||
- 核心安全路径都有自动化测试
|
||||
|
||||
---
|
||||
|
||||
## Phase 12 — 文档与交付
|
||||
|
||||
### YNX-1201 补齐 Server README
|
||||
**目标**
|
||||
- Server 仓库可独立被安装与使用
|
||||
|
||||
### YNX-1202 补齐 Client README
|
||||
**目标**
|
||||
- Client 仓库可独立被安装与使用
|
||||
|
||||
### YNX-1203 输出部署文档
|
||||
**目标**
|
||||
- 写清楚单主多从部署方式、配置示例、配对流程
|
||||
|
||||
### YNX-1204 输出运维排障文档
|
||||
**目标**
|
||||
- 写清楚常见报错、状态含义、恢复步骤
|
||||
|
||||
### YNX-1205 输出协议测试与验收清单
|
||||
**目标**
|
||||
- 让后续改动有统一回归基线
|
||||
|
||||
---
|
||||
|
||||
## 推荐执行顺序(最小闭环)
|
||||
|
||||
### Milestone A — 能启动
|
||||
- YNX-0101
|
||||
- YNX-0102
|
||||
- YNX-0201
|
||||
- YNX-0202
|
||||
- YNX-0301
|
||||
- YNX-0302
|
||||
- YNX-0401
|
||||
- YNX-0402
|
||||
- YNX-0403
|
||||
|
||||
### Milestone B — 能连接
|
||||
- YNX-0203
|
||||
- YNX-0303
|
||||
- YNX-0501
|
||||
- YNX-0502
|
||||
- YNX-0503
|
||||
- YNX-0504
|
||||
|
||||
### Milestone C — 能配对认证
|
||||
- YNX-0601
|
||||
- YNX-0602
|
||||
- YNX-0603
|
||||
- YNX-0604
|
||||
- YNX-0605
|
||||
- YNX-0701
|
||||
- YNX-0702
|
||||
- YNX-0703
|
||||
- YNX-0704
|
||||
|
||||
### Milestone D — 能稳定通信
|
||||
- YNX-0801
|
||||
- YNX-0802
|
||||
- YNX-0803
|
||||
- YNX-0901
|
||||
- YNX-0902
|
||||
- YNX-0903
|
||||
- YNX-0904
|
||||
- YNX-0905
|
||||
|
||||
### Milestone E — 能交付
|
||||
- YNX-1001
|
||||
- YNX-1002
|
||||
- YNX-1003
|
||||
- YNX-1004
|
||||
- YNX-1101
|
||||
- YNX-1102
|
||||
- YNX-1103
|
||||
- YNX-1104
|
||||
- YNX-1105
|
||||
- YNX-1201
|
||||
- YNX-1202
|
||||
- YNX-1203
|
||||
- YNX-1204
|
||||
- YNX-1205
|
||||
|
||||
---
|
||||
|
||||
## 可直接开工的首批任务
|
||||
|
||||
如果要我给出“今天就能开始做”的第一批,我建议是:
|
||||
|
||||
1. YNX-0101 固化共享协议类型
|
||||
2. YNX-0102 实现协议编解码工具
|
||||
3. YNX-0201 创建 Yonexus.Server 插件骨架
|
||||
4. YNX-0301 创建 Yonexus.Client 插件骨架
|
||||
5. YNX-0401 定义 Server / Client 状态模型
|
||||
6. YNX-0503 实现 hello / hello_ack 最小握手
|
||||
|
||||
这样最快能拿到一个“能启动、能连接、能判断下一步动作”的基础闭环。
|
||||
1
Yonexus.Client
Submodule
1
Yonexus.Client
Submodule
Submodule Yonexus.Client added at 5234358cac
1
Yonexus.Protocol
Submodule
1
Yonexus.Protocol
Submodule
Submodule Yonexus.Protocol added at 9232aa7c17
1
Yonexus.Server
Submodule
1
Yonexus.Server
Submodule
Submodule Yonexus.Server added at d8290c0aa7
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"organizations": [
|
||||
{ "id": "org:yonexus", "name": "Yonexus" }
|
||||
],
|
||||
"departments": [
|
||||
{ "id": "dept:platform", "name": "Platform", "orgId": "org:yonexus" },
|
||||
{ "id": "dept:ai", "name": "AI", "orgId": "org:yonexus" }
|
||||
],
|
||||
"teams": [
|
||||
{ "id": "team:platform-core", "name": "Core", "deptId": "dept:platform" },
|
||||
{ "id": "team:ai-agent", "name": "Agent", "deptId": "dept:ai" }
|
||||
],
|
||||
"agents": [
|
||||
{ "id": "orion", "name": "Orion", "roles": ["org_admin", "agent"] },
|
||||
{ "id": "hangman", "name": "Hangman", "roles": ["agent"] }
|
||||
],
|
||||
"identities": [
|
||||
{
|
||||
"id": "identity:orion-platform",
|
||||
"agentId": "orion",
|
||||
"deptId": "dept:platform",
|
||||
"teamId": "team:platform-core",
|
||||
"meta": {
|
||||
"position": "assistant",
|
||||
"discord_user_id": "1474088632750047324",
|
||||
"git_user_name": "orion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "identity:hangman-ai",
|
||||
"agentId": "hangman",
|
||||
"deptId": "dept:ai",
|
||||
"teamId": "team:ai-agent",
|
||||
"meta": {
|
||||
"position": "owner",
|
||||
"discord_user_id": "561921120408698910",
|
||||
"git_user_name": "hangman"
|
||||
}
|
||||
}
|
||||
],
|
||||
"supervisors": [
|
||||
{ "agentId": "orion", "supervisorId": "hangman" }
|
||||
]
|
||||
}
|
||||
591
package-lock.json
generated
591
package-lock.json
generated
@@ -1,591 +0,0 @@
|
||||
{
|
||||
"name": "openclaw-plugin-yonexus",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openclaw-plugin-yonexus",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.6",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
package.json
21
package.json
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "openclaw-plugin-yonexus",
|
||||
"version": "0.1.0",
|
||||
"description": "Yonexus OpenClaw plugin: hierarchy, identities, permissions, and scoped memory",
|
||||
"main": "dist/yonexus/index.js",
|
||||
"types": "dist/yonexus/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"clean": "rm -rf dist",
|
||||
"prepare": "npm run clean && npm run build",
|
||||
"test:smoke": "tsx tests/smoke.ts",
|
||||
"demo": "tsx scripts/demo.ts"
|
||||
},
|
||||
"keywords": ["openclaw", "plugin", "organization", "agents"],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3",
|
||||
"@types/node": "^22.13.10",
|
||||
"tsx": "^4.19.2"
|
||||
}
|
||||
}
|
||||
13
plugin.client.json
Normal file
13
plugin.client.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Yonexus.Client",
|
||||
"version": "0.1.0",
|
||||
"description": "Yonexus client plugin for cross-instance OpenClaw communication",
|
||||
"entry": "dist/client/index.js",
|
||||
"permissions": [],
|
||||
"config": {
|
||||
"mainHost": "",
|
||||
"identifier": "",
|
||||
"notifyBotToken": "",
|
||||
"adminUserId": ""
|
||||
}
|
||||
}
|
||||
21
plugin.json
21
plugin.json
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "yonexus",
|
||||
"version": "0.1.0",
|
||||
"entry": "dist/yonexus/index.js",
|
||||
"description": "Organization hierarchy, agent identity management, query DSL, and scope memory for OpenClaw",
|
||||
"permissions": [
|
||||
"memory_store",
|
||||
"memory_recall"
|
||||
],
|
||||
"config": {
|
||||
"dataFile": "./data/org.json",
|
||||
"registrars": [],
|
||||
"schema": {
|
||||
"position": { "type": "string", "queryable": true },
|
||||
"discord_user_id": { "type": "string", "queryable": true },
|
||||
"git_user_name": { "type": "string", "queryable": true },
|
||||
"department": { "type": "string", "queryable": false },
|
||||
"team": { "type": "string", "queryable": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
15
plugin.server.json
Normal file
15
plugin.server.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Yonexus.Server",
|
||||
"version": "0.1.0",
|
||||
"description": "Yonexus central server plugin for cross-instance OpenClaw communication",
|
||||
"entry": "dist/server/index.js",
|
||||
"permissions": [],
|
||||
"config": {
|
||||
"followerIdentifiers": [],
|
||||
"notifyBotToken": "",
|
||||
"adminUserId": "",
|
||||
"listenHost": "0.0.0.0",
|
||||
"listenPort": 8787,
|
||||
"publicWsUrl": ""
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { Yonexus } from '../src/index';
|
||||
|
||||
const dataFile = path.resolve(process.cwd(), 'data/demo-org.json');
|
||||
if (fs.existsSync(dataFile)) fs.unlinkSync(dataFile);
|
||||
|
||||
const yx = new Yonexus({ dataFile, registrars: ['orion'] });
|
||||
|
||||
yx.registerAgent({ agentId: 'orion' }, 'orion', 'Orion', ['org_admin', 'agent']);
|
||||
yx.registerAgent({ agentId: 'orion' }, 'hangman', 'Hangman', ['agent']);
|
||||
|
||||
const org = yx.createOrganization({ agentId: 'orion' }, 'Yonexus');
|
||||
const dept = yx.createDepartment({ agentId: 'orion' }, 'Platform', org.id);
|
||||
const team = yx.createTeam({ agentId: 'orion' }, 'Core', dept.id);
|
||||
|
||||
yx.assignIdentity({ agentId: 'orion' }, 'orion', dept.id, team.id, {
|
||||
position: 'assistant',
|
||||
discord_user_id: '1474088632750047324',
|
||||
git_user_name: 'orion'
|
||||
});
|
||||
|
||||
yx.setSupervisor({ agentId: 'orion' }, 'orion', 'hangman', dept.id);
|
||||
|
||||
const query = yx.queryAgents(
|
||||
{ agentId: 'orion' },
|
||||
{ deptId: dept.id },
|
||||
{
|
||||
filters: [{ field: 'git_user_name', op: 'eq', value: 'orion' }],
|
||||
options: { limit: 10, offset: 0 }
|
||||
}
|
||||
);
|
||||
|
||||
console.log(JSON.stringify({ dept, team, query, audit: yx.listAuditLogs(20, 0) }, null, 2));
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Yonexus install script
|
||||
# - Registers plugin name: yonexus
|
||||
# - Places build output in dist/yonexus
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DIST_DIR="$ROOT_DIR/dist/yonexus"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [ ! -d node_modules ]; then
|
||||
npm install
|
||||
fi
|
||||
|
||||
npm run build
|
||||
mkdir -p "$DIST_DIR"
|
||||
cp -f "$ROOT_DIR/plugin.json" "$DIST_DIR/plugin.json"
|
||||
|
||||
echo "[yonexus] install complete -> $DIST_DIR"
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { StoreState, YonexusSchema } from "../models/types";
|
||||
|
||||
export const DEFAULT_STATE: StoreState = {
|
||||
organizations: [],
|
||||
departments: [],
|
||||
teams: [],
|
||||
agents: [],
|
||||
identities: [],
|
||||
supervisors: []
|
||||
};
|
||||
|
||||
export const DEFAULT_SCHEMA: YonexusSchema = {
|
||||
position: { type: "string", queryable: true },
|
||||
discord_user_id: { type: "string", queryable: true },
|
||||
git_user_name: { type: "string", queryable: true },
|
||||
department: { type: "string", queryable: false },
|
||||
team: { type: "string", queryable: false }
|
||||
};
|
||||
247
src/index.ts
247
src/index.ts
@@ -1,247 +0,0 @@
|
||||
import path from "node:path";
|
||||
import { DEFAULT_SCHEMA } from "./config/defaults";
|
||||
import type { AuditLogEntry } from "./models/audit";
|
||||
import { YonexusError } from "./models/errors";
|
||||
import type {
|
||||
Actor,
|
||||
Agent,
|
||||
DocsScope,
|
||||
DocsTopic,
|
||||
Identity,
|
||||
QueryInput,
|
||||
Scope,
|
||||
StoreState,
|
||||
YonexusSchema
|
||||
} from "./models/types";
|
||||
import { authorize } from "./permissions/authorize";
|
||||
import { AuditStore } from "./store/auditStore";
|
||||
import { JsonStore } from "./store/jsonStore";
|
||||
import { queryIdentities } from "./tools/query";
|
||||
import { ResourceLayout } from "./tools/resources";
|
||||
import { makeId } from "./utils/id";
|
||||
|
||||
export interface YonexusOptions {
|
||||
dataFile?: string;
|
||||
schema?: YonexusSchema;
|
||||
registrars?: string[];
|
||||
openclawDir?: string;
|
||||
}
|
||||
|
||||
export class Yonexus {
|
||||
private readonly schema: YonexusSchema;
|
||||
private readonly registrars: Set<string>;
|
||||
private readonly store: JsonStore;
|
||||
private readonly audit = new AuditStore();
|
||||
private readonly resources: ResourceLayout;
|
||||
|
||||
constructor(options: YonexusOptions = {}) {
|
||||
const dataFile = options.dataFile ?? path.resolve(process.cwd(), "data/org.json");
|
||||
this.store = new JsonStore(dataFile);
|
||||
this.schema = options.schema ?? DEFAULT_SCHEMA;
|
||||
this.registrars = new Set(options.registrars ?? []);
|
||||
|
||||
const openclawDir =
|
||||
options.openclawDir ??
|
||||
process.env.OPENCLAW_DIR ??
|
||||
path.resolve(process.env.HOME ?? process.cwd(), ".openclaw");
|
||||
this.resources = new ResourceLayout(path.join(openclawDir, "yonexus"));
|
||||
}
|
||||
|
||||
private log(entry: Omit<AuditLogEntry, "id" | "ts">): void {
|
||||
this.audit.append({
|
||||
id: makeId("audit", `${entry.actorId}-${entry.action}-${Date.now()}`),
|
||||
ts: new Date().toISOString(),
|
||||
...entry
|
||||
});
|
||||
}
|
||||
|
||||
createOrganization(actor: Actor, name: string) {
|
||||
authorize("create_organization", actor, {}, this.store);
|
||||
const orgId = makeId("org", name);
|
||||
if (this.store.findOrganization(orgId)) {
|
||||
throw new YonexusError("ALREADY_EXISTS", `organization_exists: ${orgId}`);
|
||||
}
|
||||
const org = this.store.addOrganization({ id: orgId, name });
|
||||
this.resources.ensureOrganization(name);
|
||||
this.log({ actorId: actor.agentId, action: "create_organization", target: org.id, status: "ok" });
|
||||
return org;
|
||||
}
|
||||
|
||||
createDepartment(actor: Actor, name: string, orgId: string) {
|
||||
try {
|
||||
authorize("create_department", actor, { orgId }, this.store);
|
||||
if (!this.store.findOrganization(orgId)) {
|
||||
throw new YonexusError("NOT_FOUND", `organization_not_found: ${orgId}`, { orgId });
|
||||
}
|
||||
const dept = { id: makeId("dept", name), name, orgId };
|
||||
const result = this.store.addDepartment(dept);
|
||||
this.log({ actorId: actor.agentId, action: "create_department", target: result.id, status: "ok" });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.log({
|
||||
actorId: actor.agentId,
|
||||
action: "create_department",
|
||||
target: name,
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
createTeam(actor: Actor, name: string, deptId: string) {
|
||||
authorize("create_team", actor, { deptId }, this.store);
|
||||
const dept = this.store.findDepartment(deptId);
|
||||
if (!dept) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`, { deptId });
|
||||
|
||||
const org = this.store.findOrganization(dept.orgId);
|
||||
if (!org) throw new YonexusError("NOT_FOUND", `organization_not_found: ${dept.orgId}`);
|
||||
|
||||
const team = { id: makeId("team", `${deptId}-${name}`), name, deptId };
|
||||
const result = this.store.addTeam(team);
|
||||
this.resources.ensureTeam(org.name, name);
|
||||
this.log({ actorId: actor.agentId, action: "create_team", target: result.id, status: "ok" });
|
||||
return result;
|
||||
}
|
||||
|
||||
registerAgent(actor: Actor, agentId: string, name: string, roles: Agent["roles"] = ["agent"]) {
|
||||
if (this.registrars.size > 0 && !this.registrars.has(actor.agentId)) {
|
||||
throw new YonexusError("REGISTRAR_DENIED", `registrar_denied: ${actor.agentId}`);
|
||||
}
|
||||
|
||||
const isBootstrap = this.store.listAgents().length === 0 && actor.agentId === agentId;
|
||||
if (!isBootstrap) authorize("register_agent", actor, {}, this.store);
|
||||
|
||||
if (this.store.findAgent(agentId)) {
|
||||
throw new YonexusError("ALREADY_EXISTS", `agent_exists: ${agentId}`, { agentId });
|
||||
}
|
||||
const result = this.store.addAgent({ id: agentId, name, roles });
|
||||
this.log({ actorId: actor.agentId, action: "register_agent", target: result.id, status: "ok" });
|
||||
return result;
|
||||
}
|
||||
|
||||
assignIdentity(actor: Actor, agentId: string, deptId: string, teamId: string, meta: Record<string, string>): Identity {
|
||||
authorize("assign_identity", actor, { deptId, teamId }, this.store);
|
||||
if (!this.store.findAgent(agentId)) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||
|
||||
const dept = this.store.findDepartment(deptId);
|
||||
if (!dept) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||
|
||||
const team = this.store.findTeam(teamId);
|
||||
if (!team) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||
|
||||
const org = this.store.findOrganization(dept.orgId);
|
||||
if (!org) throw new YonexusError("NOT_FOUND", `organization_not_found: ${dept.orgId}`);
|
||||
|
||||
const validatedMeta: Record<string, string> = {};
|
||||
for (const [field, value] of Object.entries(meta)) {
|
||||
if (!this.schema[field]) continue;
|
||||
validatedMeta[field] = String(value);
|
||||
}
|
||||
|
||||
const result = this.store.addIdentity({
|
||||
id: makeId("identity", `${agentId}-${deptId}-${teamId}-${Date.now()}`),
|
||||
agentId,
|
||||
deptId,
|
||||
teamId,
|
||||
meta: validatedMeta
|
||||
});
|
||||
|
||||
this.resources.ensureAgent(org.name, team.name, agentId);
|
||||
this.log({ actorId: actor.agentId, action: "assign_identity", target: result.id, status: "ok" });
|
||||
return result;
|
||||
}
|
||||
|
||||
setSupervisor(actor: Actor, agentId: string, supervisorId: string, deptId?: string) {
|
||||
authorize("set_supervisor", actor, { deptId }, this.store);
|
||||
if (!this.store.findAgent(agentId)) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||
if (!this.store.findAgent(supervisorId)) throw new YonexusError("NOT_FOUND", `supervisor_not_found: ${supervisorId}`);
|
||||
if (agentId === supervisorId) throw new YonexusError("INVALID_SUPERVISOR", "invalid_supervisor: self_reference");
|
||||
const result = this.store.upsertSupervisor({ agentId, supervisorId });
|
||||
this.log({ actorId: actor.agentId, action: "set_supervisor", target: `${agentId}->${supervisorId}`, status: "ok" });
|
||||
return result;
|
||||
}
|
||||
|
||||
whoami(agentId: string) {
|
||||
const agent = this.store.findAgent(agentId);
|
||||
if (!agent) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||
|
||||
const identities = this.store.listIdentities().filter((x) => x.agentId === agentId);
|
||||
const supervisor = this.store.findSupervisor(agentId);
|
||||
return { agent, identities, supervisor };
|
||||
}
|
||||
|
||||
queryAgents(actor: Actor, scope: Scope, query: QueryInput) {
|
||||
authorize("query_agents", actor, scope, this.store);
|
||||
const identities = this.store.listIdentities();
|
||||
return queryIdentities(identities, query, this.schema);
|
||||
}
|
||||
|
||||
getDocs(scope: DocsScope, topic: DocsTopic, keyword: string): string {
|
||||
return this.resources.getDocs(scope, topic, keyword);
|
||||
}
|
||||
|
||||
renameDepartment(actor: Actor, deptId: string, newName: string) {
|
||||
authorize("create_department", actor, {}, this.store);
|
||||
const updated = this.store.renameDepartment(deptId, newName);
|
||||
if (!updated) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||
this.log({ actorId: actor.agentId, action: "rename_department", target: deptId, status: "ok" });
|
||||
return updated;
|
||||
}
|
||||
|
||||
renameTeam(actor: Actor, teamId: string, newName: string, deptId?: string) {
|
||||
authorize("create_team", actor, { deptId }, this.store);
|
||||
const updated = this.store.renameTeam(teamId, newName);
|
||||
if (!updated) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||
this.log({ actorId: actor.agentId, action: "rename_team", target: teamId, status: "ok" });
|
||||
return updated;
|
||||
}
|
||||
|
||||
migrateTeam(actor: Actor, teamId: string, newDeptId: string) {
|
||||
authorize("create_team", actor, { deptId: newDeptId }, this.store);
|
||||
if (!this.store.findDepartment(newDeptId)) throw new YonexusError("NOT_FOUND", `department_not_found: ${newDeptId}`);
|
||||
const updated = this.store.migrateTeam(teamId, newDeptId);
|
||||
if (!updated) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||
this.log({ actorId: actor.agentId, action: "migrate_team", target: `${teamId}->${newDeptId}`, status: "ok" });
|
||||
return updated;
|
||||
}
|
||||
|
||||
deleteDepartment(actor: Actor, deptId: string) {
|
||||
authorize("create_department", actor, {}, this.store);
|
||||
const ok = this.store.deleteDepartment(deptId);
|
||||
if (!ok) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||
this.log({ actorId: actor.agentId, action: "delete_department", target: deptId, status: "ok" });
|
||||
return { ok };
|
||||
}
|
||||
|
||||
deleteTeam(actor: Actor, teamId: string, deptId?: string) {
|
||||
authorize("create_team", actor, { deptId }, this.store);
|
||||
const ok = this.store.deleteTeam(teamId);
|
||||
if (!ok) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||
this.log({ actorId: actor.agentId, action: "delete_team", target: teamId, status: "ok" });
|
||||
return { ok };
|
||||
}
|
||||
|
||||
exportData(actor: Actor): StoreState {
|
||||
authorize("query_agents", actor, {}, this.store);
|
||||
this.log({ actorId: actor.agentId, action: "export_data", status: "ok" });
|
||||
return this.store.snapshot();
|
||||
}
|
||||
|
||||
importData(actor: Actor, state: StoreState): { ok: true } {
|
||||
authorize("create_department", actor, {}, this.store);
|
||||
this.store.replace(state);
|
||||
this.log({ actorId: actor.agentId, action: "import_data", status: "ok" });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
listAuditLogs(limit = 100, offset = 0): AuditLogEntry[] {
|
||||
return this.audit.list(limit, offset);
|
||||
}
|
||||
|
||||
debugSnapshot(): StoreState {
|
||||
return this.store.snapshot();
|
||||
}
|
||||
}
|
||||
|
||||
export default Yonexus;
|
||||
@@ -1,16 +0,0 @@
|
||||
export interface MemoryPort {
|
||||
store(input: { text: string; scope: string; metadata?: Record<string, string> }): Promise<unknown>;
|
||||
recall(input: { query: string; scope: string; limit?: number }): Promise<unknown>;
|
||||
}
|
||||
|
||||
export class ScopeMemory {
|
||||
constructor(private readonly memory: MemoryPort) {}
|
||||
|
||||
async put(scopeId: string, text: string, metadata?: Record<string, string>): Promise<unknown> {
|
||||
return this.memory.store({ text, scope: scopeId, metadata });
|
||||
}
|
||||
|
||||
async search(scopeId: string, query: string, limit = 5): Promise<unknown> {
|
||||
return this.memory.recall({ query, scope: scopeId, limit });
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface AuditLogEntry {
|
||||
id: string;
|
||||
ts: string;
|
||||
actorId: string;
|
||||
action: string;
|
||||
target?: string;
|
||||
status: 'ok' | 'error';
|
||||
message?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export type ErrorCode =
|
||||
| 'PERMISSION_DENIED'
|
||||
| 'NOT_FOUND'
|
||||
| 'ALREADY_EXISTS'
|
||||
| 'VALIDATION_ERROR'
|
||||
| 'FIELD_NOT_QUERYABLE'
|
||||
| 'INVALID_SUPERVISOR'
|
||||
| 'REGISTRAR_DENIED';
|
||||
|
||||
export class YonexusError extends Error {
|
||||
constructor(
|
||||
public readonly code: ErrorCode,
|
||||
message: string,
|
||||
public readonly details?: Record<string, unknown>
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'YonexusError';
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
export type Role = "org_admin" | "dept_admin" | "team_lead" | "agent";
|
||||
|
||||
export interface Organization {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Department {
|
||||
id: string;
|
||||
name: string;
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
deptId: string;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
export interface Identity {
|
||||
id: string;
|
||||
agentId: string;
|
||||
deptId: string;
|
||||
teamId: string;
|
||||
meta: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Supervisor {
|
||||
agentId: string;
|
||||
supervisorId: string;
|
||||
}
|
||||
|
||||
export interface SchemaField {
|
||||
type: "string";
|
||||
queryable: boolean;
|
||||
}
|
||||
|
||||
export interface YonexusSchema {
|
||||
[field: string]: SchemaField;
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
organizations: Organization[];
|
||||
departments: Department[];
|
||||
teams: Team[];
|
||||
agents: Agent[];
|
||||
identities: Identity[];
|
||||
supervisors: Supervisor[];
|
||||
}
|
||||
|
||||
export interface Actor {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| "create_organization"
|
||||
| "create_department"
|
||||
| "create_team"
|
||||
| "register_agent"
|
||||
| "assign_identity"
|
||||
| "set_supervisor"
|
||||
| "query_agents";
|
||||
|
||||
export type DocsScope = "organization" | "department" | "team" | "agent";
|
||||
export type DocsTopic = "docs" | "notes" | "knowledge" | "rules" | "lessons" | "workflows";
|
||||
|
||||
export interface Scope {
|
||||
orgId?: string;
|
||||
deptId?: string;
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export interface QueryFilter {
|
||||
field: string;
|
||||
op: "eq" | "contains" | "regex";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface QueryOptions {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface QueryInput {
|
||||
filters: QueryFilter[];
|
||||
options?: QueryOptions;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { YonexusError } from '../models/errors';
|
||||
import type { Action, Actor, Scope } from "../models/types";
|
||||
import { JsonStore } from "../store/jsonStore";
|
||||
|
||||
function hasRole(store: JsonStore, actor: Actor, role: string): boolean {
|
||||
const me = store.findAgent(actor.agentId);
|
||||
return Boolean(me?.roles.includes(role as never));
|
||||
}
|
||||
|
||||
function inDeptScope(scope: Scope): boolean {
|
||||
return Boolean(scope.deptId);
|
||||
}
|
||||
|
||||
function inTeamScope(scope: Scope): boolean {
|
||||
return Boolean(scope.teamId);
|
||||
}
|
||||
|
||||
export function authorize(action: Action, actor: Actor, scope: Scope, store: JsonStore): void {
|
||||
const orgAdmin = hasRole(store, actor, "org_admin");
|
||||
const deptAdmin = hasRole(store, actor, "dept_admin") && inDeptScope(scope);
|
||||
const teamLead = hasRole(store, actor, "team_lead") && inTeamScope(scope);
|
||||
const agent = hasRole(store, actor, "agent");
|
||||
|
||||
const allowed =
|
||||
(action === "create_organization" && orgAdmin) ||
|
||||
(action === "create_department" && orgAdmin) ||
|
||||
(action === "create_team" && (orgAdmin || deptAdmin)) ||
|
||||
(action === "assign_identity" && (orgAdmin || deptAdmin || teamLead)) ||
|
||||
(action === "register_agent" && (orgAdmin || deptAdmin || teamLead)) ||
|
||||
(action === "set_supervisor" && (orgAdmin || deptAdmin)) ||
|
||||
(action === "query_agents" && (orgAdmin || deptAdmin || teamLead || agent));
|
||||
|
||||
if (!allowed) {
|
||||
throw new YonexusError('PERMISSION_DENIED', `permission_denied: ${action}`, {
|
||||
action,
|
||||
actorId: actor.agentId,
|
||||
scope
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { AuditLogEntry } from '../models/audit';
|
||||
|
||||
const MAX_AUDIT = 1000;
|
||||
|
||||
export class AuditStore {
|
||||
private logs: AuditLogEntry[] = [];
|
||||
|
||||
append(entry: AuditLogEntry): AuditLogEntry {
|
||||
this.logs.push(entry);
|
||||
if (this.logs.length > MAX_AUDIT) this.logs.shift();
|
||||
return entry;
|
||||
}
|
||||
|
||||
list(limit = 100, offset = 0): AuditLogEntry[] {
|
||||
const safeLimit = Math.min(Math.max(1, limit), 500);
|
||||
const safeOffset = Math.max(0, offset);
|
||||
return this.logs.slice(safeOffset, safeOffset + safeLimit);
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import { DEFAULT_STATE } from "../config/defaults";
|
||||
import type {
|
||||
Agent,
|
||||
Department,
|
||||
Identity,
|
||||
Organization,
|
||||
StoreState,
|
||||
Supervisor,
|
||||
Team
|
||||
} from "../models/types";
|
||||
import { readJsonFile, writeJsonFile } from "../utils/fs";
|
||||
|
||||
export class JsonStore {
|
||||
private state: StoreState;
|
||||
|
||||
constructor(private readonly filePath: string) {
|
||||
this.state = readJsonFile<StoreState>(filePath, DEFAULT_STATE);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
writeJsonFile(this.filePath, this.state);
|
||||
}
|
||||
|
||||
snapshot(): StoreState {
|
||||
return JSON.parse(JSON.stringify(this.state)) as StoreState;
|
||||
}
|
||||
|
||||
replace(state: StoreState): void {
|
||||
this.state = JSON.parse(JSON.stringify(state)) as StoreState;
|
||||
this.save();
|
||||
}
|
||||
|
||||
addOrganization(org: Organization): Organization {
|
||||
this.state.organizations.push(org);
|
||||
this.save();
|
||||
return org;
|
||||
}
|
||||
|
||||
addDepartment(dept: Department): Department {
|
||||
this.state.departments.push(dept);
|
||||
this.save();
|
||||
return dept;
|
||||
}
|
||||
|
||||
renameDepartment(deptId: string, name: string): Department | undefined {
|
||||
const dept = this.findDepartment(deptId);
|
||||
if (!dept) return undefined;
|
||||
dept.name = name;
|
||||
this.save();
|
||||
return dept;
|
||||
}
|
||||
|
||||
deleteDepartment(deptId: string): boolean {
|
||||
const before = this.state.departments.length;
|
||||
this.state.departments = this.state.departments.filter((d) => d.id !== deptId);
|
||||
this.state.teams = this.state.teams.filter((t) => t.deptId !== deptId);
|
||||
this.state.identities = this.state.identities.filter((i) => i.deptId !== deptId);
|
||||
const changed = this.state.departments.length !== before;
|
||||
if (changed) this.save();
|
||||
return changed;
|
||||
}
|
||||
|
||||
addTeam(team: Team): Team {
|
||||
this.state.teams.push(team);
|
||||
this.save();
|
||||
return team;
|
||||
}
|
||||
|
||||
renameTeam(teamId: string, name: string): Team | undefined {
|
||||
const team = this.findTeam(teamId);
|
||||
if (!team) return undefined;
|
||||
team.name = name;
|
||||
this.save();
|
||||
return team;
|
||||
}
|
||||
|
||||
migrateTeam(teamId: string, newDeptId: string): Team | undefined {
|
||||
const team = this.findTeam(teamId);
|
||||
if (!team) return undefined;
|
||||
team.deptId = newDeptId;
|
||||
for (const identity of this.state.identities) {
|
||||
if (identity.teamId === teamId) identity.deptId = newDeptId;
|
||||
}
|
||||
this.save();
|
||||
return team;
|
||||
}
|
||||
|
||||
deleteTeam(teamId: string): boolean {
|
||||
const before = this.state.teams.length;
|
||||
this.state.teams = this.state.teams.filter((t) => t.id !== teamId);
|
||||
this.state.identities = this.state.identities.filter((i) => i.teamId !== teamId);
|
||||
const changed = before !== this.state.teams.length;
|
||||
if (changed) this.save();
|
||||
return changed;
|
||||
}
|
||||
|
||||
addAgent(agent: Agent): Agent {
|
||||
this.state.agents.push(agent);
|
||||
this.save();
|
||||
return agent;
|
||||
}
|
||||
|
||||
addIdentity(identity: Identity): Identity {
|
||||
this.state.identities.push(identity);
|
||||
this.save();
|
||||
return identity;
|
||||
}
|
||||
|
||||
upsertSupervisor(rel: Supervisor): Supervisor {
|
||||
const idx = this.state.supervisors.findIndex((x) => x.agentId === rel.agentId);
|
||||
if (idx >= 0) this.state.supervisors[idx] = rel;
|
||||
else this.state.supervisors.push(rel);
|
||||
this.save();
|
||||
return rel;
|
||||
}
|
||||
|
||||
findAgent(agentId: string): Agent | undefined {
|
||||
return this.state.agents.find((a) => a.id === agentId);
|
||||
}
|
||||
|
||||
findOrganization(orgId: string): Organization | undefined {
|
||||
return this.state.organizations.find((o) => o.id === orgId);
|
||||
}
|
||||
|
||||
listOrganizations(): Organization[] {
|
||||
return this.state.organizations;
|
||||
}
|
||||
|
||||
findDepartment(deptId: string): Department | undefined {
|
||||
return this.state.departments.find((d) => d.id === deptId);
|
||||
}
|
||||
|
||||
listDepartments(): Department[] {
|
||||
return this.state.departments;
|
||||
}
|
||||
|
||||
findTeam(teamId: string): Team | undefined {
|
||||
return this.state.teams.find((t) => t.id === teamId);
|
||||
}
|
||||
|
||||
listTeams(): Team[] {
|
||||
return this.state.teams;
|
||||
}
|
||||
|
||||
listAgents(): Agent[] {
|
||||
return this.state.agents;
|
||||
}
|
||||
|
||||
listIdentities(): Identity[] {
|
||||
return this.state.identities;
|
||||
}
|
||||
|
||||
findSupervisor(agentId: string): Supervisor | undefined {
|
||||
return this.state.supervisors.find((s) => s.agentId === agentId);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { YonexusError } from '../models/errors';
|
||||
import type { Identity, QueryFilter, QueryInput, QueryOptions, YonexusSchema } from "../models/types";
|
||||
|
||||
const DEFAULT_LIMIT = 20;
|
||||
const MAX_LIMIT = 100;
|
||||
|
||||
const regexCache = new Map<string, RegExp>();
|
||||
const containsCache = new Map<string, string>();
|
||||
|
||||
function getRegex(pattern: string): RegExp {
|
||||
const cached = regexCache.get(pattern);
|
||||
if (cached) return cached;
|
||||
const created = new RegExp(pattern);
|
||||
regexCache.set(pattern, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
function normalizeNeedle(value: string): string {
|
||||
const cached = containsCache.get(value);
|
||||
if (cached) return cached;
|
||||
const normalized = value.toLowerCase();
|
||||
containsCache.set(value, normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function isQueryable(field: string, schema: YonexusSchema): boolean {
|
||||
return Boolean(schema[field]?.queryable);
|
||||
}
|
||||
|
||||
function matchFilter(identity: Identity, filter: QueryFilter): boolean {
|
||||
const raw = identity.meta[filter.field] ?? "";
|
||||
|
||||
switch (filter.op) {
|
||||
case "eq":
|
||||
return raw === filter.value;
|
||||
case "contains":
|
||||
return raw.toLowerCase().includes(normalizeNeedle(filter.value));
|
||||
case "regex": {
|
||||
const re = getRegex(filter.value);
|
||||
return re.test(raw);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOptions(options?: QueryOptions): Required<QueryOptions> {
|
||||
const limit = Math.min(Math.max(1, options?.limit ?? DEFAULT_LIMIT), MAX_LIMIT);
|
||||
const offset = Math.max(0, options?.offset ?? 0);
|
||||
return { limit, offset };
|
||||
}
|
||||
|
||||
function sortFilters(filters: QueryFilter[]): QueryFilter[] {
|
||||
const weight = (f: QueryFilter): number => {
|
||||
if (f.op === 'eq') return 1;
|
||||
if (f.op === 'contains') return 2;
|
||||
return 3;
|
||||
};
|
||||
return [...filters].sort((a, b) => weight(a) - weight(b));
|
||||
}
|
||||
|
||||
export function queryIdentities(identities: Identity[], input: QueryInput, schema: YonexusSchema): Identity[] {
|
||||
for (const filter of input.filters) {
|
||||
if (!isQueryable(filter.field, schema)) {
|
||||
throw new YonexusError('FIELD_NOT_QUERYABLE', `field_not_queryable: ${filter.field}`, {
|
||||
field: filter.field
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const orderedFilters = sortFilters(input.filters);
|
||||
const filtered = identities.filter((identity) => orderedFilters.every((f) => matchFilter(identity, f)));
|
||||
const { limit, offset } = normalizeOptions(input.options);
|
||||
return filtered.slice(offset, offset + limit);
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { YonexusError } from '../models/errors';
|
||||
import type { DocsScope, DocsTopic } from '../models/types';
|
||||
import { slug } from '../utils/id';
|
||||
|
||||
const TOPICS: DocsTopic[] = ['docs', 'notes', 'knowledge', 'rules', 'lessons', 'workflows'];
|
||||
|
||||
function ensureDirs(base: string, dirs: string[]): void {
|
||||
for (const d of dirs) fs.mkdirSync(path.join(base, d), { recursive: true });
|
||||
}
|
||||
|
||||
export class ResourceLayout {
|
||||
constructor(private readonly rootDir: string) {}
|
||||
|
||||
get organizationsRoot(): string {
|
||||
return path.join(this.rootDir, 'organizations');
|
||||
}
|
||||
|
||||
orgPath(orgName: string): string {
|
||||
return path.join(this.organizationsRoot, slug(orgName));
|
||||
}
|
||||
|
||||
teamPath(orgName: string, teamName: string): string {
|
||||
return path.join(this.orgPath(orgName), 'teams', slug(teamName));
|
||||
}
|
||||
|
||||
agentPath(orgName: string, teamName: string, agentId: string): string {
|
||||
return path.join(this.teamPath(orgName, teamName), 'agents', slug(agentId));
|
||||
}
|
||||
|
||||
ensureOrganization(orgName: string): void {
|
||||
const root = this.orgPath(orgName);
|
||||
ensureDirs(root, ['teams', ...TOPICS]);
|
||||
}
|
||||
|
||||
ensureTeam(orgName: string, teamName: string): void {
|
||||
const root = this.teamPath(orgName, teamName);
|
||||
ensureDirs(root, ['agents', ...TOPICS]);
|
||||
}
|
||||
|
||||
ensureAgent(orgName: string, teamName: string, agentId: string): void {
|
||||
const root = this.agentPath(orgName, teamName, agentId);
|
||||
ensureDirs(root, TOPICS);
|
||||
}
|
||||
|
||||
private readTopicFiles(topicRoot: string, keyword: string): string[] {
|
||||
if (!fs.existsSync(topicRoot)) return [];
|
||||
let re: RegExp;
|
||||
try {
|
||||
re = new RegExp(keyword);
|
||||
} catch {
|
||||
throw new YonexusError('VALIDATION_ERROR', 'invalid_regex', { keyword });
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(topicRoot, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((e) => e.isFile() && re.test(e.name))
|
||||
.map((e) => path.join(topicRoot, e.name));
|
||||
}
|
||||
|
||||
getDocs(scope: DocsScope, topic: DocsTopic, keyword: string): string {
|
||||
const groups: Record<'ORG' | 'DEPT' | 'TEAM' | 'AGENT', string[]> = {
|
||||
ORG: [],
|
||||
DEPT: [],
|
||||
TEAM: [],
|
||||
AGENT: []
|
||||
};
|
||||
|
||||
const orgsRoot = this.organizationsRoot;
|
||||
if (!fs.existsSync(orgsRoot)) {
|
||||
return '----ORG\n----DEPT\n----TEAM\n----AGENT';
|
||||
}
|
||||
|
||||
const orgs = fs.readdirSync(orgsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
||||
|
||||
for (const orgName of orgs) {
|
||||
const orgPath = path.join(orgsRoot, orgName);
|
||||
if (scope === 'organization') {
|
||||
groups.ORG.push(...this.readTopicFiles(path.join(orgPath, topic), keyword));
|
||||
}
|
||||
|
||||
const teamsRoot = path.join(orgPath, 'teams');
|
||||
if (!fs.existsSync(teamsRoot)) continue;
|
||||
|
||||
const teams = fs.readdirSync(teamsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
||||
for (const teamName of teams) {
|
||||
const teamPath = path.join(teamsRoot, teamName);
|
||||
if (scope === 'team') {
|
||||
groups.TEAM.push(...this.readTopicFiles(path.join(teamPath, topic), keyword));
|
||||
}
|
||||
|
||||
const agentsRoot = path.join(teamPath, 'agents');
|
||||
if (!fs.existsSync(agentsRoot)) continue;
|
||||
|
||||
const agents = fs.readdirSync(agentsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
||||
for (const agentId of agents) {
|
||||
if (scope === 'agent') {
|
||||
groups.AGENT.push(...this.readTopicFiles(path.join(agentsRoot, agentId, topic), keyword));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// department folders are not defined in this layout; reserved empty group for compatible output.
|
||||
const printGroup = (k: 'ORG' | 'DEPT' | 'TEAM' | 'AGENT'): string =>
|
||||
[`----${k}`, ...groups[k]].join('\n');
|
||||
|
||||
return [printGroup('ORG'), printGroup('DEPT'), printGroup('TEAM'), printGroup('AGENT')].join('\n');
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export function ensureDirForFile(filePath: string): void {
|
||||
const dir = path.dirname(filePath);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
export function readJsonFile<T>(filePath: string, fallback: T): T {
|
||||
if (!fs.existsSync(filePath)) return fallback;
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
return JSON.parse(raw) as T;
|
||||
}
|
||||
|
||||
export function writeJsonFile(filePath: string, data: unknown): void {
|
||||
ensureDirForFile(filePath);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
const SAFE = /[^a-z0-9]+/g;
|
||||
|
||||
export function slug(input: string): string {
|
||||
return input.trim().toLowerCase().replace(SAFE, "-").replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
export function makeId(prefix: string, input: string): string {
|
||||
const s = slug(input);
|
||||
return `${prefix}:${s || "unknown"}`;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import assert from 'node:assert';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { Yonexus } from '../src/index';
|
||||
import { YonexusError } from '../src/models/errors';
|
||||
|
||||
const root = path.resolve(process.cwd(), 'data/test-openclaw');
|
||||
const dataFile = path.resolve(process.cwd(), 'data/test-org.json');
|
||||
if (fs.existsSync(dataFile)) fs.unlinkSync(dataFile);
|
||||
if (fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
|
||||
|
||||
const app = new Yonexus({ dataFile, registrars: ['orion'], openclawDir: root });
|
||||
|
||||
app.registerAgent({ agentId: 'orion' }, 'orion', 'Orion', ['org_admin', 'agent']);
|
||||
app.registerAgent({ agentId: 'orion' }, 'u1', 'U1', ['agent']);
|
||||
const org = app.createOrganization({ agentId: 'orion' }, 'Yonexus');
|
||||
const dept = app.createDepartment({ agentId: 'orion' }, 'Eng', org.id);
|
||||
const team = app.createTeam({ agentId: 'orion' }, 'API', dept.id);
|
||||
app.assignIdentity({ agentId: 'orion' }, 'u1', dept.id, team.id, {
|
||||
git_user_name: 'u1',
|
||||
position: 'dev'
|
||||
});
|
||||
|
||||
const result = app.queryAgents(
|
||||
{ agentId: 'orion' },
|
||||
{ deptId: dept.id },
|
||||
{ filters: [{ field: 'git_user_name', op: 'eq', value: 'u1' }] }
|
||||
);
|
||||
assert.equal(result.length, 1);
|
||||
|
||||
const expectedAgentDocsDir = path.join(root, 'yonexus', 'organizations', 'yonexus', 'teams', 'api', 'agents', 'u1', 'docs');
|
||||
assert.equal(fs.existsSync(expectedAgentDocsDir), true);
|
||||
|
||||
let thrown = false;
|
||||
try {
|
||||
app.queryAgents(
|
||||
{ agentId: 'orion' },
|
||||
{ deptId: dept.id },
|
||||
{ filters: [{ field: 'team', op: 'eq', value: 'API' }] }
|
||||
);
|
||||
} catch (e) {
|
||||
thrown = e instanceof YonexusError && e.code === 'FIELD_NOT_QUERYABLE';
|
||||
}
|
||||
assert.equal(thrown, true);
|
||||
|
||||
let invalidRegexThrown = false;
|
||||
try {
|
||||
app.getDocs('agent', 'docs', '[');
|
||||
} catch (e) {
|
||||
invalidRegexThrown = e instanceof YonexusError && e.code === 'VALIDATION_ERROR';
|
||||
}
|
||||
assert.equal(invalidRegexThrown, true);
|
||||
|
||||
console.log('smoke test passed');
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist/yonexus",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user