fix(ci): normalize bundle mcp paths and skip explicit channel scans

This commit is contained in:
Vincent Koc 2026-03-19 09:16:34 -07:00
parent ff6541f69d
commit 9d772d6eab
3 changed files with 44 additions and 14 deletions

View File

@ -143,6 +143,23 @@ describe("resolveMessageChannelSelection", () => {
});
});
it("does not probe configured channels when an explicit channel is available", async () => {
const isConfigured = vi.fn(async () => true);
mocks.listChannelPlugins.mockReturnValue([makePlugin({ id: "slack", isConfigured })]);
const selection = await resolveMessageChannelSelection({
cfg: {} as never,
channel: "slack",
});
expect(selection).toEqual({
channel: "slack",
configured: [],
source: "explicit",
});
expect(isConfigured).not.toHaveBeenCalled();
});
it("falls back to tool context channel when explicit channel is unknown", async () => {
const selection = await resolveMessageChannelSelection({
cfg: {} as never,

View File

@ -1,6 +1,6 @@
import { listChannelPlugins } from "../../channels/plugins/index.js";
import type { ChannelPlugin } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import { defaultRuntime } from "../../runtime.js";
import {
listDeliverableMessageChannels,
@ -165,7 +165,7 @@ export async function resolveMessageChannelSelection(params: {
if (fallback) {
return {
channel: fallback,
configured: await listConfiguredMessageChannels(params.cfg),
configured: [],
source: "tool-context-fallback",
};
}
@ -176,7 +176,7 @@ export async function resolveMessageChannelSelection(params: {
}
return {
channel: availableExplicit,
configured: await listConfiguredMessageChannels(params.cfg),
configured: [],
source: "explicit",
};
}
@ -188,7 +188,7 @@ export async function resolveMessageChannelSelection(params: {
if (fallback) {
return {
channel: fallback,
configured: await listConfiguredMessageChannels(params.cfg),
configured: [],
source: "tool-context-fallback",
};
}

View File

@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import type { PluginBundleFormat } from "./types.js";
import { applyMergePatch } from "../config/merge-patch.js";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { isRecord } from "../utils.js";
@ -13,7 +14,7 @@ import {
} from "./bundle-manifest.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import type { PluginBundleFormat } from "./types.js";
import { safeRealpathSync } from "./path-safety.js";
export type BundleMcpServerConfig = Record<string, unknown>;
@ -121,6 +122,14 @@ function expandBundleRootPlaceholders(value: string, rootDir: string): string {
return value.split(CLAUDE_PLUGIN_ROOT_PLACEHOLDER).join(rootDir);
}
function canonicalizeBundlePath(targetPath: string): string {
return path.normalize(safeRealpathSync(targetPath) ?? path.resolve(targetPath));
}
function normalizeExpandedAbsolutePath(value: string): string {
return path.isAbsolute(value) ? path.normalize(value) : value;
}
function absolutizeBundleMcpServer(params: {
rootDir: string;
baseDir: string;
@ -137,7 +146,7 @@ function absolutizeBundleMcpServer(params: {
const expanded = expandBundleRootPlaceholders(command, params.rootDir);
next.command = isExplicitRelativePath(expanded)
? path.resolve(params.baseDir, expanded)
: expanded;
: normalizeExpandedAbsolutePath(expanded);
}
const cwd = next.cwd;
@ -150,7 +159,7 @@ function absolutizeBundleMcpServer(params: {
if (typeof workingDirectory === "string") {
const expanded = expandBundleRootPlaceholders(workingDirectory, params.rootDir);
next.workingDirectory = path.isAbsolute(expanded)
? expanded
? path.normalize(expanded)
: path.resolve(params.baseDir, expanded);
}
@ -161,7 +170,7 @@ function absolutizeBundleMcpServer(params: {
}
const expanded = expandBundleRootPlaceholders(entry, params.rootDir);
if (!isExplicitRelativePath(expanded)) {
return expanded;
return normalizeExpandedAbsolutePath(expanded);
}
return path.resolve(params.baseDir, expanded);
});
@ -171,7 +180,9 @@ function absolutizeBundleMcpServer(params: {
next.env = Object.fromEntries(
Object.entries(next.env).map(([key, value]) => [
key,
typeof value === "string" ? expandBundleRootPlaceholders(value, params.rootDir) : value,
typeof value === "string"
? normalizeExpandedAbsolutePath(expandBundleRootPlaceholders(value, params.rootDir))
: value,
]),
);
}
@ -183,10 +194,11 @@ function loadBundleFileBackedMcpConfig(params: {
rootDir: string;
relativePath: string;
}): BundleMcpConfig {
const absolutePath = path.resolve(params.rootDir, params.relativePath);
const rootDir = canonicalizeBundlePath(params.rootDir);
const absolutePath = path.resolve(rootDir, params.relativePath);
const opened = openBoundaryFileSync({
absolutePath,
rootPath: params.rootDir,
rootPath: rootDir,
boundaryLabel: "plugin root",
rejectHardlinks: true,
});
@ -200,12 +212,12 @@ function loadBundleFileBackedMcpConfig(params: {
}
const raw = JSON.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;
const servers = extractMcpServerMap(raw);
const baseDir = path.dirname(absolutePath);
const baseDir = canonicalizeBundlePath(path.dirname(absolutePath));
return {
mcpServers: Object.fromEntries(
Object.entries(servers).map(([serverName, server]) => [
serverName,
absolutizeBundleMcpServer({ rootDir: params.rootDir, baseDir, server }),
absolutizeBundleMcpServer({ rootDir, baseDir, server }),
]),
),
};
@ -221,12 +233,13 @@ function loadBundleInlineMcpConfig(params: {
if (!isRecord(params.raw.mcpServers)) {
return { mcpServers: {} };
}
const baseDir = canonicalizeBundlePath(params.baseDir);
const servers = extractMcpServerMap(params.raw.mcpServers);
return {
mcpServers: Object.fromEntries(
Object.entries(servers).map(([serverName, server]) => [
serverName,
absolutizeBundleMcpServer({ rootDir: params.baseDir, baseDir: params.baseDir, server }),
absolutizeBundleMcpServer({ rootDir: baseDir, baseDir, server }),
]),
),
};