mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 11:04:06 +00:00
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 headc01fde7990. - Required merge gates passed before the squash merge. Prepared head SHA:c01fde7990Review: https://github.com/openclaw/openclaw/pull/84595#issuecomment-4499178477 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>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -123,6 +123,7 @@ const configMocks = vi.hoisted(() => ({
|
||||
() => {
|
||||
browser: Record<string, unknown>;
|
||||
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<string, unknown>) => ({
|
||||
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("<<<EXTERNAL_UNTRUSTED_CONTENT");
|
||||
expect(imageParams.imageSanitization).toEqual({ maxDimensionPx: 2000 });
|
||||
expect(result).toEqual(imageResult);
|
||||
expect(result?.content).toHaveLength(2);
|
||||
expect(result?.content?.[0]).toEqual({ type: "text", text: "label text" });
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
readStringParam,
|
||||
readStringValue,
|
||||
resolveBrowserConfig,
|
||||
resolveRuntimeImageSanitization,
|
||||
resolveExistingPathsWithinRoot,
|
||||
resolveNodeIdFromList,
|
||||
resolveProfile,
|
||||
@@ -770,6 +771,7 @@ export function createBrowserTool(opts?: {
|
||||
label: "browser:screenshot",
|
||||
path: result.path,
|
||||
details: result,
|
||||
imageSanitization: resolveRuntimeImageSanitization(),
|
||||
});
|
||||
}
|
||||
case "navigate": {
|
||||
|
||||
@@ -56,9 +56,9 @@ function mergeOpenRouterProviderRouting(params: {
|
||||
const modelRouting = readRecord(params.modelParams?.provider);
|
||||
const extraRouting = readRecord(params.extraParams.provider);
|
||||
const merged = {
|
||||
...(providerRouting ?? {}),
|
||||
...(modelRouting ?? {}),
|
||||
...(extraRouting ?? {}),
|
||||
...providerRouting,
|
||||
...modelRouting,
|
||||
...extraRouting,
|
||||
};
|
||||
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||
}
|
||||
@@ -78,8 +78,8 @@ export function resolveOpenRouterExtraParamsForTransport(
|
||||
}
|
||||
return {
|
||||
patch: {
|
||||
...(providerConfigParams ?? {}),
|
||||
...(modelParams ?? {}),
|
||||
...providerConfigParams,
|
||||
...modelParams,
|
||||
...ctx.extraParams,
|
||||
...(providerRouting ? { provider: providerRouting } : {}),
|
||||
},
|
||||
|
||||
@@ -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"],
|
||||
};
|
||||
|
||||
@@ -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 () => " ",
|
||||
|
||||
Reference in New Issue
Block a user