mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
refactor: unify plugin control-plane cache context
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const pluginRegistryMocks = vi.hoisted(() => {
|
||||
const loadManifestRegistry = vi.fn();
|
||||
@@ -20,9 +20,20 @@ vi.mock("../plugins/plugin-registry.js", () => ({
|
||||
loadPluginRegistrySnapshot: pluginRegistryMocks.loadPluginRegistrySnapshot,
|
||||
}));
|
||||
|
||||
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
import {
|
||||
resetProviderAuthAliasMapCacheForTest,
|
||||
resolveProviderIdForAuth,
|
||||
} from "./provider-auth-aliases.js";
|
||||
|
||||
describe("provider auth aliases", () => {
|
||||
beforeEach(() => {
|
||||
resetProviderAuthAliasMapCacheForTest();
|
||||
pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockReset();
|
||||
pluginRegistryMocks.loadPluginManifestRegistryForPluginRegistry.mockReset();
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshot.mockReset();
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] });
|
||||
});
|
||||
|
||||
it("treats deprecated auth choice ids as provider auth aliases", () => {
|
||||
pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({
|
||||
plugins: [
|
||||
@@ -46,4 +57,39 @@ describe("provider auth aliases", () => {
|
||||
expect(resolveProviderIdForAuth("openai-codex-import")).toBe("openai-codex");
|
||||
expect(resolveProviderIdForAuth("openai-codex")).toBe("openai-codex");
|
||||
});
|
||||
|
||||
it("does not reuse aliases across env-resolved plugin roots", () => {
|
||||
const env = {
|
||||
HOME: "/home/one",
|
||||
OPENCLAW_HOME: undefined,
|
||||
} as NodeJS.ProcessEnv;
|
||||
pluginRegistryMocks.loadPluginManifestRegistryForPluginRegistry
|
||||
.mockReturnValueOnce({
|
||||
plugins: [
|
||||
{
|
||||
id: "one",
|
||||
origin: "global",
|
||||
providerAuthAliases: { fixture: "provider-one" },
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
plugins: [
|
||||
{
|
||||
id: "two",
|
||||
origin: "global",
|
||||
providerAuthAliases: { fixture: "provider-two" },
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
expect(resolveProviderIdForAuth("fixture", { config: {}, env })).toBe("provider-one");
|
||||
env.HOME = "/home/two";
|
||||
expect(resolveProviderIdForAuth("fixture", { config: {}, env })).toBe("provider-two");
|
||||
expect(pluginRegistryMocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isWorkspacePluginAllowedByConfig,
|
||||
normalizePluginConfigId,
|
||||
} from "../plugins/plugin-config-trust.js";
|
||||
import { resolvePluginControlPlaneFingerprint } from "../plugins/plugin-control-plane-context.js";
|
||||
import type { PluginOrigin } from "../plugins/plugin-origin.types.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
@@ -31,9 +32,16 @@ let providerAuthAliasMapCache = new WeakMap<
|
||||
Map<string, Record<string, string>>
|
||||
>();
|
||||
|
||||
function buildProviderAuthAliasMapCacheKey(params?: ProviderAuthAliasLookupParams): string {
|
||||
function buildProviderAuthAliasMapCacheKey(
|
||||
params: ProviderAuthAliasLookupParams | undefined,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): string {
|
||||
return JSON.stringify({
|
||||
workspaceDir: params?.workspaceDir ?? "",
|
||||
pluginControlPlane: resolvePluginControlPlaneFingerprint({
|
||||
config: params?.config,
|
||||
env,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
}),
|
||||
includeUntrustedWorkspacePlugins: params?.includeUntrustedWorkspacePlugins === true,
|
||||
plugins: params?.config?.plugins ?? null,
|
||||
});
|
||||
@@ -100,7 +108,7 @@ export function resolveProviderAuthAliasMap(
|
||||
params?: ProviderAuthAliasLookupParams,
|
||||
): Record<string, string> {
|
||||
const env = params?.env ?? process.env;
|
||||
const cacheKey = buildProviderAuthAliasMapCacheKey(params);
|
||||
const cacheKey = buildProviderAuthAliasMapCacheKey(params, env);
|
||||
let envCache = providerAuthAliasMapCache.get(env);
|
||||
if (!envCache) {
|
||||
envCache = new Map<string, Record<string, string>>();
|
||||
|
||||
@@ -117,6 +117,23 @@ describe("current plugin metadata snapshot", () => {
|
||||
expect(getCurrentPluginMetadataSnapshot({ config, env: requestedEnv })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects a current snapshot when env-resolved plugin roots change", () => {
|
||||
const config = {};
|
||||
const snapshot = createSnapshot({ config });
|
||||
const snapshotEnv = {
|
||||
HOME: "/home/snapshot",
|
||||
OPENCLAW_HOME: undefined,
|
||||
} as NodeJS.ProcessEnv;
|
||||
const requestedEnv = {
|
||||
HOME: "/home/requested",
|
||||
OPENCLAW_HOME: undefined,
|
||||
} as NodeJS.ProcessEnv;
|
||||
setCurrentPluginMetadataSnapshot(snapshot, { config, env: snapshotEnv });
|
||||
|
||||
expect(getCurrentPluginMetadataSnapshot({ config, env: snapshotEnv })).toBe(snapshot);
|
||||
expect(getCurrentPluginMetadataSnapshot({ config, env: requestedEnv })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps source-policy compatibility when storing an auto-enabled runtime config", () => {
|
||||
const sourceConfig = { channels: { telegram: { botToken: "token" } } };
|
||||
const autoEnabledConfig = {
|
||||
|
||||
@@ -13,14 +13,16 @@ export { resolvePluginMetadataSnapshotConfigFingerprint } from "./plugin-metadat
|
||||
// never accumulate historical metadata snapshots here.
|
||||
export function setCurrentPluginMetadataSnapshot(
|
||||
snapshot: PluginMetadataSnapshot | undefined,
|
||||
options: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv } = {},
|
||||
options: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv; workspaceDir?: string } = {},
|
||||
): void {
|
||||
setCurrentPluginMetadataSnapshotState(
|
||||
snapshot,
|
||||
snapshot
|
||||
? resolvePluginMetadataSnapshotConfigFingerprint(options.config, {
|
||||
env: options.env,
|
||||
index: snapshot.index,
|
||||
policyHash: snapshot.policyHash,
|
||||
workspaceDir: options.workspaceDir ?? snapshot.workspaceDir,
|
||||
})
|
||||
: undefined,
|
||||
);
|
||||
@@ -53,6 +55,9 @@ export function getCurrentPluginMetadataSnapshot(
|
||||
params.config,
|
||||
{
|
||||
env: params.env,
|
||||
index: snapshot.index,
|
||||
policyHash: snapshot.policyHash,
|
||||
workspaceDir: params.workspaceDir,
|
||||
},
|
||||
);
|
||||
if (configFingerprint && configFingerprint !== requestedConfigFingerprint) {
|
||||
|
||||
@@ -102,6 +102,10 @@ import {
|
||||
} from "./memory-state.js";
|
||||
import { unwrapDefaultModuleExport } from "./module-export.js";
|
||||
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
|
||||
import {
|
||||
fingerprintPluginDiscoveryContext,
|
||||
resolvePluginDiscoveryContext,
|
||||
} from "./plugin-control-plane-context.js";
|
||||
import { withProfile } from "./plugin-load-profile.js";
|
||||
import {
|
||||
getCachedPluginSourceModuleLoader,
|
||||
@@ -116,7 +120,6 @@ import {
|
||||
import { ensureOpenClawPluginSdkAlias } from "./plugin-sdk-dist-alias.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
import {
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryKey,
|
||||
@@ -616,11 +619,12 @@ function buildCacheKey(params: {
|
||||
coreGatewayMethodNames?: string[];
|
||||
activate?: boolean;
|
||||
}): string {
|
||||
const { roots, loadPaths } = resolvePluginCacheInputs({
|
||||
const discoveryContext = resolvePluginDiscoveryContext({
|
||||
workspaceDir: params.workspaceDir,
|
||||
loadPaths: params.plugins.loadPaths,
|
||||
env: params.env,
|
||||
});
|
||||
const { roots, loadPaths } = discoveryContext;
|
||||
const bundledPackage = resolveBundledPackageCacheIdentity(roots.stock);
|
||||
const installs = Object.fromEntries(
|
||||
Object.entries(params.installs ?? {}).map(([pluginId, install]) => [
|
||||
@@ -655,6 +659,7 @@ function buildCacheKey(params: {
|
||||
const activationMode = params.activate === false ? "snapshot" : "active";
|
||||
return `${roots.workspace ?? ""}::${roots.global ?? ""}::${roots.stock ?? ""}::${JSON.stringify({
|
||||
bundledPackage,
|
||||
discoveryFingerprint: fingerprintPluginDiscoveryContext(discoveryContext),
|
||||
...params.plugins,
|
||||
installs,
|
||||
loadPaths,
|
||||
|
||||
110
src/plugins/plugin-control-plane-context.test.ts
Normal file
110
src/plugins/plugin-control-plane-context.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import {
|
||||
resolvePluginControlPlaneContext,
|
||||
resolvePluginControlPlaneFingerprint,
|
||||
resolvePluginDiscoveryContext,
|
||||
resolvePluginDiscoveryFingerprint,
|
||||
} from "./plugin-control-plane-context.js";
|
||||
|
||||
function createIndex(pluginId: string): InstalledPluginIndex {
|
||||
return {
|
||||
version: 1,
|
||||
hostContractVersion: "test",
|
||||
compatRegistryVersion: "test",
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy",
|
||||
generatedAtMs: 1,
|
||||
installRecords: {},
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
pluginId,
|
||||
manifestPath: `/plugins/${pluginId}/openclaw.plugin.json`,
|
||||
manifestHash: `${pluginId}-manifest-hash`,
|
||||
rootDir: `/plugins/${pluginId}`,
|
||||
origin: "global",
|
||||
enabled: true,
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
||||
agentHarnesses: [],
|
||||
},
|
||||
compat: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("plugin control-plane context", () => {
|
||||
it("resolves env-sensitive discovery roots and load paths before fingerprinting", () => {
|
||||
const config = { plugins: { load: { paths: ["~/plugins", "/opt/shared"] } } };
|
||||
const envA = { HOME: "/home/a", OPENCLAW_HOME: "/openclaw/a" } as NodeJS.ProcessEnv;
|
||||
const envB = { HOME: "/home/b", OPENCLAW_HOME: "/openclaw/b" } as NodeJS.ProcessEnv;
|
||||
|
||||
const contextA = resolvePluginDiscoveryContext({ config, env: envA });
|
||||
const contextB = resolvePluginDiscoveryContext({ config, env: envB });
|
||||
|
||||
expect(contextA.loadPaths).toEqual(["/openclaw/a/plugins", "/opt/shared"]);
|
||||
expect(contextB.loadPaths).toEqual(["/openclaw/b/plugins", "/opt/shared"]);
|
||||
expect(resolvePluginDiscoveryFingerprint({ config, env: envA })).not.toBe(
|
||||
resolvePluginDiscoveryFingerprint({ config, env: envB }),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes policy, inventory, and activation in one control-plane fingerprint", () => {
|
||||
const config = { plugins: { allow: ["demo"] } };
|
||||
const base = resolvePluginControlPlaneFingerprint({
|
||||
config,
|
||||
env: { HOME: "/home/a", OPENCLAW_HOME: "/openclaw/a" } as NodeJS.ProcessEnv,
|
||||
index: createIndex("demo"),
|
||||
activationFingerprint: "activation-a",
|
||||
});
|
||||
|
||||
expect(
|
||||
resolvePluginControlPlaneFingerprint({
|
||||
config,
|
||||
env: { HOME: "/home/a", OPENCLAW_HOME: "/openclaw/a" } as NodeJS.ProcessEnv,
|
||||
index: createIndex("other"),
|
||||
activationFingerprint: "activation-a",
|
||||
}),
|
||||
).not.toBe(base);
|
||||
expect(
|
||||
resolvePluginControlPlaneFingerprint({
|
||||
config,
|
||||
env: { HOME: "/home/a", OPENCLAW_HOME: "/openclaw/a" } as NodeJS.ProcessEnv,
|
||||
index: createIndex("demo"),
|
||||
activationFingerprint: "activation-b",
|
||||
}),
|
||||
).not.toBe(base);
|
||||
expect(
|
||||
resolvePluginControlPlaneFingerprint({
|
||||
config: { plugins: { deny: ["demo"] } },
|
||||
env: { HOME: "/home/a", OPENCLAW_HOME: "/openclaw/a" } as NodeJS.ProcessEnv,
|
||||
index: createIndex("demo"),
|
||||
activationFingerprint: "activation-a",
|
||||
}),
|
||||
).not.toBe(base);
|
||||
});
|
||||
|
||||
it("keeps the canonical context inspectable for cache diagnostics", () => {
|
||||
const context = resolvePluginControlPlaneContext({
|
||||
config: { plugins: { load: { paths: ["/opt/plugins"] } } },
|
||||
env: { HOME: "/home/a", OPENCLAW_HOME: "/openclaw/a" } as NodeJS.ProcessEnv,
|
||||
inventoryFingerprint: "inventory",
|
||||
policyHash: "policy",
|
||||
});
|
||||
|
||||
expect(context).toMatchObject({
|
||||
discovery: {
|
||||
loadPaths: ["/opt/plugins"],
|
||||
roots: {
|
||||
global: "/openclaw/a/.openclaw/extensions",
|
||||
},
|
||||
},
|
||||
inventoryFingerprint: "inventory",
|
||||
policyFingerprint: "policy",
|
||||
});
|
||||
});
|
||||
});
|
||||
85
src/plugins/plugin-control-plane-context.ts
Normal file
85
src/plugins/plugin-control-plane-context.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { hashJson } from "./installed-plugin-index-hash.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { resolveInstalledManifestRegistryIndexFingerprint } from "./manifest-registry-installed.js";
|
||||
import { resolvePluginCacheInputs, type PluginSourceRoots } from "./roots.js";
|
||||
|
||||
export type PluginDiscoveryContext = {
|
||||
roots: PluginSourceRoots;
|
||||
loadPaths: readonly string[];
|
||||
};
|
||||
|
||||
export type PluginControlPlaneContext = {
|
||||
discovery: PluginDiscoveryContext;
|
||||
policyFingerprint: string;
|
||||
inventoryFingerprint?: string;
|
||||
activationFingerprint?: string;
|
||||
};
|
||||
|
||||
export type ResolvePluginDiscoveryContextParams = {
|
||||
config?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
workspaceDir?: string;
|
||||
loadPaths?: readonly string[];
|
||||
};
|
||||
|
||||
export type ResolvePluginControlPlaneContextParams = ResolvePluginDiscoveryContextParams & {
|
||||
activationFingerprint?: string;
|
||||
index?: InstalledPluginIndex;
|
||||
inventoryFingerprint?: string;
|
||||
policyHash?: string;
|
||||
};
|
||||
|
||||
function resolveConfiguredPluginLoadPaths(
|
||||
config: OpenClawConfig | undefined,
|
||||
): readonly string[] | undefined {
|
||||
const paths = config?.plugins?.load?.paths;
|
||||
return Array.isArray(paths) ? paths : undefined;
|
||||
}
|
||||
|
||||
export function resolvePluginDiscoveryContext(
|
||||
params: ResolvePluginDiscoveryContextParams = {},
|
||||
): PluginDiscoveryContext {
|
||||
return resolvePluginCacheInputs({
|
||||
env: params.env ?? process.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
loadPaths: [...(params.loadPaths ?? resolveConfiguredPluginLoadPaths(params.config) ?? [])],
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginDiscoveryFingerprint(
|
||||
params: ResolvePluginDiscoveryContextParams = {},
|
||||
): string {
|
||||
return fingerprintPluginDiscoveryContext(resolvePluginDiscoveryContext(params));
|
||||
}
|
||||
|
||||
export function fingerprintPluginDiscoveryContext(context: PluginDiscoveryContext): string {
|
||||
return hashJson(context);
|
||||
}
|
||||
|
||||
export function resolvePluginControlPlaneContext(
|
||||
params: ResolvePluginControlPlaneContextParams = {},
|
||||
): PluginControlPlaneContext {
|
||||
const inventoryFingerprint =
|
||||
params.inventoryFingerprint ??
|
||||
(params.index ? resolveInstalledManifestRegistryIndexFingerprint(params.index) : undefined);
|
||||
return {
|
||||
discovery: resolvePluginDiscoveryContext(params),
|
||||
policyFingerprint: params.policyHash ?? resolveInstalledPluginIndexPolicyHash(params.config),
|
||||
...(inventoryFingerprint ? { inventoryFingerprint } : {}),
|
||||
...(params.activationFingerprint
|
||||
? { activationFingerprint: params.activationFingerprint }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePluginControlPlaneFingerprint(
|
||||
params: ResolvePluginControlPlaneContextParams = {},
|
||||
): string {
|
||||
return fingerprintPluginControlPlaneContext(resolvePluginControlPlaneContext(params));
|
||||
}
|
||||
|
||||
export function fingerprintPluginControlPlaneContext(context: PluginControlPlaneContext): string {
|
||||
return hashJson(context);
|
||||
}
|
||||
@@ -409,6 +409,57 @@ describe("loadPluginLookUpTable", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rebuilds when a provided metadata snapshot has stale env-resolved plugin roots", async () => {
|
||||
const plugins = [
|
||||
createManifestRecord({
|
||||
id: "telegram",
|
||||
origin: "bundled",
|
||||
channels: ["telegram"],
|
||||
}),
|
||||
];
|
||||
const config = {} as OpenClawConfig;
|
||||
const snapshotEnv = {
|
||||
HOME: "/home/snapshot",
|
||||
OPENCLAW_HOME: undefined,
|
||||
} as NodeJS.ProcessEnv;
|
||||
const requestedEnv = {
|
||||
HOME: "/home/requested",
|
||||
OPENCLAW_HOME: undefined,
|
||||
} as NodeJS.ProcessEnv;
|
||||
const policyHash = resolveInstalledPluginIndexPolicyHash(config);
|
||||
const index = createIndex(plugins, { policyHash });
|
||||
const manifestRegistry: PluginManifestRegistry = {
|
||||
plugins,
|
||||
diagnostics: [],
|
||||
};
|
||||
loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry);
|
||||
const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js");
|
||||
const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js");
|
||||
|
||||
const metadataSnapshot = loadPluginMetadataSnapshot({
|
||||
config,
|
||||
env: snapshotEnv,
|
||||
index,
|
||||
});
|
||||
loadPluginManifestRegistryForInstalledIndex.mockClear();
|
||||
|
||||
loadPluginLookUpTable({
|
||||
config,
|
||||
env: requestedEnv,
|
||||
index,
|
||||
metadataSnapshot,
|
||||
});
|
||||
|
||||
expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce();
|
||||
expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
index,
|
||||
config,
|
||||
env: requestedEnv,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rebuilds when a provided metadata snapshot has stale plugin inventory", async () => {
|
||||
const snapshotPlugins = [
|
||||
createManifestRecord({
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveHomeRelativePath } from "../infra/home-dir.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js";
|
||||
|
||||
function normalizeResolvedLoadPaths(
|
||||
config: OpenClawConfig | undefined,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): readonly string[] {
|
||||
const paths = config?.plugins?.load?.paths;
|
||||
if (!Array.isArray(paths)) {
|
||||
return [];
|
||||
}
|
||||
return paths.flatMap((entry) => {
|
||||
if (typeof entry !== "string") {
|
||||
return [];
|
||||
}
|
||||
const trimmed = entry.trim();
|
||||
return trimmed ? [resolveHomeRelativePath(trimmed, { env })] : [];
|
||||
});
|
||||
}
|
||||
export {
|
||||
fingerprintPluginControlPlaneContext,
|
||||
fingerprintPluginDiscoveryContext,
|
||||
resolvePluginControlPlaneContext,
|
||||
resolvePluginControlPlaneFingerprint,
|
||||
resolvePluginDiscoveryContext,
|
||||
resolvePluginDiscoveryFingerprint,
|
||||
} from "./plugin-control-plane-context.js";
|
||||
|
||||
export function resolvePluginMetadataSnapshotConfigFingerprint(
|
||||
config: OpenClawConfig | undefined,
|
||||
options: { env?: NodeJS.ProcessEnv; policyHash?: string } = {},
|
||||
options: {
|
||||
activationFingerprint?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
index?: InstalledPluginIndex;
|
||||
inventoryFingerprint?: string;
|
||||
policyHash?: string;
|
||||
workspaceDir?: string;
|
||||
} = {},
|
||||
): string {
|
||||
const env = options.env ?? process.env;
|
||||
return JSON.stringify({
|
||||
policyHash: options.policyHash ?? resolveInstalledPluginIndexPolicyHash(config),
|
||||
pluginLoadPaths: normalizeResolvedLoadPaths(config, env),
|
||||
return resolvePluginControlPlaneFingerprint({
|
||||
config,
|
||||
activationFingerprint: options.activationFingerprint,
|
||||
env: options.env,
|
||||
index: options.index,
|
||||
inventoryFingerprint: options.inventoryFingerprint,
|
||||
policyHash: options.policyHash,
|
||||
workspaceDir: options.workspaceDir,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +53,9 @@ export function isPluginMetadataSnapshotCompatible(params: {
|
||||
params.snapshot.configFingerprint ===
|
||||
resolvePluginMetadataSnapshotConfigFingerprint(params.config, {
|
||||
env,
|
||||
index: params.index ?? params.snapshot.index,
|
||||
policyHash: params.snapshot.policyHash,
|
||||
workspaceDir: params.workspaceDir,
|
||||
})) &&
|
||||
(params.snapshot.workspaceDir ?? "") === (params.workspaceDir ?? "") &&
|
||||
indexesMatch(params.snapshot.index, params.index)
|
||||
@@ -185,7 +187,9 @@ function loadPluginMetadataSnapshotImpl(
|
||||
policyHash: index.policyHash,
|
||||
configFingerprint: resolvePluginMetadataSnapshotConfigFingerprint(params.config, {
|
||||
env: params.env,
|
||||
index,
|
||||
policyHash: index.policyHash,
|
||||
workspaceDir: params.workspaceDir,
|
||||
}),
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
index,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js";
|
||||
import { resolveProviderConfigApiOwnerHint } from "./provider-config-owner.js";
|
||||
import { isPluginProvidersLoadInFlight, resolvePluginProviders } from "./providers.runtime.js";
|
||||
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
|
||||
@@ -45,6 +46,11 @@ function matchesProviderId(provider: ProviderPlugin, providerId: string): boolea
|
||||
function resolveProviderRuntimePluginCacheKey(params: ProviderRuntimePluginLookupParams): string {
|
||||
return JSON.stringify({
|
||||
provider: normalizeLowercaseStringOrEmpty(params.provider),
|
||||
pluginControlPlane: resolvePluginControlPlaneFingerprint({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
}),
|
||||
plugins: params.config?.plugins,
|
||||
models: params.config?.models?.providers,
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
|
||||
@@ -437,6 +437,49 @@ describe("provider-runtime", () => {
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not reuse runtime provider cache entries across env-resolved plugin roots", () => {
|
||||
const firstProvider: ProviderPlugin = {
|
||||
id: DEMO_PROVIDER_ID,
|
||||
label: "Demo one",
|
||||
auth: [],
|
||||
};
|
||||
const secondProvider: ProviderPlugin = {
|
||||
id: DEMO_PROVIDER_ID,
|
||||
label: "Demo two",
|
||||
auth: [],
|
||||
};
|
||||
const config = {} as OpenClawConfig;
|
||||
const originalHome = process.env.HOME;
|
||||
const originalOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
try {
|
||||
process.env.HOME = "/home/one";
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
resolvePluginProvidersMock.mockReturnValueOnce([firstProvider]);
|
||||
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config })).toBe(
|
||||
firstProvider,
|
||||
);
|
||||
|
||||
process.env.HOME = "/home/two";
|
||||
resolvePluginProvidersMock.mockReturnValueOnce([secondProvider]);
|
||||
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config })).toBe(
|
||||
secondProvider,
|
||||
);
|
||||
} finally {
|
||||
if (originalHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = originalHome;
|
||||
}
|
||||
if (originalOpenClawHome === undefined) {
|
||||
delete process.env.OPENCLAW_HOME;
|
||||
} else {
|
||||
process.env.OPENCLAW_HOME = originalOpenClawHome;
|
||||
}
|
||||
}
|
||||
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not reuse auto-enabled runtime providers for synthetic auth fallback", () => {
|
||||
const runtimeProvider: ProviderPlugin = {
|
||||
id: DEMO_PROVIDER_ID,
|
||||
|
||||
Reference in New Issue
Block a user