fix(browser): gate evaluate behind config flag

This commit is contained in:
Peter Steinberger
2026-01-27 05:00:07 +00:00
parent cb770f2cec
commit 78f0bc3ec0
20 changed files with 162 additions and 14 deletions

View File

@@ -14,7 +14,13 @@ function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number):
? "If this is a sandboxed session, ensure the sandbox browser is running and try again."
: `Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or \`${formatCliCommand("clawdbot gateway")}\`) and try again.`;
const msg = String(err);
if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
const msgLower = msg.toLowerCase();
const looksLikeTimeout =
msgLower.includes("timed out") ||
msgLower.includes("timeout") ||
msgLower.includes("aborted") ||
msgLower.includes("abort");
if (looksLikeTimeout) {
return new Error(
`Can't reach the clawd browser control service (timed out after ${timeoutMs}ms). ${hint}`,
);
@@ -48,7 +54,7 @@ export async function fetchBrowserJson<T>(
const timeoutMs = init?.timeoutMs ?? 5000;
try {
if (isAbsoluteHttp(url)) {
return await fetchHttpJson<T>(url, init ? { ...init, timeoutMs } : { timeoutMs });
return await fetchHttpJson<T>(url, { ...init, timeoutMs });
}
const started = await startBrowserControlServiceFromConfig();
if (!started) {

View File

@@ -8,6 +8,7 @@ import { resolveGatewayPort } from "../config/paths.js";
import {
DEFAULT_CLAWD_BROWSER_COLOR,
DEFAULT_CLAWD_BROWSER_ENABLED,
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
DEFAULT_CLAWD_BROWSER_PROFILE_NAME,
} from "./constants.js";
@@ -15,6 +16,7 @@ import { CDP_PORT_RANGE_START, getUsedPorts } from "./profiles.js";
export type ResolvedBrowserConfig = {
enabled: boolean;
evaluateEnabled: boolean;
controlPort: number;
cdpProtocol: "http" | "https";
cdpHost: string;
@@ -140,6 +142,7 @@ export function resolveBrowserConfig(
rootConfig?: ClawdbotConfig,
): ResolvedBrowserConfig {
const enabled = cfg?.enabled ?? DEFAULT_CLAWD_BROWSER_ENABLED;
const evaluateEnabled = cfg?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED;
const gatewayPort = resolveGatewayPort(rootConfig);
const controlPort = deriveDefaultBrowserControlPort(gatewayPort ?? DEFAULT_BROWSER_CONTROL_PORT);
const defaultColor = normalizeHexColor(cfg?.color);
@@ -197,6 +200,7 @@ export function resolveBrowserConfig(
return {
enabled,
evaluateEnabled,
controlPort,
cdpProtocol,
cdpHost: cdpInfo.parsed.hostname,

View File

@@ -1,4 +1,5 @@
export const DEFAULT_CLAWD_BROWSER_ENABLED = true;
export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
export const DEFAULT_CLAWD_BROWSER_COLOR = "#FF4500";
export const DEFAULT_CLAWD_BROWSER_PROFILE_NAME = "clawd";
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";

View File

@@ -39,6 +39,7 @@ export function registerBrowserAgentActRoutes(
const cdpUrl = profileCtx.profile.cdpUrl;
const pw = await requirePwAi(res, `act:${kind}`);
if (!pw) return;
const evaluateEnabled = ctx.state().resolved.evaluateEnabled;
switch (kind) {
case "click": {
@@ -210,6 +211,16 @@ export function registerBrowserAgentActRoutes(
: undefined;
const fn = toStringOrEmpty(body.fn) || undefined;
const timeoutMs = toNumber(body.timeoutMs) ?? undefined;
if (fn && !evaluateEnabled) {
return jsonError(
res,
403,
[
"wait --fn is disabled by config (browser.evaluateEnabled=false).",
"Docs: /gateway/configuration#browser-clawd-managed-browser",
].join("\n"),
);
}
if (
timeMs === undefined &&
!text &&
@@ -240,6 +251,16 @@ export function registerBrowserAgentActRoutes(
return res.json({ ok: true, targetId: tab.targetId });
}
case "evaluate": {
if (!evaluateEnabled) {
return jsonError(
res,
403,
[
"act:evaluate is disabled by config (browser.evaluateEnabled=false).",
"Docs: /gateway/configuration#browser-clawd-managed-browser",
].join("\n"),
);
}
const fn = toStringOrEmpty(body.fn);
if (!fn) return jsonError(res, 400, "fn is required");
const ref = toStringOrEmpty(body.ref) || undefined;

View File

@@ -7,6 +7,7 @@ let testPort = 0;
let cdpBaseUrl = "";
let reachable = false;
let cfgAttachOnly = false;
let cfgEvaluateEnabled = true;
let createTargetId: string | null = null;
let prevGatewayPort: string | undefined;
@@ -89,6 +90,7 @@ vi.mock("../config/config.js", async (importOriginal) => {
loadConfig: () => ({
browser: {
enabled: true,
evaluateEnabled: cfgEvaluateEnabled,
color: "#FF4500",
attachOnly: cfgAttachOnly,
headless: true,
@@ -185,6 +187,7 @@ describe("browser control server", () => {
beforeEach(async () => {
reachable = false;
cfgAttachOnly = false;
cfgEvaluateEnabled = true;
createTargetId = null;
cdpMocks.createTargetViaCdp.mockImplementation(async () => {
@@ -349,6 +352,30 @@ describe("browser control server", () => {
slowTimeoutMs,
);
it(
"blocks act:evaluate when browser.evaluateEnabled=false",
async () => {
cfgEvaluateEnabled = false;
const base = await startServerAndBase();
const waitRes = (await postJson(`${base}/act`, {
kind: "wait",
fn: "() => window.ready === true",
})) as { error?: string };
expect(waitRes.error).toContain("browser.evaluateEnabled=false");
expect(pwMocks.waitForViaPlaywright).not.toHaveBeenCalled();
const res = (await postJson(`${base}/act`, {
kind: "evaluate",
fn: "() => 1",
})) as { error?: string };
expect(res.error).toContain("browser.evaluateEnabled=false");
expect(pwMocks.evaluateViaPlaywright).not.toHaveBeenCalled();
},
slowTimeoutMs,
);
it("agent contract: hooks + response + downloads + screenshot", async () => {
const base = await startServerAndBase();

View File

@@ -308,6 +308,8 @@ describe("backward compatibility (profile parameter)", () => {
testPort = await getFreePort();
_cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`;
prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT;
process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2);
vi.stubGlobal(
"fetch",
@@ -344,6 +346,11 @@ describe("backward compatibility (profile parameter)", () => {
afterEach(async () => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
if (prevGatewayPort === undefined) {
delete process.env.CLAWDBOT_GATEWAY_PORT;
} else {
process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort;
}
const { stopBrowserControlServer } = await import("./server.js");
await stopBrowserControlServer();
});

View File

@@ -285,6 +285,8 @@ describe("profile CRUD endpoints", () => {
testPort = await getFreePort();
_cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`;
prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT;
process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2);
vi.stubGlobal(
"fetch",
@@ -299,6 +301,11 @@ describe("profile CRUD endpoints", () => {
afterEach(async () => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
if (prevGatewayPort === undefined) {
delete process.env.CLAWDBOT_GATEWAY_PORT;
} else {
process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort;
}
const { stopBrowserControlServer } = await import("./server.js");
await stopBrowserControlServer();
});