diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 83358b5..0000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -dist/ -*.log -.DS_Store -data/*.json -!data/.gitkeep diff --git a/AGENT_TASKS.md b/AGENT_TASKS.md deleted file mode 100644 index 497ac24..0000000 --- a/AGENT_TASKS.md +++ /dev/null @@ -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 关系不隐含权限 diff --git a/FEAT.md b/FEAT.md deleted file mode 100644 index dc6c33f..0000000 --- a/FEAT.md +++ /dev/null @@ -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//...` - -Auto-create (idempotent): -- On `createOrganization`: - - `teams/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/` -- On `createTeam`: - - `teams//agents/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/` -- On `assignIdentity`: - - `teams//agents//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. diff --git a/PLAN.md b/PLAN.md index 1da6cbf..f0cac13 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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//` contains: `teams/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/` - - On **create_organization**: create `` folder and its subfolders - - On **create_team**: create `organizations//teams//` with `agents/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/` - - On **assign_identity**: create `organizations//teams//agents//` 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 follower’s 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 +``` 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 ``` -## 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 \ No newline at end of file diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..078ed21 --- /dev/null +++ b/PROTOCOL.md @@ -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; +} +``` + +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":"", + "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":"", + "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":"", + "publicKey":"" + } +} +``` + +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. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 985a071..0000000 --- a/README.md +++ /dev/null @@ -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. diff --git a/README.zh.md b/README.zh.md deleted file mode 100644 index 116b412..0000000 --- a/README.zh.md +++ /dev/null @@ -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 中标记为可查询。 diff --git a/data/.gitkeep b/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/examples/sample-data.json b/examples/sample-data.json deleted file mode 100644 index b5b22ba..0000000 --- a/examples/sample-data.json +++ /dev/null @@ -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" } - ] -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6e9980f..0000000 --- a/package-lock.json +++ /dev/null @@ -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" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 6faa4f5..0000000 --- a/package.json +++ /dev/null @@ -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" - } -} diff --git a/plugin.json b/plugin.json deleted file mode 100644 index 6e4a8b4..0000000 --- a/plugin.json +++ /dev/null @@ -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 } - } - } -} diff --git a/scripts/demo.ts b/scripts/demo.ts deleted file mode 100644 index 917d9d8..0000000 --- a/scripts/demo.ts +++ /dev/null @@ -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)); diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100755 index a9153c6..0000000 --- a/scripts/install.sh +++ /dev/null @@ -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" diff --git a/src/config/defaults.ts b/src/config/defaults.ts deleted file mode 100644 index 566b6f5..0000000 --- a/src/config/defaults.ts +++ /dev/null @@ -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 } -}; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 468a331..0000000 --- a/src/index.ts +++ /dev/null @@ -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; - 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): 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): 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 = {}; - 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; diff --git a/src/memory/scopeMemory.ts b/src/memory/scopeMemory.ts deleted file mode 100644 index d21939c..0000000 --- a/src/memory/scopeMemory.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface MemoryPort { - store(input: { text: string; scope: string; metadata?: Record }): Promise; - recall(input: { query: string; scope: string; limit?: number }): Promise; -} - -export class ScopeMemory { - constructor(private readonly memory: MemoryPort) {} - - async put(scopeId: string, text: string, metadata?: Record): Promise { - return this.memory.store({ text, scope: scopeId, metadata }); - } - - async search(scopeId: string, query: string, limit = 5): Promise { - return this.memory.recall({ query, scope: scopeId, limit }); - } -} diff --git a/src/models/audit.ts b/src/models/audit.ts deleted file mode 100644 index 7ff07fd..0000000 --- a/src/models/audit.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface AuditLogEntry { - id: string; - ts: string; - actorId: string; - action: string; - target?: string; - status: 'ok' | 'error'; - message?: string; - meta?: Record; -} diff --git a/src/models/errors.ts b/src/models/errors.ts deleted file mode 100644 index ddfbf23..0000000 --- a/src/models/errors.ts +++ /dev/null @@ -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 - ) { - super(message); - this.name = 'YonexusError'; - } -} diff --git a/src/models/types.ts b/src/models/types.ts deleted file mode 100644 index 32de91f..0000000 --- a/src/models/types.ts +++ /dev/null @@ -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; -} - -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; -} diff --git a/src/permissions/authorize.ts b/src/permissions/authorize.ts deleted file mode 100644 index 7547d5e..0000000 --- a/src/permissions/authorize.ts +++ /dev/null @@ -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 - }); - } -} diff --git a/src/store/auditStore.ts b/src/store/auditStore.ts deleted file mode 100644 index 907afa2..0000000 --- a/src/store/auditStore.ts +++ /dev/null @@ -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); - } -} diff --git a/src/store/jsonStore.ts b/src/store/jsonStore.ts deleted file mode 100644 index 67400bf..0000000 --- a/src/store/jsonStore.ts +++ /dev/null @@ -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(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); - } -} diff --git a/src/tools/query.ts b/src/tools/query.ts deleted file mode 100644 index cf506b6..0000000 --- a/src/tools/query.ts +++ /dev/null @@ -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(); -const containsCache = new Map(); - -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 { - 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); -} diff --git a/src/tools/resources.ts b/src/tools/resources.ts deleted file mode 100644 index 8b0f194..0000000 --- a/src/tools/resources.ts +++ /dev/null @@ -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'); - } -} diff --git a/src/utils/fs.ts b/src/utils/fs.ts deleted file mode 100644 index c015a57..0000000 --- a/src/utils/fs.ts +++ /dev/null @@ -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(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"); -} diff --git a/src/utils/id.ts b/src/utils/id.ts deleted file mode 100644 index af3a63d..0000000 --- a/src/utils/id.ts +++ /dev/null @@ -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"}`; -} diff --git a/tests/smoke.ts b/tests/smoke.ts deleted file mode 100644 index d500d58..0000000 --- a/tests/smoke.ts +++ /dev/null @@ -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'); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 60ab324..0000000 --- a/tsconfig.json +++ /dev/null @@ -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"] -}