fix: preserve bundled facade fallback semantics

This commit is contained in:
Peter Steinberger
2026-04-28 00:50:28 +01:00
parent b90f29d313
commit da3cf1c1a8
7 changed files with 92 additions and 7 deletions

View File

@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugin SDK: fall back from partial bundled plugin directory overrides to package source public surfaces while preserving `OPENCLAW_DISABLE_BUNDLED_PLUGINS` as a hard disable. (#72817) Thanks @serkonyc.
- Agents/ACPX: stop forwarding Codex ACP timeout config controls that Codex rejects while preserving OpenClaw's run-timeout watchdog for ACP subagents. Fixes #73052. Thanks @pfrederiksen and @richa65.
- Memory/Ollama: add `memorySearch.remote.nonBatchConcurrency` for inline embedding indexing, default Ollama non-batch indexing to one request at a time, and keep batch concurrency separate from non-batch concurrency so local embedding backfills avoid timeout storms on smaller hosts. Carries forward #57733. Thanks @itilys.
- Docs/tools: clarify that `tools.profile: "messaging"` is intentionally narrow and that `tools.profile: "full"` is the unrestricted baseline for broader command/control access. Carries forward #39954. Thanks @posigit.

View File

@@ -17,6 +17,7 @@ import {
const { createTempDirSync } = createPluginSdkTestHarness();
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const originalDisableBundledPlugins = process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
const FACADE_LOADER_GLOBAL = "__openclawTestLoadBundledPluginPublicSurfaceModuleSync";
type FacadeLoaderJitiFactory = NonNullable<Parameters<typeof setFacadeLoaderJitiFactoryForTest>[0]>;
@@ -86,6 +87,11 @@ afterEach(() => {
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
}
if (originalDisableBundledPlugins === undefined) {
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
} else {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = originalDisableBundledPlugins;
}
});
describe("plugin-sdk facade loader", () => {
@@ -108,6 +114,31 @@ describe("plugin-sdk facade loader", () => {
expect(fromB.marker).toBe("override-b");
});
it("falls back to package source surfaces when an override dir lacks a bundled plugin", () => {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = createTempDirSync("openclaw-facade-loader-empty-");
const loaded = loadBundledPluginPublicSurfaceModuleSync<{
closeTrackedBrowserTabsForSessions: unknown;
}>({
dirName: "browser",
artifactBasename: "browser-maintenance.js",
});
expect(loaded.closeTrackedBrowserTabsForSessions).toEqual(expect.any(Function));
});
it("keeps bundled facade loads disabled when bundled plugins are disabled", () => {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
expect(() =>
loadBundledPluginPublicSurfaceModuleSync({
dirName: "browser",
artifactBasename: "browser-maintenance.js",
}),
).toThrow("Unable to resolve bundled plugin public surface browser/browser-maintenance.js");
});
it("shares loaded facade ids with facade-runtime", () => {
const dir = createBundledPluginDir("openclaw-facade-loader-ids-", "identity-check");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;

View File

@@ -59,7 +59,11 @@ function createFacadeResolutionKey(params: {
env?: NodeJS.ProcessEnv;
}): string {
const bundledPluginsDir = resolveBundledPluginsDir(params.env ?? process.env);
return createFacadeResolutionKeyShared({ ...params, bundledPluginsDir });
return createFacadeResolutionKeyShared({
...params,
bundledPluginsDir,
...(params.env ? { env: params.env } : {}),
});
}
function resolveFacadeModuleLocationUncached(params: {

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { areBundledPluginsDisabled } from "../plugins/bundled-dir.js";
import {
PUBLIC_SURFACE_SOURCE_EXTENSIONS,
normalizeBundledPluginArtifactSubpath,
@@ -22,10 +23,12 @@ export function createFacadeResolutionKey(params: {
dirName: string;
artifactBasename: string;
bundledPluginsDir?: string | null;
env?: NodeJS.ProcessEnv;
}): string {
const disabledKey = areBundledPluginsDisabled(params.env ?? process.env) ? "disabled" : "enabled";
return `${params.dirName}::${params.artifactBasename}::${
params.bundledPluginsDir ? path.resolve(params.bundledPluginsDir) : "<default>"
}`;
}::${disabledKey}`;
}
export function resolveCachedFacadeModuleLocation<TLocation>(params: {
@@ -64,6 +67,8 @@ export function resolveBundledFacadeModuleLocation(params: {
bundledPluginsDir?: string | null;
}): FacadeModuleLocationLike | null {
const preferSource = !params.currentModulePath.includes(`${path.sep}dist${path.sep}`);
const env = params.env ?? process.env;
const packageSourceRoot = path.resolve(params.packageRoot, "extensions");
const publicSurfaceParams = {
rootDir: params.packageRoot,
env: params.env,
@@ -75,8 +80,16 @@ export function resolveBundledFacadeModuleLocation(params: {
? (resolveBundledPluginSourcePublicSurfacePath({
dirName: params.dirName,
artifactBasename: params.artifactBasename,
sourceRoot: params.bundledPluginsDir ?? path.resolve(params.packageRoot, "extensions"),
}) ?? resolveBundledPluginPublicSurfacePath(publicSurfaceParams))
sourceRoot: params.bundledPluginsDir ?? packageSourceRoot,
}) ??
(params.bundledPluginsDir && !areBundledPluginsDisabled(env)
? resolveBundledPluginSourcePublicSurfacePath({
dirName: params.dirName,
artifactBasename: params.artifactBasename,
sourceRoot: packageSourceRoot,
})
: null) ??
resolveBundledPluginPublicSurfacePath(publicSurfaceParams))
: resolveBundledPluginPublicSurfacePath(publicSurfaceParams);
return modulePath
? {

View File

@@ -25,6 +25,7 @@ import {
const { createTempDirSync } = createPluginSdkTestHarness();
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const originalDisableBundledPlugins = process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
function createBundledPluginDir(prefix: string, marker: string): string {
@@ -48,6 +49,11 @@ afterEach(() => {
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
}
if (originalDisableBundledPlugins === undefined) {
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
} else {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = originalDisableBundledPlugins;
}
if (originalStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
@@ -81,6 +87,32 @@ describe("plugin-sdk facade runtime", () => {
});
});
it("falls back to package source surfaces when an override dir is partial", () => {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = createTempDirSync("openclaw-facade-runtime-empty-");
expect(
__testing.resolveFacadeModuleLocation({
dirName: "browser",
artifactBasename: "browser-maintenance.js",
}),
).toEqual({
modulePath: path.resolve("extensions/browser/browser-maintenance.ts"),
boundaryRoot: path.resolve("."),
});
});
it("does not fall back to package source surfaces when bundled plugins are disabled", () => {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
expect(
__testing.resolveFacadeModuleLocation({
dirName: "browser",
artifactBasename: "browser-maintenance.js",
}),
).toBeNull();
});
it("returns the same object identity on repeated calls (sentinel consistency)", () => {
const dir = createBundledPluginDir("openclaw-facade-identity-", "identity-check");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;

View File

@@ -59,7 +59,11 @@ function createFacadeResolutionKey(params: {
env?: NodeJS.ProcessEnv;
}): string {
const bundledPluginsDir = resolveBundledPluginsDir(params.env ?? process.env);
return createFacadeResolutionKeyShared({ ...params, bundledPluginsDir });
return createFacadeResolutionKeyShared({
...params,
bundledPluginsDir,
...(params.env ? { env: params.env } : {}),
});
}
function resolveRegistryPluginModuleLocation(params: {

View File

@@ -8,7 +8,7 @@ import { resolveUserPath } from "../utils.js";
const DISABLED_BUNDLED_PLUGINS_DIR = path.join(os.tmpdir(), "openclaw-empty-bundled-plugins");
function bundledPluginsDisabled(env: NodeJS.ProcessEnv): boolean {
export function areBundledPluginsDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
const raw = normalizeOptionalLowercaseString(env.OPENCLAW_DISABLE_BUNDLED_PLUGINS);
return raw === "1" || raw === "true";
}
@@ -109,7 +109,7 @@ function resolveBundledDirFromPackageRoot(
}
export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env): string | undefined {
if (bundledPluginsDisabled(env)) {
if (areBundledPluginsDisabled(env)) {
return resolveDisabledBundledPluginsDir();
}