docs: document browser tool runtime

This commit is contained in:
Peter Steinberger
2026-06-04 07:24:31 -04:00
parent 83e4cfba30
commit a982f798ca
7 changed files with 63 additions and 0 deletions

View File

@@ -1,3 +1,9 @@
/**
* Shared in-process browser control runtime state.
*
* The HTTP server path and background control service both reuse this singleton
* so local tools can attach to the same browser runtime without racing owners.
*/
import type { Server } from "node:http";
import { createBrowserRuntimeState, stopBrowserRuntime } from "./browser/runtime-lifecycle.js";
import { type BrowserServerState, createBrowserRouteContext } from "./browser/server-context.js";
@@ -11,6 +17,7 @@ export function getBrowserControlState(): BrowserServerState | null {
return state;
}
/** Create a route context bound to the current shared browser runtime. */
export function createBrowserControlContext() {
return createBrowserRouteContext({
getState: () => state,
@@ -18,6 +25,7 @@ export function createBrowserControlContext() {
});
}
/** Start or attach the shared browser runtime for either the server or service owner. */
export async function ensureBrowserControlRuntime(params: {
server?: Server | null;
port: number;
@@ -27,6 +35,8 @@ export async function ensureBrowserControlRuntime(params: {
}): Promise<BrowserServerState> {
if (state) {
if (params.server) {
// A foreground server takes ownership of the already-started service
// runtime so shutdown and port reporting follow the visible server.
state.server = params.server;
state.port = params.port;
state.resolved = { ...params.resolved, controlPort: params.port };
@@ -45,6 +55,7 @@ export async function ensureBrowserControlRuntime(params: {
return state;
}
/** Stop the shared browser runtime when the requesting owner is allowed to do so. */
export async function stopBrowserControlRuntime(params: {
requestedBy: BrowserControlOwner;
closeServer?: boolean;
@@ -55,6 +66,8 @@ export async function stopBrowserControlRuntime(params: {
return;
}
if (params.requestedBy === "service" && current.server && owner === "server") {
// The background service must not close a runtime currently claimed by the
// visible HTTP server; otherwise CLI/browser calls lose their control port.
return;
}
await stopBrowserRuntime({

View File

@@ -1,3 +1,11 @@
/**
* Gateway method and scope constants for browser proxy requests.
*
* Node-hosted browser control uses these values on both sides of the gateway
* contract, so keep them as literal exports instead of duplicated strings.
*/
export const BROWSER_REQUEST_GATEWAY_METHOD = "browser.request" as const;
/** Admin scope required to proxy browser-control requests through Gateway. */
export const BROWSER_REQUEST_GATEWAY_SCOPE = "operator.admin" as const;
/** Scope tuple shape consumed by Gateway tool registration. */
export const BROWSER_REQUEST_GATEWAY_SCOPES = [BROWSER_REQUEST_GATEWAY_SCOPE] as const;

View File

@@ -1,3 +1,9 @@
/**
* Public browser runtime barrel.
*
* Exposes the browser control server, client helpers, config resolution, and
* route/runtime primitives used by the plugin entrypoints and local CLI.
*/
export { startBrowserBridgeServer, stopBrowserBridgeServer } from "./browser/bridge-server.js";
export type { BrowserBridge } from "./browser/bridge-server.js";
export {

View File

@@ -1,3 +1,9 @@
/**
* Browser agent tool action executors.
*
* Converts model-facing parameters into browser control client calls and wraps
* browser-originated text as untrusted content before returning it to agents.
*/
import type { AgentToolResult } from "openclaw/plugin-sdk/agent-core";
import {
readNonNegativeIntegerParam,
@@ -102,6 +108,8 @@ function withConfiguredActTimeout(
return request;
}
if (existingSessionRejectsActTimeout(request) && usesExistingSessionProfile(profileName)) {
// Chrome MCP existing-session actions reject per-call timeouts for these
// operations, so default timeout injection must stay disabled there.
return request;
}
@@ -197,6 +205,8 @@ function wrapBrowserExternalJson(params: {
includeWarning?: boolean;
}): { wrappedText: string; safeDetails: Record<string, unknown> } {
const extractedText = JSON.stringify(params.payload, null, 2);
// Browser tabs, snapshots, and console output are page-controlled data. Keep
// text wrapped even when details carry the structured fields for callers.
const wrappedText = wrapExternalContent(extractedText, {
source: "browser",
includeWarning: params.includeWarning ?? true,
@@ -332,6 +342,7 @@ export async function executeTabsAction(params: {
return formatTabsToolResult(tabs);
}
/** Execute and format browser snapshots for agent consumption. */
export async function executeSnapshotAction(params: {
input: Record<string, unknown>;
baseUrl?: string;
@@ -380,6 +391,8 @@ export async function executeSnapshotAction(params: {
: hasMaxChars
? maxChars
: undefined;
// AI snapshots have a compact default cap; ARIA snapshots keep full structure
// unless maxChars is explicit, because agents often need complete node refs.
const snapshotTimeoutMs =
readPositiveIntegerParam(input, "timeoutMs", {
message: "timeoutMs must be a positive integer.",
@@ -520,6 +533,7 @@ export async function executeSnapshotAction(params: {
}
}
/** Execute browser console retrieval and wrap page-controlled messages. */
export async function executeConsoleAction(params: {
input: Record<string, unknown>;
baseUrl?: string;
@@ -549,6 +563,7 @@ export async function executeConsoleAction(params: {
return formatConsoleToolResult(result);
}
/** Execute browser actions with profile-aware timeout defaults and stale-tab recovery. */
export async function executeActAction(params: {
request: BrowserActRequest;
baseUrl?: string;

View File

@@ -1,6 +1,13 @@
/**
* Runtime dependency barrel for the Browser agent tool.
*
* Kept separate from browser-tool.ts so tests can mock the tool boundary while
* production still imports SDK helpers and browser client actions lazily.
*/
import { getRuntimeConfig } from "./sdk-config.js";
export { getRuntimeConfig };
/** Resolve global image downscaling for screenshots returned to agent tools. */
export function resolveRuntimeImageSanitization(): { maxDimensionPx: number } | undefined {
const configured = getRuntimeConfig().agents?.defaults?.imageMaxDimensionPx;
if (typeof configured !== "number" || !Number.isFinite(configured)) {

View File

@@ -1,3 +1,9 @@
/**
* JSON schema for the Browser agent tool.
*
* The schema stays intentionally flat because provider function-tool validators
* reject several nested union shapes that TypeBox can otherwise emit.
*/
import {
optionalFiniteNumberSchema,
optionalNonNegativeIntegerSchema,
@@ -99,6 +105,7 @@ const BrowserActSchema = Type.Object({
// IMPORTANT: OpenAI function tool schemas must have a top-level `type: "object"`.
// A root-level `Type.Union([...])` compiles to `{ anyOf: [...] }` (no `type`),
// which OpenAI rejects ("Invalid schema ... type: None"). Keep this schema an object.
/** Provider-compatible Browser tool argument schema. */
export const BrowserToolSchema = Type.Object({
action: stringEnum(BROWSER_TOOL_ACTIONS),
target: optionalStringEnum(BROWSER_TARGETS),

View File

@@ -1,3 +1,9 @@
/**
* Browser agent tool registration.
*
* Builds the model-facing browser tool, chooses sandbox/host/node routing, and
* maps high-level actions onto browser control client calls.
*/
import crypto from "node:crypto";
import {
executeActAction,
@@ -441,6 +447,7 @@ function readToolTimeoutMs(params: Record<string, unknown>) {
});
}
/** Create the Browser tool exposed to agents. */
export function createBrowserTool(opts?: {
sandboxBridgeUrl?: string;
allowHostControl?: boolean;