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

@@ -2768,6 +2768,7 @@ scheme/host for profiles that only set `cdpPort`.
Defaults: Defaults:
- enabled: `true` - enabled: `true`
- evaluateEnabled: `true` (set `false` to disable `act:evaluate` and `wait --fn`)
- control service: loopback only (port derived from `gateway.port`, default `18791`) - control service: loopback only (port derived from `gateway.port`, default `18791`)
- CDP URL: `http://127.0.0.1:18792` (control service + 1, legacy single-profile) - CDP URL: `http://127.0.0.1:18792` (control service + 1, legacy single-profile)
- profile color: `#FF4500` (lobster-orange) - profile color: `#FF4500` (lobster-orange)
@@ -2778,6 +2779,7 @@ Defaults:
{ {
browser: { browser: {
enabled: true, enabled: true,
evaluateEnabled: true,
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override // cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
defaultProfile: "chrome", defaultProfile: "chrome",
profiles: { profiles: {

View File

@@ -572,6 +572,9 @@ If that browser profile already contains logged-in sessions, the model can
access those accounts and data. Treat browser profiles as **sensitive state**: access those accounts and data. Treat browser profiles as **sensitive state**:
- Prefer a dedicated profile for the agent (the default `clawd` profile). - Prefer a dedicated profile for the agent (the default `clawd` profile).
- Avoid pointing the agent at your personal daily-driver profile. - Avoid pointing the agent at your personal daily-driver profile.
- `act:evaluate` and `wait --fn` run arbitrary JavaScript in the page context.
Prompt injection can steer the model into calling them. If you do not need
them, set `browser.evaluateEnabled=false` (see [Configuration](/gateway/configuration#browser-clawd-managed-browser)).
- Keep host browser control disabled for sandboxed agents unless you trust them. - Keep host browser control disabled for sandboxed agents unless you trust them.
- Treat browser downloads as untrusted input; prefer an isolated downloads directory. - Treat browser downloads as untrusted input; prefer an isolated downloads directory.
- Disable browser sync/password managers in the agent profile if possible (reduces blast radius). - Disable browser sync/password managers in the agent profile if possible (reduces blast radius).

View File

@@ -505,6 +505,9 @@ These are useful for “make the site behave like X” workflows:
## Security & privacy ## Security & privacy
- The clawd browser profile may contain logged-in sessions; treat it as sensitive. - The clawd browser profile may contain logged-in sessions; treat it as sensitive.
- `browser act kind=evaluate` / `clawdbot browser evaluate` and `wait --fn`
execute arbitrary JavaScript in the page context. Prompt injection can steer
this. Disable it with `browser.evaluateEnabled=false` if you do not need it.
- For logins and anti-bot notes (X/Twitter, etc.), see [Browser login + X/Twitter posting](/tools/browser-login). - For logins and anti-bot notes (X/Twitter, etc.), see [Browser login + X/Twitter posting](/tools/browser-login).
- Keep the Gateway/node host private (loopback or tailnet-only). - Keep the Gateway/node host private (loopback or tailnet-only).
- Remote CDP endpoints are powerful; tunnel and protect them. - Remote CDP endpoints are powerful; tunnel and protect them.

View File

@@ -20,8 +20,10 @@ describe("gateway tool", () => {
vi.useFakeTimers(); vi.useFakeTimers();
const kill = vi.spyOn(process, "kill").mockImplementation(() => true); const kill = vi.spyOn(process, "kill").mockImplementation(() => true);
const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousProfile = process.env.CLAWDBOT_PROFILE;
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-test-")); const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-test-"));
process.env.CLAWDBOT_STATE_DIR = stateDir; process.env.CLAWDBOT_STATE_DIR = stateDir;
process.env.CLAWDBOT_PROFILE = "isolated";
try { try {
const tool = createClawdbotTools({ const tool = createClawdbotTools({
@@ -62,6 +64,11 @@ describe("gateway tool", () => {
} else { } else {
process.env.CLAWDBOT_STATE_DIR = previousStateDir; process.env.CLAWDBOT_STATE_DIR = previousStateDir;
} }
if (previousProfile === undefined) {
delete process.env.CLAWDBOT_PROFILE;
} else {
process.env.CLAWDBOT_PROFILE = previousProfile;
}
} }
}); });

View File

@@ -1,6 +1,9 @@
import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../../browser/bridge-server.js"; import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../../browser/bridge-server.js";
import { type ResolvedBrowserConfig, resolveProfile } from "../../browser/config.js"; import { type ResolvedBrowserConfig, resolveProfile } from "../../browser/config.js";
import { DEFAULT_CLAWD_BROWSER_COLOR } from "../../browser/constants.js"; import {
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_CLAWD_BROWSER_COLOR,
} from "../../browser/constants.js";
import { BROWSER_BRIDGES } from "./browser-bridges.js"; import { BROWSER_BRIDGES } from "./browser-bridges.js";
import { DEFAULT_SANDBOX_BROWSER_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js"; import { DEFAULT_SANDBOX_BROWSER_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js";
import { import {
@@ -39,10 +42,12 @@ function buildSandboxBrowserResolvedConfig(params: {
controlPort: number; controlPort: number;
cdpPort: number; cdpPort: number;
headless: boolean; headless: boolean;
evaluateEnabled: boolean;
}): ResolvedBrowserConfig { }): ResolvedBrowserConfig {
const cdpHost = "127.0.0.1"; const cdpHost = "127.0.0.1";
return { return {
enabled: true, enabled: true,
evaluateEnabled: params.evaluateEnabled,
controlPort: params.controlPort, controlPort: params.controlPort,
cdpProtocol: "http", cdpProtocol: "http",
cdpHost, cdpHost,
@@ -76,6 +81,7 @@ export async function ensureSandboxBrowser(params: {
workspaceDir: string; workspaceDir: string;
agentWorkspaceDir: string; agentWorkspaceDir: string;
cfg: SandboxConfig; cfg: SandboxConfig;
evaluateEnabled?: boolean;
}): Promise<SandboxBrowserContext | null> { }): Promise<SandboxBrowserContext | null> {
if (!params.cfg.browser.enabled) return null; if (!params.cfg.browser.enabled) return null;
if (!isToolAllowed(params.cfg.tools, "browser")) return null; if (!isToolAllowed(params.cfg.tools, "browser")) return null;
@@ -170,6 +176,7 @@ export async function ensureSandboxBrowser(params: {
controlPort: 0, controlPort: 0,
cdpPort: mappedCdp, cdpPort: mappedCdp,
headless: params.cfg.browser.headless, headless: params.cfg.browser.headless,
evaluateEnabled: params.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED,
}), }),
onEnsureAttachTarget, onEnsureAttachTarget,
}); });

View File

@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { resolveUserPath } from "../../utils.js"; import { resolveUserPath } from "../../utils.js";
import { DEFAULT_BROWSER_EVALUATE_ENABLED } from "../../browser/constants.js";
import { syncSkillsToWorkspace } from "../skills.js"; import { syncSkillsToWorkspace } from "../skills.js";
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js"; import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js";
import { ensureSandboxBrowser } from "./browser.js"; import { ensureSandboxBrowser } from "./browser.js";
@@ -69,11 +70,14 @@ export async function resolveSandboxContext(params: {
cfg, cfg,
}); });
const evaluateEnabled =
params.config?.browser?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED;
const browser = await ensureSandboxBrowser({ const browser = await ensureSandboxBrowser({
scopeKey, scopeKey,
workspaceDir, workspaceDir,
agentWorkspaceDir, agentWorkspaceDir,
cfg, cfg,
evaluateEnabled,
}); });
return { return {

View File

@@ -6,6 +6,7 @@ import type { SkillEligibilityContext, SkillEntry } from "./types.js";
const DEFAULT_CONFIG_VALUES: Record<string, boolean> = { const DEFAULT_CONFIG_VALUES: Record<string, boolean> = {
"browser.enabled": true, "browser.enabled": true,
"browser.evaluateEnabled": true,
}; };
function isTruthy(value: unknown): boolean { function isTruthy(value: unknown): boolean {

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." ? "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.`; : `Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or \`${formatCliCommand("clawdbot gateway")}\`) and try again.`;
const msg = String(err); 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( return new Error(
`Can't reach the clawd browser control service (timed out after ${timeoutMs}ms). ${hint}`, `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; const timeoutMs = init?.timeoutMs ?? 5000;
try { try {
if (isAbsoluteHttp(url)) { if (isAbsoluteHttp(url)) {
return await fetchHttpJson<T>(url, init ? { ...init, timeoutMs } : { timeoutMs }); return await fetchHttpJson<T>(url, { ...init, timeoutMs });
} }
const started = await startBrowserControlServiceFromConfig(); const started = await startBrowserControlServiceFromConfig();
if (!started) { if (!started) {

View File

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

View File

@@ -1,4 +1,5 @@
export const DEFAULT_CLAWD_BROWSER_ENABLED = true; 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_COLOR = "#FF4500";
export const DEFAULT_CLAWD_BROWSER_PROFILE_NAME = "clawd"; export const DEFAULT_CLAWD_BROWSER_PROFILE_NAME = "clawd";
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome"; export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";

View File

@@ -39,6 +39,7 @@ export function registerBrowserAgentActRoutes(
const cdpUrl = profileCtx.profile.cdpUrl; const cdpUrl = profileCtx.profile.cdpUrl;
const pw = await requirePwAi(res, `act:${kind}`); const pw = await requirePwAi(res, `act:${kind}`);
if (!pw) return; if (!pw) return;
const evaluateEnabled = ctx.state().resolved.evaluateEnabled;
switch (kind) { switch (kind) {
case "click": { case "click": {
@@ -210,6 +211,16 @@ export function registerBrowserAgentActRoutes(
: undefined; : undefined;
const fn = toStringOrEmpty(body.fn) || undefined; const fn = toStringOrEmpty(body.fn) || undefined;
const timeoutMs = toNumber(body.timeoutMs) ?? 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 ( if (
timeMs === undefined && timeMs === undefined &&
!text && !text &&
@@ -240,6 +251,16 @@ export function registerBrowserAgentActRoutes(
return res.json({ ok: true, targetId: tab.targetId }); return res.json({ ok: true, targetId: tab.targetId });
} }
case "evaluate": { 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); const fn = toStringOrEmpty(body.fn);
if (!fn) return jsonError(res, 400, "fn is required"); if (!fn) return jsonError(res, 400, "fn is required");
const ref = toStringOrEmpty(body.ref) || undefined; const ref = toStringOrEmpty(body.ref) || undefined;

View File

@@ -7,6 +7,7 @@ let testPort = 0;
let cdpBaseUrl = ""; let cdpBaseUrl = "";
let reachable = false; let reachable = false;
let cfgAttachOnly = false; let cfgAttachOnly = false;
let cfgEvaluateEnabled = true;
let createTargetId: string | null = null; let createTargetId: string | null = null;
let prevGatewayPort: string | undefined; let prevGatewayPort: string | undefined;
@@ -89,6 +90,7 @@ vi.mock("../config/config.js", async (importOriginal) => {
loadConfig: () => ({ loadConfig: () => ({
browser: { browser: {
enabled: true, enabled: true,
evaluateEnabled: cfgEvaluateEnabled,
color: "#FF4500", color: "#FF4500",
attachOnly: cfgAttachOnly, attachOnly: cfgAttachOnly,
headless: true, headless: true,
@@ -185,6 +187,7 @@ describe("browser control server", () => {
beforeEach(async () => { beforeEach(async () => {
reachable = false; reachable = false;
cfgAttachOnly = false; cfgAttachOnly = false;
cfgEvaluateEnabled = true;
createTargetId = null; createTargetId = null;
cdpMocks.createTargetViaCdp.mockImplementation(async () => { cdpMocks.createTargetViaCdp.mockImplementation(async () => {
@@ -349,6 +352,30 @@ describe("browser control server", () => {
slowTimeoutMs, 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 () => { it("agent contract: hooks + response + downloads + screenshot", async () => {
const base = await startServerAndBase(); const base = await startServerAndBase();

View File

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

View File

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

View File

@@ -18,6 +18,33 @@ const configMocks = vi.hoisted(() => ({
})); }));
vi.mock("../config/config.js", () => configMocks); vi.mock("../config/config.js", () => configMocks);
const sharedMocks = vi.hoisted(() => ({
callBrowserRequest: vi.fn(
async (_opts: unknown, params: { path?: string; query?: Record<string, unknown> }) => {
const format = params.query?.format === "aria" ? "aria" : "ai";
if (format === "aria") {
return {
ok: true,
format: "aria",
targetId: "t1",
url: "https://example.com",
nodes: [],
};
}
return {
ok: true,
format: "ai",
targetId: "t1",
url: "https://example.com",
snapshot: "ok",
};
},
),
}));
vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: sharedMocks.callBrowserRequest,
}));
const runtime = { const runtime = {
log: vi.fn(), log: vi.fn(),
error: vi.fn(), error: vi.fn(),
@@ -44,13 +71,13 @@ describe("browser cli snapshot defaults", () => {
await program.parseAsync(["browser", "snapshot"], { from: "user" }); await program.parseAsync(["browser", "snapshot"], { from: "user" });
expect(clientMocks.browserSnapshot).toHaveBeenCalledWith( expect(sharedMocks.callBrowserRequest).toHaveBeenCalled();
"http://127.0.0.1:18791", const [, params] = sharedMocks.callBrowserRequest.mock.calls.at(-1) ?? [];
expect.objectContaining({ expect(params?.path).toBe("/snapshot");
format: "ai", expect(params?.query).toMatchObject({
mode: "efficient", format: "ai",
}), mode: "efficient",
); });
}); });
it("does not apply config snapshot defaults to aria snapshots", async () => { it("does not apply config snapshot defaults to aria snapshots", async () => {
@@ -71,8 +98,9 @@ describe("browser cli snapshot defaults", () => {
await program.parseAsync(["browser", "snapshot", "--format", "aria"], { from: "user" }); await program.parseAsync(["browser", "snapshot", "--format", "aria"], { from: "user" });
expect(clientMocks.browserSnapshot).toHaveBeenCalled(); expect(sharedMocks.callBrowserRequest).toHaveBeenCalled();
const [, opts] = clientMocks.browserSnapshot.mock.calls.at(-1) ?? []; const [, params] = sharedMocks.callBrowserRequest.mock.calls.at(-1) ?? [];
expect(opts?.mode).toBeUndefined(); expect(params?.path).toBe("/snapshot");
expect((params?.query as { mode?: unknown } | undefined)?.mode).toBeUndefined();
}); });
}); });

View File

@@ -279,6 +279,7 @@ const FIELD_LABELS: Record<string, string> = {
"ui.seamColor": "Accent Color", "ui.seamColor": "Accent Color",
"ui.assistant.name": "Assistant Name", "ui.assistant.name": "Assistant Name",
"ui.assistant.avatar": "Assistant Avatar", "ui.assistant.avatar": "Assistant Avatar",
"browser.evaluateEnabled": "Browser Evaluate Enabled",
"browser.snapshotDefaults": "Browser Snapshot Defaults", "browser.snapshotDefaults": "Browser Snapshot Defaults",
"browser.snapshotDefaults.mode": "Browser Snapshot Mode", "browser.snapshotDefaults.mode": "Browser Snapshot Mode",
"browser.remoteCdpTimeoutMs": "Remote CDP Timeout (ms)", "browser.remoteCdpTimeoutMs": "Remote CDP Timeout (ms)",

View File

@@ -14,6 +14,8 @@ export type BrowserSnapshotDefaults = {
}; };
export type BrowserConfig = { export type BrowserConfig = {
enabled?: boolean; enabled?: boolean;
/** If false, disable browser act:evaluate (arbitrary JS). Default: true */
evaluateEnabled?: boolean;
/** Base URL of the CDP endpoint (for remote browsers). Default: loopback CDP on the derived port. */ /** Base URL of the CDP endpoint (for remote browsers). Default: loopback CDP on the derived port. */
cdpUrl?: string; cdpUrl?: string;
/** Remote CDP HTTP timeout (ms). Default: 1500. */ /** Remote CDP HTTP timeout (ms). Default: 1500. */

View File

@@ -134,6 +134,7 @@ export const ClawdbotSchema = z
browser: z browser: z
.object({ .object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
evaluateEnabled: z.boolean().optional(),
cdpUrl: z.string().optional(), cdpUrl: z.string().optional(),
remoteCdpTimeoutMs: z.number().int().nonnegative().optional(), remoteCdpTimeoutMs: z.number().int().nonnegative().optional(),
remoteCdpHandshakeTimeoutMs: z.number().int().nonnegative().optional(), remoteCdpHandshakeTimeoutMs: z.number().int().nonnegative().optional(),

View File

@@ -6,6 +6,7 @@ import type { HookEligibilityContext, HookEntry } from "./types.js";
const DEFAULT_CONFIG_VALUES: Record<string, boolean> = { const DEFAULT_CONFIG_VALUES: Record<string, boolean> = {
"browser.enabled": true, "browser.enabled": true,
"browser.evaluateEnabled": true,
"workspace.dir": true, "workspace.dir": true,
}; };

View File

@@ -1,8 +1,23 @@
import { describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { buildPairingReply } from "./pairing-messages.js"; import { buildPairingReply } from "./pairing-messages.js";
describe("buildPairingReply", () => { describe("buildPairingReply", () => {
let previousProfile: string | undefined;
beforeEach(() => {
previousProfile = process.env.CLAWDBOT_PROFILE;
process.env.CLAWDBOT_PROFILE = "isolated";
});
afterEach(() => {
if (previousProfile === undefined) {
delete process.env.CLAWDBOT_PROFILE;
return;
}
process.env.CLAWDBOT_PROFILE = previousProfile;
});
const cases = [ const cases = [
{ {
channel: "discord", channel: "discord",