docs: document browser config paths

This commit is contained in:
Peter Steinberger
2026-06-04 07:42:57 -04:00
parent e8e57f9395
commit 3720ecaf52
11 changed files with 114 additions and 0 deletions

View File

@@ -1,3 +1,9 @@
/**
* Browser config resolution.
*
* Normalizes raw browser config into resolved runtime defaults, profile
* records, SSRF policy, timeouts, headless mode, and managed Chrome settings.
*/
import os from "node:os";
import path from "node:path";
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
@@ -54,6 +60,7 @@ type BrowserSsrFPolicyCompat = NonNullable<BrowserConfig["ssrfPolicy"]> & {
allowPrivateNetwork?: boolean;
};
/** Browser config after defaults, derived ports, and profile defaults are applied. */
export type ResolvedBrowserConfig = {
enabled: boolean;
evaluateEnabled: boolean;
@@ -81,6 +88,7 @@ export type ResolvedBrowserConfig = {
extraArgs: string[];
};
/** Normalized tab-cleanup settings for session-owned browser tabs. */
export type ResolvedBrowserTabCleanupConfig = {
enabled: boolean;
idleMinutes: number;
@@ -88,6 +96,7 @@ export type ResolvedBrowserTabCleanupConfig = {
sweepMinutes: number;
};
/** Runtime browser profile settings resolved from global and profile config. */
export type ResolvedBrowserProfile = {
name: string;
cdpPort: number;
@@ -107,8 +116,10 @@ export type ResolvedBrowserProfile = {
const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800;
const MAX_BROWSER_STARTUP_TIMEOUT_MS = 120_000;
/** Environment variable that overrides managed Chrome headless mode. */
export const OPENCLAW_BROWSER_HEADLESS_ENV = "OPENCLAW_BROWSER_HEADLESS";
/** Source that determined managed Chrome headless mode. */
export type ManagedBrowserHeadlessSource =
| "request"
| "env"
@@ -122,6 +133,7 @@ type ManagedBrowserHeadlessMode = {
source: ManagedBrowserHeadlessSource;
};
/** Inputs used to resolve managed Chrome headless mode. */
export type ManagedBrowserHeadlessOptions = {
headlessOverride?: boolean;
env?: NodeJS.ProcessEnv;
@@ -333,6 +345,7 @@ function ensureDefaultUserBrowserProfile(
return result;
}
/** Resolve raw browser config into runtime browser defaults. */
export function resolveBrowserConfig(
cfg: BrowserConfig | undefined,
rootConfig?: OpenClawConfig,
@@ -457,6 +470,7 @@ export function resolveBrowserConfig(
};
}
/** Resolve one configured browser profile by name. */
export function resolveProfile(
resolved: ResolvedBrowserConfig,
profileName: string,
@@ -544,6 +558,7 @@ export function resolveProfile(
};
}
/** Resolve effective headless mode for a managed browser profile. */
export function resolveManagedBrowserHeadlessMode(
resolved: ResolvedBrowserConfig,
profile: ResolvedBrowserProfile,
@@ -576,6 +591,7 @@ export function resolveManagedBrowserHeadlessMode(
return { headless: resolved.headless, source: "default" };
}
/** Return a Linux display error for headed managed Chrome when no display exists. */
export function getManagedBrowserMissingDisplayError(
resolved: ResolvedBrowserConfig,
profile: ResolvedBrowserProfile,
@@ -610,6 +626,7 @@ export function getManagedBrowserMissingDisplayError(
);
}
/** Return whether local browser control should start for a resolved config. */
export function shouldStartLocalBrowserServer(_resolved: unknown) {
return true;
}

View File

@@ -1,3 +1,9 @@
/**
* Local browser control dispatch bridge.
*
* Starts the browser control service when needed and dispatches requests
* through the in-process route dispatcher for local Browser tool calls.
*/
import {
createBrowserControlContext,
startBrowserControlServiceFromConfig,
@@ -8,6 +14,7 @@ import {
type BrowserDispatchResponse,
} from "./routes/dispatcher.js";
/** Dispatch one browser-control request through the local in-process router. */
export async function dispatchBrowserControlRequest(
req: BrowserDispatchRequest,
): Promise<BrowserDispatchResponse> {

View File

@@ -1,3 +1,9 @@
/**
* Browser navigation SSRF guard.
*
* Validates page navigation URLs and redirect chains before or after browser
* navigation while accounting for browser proxy routing.
*/
import { isIP } from "node:net";
import {
isPrivateNetworkAllowedByPolicy,
@@ -19,6 +25,7 @@ function normalizeNavigationUrl(url: string): string {
return url.trim();
}
/** Raised when a browser navigation URL fails syntax or policy validation. */
export class InvalidBrowserNavigationUrlError extends Error {
constructor(message: string) {
super(message);
@@ -26,18 +33,22 @@ export class InvalidBrowserNavigationUrlError extends Error {
}
}
/** Policy inputs applied to browser page navigation checks. */
export type BrowserNavigationPolicyOptions = {
ssrfPolicy?: SsrFPolicy;
browserProxyMode?: BrowserNavigationProxyMode;
};
/** Describes whether the browser itself is routing page traffic through a proxy. */
export type BrowserNavigationProxyMode = "direct" | "explicit-browser-proxy";
/** Minimal request shape used to walk browser redirect chains. */
export type BrowserNavigationRequestLike = {
url(): string;
redirectedFrom(): BrowserNavigationRequestLike | null;
};
/** Build a navigation-policy object while omitting default direct proxy mode. */
export function withBrowserNavigationPolicy(
ssrfPolicy?: SsrFPolicy,
opts?: { browserProxyMode?: BrowserNavigationProxyMode },
@@ -50,10 +61,12 @@ export function withBrowserNavigationPolicy(
};
}
/** Return true when strict policy requires redirect-chain inspection. */
export function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: SsrFPolicy): boolean {
return ssrfPolicy?.dangerouslyAllowPrivateNetwork === false;
}
/** Return true when a URL needs redirect inspection under strict policy. */
export function requiresInspectableBrowserNavigationRedirectsForUrl(
url: string,
ssrfPolicy?: SsrFPolicy,
@@ -87,6 +100,7 @@ function isExplicitlyAllowedBrowserHostname(hostname: string, ssrfPolicy?: SsrFP
: false;
}
/** Assert that a requested browser navigation URL is policy-allowed. */
export async function assertBrowserNavigationAllowed(
opts: {
url: string;
@@ -178,6 +192,7 @@ export async function assertBrowserNavigationResultAllowed(
}
}
/** Assert that every URL in a browser redirect chain is policy-allowed. */
export async function assertBrowserNavigationRedirectChainAllowed(
opts: {
request?: BrowserNavigationRequestLike | null;

View File

@@ -1,6 +1,13 @@
/**
* Atomic output write helper.
*
* Ensures browser-generated files are written through a sibling temp path under
* an allowed output root before becoming visible at the target path.
*/
import { writeExternalFileWithinRoot } from "../sdk-security-runtime.js";
import { ensureOutputDirectory } from "./output-directories.js";
/** Write a file inside an output root via a caller-provided temp writer. */
export async function writeViaSiblingTempPath(params: {
rootDir: string;
targetPath: string;

View File

@@ -1,3 +1,9 @@
/**
* Browser output directory helper.
*
* Creates absolute output directories while handling macOS system symlink
* aliases such as /tmp and /var safely.
*/
import fs from "node:fs/promises";
import path from "node:path";
import { ensureAbsoluteDirectory } from "../sdk-security-runtime.js";
@@ -22,6 +28,7 @@ async function resolveSystemDirectoryAlias(dirPath: string): Promise<string> {
return dirPath;
}
/** Ensure an absolute browser output directory exists and is safe to use. */
export async function ensureOutputDirectory(dirPath: string): Promise<void> {
const result = await ensureAbsoluteDirectory(
await resolveSystemDirectoryAlias(path.resolve(dirPath)),

View File

@@ -1,7 +1,14 @@
/**
* Browser output file writer.
*
* Validates caller-provided output paths against a root before writing
* screenshots, PDFs, downloads, or traces to disk.
*/
import path from "node:path";
import { writeExternalFileWithinRoot } from "../sdk-security-runtime.js";
import { ensureOutputDirectory } from "./output-directories.js";
/** Write a browser output file within a caller-selected output root. */
export async function writeExternalFileWithinOutputRoot(params: {
rootDir?: string;
path: string;

View File

@@ -1,3 +1,9 @@
/**
* Browser filesystem path helpers.
*
* Defines browser output roots and resolves upload/media references while
* enforcing root-scoped path access for Browser tool file inputs.
*/
import fs from "node:fs/promises";
import path from "node:path";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
@@ -35,9 +41,13 @@ function canUseNodeFs(): boolean {
const DEFAULT_BROWSER_TMP_DIR = canUseNodeFs()
? resolvePreferredOpenClawTmpDir()
: DEFAULT_FALLBACK_BROWSER_TMP_DIR;
/** Default root directory for browser trace files. */
export const DEFAULT_TRACE_DIR = DEFAULT_BROWSER_TMP_DIR;
/** Default root directory for browser downloads. */
export const DEFAULT_DOWNLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "downloads");
/** Default root directory for browser upload inputs. */
export const DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "uploads");
/** Default root directory for managed inbound media references. */
export const DEFAULT_INBOUND_MEDIA_DIR = path.join(CONFIG_DIR, "media", "inbound");
type ExistingPathsResult = Awaited<ReturnType<typeof resolveExistingPathsWithinRoot>>;
@@ -190,6 +200,7 @@ async function resolveDirectInboundMediaPath(params: {
return inboundPathsResult;
}
/** Resolve upload paths and managed media references into existing file paths. */
export async function resolveExistingUploadPaths({
requestedPaths,
uploadDir = DEFAULT_UPLOAD_DIR,
@@ -234,6 +245,7 @@ export async function resolveExistingUploadPaths({
return { ok: true, paths };
}
/** Strictly resolve upload paths under the upload root only. */
export async function resolveStrictExistingUploadPaths({
requestedPaths,
uploadDir = DEFAULT_UPLOAD_DIR,

View File

@@ -1,3 +1,9 @@
/**
* Browser profile capability resolution.
*
* Derives transport and driver capability flags used by routes and the Browser
* tool to choose CDP, Playwright, or Chrome MCP behavior.
*/
import type { ResolvedBrowserProfile } from "./config.js";
type BrowserProfileMode = "local-managed" | "local-existing-session" | "remote-cdp";
@@ -14,6 +20,7 @@ type BrowserProfileCapabilities = {
supportsManagedTabLimit: boolean;
};
/** Return feature capabilities for a resolved browser profile. */
export function getBrowserProfileCapabilities(
profile: ResolvedBrowserProfile,
): BrowserProfileCapabilities {
@@ -55,6 +62,7 @@ export function getBrowserProfileCapabilities(
};
}
/** Resolve the default snapshot format for a profile and available drivers. */
export function resolveDefaultSnapshotFormat(params: {
profile: ResolvedBrowserProfile;
hasPlaywright: boolean;
@@ -76,6 +84,7 @@ export function resolveDefaultSnapshotFormat(params: {
return params.hasPlaywright ? "ai" : "aria";
}
/** Return true when screenshots should use Playwright for the profile. */
export function shouldUsePlaywrightForScreenshot(params: {
profile: ResolvedBrowserProfile;
wsUrl?: string;
@@ -85,6 +94,7 @@ export function shouldUsePlaywrightForScreenshot(params: {
return !params.wsUrl || Boolean(params.ref) || Boolean(params.element);
}
/** Return true when ARIA snapshots should use Playwright for the profile. */
export function shouldUsePlaywrightForAriaSnapshot(params: {
profile: ResolvedBrowserProfile;
wsUrl?: string;

View File

@@ -1,3 +1,9 @@
/**
* Browser profile service.
*
* Implements profile listing, creation, and deletion using browser config
* mutation helpers and route context runtime state.
*/
import fs from "node:fs";
import path from "node:path";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -18,6 +24,7 @@ import { isValidProfileName } from "./profiles.js";
import type { BrowserRouteContext, ProfileStatus } from "./server-context.js";
import { movePathToTrash } from "./trash.js";
/** Input accepted when creating a browser profile. */
export type CreateProfileParams = {
name: string;
color?: string;
@@ -26,6 +33,7 @@ export type CreateProfileParams = {
driver?: "openclaw" | "existing-session";
};
/** Result returned after creating a browser profile. */
export type CreateProfileResult = {
ok: true;
profile: string;
@@ -37,6 +45,7 @@ export type CreateProfileResult = {
isRemote: boolean;
};
/** Result returned after deleting a browser profile. */
export type DeleteProfileResult = {
ok: true;
profile: string;
@@ -45,6 +54,7 @@ export type DeleteProfileResult = {
const HEX_COLOR_RE = /^#[0-9A-Fa-f]{6}$/;
/** Create a profile service bound to one browser route context. */
export function createBrowserProfilesService(ctx: BrowserRouteContext) {
const listProfiles = async (): Promise<ProfileStatus[]> => {
return await ctx.listProfiles();

View File

@@ -1,3 +1,9 @@
/**
* Browser profile allocation helpers.
*
* Validates profile names and allocates CDP ports/colors for newly persisted
* browser profiles.
*/
import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config";
/**
@@ -14,12 +20,15 @@ import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config";
* 18792-18799 - Reserved for future one-off services (canvas at 18793)
*/
/** Default first CDP port for browser profiles. */
export const CDP_PORT_RANGE_START = 18800;
/** Default last CDP port for browser profiles. */
export const CDP_PORT_RANGE_END = 18899;
const MAX_TCP_PORT = 65_535;
const PROFILE_NAME_REGEX = /^[a-z0-9][a-z0-9-]*$/;
/** Return true when a profile name matches the supported config key format. */
export function isValidProfileName(name: string): boolean {
if (!name || name.length > 64) {
return false;
@@ -27,6 +36,7 @@ export function isValidProfileName(name: string): boolean {
return PROFILE_NAME_REGEX.test(name);
}
/** Allocate the first unused CDP port in the configured range. */
export function allocateCdpPort(
usedPorts: Set<number>,
range?: { start: number; end: number },
@@ -51,6 +61,7 @@ function isValidTcpPort(port: number): boolean {
return Number.isSafeInteger(port) && port > 0 && port <= MAX_TCP_PORT;
}
/** Extract currently used CDP ports from profile config. */
export function getUsedPorts(
profiles: Record<string, { cdpPort?: number; cdpUrl?: string }> | undefined,
): Set<number> {
@@ -76,6 +87,7 @@ export function getUsedPorts(
return used;
}
/** Default browser profile color palette. */
export const PROFILE_COLORS = [
"#FF4500", // Orange-red (openclaw default)
"#0066CC", // Blue
@@ -89,6 +101,7 @@ export const PROFILE_COLORS = [
"#339966", // Teal
];
/** Allocate the first unused profile color, cycling when all are used. */
export function allocateColor(usedColors: Set<string>): string {
// Find first unused color from palette
for (const color of PROFILE_COLORS) {
@@ -101,6 +114,7 @@ export function allocateColor(usedColors: Set<string>): string {
return PROFILE_COLORS[index] ?? PROFILE_COLORS[0];
}
/** Extract currently used profile colors from profile config. */
export function getUsedColors(
profiles: Record<string, { color: string }> | undefined,
): Set<string> {

View File

@@ -1,3 +1,9 @@
/**
* Browser proxy file helpers.
*
* Persists files returned by node-hosted browser proxy calls and rewrites
* proxied result paths to local saved media paths.
*/
import { saveMediaBuffer } from "../media/store.js";
type BrowserProxyFile = {
@@ -6,6 +12,7 @@ type BrowserProxyFile = {
mimeType?: string;
};
/** Persist proxy-returned files and return a remote-path to local-path map. */
export async function persistBrowserProxyFiles(files: BrowserProxyFile[] | undefined) {
if (!files || files.length === 0) {
return new Map<string, string>();
@@ -19,6 +26,7 @@ export async function persistBrowserProxyFiles(files: BrowserProxyFile[] | undef
return mapping;
}
/** Rewrite result.path when it points at a persisted proxy file. */
export function applyBrowserProxyPaths(result: unknown, mapping: Map<string, string>) {
if (!result || typeof result !== "object") {
return;