- 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
246 lines
9.8 KiB
TypeScript
246 lines
9.8 KiB
TypeScript
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();
|
|
}
|
|
}
|