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.
This commit is contained in:
gsxdsm
2026-07-18 17:11:36 -07:00
parent a3dda8eaff
commit b8cd3d21fb
4 changed files with 125 additions and 8 deletions

View File

@@ -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");

View File

@@ -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;
/**

View File

@@ -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,

View File

@@ -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<ToolLike> | 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<void>((resolve) => {
server.close(() => resolve());
});
try {
unlinkSync(schemaPath);
} catch {
// best-effort
}
return null;
}
return {
toolCount: defs.length,