mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 04:01:05 +00:00
refactor: move browser runtime seams behind plugin metadata
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
79
src/node-host/plugin-node-host.test.ts
Normal file
79
src/node-host/plugin-node-host.test.ts
Normal 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}');
|
||||
});
|
||||
});
|
||||
56
src/node-host/plugin-node-host.ts
Normal file
56
src/node-host/plugin-node-host.ts
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user