fix(diffs): refresh live tool config

This commit is contained in:
Vincent Koc
2026-04-22 16:13:32 -07:00
parent 1019b663ce
commit 44965bf63c
4 changed files with 258 additions and 15 deletions

View File

@@ -59,6 +59,7 @@ Docs: https://docs.openclaw.ai
- CLI sessions: persist CLI session clearing through the atomic session-store merge path, so expired Claude/Codex CLI bindings are actually removed before retrying without the stale session id. (#70298) Thanks @HFConsultant.
- ACP/sessions_spawn: honor explicit `model` overrides for ACP child sessions instead of silently falling back to the target agent default model. (#70210) Thanks @felix-miao.
- Diffs/viewer: re-read remote viewer access policy from live runtime config on each request, so toggling `plugins.entries.diffs.config.security.allowRemoteViewer` closes proxied viewer access immediately instead of waiting for a restart. Thanks @vincentkoc.
- Diffs/tooling: re-read `viewerBaseUrl`, presentation defaults, and viewer access policy from live runtime config, and fail closed when the live `diffs` plugin entry disappears instead of reviving startup viewer settings. Thanks @vincentkoc.
- Memory/LanceDB: stop resurrecting removed live `memory-lancedb` hook config from startup snapshots, so deleting or disabling the plugin entry shuts off auto-recall and auto-capture without a restart. Thanks @vincentkoc.
- Active Memory: stop reviving removed live `active-memory` config from startup snapshots, so removing the plugin entry turns the hook off immediately instead of waiting for a restart. Thanks @vincentkoc.
- Agents/subagents: drop bare `NO_REPLY` from the parent turn when the session still has pending spawned children, so direct-conversation surfaces such as Telegram DMs no longer rewrite the sentinel into visible fallback chatter while waiting for the child completion event. (#69942) Thanks @neeravmakwana.

View File

@@ -192,6 +192,138 @@ describe("PlaywrightDiffScreenshotter", () => {
});
describe("diffs plugin registration", () => {
it("uses live runtime tool config through the registered tool factory", async () => {
type RegisteredTool = {
execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
};
type HttpRouteHandler = (
req: IncomingMessage,
res: ServerResponse,
) => boolean | Promise<boolean>;
type RegisteredHttpRouteParams = Parameters<OpenClawPluginApi["registerHttpRoute"]>[0];
let registeredToolFactory:
| ((ctx: OpenClawPluginToolContext) => RegisteredTool | RegisteredTool[] | null | undefined)
| undefined;
let registeredHttpRouteHandler: HttpRouteHandler | undefined;
let configFile: OpenClawConfig = {
gateway: {
port: 18789,
bind: "loopback",
},
plugins: {
entries: {
diffs: {
config: {
viewerBaseUrl: "https://startup.example.com/openclaw",
defaults: {
mode: "view",
theme: "light",
background: false,
layout: "split",
showLineNumbers: false,
diffIndicators: "classic",
lineSpacing: 2,
},
},
},
},
},
} as OpenClawConfig;
const api = createTestPluginApi({
id: "diffs",
name: "Diffs",
description: "Diffs",
source: "test",
config: {
gateway: {
port: 18789,
bind: "loopback",
},
},
pluginConfig: {
viewerBaseUrl: "https://startup.example.com/openclaw",
defaults: {
mode: "view",
theme: "light",
background: false,
layout: "split",
showLineNumbers: false,
diffIndicators: "classic",
lineSpacing: 2,
},
},
runtime: {
config: {
loadConfig: () => configFile,
},
} as never,
registerTool(tool: Parameters<OpenClawPluginApi["registerTool"]>[0]) {
registeredToolFactory = typeof tool === "function" ? tool : () => tool;
},
registerHttpRoute(params: RegisteredHttpRouteParams) {
registeredHttpRouteHandler = params.handler as HttpRouteHandler;
},
on: vi.fn(),
});
registerDiffsPlugin(api as unknown as OpenClawPluginApi);
configFile = {
...configFile,
plugins: {
entries: {
diffs: {
config: {
viewerBaseUrl: "https://live.example.com/gateway",
defaults: {
mode: "view",
theme: "dark",
background: true,
layout: "unified",
showLineNumbers: true,
diffIndicators: "bars",
lineSpacing: 1.6,
},
},
},
},
},
} as OpenClawConfig;
const registeredTool = registeredToolFactory?.({
agentId: "main",
sessionId: "session-456",
messageChannel: "discord",
agentAccountId: "default",
}) as RegisteredTool | undefined;
const result = await registeredTool?.execute?.("tool-1", {
before: "one\n",
after: "two\n",
});
const details = (result as { details?: Record<string, unknown> } | undefined)?.details;
const viewerPath = String(details?.viewerPath);
const res = createMockServerResponse();
const handled = await registeredHttpRouteHandler?.(
localReq({
method: "GET",
url: viewerPath,
}),
res,
);
expect(handled).toBe(true);
expect(String(details?.viewerUrl)).toContain("https://live.example.com/gateway");
expect(res.statusCode).toBe(200);
expect(String(res.body)).toContain('body data-theme="dark"');
expect(String(res.body)).toContain('"backgroundEnabled":true');
expect(String(res.body)).toContain('"diffStyle":"unified"');
expect(String(res.body)).toContain('"disableLineNumbers":false');
expect(String(res.body)).toContain('"diffIndicators":"bars"');
expect(String(res.body)).toContain("--diffs-line-height: 24px;");
});
it("uses live runtime viewer-access config through the registered HTTP handler", async () => {
type RegisteredTool = {
execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
@@ -299,12 +431,6 @@ describe("diffs plugin registration", () => {
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(res.body)).toContain('body data-theme="light"');
expect(String(res.body)).toContain('"backgroundEnabled":false');
expect(String(res.body)).toContain('"diffStyle":"split"');
expect(String(res.body)).toContain('"disableLineNumbers":true');
expect(String(res.body)).toContain('"diffIndicators":"classic"');
expect(String(res.body)).toContain("--diffs-line-height: 30px;");
expect((result as { details?: Record<string, unknown> } | undefined)?.details?.context).toEqual(
{
agentId: "main",
@@ -344,6 +470,107 @@ describe("diffs plugin registration", () => {
expect(proxiedHandled).toBe(true);
expect(proxiedRes.statusCode).toBe(404);
});
it("fails closed for remote viewer access when the live diffs plugin entry is removed", async () => {
type RegisteredTool = {
execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
};
type HttpRouteHandler = (
req: IncomingMessage,
res: ServerResponse,
) => boolean | Promise<boolean>;
type RegisteredHttpRouteParams = Parameters<OpenClawPluginApi["registerHttpRoute"]>[0];
let registeredToolFactory:
| ((ctx: OpenClawPluginToolContext) => RegisteredTool | RegisteredTool[] | null | undefined)
| undefined;
let registeredHttpRouteHandler: HttpRouteHandler | undefined;
let configFile: OpenClawConfig = {
gateway: {
port: 18789,
bind: "loopback",
},
plugins: {
entries: {
diffs: {
config: {
security: {
allowRemoteViewer: true,
},
},
},
},
},
} as OpenClawConfig;
const api = createTestPluginApi({
id: "diffs",
name: "Diffs",
description: "Diffs",
source: "test",
config: {
gateway: {
port: 18789,
bind: "loopback",
},
},
pluginConfig: {
security: {
allowRemoteViewer: true,
},
},
runtime: {
config: {
loadConfig: () => configFile,
},
} as never,
registerTool(tool: Parameters<OpenClawPluginApi["registerTool"]>[0]) {
registeredToolFactory = typeof tool === "function" ? tool : () => tool;
},
registerHttpRoute(params: RegisteredHttpRouteParams) {
registeredHttpRouteHandler = params.handler as HttpRouteHandler;
},
on: vi.fn(),
});
registerDiffsPlugin(api as unknown as OpenClawPluginApi);
const registeredTool = registeredToolFactory?.({
agentId: "main",
sessionId: "session-789",
messageChannel: "discord",
agentAccountId: "default",
}) as RegisteredTool | undefined;
const result = await registeredTool?.execute?.("tool-1", {
before: "one\n",
after: "two\n",
});
const viewerPath = String(
(result as { details?: Record<string, unknown> } | undefined)?.details?.viewerPath,
);
configFile = {
...configFile,
plugins: {
entries: {},
},
} as OpenClawConfig;
const proxiedRes = createMockServerResponse();
const proxiedHandled = await registeredHttpRouteHandler?.(
localReq({
method: "GET",
url: viewerPath,
headers: {
"x-forwarded-for": "203.0.113.10",
},
}),
proxiedRes,
);
expect(proxiedHandled).toBe(true);
expect(proxiedRes.statusCode).toBe(404);
});
});
function createConfig(): OpenClawConfig {

View File

@@ -1,5 +1,5 @@
import path from "node:path";
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/config-runtime";
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/config-runtime";
import { resolvePreferredOpenClawTmpDir, type OpenClawPluginApi } from "../api.js";
import {
resolveDiffsPluginDefaults,
@@ -12,24 +12,38 @@ import { DiffArtifactStore } from "./store.js";
import { createDiffsTool } from "./tool.js";
export function registerDiffsPlugin(api: OpenClawPluginApi): void {
const defaults = resolveDiffsPluginDefaults(api.pluginConfig);
const viewerBaseUrl = resolveDiffsPluginViewerBaseUrl(api.pluginConfig);
const store = new DiffArtifactStore({
rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
logger: api.logger,
});
const resolveCurrentPluginConfig = () =>
resolveLivePluginConfigObject(
api.runtime.config?.loadConfig,
"diffs",
api.pluginConfig as Record<string, unknown>,
) ?? {};
const resolveCurrentAccessConfig = () => {
const currentConfig = api.runtime.config?.loadConfig?.() ?? api.config;
const pluginConfig = resolvePluginConfigObject(currentConfig, "diffs") ?? api.pluginConfig;
const pluginConfig = resolveCurrentPluginConfig();
return {
allowRemoteViewer: resolveDiffsPluginSecurity(pluginConfig).allowRemoteViewer,
trustedProxies: currentConfig.gateway?.trustedProxies,
allowRealIpFallback: currentConfig.gateway?.allowRealIpFallback === true,
};
};
const initialAccessConfig = resolveCurrentAccessConfig();
api.registerTool(
(ctx) => createDiffsTool({ api, store, defaults, viewerBaseUrl, context: ctx }),
(ctx) => {
const pluginConfig = resolveCurrentPluginConfig();
return createDiffsTool({
api,
store,
defaults: resolveDiffsPluginDefaults(pluginConfig),
viewerBaseUrl: resolveDiffsPluginViewerBaseUrl(pluginConfig),
context: ctx,
});
},
{
name: "diffs",
},
@@ -41,9 +55,9 @@ export function registerDiffsPlugin(api: OpenClawPluginApi): void {
handler: createDiffsHttpHandler({
store,
logger: api.logger,
allowRemoteViewer: resolveDiffsPluginSecurity(api.pluginConfig).allowRemoteViewer,
trustedProxies: api.config.gateway?.trustedProxies,
allowRealIpFallback: api.config.gateway?.allowRealIpFallback === true,
allowRemoteViewer: initialAccessConfig.allowRemoteViewer,
trustedProxies: initialAccessConfig.trustedProxies,
allowRealIpFallback: initialAccessConfig.allowRealIpFallback,
resolveAccessConfig: resolveCurrentAccessConfig,
}),
});

View File

@@ -49,7 +49,8 @@ const BUNDLED_TYPED_HOOK_REGISTRATION_GUARDS = {
const BUNDLED_LIVE_CONFIG_HOOK_GUARDS = {
"extensions/active-memory/index.ts": ["resolveLivePluginConfigObject(", '"active-memory"'],
"extensions/diffs/src/plugin.ts": [
'resolvePluginConfigObject(currentConfig, "diffs")',
"resolveLivePluginConfigObject(",
'"diffs"',
"api.runtime.config?.loadConfig?.() ?? api.config",
],
"extensions/memory-core/src/dreaming.ts": [