fix(ollama): bypass managed proxy for loopback embeddings (#85707)

* 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>
This commit is contained in:
Kaspre
2026-05-23 16:17:55 -04:00
committed by GitHub
parent f6b332c735
commit fd2a9adbe6
26 changed files with 1447 additions and 103 deletions

View File

@@ -692,6 +692,20 @@ describe("plugin-sdk package contract guardrails", () => {
expect(collectPluginSdkPackageExports()).toEqual([...publicPluginSdkEntrypoints].toSorted());
});
it("keeps configured local-origin fetch helpers out of deprecated infra-runtime", () => {
const source = fs.readFileSync(resolve(REPO_ROOT, "src/plugin-sdk/infra-runtime.ts"), "utf8");
expect(source).not.toMatch(/export\s+\*\s+from\s+["']\.\.\/infra\/net\/fetch-guard\.js["']/);
expect(source).not.toContain("fetchConfiguredLocalOriginWithSsrFGuard");
expect(source).not.toContain("GuardedFetchConfiguredLocalOriginOptions");
});
it("keeps configured local-origin fetch helpers out of the public SSRF runtime", async () => {
const ssrfRuntime = await import("../../plugin-sdk/ssrf-runtime.js");
expect(ssrfRuntime).not.toHaveProperty("fetchConfiguredLocalOriginWithSsrFGuard");
});
it("keeps bundled plugin SDK compatibility subpaths explicitly classified", () => {
const entrypoints = new Set(pluginSdkEntrypoints);
const reserved = new Set<string>(reservedBundledPluginSdkEntrypoints);

View File

@@ -422,10 +422,17 @@ describe("plugin-sdk root alias", () => {
it("ignores unsafe private local-only plugin-sdk subpaths in the CJS root alias", () => {
const packageRoot = path.dirname(path.dirname(path.dirname(rootAliasPath)));
const qaLabPath = path.join(packageRoot, "src", "plugin-sdk", "qa-lab.ts");
const ssrfRuntimeInternalPath = path.join(
packageRoot,
"src",
"plugin-sdk",
"ssrf-runtime-internal.ts",
);
const lazyModule = loadRootAliasWithStubs({
env: { OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" },
privateLocalOnlySubpaths: ["qa-lab", "../escape", "nested/path"],
existingPaths: [path.join(packageRoot, "src", "plugin-sdk", "qa-lab.ts")],
privateLocalOnlySubpaths: ["qa-lab", "../escape", "nested/path", "ssrf-runtime-internal"],
existingPaths: [qaLabPath, ssrfRuntimeInternalPath],
monolithicExports: {
slowHelper: (): string => "loaded",
},
@@ -433,14 +440,12 @@ describe("plugin-sdk root alias", () => {
expect((lazyModule.moduleExports.slowHelper as () => string)()).toBe("loaded");
const aliasMap = (lazyModule.createJitiOptions.at(-1)?.alias ?? {}) as Record<string, string>;
expect(aliasMap["openclaw/plugin-sdk/qa-lab"]).toBe(
path.join(packageRoot, "src", "plugin-sdk", "qa-lab.ts"),
);
expect(aliasMap["@openclaw/plugin-sdk/qa-lab"]).toBe(
path.join(packageRoot, "src", "plugin-sdk", "qa-lab.ts"),
);
expect(aliasMap["openclaw/plugin-sdk/qa-lab"]).toBe(qaLabPath);
expect(aliasMap["@openclaw/plugin-sdk/qa-lab"]).toBe(qaLabPath);
expect(aliasMap).not.toHaveProperty("openclaw/plugin-sdk/../escape");
expect(aliasMap).not.toHaveProperty("openclaw/plugin-sdk/nested/path");
expect(aliasMap).not.toHaveProperty("openclaw/plugin-sdk/ssrf-runtime-internal");
expect(aliasMap).not.toHaveProperty("@openclaw/plugin-sdk/ssrf-runtime-internal");
});
it("keeps non-QA private local-only plugin-sdk subpaths out of the CJS root alias", () => {

View File

@@ -1118,6 +1118,106 @@ describe("loadOpenClawPlugins", () => {
expect(fs.readFileSync(path.join(aliasDir, "core.js"), "utf8")).toContain("core.js");
});
it("keeps private local-only plugin-sdk artifacts out of package dist aliases", () => {
const packageRoot = makeTempDir();
const distRoot = path.join(packageRoot, "dist");
const pluginSdkDir = path.join(distRoot, "plugin-sdk");
const aliasRoot = path.join(distRoot, "extensions", "node_modules", "openclaw");
const aliasDir = path.join(aliasRoot, "plugin-sdk");
mkdirSafe(pluginSdkDir);
mkdirSafe(aliasDir);
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify(
{
name: "openclaw",
version: "2026.4.22",
type: "module",
exports: {
"./plugin-sdk": "./dist/plugin-sdk/index.js",
"./plugin-sdk/string-coerce-runtime": "./dist/plugin-sdk/string-coerce-runtime.js",
},
},
null,
2,
),
"utf8",
);
fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const root = true;\n", "utf8");
fs.writeFileSync(
path.join(pluginSdkDir, "string-coerce-runtime.js"),
"export const publicRuntime = true;\n",
"utf8",
);
fs.writeFileSync(
path.join(pluginSdkDir, "ssrf-runtime-internal.js"),
"export const internal = true;\n",
"utf8",
);
fs.writeFileSync(
path.join(aliasDir, "ssrf-runtime-internal.js"),
"export const staleInternal = true;\n",
"utf8",
);
ensureOpenClawPluginSdkAlias(distRoot);
const aliasPackage = JSON.parse(
fs.readFileSync(path.join(aliasRoot, "package.json"), "utf8"),
) as { exports?: Record<string, string> };
expect(aliasPackage.exports).toEqual({
"./plugin-sdk": "./plugin-sdk/index.js",
"./plugin-sdk/string-coerce-runtime": "./plugin-sdk/string-coerce-runtime.js",
});
expect(fs.existsSync(path.join(aliasDir, "index.js"))).toBe(true);
expect(fs.existsSync(path.join(aliasDir, "string-coerce-runtime.js"))).toBe(true);
expect(fs.existsSync(path.join(aliasDir, "ssrf-runtime-internal.js"))).toBe(false);
});
it("keeps private local-only plugin-sdk artifacts out of legacy package dist aliases", () => {
const packageRoot = makeTempDir();
const distRoot = path.join(packageRoot, "dist");
const pluginSdkDir = path.join(distRoot, "plugin-sdk");
const aliasRoot = path.join(distRoot, "extensions", "node_modules", "openclaw");
const aliasDir = path.join(aliasRoot, "plugin-sdk");
mkdirSafe(pluginSdkDir);
mkdirSafe(aliasDir);
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "2026.4.22", type: "module" }, null, 2),
"utf8",
);
fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const root = true;\n", "utf8");
fs.writeFileSync(
path.join(pluginSdkDir, "string-coerce-runtime.js"),
"export const publicRuntime = true;\n",
"utf8",
);
fs.writeFileSync(
path.join(pluginSdkDir, "ssrf-runtime-internal.js"),
"export const internal = true;\n",
"utf8",
);
fs.writeFileSync(
path.join(aliasDir, "ssrf-runtime-internal.js"),
"export const staleInternal = true;\n",
"utf8",
);
ensureOpenClawPluginSdkAlias(distRoot);
const aliasPackage = JSON.parse(
fs.readFileSync(path.join(aliasRoot, "package.json"), "utf8"),
) as { exports?: Record<string, string> };
expect(aliasPackage.exports).toEqual({
"./plugin-sdk": "./plugin-sdk/index.js",
"./plugin-sdk/string-coerce-runtime": "./plugin-sdk/string-coerce-runtime.js",
});
expect(fs.existsSync(path.join(aliasDir, "index.js"))).toBe(true);
expect(fs.existsSync(path.join(aliasDir, "string-coerce-runtime.js"))).toBe(true);
expect(fs.existsSync(path.join(aliasDir, "ssrf-runtime-internal.js"))).toBe(false);
});
it("disables bundled plugins by default", () => {
const bundledDir = makeTempDir();
writePlugin({

View File

@@ -1,6 +1,136 @@
import fs from "node:fs";
import path from "node:path";
import { writeJsonSync } from "../infra/json-files.js";
import { tryReadJsonSync, writeJsonSync } from "../infra/json-files.js";
type OpenClawPackageJson = {
exports?: Record<string, unknown>;
};
const PRIVATE_LOCAL_ONLY_PLUGIN_SDK_DIST_FILE_NAME_FALLBACK = [
"codex-mcp-projection.js",
"codex-native-task-runtime.js",
"qa-channel.js",
"qa-channel-protocol.js",
"qa-lab.js",
"qa-runtime.js",
"ssrf-runtime-internal.js",
"test-utils.js",
] as const;
function isSafePluginSdkSubpathSegment(subpath: string): boolean {
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(subpath);
}
function collectLegacyPublicPluginSdkDistFileNames(distRoot: string): Set<string> | undefined {
const pluginSdkDir = path.join(distRoot, "plugin-sdk");
if (!fs.existsSync(pluginSdkDir)) {
return undefined;
}
const privateFileNames = readPrivateLocalOnlyPluginSdkDistFileNames(distRoot);
const fileNames = new Set<string>();
for (const entry of fs.readdirSync(pluginSdkDir, { withFileTypes: true })) {
if (!entry.isFile() || path.extname(entry.name) !== ".js") {
continue;
}
if (privateFileNames.has(entry.name)) {
continue;
}
fileNames.add(entry.name);
}
return fileNames.size > 0 ? fileNames : undefined;
}
function readPrivateLocalOnlyPluginSdkDistFileNames(distRoot: string): Set<string> {
const packageRoot = path.dirname(path.resolve(distRoot));
const privateFileNames = new Set<string>(PRIVATE_LOCAL_ONLY_PLUGIN_SDK_DIST_FILE_NAME_FALLBACK);
const subpaths = tryReadJsonSync(
path.join(packageRoot, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"),
);
if (!Array.isArray(subpaths)) {
return privateFileNames;
}
for (const subpath of subpaths) {
if (typeof subpath === "string" && isSafePluginSdkSubpathSegment(subpath)) {
privateFileNames.add(`${subpath}.js`);
}
}
return privateFileNames;
}
function readPublicPluginSdkDistFileNames(distRoot: string): Set<string> | undefined {
const packageRoot = path.dirname(path.resolve(distRoot));
const packageJson = tryReadJsonSync<OpenClawPackageJson>(path.join(packageRoot, "package.json"));
if (!packageJson || typeof packageJson !== "object" || Array.isArray(packageJson)) {
return collectLegacyPublicPluginSdkDistFileNames(distRoot);
}
const packageExports = packageJson.exports;
if (!packageExports || typeof packageExports !== "object" || Array.isArray(packageExports)) {
return collectLegacyPublicPluginSdkDistFileNames(distRoot);
}
const fileNames = new Set<string>();
for (const exportKey of Object.keys(packageExports)) {
if (exportKey === "./plugin-sdk") {
fileNames.add("index.js");
continue;
}
if (!exportKey.startsWith("./plugin-sdk/")) {
continue;
}
const subpath = exportKey.slice("./plugin-sdk/".length);
if (isSafePluginSdkSubpathSegment(subpath)) {
fileNames.add(`${subpath}.js`);
}
}
return fileNames.size > 0 ? fileNames : collectLegacyPublicPluginSdkDistFileNames(distRoot);
}
function buildRuntimePluginSdkPackageExports(
publicDistFileNames: ReadonlySet<string> | undefined,
): Record<string, string> {
if (!publicDistFileNames) {
return {
"./plugin-sdk": "./plugin-sdk/index.js",
};
}
const sortedFileNames = [...publicDistFileNames].toSorted((left, right) => {
if (left === "index.js") {
return -1;
}
if (right === "index.js") {
return 1;
}
return left.localeCompare(right);
});
return Object.fromEntries(
sortedFileNames.map((fileName) => {
const subpath = fileName.slice(0, -".js".length);
return [
subpath === "index" ? "./plugin-sdk" : `./plugin-sdk/${subpath}`,
`./plugin-sdk/${fileName}`,
];
}),
);
}
function removeStalePrivatePluginSdkAliasFiles(
pluginSdkAliasDir: string,
publicDistFileNames: ReadonlySet<string> | undefined,
): void {
if (!publicDistFileNames || !fs.existsSync(pluginSdkAliasDir)) {
return;
}
for (const entry of fs.readdirSync(pluginSdkAliasDir, { withFileTypes: true })) {
if (!entry.isFile() || path.extname(entry.name) !== ".js") {
continue;
}
if (!publicDistFileNames.has(entry.name)) {
fs.rmSync(path.join(pluginSdkAliasDir, entry.name), { force: true });
}
}
}
function writeRuntimeJsonFile(targetPath: string, value: unknown): void {
writeJsonSync(targetPath, value);
@@ -26,15 +156,13 @@ export function ensureOpenClawPluginSdkAlias(distRoot: string): void {
return;
}
const publicDistFileNames = readPublicPluginSdkDistFileNames(distRoot);
const aliasDir = path.join(distRoot, "extensions", "node_modules", "openclaw");
const pluginSdkAliasDir = path.join(aliasDir, "plugin-sdk");
writeRuntimeJsonFile(path.join(aliasDir, "package.json"), {
name: "openclaw",
type: "module",
exports: {
"./plugin-sdk": "./plugin-sdk/index.js",
"./plugin-sdk/*": "./plugin-sdk/*.js",
},
exports: buildRuntimePluginSdkPackageExports(publicDistFileNames),
});
try {
if (fs.existsSync(pluginSdkAliasDir) && !fs.lstatSync(pluginSdkAliasDir).isDirectory()) {
@@ -44,10 +172,14 @@ export function ensureOpenClawPluginSdkAlias(distRoot: string): void {
// Another process may be creating the alias at the same time.
}
fs.mkdirSync(pluginSdkAliasDir, { recursive: true });
removeStalePrivatePluginSdkAliasFiles(pluginSdkAliasDir, publicDistFileNames);
for (const entry of fs.readdirSync(pluginSdkDir, { withFileTypes: true })) {
if (!entry.isFile() || path.extname(entry.name) !== ".js") {
continue;
}
if (publicDistFileNames && !publicDistFileNames.has(entry.name)) {
continue;
}
writeRuntimeModuleWrapper(
path.join(pluginSdkDir, entry.name),
path.join(pluginSdkAliasDir, entry.name),

View File

@@ -156,4 +156,52 @@ describe("installOpenClawPluginSdkNativeResolver", () => {
const requireFromPlugin = createRequire(externalPluginEntry);
expect(() => requireFromPlugin.resolve("openclaw/plugin-sdk/source-only")).toThrow();
});
it("scopes private Ollama SDK aliases to bundled Ollama native parents", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-native-ollama-"));
const { loaderModulePath } = writeFakeOpenClawPackage(root);
const internalPath = path.join(root, "dist", "plugin-sdk", "ssrf-runtime-internal.js");
fs.writeFileSync(internalPath, "export const ssrfInternal = true;\n", "utf8");
const ollamaEntry = path.join(root, "dist", "extensions", "ollama", "index.js");
const runtimeOllamaEntry = path.join(root, "dist-runtime", "extensions", "ollama", "index.js");
const otherEntry = path.join(root, "dist", "extensions", "demo", "index.js");
fs.mkdirSync(path.dirname(ollamaEntry), { recursive: true });
fs.mkdirSync(path.dirname(runtimeOllamaEntry), { recursive: true });
fs.mkdirSync(path.dirname(otherEntry), { recursive: true });
fs.writeFileSync(ollamaEntry, "export default {};\n", "utf8");
fs.writeFileSync(runtimeOllamaEntry, "export default {};\n", "utf8");
fs.writeFileSync(otherEntry, "export default {};\n", "utf8");
const installedAliases = installOpenClawPluginSdkNativeResolver({
modulePath: loaderModulePath,
pluginModulePath: ollamaEntry,
pluginSdkResolution: "dist",
});
installOpenClawPluginSdkNativeResolver({
modulePath: loaderModulePath,
pluginModulePath: runtimeOllamaEntry,
pluginSdkResolution: "dist",
});
installOpenClawPluginSdkNativeResolver({
modulePath: loaderModulePath,
pluginModulePath: otherEntry,
pluginSdkResolution: "dist",
});
expect(installedAliases).toContain("openclaw/plugin-sdk/ssrf-runtime-internal");
const requireFromOllama = createRequire(ollamaEntry);
expect(
fs.realpathSync(requireFromOllama.resolve("openclaw/plugin-sdk/ssrf-runtime-internal")),
).toBe(fs.realpathSync(internalPath));
const requireFromRuntimeOllama = createRequire(runtimeOllamaEntry);
expect(
fs.realpathSync(
requireFromRuntimeOllama.resolve("openclaw/plugin-sdk/ssrf-runtime-internal"),
),
).toBe(fs.realpathSync(internalPath));
const requireFromOther = createRequire(otherEntry);
expect(() => requireFromOther.resolve("openclaw/plugin-sdk/ssrf-runtime-internal")).toThrow();
});
});

View File

@@ -15,6 +15,11 @@ type ModuleWithResolver = typeof Module & {
_resolveFilename?: ResolveFilename;
};
type NativeAliasEntry = {
parentRoot: string;
target: string;
};
export type InstallOpenClawPluginSdkNativeResolverOptions = {
modulePath?: string;
pluginModulePath?: string;
@@ -27,8 +32,7 @@ export type InstallOpenClawPluginSdkNativeResolverOptions = {
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, string>();
const allowedParentRoots = new Set<string>();
const pluginSdkNativeAliases = new Map<string, NativeAliasEntry[]>();
let installed = false;
let previousResolveFilename: ResolveFilename | undefined;
@@ -76,17 +80,69 @@ function findNearestPackageRoot(modulePath: string): string {
return path.dirname(path.resolve(modulePath));
}
function addAllowedParentRoot(root: string): void {
allowedParentRoots.add(normalizePathForBoundary(root));
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 registerAllowedParentRoots(options: InstallOpenClawPluginSdkNativeResolverOptions): void {
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) {
addAllowedParentRoot(findNearestPackageRoot(options.pluginModulePath));
roots.add(normalizePathForBoundary(resolveAllowedParentRoot(options.pluginModulePath)));
}
for (const root of options.allowedParentRoots ?? []) {
addAllowedParentRoot(root);
roots.add(normalizePathForBoundary(root));
}
return [...roots];
}
function isWithinRoot(candidate: string, root: string): boolean {
@@ -94,18 +150,22 @@ function isWithinRoot(candidate: string, root: string): boolean {
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function canResolveForParent(parent: NodeJS.Module | undefined): boolean {
function resolveAliasTargetForParent(
request: string,
parent: NodeJS.Module | undefined,
): string | undefined {
const entries = pluginSdkNativeAliases.get(request);
const parentFilename = parent?.filename;
if (!parentFilename || allowedParentRoots.size === 0) {
return false;
if (!entries || !parentFilename) {
return undefined;
}
return [...allowedParentRoots].some((root) => isWithinRoot(parentFilename, root));
return entries.find((entry) => isWithinRoot(parentFilename, entry.parentRoot))?.target;
}
function listPluginSdkNativeAliases(
options: InstallOpenClawPluginSdkNativeResolverOptions,
): Array<readonly [string, string]> {
const modulePath = resolveLoaderModulePath(options);
const modulePath = options.pluginModulePath ?? resolveLoaderModulePath(options);
return Object.entries(
buildPluginLoaderAliasMap(
modulePath,
@@ -135,8 +195,8 @@ function installResolver(): void {
}
previousResolveFilename = moduleWithResolver[nodeResolveFilenameProperty];
moduleWithResolver[nodeResolveFilenameProperty] = ((request, parent, isMain, options) => {
const aliasTarget = pluginSdkNativeAliases.get(request);
if (aliasTarget && canResolveForParent(parent)) {
const aliasTarget = resolveAliasTargetForParent(request, parent);
if (aliasTarget) {
return aliasTarget;
}
return previousResolveFilename?.(request, parent, isMain, options) ?? request;
@@ -144,20 +204,38 @@ function installResolver(): void {
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)) {
pluginSdkNativeAliases.set(specifier, target);
registerNativeAlias({ request: specifier, target, parentRoots });
}
registerAllowedParentRoots(options);
installResolver();
return [...pluginSdkNativeAliases.keys()].toSorted();
}
export function resetOpenClawPluginSdkNativeResolverForTest(): void {
pluginSdkNativeAliases.clear();
allowedParentRoots.clear();
if (installed && previousResolveFilename) {
moduleWithResolver[nodeResolveFilenameProperty] = previousResolveFilename;
}

View File

@@ -1071,6 +1071,161 @@ describe("plugin sdk alias helpers", () => {
expect(shadowCodexAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
});
it("aliases the Ollama SSRF internal helper only for the bundled Ollama plugin", async () => {
const fixture = createPluginSdkAliasFixture({
packageExports: {
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
},
});
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
const sourceSsrFInternalPath = path.join(
fixture.root,
"src",
"plugin-sdk",
"ssrf-runtime-internal.ts",
);
const distSsrFInternalPath = path.join(
fixture.root,
"dist",
"plugin-sdk",
"ssrf-runtime-internal.js",
);
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8");
fs.rmSync(path.join(fixture.root, "scripts"), { force: true, recursive: true });
fs.writeFileSync(sourceSsrFInternalPath, "export const ssrfInternal = true;\n", "utf-8");
fs.writeFileSync(distSsrFInternalPath, "export const ssrfInternal = true;\n", "utf-8");
const sourceOllamaEntry = writePluginEntry(
fixture.root,
bundledPluginFile("ollama", "index.ts"),
);
const sourceOtherPluginEntry = writePluginEntry(
fixture.root,
bundledPluginFile("demo", "index.ts"),
);
const entryBody = [
'import { ssrfInternal } from "openclaw/plugin-sdk/ssrf-runtime-internal";',
"export const loadedSsrFInternal = ssrfInternal;",
"",
].join("\n");
fs.writeFileSync(sourceOllamaEntry, entryBody, "utf-8");
fs.writeFileSync(sourceOtherPluginEntry, entryBody, "utf-8");
const distOllamaEntry = writePluginEntry(
fixture.root,
bundledDistPluginFile("ollama", "index.js"),
);
const distRuntimeOllamaEntry = writePluginEntry(
fixture.root,
path.join("dist-runtime", "extensions", "ollama", "index.js"),
);
fs.writeFileSync(distOllamaEntry, entryBody, "utf-8");
fs.writeFileSync(distRuntimeOllamaEntry, entryBody, "utf-8");
const { packageRoot: installedOllamaRoot, pluginEntry: installedOllamaEntry } =
writeInstalledPluginEntry({
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
packageName: "@openclaw/ollama",
});
const sourceSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
listPluginSdkExportedSubpaths({
modulePath: sourceOllamaEntry,
}),
);
const privateQaOtherSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" }, () =>
listPluginSdkExportedSubpaths({
modulePath: sourceOtherPluginEntry,
}),
);
const sourceAliases = withEnv(
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
() => buildPluginLoaderAliasMap(sourceOllamaEntry),
);
const distAliases = withEnv(
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
() => buildPluginLoaderAliasMap(distOllamaEntry, undefined, undefined, "dist"),
);
const distRuntimeAliases = withEnv(
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
() => buildPluginLoaderAliasMap(distRuntimeOllamaEntry, undefined, undefined, "dist"),
);
const otherAliases = withEnv(
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
() => buildPluginLoaderAliasMap(sourceOtherPluginEntry),
);
const privateQaOtherAliases = withEnv(
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1", NODE_ENV: undefined },
() => buildPluginLoaderAliasMap(sourceOtherPluginEntry),
);
const installedAliases = withCwd(installedOllamaRoot, () =>
withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () =>
buildPluginLoaderAliasMap(
installedOllamaEntry,
path.join(fixture.root, "openclaw.mjs"),
undefined,
"dist",
),
),
);
expect(sourceSubpaths).toEqual(["core", "ssrf-runtime-internal"]);
expect(privateQaOtherSubpaths).toEqual(["core"]);
expect(fs.realpathSync(sourceAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? "")).toBe(
fs.realpathSync(sourceSsrFInternalPath),
);
expect(fs.realpathSync(distAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? "")).toBe(
fs.realpathSync(distSsrFInternalPath),
);
expect(
fs.realpathSync(distRuntimeAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? ""),
).toBe(fs.realpathSync(distSsrFInternalPath));
expect(otherAliases["openclaw/plugin-sdk/ssrf-runtime-internal"]).toBeUndefined();
expect(privateQaOtherAliases["openclaw/plugin-sdk/ssrf-runtime-internal"]).toBeUndefined();
expect(installedAliases["openclaw/plugin-sdk/ssrf-runtime-internal"]).toBeUndefined();
const createJiti = await getCreateJiti();
const sourceLoaderBaseUrl = pathToFileURL(
path.join(fixture.root, "src", "plugins", "loader.ts"),
).href;
const ollamaLoader = createJiti(sourceLoaderBaseUrl, {
...buildPluginLoaderJitiOptions(sourceAliases),
tryNative: false,
});
const loadedOllama = ollamaLoader(sourceOllamaEntry) as { loadedSsrFInternal?: unknown };
expect(loadedOllama.loadedSsrFInternal).toBe(true);
const distLoader = createJiti(sourceLoaderBaseUrl, {
...buildPluginLoaderJitiOptions(distAliases),
tryNative: true,
});
const loadedDistOllama = distLoader(distOllamaEntry) as {
loadedSsrFInternal?: unknown;
};
expect(loadedDistOllama.loadedSsrFInternal).toBe(true);
const distRuntimeLoader = createJiti(sourceLoaderBaseUrl, {
...buildPluginLoaderJitiOptions(distRuntimeAliases),
tryNative: true,
});
const loadedDistRuntimeOllama = distRuntimeLoader(distRuntimeOllamaEntry) as {
loadedSsrFInternal?: unknown;
};
expect(loadedDistRuntimeOllama.loadedSsrFInternal).toBe(true);
const otherLoader = createJiti(sourceLoaderBaseUrl, {
...buildPluginLoaderJitiOptions(privateQaOtherAliases),
tryNative: false,
});
let otherLoadError: unknown;
try {
otherLoader(sourceOtherPluginEntry);
} catch (error) {
otherLoadError = error;
}
expect(otherLoadError).toBeInstanceOf(Error);
expect((otherLoadError as Error).message).toContain("ssrf-runtime-internal");
});
it("applies explicit dist resolution to plugin-sdk subpath aliases too", () => {
const { fixture, distRootAlias, distChannelRuntimePath } = createPluginSdkAliasTargetFixture();
const sourcePluginEntry = writePluginEntry(

View File

@@ -268,13 +268,31 @@ const cachedBundledPluginPublicSurfaceAliasMaps = new PluginLruCache<Record<stri
MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES,
);
const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const;
const OFFICIAL_CODEX_PLUGIN_PACKAGE_NAME = "@openclaw/codex";
const CODEX_NATIVE_TASK_RUNTIME_PLUGIN_SDK_SUBPATH = "codex-native-task-runtime";
const CODEX_MCP_PROJECTION_PLUGIN_SDK_SUBPATH = "codex-mcp-projection";
const BUNDLED_CODEX_PRIVATE_PLUGIN_SDK_SUBPATHS = new Set([
CODEX_NATIVE_TASK_RUNTIME_PLUGIN_SDK_SUBPATH,
CODEX_MCP_PROJECTION_PLUGIN_SDK_SUBPATH,
]);
const OLLAMA_CONFIGURED_LOCAL_ORIGIN_RUNTIME_PLUGIN_SDK_SUBPATH = "ssrf-runtime-internal";
type PrivatePluginSdkSubpathOwner = {
bundledPluginId: string;
officialInstalledPackageName?: string;
allowPrivateQaCli: boolean;
subpaths: readonly string[];
};
const PRIVATE_PLUGIN_SDK_SUBPATH_OWNERS: readonly PrivatePluginSdkSubpathOwner[] = [
{
bundledPluginId: "codex",
officialInstalledPackageName: "@openclaw/codex",
allowPrivateQaCli: true,
subpaths: [
CODEX_NATIVE_TASK_RUNTIME_PLUGIN_SDK_SUBPATH,
CODEX_MCP_PROJECTION_PLUGIN_SDK_SUBPATH,
],
},
{
bundledPluginId: "ollama",
allowPrivateQaCli: false,
subpaths: [OLLAMA_CONFIGURED_LOCAL_ORIGIN_RUNTIME_PLUGIN_SDK_SUBPATH],
},
];
const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [
".ts",
".mts",
@@ -322,6 +340,7 @@ function readPrivateLocalOnlyPluginSdkSubpaths(packageRoot: string): string[] {
...new Set([
CODEX_NATIVE_TASK_RUNTIME_PLUGIN_SDK_SUBPATH,
CODEX_MCP_PROJECTION_PLUGIN_SDK_SUBPATH,
OLLAMA_CONFIGURED_LOCAL_ORIGIN_RUNTIME_PLUGIN_SDK_SUBPATH,
...(Array.isArray(parsed)
? parsed.filter((subpath): subpath is string => isSafePluginSdkSubpathSegment(subpath))
: []),
@@ -475,12 +494,16 @@ function shouldIncludePrivateLocalOnlyPluginSdkSubpaths() {
return process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI === "1";
}
function isBundledCodexPluginModulePath(params: { packageRoot: string; modulePath: string }) {
function isBundledPluginModulePath(params: {
packageRoot: string;
modulePath: string;
pluginId: string;
}) {
const normalizedModulePath = path.resolve(params.modulePath);
const roots = [
path.join(params.packageRoot, "extensions", "codex"),
path.join(params.packageRoot, "dist", "extensions", "codex"),
path.join(params.packageRoot, "dist-runtime", "extensions", "codex"),
path.join(params.packageRoot, "extensions", params.pluginId),
path.join(params.packageRoot, "dist", "extensions", params.pluginId),
path.join(params.packageRoot, "dist-runtime", "extensions", params.pluginId),
];
return roots.some(
(root) =>
@@ -488,22 +511,32 @@ function isBundledCodexPluginModulePath(params: { packageRoot: string; modulePat
);
}
function isOfficialInstalledCodexPluginPackageRoot(packageRoot: string) {
const segments = path.resolve(packageRoot).split(path.sep).filter(Boolean);
function isOfficialInstalledPluginPackageRoot(params: {
packageRoot: string;
packageName: string;
}) {
const [scope, name] = params.packageName.split("/");
if (!scope || !name) {
return false;
}
const segments = path.resolve(params.packageRoot).split(path.sep).filter(Boolean);
const last = segments.at(-1);
const scope = segments.at(-2);
const packageScope = segments.at(-2);
const nodeModules = segments.at(-3);
return last === "codex" && scope === "@openclaw" && nodeModules === "node_modules";
return last === name && packageScope === scope && nodeModules === "node_modules";
}
function isOfficialInstalledCodexPluginModulePath(params: { modulePath: string }) {
function isOfficialInstalledPluginModulePath(params: { modulePath: string; packageName: string }) {
let cursor = path.dirname(path.resolve(params.modulePath));
for (let depth = 0; depth < 12; depth += 1) {
const packageJson = tryReadJsonSync<{ name?: unknown }>(path.join(cursor, "package.json"));
if (packageJson) {
return (
packageJson.name === OFFICIAL_CODEX_PLUGIN_PACKAGE_NAME &&
isOfficialInstalledCodexPluginPackageRoot(cursor)
packageJson.name === params.packageName &&
isOfficialInstalledPluginPackageRoot({
packageRoot: cursor,
packageName: params.packageName,
})
);
}
const parent = path.dirname(cursor);
@@ -515,11 +548,41 @@ function isOfficialInstalledCodexPluginModulePath(params: { modulePath: string }
return false;
}
function isTrustedCodexPluginModulePath(params: { packageRoot: string; modulePath: string }) {
return (
isBundledCodexPluginModulePath(params) ||
isOfficialInstalledCodexPluginModulePath({ modulePath: params.modulePath })
);
function isTrustedPrivatePluginSdkOwnerPath(params: {
packageRoot: string;
modulePath: string;
owner: PrivatePluginSdkSubpathOwner;
}) {
if (
isBundledPluginModulePath({
packageRoot: params.packageRoot,
modulePath: params.modulePath,
pluginId: params.owner.bundledPluginId,
})
) {
return true;
}
return params.owner.officialInstalledPackageName
? isOfficialInstalledPluginModulePath({
modulePath: params.modulePath,
packageName: params.owner.officialInstalledPackageName,
})
: false;
}
function findPrivatePluginSdkSubpathOwner(
subpath: string,
): PrivatePluginSdkSubpathOwner | undefined {
return PRIVATE_PLUGIN_SDK_SUBPATH_OWNERS.find((owner) => owner.subpaths.includes(subpath));
}
function listTrustedPrivatePluginSdkOwnerKeys(params: {
packageRoot: string;
modulePath: string;
}): string[] {
return PRIVATE_PLUGIN_SDK_SUBPATH_OWNERS.filter((owner) =>
isTrustedPrivatePluginSdkOwnerPath({ ...params, owner }),
).map((owner) => owner.bundledPluginId);
}
function shouldIncludePrivateLocalOnlyPluginSdkSubpath(params: {
@@ -527,13 +590,13 @@ function shouldIncludePrivateLocalOnlyPluginSdkSubpath(params: {
modulePath: string;
subpath: string;
}) {
const owner = findPrivatePluginSdkSubpathOwner(params.subpath);
if (!owner) {
return shouldIncludePrivateLocalOnlyPluginSdkSubpaths();
}
return (
shouldIncludePrivateLocalOnlyPluginSdkSubpaths() ||
(BUNDLED_CODEX_PRIVATE_PLUGIN_SDK_SUBPATHS.has(params.subpath) &&
isTrustedCodexPluginModulePath({
packageRoot: params.packageRoot,
modulePath: params.modulePath,
}))
isTrustedPrivatePluginSdkOwnerPath({ ...params, owner }) ||
(owner.allowPrivateQaCli && shouldIncludePrivateLocalOnlyPluginSdkSubpaths())
);
}
@@ -590,8 +653,8 @@ export function listPluginSdkExportedSubpaths(
if (!packageRoot) {
return [];
}
const includeCodexPrivateRuntime = isTrustedCodexPluginModulePath({ packageRoot, modulePath });
const cacheKey = `${packageRoot}::privateQa=${shouldIncludePrivateLocalOnlyPluginSdkSubpaths() ? "1" : "0"}::codexPrivate=${includeCodexPrivateRuntime ? "1" : "0"}`;
const trustedPrivateOwners = listTrustedPrivatePluginSdkOwnerKeys({ packageRoot, modulePath });
const cacheKey = `${packageRoot}::privateQa=${shouldIncludePrivateLocalOnlyPluginSdkSubpaths() ? "1" : "0"}::privateOwners=${trustedPrivateOwners.join(",")}`;
const cached = cachedPluginSdkExportedSubpaths.get(cacheKey);
if (cached) {
return cached;
@@ -628,8 +691,8 @@ export function resolvePluginSdkScopedAliasMap(
isProduction: process.env.NODE_ENV === "production",
pluginSdkResolution: params.pluginSdkResolution,
});
const includeCodexPrivateRuntime = isTrustedCodexPluginModulePath({ packageRoot, modulePath });
const cacheKey = `${packageRoot}::${orderedKinds.join(",")}::privateQa=${shouldIncludePrivateLocalOnlyPluginSdkSubpaths() ? "1" : "0"}::codexPrivate=${includeCodexPrivateRuntime ? "1" : "0"}`;
const trustedPrivateOwners = listTrustedPrivatePluginSdkOwnerKeys({ packageRoot, modulePath });
const cacheKey = `${packageRoot}::${orderedKinds.join(",")}::privateQa=${shouldIncludePrivateLocalOnlyPluginSdkSubpaths() ? "1" : "0"}::privateOwners=${trustedPrivateOwners.join(",")}`;
const cached = cachedPluginSdkScopedAliasMaps.get(cacheKey);
if (cached) {
return cached;

View File

@@ -106,6 +106,7 @@ describe("stageBundledPluginRuntime", () => {
"dist/plugin-sdk/index.js": "export const sdk = true;\n",
"dist/plugin-sdk/channel-entry-contract.js":
"export { contract } from '../channel-entry-contract-abc.js';\n",
"dist/plugin-sdk/ssrf-runtime-internal.js": "export const internal = true;\n",
"dist/channel-entry-contract-abc.js": "export const contract = true;\n",
[bundledDistPluginFile("diffs", "index.js")]: "export default {}\n",
[bundledDistPluginFile("diffs", "node_modules/@pierre/diffs/index.js")]:
@@ -135,17 +136,22 @@ describe("stageBundledPluginRuntime", () => {
.isSymbolicLink(),
).toBe(false);
expect(
fs.readFileSync(
path.join(repoRoot, "dist", "extensions", "node_modules", "openclaw", "package.json"),
"utf8",
),
).toContain('"./plugin-sdk": "./plugin-sdk/index.js"');
JSON.parse(
fs.readFileSync(
path.join(repoRoot, "dist", "extensions", "node_modules", "openclaw", "package.json"),
"utf8",
),
).exports,
).toMatchObject({
"./plugin-sdk": "./plugin-sdk/index.js",
"./plugin-sdk/channel-entry-contract": "./plugin-sdk/channel-entry-contract.js",
});
expect(
fs.readFileSync(
path.join(repoRoot, "dist", "extensions", "node_modules", "openclaw", "package.json"),
"utf8",
),
).toContain('"./plugin-sdk/*": "./plugin-sdk/*.js"');
).not.toContain('"./plugin-sdk/*"');
expect(
fs.readFileSync(
path.join(
@@ -160,9 +166,65 @@ describe("stageBundledPluginRuntime", () => {
"utf8",
),
).toContain("../../../../plugin-sdk/channel-entry-contract.js");
expect(
fs.existsSync(
path.join(
repoRoot,
"dist",
"extensions",
"node_modules",
"openclaw",
"plugin-sdk",
"ssrf-runtime-internal.js",
),
),
).toBe(false);
expect(fs.existsSync(path.join(runtimePluginDir, "node_modules", "openclaw"))).toBe(false);
});
it("stages only public plugin-sdk package exports for bundled runtime aliases", () => {
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-sdk-public-");
createDistPluginDir(repoRoot, "ollama");
setupRepoFiles(repoRoot, {
"package.json": JSON.stringify(
{
name: "openclaw",
type: "module",
exports: {
"./plugin-sdk": "./dist/plugin-sdk/index.js",
"./plugin-sdk/channel-entry-contract": "./dist/plugin-sdk/channel-entry-contract.js",
},
},
null,
2,
),
"dist/plugin-sdk/index.js": "export const sdk = true;\n",
"dist/plugin-sdk/channel-entry-contract.js": "export const contract = true;\n",
"dist/plugin-sdk/source-only.js": "export const sourceOnly = true;\n",
"dist/plugin-sdk/ssrf-runtime-internal.js": "export const internal = true;\n",
[bundledDistPluginFile("ollama", "index.js")]: "export default {}\n",
});
stageBundledPluginRuntime({ repoRoot });
const aliasRoot = path.join(repoRoot, "dist", "extensions", "node_modules", "openclaw");
const packageJson = JSON.parse(
fs.readFileSync(path.join(aliasRoot, "package.json"), "utf8"),
) as { exports: Record<string, string> };
expect(packageJson.exports).toEqual({
"./plugin-sdk": "./plugin-sdk/index.js",
"./plugin-sdk/channel-entry-contract": "./plugin-sdk/channel-entry-contract.js",
});
expect(fs.existsSync(path.join(aliasRoot, "plugin-sdk", "index.js"))).toBe(true);
expect(fs.existsSync(path.join(aliasRoot, "plugin-sdk", "channel-entry-contract.js"))).toBe(
true,
);
expect(fs.existsSync(path.join(aliasRoot, "plugin-sdk", "source-only.js"))).toBe(false);
expect(fs.existsSync(path.join(aliasRoot, "plugin-sdk", "ssrf-runtime-internal.js"))).toBe(
false,
);
});
it("keeps extension-local plugin-sdk wrappers resolving canonical dist chunks", async () => {
const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-sdk-wrapper-");
createDistPluginDir(repoRoot, "diffs");