import { Agent, Model, Plugin, Provider } from "@opencode/plugin"; import type { RpcRegistration } from "@opencode/plugin/promise/rpc"; import type { ModelInfo } from "@opencode/client"; import { z } from "zod"; import { gptAgents, settings, workerNames } from "./config.js"; import { discover } from "./provider.js"; import { descriptions, routingGuide } from "./routing.js"; import { QuotaTracker } from "./quota.js"; import { QuotaRpc } from "./quota-rpc.js"; import { playwrightMcpCommand } from "./playwright.js"; // Permission actions use wildcards, not filesystem globs. function matches(value: string, pattern: string) { const escaped = pattern .replace(/[.+^${}()|[\]\\]/g, "\\$&") .replace(/\*/g, ".*") .replace(/\?/g, "."); return new RegExp(`^${escaped}$`, "s").test(value); } const reading = [ "read", "glob", "grep", "list", "todoread", "todowrite", "webfetch", "websearch", "question", "external_directory", "doom_loop", ]; const delegation = z.object({ agent: z.string() }); const todos = z.array( z.object({ id: z.string(), content: z.string(), status: z.enum(["pending", "in_progress", "completed", "cancelled"]), priority: z.enum(["high", "medium", "low"]), }), ); export default Plugin.define({ id: "stanp-opencode", async setup(context) { const selected = settings.parse({ mode: "gpt", ...context.options }); let models = new Map(); let inherited: Agent.Info["permissions"] = Agent.Info.default( Agent.ID.make("build"), ).permissions; let permissionKey = JSON.stringify(inherited); let hasCodegraph = false; let codegraphTools: string[] = []; let agentTransformReady = false; const controller = new AbortController(); let quotaRegistration: RpcRegistration | undefined; const quota = new QuotaTracker(context, async () => { await quotaRegistration?.events.emit("updated", {}); }); quotaRegistration = await context.rpc.register(QuotaRpc, { status: async () => quota.status() ?? null, }); await quota.init(); // Subscribe before the first read: configuration can publish its models while // asynchronous setup is running. const updates = (async () => { for await (const event of context.event.subscribe({ signal: controller.signal })) { if (event.location) { const workspaceID = "workspaceID" in event.location ? event.location.workspaceID : undefined; if ( event.location.directory !== context.location.directory || workspaceID !== context.location.workspaceID ) continue; } if (event.type === "model.updated") { models = await discover(context, selected.provider); await context.agent.reload(); } else if (event.type === "agent.updated") { // The configured build transform may run after ours. Read the final // registry, then replay only when its effective permissions change. const { data } = await context.agent.list(); const permissions = data.find((agent) => agent.id === "build")?.permissions ?? Agent.Info.default(Agent.ID.make("build")).permissions; const key = JSON.stringify(permissions); if (key === permissionKey) continue; inherited = permissions; permissionKey = key; await context.agent.reload(); } else if ( event.type === "session.execution.succeeded" || event.type === "session.execution.failed" || event.type === "session.execution.interrupted" || event.type === "session.idle" ) { await quota.complete(event.data.sessionID); } else if (event.type === "credential.updated") { await quota.authChanged(); } else if ( event.type === "credential.switched" && String(event.data.integrationID) === "openai" ) { await quota.authChanged(true); } } })().catch((error) => { if (!controller.signal.aborted) console.error("stanp-opencode model update failed", error); }); models = await discover(context, selected.provider); const { data: initialAgents } = await context.agent.list(); inherited = initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited; permissionKey = JSON.stringify(inherited); const assignments = selected.mode === "gpt" ? gptAgents : selected.agents; let workers: (typeof workerNames)[number][] = []; const active = new Set(); const playwrightCommand = selected.playwright ? await playwrightMcpCommand(context.location.directory) : undefined; await context.mcp.transform((editor) => { if (playwrightCommand && !editor.get("playwright")) editor.set("playwright", { type: "local", command: playwrightCommand, timeout: { startup: 300_000 }, }); hasCodegraph = editor.get("codegraph") !== undefined; if (hasCodegraph) editor.update("codegraph", (server) => { server.codemode = false; }); }); await context.tool.transform((editor) => { const next = editor .list() .map((tool) => tool.id) .filter((id) => id.startsWith("codegraph_")) .sort(); if (JSON.stringify(next) === JSON.stringify(codegraphTools)) return; codegraphTools = next; if (agentTransformReady) queueMicrotask(() => { void context.agent .reload() .catch((error) => console.error("stanp-opencode agent reload failed", error), ); }); }); await context.agent.transform((editor) => { const deep = selected.mode === "custom" || models.has("gpt-6-astra"); const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-")); const coordinate = selected.mode === "gpt" && !deep ? { model: "gpt-5.6-sol", effort: "medium" } : assignments.coordinate; workers = [coordinate, ...candidates.map((name) => assignments[name])].every( ({ model }) => models.has(model), ) ? candidates : []; active.clear(); for (const name of workers) active.add(name); if (workers.length === 0) return; for (const name of workers) { const assignment = assignments[name]; const available = models.get(assignment.model)!; editor.update(name, (agent) => { const hasVariant = available.model.variants.some( ({ id }) => id === assignment.effort, ); agent.name = Agent.Name.make(name); agent.mode = "subagent"; agent.hidden = false; agent.description = descriptions[name]; agent.model = { providerID: Provider.ID.make(available.model.providerID), id: Model.ID.make(available.model.id), }; if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.effort); agent.request = { settings: hasVariant ? {} : { reasoningEffort: assignment.effort }, headers: {}, body: {}, }; if (selected.lunaPriority && available.serviceTier) agent.request.settings.serviceTier = available.serviceTier; agent.permissions = [ ...structuredClone(inherited), { action: "subagent", resource: "*", effect: "deny" }, ]; }); } editor.update("coordinate", (agent) => { const available = models.get(coordinate.model)!; const hasVariant = available.model.variants.some( ({ id }) => id === coordinate.effort, ); agent.name = Agent.Name.make("coordinate"); agent.mode = "primary"; agent.hidden = false; agent.description = "Read, plan, and coordinate specialist workers; never implement directly."; agent.model = { providerID: Provider.ID.make(available.model.providerID), id: Model.ID.make(available.model.id), }; if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort); agent.request = { settings: hasVariant ? {} : { reasoningEffort: coordinate.effort }, headers: {}, body: {}, }; agent.system = routingGuide(deep); agent.permissions = [ { action: "*", resource: "*", effect: "deny" }, ...reading.flatMap((action) => inherited .filter((rule) => matches(action, rule.action)) .map((rule) => ({ ...rule, action })), ), ...(hasCodegraph ? codegraphTools.flatMap((action) => inherited .filter((rule) => matches(action, rule.action)) .map((rule) => ({ ...rule, action })), ) : []), ...workers.map((resource) => ({ action: "subagent", resource, effect: "allow" as const, })), ]; }); }); agentTransformReady = true; async function assertTodo(agentID: Agent.ID, action: string) { const { data: agent } = await context.agent.get({ agentID }); const effect = agent.permissions.findLast( (rule) => matches(action, rule.action) && matches("*", rule.resource), )?.effect ?? "ask"; if (effect !== "allow") throw new Error( `${action} requires an allow permission: OpenCode's public plugin API cannot request custom-tool permission approval (${effect}).`, ); } await context.tool.transform((editor) => { if (!editor.get("todowrite")) editor.add({ name: "todowrite", description: "Replace this session's todo list to track the work and its progress.", input: z.object({ todos }), options: { codemode: false }, async execute(input, tool) { await assertTodo(tool.agent, "todowrite"); await context.storage.set(`todos:${tool.sessionID}`, input.todos); return { content: JSON.stringify(input.todos) }; }, }); if (!editor.get("todoread")) editor.add({ name: "todoread", description: "Read this session's todo list.", input: z.object({}), options: { codemode: false }, async execute(_input, tool) { await assertTodo(tool.agent, "todoread"); const stored = await context.storage.get(`todos:${tool.sessionID}`); return { content: JSON.stringify( stored === undefined ? [] : todos.parse(stored), ), }; }, }); }); await context.session.hook("context", (event) => { if (workerNames.some((name) => name === event.agent)) { delete event.tools.subagent; return; } const tool = event.tools.subagent; if (event.agent === "coordinate" && workers.length === 0) { delete event.tools.subagent; return; } if (event.agent !== "coordinate" || !tool) return; tool.description = `Delegate a self-contained assignment to one of these workers:\n${workers.map((name) => `- ${name}: ${descriptions[name]}`).join("\n")}`; // The executable definition remains native; constrain only this request's schema. const properties = z.record(z.string(), z.unknown()).parse(tool.input.properties ?? {}); tool.input = { ...tool.input, properties: { ...properties, agent: { type: "string", enum: workers } }, }; }); await context.session.hook("model.request", async (event) => quota.start(event.sessionID)); await context.tool.hook("execute.before", (event) => { if (event.tool !== "subagent") return; const { agent } = delegation.parse(event.input); if (agent === "coordinate") throw new Error("coordinate cannot run as a subagent"); if (workerNames.some((name) => name === event.agent)) throw new Error("Workers cannot delegate tasks"); if (event.agent === "coordinate" && !active.has(agent)) throw new Error("coordinate may delegate only to its available workers"); }); // Commands can create child sessions without going through the subagent tool. await context.session.hook("prompt", async (event) => { const child = await context.session.get({ sessionID: event.sessionID }); if (!child.parentID) return; if (child.agent === "coordinate") throw new Error("coordinate cannot run as a subagent"); const parent = await context.session.get({ sessionID: child.parentID }); if (workerNames.some((name) => name === parent.agent)) throw new Error("Workers cannot delegate tasks"); if ( parent.agent === "coordinate" && (child.agent === undefined || !active.has(child.agent)) ) throw new Error("coordinate may delegate only to its available workers"); }); return async () => { controller.abort(); await updates; await quota.dispose(); await quotaRegistration?.dispose(); }; }, });