mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 21:14:46 +00:00
fix(plugins): attribute runtime config deprecations (#81425) (thanks @BKF-Gitty)
Co-authored-by: BKF-Gitty <bandark@mac.com>
This commit is contained in:
@@ -85,6 +85,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/commitments: write `--json` output to stdout instead of diagnostic logs so automation can parse commitment list and dismiss results. (#81215) Thanks @giodl73-repo.
|
||||
- Update: allow pnpm GitHub-source OpenClaw updates to approve the OpenClaw package build, so source installs complete their prepare/prepack lifecycle. (#81294) Thanks @fuller-stack-dev.
|
||||
- Test state: seed isolated auth-profile secret keys for generated homes, preventing helper-backed proof runs from falling back to host Keychain secrets. (#81393) Thanks @altaywtf.
|
||||
- Plugins/runtime: attribute deprecated runtime config load/write warnings to the plugin id and source that triggered them so logs and plugin doctor runs are actionable. Refs #81394. (#81425) Thanks @BKF-Gitty.
|
||||
|
||||
### Changes
|
||||
|
||||
|
||||
67
src/plugins/registry.runtime-config.test.ts
Normal file
67
src/plugins/registry.runtime-config.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createPluginRecord } from "./loader-records.js";
|
||||
import { createPluginRegistry } from "./registry.js";
|
||||
import { getPluginRuntimeGatewayRequestScope } from "./runtime/gateway-request-scope.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
|
||||
function createTestRegistry(runtime: PluginRuntime) {
|
||||
return createPluginRegistry({
|
||||
logger: {
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
debug() {},
|
||||
},
|
||||
runtime,
|
||||
activateGlobalSideEffects: false,
|
||||
});
|
||||
}
|
||||
|
||||
describe("plugin registry runtime config scope", () => {
|
||||
it("runs deprecated config helpers with the owning plugin scope", async () => {
|
||||
let loadScope = getPluginRuntimeGatewayRequestScope();
|
||||
let writeScope = getPluginRuntimeGatewayRequestScope();
|
||||
const config = {} as OpenClawConfig;
|
||||
const replaceResult = {
|
||||
previousHash: null,
|
||||
nextHash: "next",
|
||||
} as unknown as Awaited<ReturnType<PluginRuntime["config"]["replaceConfigFile"]>>;
|
||||
const configRuntime = {
|
||||
current: vi.fn(() => config),
|
||||
mutateConfigFile: async <T = void>() => ({
|
||||
...replaceResult,
|
||||
result: undefined as T | undefined,
|
||||
}),
|
||||
replaceConfigFile: async () => replaceResult,
|
||||
loadConfig: vi.fn(() => {
|
||||
loadScope = getPluginRuntimeGatewayRequestScope();
|
||||
return config;
|
||||
}),
|
||||
writeConfigFile: vi.fn(async () => {
|
||||
writeScope = getPluginRuntimeGatewayRequestScope();
|
||||
}),
|
||||
} satisfies PluginRuntime["config"];
|
||||
const pluginRegistry = createTestRegistry({ config: configRuntime } as PluginRuntime);
|
||||
const record = createPluginRecord({
|
||||
id: "legacy-plugin",
|
||||
name: "Legacy Plugin",
|
||||
source: "/plugins/legacy-plugin/index.js",
|
||||
origin: "global",
|
||||
enabled: true,
|
||||
});
|
||||
const api = pluginRegistry.createApi(record, { config });
|
||||
|
||||
expect(api.runtime.config.loadConfig()).toBe(config);
|
||||
await api.runtime.config.writeConfigFile(config);
|
||||
|
||||
expect(loadScope).toMatchObject({
|
||||
pluginId: "legacy-plugin",
|
||||
pluginSource: "/plugins/legacy-plugin/index.js",
|
||||
});
|
||||
expect(writeScope).toMatchObject({
|
||||
pluginId: "legacy-plugin",
|
||||
pluginSource: "/plugins/legacy-plugin/index.js",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -124,7 +124,10 @@ export type {
|
||||
PluginSessionExtensionRegistryRegistration,
|
||||
} from "./registry-types.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
import { withPluginRuntimePluginIdScope } from "./runtime/gateway-request-scope.js";
|
||||
import {
|
||||
withPluginRuntimePluginIdScope,
|
||||
withPluginRuntimePluginScope,
|
||||
} from "./runtime/gateway-request-scope.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import { validateJsonSchemaValue, type JsonSchemaValue } from "./schema-validator.js";
|
||||
import { normalizeSessionEntrySlotKey } from "./session-entry-slot-keys.js";
|
||||
@@ -2367,6 +2370,14 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
}
|
||||
const runtime = new Proxy(registryParams.runtime, {
|
||||
get(target, prop, receiver) {
|
||||
const runWithPluginScope = <T>(run: () => T): T => {
|
||||
const record =
|
||||
pluginRuntimeRecordById.get(pluginId) ??
|
||||
registry.plugins.find((entry) => entry.id === pluginId);
|
||||
return record?.source
|
||||
? withPluginRuntimePluginScope({ pluginId, pluginSource: record.source }, run)
|
||||
: withPluginRuntimePluginScope({ pluginId }, run);
|
||||
};
|
||||
if (prop === "state") {
|
||||
const baseState = Reflect.get(target, prop, receiver);
|
||||
return {
|
||||
@@ -2384,6 +2395,15 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
},
|
||||
} satisfies PluginRuntime["state"];
|
||||
}
|
||||
if (prop === "config") {
|
||||
const config = Reflect.get(target, prop, receiver) as PluginRuntime["config"];
|
||||
return {
|
||||
...config,
|
||||
loadConfig: () => runWithPluginScope(() => config.loadConfig()),
|
||||
writeConfigFile: (cfg, options) =>
|
||||
runWithPluginScope(() => config.writeConfigFile(cfg, options)),
|
||||
} satisfies PluginRuntime["config"];
|
||||
}
|
||||
if (prop === "llm") {
|
||||
const llm = Reflect.get(target, prop, receiver);
|
||||
return {
|
||||
|
||||
@@ -10,6 +10,12 @@ export type PluginRuntimeGatewayRequestScope = {
|
||||
client?: GatewayRequestOptions["client"];
|
||||
isWebchatConnect: GatewayRequestOptions["isWebchatConnect"];
|
||||
pluginId?: string;
|
||||
pluginSource?: string;
|
||||
};
|
||||
|
||||
export type PluginRuntimePluginScope = {
|
||||
pluginId: string;
|
||||
pluginSource?: string;
|
||||
};
|
||||
|
||||
const PLUGIN_RUNTIME_GATEWAY_REQUEST_SCOPE_KEY: unique symbol = Symbol.for(
|
||||
@@ -36,17 +42,29 @@ export function withPluginRuntimeGatewayRequestScope<T>(
|
||||
/**
|
||||
* Runs work under the current gateway request scope while attaching plugin identity.
|
||||
*/
|
||||
export function withPluginRuntimePluginIdScope<T>(pluginId: string, run: () => T): T {
|
||||
export function withPluginRuntimePluginScope<T>(scope: PluginRuntimePluginScope, run: () => T): T {
|
||||
const current = pluginRuntimeGatewayRequestScope.getStore();
|
||||
const scoped: PluginRuntimeGatewayRequestScope = current
|
||||
? { ...current, pluginId }
|
||||
? { ...current, pluginId: scope.pluginId }
|
||||
: {
|
||||
pluginId,
|
||||
pluginId: scope.pluginId,
|
||||
isWebchatConnect: () => false,
|
||||
};
|
||||
if (scope.pluginSource !== undefined) {
|
||||
scoped.pluginSource = scope.pluginSource;
|
||||
} else {
|
||||
delete scoped.pluginSource;
|
||||
}
|
||||
return pluginRuntimeGatewayRequestScope.run(scoped, run);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs work under the current gateway request scope while attaching plugin identity.
|
||||
*/
|
||||
export function withPluginRuntimePluginIdScope<T>(pluginId: string, run: () => T): T {
|
||||
return withPluginRuntimePluginScope({ pluginId }, run);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current plugin gateway request scope when called from a plugin request handler.
|
||||
*/
|
||||
|
||||
@@ -19,11 +19,14 @@ vi.mock("../../logger.js", () => ({
|
||||
logWarn: (...args: unknown[]) => logWarnMock(...args),
|
||||
}));
|
||||
|
||||
const { createRuntimeConfig } = await import("./runtime-config.js");
|
||||
const { withPluginRuntimePluginScope } = await import("./gateway-request-scope.js");
|
||||
const { createRuntimeConfig, resetRuntimeConfigDeprecationWarningStateForTest } =
|
||||
await import("./runtime-config.js");
|
||||
const deprecatedConfigCode = "runtime-config-load-write";
|
||||
|
||||
describe("createRuntimeConfig", () => {
|
||||
beforeEach(() => {
|
||||
resetRuntimeConfigDeprecationWarningStateForTest();
|
||||
getRuntimeConfigMock.mockReset();
|
||||
mutateConfigFileMock.mockReset();
|
||||
replaceConfigFileMock.mockReset();
|
||||
@@ -46,6 +49,40 @@ describe("createRuntimeConfig", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("attributes deprecated loadConfig warnings to the active plugin scope", () => {
|
||||
const runtimeConfig = { plugins: { entries: {} } };
|
||||
getRuntimeConfigMock.mockReturnValue(runtimeConfig);
|
||||
const configApi = createRuntimeConfig();
|
||||
|
||||
const loaded = withPluginRuntimePluginScope(
|
||||
{ pluginId: "legacy-plugin", pluginSource: "/plugins/legacy-plugin/index.js" },
|
||||
() => configApi.loadConfig(),
|
||||
);
|
||||
|
||||
expect(loaded).toBe(runtimeConfig);
|
||||
expect(logWarnMock).toHaveBeenCalledWith(
|
||||
`plugin "legacy-plugin" runtime config.loadConfig() is deprecated (${deprecatedConfigCode}); use config.current(). Source: /plugins/legacy-plugin/index.js`,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps deprecated loadConfig warning attribution per plugin", () => {
|
||||
const configApi = createRuntimeConfig();
|
||||
|
||||
withPluginRuntimePluginScope({ pluginId: "first" }, () => configApi.loadConfig());
|
||||
withPluginRuntimePluginScope({ pluginId: "first" }, () => configApi.loadConfig());
|
||||
withPluginRuntimePluginScope({ pluginId: "second" }, () => configApi.loadConfig());
|
||||
|
||||
expect(logWarnMock).toHaveBeenCalledTimes(2);
|
||||
expect(logWarnMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`plugin "first" runtime config.loadConfig() is deprecated (${deprecatedConfigCode}); use config.current().`,
|
||||
);
|
||||
expect(logWarnMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
`plugin "second" runtime config.loadConfig() is deprecated (${deprecatedConfigCode}); use config.current().`,
|
||||
);
|
||||
});
|
||||
|
||||
it("routes deprecated writeConfigFile through replaceConfigFile with afterWrite", async () => {
|
||||
const configApi = createRuntimeConfig();
|
||||
const nextConfig = { plugins: { entries: {} } } as OpenClawConfig;
|
||||
@@ -62,6 +99,25 @@ describe("createRuntimeConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("attributes deprecated writeConfigFile warnings to the active plugin scope", async () => {
|
||||
const configApi = createRuntimeConfig();
|
||||
const nextConfig = { plugins: { entries: {} } } as OpenClawConfig;
|
||||
|
||||
await withPluginRuntimePluginScope(
|
||||
{ pluginId: "legacy-plugin", pluginSource: "/plugins/legacy-plugin/index.js" },
|
||||
async () => await configApi.writeConfigFile(nextConfig),
|
||||
);
|
||||
|
||||
expect(logWarnMock).toHaveBeenCalledWith(
|
||||
`plugin "legacy-plugin" runtime config.writeConfigFile() is deprecated (${deprecatedConfigCode}); use config.mutateConfigFile(...) or config.replaceConfigFile(...). Source: /plugins/legacy-plugin/index.js`,
|
||||
);
|
||||
expect(replaceConfigFileMock).toHaveBeenCalledWith({
|
||||
nextConfig,
|
||||
afterWrite: { mode: "auto" },
|
||||
writeOptions: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves explicit afterWrite intent for deprecated writeConfigFile", async () => {
|
||||
const configApi = createRuntimeConfig();
|
||||
const nextConfig = { plugins: { entries: {} } } as OpenClawConfig;
|
||||
|
||||
@@ -4,25 +4,50 @@ import {
|
||||
replaceConfigFile as replaceConfigFileInternal,
|
||||
} from "../../config/mutate.js";
|
||||
import { logWarn } from "../../logger.js";
|
||||
import { getPluginRuntimeGatewayRequestScope } from "./gateway-request-scope.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
const RUNTIME_CONFIG_LOAD_WRITE_COMPAT_CODE = "runtime-config-load-write";
|
||||
|
||||
const warnedDeprecatedConfigApis = new Set<string>();
|
||||
|
||||
function formatDeprecatedConfigApiSubject(name: "loadConfig" | "writeConfigFile"): string {
|
||||
const scope = getPluginRuntimeGatewayRequestScope();
|
||||
if (!scope?.pluginId) {
|
||||
return `plugin runtime config.${name}()`;
|
||||
}
|
||||
return `plugin "${scope.pluginId}" runtime config.${name}()`;
|
||||
}
|
||||
|
||||
function formatDeprecatedConfigApiSource(): string {
|
||||
const scope = getPluginRuntimeGatewayRequestScope();
|
||||
return scope?.pluginSource ? ` Source: ${scope.pluginSource}` : "";
|
||||
}
|
||||
|
||||
function formatDeprecatedConfigApiWarningKey(name: "loadConfig" | "writeConfigFile"): string {
|
||||
const scope = getPluginRuntimeGatewayRequestScope();
|
||||
return `${name}:${scope?.pluginId ?? "anonymous"}`;
|
||||
}
|
||||
|
||||
function warnDeprecatedConfigApiOnce(
|
||||
name: "loadConfig" | "writeConfigFile",
|
||||
replacement: string,
|
||||
): void {
|
||||
if (warnedDeprecatedConfigApis.has(name)) {
|
||||
const warningKey = formatDeprecatedConfigApiWarningKey(name);
|
||||
if (warnedDeprecatedConfigApis.has(warningKey)) {
|
||||
return;
|
||||
}
|
||||
warnedDeprecatedConfigApis.add(name);
|
||||
warnedDeprecatedConfigApis.add(warningKey);
|
||||
logWarn(
|
||||
`plugin runtime config.${name}() is deprecated (${RUNTIME_CONFIG_LOAD_WRITE_COMPAT_CODE}); use ${replacement}.`,
|
||||
`${formatDeprecatedConfigApiSubject(name)} is deprecated (${RUNTIME_CONFIG_LOAD_WRITE_COMPAT_CODE}); use ${replacement}.${formatDeprecatedConfigApiSource()}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** @internal Test-only reset for the runtime config compatibility warning cache. */
|
||||
export function resetRuntimeConfigDeprecationWarningStateForTest(): void {
|
||||
warnedDeprecatedConfigApis.clear();
|
||||
}
|
||||
|
||||
export function createRuntimeConfig(): PluginRuntime["config"] {
|
||||
return {
|
||||
current: getRuntimeConfig,
|
||||
|
||||
Reference in New Issue
Block a user