|
1 | 1 | // Gateway plugin tests cover plugin loading, auto-enable, runtime registry setup, |
2 | 2 | // request-scope injection, diagnostics, and handler dispatch integration. |
3 | 3 | import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; |
| 4 | +import { createPluginRecord } from "../plugins/loader-records.js"; |
4 | 5 | import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js"; |
5 | 6 | import type { PluginRegistry } from "../plugins/registry.js"; |
6 | 7 | import type { PluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; |
@@ -116,6 +117,30 @@ const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({ |
116 | 117 | diagnostics, |
117 | 118 | }); |
118 | 119 |
|
| 120 | +function addLoadedPlugin( |
| 121 | + registry: PluginRegistry, |
| 122 | + params: { |
| 123 | + id: string; |
| 124 | + origin?: PluginRegistry["plugins"][number]["origin"]; |
| 125 | + trustedOfficialInstall?: boolean; |
| 126 | + }, |
| 127 | +): PluginRegistry { |
| 128 | + registry.plugins.push( |
| 129 | + createPluginRecord({ |
| 130 | + id: params.id, |
| 131 | + name: params.id, |
| 132 | + source: `/tmp/${params.id}/index.js`, |
| 133 | + origin: params.origin ?? "bundled", |
| 134 | + enabled: true, |
| 135 | + configSchema: false, |
| 136 | + ...(params.trustedOfficialInstall !== undefined |
| 137 | + ? { trustedOfficialInstall: params.trustedOfficialInstall } |
| 138 | + : {}), |
| 139 | + }), |
| 140 | + ); |
| 141 | + return registry; |
| 142 | +} |
| 143 | + |
119 | 144 | function createLookUpTableForTest(params: { |
120 | 145 | manifestRegistry?: PluginLookUpTable["manifestRegistry"]; |
121 | 146 | pluginIds?: readonly string[]; |
@@ -899,6 +924,67 @@ describe("loadGatewayPlugins", () => { |
899 | 924 | expect(result.nodes).toEqual([{ nodeId: "connected", connected: true }]); |
900 | 925 | }); |
901 | 926 |
|
| 927 | + test("lets trusted official plugin runtime request admin scope for browser proxy", async () => { |
| 928 | + loadOpenClawPlugins.mockReturnValue(addLoadedPlugin(createRegistry([]), { id: "google-meet" })); |
| 929 | + loadGatewayStartupPluginsForTest(); |
| 930 | + serverPluginsModule.setFallbackGatewayContext(createTestContext("nodes-invoke-browser-proxy")); |
| 931 | + |
| 932 | + const runtime = runtimeModule.createPluginRuntime({ |
| 933 | + allowGatewaySubagentBinding: true, |
| 934 | + }); |
| 935 | + await gatewayRequestScopeModule.withPluginRuntimePluginScope( |
| 936 | + { pluginId: "google-meet", pluginOrigin: "bundled" }, |
| 937 | + () => |
| 938 | + runtime.nodes.invoke({ |
| 939 | + nodeId: "node-1", |
| 940 | + command: "browser.proxy", |
| 941 | + params: { method: "GET", path: "/profiles" }, |
| 942 | + scopes: ["operator.admin"], |
| 943 | + }), |
| 944 | + ); |
| 945 | + |
| 946 | + expect(getLastDispatchedParams()).toMatchObject({ |
| 947 | + nodeId: "node-1", |
| 948 | + command: "browser.proxy", |
| 949 | + params: { method: "GET", path: "/profiles" }, |
| 950 | + }); |
| 951 | + expect(getLastDispatchedClientScopes()).toEqual(["operator.admin"]); |
| 952 | + expect(getLastDispatchedClientInternal().pluginRuntimeOwnerId).toBe("google-meet"); |
| 953 | + }); |
| 954 | + |
| 955 | + test("does not let arbitrary plugin nodes runtime mint admin scope for browser proxy", async () => { |
| 956 | + loadOpenClawPlugins.mockReturnValue( |
| 957 | + addLoadedPlugin(createRegistry([]), { id: "third-party", origin: "global" }), |
| 958 | + ); |
| 959 | + loadGatewayStartupPluginsForTest(); |
| 960 | + serverPluginsModule.setFallbackGatewayContext( |
| 961 | + createTestContext("nodes-invoke-browser-proxy-no-elevate"), |
| 962 | + ); |
| 963 | + |
| 964 | + const runtime = runtimeModule.createPluginRuntime({ |
| 965 | + allowGatewaySubagentBinding: true, |
| 966 | + }); |
| 967 | + await gatewayRequestScopeModule.withPluginRuntimePluginScope( |
| 968 | + { pluginId: "third-party", pluginOrigin: "global" }, |
| 969 | + () => |
| 970 | + runtime.nodes.invoke({ |
| 971 | + nodeId: "node-1", |
| 972 | + command: "browser.proxy", |
| 973 | + params: { method: "GET", path: "/profiles" }, |
| 974 | + scopes: ["operator.admin"], |
| 975 | + }), |
| 976 | + ); |
| 977 | + |
| 978 | + expect(getLastDispatchedParams()).toMatchObject({ |
| 979 | + nodeId: "node-1", |
| 980 | + command: "browser.proxy", |
| 981 | + params: { method: "GET", path: "/profiles" }, |
| 982 | + }); |
| 983 | + expect(getLastDispatchedClientScopes()).toEqual(["operator.write"]); |
| 984 | + expect(getLastDispatchedClientScopes()).not.toContain("operator.admin"); |
| 985 | + expect(getLastDispatchedClientInternal().pluginRuntimeOwnerId).toBe("third-party"); |
| 986 | + }); |
| 987 | + |
902 | 988 | test("forwards provider and model overrides when the request scope is authorized", async () => { |
903 | 989 | const serverPlugins = serverPluginsModule; |
904 | 990 | const runtime = await createSubagentRuntime(serverPlugins); |
|
0 commit comments