feat: scaffold yonexus MVP core with storage, auth, query, and scope memory

This commit is contained in:
2026-03-07 05:21:25 +00:00
parent e7f4aeae1a
commit 1436d63a8c
18 changed files with 600 additions and 29 deletions

18
src/config/defaults.ts Normal file
View File

@@ -0,0 +1,18 @@
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 }
};

105
src/index.ts Normal file
View File

@@ -0,0 +1,105 @@
import path from "node:path";
import { DEFAULT_SCHEMA } from "./config/defaults";
import type {
Actor,
Agent,
Identity,
QueryInput,
Scope,
StoreState,
YonexusSchema
} from "./models/types";
import { authorize } from "./permissions/authorize";
import { JsonStore } from "./store/jsonStore";
import { queryIdentities } from "./tools/query";
import { makeId } from "./utils/id";
export interface YonexusOptions {
dataFile?: string;
schema?: YonexusSchema;
registrars?: string[];
}
export class Yonexus {
private readonly schema: YonexusSchema;
private readonly registrars: Set<string>;
private readonly store: JsonStore;
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 ?? []);
}
createDepartment(actor: Actor, name: string, orgId: string) {
authorize("create_department", actor, { orgId }, this.store);
const dept = { id: makeId("dept", name), name, orgId };
return this.store.addDepartment(dept);
}
createTeam(actor: Actor, name: string, deptId: string) {
authorize("create_team", actor, { deptId }, this.store);
if (!this.store.findDepartment(deptId)) throw new Error(`department_not_found: ${deptId}`);
const team = { id: makeId("team", `${deptId}-${name}`), name, deptId };
return this.store.addTeam(team);
}
registerAgent(actor: Actor, agentId: string, name: string, roles: Agent["roles"] = ["agent"]) {
if (this.registrars.size > 0 && !this.registrars.has(actor.agentId)) {
authorize("register_agent", actor, {}, this.store);
}
if (this.store.findAgent(agentId)) throw new Error(`agent_exists: ${agentId}`);
return this.store.addAgent({ id: agentId, name, roles });
}
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 Error(`agent_not_found: ${agentId}`);
if (!this.store.findDepartment(deptId)) throw new Error(`department_not_found: ${deptId}`);
if (!this.store.findTeam(teamId)) throw new Error(`team_not_found: ${teamId}`);
const validatedMeta: Record<string, string> = {};
for (const [field, value] of Object.entries(meta)) {
if (!this.schema[field]) continue;
validatedMeta[field] = String(value);
}
return this.store.addIdentity({
id: makeId("identity", `${agentId}-${deptId}-${teamId}-${Date.now()}`),
agentId,
deptId,
teamId,
meta: validatedMeta
});
}
setSupervisor(actor: Actor, agentId: string, supervisorId: string, deptId?: string) {
authorize("set_supervisor", actor, { deptId }, this.store);
if (!this.store.findAgent(agentId)) throw new Error(`agent_not_found: ${agentId}`);
if (!this.store.findAgent(supervisorId)) throw new Error(`supervisor_not_found: ${supervisorId}`);
if (agentId === supervisorId) throw new Error("invalid_supervisor: self_reference");
return this.store.upsertSupervisor({ agentId, supervisorId });
}
whoami(agentId: string) {
const agent = this.store.findAgent(agentId);
if (!agent) throw new Error(`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);
}
debugSnapshot(): StoreState {
return this.store.snapshot();
}
}
export default Yonexus;

16
src/memory/scopeMemory.ts Normal file
View File

@@ -0,0 +1,16 @@
export interface MemoryPort {
store(input: { text: string; scope: string; metadata?: Record<string, string> }): Promise<unknown>;
recall(input: { query: string; scope: string; limit?: number }): Promise<unknown>;
}
export class ScopeMemory {
constructor(private readonly memory: MemoryPort) {}
async put(scopeId: string, text: string, metadata?: Record<string, string>): Promise<unknown> {
return this.memory.store({ text, scope: scopeId, metadata });
}
async search(scopeId: string, query: string, limit = 5): Promise<unknown> {
return this.memory.recall({ query, scope: scopeId, limit });
}
}

89
src/models/types.ts Normal file
View File

@@ -0,0 +1,89 @@
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<string, string>;
}
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_department"
| "create_team"
| "register_agent"
| "assign_identity"
| "set_supervisor"
| "query_agents";
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;
}

View File

@@ -0,0 +1,34 @@
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_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 Error(`permission_denied: ${action}`);
}
}

89
src/store/jsonStore.ts Normal file
View File

@@ -0,0 +1,89 @@
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<StoreState>(filePath, DEFAULT_STATE);
}
save(): void {
writeJsonFile(this.filePath, this.state);
}
snapshot(): StoreState {
return JSON.parse(JSON.stringify(this.state)) as StoreState;
}
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;
}
addTeam(team: Team): Team {
this.state.teams.push(team);
this.save();
return team;
}
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);
}
findDepartment(deptId: string): Department | undefined {
return this.state.departments.find((d) => d.id === deptId);
}
findTeam(teamId: string): Team | undefined {
return this.state.teams.find((t) => t.id === teamId);
}
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);
}
}

43
src/tools/query.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { Identity, QueryFilter, QueryInput, QueryOptions, YonexusSchema } from "../models/types";
const DEFAULT_LIMIT = 20;
const MAX_LIMIT = 100;
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(filter.value.toLowerCase());
case "regex": {
const re = new RegExp(filter.value);
return re.test(raw);
}
default:
return false;
}
}
function normalizeOptions(options?: QueryOptions): Required<QueryOptions> {
const limit = Math.min(Math.max(1, options?.limit ?? DEFAULT_LIMIT), MAX_LIMIT);
const offset = Math.max(0, options?.offset ?? 0);
return { limit, offset };
}
export function queryIdentities(identities: Identity[], input: QueryInput, schema: YonexusSchema): Identity[] {
for (const filter of input.filters) {
if (!isQueryable(filter.field, schema)) {
throw new Error(`field_not_queryable: ${filter.field}`);
}
}
const filtered = identities.filter((identity) => input.filters.every((f) => matchFilter(identity, f)));
const { limit, offset } = normalizeOptions(input.options);
return filtered.slice(offset, offset + limit);
}

18
src/utils/fs.ts Normal file
View File

@@ -0,0 +1,18 @@
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<T>(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");
}

10
src/utils/id.ts Normal file
View File

@@ -0,0 +1,10 @@
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"}`;
}