openclaw/extensions/browser/plugin-registration.ts
Gary 981d67a703
fix(browser): remove hardcoded profile="user" suggestion from tool description (#102582)
* fix(browser): remove hardcoded profile="user" suggestion from browser tool description

The browser tool description hardcoded a suggestion to use profile="user"
for logged-in sessions, even when the operator has configured a different
default profile (e.g. browser.defaultProfile: "openclaw").

Replace with profile-agnostic guidance that references the configured
browser.defaultProfile setting instead of prescribing a specific value.

Fixes #102566

* test: update browser tool description assertion for profile-agnostic wording

* refactor(browser): unify tool profile guidance

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-09 12:32:56 +01:00

230 lines
7.7 KiB
TypeScript

/**
* Browser plugin registration helpers. This file keeps registration lazy while
* advertising Browser tools, services, node-host commands, and audits.
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import type { Duplex } from "node:stream";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import type {
AnyAgentTool,
OpenClawPluginApi,
OpenClawPluginNodeHostCommand,
OpenClawPluginSecurityAuditCollector,
OpenClawPluginService,
OpenClawPluginToolContext,
OpenClawPluginToolFactory,
} from "openclaw/plugin-sdk/plugin-entry";
import {
BROWSER_REQUEST_GATEWAY_METHOD,
BROWSER_REQUEST_GATEWAY_SCOPE,
} from "./src/browser-gateway-contract.js";
import { describeBrowserTool } from "./src/browser-tool-description.js";
import { BrowserToolSchema } from "./src/browser-tool.schema.js";
const EAGER_BROWSER_CONTROL_SERVICE_ENV = "OPENCLAW_EAGER_BROWSER_CONTROL_SERVER";
const loadBrowserRegistrationRuntimeModule = createLazyRuntimeModule(
() => import("./register.runtime.js"),
);
function isTruthyEnvValue(value: string | undefined): boolean {
return /^(?:1|true|yes|on)$/iu.test(value?.trim() ?? "");
}
function deriveChatTypeFromSessionKey(
sessionKey: string | undefined,
): "direct" | "group" | "channel" | undefined {
const tokens = new Set(sessionKey?.toLowerCase().split(":").filter(Boolean) ?? []);
if (tokens.has("group")) {
return "group";
}
if (tokens.has("channel")) {
return "channel";
}
if (tokens.has("direct") || tokens.has("dm")) {
return "direct";
}
return undefined;
}
const BROWSER_CLI_DESCRIPTOR = {
name: "browser",
description: "Manage OpenClaw's dedicated browser (Chrome/Chromium)",
hasSubcommands: true,
};
function createLazyBrowserTool(opts?: {
sandboxBridgeUrl?: string;
allowHostControl?: boolean;
agentSessionKey?: string;
agentDir?: string;
workspaceDir?: string;
activeModel?: {
provider?: string;
model?: string;
};
mediaScope?: {
sessionKey?: string;
channel?: string;
chatType?: string;
};
}): AnyAgentTool {
const targetDefault = opts?.sandboxBridgeUrl ? "sandbox" : "host";
const hostHint =
opts?.allowHostControl === false ? "Host target blocked by policy." : "Host target allowed.";
return {
label: "Browser",
name: "browser",
description: describeBrowserTool({ targetDefault, hostHint }),
parameters: BrowserToolSchema,
execute: async (toolCallId, args, signal, onUpdate) => {
const { createBrowserTool } = await loadBrowserRegistrationRuntimeModule();
const tool = createBrowserTool(opts);
return await tool.execute(toolCallId, args, signal, onUpdate);
},
};
}
function createBrowserToolOptions(ctx: OpenClawPluginToolContext): {
sandboxBridgeUrl?: string;
allowHostControl?: boolean;
agentSessionKey?: string;
agentDir?: string;
workspaceDir?: string;
activeModel?: {
provider?: string;
model?: string;
};
mediaScope?: {
sessionKey?: string;
channel?: string;
chatType?: string;
};
} {
const mediaChannel = ctx.deliveryContext?.channel ?? ctx.messageChannel;
const mediaChatType = deriveChatTypeFromSessionKey(ctx.sessionKey);
return {
...(ctx.browser?.sandboxBridgeUrl ? { sandboxBridgeUrl: ctx.browser.sandboxBridgeUrl } : {}),
...(ctx.browser?.allowHostControl !== undefined
? { allowHostControl: ctx.browser.allowHostControl }
: {}),
...(ctx.sessionKey ? { agentSessionKey: ctx.sessionKey } : {}),
...(ctx.agentDir ? { agentDir: ctx.agentDir } : {}),
...(ctx.workspaceDir ? { workspaceDir: ctx.workspaceDir } : {}),
...(ctx.activeModel?.provider || ctx.activeModel?.modelId
? {
activeModel: {
provider: ctx.activeModel.provider,
model: ctx.activeModel.modelId,
},
}
: {}),
...(ctx.sessionKey || mediaChannel
? {
mediaScope: {
...(ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {}),
...(mediaChannel ? { channel: mediaChannel } : {}),
...(mediaChatType ? { chatType: mediaChatType } : {}),
},
}
: {}),
};
}
/** Browser plugin reload policy. */
export const browserPluginReload = {
restartPrefixes: ["browser"],
hotPrefixes: ["browser.profiles"],
};
/** Node-host command descriptors exposed by the Browser plugin. */
export const browserPluginNodeHostCommands: OpenClawPluginNodeHostCommand[] = [
{
command: "browser.proxy",
cap: "browser",
handle: async (paramsJSON) => {
const { runBrowserProxyCommand } = await loadBrowserRegistrationRuntimeModule();
return await runBrowserProxyCommand(paramsJSON);
},
},
];
/** Security audit collectors contributed by the Browser plugin. */
export const browserSecurityAuditCollectors: OpenClawPluginSecurityAuditCollector[] = [
async (ctx) => {
const { collectBrowserSecurityAuditFindings } = await loadBrowserRegistrationRuntimeModule();
return collectBrowserSecurityAuditFindings(ctx);
},
];
function createLazyBrowserPluginService(): OpenClawPluginService {
let service: OpenClawPluginService | null = null;
const loadService = async () => {
if (!service) {
const { createBrowserPluginService } = await loadBrowserRegistrationRuntimeModule();
service = createBrowserPluginService();
}
return service;
};
return {
id: "browser-control",
start: async (ctx) => {
if (!isTruthyEnvValue(process.env[EAGER_BROWSER_CONTROL_SERVICE_ENV])) {
return;
}
const loaded = await loadService();
await loaded.start(ctx);
},
stop: async (ctx) => {
if (!service) {
const { stopBrowserControlService } = await import("./src/control-service.js");
await stopBrowserControlService().catch(() => {});
return;
}
await service.stop?.(ctx);
},
};
}
/** Register Browser tool factories, CLI, gateway methods, services, and audits. */
export function registerBrowserPlugin(api: OpenClawPluginApi) {
api.registerTool(((ctx: OpenClawPluginToolContext) =>
createLazyBrowserTool(createBrowserToolOptions(ctx))) as OpenClawPluginToolFactory);
api.registerCli(
async ({ program }) => {
const { registerBrowserCli } = await import("./src/cli/browser-cli.js");
registerBrowserCli(program, process.argv, api.rootDir);
},
{ commands: ["browser"], descriptors: [BROWSER_CLI_DESCRIPTOR] },
);
api.registerGatewayMethod(
BROWSER_REQUEST_GATEWAY_METHOD,
async (opts) => {
const { handleBrowserGatewayRequest } = await loadBrowserRegistrationRuntimeModule();
return await handleBrowserGatewayRequest(opts);
},
{
scope: BROWSER_REQUEST_GATEWAY_SCOPE,
},
);
// Remote extension relay: lets the Chrome extension connect directly to this
// gateway over wss:// (no node host on the browser machine). auth:"plugin"
// with no nodeCapability means the gateway does not pre-enforce token auth;
// the handler self-validates the host-local relay secret. Path kept in sync
// with GATEWAY_EXTENSION_RELAY_PATH (hardcoded here to stay lazy).
api.registerHttpRoute({
path: "/browser/extension",
auth: "plugin",
match: "exact",
handler: (_req: IncomingMessage, res: ServerResponse) => {
res.writeHead(426, { "Content-Type": "text/plain" });
res.end("Upgrade Required: connect the OpenClaw Chrome extension over WebSocket.");
},
handleUpgrade: async (req: IncomingMessage, socket: Duplex, head: Buffer) => {
const { handleGatewayExtensionUpgrade } =
await import("./src/browser/extension-relay/gateway-relay-route.js");
return await handleGatewayExtensionUpgrade(req, socket, head);
},
});
api.registerService(createLazyBrowserPluginService());
}