This commit is contained in:
+140
-18
@@ -5,6 +5,7 @@ import type { DeepMutable } from "@opencode/plugin/promise/types";
|
||||
import type { Info, ToolContext, ToolEditor } from "@opencode/plugin/promise/tool";
|
||||
import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client";
|
||||
import type { SessionContext } from "@opencode/plugin/promise/session";
|
||||
import { z } from "zod";
|
||||
import { gptAgents } from "../src/config.js";
|
||||
import plugin from "../src/index.js";
|
||||
|
||||
@@ -26,6 +27,11 @@ type FixtureSetup = (
|
||||
async function host(
|
||||
models: ModelInfo[] = allModels,
|
||||
options: Plugin.Context["options"] = { provider: "test" },
|
||||
configuredMcp: Record<
|
||||
string,
|
||||
{ type: "local"; command: string[]; codemode?: boolean; disabled?: boolean }
|
||||
> = {},
|
||||
configuredMcpTools: string[] = [],
|
||||
) {
|
||||
const agents = new Map<string, DeepMutable<Agent.Info>>();
|
||||
const build: DeepMutable<Agent.Info> = Agent.Info.default(Agent.ID.make("build"));
|
||||
@@ -48,6 +54,50 @@ async function host(
|
||||
});
|
||||
const storage = new Map<string, JsonValue>();
|
||||
const tools = new Map<string, Info>();
|
||||
const mcpTools = new Set(configuredMcpTools);
|
||||
const mcpServers = new Map(Object.entries(structuredClone(configuredMcp)));
|
||||
let mcpTransform!: (editor: {
|
||||
get(name: string): (typeof configuredMcp)[string] | undefined;
|
||||
update(name: string, update: (server: (typeof configuredMcp)[string]) => void): void;
|
||||
}) => void;
|
||||
const toolTransforms: ((editor: ToolEditor) => void)[] = [];
|
||||
const editMcp = () => ({
|
||||
get: (name: string) => mcpServers.get(name),
|
||||
update(name: string, update: (server: (typeof configuredMcp)[string]) => void) {
|
||||
const server = mcpServers.get(name);
|
||||
if (server) update(server);
|
||||
},
|
||||
});
|
||||
const editTools = (): ToolEditor => ({
|
||||
get(id) {
|
||||
const tool = tools.get(id);
|
||||
return tool ? { ...tool, id } : undefined;
|
||||
},
|
||||
list: () => [
|
||||
...[...tools].map(([id, tool]) => ({ ...tool, id })),
|
||||
...[...mcpTools].map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
description: id,
|
||||
input: z.object({}),
|
||||
options: { codemode: false },
|
||||
async execute() {
|
||||
return { content: "" };
|
||||
},
|
||||
})),
|
||||
],
|
||||
namespace() {},
|
||||
add(tool) {
|
||||
tools.set(tool.name, tool);
|
||||
},
|
||||
update(id, update) {
|
||||
const tool = tools.get(id);
|
||||
if (tool) update(tool);
|
||||
},
|
||||
remove(id) {
|
||||
tools.delete(id);
|
||||
},
|
||||
});
|
||||
// SAFETY: This test host implements the native APIs used by setup; unrelated context
|
||||
// services are deliberately absent so unexpected new dependencies fail the test.
|
||||
const context = {
|
||||
@@ -73,6 +123,13 @@ async function host(
|
||||
storage.set(key, value);
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
async transform(callback: typeof mcpTransform) {
|
||||
mcpTransform = callback;
|
||||
callback(editMcp());
|
||||
return registration;
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
async list() {
|
||||
return { data: [...agents.values()] };
|
||||
@@ -119,24 +176,8 @@ async function host(
|
||||
},
|
||||
tool: {
|
||||
async transform(callback: (editor: ToolEditor) => void) {
|
||||
callback({
|
||||
get(id) {
|
||||
const tool = tools.get(id);
|
||||
return tool ? { ...tool, id } : undefined;
|
||||
},
|
||||
list: () => [...tools].map(([id, tool]) => ({ ...tool, id })),
|
||||
namespace() {},
|
||||
add(tool) {
|
||||
tools.set(tool.name, tool);
|
||||
},
|
||||
update(id, update) {
|
||||
const tool = tools.get(id);
|
||||
if (tool) update(tool);
|
||||
},
|
||||
remove(id) {
|
||||
tools.delete(id);
|
||||
},
|
||||
});
|
||||
toolTransforms.push(callback);
|
||||
callback(editTools());
|
||||
return registration;
|
||||
},
|
||||
async hook(_name: string, callback: typeof before) {
|
||||
@@ -153,6 +194,7 @@ async function host(
|
||||
return {
|
||||
agents,
|
||||
build,
|
||||
mcpServers,
|
||||
defaultAgent,
|
||||
originalBuild,
|
||||
async refresh(next: ModelInfo[]) {
|
||||
@@ -167,6 +209,21 @@ async function host(
|
||||
events.enqueue({ type: "agent.updated" });
|
||||
await reloaded.promise;
|
||||
},
|
||||
async changeCodegraph(
|
||||
server:
|
||||
| { type: "local"; command: string[]; codemode?: boolean; disabled?: boolean }
|
||||
| undefined,
|
||||
toolNames: string[],
|
||||
) {
|
||||
if (server) mcpServers.set("codegraph", server);
|
||||
else mcpServers.delete("codegraph");
|
||||
mcpTools.clear();
|
||||
for (const name of toolNames) mcpTools.add(name);
|
||||
reloaded = Promise.withResolvers<void>();
|
||||
mcpTransform(editMcp());
|
||||
for (const callback of toolTransforms) callback(editTools());
|
||||
await reloaded.promise;
|
||||
},
|
||||
async todo(name: string, sessionID: string, input: JsonValue = {}) {
|
||||
// SAFETY: Todo executors use only sessionID and agent from the native context.
|
||||
return tools
|
||||
@@ -257,6 +314,71 @@ test("worker inheritance and coordinator isolation preserve build and default",
|
||||
});
|
||||
});
|
||||
|
||||
test("coordinator inherits only codegraph MCP permissions when that server exists", async () => {
|
||||
const absent = await host();
|
||||
expect(
|
||||
absent.agents
|
||||
.get("coordinate")
|
||||
?.permissions.some((rule) => rule.action.startsWith("codegraph_")),
|
||||
).toBe(false);
|
||||
expect(absent.mcpServers.has("codegraph")).toBe(false);
|
||||
|
||||
const result = await host(
|
||||
allModels,
|
||||
{ provider: "test" },
|
||||
{
|
||||
codegraph: { type: "local", command: ["codegraph"], codemode: true },
|
||||
docs: { type: "local", command: ["docs"], codemode: true },
|
||||
},
|
||||
["codegraph_codegraph_search", "codegraph_future_query", "docs_search"],
|
||||
);
|
||||
expect(result.mcpServers.get("codegraph")?.codemode).toBe(false);
|
||||
expect(result.mcpServers.get("docs")?.codemode).toBe(true);
|
||||
let coordinate = result.agents.get("coordinate")!;
|
||||
expect(coordinate.permissions).toContainEqual({
|
||||
action: "codegraph_codegraph_search",
|
||||
resource: "*",
|
||||
effect: "allow",
|
||||
});
|
||||
expect(coordinate.permissions.some((rule) => rule.action.startsWith("docs_"))).toBe(false);
|
||||
expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false);
|
||||
expect(coordinate.permissions.some((rule) => rule.action === "execute")).toBe(false);
|
||||
|
||||
await result.changeBuildPermissions([
|
||||
{ action: "*", resource: "*", effect: "allow" },
|
||||
{ action: "code*", resource: "*", effect: "ask" },
|
||||
{ action: "*search", resource: "*", effect: "deny" },
|
||||
{ action: "docs_*", resource: "*", effect: "allow" },
|
||||
{ action: "shell", resource: "*", effect: "allow" },
|
||||
]);
|
||||
coordinate = result.agents.get("coordinate")!;
|
||||
expect(coordinate.permissions).toContainEqual({
|
||||
action: "codegraph_future_query",
|
||||
resource: "*",
|
||||
effect: "ask",
|
||||
});
|
||||
expect(coordinate.permissions).toContainEqual({
|
||||
action: "codegraph_codegraph_search",
|
||||
resource: "*",
|
||||
effect: "deny",
|
||||
});
|
||||
expect(coordinate.permissions.some((rule) => rule.action === "docs_*")).toBe(false);
|
||||
expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false);
|
||||
|
||||
await result.changeCodegraph(undefined, []);
|
||||
coordinate = result.agents.get("coordinate")!;
|
||||
expect(coordinate.permissions.some((rule) => rule.action.startsWith("codegraph_"))).toBe(false);
|
||||
await result.changeCodegraph({ type: "local", command: ["codegraph"], codemode: true }, [
|
||||
"codegraph_added_later",
|
||||
]);
|
||||
expect(result.mcpServers.get("codegraph")?.codemode).toBe(false);
|
||||
expect(result.agents.get("coordinate")?.permissions).toContainEqual({
|
||||
action: "codegraph_added_later",
|
||||
resource: "*",
|
||||
effect: "ask",
|
||||
});
|
||||
});
|
||||
|
||||
test("manual calls and command-created child sessions cannot bypass delegation restrictions", async () => {
|
||||
const result = await host(allModels.slice(0, 2));
|
||||
for (const invoke of [result.delegate, result.command]) {
|
||||
|
||||
Reference in New Issue
Block a user