Skip to content

Commit d5cc0d5

Browse files
xx205clawsweeper[bot]Takhoffman
authored
fix(browser): honor image sanitization config for screenshots (#84595)
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 c01fde7. - Required merge gates passed before the squash merge. Prepared head SHA: c01fde7 Review: #84595 (comment) Co-authored-by: Xu Xiang <xx205@outlook.com> 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>
1 parent 1a7669b commit d5cc0d5

8 files changed

Lines changed: 79 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717
### Fixes
1818

1919
- 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.
20+
- 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)
2021
- 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`.
2122
- 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.
2223
- macOS app: update the embedded Peekaboo bridge to 3.2.1 so OpenClaw-hosted UI automation works with current Peekaboo CLI capture flows.

extensions/browser/src/browser-tool.actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
readStringValue,
1414
resolveBrowserConfig,
1515
resolveProfile,
16+
resolveRuntimeImageSanitization,
1617
wrapExternalContent,
1718
} from "./browser-tool.runtime.js";
1819
import { DEFAULT_BROWSER_ACTION_TIMEOUT_MS } from "./browser/constants.js";
@@ -463,6 +464,7 @@ export async function executeSnapshotAction(params: {
463464
path: snapshot.imagePath,
464465
extraText: wrappedSnapshot,
465466
details: safeDetails,
467+
imageSanitization: resolveRuntimeImageSanitization(),
466468
});
467469
}
468470
return {

extensions/browser/src/browser-tool.runtime.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
export { getRuntimeConfig } from "./sdk-config.js";
1+
import { getRuntimeConfig } from "./sdk-config.js";
2+
3+
export { getRuntimeConfig };
4+
export function resolveRuntimeImageSanitization(): { maxDimensionPx: number } | undefined {
5+
const configured = getRuntimeConfig().agents?.defaults?.imageMaxDimensionPx;
6+
if (typeof configured !== "number" || !Number.isFinite(configured)) {
7+
return undefined;
8+
}
9+
return { maxDimensionPx: Math.max(1, Math.floor(configured)) };
10+
}
211
export {
312
callGatewayTool,
413
imageResultFromFile,

extensions/browser/src/browser-tool.test.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ const configMocks = vi.hoisted(() => ({
123123
() => {
124124
browser: Record<string, unknown>;
125125
gateway?: { nodes?: { browser?: { node?: string } } };
126+
agents?: { defaults?: { imageMaxDimensionPx?: number } };
126127
}
127128
>(() => ({ browser: {} })),
128129
}));
@@ -185,6 +186,12 @@ vi.mock("./browser-tool.runtime.js", () => {
185186
...gatewayMocks,
186187
...sessionTabRegistryMocks,
187188
getRuntimeConfig: configMocks.loadConfig,
189+
resolveRuntimeImageSanitization: () => {
190+
const configured = configMocks.loadConfig().agents?.defaults?.imageMaxDimensionPx;
191+
return typeof configured === "number" && Number.isFinite(configured)
192+
? { maxDimensionPx: Math.max(1, Math.floor(configured)) }
193+
: undefined;
194+
},
188195
applyBrowserProxyPaths: vi.fn(),
189196
getBrowserProfileCapabilities: (profile: Record<string, unknown>) => ({
190197
usesChromeMcp: profile.driver === "existing-session",
@@ -715,6 +722,29 @@ describe("browser tool snapshot maxChars", () => {
715722
expect(opts.timeoutMs).toBe(12_345);
716723
});
717724

725+
it("passes configured image sanitization to screenshot image results", async () => {
726+
configMocks.loadConfig.mockReturnValue({
727+
browser: {},
728+
agents: { defaults: { imageMaxDimensionPx: 2000 } },
729+
} as never);
730+
toolCommonMocks.imageResultFromFile.mockResolvedValueOnce({
731+
content: [{ type: "image", data: "base64", mimeType: "image/png" }],
732+
details: { path: "/tmp/test.png" },
733+
});
734+
735+
const tool = createBrowserTool();
736+
await tool.execute?.("call-1", {
737+
action: "screenshot",
738+
target: "host",
739+
targetId: "tab-1",
740+
});
741+
742+
const imageParams = lastMockCallArg<{
743+
imageSanitization?: { maxDimensionPx?: number };
744+
}>(toolCommonMocks.imageResultFromFile, 0);
745+
expect(imageParams.imageSanitization).toEqual({ maxDimensionPx: 2000 });
746+
});
747+
718748
it("passes screenshot timeoutMs through the node browser proxy", async () => {
719749
mockSingleBrowserProxyNode();
720750
gatewayMocks.callGatewayTool.mockResolvedValueOnce({
@@ -1163,6 +1193,10 @@ describe("browser tool snapshot labels", () => {
11631193
registerBrowserToolAfterEachReset();
11641194

11651195
it("returns image + text when labels are requested", async () => {
1196+
configMocks.loadConfig.mockReturnValue({
1197+
browser: {},
1198+
agents: { defaults: { imageMaxDimensionPx: 2000 } },
1199+
} as never);
11661200
const tool = createBrowserTool();
11671201
const imageResult = {
11681202
content: [
@@ -1188,12 +1222,14 @@ describe("browser tool snapshot labels", () => {
11881222
labels: true,
11891223
});
11901224

1191-
const imageParams = lastMockCallArg<{ path?: string; extraText?: string }>(
1192-
toolCommonMocks.imageResultFromFile,
1193-
0,
1194-
);
1225+
const imageParams = lastMockCallArg<{
1226+
path?: string;
1227+
extraText?: string;
1228+
imageSanitization?: { maxDimensionPx?: number };
1229+
}>(toolCommonMocks.imageResultFromFile, 0);
11951230
expect(imageParams.path).toBe("/tmp/snap.png");
11961231
expect(imageParams.extraText).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT");
1232+
expect(imageParams.imageSanitization).toEqual({ maxDimensionPx: 2000 });
11971233
expect(result).toEqual(imageResult);
11981234
expect(result?.content).toHaveLength(2);
11991235
expect(result?.content?.[0]).toEqual({ type: "text", text: "label text" });

extensions/browser/src/browser-tool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
readStringParam,
3737
readStringValue,
3838
resolveBrowserConfig,
39+
resolveRuntimeImageSanitization,
3940
resolveExistingPathsWithinRoot,
4041
resolveNodeIdFromList,
4142
resolveProfile,
@@ -770,6 +771,7 @@ export function createBrowserTool(opts?: {
770771
label: "browser:screenshot",
771772
path: result.path,
772773
details: result,
774+
imageSanitization: resolveRuntimeImageSanitization(),
773775
});
774776
}
775777
case "navigate": {

extensions/openrouter/provider-routing.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ function mergeOpenRouterProviderRouting(params: {
5656
const modelRouting = readRecord(params.modelParams?.provider);
5757
const extraRouting = readRecord(params.extraParams.provider);
5858
const merged = {
59-
...(providerRouting ?? {}),
60-
...(modelRouting ?? {}),
61-
...(extraRouting ?? {}),
59+
...providerRouting,
60+
...modelRouting,
61+
...extraRouting,
6262
};
6363
return Object.keys(merged).length > 0 ? merged : undefined;
6464
}
@@ -78,8 +78,8 @@ export function resolveOpenRouterExtraParamsForTransport(
7878
}
7979
return {
8080
patch: {
81-
...(providerConfigParams ?? {}),
82-
...(modelParams ?? {}),
81+
...providerConfigParams,
82+
...modelParams,
8383
...ctx.extraParams,
8484
...(providerRouting ? { provider: providerRouting } : {}),
8585
},

src/commands/status.summary.redaction.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, expect, it } from "vitest";
22
import { redactSensitiveStatusSummary } from "./status.summary.js";
3-
import type { StatusSummary } from "./status.types.js";
3+
import type { SessionStatus, StatusSummary } from "./status.types.js";
44

5-
function createRecentSessionRow() {
5+
function createRecentSessionRow(): SessionStatus {
66
return {
77
key: "main",
88
kind: "direct" as const,
@@ -14,6 +14,9 @@ function createRecentSessionRow() {
1414
remainingTokens: 4,
1515
percentUsed: 5,
1616
model: "gpt-5",
17+
configuredModel: "gpt-5",
18+
selectedModel: "gpt-5",
19+
modelSelectionReason: null,
1720
contextTokens: 200_000,
1821
flags: ["id:sess-1"],
1922
};

src/infra/secret-file.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,21 @@ describe("readSecretFileSync", () => {
111111
await expectSecretFileError({ setup, expectedMessage, options });
112112
});
113113

114+
it("throws from the try helper for rejected files", async () => {
115+
const file = await createSecretPath(async (dir) => {
116+
const target = path.join(dir, "target.txt");
117+
const link = path.join(dir, "secret-link.txt");
118+
await fsPromises.writeFile(target, "top-secret\n", "utf8");
119+
await fsPromises.symlink(target, link);
120+
return link;
121+
});
122+
123+
expect(() =>
124+
tryReadSecretFileSync(file, "Telegram bot token", { rejectSymlink: true }),
125+
).toThrow(`Telegram bot token file at ${file} must not be a symlink.`);
126+
});
127+
114128
it.each([
115-
{
116-
name: "returns undefined from the non-throwing helper for rejected files",
117-
pathValue: async () =>
118-
createSecretPath(async (dir) => {
119-
const target = path.join(dir, "target.txt");
120-
const link = path.join(dir, "secret-link.txt");
121-
await fsPromises.writeFile(target, "top-secret\n", "utf8");
122-
await fsPromises.symlink(target, link);
123-
return link;
124-
}),
125-
label: "Telegram bot token",
126-
options: { rejectSymlink: true },
127-
expected: undefined,
128-
},
129129
{
130130
name: "returns undefined from the non-throwing helper for blank file paths",
131131
pathValue: async () => " ",

0 commit comments

Comments
 (0)