diff --git a/src/auto-reply/reply/command-gates.ts b/src/auto-reply/reply/command-gates.ts index 1f0b441f51a..3a075f35cd4 100644 --- a/src/auto-reply/reply/command-gates.ts +++ b/src/auto-reply/reply/command-gates.ts @@ -6,6 +6,13 @@ import { isInternalMessageChannel } from "../../utils/message-channel.js"; import type { ReplyPayload } from "../types.js"; import type { CommandHandlerResult, HandleCommandsParams } from "./commands-types.js"; +function buildNativeCommandGateReply(text: string): CommandHandlerResult { + return { + shouldContinue: false, + reply: { text }, + }; +} + export function rejectUnauthorizedCommand( params: HandleCommandsParams, commandLabel: string, @@ -16,6 +23,9 @@ export function rejectUnauthorizedCommand( logVerbose( `Ignoring ${commandLabel} from unauthorized sender: ${redactIdentifier(params.command.senderId)}`, ); + if (params.ctx.CommandSource === "native") { + return buildNativeCommandGateReply("You are not authorized to use this command."); + } return { shouldContinue: false }; } @@ -29,6 +39,9 @@ export function rejectNonOwnerCommand( logVerbose( `Ignoring ${commandLabel} from non-owner sender: ${redactIdentifier(params.command.senderId)}`, ); + if (params.ctx.CommandSource === "native") { + return buildNativeCommandGateReply("You are not authorized to use this command."); + } return { shouldContinue: false }; } diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index ebe49cbd9fc..1ec69bcb2bb 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -818,6 +818,48 @@ describe("handleCommands owner gating for privileged show commands", () => { testCase.assert(result); } }); + + it("returns an explicit unauthorized reply for native privileged commands", async () => { + const configParams = buildParams( + "/config show", + { + commands: { config: true, text: true }, + channels: { discord: { dm: { enabled: true, policy: "open" } } }, + } as OpenClawConfig, + { + Provider: "discord", + Surface: "discord", + CommandSource: "native", + }, + ); + configParams.command.senderIsOwner = false; + + const configResult = await handleCommands(configParams); + expect(configResult).toEqual({ + shouldContinue: false, + reply: { text: "You are not authorized to use this command." }, + }); + + const pluginParams = buildParams( + "/plugins list", + { + commands: { plugins: true, text: true }, + channels: { discord: { dm: { enabled: true, policy: "open" } } }, + } as OpenClawConfig, + { + Provider: "discord", + Surface: "discord", + CommandSource: "native", + }, + ); + pluginParams.command.senderIsOwner = false; + + const pluginResult = await handleCommands(pluginParams); + expect(pluginResult).toEqual({ + shouldContinue: false, + reply: { text: "You are not authorized to use this command." }, + }); + }); }); describe("handleCommands /config configWrites gating", () => {