Add ACP-backed role sessions
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { AppConfig, RoleConfig } from "../config.js";
|
||||
import { SkillLoader, type LoadedSkill } from "./skill-loader.js";
|
||||
|
||||
export interface ResolvedRole extends RoleConfig {
|
||||
loadedSkills: LoadedSkill[];
|
||||
fingerprint: string;
|
||||
bootstrap: string;
|
||||
}
|
||||
|
||||
export class RoleRegistry {
|
||||
private readonly roles = new Map<string, ResolvedRole>();
|
||||
|
||||
constructor(private readonly config: AppConfig) {
|
||||
const loader = new SkillLoader(config.skills);
|
||||
for (const role of config.roles) {
|
||||
const loadedSkills = role.skills.map((id) => loader.load(id));
|
||||
const fingerprint = crypto.createHash("sha256").update(JSON.stringify({
|
||||
id: role.id,
|
||||
backend: role.backend,
|
||||
workspace: role.workspace,
|
||||
persona: role.persona,
|
||||
policy: role.policy,
|
||||
skills: loadedSkills.map(({ id, file, hash }) => ({ id, file, hash }))
|
||||
})).digest("hex");
|
||||
this.roles.set(role.id, { ...role, loadedSkills, fingerprint, bootstrap: buildBootstrap(role, loadedSkills) });
|
||||
}
|
||||
}
|
||||
|
||||
get(id?: string): ResolvedRole {
|
||||
const roleId = id || this.config.defaultRole;
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) throw new Error(`Unknown role: ${roleId}`);
|
||||
return role;
|
||||
}
|
||||
|
||||
has(id: string): boolean { return this.roles.has(id); }
|
||||
list(): string[] { return [...this.roles.keys()].sort(); }
|
||||
defaultId(): string { return this.config.defaultRole; }
|
||||
}
|
||||
|
||||
function buildBootstrap(role: RoleConfig, skills: LoadedSkill[]): string {
|
||||
return [
|
||||
"Initialize this ACP session with the following role. Treat these instructions as persistent context. Reply only with READY.",
|
||||
`Role: ${role.id}`,
|
||||
`Workspace: ${role.workspace}`,
|
||||
role.persona ? `Persona:\n${role.persona}` : "Persona: general coding assistant",
|
||||
`Permission policy enforced by the ACP client: ${JSON.stringify(role.policy)}`,
|
||||
...skills.map((skill) => `Skill ${skill.id} (${skill.file}):\n${skill.content}`)
|
||||
].join("\n\n");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import type { SkillConfig } from "../config.js";
|
||||
|
||||
export interface LoadedSkill {
|
||||
id: string;
|
||||
file: string;
|
||||
content: string;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export class SkillLoader {
|
||||
constructor(private readonly skills: SkillConfig[]) {}
|
||||
|
||||
load(id: string): LoadedSkill {
|
||||
const config = this.skills.find((skill) => skill.id === id);
|
||||
if (!config) throw new Error(`Unknown skill: ${id}`);
|
||||
const stat = fs.statSync(config.file);
|
||||
if (!stat.isFile()) throw new Error(`Skill is not a file: ${config.file}`);
|
||||
if (stat.size > config.maxBytes) throw new Error(`Skill '${id}' exceeds ${config.maxBytes} bytes`);
|
||||
const content = fs.readFileSync(config.file, "utf8");
|
||||
return { id, file: config.file, content, hash: crypto.createHash("sha256").update(content).digest("hex") };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user