refactor: restructure project layout and add install.mjs
- Move src/ → plugin/ with subdirectories: - plugin/core/ (business logic, models, store, permissions, utils, memory) - plugin/tools/ (query, resources) - plugin/commands/ (placeholder for slash commands) - plugin/hooks/ (placeholder for lifecycle hooks) - plugin/index.ts (wiring layer only, no business logic) - Add install.mjs with --install, --uninstall, --openclaw-profile-path - Add skills/ and docs/ root directories - Move planning docs (PLAN.md, FEAT.md, AGENT_TASKS.md) to docs/ - Remove old scripts/install.sh - Update tsconfig rootDir: src → plugin - Update README.md and README.zh.md with new layout - Bump version to 0.2.0 - All tests pass
This commit is contained in:
245
plugin/core/yonexus.ts
Normal file
245
plugin/core/yonexus.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import path from "node:path";
|
||||
import { DEFAULT_SCHEMA } from "./config/defaults";
|
||||
import type { AuditLogEntry } from "./models/audit";
|
||||
import { YonexusError } from "./models/errors";
|
||||
import type {
|
||||
Actor,
|
||||
Agent,
|
||||
DocsScope,
|
||||
DocsTopic,
|
||||
Identity,
|
||||
QueryInput,
|
||||
Scope,
|
||||
StoreState,
|
||||
YonexusSchema
|
||||
} from "./models/types";
|
||||
import { authorize } from "./permissions/authorize";
|
||||
import { AuditStore } from "./store/auditStore";
|
||||
import { JsonStore } from "./store/jsonStore";
|
||||
import { queryIdentities } from "../tools/query";
|
||||
import { ResourceLayout } from "../tools/resources";
|
||||
import { makeId } from "./utils/id";
|
||||
|
||||
export interface YonexusOptions {
|
||||
dataFile?: string;
|
||||
schema?: YonexusSchema;
|
||||
registrars?: string[];
|
||||
openclawDir?: string;
|
||||
}
|
||||
|
||||
export class Yonexus {
|
||||
private readonly schema: YonexusSchema;
|
||||
private readonly registrars: Set<string>;
|
||||
private readonly store: JsonStore;
|
||||
private readonly audit = new AuditStore();
|
||||
private readonly resources: ResourceLayout;
|
||||
|
||||
constructor(options: YonexusOptions = {}) {
|
||||
const dataFile = options.dataFile ?? path.resolve(process.cwd(), "data/org.json");
|
||||
this.store = new JsonStore(dataFile);
|
||||
this.schema = options.schema ?? DEFAULT_SCHEMA;
|
||||
this.registrars = new Set(options.registrars ?? []);
|
||||
|
||||
const openclawDir =
|
||||
options.openclawDir ??
|
||||
process.env.OPENCLAW_DIR ??
|
||||
path.resolve(process.env.HOME ?? process.cwd(), ".openclaw");
|
||||
this.resources = new ResourceLayout(path.join(openclawDir, "yonexus"));
|
||||
}
|
||||
|
||||
private log(entry: Omit<AuditLogEntry, "id" | "ts">): void {
|
||||
this.audit.append({
|
||||
id: makeId("audit", `${entry.actorId}-${entry.action}-${Date.now()}`),
|
||||
ts: new Date().toISOString(),
|
||||
...entry
|
||||
});
|
||||
}
|
||||
|
||||
createOrganization(actor: Actor, name: string) {
|
||||
authorize("create_organization", actor, {}, this.store);
|
||||
const orgId = makeId("org", name);
|
||||
if (this.store.findOrganization(orgId)) {
|
||||
throw new YonexusError("ALREADY_EXISTS", `organization_exists: ${orgId}`);
|
||||
}
|
||||
const org = this.store.addOrganization({ id: orgId, name });
|
||||
this.resources.ensureOrganization(name);
|
||||
this.log({ actorId: actor.agentId, action: "create_organization", target: org.id, status: "ok" });
|
||||
return org;
|
||||
}
|
||||
|
||||
createDepartment(actor: Actor, name: string, orgId: string) {
|
||||
try {
|
||||
authorize("create_department", actor, { orgId }, this.store);
|
||||
if (!this.store.findOrganization(orgId)) {
|
||||
throw new YonexusError("NOT_FOUND", `organization_not_found: ${orgId}`, { orgId });
|
||||
}
|
||||
const dept = { id: makeId("dept", name), name, orgId };
|
||||
const result = this.store.addDepartment(dept);
|
||||
this.log({ actorId: actor.agentId, action: "create_department", target: result.id, status: "ok" });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.log({
|
||||
actorId: actor.agentId,
|
||||
action: "create_department",
|
||||
target: name,
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
createTeam(actor: Actor, name: string, deptId: string) {
|
||||
authorize("create_team", actor, { deptId }, this.store);
|
||||
const dept = this.store.findDepartment(deptId);
|
||||
if (!dept) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`, { deptId });
|
||||
|
||||
const org = this.store.findOrganization(dept.orgId);
|
||||
if (!org) throw new YonexusError("NOT_FOUND", `organization_not_found: ${dept.orgId}`);
|
||||
|
||||
const team = { id: makeId("team", `${deptId}-${name}`), name, deptId };
|
||||
const result = this.store.addTeam(team);
|
||||
this.resources.ensureTeam(org.name, name);
|
||||
this.log({ actorId: actor.agentId, action: "create_team", target: result.id, status: "ok" });
|
||||
return result;
|
||||
}
|
||||
|
||||
registerAgent(actor: Actor, agentId: string, name: string, roles: Agent["roles"] = ["agent"]) {
|
||||
if (this.registrars.size > 0 && !this.registrars.has(actor.agentId)) {
|
||||
throw new YonexusError("REGISTRAR_DENIED", `registrar_denied: ${actor.agentId}`);
|
||||
}
|
||||
|
||||
const isBootstrap = this.store.listAgents().length === 0 && actor.agentId === agentId;
|
||||
if (!isBootstrap) authorize("register_agent", actor, {}, this.store);
|
||||
|
||||
if (this.store.findAgent(agentId)) {
|
||||
throw new YonexusError("ALREADY_EXISTS", `agent_exists: ${agentId}`, { agentId });
|
||||
}
|
||||
const result = this.store.addAgent({ id: agentId, name, roles });
|
||||
this.log({ actorId: actor.agentId, action: "register_agent", target: result.id, status: "ok" });
|
||||
return result;
|
||||
}
|
||||
|
||||
assignIdentity(actor: Actor, agentId: string, deptId: string, teamId: string, meta: Record<string, string>): Identity {
|
||||
authorize("assign_identity", actor, { deptId, teamId }, this.store);
|
||||
if (!this.store.findAgent(agentId)) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||
|
||||
const dept = this.store.findDepartment(deptId);
|
||||
if (!dept) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||
|
||||
const team = this.store.findTeam(teamId);
|
||||
if (!team) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||
|
||||
const org = this.store.findOrganization(dept.orgId);
|
||||
if (!org) throw new YonexusError("NOT_FOUND", `organization_not_found: ${dept.orgId}`);
|
||||
|
||||
const validatedMeta: Record<string, string> = {};
|
||||
for (const [field, value] of Object.entries(meta)) {
|
||||
if (!this.schema[field]) continue;
|
||||
validatedMeta[field] = String(value);
|
||||
}
|
||||
|
||||
const result = this.store.addIdentity({
|
||||
id: makeId("identity", `${agentId}-${deptId}-${teamId}-${Date.now()}`),
|
||||
agentId,
|
||||
deptId,
|
||||
teamId,
|
||||
meta: validatedMeta
|
||||
});
|
||||
|
||||
this.resources.ensureAgent(org.name, team.name, agentId);
|
||||
this.log({ actorId: actor.agentId, action: "assign_identity", target: result.id, status: "ok" });
|
||||
return result;
|
||||
}
|
||||
|
||||
setSupervisor(actor: Actor, agentId: string, supervisorId: string, deptId?: string) {
|
||||
authorize("set_supervisor", actor, { deptId }, this.store);
|
||||
if (!this.store.findAgent(agentId)) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||
if (!this.store.findAgent(supervisorId)) throw new YonexusError("NOT_FOUND", `supervisor_not_found: ${supervisorId}`);
|
||||
if (agentId === supervisorId) throw new YonexusError("INVALID_SUPERVISOR", "invalid_supervisor: self_reference");
|
||||
const result = this.store.upsertSupervisor({ agentId, supervisorId });
|
||||
this.log({ actorId: actor.agentId, action: "set_supervisor", target: `${agentId}->${supervisorId}`, status: "ok" });
|
||||
return result;
|
||||
}
|
||||
|
||||
whoami(agentId: string) {
|
||||
const agent = this.store.findAgent(agentId);
|
||||
if (!agent) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||
|
||||
const identities = this.store.listIdentities().filter((x) => x.agentId === agentId);
|
||||
const supervisor = this.store.findSupervisor(agentId);
|
||||
return { agent, identities, supervisor };
|
||||
}
|
||||
|
||||
queryAgents(actor: Actor, scope: Scope, query: QueryInput) {
|
||||
authorize("query_agents", actor, scope, this.store);
|
||||
const identities = this.store.listIdentities();
|
||||
return queryIdentities(identities, query, this.schema);
|
||||
}
|
||||
|
||||
getDocs(scope: DocsScope, topic: DocsTopic, keyword: string): string {
|
||||
return this.resources.getDocs(scope, topic, keyword);
|
||||
}
|
||||
|
||||
renameDepartment(actor: Actor, deptId: string, newName: string) {
|
||||
authorize("create_department", actor, {}, this.store);
|
||||
const updated = this.store.renameDepartment(deptId, newName);
|
||||
if (!updated) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||
this.log({ actorId: actor.agentId, action: "rename_department", target: deptId, status: "ok" });
|
||||
return updated;
|
||||
}
|
||||
|
||||
renameTeam(actor: Actor, teamId: string, newName: string, deptId?: string) {
|
||||
authorize("create_team", actor, { deptId }, this.store);
|
||||
const updated = this.store.renameTeam(teamId, newName);
|
||||
if (!updated) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||
this.log({ actorId: actor.agentId, action: "rename_team", target: teamId, status: "ok" });
|
||||
return updated;
|
||||
}
|
||||
|
||||
migrateTeam(actor: Actor, teamId: string, newDeptId: string) {
|
||||
authorize("create_team", actor, { deptId: newDeptId }, this.store);
|
||||
if (!this.store.findDepartment(newDeptId)) throw new YonexusError("NOT_FOUND", `department_not_found: ${newDeptId}`);
|
||||
const updated = this.store.migrateTeam(teamId, newDeptId);
|
||||
if (!updated) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||
this.log({ actorId: actor.agentId, action: "migrate_team", target: `${teamId}->${newDeptId}`, status: "ok" });
|
||||
return updated;
|
||||
}
|
||||
|
||||
deleteDepartment(actor: Actor, deptId: string) {
|
||||
authorize("create_department", actor, {}, this.store);
|
||||
const ok = this.store.deleteDepartment(deptId);
|
||||
if (!ok) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||
this.log({ actorId: actor.agentId, action: "delete_department", target: deptId, status: "ok" });
|
||||
return { ok };
|
||||
}
|
||||
|
||||
deleteTeam(actor: Actor, teamId: string, deptId?: string) {
|
||||
authorize("create_team", actor, { deptId }, this.store);
|
||||
const ok = this.store.deleteTeam(teamId);
|
||||
if (!ok) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||
this.log({ actorId: actor.agentId, action: "delete_team", target: teamId, status: "ok" });
|
||||
return { ok };
|
||||
}
|
||||
|
||||
exportData(actor: Actor): StoreState {
|
||||
authorize("query_agents", actor, {}, this.store);
|
||||
this.log({ actorId: actor.agentId, action: "export_data", status: "ok" });
|
||||
return this.store.snapshot();
|
||||
}
|
||||
|
||||
importData(actor: Actor, state: StoreState): { ok: true } {
|
||||
authorize("create_department", actor, {}, this.store);
|
||||
this.store.replace(state);
|
||||
this.log({ actorId: actor.agentId, action: "import_data", status: "ok" });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
listAuditLogs(limit = 100, offset = 0): AuditLogEntry[] {
|
||||
return this.audit.list(limit, offset);
|
||||
}
|
||||
|
||||
debugSnapshot(): StoreState {
|
||||
return this.store.snapshot();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user