feat(browser): configure local startup timeouts

This commit is contained in:
Peter Steinberger
2026-04-25 12:29:47 +01:00
parent 4ac6729d12
commit b2b898c2a8
27 changed files with 231 additions and 14 deletions

View File

@@ -6,6 +6,9 @@ Docs: https://docs.openclaw.ai
### Changes
- Browser/config: allow local managed Chrome launch discovery and post-launch
CDP readiness timeouts to be raised for slower hosts such as Raspberry Pi.
Fixes #66803. Thanks @beat843796.
- Browser/CLI: add `openclaw browser start --headless` as a one-shot local managed browser launch override without rewriting persisted browser config. Thanks @BenediktSchackenberg.
- CLI/Crestodian: open interactive Crestodian in the full OpenClaw TUI shell instead of a basic readline prompt.
- CLI/Crestodian: shorten the startup greeting to the active planner/model, config state, Gateway probe result, and next debug action instead of dumping every discovered backend.

View File

@@ -266,6 +266,10 @@ See [Plugins](/tools/plugin).
- Local managed profiles can set `executablePath` to override the global
`browser.executablePath` for that profile. Use this to run one profile in
Chrome and another in Brave.
- Local managed profiles use `browser.localLaunchTimeoutMs` for Chrome CDP HTTP
discovery after process start and `browser.localCdpReadyTimeoutMs` for
post-launch CDP websocket readiness. Raise them on slower hosts where Chrome
starts successfully but readiness checks race startup.
- Auto-detect order: default browser if Chromium-based → Chrome → Brave → Edge → Chromium → Chrome Canary.
- `browser.executablePath` accepts `~` for your OS home directory.
- Control service: loopback only (port derived from `gateway.port`, default `18791`).

View File

@@ -125,15 +125,23 @@ curl -s http://127.0.0.1:18791/tabs
### Config Reference
| Option | Description | Default |
| --------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------- |
| `browser.enabled` | Enable browser control | `true` |
| `browser.executablePath` | Path to a Chromium-based browser binary (Chrome/Brave/Edge/Chromium) | auto-detected (prefers default browser when Chromium-based) |
| `browser.headless` | Run without GUI | `false` |
| `OPENCLAW_BROWSER_HEADLESS` | Per-process override for local managed browser headless mode | unset |
| `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` |
| `browser.attachOnly` | Don't launch browser, only attach to existing | `false` |
| `browser.cdpPort` | Chrome DevTools Protocol port | `18800` |
| Option | Description | Default |
| -------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------- |
| `browser.enabled` | Enable browser control | `true` |
| `browser.executablePath` | Path to a Chromium-based browser binary (Chrome/Brave/Edge/Chromium) | auto-detected (prefers default browser when Chromium-based) |
| `browser.headless` | Run without GUI | `false` |
| `OPENCLAW_BROWSER_HEADLESS` | Per-process override for local managed browser headless mode | unset |
| `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` |
| `browser.attachOnly` | Don't launch browser, only attach to existing | `false` |
| `browser.cdpPort` | Chrome DevTools Protocol port | `18800` |
| `browser.localLaunchTimeoutMs` | Local managed Chrome discovery timeout | `15000` |
| `browser.localCdpReadyTimeoutMs` | Local managed post-launch CDP readiness timeout | `8000` |
On Raspberry Pi, older VPS hosts, or slow storage, raise
`browser.localLaunchTimeoutMs` when Chrome needs more time to expose its CDP HTTP
endpoint. Raise `browser.localCdpReadyTimeoutMs` when launch succeeds but
`openclaw browser start` still reports `not reachable after start`. Values are
capped at 120000 ms.
### Problem: "No Chrome tabs found for profile=\"user\""

View File

