reset project and add new yonexus communication plan

This commit is contained in:
nav
2026-03-31 13:59:40 +00:00
parent 00ffef0d8e
commit 83e02829e7
29 changed files with 1465 additions and 2111 deletions

6
.gitignore vendored
View File

@@ -1,6 +0,0 @@
node_modules/
dist/
*.log
.DS_Store
data/*.json
!data/.gitkeep

View File

@@ -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] paginationlimit/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 关系不隐含权限

86
FEAT.md
View File

@@ -1,86 +0,0 @@
# FEAT — Yonexus Feature List
## Existing Features
### 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
### 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`)
---
## New Features (from NEW_FEAT)
### 1) Filesystem Resource Layout
Data-only filesystem tree under:
- `${openclaw dir}/yonexus/organizations/<org-name>/...`
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`
### 2) Document Query Tool
New API:
- `getDocs(scope, topic, keyword)`
Parameters:
- `scope`: `organization | department | team | agent`
- `topic`: `docs | notes | knowledge | rules | lessons | workflows`
- `keyword`: regex string
Behavior:
- Read-only search by filename regex under filesystem resources
- Structured output:
- `----ORG`
- `----DEPT`
- `----TEAM`
- `----AGENT`
- Invalid regex returns structured error (`YonexusError: VALIDATION_ERROR`)
## 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.

654
PLAN.md
View File

@@ -1,127 +1,567 @@
# 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 an OpenClaw plugin for **cross-instance communication** between multiple OpenClaw deployments.
## 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/`
A Yonexus network contains:
- exactly one instance with role `main`
- one or more instances with role `follower`
## 4) Permissions Model (B)
Roles:
- Org Admin
- Dept Admin
- Team Lead
- Agent
The plugin provides:
- a WebSocket-based communication layer between OpenClaw instances
- pairing and identity verification for followers
- persistent follower registry and trust state on the main node
- heartbeat-based follower status tracking
- a rule-based message dispatch mechanism
- TypeScript function interfaces for other plugin/runtime code
This project is **not** an organization/identity management plugin anymore. All prior goals are discarded.
---
## 2. High-Level Architecture
### 2.1 Roles
Each OpenClaw instance running Yonexus must be configured with a `role`:
- `main`
- `follower`
Role semantics:
- `main` is the hub/server for all Yonexus communication
- `follower` connects outbound to the `main` instance
### 2.2 Network Topology
- The `main` instance must expose a fixed reachable IP/domain and run a WebSocket service.
- `follower` instances do not need fixed IP/domain.
- All `follower` instances connect to the `main` WebSocket endpoint.
- No direct follower-to-follower communication is required in v1.
- Messages between followers, if needed, are relayed by `main`.
### 2.3 Runtime Lifecycle
- On OpenClaw gateway startup:
- if role is `main`, Yonexus starts a WebSocket server through a hook
- if role is `follower`, Yonexus starts a WebSocket client and attempts to connect to `mainHost`
---
## 3. Configuration Model
## 3.1 Common Config
```ts
role: "main" | "follower"
```
## 3.2 Follower Config
Required when `role === "follower"`:
```ts
mainHost: string
identifier: string
```
Semantics:
- `mainHost`: WebSocket endpoint of the main instance (`ip:port` or full URL)
- `identifier`: unique follower identity inside the Yonexus network
## 3.3 Main Config
Required when `role === "main"`:
```ts
followerIdentifiers: string[]
```
Semantics:
- `followerIdentifiers`: allowlist of follower identifiers that are permitted to pair/connect
## 3.4 Validation Rules
### Main
- must have `role = main`
- must provide `followerIdentifiers`
- must expose a stable/reachable IP/domain outside the plugin itself
### Follower
- must have `role = follower`
- must provide `mainHost`
- must provide `identifier`
### Shared
- invalid or missing role-specific fields must fail plugin initialization
- unknown follower identifiers must be rejected by `main`
---
## 4. Main Responsibilities
The `main` instance must maintain a registry keyed by follower `identifier`.
Each follower record contains at minimum:
- `identifier`
- `publicKey`
- `secret`
- pairing state
- pairing expiration data
- connection status
- security counters/window data
- heartbeat timestamps
- last known connection/session metadata
The registry must use:
- in-memory runtime state for active operations
- persistent on-disk storage for restart survival
### 4.1 Persistent Main Registry Model
Proposed shape:
```ts
interface FollowerRecord {
identifier: string;
publicKey?: string;
secret?: string;
pairingStatus: "unpaired" | "pending" | "paired" | "revoked";
pairingCode?: string;
pairingExpiresAt?: number;
status: "online" | "offline" | "unstable";
lastHeartbeatAt?: number;
lastAuthenticatedAt?: number;
recentNonces: Array<{
nonce: string;
timestamp: number;
}>;
recentHandshakeAttempts: number[];
createdAt: number;
updatedAt: number;
}
```
Notes:
- `recentNonces` stores only the recent nonce window needed for replay detection
- `recentHandshakeAttempts` stores timestamps for rate-limiting / unsafe reconnect detection
- actual field names can change during implementation, but these semantics must remain
---
## 5. Pairing and Authentication Flow
## 5.1 First Connection: Key Generation
When a follower connects to main for the first time:
- the follower generates a public/private key pair locally
- the private key remains only on the follower
- the public key is sent to `main` during handshake
If `main` sees that:
- the follower identifier is allowed, and
- no valid `secret` is currently associated with that identifier
then `main` must enter pairing flow.
## 5.2 Pairing Flow
### Step A: Pairing Request
`main` responds with a pairing request containing:
- a random pairing string
- an expiration time
### Step B: Pairing Confirmation
If the follower sends that random pairing string back to `main` before expiration:
- pairing succeeds
### Step C: Secret Issuance
After successful pairing:
- `main` generates a random `secret`
- `main` returns that `secret` to the follower
- `main` stores follower `publicKey` + `secret`
- `follower` stores private key + secret locally
If pairing expires before confirmation:
- pairing fails
- follower must restart the pairing process
## 5.3 Reconnection Authentication Flow
After pairing is complete, future follower authentication must use:
- the stored `secret`
- a 24-character random nonce
- current UTC Unix timestamp
The follower builds a plaintext proof payload from:
- `secret`
- `nonce`
- `timestamp`
Concatenation order:
```text
secret + nonce + timestamp
```
The follower encrypts/signs this payload using its private key and sends it to `main`.
`main` verifies:
1. the follower identifier is known and paired
2. the public key matches stored state
3. decrypted/verified payload contains the correct `secret`
4. timestamp difference from current UTC time is less than 10 seconds
5. nonce does not collide with the recent nonce window
6. handshake attempts in the last 10 seconds do not exceed 10
If all checks pass:
- authentication succeeds
- follower is considered authenticated for the connection/session
If any check fails:
- authentication fails
- main may downgrade/revoke trust state
## 5.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 follower must re-pair
- main should clear or rotate the stored `secret`
- main should reset security windows as part of re-pairing
---
## 6. Heartbeat and Follower Status
The main instance must track each followers liveness state:
- `online`
- `unstable`
- `offline`
## 6.1 Heartbeat Rules
Each follower must send a heartbeat to main every 5 minutes.
## 6.2 Status Transitions
### online
A follower is `online` when:
- it has an active authenticated WebSocket connection, and
- main has received a recent heartbeat
### unstable
A follower becomes `unstable` when:
- no heartbeat has been received for 7 minutes
### offline
A follower becomes `offline` when:
- no heartbeat has been received for 11 minutes
When follower becomes `offline`:
- main must close/terminate the WebSocket connection for that follower
## 6.3 Status Evaluation Strategy
Main should run a periodic status sweep timer to evaluate heartbeat freshness.
Recommended initial interval:
- every 30 to 60 seconds
---
## 7. Messaging Model
Yonexus provides rule-based message dispatch over WebSocket.
## 7.1 Base Message Format
All application messages must use the format:
```text
${rule_identifier}::${message_content}
```
Constraints:
- `rule_identifier` is a string token
- `message_content` is the remainder payload as string
## 7.2 Main-Side Rewriting
When `main` receives a message from a follower, before rule matching it must rewrite the message into:
```text
${rule_identifier}::${sender_identifier}::${message_content}
```
This ensures rule processors on `main` can identify which follower sent the message.
## 7.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`.
---
## 8. Rule Registration and Dispatch
## 8.1 Public API
```ts
registerRule(rule: string, processor: (message: string) => unknown): void
```
## 8.2 Rule Format
`rule` must use the format:
```text
${rule_identifier}
```
Validation rules:
- must be non-empty
- must not contain the message delimiter sequence in invalid ways
- must not equal `builtin`
## 8.3 Dispatch Rules
When Yonexus receives a message over WebSocket:
- it iterates registered rules in registration order
- it finds the first matching rule
- it invokes the corresponding processor
- only the first match is used
Clarification for implementation:
- matching should initially be exact match on `rule_identifier`
- if pattern-based matching is desired later, that must be explicitly added in a future phase
If no rule matches:
- the message is ignored or logged as unhandled, depending on runtime policy
---
## 9. TypeScript API Surface
## 9.1 sendMessageToMain
```ts
sendMessageToMain(message: string): Promise<void>
```
Rules:
- Supervisor is **not** a role (no inherent permissions)
- Registration **not** self-service
- only configured agent list or human via slash command
- allowed only on `follower`
- calling from `main` must throw an error
- sends message to connected `main`
- 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`
## 9.2 sendMessageToFollower
## 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
sendMessageToFollower(identifier: string, message: string): Promise<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:
- allowed only on `main`
- calling from `follower` must throw an error
- target follower must be known and currently connected/authenticated
- message must already conform to `${rule_identifier}::${message_content}`
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}
}
## 9.3 registerRule
```ts
registerRule(rule: string, processor: (message: string) => unknown): 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:
- rejects `builtin`
- rejects duplicate rule registration unless an explicit override mode is added later
- processors are invoked with the final received string after any main-side rewrite
## 8) Milestones
**Phase 0 (Design)**
- finalize schema
- confirm permission rules
---
**Phase 1 (MVP)**
- storage + JSON persistence
- core models + tools
- query DSL
- scope memory adapter
## 10. Hooks and Runtime Integration
**Phase 2 (v1)**
- policy refinements
- better query pagination & filtering
- management commands & validation
## 10.1 Main Hook
## 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
```
The plugin must register a hook so that when OpenClaw gateway starts:
- Yonexus initializes internal state
- Yonexus starts a WebSocket server
- Yonexus begins follower status sweep tasks
## 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`**
## 10.2 Follower Runtime Behavior
## 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.
On startup, follower should:
- load local identity/secret/private key state
- connect to `mainHost`
- perform pairing or authentication flow
- start periodic heartbeats when authenticated
- attempt reconnect when disconnected
## 10.3 Persistence Requirements
### Main persists:
- follower registry
- public keys
- secrets
- pairing state
- security/rate-limit windows if needed across restart, or resets them safely
### Follower persists:
- identifier
- private key
- current secret
- minimal pairing/auth state needed for reconnect
---
## 11. Storage Strategy
## 11.1 Main Storage
Main needs a local data file for follower registry persistence.
Suggested persisted sections:
- trusted followers
- pairing pending records
- last known status metadata
- security-related rolling records when persistence is desirable
## 11.2 Follower Storage
Follower needs a local secure data file for:
- private key
- secret
- identifier
- optional last successful connection metadata
## 11.3 Security Notes
- private key must never be sent to main
- secret must be treated as sensitive material
- storage format should support future encryption-at-rest, but plaintext local file may be acceptable in initial implementation if clearly documented as a limitation
---
## 12. Error Handling
The plugin should define structured errors for at least:
- invalid configuration
- invalid role usage
- unauthorized identifier
- pairing required
- pairing expired
- handshake verification failed
- replay/nonce collision detected
- rate limit / unsafe handshake detected
- follower not connected
- duplicate rule registration
- reserved rule registration
- malformed message
---
## 13. Initial Implementation Phases
## Phase 0 — Protocol and Skeleton
- finalize config schema
- define persisted data models
- define protocol message types for builtin traffic
- define hook startup behavior
- define rule registry behavior
## Phase 1 — Main/Follower Transport MVP
- main WebSocket server startup
- follower WebSocket client startup
- reconnect logic
- basic builtin protocol channel
- persistent registry scaffolding
## Phase 2 — Pairing and Authentication
- follower keypair generation
- pairing request/confirmation flow
- secret issuance and persistence
- signed/encrypted handshake proof verification
- nonce/replay protection
- unsafe-condition reset to pairing
## Phase 3 — Heartbeat and Status Tracking
- follower heartbeat sender
- main heartbeat receiver
- periodic sweep
- status transitions: online / unstable / offline
- forced disconnect on offline
## Phase 4 — Public APIs and Message Dispatch
- `sendMessageToMain`
- `sendMessageToFollower`
- `registerRule`
- first-match dispatch
- main-side sender rewrite behavior
## Phase 5 — Hardening and Docs
- integration tests
- failure-path coverage
- restart recovery checks
- protocol docs
- operator setup docs for main/follower deployment
---
## 14. Non-Goals for Initial Version
Not required in the first version unless explicitly added later:
- direct follower-to-follower sockets
- multi-main clustering
- distributed consensus
- message ordering guarantees across reconnects
- end-to-end application payload encryption beyond the handshake/authentication requirements
- UI management panel
---
## 15. Open Questions To Confirm Later
These should be resolved before implementation starts:
1. Is the handshake primitive meant to be:
- asymmetric encryption with private/public key, or
- digital signature with verification by public key?
Recommended: **signature**, not “private-key encryption” wording.
2. Should `mainHost` accept only full WebSocket URLs (`ws://` / `wss://`) or also raw `ip:port` strings?
3. Should pairing require explicit operator approval on main, or is allowlist membership enough for automatic pairing?
4. On unsafe condition, should the old public key be retained or must the follower generate a brand-new keypair?
5. Should offline followers be allowed queued outbound messages from main, or should send fail immediately?
6. Are rule identifiers exact strings only, or should regex/prefix matching exist in future?
---
## 16. Immediate Next Deliverables
After this plan, the next files to create should be:
- `FEAT.md` — feature checklist derived from this plan
- `README.md` — concise operator/developer overview
- `plugin.json` — plugin config schema and entry declaration
- protocol notes for builtin messages
- implementation task breakdown

918
PROTOCOL.md Normal file
View File

@@ -0,0 +1,918 @@
# Yonexus Protocol Specification
Version: draft v0.1
Status: planning
---
## 1. Purpose
This document defines the **Yonexus built-in communication protocol** used between OpenClaw instances.
Yonexus has two roles:
- `main`: the central WebSocket server/hub
- `follower`: an outbound WebSocket client connected to `main`
This protocol covers only the **built-in/system channel** required for:
- connection setup
- pairing
- authentication
- heartbeat
- status/lifecycle events
- protocol-level errors
Application-level business messages are transported separately through the Yonexus rule dispatch layer, but they still use the same WebSocket connection.
---
## 2. Transport
## 2.1 Protocol Transport
Transport is WebSocket.
- `main` listens as WebSocket server
- `follower` connects as WebSocket client
- all protocol frames are UTF-8 text messages in the first version
Binary frames are not required in v1.
## 2.2 Endpoint
The `follower` connects to `mainHost`, which may be configured as:
- full URL: `ws://host:port/path` or `wss://host:port/path`
- or raw `host:port` if implementation normalizes it
Recommended canonical configuration for docs and production:
- prefer full WebSocket URL
---
## 3. Message Categories
Yonexus messages over WebSocket are split into two categories:
### 3.1 Builtin Protocol Messages
Builtin/system messages always use the reserved rule identifier:
```text
builtin::${json_payload}
```
User code must never register a handler for `builtin`.
### 3.2 Application Rule Messages
Application messages use the normal format:
```text
${rule_identifier}::${message_content}
```
When received by `main` from a follower, before dispatch they are rewritten to:
```text
${rule_identifier}::${sender_identifier}::${message_content}
```
---
## 4. Builtin Message Envelope
Builtin messages use this wire format:
```text
builtin::{JSON}
```
Where the JSON payload has this envelope shape:
```ts
interface BuiltinEnvelope {
type: string;
requestId?: string;
timestamp?: number;
payload?: Record<string, unknown>;
}
```
Field semantics:
- `type`: builtin message type
- `requestId`: optional correlation id for request/response pairs
- `timestamp`: sender-side UTC unix timestamp in seconds or milliseconds (must be standardized in implementation)
- `payload`: type-specific object
## 4.1 Timestamp Unit
To avoid ambiguity, protocol implementation should standardize on:
```text
UTC Unix timestamp in seconds
```
If milliseconds are ever used internally, they must not leak into protocol payloads without explicit versioning.
---
## 5. Builtin Message Types
The first protocol version should define these builtin message types.
## 5.1 Session / Connection
### `hello`
Sent by follower immediately after WebSocket connection opens.
Purpose:
- identify follower intent
- declare identifier
- advertise available auth material/state
Example:
```text
builtin::{
"type":"hello",
"requestId":"req_001",
"timestamp":1711886400,
"payload":{
"identifier":"follower-a",
"hasSecret":true,
"hasKeyPair":true,
"publicKey":"<optional-public-key>",
"protocolVersion":"1"
}
}
```
Notes:
- `publicKey` may be included during first connection and may also be re-sent during re-pair
- `hasSecret` indicates whether follower believes it already holds a valid secret
- `hasKeyPair` indicates whether follower has generated a key pair locally
### `hello_ack`
Sent by main in response to `hello`.
Purpose:
- acknowledge identifier
- indicate required next step
Possible next actions:
- `pair_required`
- `auth_required`
- `rejected`
Example:
```text
builtin::{
"type":"hello_ack",
"requestId":"req_001",
"timestamp":1711886401,
"payload":{
"identifier":"follower-a",
"nextAction":"pair_required"
}
}
```
---
## 5.2 Pairing Flow
### `pair_request`
Sent by main when follower needs pairing.
Purpose:
- start pairing challenge
- deliver pairing code and expiry
Example:
```text
builtin::{
"type":"pair_request",
"requestId":"req_002",
"timestamp":1711886402,
"payload":{
"identifier":"follower-a",
"pairingCode":"ABCD-1234-XYZ",
"expiresAt":1711886702,
"ttlSeconds":300,
"publicKeyAccepted":true
}
}
```
### `pair_confirm`
Sent by follower to confirm pairing.
Purpose:
- prove receipt of pairing code before expiry
Example:
```text
builtin::{
"type":"pair_confirm",
"requestId":"req_002",
"timestamp":1711886410,
"payload":{
"identifier":"follower-a",
"pairingCode":"ABCD-1234-XYZ"
}
}
```
### `pair_success`
Sent by main after successful pairing.
Purpose:
- return generated secret
- confirm trusted pairing state
Example:
```text
builtin::{
"type":"pair_success",
"requestId":"req_002",
"timestamp":1711886411,
"payload":{
"identifier":"follower-a",
"secret":"<random-secret>",
"pairedAt":1711886411
}
}
```
### `pair_failed`
Sent by main if pairing fails.
Example reasons:
- expired
- invalid_code
- identifier_not_allowed
- internal_error
Example:
```text
builtin::{
"type":"pair_failed",
"requestId":"req_002",
"timestamp":1711886710,
"payload":{
"identifier":"follower-a",
"reason":"expired"
}
}
```
---
## 5.3 Authentication Flow
After pairing, reconnect authentication uses the stored `secret`, nonce, timestamp, and follower private key.
## 5.3.1 Authentication Payload Construction
Follower constructs plaintext proof data as:
```text
secret + nonce + timestamp
```
Where:
- `secret`: the current shared secret issued by main
- `nonce`: 24 random characters
- `timestamp`: current UTC Unix timestamp in seconds
Recommended future improvement:
- use a canonical delimiter or JSON encoding before signing to avoid ambiguity
For v1 planning, the required logical content is unchanged, but implementation should prefer a canonical serialized object like:
```json
{
"secret":"...",
"nonce":"...",
"timestamp":1711886500
}
```
and then sign the serialized bytes.
## 5.3.2 Signature Primitive
The requirement says the follower uses the private key to encrypt/sign the proof.
For implementation, the recommended primitive is:
- **digital signature using the private key**
- verified by main using the stored public key
This should replace any literal “private-key encryption” wording in implementation docs.
### `auth_request`
Sent by follower to authenticate after pairing.
Example:
```text
builtin::{
"type":"auth_request",
"requestId":"req_003",
"timestamp":1711886500,
"payload":{
"identifier":"follower-a",
"nonce":"RANDOM24CHARACTERSTRINGX",
"proofTimestamp":1711886500,
"signature":"<base64-signature>",
"publicKey":"<optional-public-key-if-rotating>"
}
}
```
Validation performed by main:
1. identifier is allowlisted
2. identifier exists in registry
3. follower is in paired state
4. public key matches expected key if provided
5. signature verifies successfully against canonical proof payload
6. proof contains correct secret
7. `abs(now - proofTimestamp) < 10`
8. nonce has not appeared in recent nonce window
9. handshake attempts in last 10 seconds do not exceed 10
### `auth_success`
Sent by main on successful authentication.
Example:
```text
builtin::{
"type":"auth_success",
"requestId":"req_003",
"timestamp":1711886501,
"payload":{
"identifier":"follower-a",
"authenticatedAt":1711886501,
"status":"online"
}
}
```
### `auth_failed`
Sent by main if authentication fails.
Allowed failure reasons include:
- unknown_identifier
- not_paired
- invalid_signature
- invalid_secret
- stale_timestamp
- future_timestamp
- nonce_collision
- rate_limited
- re_pair_required
Example:
```text
builtin::{
"type":"auth_failed",
"requestId":"req_003",
"timestamp":1711886501,
"payload":{
"identifier":"follower-a",
"reason":"stale_timestamp",
"rePairRequired":false
}
}
```
### `re_pair_required`
Sent by main when unsafe conditions or trust reset require full pairing again.
Example:
```text
builtin::{
"type":"re_pair_required",
"requestId":"req_004",
"timestamp":1711886510,
"payload":{
"identifier":"follower-a",
"reason":"nonce_collision"
}
}
```
---
## 5.4 Heartbeat
### `heartbeat`
Sent by follower every 5 minutes after authentication.
Example:
```text
builtin::{
"type":"heartbeat",
"timestamp":1711886800,
"payload":{
"identifier":"follower-a",
"status":"alive"
}
}
```
### `heartbeat_ack`
Optional response by main.
Purpose:
- confirm receipt
- provide server time or status hints if desired
Example:
```text
builtin::{
"type":"heartbeat_ack",
"timestamp":1711886801,
"payload":{
"identifier":"follower-a",
"status":"online"
}
}
```
---
## 5.5 Status / Lifecycle Notifications
### `status_update`
Sent by main when follower state changes.
Example:
```text
builtin::{
"type":"status_update",
"timestamp":1711887220,
"payload":{
"identifier":"follower-a",
"status":"unstable",
"reason":"heartbeat_timeout_7m"
}
}
```
### `disconnect_notice`
Sent before main deliberately closes a follower connection.
Example:
```text
builtin::{
"type":"disconnect_notice",
"timestamp":1711887460,
"payload":{
"identifier":"follower-a",
"reason":"heartbeat_timeout_11m"
}
}
```
---
## 5.6 Errors
### `error`
Generic protocol-level error envelope.
Example:
```text
builtin::{
"type":"error",
"requestId":"req_999",
"timestamp":1711887000,
"payload":{
"code":"MALFORMED_MESSAGE",
"message":"missing type field"
}
}
```
Recommended builtin error codes:
- `MALFORMED_MESSAGE`
- `UNSUPPORTED_PROTOCOL_VERSION`
- `IDENTIFIER_NOT_ALLOWED`
- `PAIRING_REQUIRED`
- `PAIRING_EXPIRED`
- `AUTH_FAILED`
- `NONCE_COLLISION`
- `RATE_LIMITED`
- `RE_PAIR_REQUIRED`
- `FOLLOWER_OFFLINE`
- `INTERNAL_ERROR`
---
## 6. Builtin State Machines
## 6.1 Follower State Machine
Suggested follower 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 (on socket close)
-> connecting
```
On `re_pair_required`:
```text
authenticated | authenticating -> pairing_required
```
## 6.2 Main-Side Follower Trust State
Per follower trust state:
- `unpaired`
- `pending`
- `paired`
- `revoked`
Per follower liveness state:
- `online`
- `unstable`
- `offline`
These are related but not identical.
Example:
- follower may be `paired` + `offline`
- follower may be `pending` + `offline`
---
## 7. Security Windows and Replay Protection
## 7.1 Nonce Requirements
Nonce rules:
- exactly 24 random characters
- generated fresh for each auth attempt
- must not repeat within the recent security window
## 7.2 Recent Nonce Window
Main stores for each follower:
- the last 10 nonces seen within the recent validity window
If a nonce collides with the recent stored set:
- authentication must fail
- main must mark the situation unsafe
- follower must re-pair
## 7.3 Handshake Attempt Window
Main stores recent handshake attempt timestamps for each follower.
If more than 10 handshake attempts occur within 10 seconds:
- authentication must fail
- main must mark situation unsafe
- follower must re-pair
## 7.4 Time Drift Validation
Main validates:
```text
abs(current_utc_unix_time - proofTimestamp) < 10
```
If validation fails:
- auth fails
- no successful session is established
Implementation note:
- use server time only on main as source of truth
---
## 8. Rule Message Dispatch Semantics
## 8.1 Message Format
All non-builtin messages use:
```text
${rule_identifier}::${message_content}
```
### Examples
Follower to main:
```text
chat_sync::{"conversationId":"abc","body":"hello"}
```
Main rewrites before matching:
```text
chat_sync::follower-a::{"conversationId":"abc","body":"hello"}
```
## 8.2 Rule Matching
Initial rule matching should be exact string match against `rule_identifier`.
Dispatch algorithm:
1. parse first delimiter section as `rule_identifier`
2. if `rule_identifier === builtin`, route to builtin protocol handler
3. otherwise iterate registered rules in registration order
4. invoke the first exact match
5. ignore/log if no match is found
## 8.3 Processor Input
Processor receives the final message string exactly as seen after protocol-level rewrite.
This means:
- on follower, processor input remains `${rule_identifier}::${message_content}`
- on main for follower-originated messages, processor input becomes `${rule_identifier}::${sender_identifier}::${message_content}`
---
## 9. Connection Rules
## 9.1 Allowed Connection Policy
Main should reject connection attempts when:
- identifier is absent
- identifier is not in configured `followerIdentifiers`
- protocol version is unsupported
- builtin hello/auth payload is malformed
## 9.2 One Active Connection Per Identifier
Recommended v1 policy:
- only one active authenticated connection per follower identifier
If a second connection for the same identifier appears:
- main may reject the new one, or
- terminate the previous one and accept the new one
This behavior must be chosen explicitly in implementation.
Recommended default:
- terminate old, accept new after successful auth
---
## 10. Persistence Semantics
## 10.1 Main Persists
At minimum:
- identifier
- public key
- secret
- trust state
- pairing code + expiry if pairing is pending
- 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 Follower Persists
At minimum:
- identifier
- private key
- secret
- protocol version if useful
- last successful pair/auth metadata if useful
---
## 11. Versioning
Protocol payloads should include a protocol version during `hello`.
Initial protocol version:
```text
1
```
If main does not support the provided version, it should respond with:
- `error`
- code: `UNSUPPORTED_PROTOCOL_VERSION`
---
## 12. Recommended Canonical JSON Shapes
To reduce ambiguity during implementation, these payload models are recommended.
```ts
interface HelloPayload {
identifier: string;
hasSecret: boolean;
hasKeyPair: boolean;
publicKey?: string;
protocolVersion: string;
}
interface PairRequestPayload {
identifier: string;
pairingCode: string;
expiresAt: number;
ttlSeconds: number;
publicKeyAccepted: boolean;
}
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 End-to-End Flows
## 13.1 First-Time Pairing Flow
```text
Follower connects WS
Follower -> builtin::hello
Main -> builtin::hello_ack(nextAction=pair_required)
Main -> builtin::pair_request(pairingCode, expiresAt)
Follower -> builtin::pair_confirm(pairingCode)
Main -> builtin::pair_success(secret)
Follower stores secret
Follower -> builtin::auth_request(signature over secret+nonce+timestamp)
Main -> builtin::auth_success
Follower enters authenticated state
```
## 13.2 Normal Reconnect Flow
```text
Follower connects WS
Follower -> builtin::hello(hasSecret=true)
Main -> builtin::hello_ack(nextAction=auth_required)
Follower -> builtin::auth_request(...)
Main -> builtin::auth_success
Follower begins heartbeat schedule
```
## 13.3 Unsafe Replay / Collision Flow
```text
Follower -> builtin::auth_request(nonce collision)
Main detects unsafe condition
Main -> builtin::re_pair_required(reason=nonce_collision)
Main invalidates existing secret/session trust
Follower returns to pairing_required state
```
## 13.4 Heartbeat Timeout Flow
```text
Follower authenticated
No heartbeat for 7 min -> main marks unstable
No heartbeat for 11 min -> main marks offline
Main -> builtin::disconnect_notice
Main closes WS connection
```
---
## 14. Implementation Notes
## 14.1 Parsing
Because the top-level wire format is string-based with `::` delimiters:
- only the first delimiter split should determine the `rule_identifier`
- for `builtin`, the remainder should be treated as JSON string and parsed once
- message content itself may contain `::`, so avoid naive full split logic
## 14.2 Payload Encoding
Recommended message content for application rules:
- JSON string payloads where applicable
- but Yonexus itself only requires string content
## 14.3 Logging
Main should log at least:
- connection open/close
- hello received
- pairing created/succeeded/expired/failed
- auth success/failure
- nonce collisions
- handshake rate-limit triggers
- status transitions
- unhandled rule messages
Sensitive values must never be logged in plaintext:
- `secret`
- private key
- raw proof material
- full signature verification internals unless safely redacted
---
## 15. Open Clarifications
Before implementation, these should be finalized:
1. Exact signing algorithm:
- Ed25519 is a strong default candidate
2. Secret length and generation requirements
3. Pairing code format and length
4. Whether `pair_request` should require operator confirmation or stay automatic for allowlisted identifiers
5. Whether `heartbeat_ack` is mandatory or optional
6. Whether follower should auto-reconnect with backoff strategy after disconnect
7. Whether duplicate active connections should replace old sessions or be rejected
---
## 16. Summary of Reserved Builtin Types
Current 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.

112
README.md
View File

@@ -1,112 +0,0 @@
[English](./README.md) | [中文](./README.zh.md)
---
# Yonexus
Yonexus is an OpenClaw plugin for organization hierarchy and agent identity management.
## 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
## Project Layout
```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/
```
## Requirements
- Node.js 22+
- npm 10+
## Quick Start
```bash
npm install
npm run build
bash scripts/install.sh
npm run test:smoke
npm run demo
```
## Configuration
`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
```
## 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.

View File

@@ -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 中标记为可查询。

View File

View File

@@ -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
View File

@@ -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"
}
}
}

View File

@@ -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"
}
}

View File

@@ -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 }
}
}
}

View File

@@ -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));

View File

@@ -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"

View File

@@ -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 }
};

View File

@@ -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;

View File

@@ -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 });
}
}

View File

@@ -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>;
}

View File

@@ -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';
}
}

View File

@@ -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;
}

View File

@@ -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
});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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');
}
}

View File

@@ -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");
}

View File

@@ -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"}`;
}

View File

@@ -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');

View File

@@ -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"]
}