Files
openclaw/extensions/browser/src/plugin-service.ts
Peter Steinberger 302f262e6b refactor: deduplicate extension normalization primitives (#115650)
* refactor(plugins): reuse SDK normalization primitives

* fix(ci): repair code-mode matrix checks

* fix(ci): satisfy code-mode matrix gates

* fix(ci): use matrix evidence export

* fix(ci): validate matrix evidence artifact
2026-07-29 04:10:06 -04:00

69 lines
2.5 KiB
TypeScript

/**
* Browser plugin service factory that lazily starts the control server.
*/
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
import {
startLazyPluginServiceModule,
type LazyPluginServiceHandle,
type OpenClawPluginService,
} from "./sdk-node-runtime.js";
type BrowserControlHandle = LazyPluginServiceHandle | null;
const EAGER_BROWSER_CONTROL_SERVICE_ENV = "OPENCLAW_EAGER_BROWSER_CONTROL_SERVER";
const UNSAFE_BROWSER_CONTROL_OVERRIDE_SPECIFIER = /^(?:data|http|https|node):/i;
function validateBrowserControlOverrideSpecifier(specifier: string): string {
const trimmed = specifier.trim();
if (UNSAFE_BROWSER_CONTROL_OVERRIDE_SPECIFIER.test(trimmed)) {
throw new Error(`Refusing unsafe browser control override specifier: ${trimmed}`);
}
return trimmed;
}
/** Creates the Browser plugin service registered by the plugin entrypoint. */
export function createBrowserPluginService(): OpenClawPluginService {
let handle: BrowserControlHandle = null;
return {
id: "browser-control",
start: async () => {
const pageShare = await import("./browser/extension-relay/page-share.js");
// Plugin services start only in the Gateway process. The sink marks this
// process as able to deliver page shares to the main session.
pageShare.setPageShareSink(pageShare.createGatewayPageShareSink());
if (!isTruthyEnvValue(process.env[EAGER_BROWSER_CONTROL_SERVICE_ENV])) {
return;
}
if (handle) {
return;
}
handle = await startLazyPluginServiceModule({
skipEnvVar: "OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
overrideEnvVar: "OPENCLAW_BROWSER_CONTROL_MODULE",
validateOverrideSpecifier: validateBrowserControlOverrideSpecifier,
// Keep the default module import static so compiled builds still bundle it.
loadDefaultModule: async () => await import("./server.js"),
startExportNames: [
"startBrowserControlServiceFromConfig",
"startBrowserControlServerFromConfig",
],
stopExportNames: ["stopBrowserControlService", "stopBrowserControlServer"],
});
},
stop: async () => {
const { setPageShareSink } = await import("./browser/extension-relay/page-share.js");
setPageShareSink(null);
const current = handle;
if (current) {
await current.stop();
if (handle === current) {
handle = null;
}
return;
}
const { stopBrowserControlService } = await import("./control-service.js");
await stopBrowserControlService();
},
};
}