mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 19:16:33 +00:00
* fix(ollama): bypass proxy for local embeddings * fix(ollama): keep managed proxy bypass loopback-only * fix(ollama): keep proxy bypass internal * fix(ollama): keep proxy bypass private * fix(ollama): harden internal proxy bypass * chore(plugin-sdk): refresh api baseline * fix(ollama): keep internal bypass out of qa aliases * test(ollama): keep ssrf runtime mock complete * fix(ollama): keep dist sdk aliases public-only * fix(ollama): keep fetch bypass out of infra runtime * fix(ollama): preserve packaged private sdk alias * test(ollama): harden private ssrf alias coverage * test(ollama): cover private ssrf resolver edges * fix(ollama): scope private sdk native aliases * test(ollama): audit blocked loopback bypasses * fix(plugins): keep staged sdk aliases public-only * test(ollama): harden proxy bypass proof * test(ollama): cover origin mismatch proxy path * test(ollama): cover ipv6 and batch bypass paths * fix lint findings in Ollama proxy tests * refactor: tighten Ollama proxy bypass * fix: widen private sdk owner registry type * test: stabilize Ollama proxy PR checks --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
245 lines
7.8 KiB
TypeScript
245 lines
7.8 KiB
TypeScript
import fs from "node:fs";
|
|
import Module from "node:module";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { buildPluginLoaderAliasMap, type PluginSdkResolutionPreference } from "./sdk-alias.js";
|
|
|
|
type ResolveFilename = (
|
|
request: string,
|
|
parent: NodeJS.Module | undefined,
|
|
isMain: boolean,
|
|
options?: { paths?: string[] },
|
|
) => string;
|
|
|
|
type ModuleWithResolver = typeof Module & {
|
|
_resolveFilename?: ResolveFilename;
|
|
};
|
|
|
|
type NativeAliasEntry = {
|
|
parentRoot: string;
|
|
target: string;
|
|
};
|
|
|
|
export type InstallOpenClawPluginSdkNativeResolverOptions = {
|
|
modulePath?: string;
|
|
pluginModulePath?: string;
|
|
allowedParentRoots?: readonly string[];
|
|
argv1?: string;
|
|
moduleUrl?: string;
|
|
pluginSdkResolution?: PluginSdkResolutionPreference;
|
|
};
|
|
|
|
const moduleWithResolver = Module as ModuleWithResolver;
|
|
const nodeResolveFilenameProperty = "_resolveFilename" as const;
|
|
const PLUGIN_SDK_PACKAGE_PREFIXES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const;
|
|
const pluginSdkNativeAliases = new Map<string, NativeAliasEntry[]>();
|
|
let installed = false;
|
|
let previousResolveFilename: ResolveFilename | undefined;
|
|
|
|
function resolveLoaderModulePath(options: InstallOpenClawPluginSdkNativeResolverOptions): string {
|
|
return options.modulePath ?? fileURLToPath(options.moduleUrl ?? import.meta.url);
|
|
}
|
|
|
|
function isPluginSdkAliasSpecifier(specifier: string): boolean {
|
|
return PLUGIN_SDK_PACKAGE_PREFIXES.some(
|
|
(prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`),
|
|
);
|
|
}
|
|
|
|
function isNativeLoadableSdkTarget(targetPath: string): boolean {
|
|
switch (path.extname(targetPath)) {
|
|
case ".cjs":
|
|
case ".js":
|
|
case ".mjs":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function normalizePathForBoundary(candidate: string): string {
|
|
try {
|
|
return fs.realpathSync(candidate);
|
|
} catch {
|
|
return path.resolve(candidate);
|
|
}
|
|
}
|
|
|
|
function findNearestPackageRoot(modulePath: string): string {
|
|
let cursor = path.dirname(path.resolve(modulePath));
|
|
for (let i = 0; i < 12; i += 1) {
|
|
if (fs.existsSync(path.join(cursor, "package.json"))) {
|
|
return cursor;
|
|
}
|
|
const parent = path.dirname(cursor);
|
|
if (parent === cursor) {
|
|
break;
|
|
}
|
|
cursor = parent;
|
|
}
|
|
return path.dirname(path.resolve(modulePath));
|
|
}
|
|
|
|
function findBundledPluginRoot(modulePath: string): string | undefined {
|
|
const resolvedModulePath = normalizePathForBoundary(modulePath);
|
|
const packageRoot = normalizePathForBoundary(resolveLoaderPackageRootFromModulePath(modulePath));
|
|
for (const relativeRoot of ["extensions", "dist/extensions", "dist-runtime/extensions"]) {
|
|
const bundledRoot = path.join(packageRoot, relativeRoot);
|
|
const relative = path.relative(bundledRoot, resolvedModulePath);
|
|
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
continue;
|
|
}
|
|
const [pluginId] = relative.split(path.sep);
|
|
if (pluginId) {
|
|
return path.join(bundledRoot, pluginId);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function resolveLoaderPackageRootFromModulePath(modulePath: string): string {
|
|
let cursor = path.dirname(path.resolve(modulePath));
|
|
for (let i = 0; i < 12; i += 1) {
|
|
const packageJsonPath = path.join(cursor, "package.json");
|
|
if (fs.existsSync(packageJsonPath)) {
|
|
try {
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
|
bin?: unknown;
|
|
name?: unknown;
|
|
};
|
|
if (
|
|
packageJson.name === "openclaw" ||
|
|
(typeof packageJson.bin === "object" &&
|
|
packageJson.bin !== null &&
|
|
typeof (packageJson.bin as { openclaw?: unknown }).openclaw === "string")
|
|
) {
|
|
return cursor;
|
|
}
|
|
} catch {
|
|
// Keep walking; malformed package metadata should not widen alias scope.
|
|
}
|
|
}
|
|
const parent = path.dirname(cursor);
|
|
if (parent === cursor) {
|
|
break;
|
|
}
|
|
cursor = parent;
|
|
}
|
|
return findNearestPackageRoot(modulePath);
|
|
}
|
|
|
|
function resolveAllowedParentRoot(modulePath: string): string {
|
|
return findBundledPluginRoot(modulePath) ?? findNearestPackageRoot(modulePath);
|
|
}
|
|
|
|
function resolveAllowedParentRoots(
|
|
options: InstallOpenClawPluginSdkNativeResolverOptions,
|
|
): string[] {
|
|
const roots = new Set<string>();
|
|
if (options.pluginModulePath) {
|
|
roots.add(normalizePathForBoundary(resolveAllowedParentRoot(options.pluginModulePath)));
|
|
}
|
|
for (const root of options.allowedParentRoots ?? []) {
|
|
roots.add(normalizePathForBoundary(root));
|
|
}
|
|
return [...roots];
|
|
}
|
|
|
|
function isWithinRoot(candidate: string, root: string): boolean {
|
|
const relative = path.relative(root, normalizePathForBoundary(candidate));
|
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
}
|
|
|
|
function resolveAliasTargetForParent(
|
|
request: string,
|
|
parent: NodeJS.Module | undefined,
|
|
): string | undefined {
|
|
const entries = pluginSdkNativeAliases.get(request);
|
|
const parentFilename = parent?.filename;
|
|
if (!entries || !parentFilename) {
|
|
return undefined;
|
|
}
|
|
return entries.find((entry) => isWithinRoot(parentFilename, entry.parentRoot))?.target;
|
|
}
|
|
|
|
function listPluginSdkNativeAliases(
|
|
options: InstallOpenClawPluginSdkNativeResolverOptions,
|
|
): Array<readonly [string, string]> {
|
|
const modulePath = options.pluginModulePath ?? resolveLoaderModulePath(options);
|
|
return Object.entries(
|
|
buildPluginLoaderAliasMap(
|
|
modulePath,
|
|
options.argv1 ?? process.argv[1],
|
|
options.moduleUrl,
|
|
// Native require hooks must point at JavaScript artifacts, even when the
|
|
// plugin loader itself is configured to prefer source imports.
|
|
"dist",
|
|
),
|
|
)
|
|
.filter(([specifier]) => isPluginSdkAliasSpecifier(specifier))
|
|
.filter(([, target]) => isNativeLoadableSdkTarget(target))
|
|
.flatMap(([specifier, target]) => {
|
|
if (specifier.endsWith(".js")) {
|
|
return [[specifier, target]] as Array<readonly [string, string]>;
|
|
}
|
|
return [
|
|
[specifier, target],
|
|
[`${specifier}.js`, target],
|
|
] as Array<readonly [string, string]>;
|
|
});
|
|
}
|
|
|
|
function installResolver(): void {
|
|
if (installed || !moduleWithResolver[nodeResolveFilenameProperty]) {
|
|
return;
|
|
}
|
|
previousResolveFilename = moduleWithResolver[nodeResolveFilenameProperty];
|
|
moduleWithResolver[nodeResolveFilenameProperty] = ((request, parent, isMain, options) => {
|
|
const aliasTarget = resolveAliasTargetForParent(request, parent);
|
|
if (aliasTarget) {
|
|
return aliasTarget;
|
|
}
|
|
return previousResolveFilename?.(request, parent, isMain, options) ?? request;
|
|
}) satisfies ResolveFilename;
|
|
installed = true;
|
|
}
|
|
|
|
function registerNativeAlias(params: {
|
|
request: string;
|
|
target: string;
|
|
parentRoots: readonly string[];
|
|
}): void {
|
|
const entries = pluginSdkNativeAliases.get(params.request) ?? [];
|
|
for (const parentRoot of params.parentRoots) {
|
|
if (
|
|
entries.some((entry) => entry.parentRoot === parentRoot && entry.target === params.target)
|
|
) {
|
|
continue;
|
|
}
|
|
entries.push({ parentRoot, target: params.target });
|
|
}
|
|
if (entries.length > 0) {
|
|
pluginSdkNativeAliases.set(params.request, entries);
|
|
}
|
|
}
|
|
|
|
export function installOpenClawPluginSdkNativeResolver(
|
|
options: InstallOpenClawPluginSdkNativeResolverOptions = {},
|
|
): string[] {
|
|
const parentRoots = resolveAllowedParentRoots(options);
|
|
for (const [specifier, target] of listPluginSdkNativeAliases(options)) {
|
|
registerNativeAlias({ request: specifier, target, parentRoots });
|
|
}
|
|
installResolver();
|
|
return [...pluginSdkNativeAliases.keys()].toSorted();
|
|
}
|
|
|
|
export function resetOpenClawPluginSdkNativeResolverForTest(): void {
|
|
pluginSdkNativeAliases.clear();
|
|
if (installed && previousResolveFilename) {
|
|
moduleWithResolver[nodeResolveFilenameProperty] = previousResolveFilename;
|
|
}
|
|
previousResolveFilename = undefined;
|
|
installed = false;
|
|
}
|