Move project and host queries into native MCP with host-owned OAuth/context, while retaining memory, skills, actions and publication plugins. Add nearby-model queries and explicit chat/task/learning model defaults. Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped session ownership, pending input edits and investigation communication. Disable local-provider discovery: Server /health allowed native vLLM to probe /v1/models on port 8000. A native before/after regression prevents recurrence. Validation: 170 focused Bun tests; service/plugin typechecks; native discovery, provider-discovery and input-inbox smoke checks; no paid model inference.
59 lines
3.1 KiB
TypeScript
59 lines
3.1 KiB
TypeScript
import type { Plugin } from "@opencode/plugin";
|
|
import { z } from "zod";
|
|
import { getInternalBridgeConfig } from "./internalBridge.js";
|
|
import { TASK_AGENT, TASK_READER_AGENT, TASK_READER_TOOLS, TASK_SUBAGENT_LIMIT } from "../src/runtime/task-agent-policy.js";
|
|
|
|
type Context = Parameters<Plugin.Plugin["setup"]>[0];
|
|
const policySchema = z.object({ role: z.enum(["coordinator", "reader", "denied"]) });
|
|
async function policy(sessionId: string, signal?: AbortSignal) {
|
|
const { baseUrl, internalToken } = getInternalBridgeConfig();
|
|
const response = await fetch(`${baseUrl}/internal/operational-task/agent-policy`, {
|
|
method: "POST", headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken },
|
|
body: JSON.stringify({ session_id: sessionId }),
|
|
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(5000)]) : AbortSignal.timeout(5000),
|
|
});
|
|
if (!response.ok) throw new Error("task agent policy unavailable");
|
|
return policySchema.parse(await response.json()).role;
|
|
}
|
|
|
|
export async function registerTaskAgentPolicy(ctx: Context) {
|
|
const active = new Map<string, number>();
|
|
await ctx.tool.transform((editor) => {
|
|
editor.update("subagent", (tool) => {
|
|
const execute = tool.execute;
|
|
tool.execute = async (input, context) => {
|
|
const args = z.object({ agent: z.literal(TASK_READER_AGENT), background: z.literal(false).optional(),
|
|
sessionID: z.never().optional() }).passthrough().parse(input);
|
|
if (await policy(context.sessionID, context.signal) !== "coordinator") throw new Error("only an active investigation may launch subagents");
|
|
const count = active.get(context.sessionID) ?? 0;
|
|
if (count >= TASK_SUBAGENT_LIMIT) throw new Error("at most two investigation subagents may run concurrently");
|
|
active.set(context.sessionID, count + 1);
|
|
try { return await execute(args, context); }
|
|
finally {
|
|
const remaining = (active.get(context.sessionID) ?? 1) - 1;
|
|
if (remaining) active.set(context.sessionID, remaining);
|
|
else active.delete(context.sessionID);
|
|
}
|
|
};
|
|
});
|
|
});
|
|
await ctx.permission.hook("evaluate", async (event) => {
|
|
if (event.action !== "subagent") return;
|
|
// Respect static denials, including administrator overrides.
|
|
if (event.effect === "deny") return;
|
|
try {
|
|
if (event.agent !== TASK_AGENT || !event.resources.length
|
|
|| event.resources.some((id) => id !== TASK_READER_AGENT)
|
|
|| await policy(event.sessionID) !== "coordinator") event.effect = "deny";
|
|
} catch { event.effect = "deny"; }
|
|
});
|
|
await ctx.session.hook("context", async (event) => {
|
|
if (event.agent === TASK_READER_AGENT) {
|
|
if (await policy(event.sessionID) !== "reader") throw new Error("task reader is not bound to an active investigation");
|
|
for (const name of Object.keys(event.tools)) if (!(TASK_READER_TOOLS as readonly string[]).includes(name)) delete event.tools[name];
|
|
} else if (event.agent !== TASK_AGENT || await policy(event.sessionID) !== "coordinator") {
|
|
delete event.tools.subagent;
|
|
}
|
|
});
|
|
}
|