-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Problem
Profile-specific logic is scattered across the codebase:
server.tsinterprets profile manifests and routes channelshttp-routes.tshas/api/audit/*endpoints hardcoded (200+ lines)audit-dashboard.tslives inlibclaudebox/html/(generic lib) despite being audit-specifichost-manifest.tsdeclares channel binding but server.ts interprets it separatelymcp-sidecar.tsruns inside container, disconnected from the rest
Adding a new profile means touching 4+ files across different packages. The channel-to-profile binding is declarative, but everything it does is imperative — that's the tension.
Design
Plugin interface
Minimal — just a name and a setup function:
interface Plugin {
name: string;
setup(ctx: PluginContext): void | Promise<void>;
}
interface PluginContext {
// Raw event listeners — plugin decides what to do
onSlackMessage(handler: (msg: SlackMessage) => Promise<boolean | void>): void;
onSlackReaction(handler: (reaction: SlackReaction) => Promise<boolean | void>): void;
// HTTP route registration
route(method: string, path: string, handler: RouteHandler): void;
// Shared infra — the plugin doesn't own these, it uses them
docker: DockerManager;
store: SessionStore;
slack: SlackClient;
}No channels field. Channel filtering is just an if statement inside the handler — plain code, no framework magic.
barretenberg-audit as a plugin
profiles/barretenberg-audit/
plugin.ts ← entry point, composes everything
mcp-sidecar.ts ← runs inside container (unchanged)
container-claude.md
html/
dashboard.ts ← moves FROM libclaudebox/html/
routes/
coverage.ts ← /api/audit/coverage handler
findings.ts ← /api/audit/findings handler
questions.ts ← /api/audit/questions handler
// profiles/barretenberg-audit/plugin.ts
const plugin: Plugin = {
name: "barretenberg-audit",
setup(ctx) {
ctx.onSlackMessage(async (msg) => {
if (msg.channel !== "C0AJCUKUNGP") return false; // not ours
const session = await ctx.docker.startSession({
prompt: msg.text,
user: msg.user,
extraEnv: ["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1"],
sidecar: import.meta.dirname + "/mcp-sidecar.ts",
});
await ctx.slack.reply(msg.thread, `Audit started: ${session.url}`);
return true;
});
ctx.route("GET", "/audit", (req, res) => res.html(auditDashboardHTML()));
ctx.route("GET", "/api/audit/coverage", coverageHandler(ctx.store));
ctx.route("GET", "/api/audit/findings", findingsHandler);
ctx.route("POST", "/api/audit/questions/:id/answer", answerHandler(ctx.store));
},
};Server becomes a thin shell
const plugins = await loadPlugins(["barretenberg-audit", "default"]);
for (const p of plugins) {
await p.setup(pluginContext);
}
// Handlers fire in registration order.
// barretenberg-audit claims its channel, returns true.
// Default handler gets everything else.What stays in libclaudebox (shared infra)
DockerManager— container lifecycleSessionStore— session JSONL, worktree managementSlackClient— posting messages, reactionshtml/shared.ts—linkify(),esc(),timeAgo(), base styleshtml/app-shell.ts— page chrome, auth wrappermcp/base.ts— shared MCP tool framework (used by sidecars)
Plugins compose on top of these primitives.
Key design decisions
- No declarative channel binding — channel/repo/user filtering is plain
ifstatements in the handler. Default profile has no restrictions. - Ordering = priority — plugins loaded first get first crack at events. First handler to return
trueclaims the event. setup()is imperative — the plugin callsctx.route()andctx.onSlackMessage(). No manifest to interpret.- Composable internals — a plugin's
setup()can compose smaller functions (setupCoverageRoutes(ctx), etc.) but that's the plugin's business.
Migration path
- Define
Plugin+PluginContextinterfaces in libclaudebox - Move audit routes from
http-routes.ts→profiles/barretenberg-audit/routes/ - Move
audit-dashboard.ts→profiles/barretenberg-audit/html/ - Write
profiles/barretenberg-audit/plugin.tsthat registers everything - Server loads plugins, dispatches events through handler chain
- Default profile extracted last (current behavior, just wrapped)
No big bang needed — barretenberg-audit migrates first, default stays as-is until later.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels