fix(opencode): preserve tool validation errors in v2 (#6519)

Failed bridge requests discarded the daemon error body before OpenCode could show the validation explanation to the agent. Preserve that message and status while retaining the HTTP fallback.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-10 21:01:07 +02:00
committed by GitHub
parent 0ba4d2526e
commit d0728cd1ed
2 changed files with 84 additions and 2 deletions
@@ -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<string, V2TestTool>();
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);
@@ -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) {