diff --git a/packages/server/src/server/agent/providers/opencode/bridge.test.ts b/packages/server/src/server/agent/providers/opencode/bridge.test.ts index 7130ac13b9..076cbe94bd 100644 --- a/packages/server/src/server/agent/providers/opencode/bridge.test.ts +++ b/packages/server/src/server/agent/providers/opencode/bridge.test.ts @@ -9,6 +9,10 @@ import { afterEach, describe, expect, test, vi } from "vitest"; import { createTestLogger } from "../../../../test-utils/test-logger.js"; import type { PaseoToolCatalog } from "../../tools/types.js"; +import { createPaseoToolCatalog } from "../../tools/paseo-tools.js"; +import { AgentManager } from "../../agent-manager.js"; +import { AgentStorage } from "../../agent-storage.js"; +import { ProviderSnapshotManager } from "../../provider-snapshot-manager.js"; import { OpenCodeBridge, loadOpenCodeBridgePluginArtifact, @@ -291,7 +295,10 @@ describe("OpenCodeBridge", () => { ).resolves.toMatchObject({ content: [{ type: "text", text: "child result" }] }); await expect( tools.get("paseo_echo_context")!.execute({ value: "blocked" }, { sessionID: "disabled" }), - ).rejects.toThrow("HTTP 403"); + ).rejects.toMatchObject({ + message: "Paseo tools are disabled for this session", + status: 403, + }); await dispose(); } finally { release(); @@ -300,6 +307,72 @@ describe("OpenCodeBridge", () => { } }); + test("v2 plugin preserves create_agent validation errors from the daemon", async () => { + const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-opencode-v2-errors-")); + temporaryDirectories.push(paseoHome); + const logger = createTestLogger(); + const providerSnapshotManager = new ProviderSnapshotManager({ logger }); + const catalog = createPaseoToolCatalog({ + agentManager: new AgentManager({ logger }), + agentStorage: new AgentStorage(paseoHome, logger), + providerSnapshotManager, + logger, + }); + const bridge = new OpenCodeBridge({ paseoHome, logger }); + bridge.setManifestCatalog(catalog); + await bridge.start(); + const release = bridge.bindSession({ sessionId: "caller", env: {}, tools: catalog }); + try { + const env = await bridge.decorateV2ServerEnv({}); + const { plugins } = JSON.parse(env.OPENCODE_CONFIG_CONTENT); + const plugin = plugins[0]; + const module = await import( + pathToFileURL(path.join(fileURLToPath(plugin.package), "server.js")).href + ); + const tools = new Map(); + const dispose = await module.default.setup({ + options: plugin.options, + tool: { + transform: async (transform: (editor: { add(tool: V2TestTool): void }) => void) => { + transform({ add: (tool) => tools.set(tool.name, tool) }); + return { dispose: async () => undefined }; + }, + }, + session: { + context: async () => [], + get: async () => ({ parentID: "" }), + hook: async () => ({ dispose: async () => undefined }), + }, + } satisfies V2TestPluginContext); + try { + const input = { provider: "codex", title: "Invalid provider", initialPrompt: "Hello" }; + const response = await fetch( + `${plugin.options.baseUrl}/_internal/opencode/sessions/caller/tools/create_agent`, + { + method: "POST", + headers: { + Authorization: `Bearer ${plugin.options.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }, + ); + expect(response.status).toBe(500); + const payload = await response.json(); + expect(payload.error).toContain("provider must be provider/model"); + await expect( + tools.get("paseo_create_agent")!.execute(input, { sessionID: "caller" }), + ).rejects.toMatchObject({ message: payload.error, status: 500 }); + } finally { + await dispose(); + } + } finally { + release(); + await bridge.close(); + await providerSnapshotManager.shutdown(); + } + }); + test("v2 plugin returns a tool image as OpenCode file content", async () => { const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-opencode-v2-image-")); temporaryDirectories.push(paseoHome); diff --git a/packages/server/src/server/agent/providers/opencode/v2/bridge-plugin.mjs b/packages/server/src/server/agent/providers/opencode/v2/bridge-plugin.mjs index 190e015d8e..6b845d19ed 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/bridge-plugin.mjs +++ b/packages/server/src/server/agent/providers/opencode/v2/bridge-plugin.mjs @@ -13,7 +13,16 @@ export default { ...(body === undefined ? {} : { body: JSON.stringify(body) }), }); if (allowMissing && response.status === 404) return null; - if (!response.ok) throw new Error(`Paseo tool bridge returned HTTP ${response.status}`); + if (!response.ok) { + const payload = await response.json().catch(() => null); + const message = + typeof payload?.error === "string" && payload.error + ? payload.error + : `Paseo tool bridge returned HTTP ${response.status}`; + const error = new Error(message); + error.status = response.status; + throw error; + } return response.json(); } async function scope(sessionID) {