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,