feat: add filesystem resource layout and docs query tool; replace NEW_FEAT with FEAT
This commit is contained in:
86
FEAT.md
Normal file
86
FEAT.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# FEAT — Yonexus Feature List
|
||||||
|
|
||||||
|
## Existing Features
|
||||||
|
|
||||||
|
### Core Model & Storage
|
||||||
|
- Organization / Department / Team / Agent / Identity / Supervisor data model
|
||||||
|
- In-memory runtime with JSON persistence (`data/org.json`)
|
||||||
|
- Import/export of structure data
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
- Role model: `org_admin`, `dept_admin`, `team_lead`, `agent`
|
||||||
|
- `authorize(action, actor, scope)` permission check
|
||||||
|
- Registrar whitelist (`registrars`) and bootstrap registration support
|
||||||
|
|
||||||
|
### Core APIs
|
||||||
|
- `createOrganization(actor, name)`
|
||||||
|
- `createDepartment(actor, name, orgId)`
|
||||||
|
- `createTeam(actor, name, deptId)`
|
||||||
|
- `registerAgent(actor, agentId, name, roles?)`
|
||||||
|
- `assignIdentity(actor, agentId, deptId, teamId, meta)`
|
||||||
|
- `setSupervisor(actor, agentId, supervisorId, deptId?)`
|
||||||
|
- `whoami(agentId)`
|
||||||
|
- `queryAgents(actor, scope, query)`
|
||||||
|
|
||||||
|
### Query & Search
|
||||||
|
- Query ops: `eq`, `contains`, `regex`
|
||||||
|
- Schema `queryable` whitelist enforcement
|
||||||
|
- Pagination (`limit`, `offset`)
|
||||||
|
- Basic query performance optimization (regex cache + ordered filter eval)
|
||||||
|
|
||||||
|
### Management & Audit
|
||||||
|
- `renameDepartment`, `renameTeam`, `migrateTeam`, `deleteDepartment`, `deleteTeam`
|
||||||
|
- Structured errors via `YonexusError`
|
||||||
|
- In-memory audit log (`listAuditLogs`)
|
||||||
|
|
||||||
|
### Scope Memory
|
||||||
|
- Scope memory adapter:
|
||||||
|
- `scope_memory.put(scopeId, text, metadata)`
|
||||||
|
- `scope_memory.search(scopeId, query, limit)`
|
||||||
|
|
||||||
|
### Developer Experience
|
||||||
|
- `README.md` + `README.zh.md`
|
||||||
|
- Example data (`examples/sample-data.json`)
|
||||||
|
- Demo script (`scripts/demo.ts`)
|
||||||
|
- Smoke test (`tests/smoke.ts`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New Features (from NEW_FEAT)
|
||||||
|
|
||||||
|
### 1) Filesystem Resource Layout
|
||||||
|
Data-only filesystem tree under:
|
||||||
|
- `${openclaw dir}/yonexus/organizations/<org-name>/...`
|
||||||
|
|
||||||
|
Auto-create (idempotent):
|
||||||
|
- On `createOrganization`:
|
||||||
|
- `teams/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/`
|
||||||
|
- On `createTeam`:
|
||||||
|
- `teams/<team-name>/agents/`, `docs/`, `notes/`, `knowledge/`, `rules/`, `lessons/`, `workflows/`
|
||||||
|
- On `assignIdentity`:
|
||||||
|
- `teams/<team-name>/agents/<agent-id>/docs|notes|knowledge|rules|lessons|workflows`
|
||||||
|
|
||||||
|
### 2) Document Query Tool
|
||||||
|
New API:
|
||||||
|
- `getDocs(scope, topic, keyword)`
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `scope`: `organization | department | team | agent`
|
||||||
|
- `topic`: `docs | notes | knowledge | rules | lessons | workflows`
|
||||||
|
- `keyword`: regex string
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Read-only search by filename regex under filesystem resources
|
||||||
|
- Structured output:
|
||||||
|
- `----ORG`
|
||||||
|
- `----DEPT`
|
||||||
|
- `----TEAM`
|
||||||
|
- `----AGENT`
|
||||||
|
- Invalid regex returns structured error (`YonexusError: VALIDATION_ERROR`)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `${openclaw dir}` resolution order:
|
||||||
|
1. `YonexusOptions.openclawDir`
|
||||||
|
2. `OPENCLAW_DIR` env
|
||||||
|
3. `${HOME}/.openclaw`
|
||||||
|
- Plugin code is not written into `${openclaw dir}/yonexus`; only data folders/files are used there.
|
||||||
83
NEW_FEAT.md
83
NEW_FEAT.md
@@ -1,83 +0,0 @@
|
|||||||
# NEW_FEAT — Filesystem Resource Layout
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Introduce filesystem-backed resource folders under `${openclaw dir}/yonexus` (data-only, no plugin code). These resources are created automatically when orgs/teams/identities are created.
|
|
||||||
|
|
||||||
## Root
|
|
||||||
- `${openclaw dir}/yonexus/organizations/<org-name>/`
|
|
||||||
|
|
||||||
### Organization folders
|
|
||||||
Create on **create_organization**:
|
|
||||||
```
|
|
||||||
teams/
|
|
||||||
docs/
|
|
||||||
notes/
|
|
||||||
knowledge/
|
|
||||||
rules/
|
|
||||||
lessons/
|
|
||||||
workflows/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Team folders
|
|
||||||
Create on **create_team**:
|
|
||||||
```
|
|
||||||
${openclaw dir}/yonexus/organizations/<org-name>/teams/<team-name>/
|
|
||||||
agents/
|
|
||||||
docs/
|
|
||||||
notes/
|
|
||||||
knowledge/
|
|
||||||
rules/
|
|
||||||
lessons/
|
|
||||||
workflows/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Agent folders
|
|
||||||
Create on **assign_identity**:
|
|
||||||
```
|
|
||||||
${openclaw dir}/yonexus/organizations/<org-name>/teams/<team-name>/agents/<agent-id>/
|
|
||||||
docs/
|
|
||||||
notes/
|
|
||||||
knowledge/
|
|
||||||
rules/
|
|
||||||
lessons/
|
|
||||||
workflows/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- The plugin must not store code in `${openclaw dir}/yonexus`, only data.
|
|
||||||
- If folders already exist, creation should be idempotent.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# NEW_FEAT — Document Query Tool
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Add a tool to retrieve documentation files based on scope, topic, and keyword.
|
|
||||||
|
|
||||||
## Tool
|
|
||||||
`get_docs(scope, topic, keyword)`
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
- **scope**: enum `organization | department | team | agent`
|
|
||||||
- **topic**: enum `docs | notes | knowledge | rules | lessons | workflows`
|
|
||||||
- **keyword**: string (supports regular expressions)
|
|
||||||
|
|
||||||
## Behavior
|
|
||||||
- Search matching files under `${openclaw dir}/yonexus/organizations/...` based on the given scope/topic.
|
|
||||||
- `keyword` is applied to filenames (regex supported).
|
|
||||||
- Output grouped by scope in the following format:
|
|
||||||
```
|
|
||||||
----ORG
|
|
||||||
<file list>
|
|
||||||
----DEPT
|
|
||||||
<file list>
|
|
||||||
----TEAM
|
|
||||||
<file list>
|
|
||||||
----AGENT
|
|
||||||
<file list>
|
|
||||||
```
|
|
||||||
- If no match in a group, output the header with no files beneath.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Search should be read-only.
|
|
||||||
- Pattern errors should return a structured error (invalid regex).
|
|
||||||
@@ -9,6 +9,7 @@ Yonexus is an OpenClaw plugin for organization hierarchy and agent identity mana
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Organization hierarchy: `Organization -> Department -> Team -> Agent`
|
- Organization hierarchy: `Organization -> Department -> Team -> Agent`
|
||||||
|
- Filesystem-backed resource layout under `${openclaw dir}/yonexus`
|
||||||
- Agent registration and multi-identity assignment
|
- Agent registration and multi-identity assignment
|
||||||
- Supervisor relationship mapping (does **not** imply permissions)
|
- Supervisor relationship mapping (does **not** imply permissions)
|
||||||
- Role-based authorization
|
- Role-based authorization
|
||||||
@@ -71,6 +72,7 @@ npm run demo
|
|||||||
## Implemented APIs
|
## Implemented APIs
|
||||||
|
|
||||||
Core:
|
Core:
|
||||||
|
- `createOrganization(actor, name)`
|
||||||
- `createDepartment(actor, name, orgId)`
|
- `createDepartment(actor, name, orgId)`
|
||||||
- `createTeam(actor, name, deptId)`
|
- `createTeam(actor, name, deptId)`
|
||||||
- `registerAgent(actor, agentId, name, roles?)`
|
- `registerAgent(actor, agentId, name, roles?)`
|
||||||
@@ -86,6 +88,9 @@ Management:
|
|||||||
- `deleteDepartment(actor, deptId)`
|
- `deleteDepartment(actor, deptId)`
|
||||||
- `deleteTeam(actor, teamId, deptId?)`
|
- `deleteTeam(actor, teamId, deptId?)`
|
||||||
|
|
||||||
|
Docs:
|
||||||
|
- `getDocs(scope, topic, keyword)`
|
||||||
|
|
||||||
Data & audit:
|
Data & audit:
|
||||||
- `exportData(actor)`
|
- `exportData(actor)`
|
||||||
- `importData(actor, state)`
|
- `importData(actor, state)`
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Yonexus 是一个用于 OpenClaw 的组织结构与 Agent 身份管理插件。
|
|||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 组织层级:`Organization -> Department -> Team -> Agent`
|
- 组织层级:`Organization -> Department -> Team -> Agent`
|
||||||
|
- 基于文件系统的资源目录:`${openclaw dir}/yonexus`
|
||||||
- Agent 注册与多身份(Identity)管理
|
- Agent 注册与多身份(Identity)管理
|
||||||
- 上下级关系(Supervisor,**不自动赋权**)
|
- 上下级关系(Supervisor,**不自动赋权**)
|
||||||
- 基于角色的权限控制
|
- 基于角色的权限控制
|
||||||
@@ -71,6 +72,7 @@ npm run demo
|
|||||||
## 已实现 API
|
## 已实现 API
|
||||||
|
|
||||||
核心 API:
|
核心 API:
|
||||||
|
- `createOrganization(actor, name)`
|
||||||
- `createDepartment(actor, name, orgId)`
|
- `createDepartment(actor, name, orgId)`
|
||||||
- `createTeam(actor, name, deptId)`
|
- `createTeam(actor, name, deptId)`
|
||||||
- `registerAgent(actor, agentId, name, roles?)`
|
- `registerAgent(actor, agentId, name, roles?)`
|
||||||
@@ -86,6 +88,9 @@ npm run demo
|
|||||||
- `deleteDepartment(actor, deptId)`
|
- `deleteDepartment(actor, deptId)`
|
||||||
- `deleteTeam(actor, teamId, deptId?)`
|
- `deleteTeam(actor, teamId, deptId?)`
|
||||||
|
|
||||||
|
文档检索:
|
||||||
|
- `getDocs(scope, topic, keyword)`
|
||||||
|
|
||||||
数据与审计:
|
数据与审计:
|
||||||
- `exportData(actor)`
|
- `exportData(actor)`
|
||||||
- `importData(actor, state)`
|
- `importData(actor, state)`
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ const yx = new Yonexus({ dataFile, registrars: ['orion'] });
|
|||||||
yx.registerAgent({ agentId: 'orion' }, 'orion', 'Orion', ['org_admin', 'agent']);
|
yx.registerAgent({ agentId: 'orion' }, 'orion', 'Orion', ['org_admin', 'agent']);
|
||||||
yx.registerAgent({ agentId: 'orion' }, 'hangman', 'Hangman', ['agent']);
|
yx.registerAgent({ agentId: 'orion' }, 'hangman', 'Hangman', ['agent']);
|
||||||
|
|
||||||
const dept = yx.createDepartment({ agentId: 'orion' }, 'Platform', 'org:yonexus');
|
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);
|
const team = yx.createTeam({ agentId: 'orion' }, 'Core', dept.id);
|
||||||
|
|
||||||
yx.assignIdentity({ agentId: 'orion' }, 'orion', dept.id, team.id, {
|
yx.assignIdentity({ agentId: 'orion' }, 'orion', dept.id, team.id, {
|
||||||
|
|||||||
57
src/index.ts
57
src/index.ts
@@ -5,6 +5,8 @@ import { YonexusError } from "./models/errors";
|
|||||||
import type {
|
import type {
|
||||||
Actor,
|
Actor,
|
||||||
Agent,
|
Agent,
|
||||||
|
DocsScope,
|
||||||
|
DocsTopic,
|
||||||
Identity,
|
Identity,
|
||||||
QueryInput,
|
QueryInput,
|
||||||
Scope,
|
Scope,
|
||||||
@@ -15,12 +17,14 @@ import { authorize } from "./permissions/authorize";
|
|||||||
import { AuditStore } from "./store/auditStore";
|
import { AuditStore } from "./store/auditStore";
|
||||||
import { JsonStore } from "./store/jsonStore";
|
import { JsonStore } from "./store/jsonStore";
|
||||||
import { queryIdentities } from "./tools/query";
|
import { queryIdentities } from "./tools/query";
|
||||||
|
import { ResourceLayout } from "./tools/resources";
|
||||||
import { makeId } from "./utils/id";
|
import { makeId } from "./utils/id";
|
||||||
|
|
||||||
export interface YonexusOptions {
|
export interface YonexusOptions {
|
||||||
dataFile?: string;
|
dataFile?: string;
|
||||||
schema?: YonexusSchema;
|
schema?: YonexusSchema;
|
||||||
registrars?: string[];
|
registrars?: string[];
|
||||||
|
openclawDir?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Yonexus {
|
export class Yonexus {
|
||||||
@@ -28,12 +32,19 @@ export class Yonexus {
|
|||||||
private readonly registrars: Set<string>;
|
private readonly registrars: Set<string>;
|
||||||
private readonly store: JsonStore;
|
private readonly store: JsonStore;
|
||||||
private readonly audit = new AuditStore();
|
private readonly audit = new AuditStore();
|
||||||
|
private readonly resources: ResourceLayout;
|
||||||
|
|
||||||
constructor(options: YonexusOptions = {}) {
|
constructor(options: YonexusOptions = {}) {
|
||||||
const dataFile = options.dataFile ?? path.resolve(process.cwd(), "data/org.json");
|
const dataFile = options.dataFile ?? path.resolve(process.cwd(), "data/org.json");
|
||||||
this.store = new JsonStore(dataFile);
|
this.store = new JsonStore(dataFile);
|
||||||
this.schema = options.schema ?? DEFAULT_SCHEMA;
|
this.schema = options.schema ?? DEFAULT_SCHEMA;
|
||||||
this.registrars = new Set(options.registrars ?? []);
|
this.registrars = new Set(options.registrars ?? []);
|
||||||
|
|
||||||
|
const openclawDir =
|
||||||
|
options.openclawDir ??
|
||||||
|
process.env.OPENCLAW_DIR ??
|
||||||
|
path.resolve(process.env.HOME ?? process.cwd(), ".openclaw");
|
||||||
|
this.resources = new ResourceLayout(path.join(openclawDir, "yonexus"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private log(entry: Omit<AuditLogEntry, "id" | "ts">): void {
|
private log(entry: Omit<AuditLogEntry, "id" | "ts">): void {
|
||||||
@@ -44,9 +55,24 @@ export class Yonexus {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
createDepartment(actor: Actor, name: string, orgId: string) {
|
||||||
try {
|
try {
|
||||||
authorize("create_department", actor, { orgId }, this.store);
|
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 dept = { id: makeId("dept", name), name, orgId };
|
||||||
const result = this.store.addDepartment(dept);
|
const result = this.store.addDepartment(dept);
|
||||||
this.log({ actorId: actor.agentId, action: "create_department", target: result.id, status: "ok" });
|
this.log({ actorId: actor.agentId, action: "create_department", target: result.id, status: "ok" });
|
||||||
@@ -65,11 +91,15 @@ export class Yonexus {
|
|||||||
|
|
||||||
createTeam(actor: Actor, name: string, deptId: string) {
|
createTeam(actor: Actor, name: string, deptId: string) {
|
||||||
authorize("create_team", actor, { deptId }, this.store);
|
authorize("create_team", actor, { deptId }, this.store);
|
||||||
if (!this.store.findDepartment(deptId)) {
|
const dept = this.store.findDepartment(deptId);
|
||||||
throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`, { 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 team = { id: makeId("team", `${deptId}-${name}`), name, deptId };
|
||||||
const result = this.store.addTeam(team);
|
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" });
|
this.log({ actorId: actor.agentId, action: "create_team", target: result.id, status: "ok" });
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -80,9 +110,7 @@ export class Yonexus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isBootstrap = this.store.listAgents().length === 0 && actor.agentId === agentId;
|
const isBootstrap = this.store.listAgents().length === 0 && actor.agentId === agentId;
|
||||||
if (!isBootstrap) {
|
if (!isBootstrap) authorize("register_agent", actor, {}, this.store);
|
||||||
authorize("register_agent", actor, {}, this.store);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.store.findAgent(agentId)) {
|
if (this.store.findAgent(agentId)) {
|
||||||
throw new YonexusError("ALREADY_EXISTS", `agent_exists: ${agentId}`, { agentId });
|
throw new YonexusError("ALREADY_EXISTS", `agent_exists: ${agentId}`, { agentId });
|
||||||
@@ -95,8 +123,15 @@ export class Yonexus {
|
|||||||
assignIdentity(actor: Actor, agentId: string, deptId: string, teamId: string, meta: Record<string, string>): Identity {
|
assignIdentity(actor: Actor, agentId: string, deptId: string, teamId: string, meta: Record<string, string>): Identity {
|
||||||
authorize("assign_identity", actor, { deptId, teamId }, this.store);
|
authorize("assign_identity", actor, { deptId, teamId }, this.store);
|
||||||
if (!this.store.findAgent(agentId)) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
if (!this.store.findAgent(agentId)) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||||
if (!this.store.findDepartment(deptId)) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
|
||||||
if (!this.store.findTeam(teamId)) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
const dept = this.store.findDepartment(deptId);
|
||||||
|
if (!dept) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||||
|
|
||||||
|
const team = this.store.findTeam(teamId);
|
||||||
|
if (!team) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||||
|
|
||||||
|
const org = this.store.findOrganization(dept.orgId);
|
||||||
|
if (!org) throw new YonexusError("NOT_FOUND", `organization_not_found: ${dept.orgId}`);
|
||||||
|
|
||||||
const validatedMeta: Record<string, string> = {};
|
const validatedMeta: Record<string, string> = {};
|
||||||
for (const [field, value] of Object.entries(meta)) {
|
for (const [field, value] of Object.entries(meta)) {
|
||||||
@@ -111,6 +146,8 @@ export class Yonexus {
|
|||||||
teamId,
|
teamId,
|
||||||
meta: validatedMeta
|
meta: validatedMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.resources.ensureAgent(org.name, team.name, agentId);
|
||||||
this.log({ actorId: actor.agentId, action: "assign_identity", target: result.id, status: "ok" });
|
this.log({ actorId: actor.agentId, action: "assign_identity", target: result.id, status: "ok" });
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -140,6 +177,10 @@ export class Yonexus {
|
|||||||
return queryIdentities(identities, query, this.schema);
|
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) {
|
renameDepartment(actor: Actor, deptId: string, newName: string) {
|
||||||
authorize("create_department", actor, {}, this.store);
|
authorize("create_department", actor, {}, this.store);
|
||||||
const updated = this.store.renameDepartment(deptId, newName);
|
const updated = this.store.renameDepartment(deptId, newName);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface Actor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
|
| "create_organization"
|
||||||
| "create_department"
|
| "create_department"
|
||||||
| "create_team"
|
| "create_team"
|
||||||
| "register_agent"
|
| "register_agent"
|
||||||
@@ -66,6 +67,9 @@ export type Action =
|
|||||||
| "set_supervisor"
|
| "set_supervisor"
|
||||||
| "query_agents";
|
| "query_agents";
|
||||||
|
|
||||||
|
export type DocsScope = "organization" | "department" | "team" | "agent";
|
||||||
|
export type DocsTopic = "docs" | "notes" | "knowledge" | "rules" | "lessons" | "workflows";
|
||||||
|
|
||||||
export interface Scope {
|
export interface Scope {
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
deptId?: string;
|
deptId?: string;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function authorize(action: Action, actor: Actor, scope: Scope, store: Jso
|
|||||||
const agent = hasRole(store, actor, "agent");
|
const agent = hasRole(store, actor, "agent");
|
||||||
|
|
||||||
const allowed =
|
const allowed =
|
||||||
|
(action === "create_organization" && orgAdmin) ||
|
||||||
(action === "create_department" && orgAdmin) ||
|
(action === "create_department" && orgAdmin) ||
|
||||||
(action === "create_team" && (orgAdmin || deptAdmin)) ||
|
(action === "create_team" && (orgAdmin || deptAdmin)) ||
|
||||||
(action === "assign_identity" && (orgAdmin || deptAdmin || teamLead)) ||
|
(action === "assign_identity" && (orgAdmin || deptAdmin || teamLead)) ||
|
||||||
|
|||||||
@@ -118,14 +118,30 @@ export class JsonStore {
|
|||||||
return this.state.agents.find((a) => a.id === agentId);
|
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 {
|
findDepartment(deptId: string): Department | undefined {
|
||||||
return this.state.departments.find((d) => d.id === deptId);
|
return this.state.departments.find((d) => d.id === deptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listDepartments(): Department[] {
|
||||||
|
return this.state.departments;
|
||||||
|
}
|
||||||
|
|
||||||
findTeam(teamId: string): Team | undefined {
|
findTeam(teamId: string): Team | undefined {
|
||||||
return this.state.teams.find((t) => t.id === teamId);
|
return this.state.teams.find((t) => t.id === teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listTeams(): Team[] {
|
||||||
|
return this.state.teams;
|
||||||
|
}
|
||||||
|
|
||||||
listAgents(): Agent[] {
|
listAgents(): Agent[] {
|
||||||
return this.state.agents;
|
return this.state.agents;
|
||||||
}
|
}
|
||||||
|
|||||||
111
src/tools/resources.ts
Normal file
111
src/tools/resources.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,17 @@ import fs from 'node:fs';
|
|||||||
import { Yonexus } from '../src/index';
|
import { Yonexus } from '../src/index';
|
||||||
import { YonexusError } from '../src/models/errors';
|
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');
|
const dataFile = path.resolve(process.cwd(), 'data/test-org.json');
|
||||||
if (fs.existsSync(dataFile)) fs.unlinkSync(dataFile);
|
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'] });
|
const app = new Yonexus({ dataFile, registrars: ['orion'], openclawDir: root });
|
||||||
|
|
||||||
app.registerAgent({ agentId: 'orion' }, 'orion', 'Orion', ['org_admin', 'agent']);
|
app.registerAgent({ agentId: 'orion' }, 'orion', 'Orion', ['org_admin', 'agent']);
|
||||||
app.registerAgent({ agentId: 'orion' }, 'u1', 'U1', ['agent']);
|
app.registerAgent({ agentId: 'orion' }, 'u1', 'U1', ['agent']);
|
||||||
const dept = app.createDepartment({ agentId: 'orion' }, 'Eng', 'org:yonexus');
|
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);
|
const team = app.createTeam({ agentId: 'orion' }, 'API', dept.id);
|
||||||
app.assignIdentity({ agentId: 'orion' }, 'u1', dept.id, team.id, {
|
app.assignIdentity({ agentId: 'orion' }, 'u1', dept.id, team.id, {
|
||||||
git_user_name: 'u1',
|
git_user_name: 'u1',
|
||||||
@@ -25,6 +28,9 @@ const result = app.queryAgents(
|
|||||||
);
|
);
|
||||||
assert.equal(result.length, 1);
|
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;
|
let thrown = false;
|
||||||
try {
|
try {
|
||||||
app.queryAgents(
|
app.queryAgents(
|
||||||
@@ -37,4 +43,12 @@ try {
|
|||||||
}
|
}
|
||||||
assert.equal(thrown, true);
|
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');
|
console.log('smoke test passed');
|
||||||
|
|||||||
Reference in New Issue
Block a user