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