























@@ -60,12 +60,31 @@ vi.mock("../../cli/plugins-install-persist.js", async (importOriginal) => ({
60606161const workspaceHarness = createCommandWorkspaceHarness("openclaw-command-plugins-install-");
626263-function buildPluginsParams(commandBodyNormalized: string, workspaceDir: string) {
64-return buildPluginsCommandParams({
63+function buildPluginsParams(
64+commandBodyNormalized: string,
65+workspaceDir: string,
66+options: {
67+gatewayClientScopes?: string[];
68+omitGatewayClientScopes?: boolean;
69+senderIsOwner?: boolean;
70+} = {},
71+) {
72+const params = buildPluginsCommandParams({
6573 commandBodyNormalized,
6674 workspaceDir,
67-gatewayClientScopes: ["operator.admin", "operator.write", "operator.pairing"],
75+gatewayClientScopes: options.gatewayClientScopes ?? [
76+"operator.admin",
77+"operator.write",
78+"operator.pairing",
79+],
6880});
81+if (options.senderIsOwner !== undefined) {
82+params.command.senderIsOwner = options.senderIsOwner;
83+}
84+if (options.omitGatewayClientScopes) {
85+delete params.ctx.GatewayClientScopes;
86+}
87+return params;
6988}
70897190function expectPersistedInstall(pluginId: string, expectedInstall: Record<string, unknown>): void {
@@ -125,6 +144,66 @@ describe("handleCommands /plugins install", () => {
125144});
126145});
127146147+it("blocks channel-authorized non-owner plugin installs before installer side effects", async () => {
148+await withTempHome("openclaw-command-plugins-home-", async () => {
149+const workspaceDir = await workspaceHarness.createWorkspace();
150+const pluginDir = path.join(workspaceDir, "fixtures", "channel-installed-plugin");
151+await fs.mkdir(pluginDir, { recursive: true });
152+153+const params = buildPluginsParams(`/plugins install ${pluginDir}`, workspaceDir, {
154+omitGatewayClientScopes: true,
155+senderIsOwner: false,
156+});
157+params.command.channel = "telegram";
158+params.command.channelId = "telegram";
159+params.command.surface = "telegram";
160+params.command.senderId = "telegram-user-3";
161+params.command.isAuthorizedSender = true;
162+params.ctx.Provider = "telegram";
163+params.ctx.Surface = "telegram";
164+165+const result = await handlePluginsCommand(params, true);
166+167+expect(result?.shouldContinue).toBe(false);
168+expect(installPluginFromPathMock).not.toHaveBeenCalled();
169+expect(persistPluginInstallMock).not.toHaveBeenCalled();
170+});
171+});
172+173+it("allows gateway clients with operator.admin to install plugins", async () => {
174+installPluginFromPathMock.mockResolvedValue({
175+ok: true,
176+pluginId: "gateway-admin-plugin",
177+targetDir: "/tmp/gateway-admin-plugin",
178+version: "0.0.1",
179+extensions: ["index.js"],
180+});
181+persistPluginInstallMock.mockResolvedValue({});
182+183+await withTempHome("openclaw-command-plugins-home-", async () => {
184+const workspaceDir = await workspaceHarness.createWorkspace();
185+const pluginDir = path.join(workspaceDir, "fixtures", "gateway-admin-plugin");
186+await fs.mkdir(pluginDir, { recursive: true });
187+188+const params = buildPluginsParams(`/plugins install ${pluginDir}`, workspaceDir, {
189+gatewayClientScopes: ["operator.admin", "operator.write"],
190+senderIsOwner: false,
191+});
192+193+const result = await handlePluginsCommand(params, true);
194+195+expect(result?.shouldContinue).toBe(false);
196+expect(result?.reply?.text).toContain('Installed plugin "gateway-admin-plugin"');
197+expect(mockFirstObjectArg(installPluginFromPathMock).path).toBe(pluginDir);
198+expectPersistedInstall("gateway-admin-plugin", {
199+source: "path",
200+sourcePath: pluginDir,
201+installPath: "/tmp/gateway-admin-plugin",
202+version: "0.0.1",
203+});
204+});
205+});
206+128207it("installs from an explicit clawhub: spec", async () => {
129208installPluginFromClawHubMock.mockResolvedValue({
130209ok: true,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。