mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 06:54:55 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user