refactor: move browser runtime seams behind plugin metadata

This commit is contained in:
Peter Steinberger
2026-04-05 23:13:03 +01:00
parent 1351bacaa4
commit 471d056e2f
44 changed files with 1441 additions and 1026 deletions

View File

@@ -19,7 +19,6 @@ import {
type ExecHostResponse,
} from "../infra/exec-host.js";
import { sanitizeHostExecEnv } from "../infra/host-env-security.js";
import { runBrowserProxyCommand } from "../plugin-sdk/browser-node-host.js";
import { buildSystemRunApprovalPlan, handleSystemRunInvoke } from "./invoke-system-run.js";
import type {
ExecEventPayload,
@@ -28,6 +27,7 @@ import type {
SkillBinsProvider,
SystemRunParams,
} from "./invoke-types.js";
import { invokeRegisteredNodeHostCommand } from "./plugin-node-host.js";
const OUTPUT_CAP = 200_000;
const OUTPUT_EVENT_TAIL = 20_000;
@@ -480,13 +480,14 @@ export async function handleInvoke(
return;
}
if (command === "browser.proxy") {
try {
const payload = await runBrowserProxyCommand(frame.paramsJSON);
await sendRawPayloadResult(client, frame, payload);
} catch (err) {
await sendInvalidRequestResult(client, frame, err);
try {
const pluginNodeHostResult = await invokeRegisteredNodeHostCommand(command, frame.paramsJSON);
if (pluginNodeHostResult !== null) {
await sendRawPayloadResult(client, frame, pluginNodeHostResult);
return;
}
} catch (err) {
await sendInvalidRequestResult(client, frame, err);
return;
}

View File

@@ -0,0 +1,79 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
import {
invokeRegisteredNodeHostCommand,
listRegisteredNodeHostCapsAndCommands,
} from "./plugin-node-host.js";
afterEach(() => {
resetPluginRuntimeStateForTest();
});
describe("plugin node-host registry", () => {
it("lists plugin-declared caps and commands", () => {
const registry = createEmptyPluginRegistry();
registry.nodeHostCommands = [
{
pluginId: "browser",
pluginName: "Browser",
command: {
command: "browser.proxy",
cap: "browser",
handle: vi.fn(async () => "{}"),
},
source: "test",
},
{
pluginId: "photos",
pluginName: "Photos",
command: {
command: "photos.proxy",
cap: "photos",
handle: vi.fn(async () => "{}"),
},
source: "test",
},
{
pluginId: "browser-dup",
pluginName: "Browser Dup",
command: {
command: "browser.inspect",
cap: "browser",
handle: vi.fn(async () => "{}"),
},
source: "test",
},
];
setActivePluginRegistry(registry);
expect(listRegisteredNodeHostCapsAndCommands()).toEqual({
caps: ["browser", "photos"],
commands: ["browser.inspect", "browser.proxy", "photos.proxy"],
});
});
it("dispatches plugin-declared node-host commands", async () => {
const handle = vi.fn(async (paramsJSON?: string | null) => paramsJSON ?? "");
const registry = createEmptyPluginRegistry();
registry.nodeHostCommands = [
{
pluginId: "browser",
pluginName: "Browser",
command: {
command: "browser.proxy",
cap: "browser",
handle,
},
source: "test",
},
];
setActivePluginRegistry(registry);
await expect(invokeRegisteredNodeHostCommand("browser.proxy", '{"ok":true}')).resolves.toBe(
'{"ok":true}',
);
await expect(invokeRegisteredNodeHostCommand("missing.command", null)).resolves.toBeNull();
expect(handle).toHaveBeenCalledWith('{"ok":true}');
});
});

View File

@@ -0,0 +1,56 @@
import type { OpenClawConfig } from "../config/config.js";
import { getActivePluginRegistry } from "../plugins/runtime.js";
let pluginRegistryLoaderModulePromise:
| Promise<typeof import("../plugins/runtime/runtime-registry-loader.js")>
| undefined;
async function loadPluginRegistryLoaderModule() {
pluginRegistryLoaderModulePromise ??= import("../plugins/runtime/runtime-registry-loader.js");
return await pluginRegistryLoaderModulePromise;
}
export async function ensureNodeHostPluginRegistry(params: {
config: OpenClawConfig;
env?: NodeJS.ProcessEnv;
}): Promise<void> {
(await loadPluginRegistryLoaderModule()).ensurePluginRegistryLoaded({
scope: "all",
config: params.config,
activationSourceConfig: params.config,
env: params.env,
});
}
export function listRegisteredNodeHostCapsAndCommands(): {
caps: string[];
commands: string[];
} {
const registry = getActivePluginRegistry();
const caps = new Set<string>();
const commands = new Set<string>();
for (const entry of registry?.nodeHostCommands ?? []) {
if (entry.command.cap) {
caps.add(entry.command.cap);
}
commands.add(entry.command.command);
}
return {
caps: [...caps].toSorted((left, right) => left.localeCompare(right)),
commands: [...commands].toSorted((left, right) => left.localeCompare(right)),
};
}
export async function invokeRegisteredNodeHostCommand(
command: string,
paramsJSON?: string | null,
): Promise<string | null> {
const registry = getActivePluginRegistry();
const match = (registry?.nodeHostCommands ?? []).find(
(entry) => entry.command.command === command,
);
if (!match) {
return null;
}
return await match.command.handle(paramsJSON);
}

View File

@@ -5,13 +5,8 @@ import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
import type { SkillBinTrustEntry } from "../infra/exec-approvals.js";
import { resolveExecutableFromPathEnv } from "../infra/executable-path.js";
import { getMachineDisplayName } from "../infra/machine-name.js";
import {
NODE_BROWSER_PROXY_COMMAND,
NODE_EXEC_APPROVALS_COMMANDS,
NODE_SYSTEM_RUN_COMMANDS,
} from "../infra/node-commands.js";
import { NODE_EXEC_APPROVALS_COMMANDS, NODE_SYSTEM_RUN_COMMANDS } from "../infra/node-commands.js";
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
import { resolveBrowserConfig } from "../plugin-sdk/browser-profiles.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { VERSION } from "../version.js";
import { ensureNodeHostConfig, saveNodeHostConfig, type NodeHostGatewayConfig } from "./config.js";
@@ -21,6 +16,10 @@ import {
buildNodeInvokeResultParams,
handleInvoke,
} from "./invoke.js";
import {
ensureNodeHostPluginRegistry,
listRegisteredNodeHostCapsAndCommands,
} from "./plugin-node-host.js";
export { buildNodeInvokeResultParams };
@@ -164,9 +163,8 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise<void> {
await saveNodeHostConfig(config);
const cfg = loadConfig();
const resolvedBrowser = resolveBrowserConfig(cfg.browser, cfg);
const browserProxyEnabled =
cfg.nodeHost?.browserProxy?.enabled !== false && resolvedBrowser.enabled;
await ensureNodeHostPluginRegistry({ config: cfg, env: process.env });
const pluginNodeHost = listRegisteredNodeHostCapsAndCommands();
const { token, password } = await resolveNodeHostGatewayCredentials({
config: cfg,
env: process.env,
@@ -190,11 +188,11 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise<void> {
mode: GATEWAY_CLIENT_MODES.NODE,
role: "node",
scopes: [],
caps: ["system", ...(browserProxyEnabled ? ["browser"] : [])],
caps: ["system", ...pluginNodeHost.caps],
commands: [
...NODE_SYSTEM_RUN_COMMANDS,
...NODE_EXEC_APPROVALS_COMMANDS,
...(browserProxyEnabled ? [NODE_BROWSER_PROXY_COMMAND] : []),
...pluginNodeHost.commands,
],
pathEnv,
permissions: undefined,