diff --git a/src/auto-reply/reply/commands-allowlist.ts b/src/auto-reply/reply/commands-allowlist.ts index d9568f080b1..f22aed859c5 100644 --- a/src/auto-reply/reply/commands-allowlist.ts +++ b/src/auto-reply/reply/commands-allowlist.ts @@ -58,6 +58,22 @@ type AllowlistCommand = const ACTIONS = new Set(["list", "add", "remove"]); const SCOPES = new Set(["dm", "group", "all"]); +function resolveAllowlistAccountId(params: { + cfg: OpenClawConfig; + channelId: ChannelId; + parsedAccount?: string; + ctxAccountId?: string; +}): string { + const explicitAccountId = normalizeOptionalAccountId(params.parsedAccount); + if (explicitAccountId) { + return explicitAccountId; + } + const plugin = getChannelPlugin(params.channelId); + const configuredDefaultAccountId = plugin?.config.defaultAccountId?.(params.cfg)?.trim(); + const ctxAccountId = normalizeOptionalAccountId(params.ctxAccountId); + return configuredDefaultAccountId || ctxAccountId || DEFAULT_ACCOUNT_ID; +} + function parseAllowlistCommand(raw: string): AllowlistCommand | null { const trimmed = raw.trim(); if (!trimmed.toLowerCase().startsWith("/allowlist")) { @@ -276,7 +292,12 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo }, }; } - const accountId = normalizeAccountId(parsed.account ?? params.ctx.AccountId); + const accountId = resolveAllowlistAccountId({ + cfg: params.cfg, + channelId, + parsedAccount: parsed.account, + ctxAccountId: params.ctx.AccountId, + }); const plugin = getChannelPlugin(channelId); if (parsed.action === "list") { @@ -464,7 +485,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo cfg: params.cfg, channel: params.command.channel, channelId, - accountId: params.ctx.AccountId, + accountId, gatewayClientScopes: params.ctx.GatewayClientScopes, target: editResult.writeTarget, }); diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index 8b11ee7d59e..104b4f5dcdb 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -2169,6 +2169,48 @@ describe("handleCommands /allowlist", () => { } }); + it("uses the configured default account for omitted-account /allowlist list", async () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "telegram", + source: "test", + plugin: { + ...telegramCommandTestPlugin, + config: { + ...telegramCommandTestPlugin.config, + defaultAccountId: (cfg) => + ((cfg.channels?.telegram as { defaultAccount?: string } | undefined)?.defaultAccount ?? + DEFAULT_ACCOUNT_ID), + }, + }, + }, + ]), + ); + + const cfg = { + commands: { text: true, config: true }, + channels: { + telegram: { + defaultAccount: "work", + accounts: { work: { allowFrom: ["123"] } }, + }, + }, + } as OpenClawConfig; + readChannelAllowFromStoreMock.mockResolvedValueOnce([]); + + const params = buildPolicyParams("/allowlist list dm", cfg, { + Provider: "telegram", + Surface: "telegram", + AccountId: undefined, + }); + const result = await handleCommands(params); + + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("Channel: telegram (account work)"); + expect(result.reply?.text).toContain("DM allowFrom (config): 123"); + }); + it("blocks config-targeted /allowlist edits when the target account disables writes", async () => { const previousWriteCount = writeConfigFileMock.mock.calls.length; const cfg = { @@ -2199,6 +2241,55 @@ describe("handleCommands /allowlist", () => { expect(writeConfigFileMock.mock.calls.length).toBe(previousWriteCount); }); + it("honors the configured default account when gating omitted-account /allowlist config edits", async () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "telegram", + source: "test", + plugin: { + ...telegramCommandTestPlugin, + config: { + ...telegramCommandTestPlugin.config, + defaultAccountId: (cfg) => + ((cfg.channels?.telegram as { defaultAccount?: string } | undefined)?.defaultAccount ?? + DEFAULT_ACCOUNT_ID), + }, + }, + }, + ]), + ); + + const previousWriteCount = writeConfigFileMock.mock.calls.length; + const cfg = { + commands: { text: true, config: true }, + channels: { + telegram: { + defaultAccount: "work", + configWrites: true, + accounts: { + work: { configWrites: false, allowFrom: ["123"] }, + }, + }, + }, + } as OpenClawConfig; + readConfigFileSnapshotMock.mockResolvedValueOnce({ + valid: true, + parsed: structuredClone(cfg), + }); + const params = buildPolicyParams("/allowlist add dm --config 789", cfg, { + Provider: "telegram", + Surface: "telegram", + AccountId: undefined, + }); + params.command.senderIsOwner = true; + const result = await handleCommands(params); + + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("channels.telegram.accounts.work.configWrites=true"); + expect(writeConfigFileMock.mock.calls.length).toBe(previousWriteCount); + }); + it("blocks allowlist writes from authorized non-owner senders, including cross-channel targets", async () => { const cfg = { commands: {