@@ -129,6 +129,8 @@ Browser settings live in `~/.openclaw/openclaw.json`.
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms)
remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms)
localLaunchTimeoutMs: 15000, // local managed Chrome discovery timeout (ms)
localCdpReadyTimeoutMs: 8000, // local managed post-launch CDP readiness timeout (ms)
actionTimeoutMs: 60000, // default browser act timeout (ms)
tabCleanup: {
enabled: true, // default: true
@@ -174,6 +176,11 @@ Browser settings live in `~/.openclaw/openclaw.json`.
- Control service binds to loopback on a port derived from `gateway.port` (default `18791` = gateway + 2). Overriding `gateway.port` or `OPENCLAW_GATEWAY_PORT` shifts the derived ports in the same family.
- Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl`; set those only for remote CDP. `cdpUrl` defaults to the managed local CDP port when unset.
- `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP HTTP reachability checks; `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket handshakes.
- `localLaunchTimeoutMs` is the budget for a locally launched managed Chrome
process to expose its CDP HTTP endpoint. `localCdpReadyTimeoutMs` is the
follow-up budget for CDP websocket readiness after the process is discovered.
Raise these on Raspberry Pi, low-end VPS, or older hardware where Chromium
starts slowly. Values are capped at 120000 ms.
- `actionTimeoutMs` is the default budget for browser `act` requests when the caller does not pass `timeoutMs`. The client transport adds a small slack window so long waits can finish instead of timing out at the HTTP boundary.
- `tabCleanup` is best-effort cleanup for tabs opened by primary-agent browser sessions. Subagent, cron, and ACP lifecycle cleanup still closes their explicit tracked tabs at session end; primary sessions keep active tabs reusable, then close idle or excess tracked tabs in the background.

View File

@@ -18,6 +18,8 @@ function buildResolvedConfig(): ResolvedBrowserConfig {
cdpIsLoopback: true,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
localLaunchTimeoutMs: 15_000,
localCdpReadyTimeoutMs: 8_000,
extraArgs: [],
color: DEFAULT_OPENCLAW_BROWSER_COLOR,
executablePath: undefined,

View File

@@ -1,3 +1,5 @@
import { DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS } from "./constants.js";
export const CDP_HTTP_REQUEST_TIMEOUT_MS = 1500;
export const CDP_WS_HANDSHAKE_TIMEOUT_MS = 5000;
export const CDP_JSON_NEW_TIMEOUT_MS = 1500;
@@ -8,7 +10,7 @@ export const CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS = 10_000;
export const CHROME_BOOTSTRAP_PREFS_POLL_MS = 100;
export const CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS = 5000;
export const CHROME_BOOTSTRAP_EXIT_POLL_MS = 50;
export const CHROME_LAUNCH_READY_WINDOW_MS = 15_000;
export const CHROME_LAUNCH_READY_WINDOW_MS = DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS;
export const CHROME_LAUNCH_READY_POLL_MS = 200;
export const CHROME_STOP_TIMEOUT_MS = 2500;
export const CHROME_STOP_PROBE_TIMEOUT_MS = 200;

View File

@@ -374,6 +374,8 @@ describe("chrome.ts internal", () => {
headless: true,
noSandbox: true,
extraArgs: [],
localLaunchTimeoutMs: 15_000,
localCdpReadyTimeoutMs: 8_000,
}) as unknown as ResolvedBrowserConfig;
it("rejects a remote profile before attempting to spawn", async () => {
@@ -544,7 +546,11 @@ describe("chrome.ts internal", () => {
try {
vi.spyOn(fs, "existsSync").mockImplementation((p) => {
const s = String(p);
if (s.includes("google-chrome")) {
if (
s.includes("Google Chrome") ||
s.includes("google-chrome") ||
s.includes("/usr/bin/chromium")
) {
return true;
}
if (s.endsWith("Local State") || s.endsWith("Preferences")) {
@@ -574,6 +580,41 @@ describe("chrome.ts internal", () => {
Object.defineProperty(process, "platform", { value: originalPlatform });
}
});
it("uses the configured local launch timeout while waiting for CDP discovery", async () => {
vi.useFakeTimers();
try {
const executablePath = path.join(tmpDir, "chrome");
await fsp.writeFile(executablePath, "");
const existsSync = fs.existsSync.bind(fs);
vi.spyOn(fs, "existsSync").mockImplementation((p) => {
const s = String(p);
if (s.endsWith("Local State") || s.endsWith("Preferences")) {
return true;
}
return existsSync(p);
});
const fakeProc = makeFakeProc();
spawnMock.mockReturnValue(fakeProc);
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ECONNREFUSED")));
const resolved = {
...makeResolved(),
executablePath,
localLaunchTimeoutMs: 1,
};
const profile = makeProfile(55556);
const rejection = expect(launchOpenClawChrome(resolved, profile)).rejects.toThrow(
/Failed to start Chrome CDP/,
);
await vi.advanceTimersByTimeAsync(10);
await rejection;
expect(fakeProc.kill).toHaveBeenCalledWith("SIGKILL");
} finally {
vi.useRealTimers();
}
});
});
describe("stopOpenClawChrome SIGKILL fallback", () => {

View File

@@ -768,6 +768,8 @@ describe("browser chrome launch args", () => {
evaluateEnabled: false,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
localLaunchTimeoutMs: 15_000,
localCdpReadyTimeoutMs: 8_000,
actionTimeoutMs: 60_000,
extraArgs: [],
color: "#FF4500",

View File

@@ -520,7 +520,8 @@ export async function launchOpenClawChrome(
proc.stderr?.on("data", onStderr);
try {
const readyDeadline = Date.now() + CHROME_LAUNCH_READY_WINDOW_MS;
const readyDeadline =
Date.now() + (resolved.localLaunchTimeoutMs ?? CHROME_LAUNCH_READY_WINDOW_MS);
while (Date.now() < readyDeadline) {
if (await isChromeReachable(profile.cdpUrl)) {
break;

View File

@@ -445,6 +445,35 @@ describe("browser config", () => {
});
});
describe("managed browser startup timeouts", () => {
it("uses defaults for local launch and post-launch readiness windows", () => {
const resolved = resolveBrowserConfig({});
expect(resolved.localLaunchTimeoutMs).toBe(15_000);
expect(resolved.localCdpReadyTimeoutMs).toBe(8_000);
});
it("accepts custom local startup timeout values", () => {
const resolved = resolveBrowserConfig({
localLaunchTimeoutMs: 45_000,
localCdpReadyTimeoutMs: 30_000,
});
expect(resolved.localLaunchTimeoutMs).toBe(45_000);
expect(resolved.localCdpReadyTimeoutMs).toBe(30_000);
});
it("clamps oversized local startup timeout values", () => {
const resolved = resolveBrowserConfig({
localLaunchTimeoutMs: 999_999,
localCdpReadyTimeoutMs: 999_999,
});
expect(resolved.localLaunchTimeoutMs).toBe(120_000);
expect(resolved.localCdpReadyTimeoutMs).toBe(120_000);
});
});
it("inherits executablePath from global browser config when profile override is not set", () => {
const resolved = resolveBrowserConfig({
executablePath: "~/bin/chrome-global",

View File

@@ -24,6 +24,8 @@ import {
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS,
DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS,
DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES,
DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION,
DEFAULT_BROWSER_TAB_CLEANUP_SWEEP_MINUTES,
@@ -39,6 +41,8 @@ export {
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS,
DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS,
DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_ENABLED,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
@@ -69,6 +73,8 @@ export type ResolvedBrowserConfig = {
cdpIsLoopback: boolean;
remoteCdpTimeoutMs: number;
remoteCdpHandshakeTimeoutMs: number;
localLaunchTimeoutMs: number;
localCdpReadyTimeoutMs: number;
actionTimeoutMs: number;
color: string;
executablePath?: string;
@@ -106,6 +112,7 @@ export type ResolvedBrowserProfile = {
};
const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800;
const MAX_BROWSER_STARTUP_TIMEOUT_MS = 120_000;
export const OPENCLAW_BROWSER_HEADLESS_ENV = "OPENCLAW_BROWSER_HEADLESS";
export type ManagedBrowserHeadlessSource =
@@ -144,6 +151,14 @@ function normalizeTimeoutMs(raw: number | undefined, fallback: number): number {
return value < 0 ? fallback : value;
}
function normalizeStartupTimeoutMs(raw: number | undefined, fallback: number): number {
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
if (value <= 0) {
return fallback;
}
return Math.min(value, MAX_BROWSER_STARTUP_TIMEOUT_MS);
}
function normalizeNonNegativeInteger(raw: number | undefined, fallback: number): number {
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
return value < 0 ? fallback : value;
@@ -297,6 +312,14 @@ export function resolveBrowserConfig(
cfg?.remoteCdpHandshakeTimeoutMs,
Math.max(2000, remoteCdpTimeoutMs * 2),
);
const localLaunchTimeoutMs = normalizeStartupTimeoutMs(
cfg?.localLaunchTimeoutMs,
DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS,
);
const localCdpReadyTimeoutMs = normalizeStartupTimeoutMs(
cfg?.localCdpReadyTimeoutMs,
DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS,
);
const actionTimeoutMs = normalizeTimeoutMs(
cfg?.actionTimeoutMs,
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
@@ -382,6 +405,8 @@ export function resolveBrowserConfig(
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
remoteCdpTimeoutMs,
remoteCdpHandshakeTimeoutMs,
localLaunchTimeoutMs,
localCdpReadyTimeoutMs,
actionTimeoutMs,
color: defaultColor,
executablePath,

View File

@@ -4,6 +4,8 @@ export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw";
export const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 60_000;
export const DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS = 15_000;
export const DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS = 8_000;
export const DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS = 20_000;
export const DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES = 120;
export const DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION = 8;

View File

@@ -191,7 +191,8 @@ export function createProfileAvailability({
const waitForCdpReadyAfterLaunch = async (): Promise<void> => {
// launchOpenClawChrome() can return before Chrome is fully ready to serve /json/version + CDP WS.
// If a follow-up call races ahead, we can hit PortInUseError trying to launch again on the same port.
const deadlineMs = Date.now() + CDP_READY_AFTER_LAUNCH_WINDOW_MS;
const deadlineMs =
Date.now() + (state().resolved.localCdpReadyTimeoutMs ?? CDP_READY_AFTER_LAUNCH_WINDOW_MS);
while (Date.now() < deadlineMs) {
const remainingMs = Math.max(0, deadlineMs - Date.now());
// Keep each attempt short; loopback profiles derive a WS timeout from this value.

View File

@@ -1,9 +1,11 @@
import { DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS } from "./constants.js";
export const MANAGED_BROWSER_PAGE_TAB_LIMIT = 8;
export const OPEN_TAB_DISCOVERY_WINDOW_MS = 2000;
export const OPEN_TAB_DISCOVERY_POLL_MS = 100;
export const CDP_READY_AFTER_LAUNCH_WINDOW_MS = 8000;
export const CDP_READY_AFTER_LAUNCH_WINDOW_MS = DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS;
export const CDP_READY_AFTER_LAUNCH_POLL_MS = 100;
export const CDP_READY_AFTER_LAUNCH_MIN_TIMEOUT_MS = 75;
export const CDP_READY_AFTER_LAUNCH_MAX_TIMEOUT_MS = 250;

View File

@@ -90,6 +90,22 @@ describe("browser server-context ensureBrowserAvailable", () => {
expect(stopOpenClawChrome).toHaveBeenCalledTimes(1);
});
it("uses configured local CDP readiness timeout after launching", async () => {
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile, state } =
setupEnsureBrowserAvailableHarness();
state.resolved.localCdpReadyTimeoutMs = 250;
isChromeCdpReady.mockResolvedValue(false);
mockLaunchedChrome(launchOpenClawChrome, 322);
const promise = profile.ensureBrowserAvailable();
const rejected = expect(promise).rejects.toThrow("not reachable after start");
await vi.advanceTimersByTimeAsync(300);
await rejected;
expect(launchOpenClawChrome).toHaveBeenCalledTimes(1);
expect(stopOpenClawChrome).toHaveBeenCalledTimes(1);
});
it("deduplicates concurrent lazy-start calls to prevent PortInUseError", async () => {
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile } =
setupEnsureBrowserAvailableHarness();

View File

@@ -44,6 +44,8 @@ function makeState(): BrowserServerState {
cdpIsLoopback: true,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
localLaunchTimeoutMs: 15_000,
localCdpReadyTimeoutMs: 8_000,
actionTimeoutMs: 60_000,
color: "#FF4500",
headless: false,

View File

@@ -24,6 +24,8 @@ export function makeState(
cdpIsLoopback: profile !== "remote",
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
localLaunchTimeoutMs: 15_000,
localCdpReadyTimeoutMs: 8_000,
actionTimeoutMs: 60_000,
evaluateEnabled: false,
extraArgs: [],

View File

@@ -37,6 +37,8 @@ export function makeBrowserServerState(params?: {
evaluateEnabled: false,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
localLaunchTimeoutMs: 15_000,
localCdpReadyTimeoutMs: 8_000,
actionTimeoutMs: 60_000,
extraArgs: [],
color: profile.color,

View File

@@ -245,6 +245,8 @@ describe("ensureSandboxBrowser create args", () => {
cdpPortRangeEnd: 18899,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
localLaunchTimeoutMs: 15_000,
localCdpReadyTimeoutMs: 8_000,
color: "#FF4500",
headless: false,
noSandbox: false,

View File

@@ -97,6 +97,8 @@ function buildSandboxBrowserResolvedConfig(params: {
cdpPortRangeEnd: cdpPortRange.end,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
localLaunchTimeoutMs: 15_000,
localCdpReadyTimeoutMs: 8_000,
actionTimeoutMs: DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
color: DEFAULT_OPENCLAW_BROWSER_COLOR,
executablePath: undefined,

View File

@@ -215,6 +215,28 @@ describe("config schema regressions", () => {
expect(res.ok).toBe(true);
});
it("accepts browser local startup timeout settings", () => {
const res = validateConfigObject({
browser: {
localLaunchTimeoutMs: 45_000,
localCdpReadyTimeoutMs: 30_000,
},
});
expect(res.ok).toBe(true);
});
it("rejects out-of-range browser local startup timeout settings", () => {
const res = validateConfigObject({
browser: {
localLaunchTimeoutMs: 120_001,
localCdpReadyTimeoutMs: 0,
},
});
expect(res.ok).toBe(false);
});
it("rejects browser.extraArgs with non-array value", () => {
const res = validateConfigObject({
browser: {

View File

@@ -634,6 +634,22 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
description:
"Timeout in milliseconds for post-connect CDP handshake readiness checks against remote browser targets. Raise this for slow-start remote browsers and lower to fail fast in automation loops.",
},
localLaunchTimeoutMs: {
type: "integer",
exclusiveMinimum: 0,
maximum: 120000,
title: "Browser Local Launch Timeout (ms)",
description:
"Timeout in milliseconds for locally launched managed Chrome to expose its CDP HTTP endpoint after process start. Raise this on slow single-board computers or older hosts.",
},
localCdpReadyTimeoutMs: {
type: "integer",
exclusiveMinimum: 0,
maximum: 120000,
title: "Browser Local CDP Ready Timeout (ms)",
description:
"Timeout in milliseconds for a locally launched managed browser to finish CDP websocket readiness after the process is discovered. Raise this when Chrome starts but browser start still reports CDP not reachable.",
},
actionTimeoutMs: {
type: "integer",
exclusiveMinimum: 0,
@@ -23977,6 +23993,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
help: "Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.",
tags: ["performance"],
},
"browser.localLaunchTimeoutMs": {
label: "Browser Local Launch Timeout (ms)",
help: "Timeout in milliseconds for locally launched managed Chrome to expose its CDP HTTP endpoint after process start. Raise this on slow single-board computers or older hosts.",
tags: ["performance"],
},
"browser.localCdpReadyTimeoutMs": {
label: "Browser Local CDP Ready Timeout (ms)",
help: "Timeout in milliseconds for a locally launched managed browser to finish CDP websocket readiness after the process is discovered. Raise this when Chrome starts but browser start still reports CDP not reachable.",
tags: ["performance"],
},
"browser.color": {
label: "Browser Accent Color",
help: "Default accent color used for browser profile/UI cues where colored identity hints are displayed. Use consistent colors to help operators identify active browser profile context quickly.",

View File

@@ -260,6 +260,10 @@ export const FIELD_HELP: Record<string, string> = {
"Remote CDP websocket URL used to attach to an externally managed browser instance. Use this for centralized browser hosts and keep URL access restricted to trusted network paths.",
"browser.actionTimeoutMs":
"Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.",
"browser.localLaunchTimeoutMs":
"Timeout in milliseconds for locally launched managed Chrome to expose its CDP HTTP endpoint after process start. Raise this on slow single-board computers or older hosts.",
"browser.localCdpReadyTimeoutMs":
"Timeout in milliseconds for a locally launched managed browser to finish CDP websocket readiness after the process is discovered. Raise this when Chrome starts but browser start still reports CDP not reachable.",
"browser.color":
"Default accent color used for browser profile/UI cues where colored identity hints are displayed. Use consistent colors to help operators identify active browser profile context quickly.",
"browser.executablePath":

View File

@@ -141,6 +141,8 @@ export const FIELD_LABELS: Record<string, string> = {
"browser.enabled": "Browser Enabled",
"browser.cdpUrl": "Browser CDP URL",
"browser.actionTimeoutMs": "Browser Action Timeout (ms)",
"browser.localLaunchTimeoutMs": "Browser Local Launch Timeout (ms)",
"browser.localCdpReadyTimeoutMs": "Browser Local CDP Ready Timeout (ms)",
"browser.color": "Browser Accent Color",
"browser.executablePath": "Browser Executable Path",
"browser.headless": "Browser Headless Mode",

View File

@@ -54,6 +54,10 @@ export type BrowserConfig = {
remoteCdpTimeoutMs?: number;
/** Remote CDP WebSocket handshake timeout (ms). Default: max(remoteCdpTimeoutMs * 2, 2000). */
remoteCdpHandshakeTimeoutMs?: number;
/** Local managed browser launch discovery timeout (ms). Default: 15000. */
localLaunchTimeoutMs?: number;
/** Local managed browser post-launch CDP readiness timeout (ms). Default: 8000. */
localCdpReadyTimeoutMs?: number;
/** Default browser act timeout (ms). Default: 60000. */
actionTimeoutMs?: number;
/** Accent color for the openclaw browser profile (hex). Default: #FF4500 */

View File

@@ -396,6 +396,8 @@ export const OpenClawSchema = z
cdpUrl: z.string().optional(),
remoteCdpTimeoutMs: z.number().int().nonnegative().optional(),
remoteCdpHandshakeTimeoutMs: z.number().int().nonnegative().optional(),
localLaunchTimeoutMs: z.number().int().positive().max(120_000).optional(),
localCdpReadyTimeoutMs: z.number().int().positive().max(120_000).optional(),
actionTimeoutMs: z.number().int().positive().optional(),
color: z.string().optional(),
executablePath: z.string().optional(),

View File

@@ -31,6 +31,8 @@ export type ResolvedBrowserConfig = {
cdpIsLoopback: boolean;
remoteCdpTimeoutMs: number;
remoteCdpHandshakeTimeoutMs: number;
localLaunchTimeoutMs: number;
localCdpReadyTimeoutMs: number;
actionTimeoutMs: number;
color: string;
executablePath?: string;