From 44965bf63c6f477d56c987986eef1f7374ca2c61 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 22 Apr 2026 16:13:32 -0700 Subject: [PATCH] fix(diffs): refresh live tool config --- CHANGELOG.md | 1 + extensions/diffs/src/browser.test.ts | 239 +++++++++++++++++- extensions/diffs/src/plugin.ts | 30 ++- .../contracts/boundary-invariants.test.ts | 3 +- 4 files changed, 258 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4213266595..5ac140c87f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index 502a70df4d2..d8d85ef678e 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -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) => Promise; + }; + type HttpRouteHandler = ( + req: IncomingMessage, + res: ServerResponse, + ) => boolean | Promise; + type RegisteredHttpRouteParams = Parameters[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[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 } | 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) => Promise; @@ -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 } | 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) => Promise; + }; + type HttpRouteHandler = ( + req: IncomingMessage, + res: ServerResponse, + ) => boolean | Promise; + type RegisteredHttpRouteParams = Parameters[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[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 } | 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 { diff --git a/extensions/diffs/src/plugin.ts b/extensions/diffs/src/plugin.ts index d58b180fb41..9fee1a50af5 100644 --- a/extensions/diffs/src/plugin.ts +++ b/extensions/diffs/src/plugin.ts @@ -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, + ) ?? {}; 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, }), }); diff --git a/src/plugins/contracts/boundary-invariants.test.ts b/src/plugins/contracts/boundary-invariants.test.ts index 3be4f07b5a5..48dc7c7905b 100644 --- a/src/plugins/contracts/boundary-invariants.test.ts +++ b/src/plugins/contracts/boundary-invariants.test.ts @@ -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": [