diff --git a/src/security/fix.test.ts b/src/security/fix.test.ts index 71d92a19051..d05294a5d33 100644 --- a/src/security/fix.test.ts +++ b/src/security/fix.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import type { ChannelPlugin } from "../channels/plugins/types.js"; import { fixSecurityFootguns } from "./fix.js"; const isWindows = process.platform === "win32"; @@ -30,6 +31,70 @@ describe("security fix", () => { OPENCLAW_CONFIG_PATH: configPath, }); + const createWhatsAppConfigFixTestPlugin = (storeAllowFrom: string[]): ChannelPlugin => ({ + id: "whatsapp", + meta: { + id: "whatsapp", + label: "WhatsApp", + selectionLabel: "WhatsApp", + docsPath: "/docs/testing", + blurb: "test stub", + }, + capabilities: { + chatTypes: ["direct", "group"], + }, + config: { + listAccountIds: () => ["default"], + inspectAccount: () => ({ accountId: "default", enabled: true, configured: true, config: {} }), + resolveAccount: () => ({ accountId: "default", enabled: true, config: {} }), + isEnabled: () => true, + isConfigured: () => true, + }, + security: { + applyConfigFixes: async ({ cfg }) => { + if (storeAllowFrom.length === 0) { + return { config: cfg, changes: [] }; + } + const next = structuredClone(cfg ?? {}); + const whatsapp = next.channels?.whatsapp as Record | undefined; + if (!whatsapp || typeof whatsapp !== "object") { + return { config: cfg, changes: [] }; + } + const changes: string[] = []; + let changed = false; + const maybeApply = (prefix: string, holder: Record) => { + if (holder.groupPolicy !== "allowlist") { + return; + } + const allowFrom = Array.isArray(holder.allowFrom) ? holder.allowFrom : []; + const groupAllowFrom = Array.isArray(holder.groupAllowFrom) ? holder.groupAllowFrom : []; + if (allowFrom.length > 0 || groupAllowFrom.length > 0) { + return; + } + holder.groupAllowFrom = [...storeAllowFrom]; + changes.push(`${prefix}groupAllowFrom=pairing-store`); + changed = true; + }; + + maybeApply("channels.whatsapp.", whatsapp); + const accounts = whatsapp.accounts; + if (accounts && typeof accounts === "object") { + for (const [accountId, value] of Object.entries(accounts)) { + if (!value || typeof value !== "object") { + continue; + } + maybeApply( + `channels.whatsapp.accounts.${accountId}.`, + value as Record, + ); + } + } + + return { config: changed ? next : cfg, changes }; + }, + }, + }); + const writeJsonConfig = async (configPath: string, config: Record) => { await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8"); }; @@ -45,9 +110,13 @@ describe("security fix", () => { const readParsedConfig = async (configPath: string) => JSON.parse(await fs.readFile(configPath, "utf-8")) as Record; - const runFixAndReadChannels = async (stateDir: string, configPath: string) => { + const runFixAndReadChannels = async ( + stateDir: string, + configPath: string, + channelPlugins?: ChannelPlugin[], + ) => { const env = createFixEnv(stateDir, configPath); - const res = await fixSecurityFootguns({ env, stateDir, configPath }); + const res = await fixSecurityFootguns({ env, stateDir, configPath, channelPlugins }); const parsed = await readParsedConfig(configPath); return { res, @@ -71,7 +140,9 @@ describe("security fix", () => { }) => { await writeWhatsAppConfig(params.configPath, params.whatsapp); await writeWhatsAppAllowFromStore(params.stateDir, params.allowFromStore); - return runFixAndReadChannels(params.stateDir, params.configPath); + return runFixAndReadChannels(params.stateDir, params.configPath, [ + createWhatsAppConfigFixTestPlugin(params.allowFromStore), + ]); }; const writeWhatsAppAllowFromStore = async (stateDir: string, allowFrom: string[]) => { @@ -148,7 +219,12 @@ describe("security fix", () => { await writeWhatsAppAllowFromStore(stateDir, [" +15551234567 "]); const env = createFixEnv(stateDir, configPath); - const res = await fixSecurityFootguns({ env, stateDir, configPath }); + const res = await fixSecurityFootguns({ + env, + stateDir, + configPath, + channelPlugins: [createWhatsAppConfigFixTestPlugin(["+15551234567"])], + }); expect(res.ok).toBe(true); expect(res.configWritten).toBe(true); expect(res.changes).toEqual( diff --git a/src/security/fix.ts b/src/security/fix.ts index 59bf2edcbc6..764c140e66e 100644 --- a/src/security/fix.ts +++ b/src/security/fix.ts @@ -248,10 +248,14 @@ function applyConfigFixes(params: { cfg: OpenClawConfig; env: NodeJS.ProcessEnv async function collectChannelSecurityConfigFixMutation(params: { cfg: OpenClawConfig; env: NodeJS.ProcessEnv; + channelPlugins?: ChannelPlugin[]; }) { let nextCfg = params.cfg; const changes: string[] = []; const collectPlugins = (): ChannelPlugin[] => { + if (params.channelPlugins) { + return params.channelPlugins; + } try { const pluginIds = Object.keys(params.cfg.channels ?? {}).filter(Boolean); if (pluginIds.length === 0) { @@ -358,6 +362,7 @@ export async function fixSecurityFootguns(opts?: { configPath?: string; platform?: NodeJS.Platform; exec?: ExecFn; + channelPlugins?: ChannelPlugin[]; }): Promise { const env = opts?.env ?? process.env; const platform = opts?.platform ?? process.platform; @@ -381,6 +386,7 @@ export async function fixSecurityFootguns(opts?: { const channelFixes = await collectChannelSecurityConfigFixMutation({ cfg: fixed.cfg, env, + channelPlugins: opts?.channelPlugins, }); changes = [...fixed.changes, ...channelFixes.changes];