mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
Ask OpenCode before running a Paseo tool (#5801)
OpenCode checks permissions for its own tools and wraps MCP tools in an ask, but leaves plugin tools to call ctx.ask themselves. The bridge plugin never did, so an "ask" rule for paseo_* tools never produced a prompt and the tool ran. The bridge now asks with the tool's catalog name before dispatching, the same shape OpenCode uses for MCP tools. With no paseo_* rule OpenCode's default allow applies, so nothing new prompts. Fixes #5324 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Yuval Marcus <ymarcus93@gmail.com>
This commit is contained in:
co-authored by
Yuval Marcus
parent
7b0cace991
commit
e0a0cf990d
@@ -8,6 +8,10 @@ import { expect, test } from "vitest";
|
||||
|
||||
import { execCommand } from "../../../utils/spawn.js";
|
||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||
import {
|
||||
getRealProviderConfig,
|
||||
getRealProviderRuntimeSettings,
|
||||
} from "../../daemon-e2e/real-provider-test-config.js";
|
||||
import type { PaseoToolCatalog } from "../tools/types.js";
|
||||
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
||||
import { OpenCodeV2AgentClient } from "./opencode/v2/agent.js";
|
||||
@@ -199,6 +203,72 @@ test("real OpenCode server shares one process while shell.env stays session-scop
|
||||
}
|
||||
}, 240_000);
|
||||
|
||||
test("real OpenCode server asks before running a Paseo tool with an ask rule", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "paseo-opencode-ask-"));
|
||||
const cwd = path.join(root, "repo");
|
||||
const logger = createTestLogger();
|
||||
const bridge = new OpenCodeBridge({ paseoHome: root, logger });
|
||||
await bridge.start();
|
||||
const catalog = createCallerCatalog("ask-rule-agent");
|
||||
bridge.setManifestCatalog(catalog);
|
||||
const realSettings = getRealProviderRuntimeSettings("opencode");
|
||||
const runtimeSettings = {
|
||||
...realSettings,
|
||||
env: {
|
||||
...realSettings.env,
|
||||
OPENCODE_CONFIG_CONTENT: JSON.stringify({ permission: { "paseo_*": "ask" } }),
|
||||
},
|
||||
};
|
||||
const manager = new OpenCodeServerManager({
|
||||
logger,
|
||||
runtimeSettings,
|
||||
resolveHomeDir: () => root,
|
||||
decorateServerEnv: (env) => bridge.decorateServerEnv(env),
|
||||
});
|
||||
const client = new OpenCodeAgentClient(logger, runtimeSettings, {
|
||||
serverManager: manager,
|
||||
bridge,
|
||||
});
|
||||
let session: Awaited<ReturnType<OpenCodeAgentClient["createSession"]>> | undefined;
|
||||
|
||||
try {
|
||||
await mkdir(cwd);
|
||||
session = await client.createSession(
|
||||
{ ...getRealProviderConfig("opencode"), cwd },
|
||||
{ agentId: "ask-rule-agent", paseoTools: catalog },
|
||||
{ persistSession: false },
|
||||
);
|
||||
const asked: string[] = [];
|
||||
const completedTools: string[] = [];
|
||||
const activeSession = session;
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "permission_requested") {
|
||||
asked.push(event.request.name);
|
||||
void activeSession.respondToPermission(event.request.id, { behavior: "allow" });
|
||||
}
|
||||
if (
|
||||
event.type === "timeline" &&
|
||||
event.item.type === "tool_call" &&
|
||||
event.item.status === "completed"
|
||||
) {
|
||||
completedTools.push(event.item.name);
|
||||
}
|
||||
});
|
||||
|
||||
await session.run(
|
||||
"Call the paseo_report_caller_agent_id tool once, then reply with its exact result. Do not use any other tools.",
|
||||
);
|
||||
|
||||
expect(completedTools).toContain("paseo_report_caller_agent_id");
|
||||
expect(asked).toContain("paseo_report_caller_agent_id");
|
||||
} finally {
|
||||
await session?.close();
|
||||
await manager.shutdown();
|
||||
await bridge.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
function createCallerCatalog(callerAgentId: string): PaseoToolCatalog {
|
||||
const tool = {
|
||||
name: "report_caller_agent_id",
|
||||
|
||||
@@ -33,6 +33,13 @@ export default async function paseoPlugin(input, options) {
|
||||
description: definition.description,
|
||||
args: jsonSchemaObjectToZodShape(definition.inputSchema),
|
||||
execute: async (args, context) => {
|
||||
// OpenCode leaves permission checks to plugin tools, so ask it the way it asks for MCP tools.
|
||||
await context.ask({
|
||||
permission: `paseo_${definition.name}`,
|
||||
patterns: ["*"],
|
||||
always: ["*"],
|
||||
metadata: {},
|
||||
});
|
||||
let result;
|
||||
try {
|
||||
result = await request(
|
||||
|
||||
@@ -102,6 +102,12 @@ describe("OpenCodeBridge", () => {
|
||||
const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-opencode-bridge-"));
|
||||
temporaryDirectories.push(paseoHome);
|
||||
const catalog = createCatalog();
|
||||
const executedInputs: unknown[] = [];
|
||||
const executeTool = catalog.executeTool;
|
||||
catalog.executeTool = async (name, input, context) => {
|
||||
executedInputs.push(input);
|
||||
return await executeTool(name, input, context);
|
||||
};
|
||||
const bridge = new OpenCodeBridge({ paseoHome, logger: createTestLogger() });
|
||||
await bridge.start();
|
||||
bridge.setManifestCatalog(catalog);
|
||||
@@ -173,12 +179,30 @@ describe("OpenCodeBridge", () => {
|
||||
token: plugin.token,
|
||||
},
|
||||
);
|
||||
const asked: unknown[] = [];
|
||||
await expect(
|
||||
hooks.tool.paseo_echo_context.execute(
|
||||
{ value: "through bundled plugin" },
|
||||
{ sessionID: "ses_one" },
|
||||
{ sessionID: "ses_one", ask: async (request: unknown) => void asked.push(request) },
|
||||
),
|
||||
).resolves.toMatchObject({ output: "through bundled plugin" });
|
||||
expect(asked).toEqual([
|
||||
{ permission: "paseo_echo_context", patterns: ["*"], always: ["*"], metadata: {} },
|
||||
]);
|
||||
|
||||
executedInputs.length = 0;
|
||||
await expect(
|
||||
hooks.tool.paseo_echo_context.execute(
|
||||
{ value: "rejected" },
|
||||
{
|
||||
sessionID: "ses_one",
|
||||
ask: async () => {
|
||||
throw new Error("The user rejected permission to use this specific tool call.");
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("rejected permission");
|
||||
expect(executedInputs).toEqual([]);
|
||||
|
||||
release();
|
||||
const pluginError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
|
||||
Reference in New Issue
Block a user