From d5cc0d53b7e3fdd8208034d32b6f89b55a7620ae Mon Sep 17 00:00:00 2001 From: Xu Xiang Date: Thu, 21 May 2026 04:09:32 +0800 Subject: [PATCH] fix(browser): honor image sanitization config for screenshots (#84595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: - The branch threads `agents.defaults.imageMaxDimensionPx` into browser screenshot and labeled snapshot image results, adds regression coverage and a changelog entry, and includes small repair-pass type/lint cleanup. - Reproducibility: yes. source-level reproduction is high confidence: current `main` calls `imageResultFromFil ... both browser image-returning paths, while the shared sanitizer falls back to `1200px` without an override. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(browser): honor image sanitization config for screenshots - PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8459… Validation: - ClawSweeper review passed for head c01fde7990cf7c7555b05189e238d840b2fed8e1. - Required merge gates passed before the squash merge. Prepared head SHA: c01fde7990cf7c7555b05189e238d840b2fed8e1 Review: https://github.com/openclaw/openclaw/pull/84595#issuecomment-4499178477 Co-authored-by: Xu Xiang Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + .../browser/src/browser-tool.actions.ts | 2 + .../browser/src/browser-tool.runtime.ts | 11 ++++- extensions/browser/src/browser-tool.test.ts | 44 +++++++++++++++++-- extensions/browser/src/browser-tool.ts | 2 + extensions/openrouter/provider-routing.ts | 10 ++--- src/commands/status.summary.redaction.test.ts | 7 ++- src/infra/secret-file.test.ts | 28 ++++++------ 8 files changed, 79 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0b3a08537..d5dd8fa8c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Dependencies: update `@openclaw/fs-safe` to `0.2.7` so OpenClaw's default Python-helper-off policy keeps best-effort Node write fallbacks for private stores, secret writes, run logs, and media attachments on Linux/macOS. +- Browser: honor the configured image sanitization limit for screenshots and labeled snapshots so browser-captured images follow the same resize policy as other image results. (#84595) - Status: show the configured default, session-selected model, reason, clear hint, and docs link when a session remains pinned to a model that differs from `agents.defaults.model.primary`. - Mac app: keep local packaging signed with a stable app identity for permission testing and fix Control UI production builds under current Vite/Highlight.js exports. - macOS app: update the embedded Peekaboo bridge to 3.2.1 so OpenClaw-hosted UI automation works with current Peekaboo CLI capture flows. diff --git a/extensions/browser/src/browser-tool.actions.ts b/extensions/browser/src/browser-tool.actions.ts index 1d2a2183271..f7ac173672d 100644 --- a/extensions/browser/src/browser-tool.actions.ts +++ b/extensions/browser/src/browser-tool.actions.ts @@ -13,6 +13,7 @@ import { readStringValue, resolveBrowserConfig, resolveProfile, + resolveRuntimeImageSanitization, wrapExternalContent, } from "./browser-tool.runtime.js"; import { DEFAULT_BROWSER_ACTION_TIMEOUT_MS } from "./browser/constants.js"; @@ -463,6 +464,7 @@ export async function executeSnapshotAction(params: { path: snapshot.imagePath, extraText: wrappedSnapshot, details: safeDetails, + imageSanitization: resolveRuntimeImageSanitization(), }); } return { diff --git a/extensions/browser/src/browser-tool.runtime.ts b/extensions/browser/src/browser-tool.runtime.ts index 45f397adb5c..5f384a48fe2 100644 --- a/extensions/browser/src/browser-tool.runtime.ts +++ b/extensions/browser/src/browser-tool.runtime.ts @@ -1,4 +1,13 @@ -export { getRuntimeConfig } from "./sdk-config.js"; +import { getRuntimeConfig } from "./sdk-config.js"; + +export { getRuntimeConfig }; +export function resolveRuntimeImageSanitization(): { maxDimensionPx: number } | undefined { + const configured = getRuntimeConfig().agents?.defaults?.imageMaxDimensionPx; + if (typeof configured !== "number" || !Number.isFinite(configured)) { + return undefined; + } + return { maxDimensionPx: Math.max(1, Math.floor(configured)) }; +} export { callGatewayTool, imageResultFromFile, diff --git a/extensions/browser/src/browser-tool.test.ts b/extensions/browser/src/browser-tool.test.ts index 36fd73661fa..2547167dbfb 100644 --- a/extensions/browser/src/browser-tool.test.ts +++ b/extensions/browser/src/browser-tool.test.ts @@ -123,6 +123,7 @@ const configMocks = vi.hoisted(() => ({ () => { browser: Record; gateway?: { nodes?: { browser?: { node?: string } } }; + agents?: { defaults?: { imageMaxDimensionPx?: number } }; } >(() => ({ browser: {} })), })); @@ -185,6 +186,12 @@ vi.mock("./browser-tool.runtime.js", () => { ...gatewayMocks, ...sessionTabRegistryMocks, getRuntimeConfig: configMocks.loadConfig, + resolveRuntimeImageSanitization: () => { + const configured = configMocks.loadConfig().agents?.defaults?.imageMaxDimensionPx; + return typeof configured === "number" && Number.isFinite(configured) + ? { maxDimensionPx: Math.max(1, Math.floor(configured)) } + : undefined; + }, applyBrowserProxyPaths: vi.fn(), getBrowserProfileCapabilities: (profile: Record) => ({ usesChromeMcp: profile.driver === "existing-session", @@ -715,6 +722,29 @@ describe("browser tool snapshot maxChars", () => { expect(opts.timeoutMs).toBe(12_345); }); + it("passes configured image sanitization to screenshot image results", async () => { + configMocks.loadConfig.mockReturnValue({ + browser: {}, + agents: { defaults: { imageMaxDimensionPx: 2000 } }, + } as never); + toolCommonMocks.imageResultFromFile.mockResolvedValueOnce({ + content: [{ type: "image", data: "base64", mimeType: "image/png" }], + details: { path: "/tmp/test.png" }, + }); + + const tool = createBrowserTool(); + await tool.execute?.("call-1", { + action: "screenshot", + target: "host", + targetId: "tab-1", + }); + + const imageParams = lastMockCallArg<{ + imageSanitization?: { maxDimensionPx?: number }; + }>(toolCommonMocks.imageResultFromFile, 0); + expect(imageParams.imageSanitization).toEqual({ maxDimensionPx: 2000 }); + }); + it("passes screenshot timeoutMs through the node browser proxy", async () => { mockSingleBrowserProxyNode(); gatewayMocks.callGatewayTool.mockResolvedValueOnce({ @@ -1163,6 +1193,10 @@ describe("browser tool snapshot labels", () => { registerBrowserToolAfterEachReset(); it("returns image + text when labels are requested", async () => { + configMocks.loadConfig.mockReturnValue({ + browser: {}, + agents: { defaults: { imageMaxDimensionPx: 2000 } }, + } as never); const tool = createBrowserTool(); const imageResult = { content: [ @@ -1188,12 +1222,14 @@ describe("browser tool snapshot labels", () => { labels: true, }); - const imageParams = lastMockCallArg<{ path?: string; extraText?: string }>( - toolCommonMocks.imageResultFromFile, - 0, - ); + const imageParams = lastMockCallArg<{ + path?: string; + extraText?: string; + imageSanitization?: { maxDimensionPx?: number }; + }>(toolCommonMocks.imageResultFromFile, 0); expect(imageParams.path).toBe("/tmp/snap.png"); expect(imageParams.extraText).toContain("<< 0 ? merged : undefined; } @@ -78,8 +78,8 @@ export function resolveOpenRouterExtraParamsForTransport( } return { patch: { - ...(providerConfigParams ?? {}), - ...(modelParams ?? {}), + ...providerConfigParams, + ...modelParams, ...ctx.extraParams, ...(providerRouting ? { provider: providerRouting } : {}), }, diff --git a/src/commands/status.summary.redaction.test.ts b/src/commands/status.summary.redaction.test.ts index 1bdecc15b4c..453bf7c6f8c 100644 --- a/src/commands/status.summary.redaction.test.ts +++ b/src/commands/status.summary.redaction.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; import { redactSensitiveStatusSummary } from "./status.summary.js"; -import type { StatusSummary } from "./status.types.js"; +import type { SessionStatus, StatusSummary } from "./status.types.js"; -function createRecentSessionRow() { +function createRecentSessionRow(): SessionStatus { return { key: "main", kind: "direct" as const, @@ -14,6 +14,9 @@ function createRecentSessionRow() { remainingTokens: 4, percentUsed: 5, model: "gpt-5", + configuredModel: "gpt-5", + selectedModel: "gpt-5", + modelSelectionReason: null, contextTokens: 200_000, flags: ["id:sess-1"], }; diff --git a/src/infra/secret-file.test.ts b/src/infra/secret-file.test.ts index 2645132ffac..54ead88e3d0 100644 --- a/src/infra/secret-file.test.ts +++ b/src/infra/secret-file.test.ts @@ -111,21 +111,21 @@ describe("readSecretFileSync", () => { await expectSecretFileError({ setup, expectedMessage, options }); }); + it("throws from the try helper for rejected files", async () => { + const file = await createSecretPath(async (dir) => { + const target = path.join(dir, "target.txt"); + const link = path.join(dir, "secret-link.txt"); + await fsPromises.writeFile(target, "top-secret\n", "utf8"); + await fsPromises.symlink(target, link); + return link; + }); + + expect(() => + tryReadSecretFileSync(file, "Telegram bot token", { rejectSymlink: true }), + ).toThrow(`Telegram bot token file at ${file} must not be a symlink.`); + }); + it.each([ - { - name: "returns undefined from the non-throwing helper for rejected files", - pathValue: async () => - createSecretPath(async (dir) => { - const target = path.join(dir, "target.txt"); - const link = path.join(dir, "secret-link.txt"); - await fsPromises.writeFile(target, "top-secret\n", "utf8"); - await fsPromises.symlink(target, link); - return link; - }), - label: "Telegram bot token", - options: { rejectSymlink: true }, - expected: undefined, - }, { name: "returns undefined from the non-throwing helper for blank file paths", pathValue: async () => " ",