From 9d772d6eab528b48235a036ad2585348c4860902 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 19 Mar 2026 09:16:34 -0700 Subject: [PATCH] fix(ci): normalize bundle mcp paths and skip explicit channel scans --- src/infra/outbound/channel-selection.test.ts | 17 ++++++++++ src/infra/outbound/channel-selection.ts | 8 ++--- src/plugins/bundle-mcp.ts | 33 ++++++++++++++------ 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/infra/outbound/channel-selection.test.ts b/src/infra/outbound/channel-selection.test.ts index fdb4ecd4b6f..9e6a1fa74d6 100644 --- a/src/infra/outbound/channel-selection.test.ts +++ b/src/infra/outbound/channel-selection.test.ts @@ -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, diff --git a/src/infra/outbound/channel-selection.ts b/src/infra/outbound/channel-selection.ts index 0e87a8e4950..f9c6f558769 100644 --- a/src/infra/outbound/channel-selection.ts +++ b/src/infra/outbound/channel-selection.ts @@ -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", }; } diff --git a/src/plugins/bundle-mcp.ts b/src/plugins/bundle-mcp.ts index b0960c17a93..620eb4a0a1f 100644 --- a/src/plugins/bundle-mcp.ts +++ b/src/plugins/bundle-mcp.ts @@ -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; @@ -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 }), ]), ), };