fix(browser): gate evaluate behind config flag
This commit is contained in:
@@ -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: {
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
expect(params?.query).toMatchObject({
|
||||||
format: "ai",
|
format: "ai",
|
||||||
mode: "efficient",
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user