From b8cd3d21fbf549ada92f386556c34c2cb895afa6 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 18 Jul 2026 17:11:36 -0700 Subject: [PATCH] fix: surface OMP ACP Internal error details and harden tool MCP spawn OMP session/new was dying with opaque "Internal error" when fusion-custom-tools MCP pointed at a missing schema server, or when bare models hit unauthenticated providers. Prefer data.details in diagnostics, resolve mcp-schema-server.cjs from multiple package layouts, skip the tool bridge when the asset is missing, drop stdio MCP entries whose command path does not exist, and forward common provider env keys (ZAI/MiniMax/Kimi) into the ACP subprocess. --- .../src/__tests__/tool-bridge.test.ts | 7 ++ .../src/acp-settings.ts | 8 ++ .../src/runtime-adapter.ts | 74 ++++++++++++++++++- .../src/tool-bridge.ts | 44 ++++++++++- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/plugins/fusion-plugin-omp-runtime/src/__tests__/tool-bridge.test.ts b/plugins/fusion-plugin-omp-runtime/src/__tests__/tool-bridge.test.ts index 88de7f42e5..9551e1cfd9 100644 --- a/plugins/fusion-plugin-omp-runtime/src/__tests__/tool-bridge.test.ts +++ b/plugins/fusion-plugin-omp-runtime/src/__tests__/tool-bridge.test.ts @@ -2,6 +2,7 @@ import { existsSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { FUSION_OMP_TOOL_BRIDGE_URL, + fusionToolsMcpServerPath, startFusionToolBridge, toolsToMcpToolDefs, } from "../tool-bridge.js"; @@ -97,6 +98,12 @@ describe("tool-bridge", () => { expect(await startFusionToolBridge(undefined)).toBeNull(); }); + it("resolves mcp-schema-server.cjs next to the plugin package", () => { + const path = fusionToolsMcpServerPath(); + expect(path).toBeTruthy(); + expect(existsSync(path!)).toBe(true); + }); + it("describes fusion tools in system rules", () => { const rules = buildOmpFusionToolRules({ fusionToolCount: 12, operatorMcpCount: 1 }); expect(rules).toContain("fusion-custom-tools"); diff --git a/plugins/fusion-plugin-omp-runtime/src/acp-settings.ts b/plugins/fusion-plugin-omp-runtime/src/acp-settings.ts index ed0ecfb038..abd7418c89 100644 --- a/plugins/fusion-plugin-omp-runtime/src/acp-settings.ts +++ b/plugins/fusion-plugin-omp-runtime/src/acp-settings.ts @@ -34,6 +34,14 @@ export const OMP_ACP_ENV_ALLOWLIST = [ "GEMINI_API_KEY", "XAI_API_KEY", "GROK_API_KEY", + // FNXC:OmpAcp 2026-07-18-23:50: default omp models often use zai/minimax/kimi keys in env. + "ZAI_API_KEY", + "Z_AI_API_KEY", + "GLM_API_KEY", + "MINIMAX_API_KEY", + "MINIMAX_GROUP_ID", + "KIMI_API_KEY", + "MOONSHOT_API_KEY", ] as const; /** diff --git a/plugins/fusion-plugin-omp-runtime/src/runtime-adapter.ts b/plugins/fusion-plugin-omp-runtime/src/runtime-adapter.ts index 11fa363a75..43bc16e355 100644 --- a/plugins/fusion-plugin-omp-runtime/src/runtime-adapter.ts +++ b/plugins/fusion-plugin-omp-runtime/src/runtime-adapter.ts @@ -1,3 +1,4 @@ +import { existsSync } from "node:fs"; import { AcpRuntimeAdapter } from "./acp/index.js"; import { buildOmpAcpRuntimeSettings, @@ -82,16 +83,66 @@ function compactDiagnostic(value: string): string { return value.replace(/\s+/g, " ").trim(); } +/* +FNXC:OmpAcp 2026-07-18-23:50: +omp returns JSON-RPC errors as `{ message: "Internal error", data: { details: "..." } }`. +The ACP SDK often only puts `message` on Error.message, which hid the real cause +(e.g. fusion-custom-tools ENOENT or "No API key found for alibaba-coding-plan"). +Prefer data.details / nested cause when present so operators can fix auth or packaging. +*/ +function extractErrorReason(error: unknown): string { + if (error == null) return "unknown error"; + if (typeof error === "string") return error; + if (error instanceof Error) { + const anyErr = error as Error & { + data?: { details?: unknown; message?: unknown } | string; + cause?: unknown; + }; + const data = anyErr.data; + if (typeof data === "string" && data.trim()) { + return `${anyErr.message}: ${data.trim()}`; + } + if (data && typeof data === "object") { + const details = data.details; + if (typeof details === "string" && details.trim()) { + return `${anyErr.message}: ${details.trim()}`; + } + if (typeof data.message === "string" && data.message.trim() && data.message !== anyErr.message) { + return `${anyErr.message}: ${data.message.trim()}`; + } + } + if (anyErr.cause) { + const causeReason = extractErrorReason(anyErr.cause); + if (causeReason && causeReason !== anyErr.message) { + return `${anyErr.message}: ${causeReason}`; + } + } + return anyErr.message || String(error); + } + if (typeof error === "object") { + const rec = error as { message?: unknown; data?: { details?: unknown } }; + const message = typeof rec.message === "string" ? rec.message : String(error); + const details = rec.data && typeof rec.data === "object" ? rec.data.details : undefined; + if (typeof details === "string" && details.trim()) { + return `${message}: ${details.trim()}`; + } + return message; + } + return String(error); +} + function describeCreateFailure(error: unknown): string { - const reason = error instanceof Error ? error.message : String(error ?? "unknown error"); + const reason = extractErrorReason(error); return compactDiagnostic( `OMP ACP failed to start: ${reason}. Ensure the \`omp\` binary is installed and authenticated (` + - `\`omp acp\`, credentials under ~/.omp), or set provider API keys in the environment.`, + `\`omp acp\`, credentials under ~/.omp), or set provider API keys in the environment. ` + + `If the details mention fusion-custom-tools / mcp-schema-server ENOENT, rebuild the CLI package so mcp-schema-server.cjs ships next to the omp plugin. ` + + `If they mention "No API key" for a provider, pick a qualified model (e.g. minimax-code/MiniMax-M2.5 or zai/glm-5.2) or re-auth that provider in omp.`, ); } function describePromptFailure(error: unknown): string { - const reason = error instanceof Error ? error.message : String(error ?? "unknown error"); + const reason = extractErrorReason(error); return compactDiagnostic(`OMP ACP turn failed: ${reason}`); } @@ -275,10 +326,25 @@ export class OmpRuntimeAdapter implements AgentRuntime { toolBridge = null; } + /* + FNXC:OmpAcp 2026-07-18-23:50: + Drop stdio MCP entries whose command binary is missing before session/new. + omp turns a single ENOENT into JSON-RPC Internal error and aborts the whole + ACP session (observed as "OMP ACP failed to start: Internal error"). + */ const mcpServers: AcpMcpServer[] = [ ...operatorMcp, ...(toolBridge ? [toolBridge.mcpServer] : []), - ]; + ].filter((server) => { + if (!("command" in server) || typeof server.command !== "string") return true; + const command = server.command.trim(); + if (!command) return false; + // Absolute paths must exist; PATH names are left to omp's spawn resolution. + if (command.includes("/") || command.includes("\\")) { + return existsSync(command); + } + return true; + }); const toolRules = buildOmpFusionToolRules({ fusionToolCount: toolBridge?.toolCount, diff --git a/plugins/fusion-plugin-omp-runtime/src/tool-bridge.ts b/plugins/fusion-plugin-omp-runtime/src/tool-bridge.ts index bf68b42e40..31f692108f 100644 --- a/plugins/fusion-plugin-omp-runtime/src/tool-bridge.ts +++ b/plugins/fusion-plugin-omp-runtime/src/tool-bridge.ts @@ -8,7 +8,7 @@ after the session ends. Ported from fusion-plugin-grok-runtime for full fn_* par */ import { createServer, type Server } from "node:http"; -import { unlinkSync, writeFileSync } from "node:fs"; +import { existsSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -62,9 +62,29 @@ export function toolsToMcpToolDefs(tools: ReadonlyArray | undefined): })); } -function fusionToolsMcpServerPath(): string { - // Packaged CLI copies this as mcp-schema-server.cjs next to the bundled plugin. - return join(dirname(fileURLToPath(import.meta.url)), "mcp-schema-server.cjs"); +/** + * FNXC:OmpAcp 2026-07-18-23:50: + * Resolve `mcp-schema-server.cjs` for the fusion-custom-tools stdio MCP child. + * `import.meta.url` can land on source (`src/`), packaged `bundled.js`, or a + * hot-reload sibling (`.bundled.reload-*.js`). If the file is missing, omp's + * session/new fails with JSON-RPC Internal error / ENOENT and the entire OMP + * ACP session dies before any turn runs — surface a clear miss instead. + */ +export function fusionToolsMcpServerPath(): string | null { + const here = dirname(fileURLToPath(import.meta.url)); + const candidates = [ + // Packaged CLI / same-dir as bundled.js or tool-bridge source. + join(here, "mcp-schema-server.cjs"), + // Source layout when running from dist/ that still has ../src assets. + join(here, "src", "mcp-schema-server.cjs"), + join(here, "..", "src", "mcp-schema-server.cjs"), + // Monorepo plugin root relative to src/ or dist/plugins/.../bundled.js. + join(here, "..", "mcp-schema-server.cjs"), + ]; + for (const candidate of candidates) { + if (existsSync(candidate)) return candidate; + } + return null; } function resultToText(result: unknown): string { @@ -182,6 +202,22 @@ export async function startFusionToolBridge( const bridgeUrl = `http://127.0.0.1:${address.port}`; const serverPath = fusionToolsMcpServerPath(); + /* + FNXC:OmpAcp 2026-07-18-23:50: + Prefer a session without Fusion fn_* tools over a hard session/new Internal + error when the MCP schema server asset is missing (ENOENT on posix_spawn). + */ + if (!serverPath) { + await new Promise((resolve) => { + server.close(() => resolve()); + }); + try { + unlinkSync(schemaPath); + } catch { + // best-effort + } + return null; + } return { toolCount: defs.length,