fix(channels): harden bundled runtime sidecar resolution

This commit is contained in:
Vincent Koc
2026-04-06 18:06:38 +01:00
parent 7d9a6b5572
commit 48fea1021a
2 changed files with 129 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import type {
BundledChannelEntryContract,
@@ -21,7 +21,12 @@ type GeneratedBundledChannelEntry = {
};
const log = createSubsystemLogger("channels");
const OPENCLAW_PACKAGE_ROOT = path.resolve(fileURLToPath(new URL("../../..", import.meta.url)));
const OPENCLAW_PACKAGE_ROOT =
resolveOpenClawPackageRootSync({
cwd: process.cwd(),
moduleUrl: import.meta.url,
argv1: process.argv[1],
}) ?? process.cwd();
function resolveChannelPluginModuleEntry(
moduleExport: unknown,

View File

@@ -10,6 +10,7 @@ import type { PluginRuntime } from "../plugins/runtime/types.js";
import {
buildPluginLoaderAliasMap,
buildPluginLoaderJitiOptions,
resolveLoaderPackageRoot,
shouldPreferNativeJiti,
} from "../plugins/sdk-alias.js";
import type { AnyAgentTool, OpenClawPluginApi, PluginCommandContext } from "../plugins/types.js";
@@ -85,6 +86,82 @@ function resolveEntryBoundaryRoot(importMetaUrl: string): string {
return path.dirname(fileURLToPath(importMetaUrl));
}
type BundledEntryModuleCandidate = {
path: string;
boundaryRoot: string;
};
function addBundledEntryCandidates(
candidates: BundledEntryModuleCandidate[],
basePath: string,
boundaryRoot: string,
): void {
for (const candidate of resolveSpecifierCandidates(basePath)) {
if (
candidates.some((entry) => entry.path === candidate && entry.boundaryRoot === boundaryRoot)
) {
continue;
}
candidates.push({ path: candidate, boundaryRoot });
}
}
function resolveBundledEntryModuleCandidates(
importMetaUrl: string,
specifier: string,
): BundledEntryModuleCandidate[] {
const importerPath = fileURLToPath(importMetaUrl);
const importerDir = path.dirname(importerPath);
const boundaryRoot = resolveEntryBoundaryRoot(importMetaUrl);
const candidates: BundledEntryModuleCandidate[] = [];
const primaryResolved = path.resolve(importerDir, specifier);
addBundledEntryCandidates(candidates, primaryResolved, boundaryRoot);
const sourceRelativeSpecifier = specifier.replace(/^\.\/src\//u, "./");
if (sourceRelativeSpecifier !== specifier) {
addBundledEntryCandidates(
candidates,
path.resolve(importerDir, sourceRelativeSpecifier),
boundaryRoot,
);
}
const packageRoot = resolveLoaderPackageRoot({
modulePath: importerPath,
moduleUrl: importMetaUrl,
cwd: importerDir,
argv1: process.argv[1],
});
if (!packageRoot) {
return candidates;
}
const distExtensionsRoot = path.join(packageRoot, "dist", "extensions") + path.sep;
if (!importerPath.startsWith(distExtensionsRoot)) {
return candidates;
}
const pluginDirName = path.basename(importerDir);
const sourcePluginRoot = path.join(packageRoot, "extensions", pluginDirName);
if (sourcePluginRoot === boundaryRoot) {
return candidates;
}
addBundledEntryCandidates(
candidates,
path.resolve(sourcePluginRoot, specifier),
sourcePluginRoot,
);
if (sourceRelativeSpecifier !== specifier) {
addBundledEntryCandidates(
candidates,
path.resolve(sourcePluginRoot, sourceRelativeSpecifier),
sourcePluginRoot,
);
}
return candidates;
}
function formatBundledEntryUnknownError(error: unknown): string {
if (typeof error === "string") {
return error;
@@ -120,31 +197,58 @@ function formatBundledEntryModuleOpenFailure(params: {
}
function resolveBundledEntryModulePath(importMetaUrl: string, specifier: string): string {
const importerPath = fileURLToPath(importMetaUrl);
const resolved = path.resolve(path.dirname(importerPath), specifier);
const boundaryRoot = resolveEntryBoundaryRoot(importMetaUrl);
const candidate =
resolveSpecifierCandidates(resolved).find((entry) => fs.existsSync(entry)) ?? resolved;
const opened = openBoundaryFileSync({
absolutePath: candidate,
rootPath: boundaryRoot,
boundaryLabel: "plugin root",
rejectHardlinks: false,
skipLexicalRootCheck: true,
});
if (!opened.ok) {
const candidates = resolveBundledEntryModuleCandidates(importMetaUrl, specifier);
const fallbackCandidate = candidates[0] ?? {
path: path.resolve(path.dirname(fileURLToPath(importMetaUrl)), specifier),
boundaryRoot: resolveEntryBoundaryRoot(importMetaUrl),
};
let firstFailure: {
candidate: BundledEntryModuleCandidate;
failure: Extract<ReturnType<typeof openBoundaryFileSync>, { ok: false }>;
} | null = null;
for (const candidate of candidates) {
const opened = openBoundaryFileSync({
absolutePath: candidate.path,
rootPath: candidate.boundaryRoot,
boundaryLabel: "plugin root",
rejectHardlinks: false,
skipLexicalRootCheck: true,
});
if (opened.ok) {
fs.closeSync(opened.fd);
return opened.path;
}
firstFailure ??= { candidate, failure: opened };
}
const failure = firstFailure;
if (!failure) {
throw new Error(
formatBundledEntryModuleOpenFailure({
importMetaUrl,
specifier,
resolvedPath: candidate,
boundaryRoot,
failure: opened,
resolvedPath: fallbackCandidate.path,
boundaryRoot: fallbackCandidate.boundaryRoot,
failure: {
ok: false,
reason: "path",
error: new Error(`ENOENT: no such file or directory, lstat '${fallbackCandidate.path}'`),
},
}),
);
}
fs.closeSync(opened.fd);
return opened.path;
throw new Error(
formatBundledEntryModuleOpenFailure({
importMetaUrl,
specifier,
resolvedPath: failure.candidate.path,
boundaryRoot: failure.candidate.boundaryRoot,
failure: failure.failure,
}),
);
}
function getJiti(modulePath: string) {