|
1 | 1 | import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; |
| 2 | +import { createPluginRecord } from "../plugins/loader-records.js"; |
2 | 3 | import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js"; |
3 | 4 | import type { PluginRegistry } from "../plugins/registry.js"; |
4 | 5 | import type { PluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js"; |
@@ -115,6 +116,30 @@ const createRegistry = (diagnostics: PluginDiagnostic[]): PluginRegistry => ({ |
115 | 116 | diagnostics, |
116 | 117 | }); |
117 | 118 |
|
| 119 | +function addLoadedPlugin( |
| 120 | + registry: PluginRegistry, |
| 121 | + params: { |
| 122 | + id: string; |
| 123 | + origin?: PluginRegistry["plugins"][number]["origin"]; |
| 124 | + trustedOfficialInstall?: boolean; |
| 125 | + }, |
| 126 | +): PluginRegistry { |
| 127 | + registry.plugins.push( |
| 128 | + createPluginRecord({ |
| 129 | + id: params.id, |
| 130 | + name: params.id, |
| 131 | + source: `/tmp/${params.id}/index.js`, |
| 132 | + origin: params.origin ?? "bundled", |
| 133 | + enabled: true, |
| 134 | + configSchema: false, |
| 135 | + ...(params.trustedOfficialInstall !== undefined |
| 136 | + ? { trustedOfficialInstall: params.trustedOfficialInstall } |
| 137 | + : {}), |
| 138 | + }), |
| 139 | + ); |
| 140 | + return registry; |
| 141 | +} |
| 142 | + |
118 | 143 | function createLookUpTableForTest(params: { |
119 | 144 | manifestRegistry?: PluginLookUpTable["manifestRegistry"]; |
120 | 145 | pluginIds?: readonly string[]; |
@@ -795,6 +820,67 @@ describe("loadGatewayPlugins", () => { |
795 | 820 | expect(result.nodes).toEqual([{ nodeId: "connected", connected: true }]); |
796 | 821 | }); |
797 | 822 |
|
| 823 | + test("lets trusted official plugin runtime request admin scope for browser proxy", async () => { |
| 824 | + loadOpenClawPlugins.mockReturnValue(addLoadedPlugin(createRegistry([]), { id: "google-meet" })); |
| 825 | + loadGatewayStartupPluginsForTest(); |
| 826 | + serverPluginsModule.setFallbackGatewayContext(createTestContext("nodes-invoke-browser-proxy")); |
| 827 | + |
| 828 | + const runtime = runtimeModule.createPluginRuntime({ |
| 829 | + allowGatewaySubagentBinding: true, |
| 830 | + }); |
| 831 | + await gatewayRequestScopeModule.withPluginRuntimePluginScope( |
| 832 | + { pluginId: "google-meet", pluginOrigin: "bundled" }, |
| 833 | + () => |
| 834 | + runtime.nodes.invoke({ |
| 835 | + nodeId: "node-1", |
| 836 | + command: "browser.proxy", |
| 837 | + params: { method: "GET", path: "/profiles" }, |
| 838 | + scopes: ["operator.admin"], |
| 839 | + }), |
| 840 | + ); |
| 841 | + |
| 842 | + expect(getLastDispatchedParams()).toMatchObject({ |
| 843 | + nodeId: "node-1", |
| 844 | + command: "browser.proxy", |
| 845 | + params: { method: "GET", path: "/profiles" }, |
| 846 | + }); |
| 847 | + expect(getLastDispatchedClientScopes()).toEqual(["operator.admin"]); |
| 848 | + expect(getLastDispatchedClientInternal().pluginRuntimeOwnerId).toBe("google-meet"); |
| 849 | + }); |
| 850 | + |
| 851 | + test("does not let arbitrary plugin nodes runtime mint admin scope for browser proxy", async () => { |
| 852 | + loadOpenClawPlugins.mockReturnValue( |
| 853 | + addLoadedPlugin(createRegistry([]), { id: "third-party", origin: "global" }), |
| 854 | + ); |
| 855 | + loadGatewayStartupPluginsForTest(); |
| 856 | + serverPluginsModule.setFallbackGatewayContext( |
| 857 | + createTestContext("nodes-invoke-browser-proxy-no-elevate"), |
| 858 | + ); |
| 859 | + |
| 860 | + const runtime = runtimeModule.createPluginRuntime({ |
| 861 | + allowGatewaySubagentBinding: true, |
| 862 | + }); |
| 863 | + await gatewayRequestScopeModule.withPluginRuntimePluginScope( |
| 864 | + { pluginId: "third-party", pluginOrigin: "global" }, |
| 865 | + () => |
| 866 | + runtime.nodes.invoke({ |
| 867 | + nodeId: "node-1", |
| 868 | + command: "browser.proxy", |
| 869 | + params: { method: "GET", path: "/profiles" }, |
| 870 | + scopes: ["operator.admin"], |
| 871 | + }), |
| 872 | + ); |
| 873 | + |
| 874 | + expect(getLastDispatchedParams()).toMatchObject({ |
| 875 | + nodeId: "node-1", |
| 876 | + command: "browser.proxy", |
| 877 | + params: { method: "GET", path: "/profiles" }, |
| 878 | + }); |
| 879 | + expect(getLastDispatchedClientScopes()).toEqual(["operator.write"]); |
| 880 | + expect(getLastDispatchedClientScopes()).not.toContain("operator.admin"); |
| 881 | + expect(getLastDispatchedClientInternal().pluginRuntimeOwnerId).toBe("third-party"); |
| 882 | + }); |
| 883 | + |
798 | 884 | test("forwards provider and model overrides when the request scope is authorized", async () => { |
799 | 885 | const serverPlugins = serverPluginsModule; |
800 | 886 | const runtime = await createSubagentRuntime(serverPlugins); |
|
0 commit comments