diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d4d48d6d1..8aa29c16ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - Security/Gateway + ACP: block high-risk tools (`sessions_spawn`, `sessions_send`, `gateway`, `whatsapp_login`) from HTTP `/tools/invoke` by default with `gateway.tools.{allow,deny}` overrides, and harden ACP permission selection to fail closed when tool identity/options are ambiguous while supporting `allow_always`/`reject_always`. (#15390) Thanks @aether-ai-agent. - MS Teams: preserve parsed mention entities/text when appending OneDrive fallback file links, and accept broader real-world Teams mention ID formats (`29:...`, `8:orgid:...`) while still rejecting placeholder patterns. (#15436) Thanks @hyojin. - Security/Audit: distinguish external webhooks (`hooks.enabled`) from internal hooks (`hooks.internal.enabled`) in attack-surface summaries to avoid false exposure signals when only internal hooks are enabled. (#13474) Thanks @mcaxtr. +- Security/Onboarding: clarify multi-user DM isolation remediation with explicit `openclaw config set session.dmScope ...` commands in security audit, doctor security, and channel onboarding guidance. (#13129) Thanks @VintLin. - Security/Audit: add misconfiguration checks for sandbox Docker config with sandbox mode off, ineffective `gateway.nodes.denyCommands` entries, global minimal tool-profile overrides by agent profiles, and permissive extension-plugin tool reachability. - Auto-reply/Threading: auto-inject implicit reply threading so `replyToMode` works without requiring model-emitted `[[reply_to_current]]`, while preserving `replyToMode: "off"` behavior for implicit Slack replies and keeping block-streaming chunk coalescing stable under `replyToMode: "first"`. (#14976) Thanks @Diaspar4u. - Sandbox: pass configured `sandbox.docker.env` variables to sandbox containers at `docker create` time. (#15138) Thanks @stevebot-alive. diff --git a/src/commands/doctor-security.e2e.test.ts b/src/commands/doctor-security.e2e.test.ts index f948f2617ad..c1a37bb22f1 100644 --- a/src/commands/doctor-security.e2e.test.ts +++ b/src/commands/doctor-security.e2e.test.ts @@ -2,13 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; const note = vi.hoisted(() => vi.fn()); +const pluginRegistry = vi.hoisted(() => ({ list: [] as any[] })); vi.mock("../terminal/note.js", () => ({ note, })); vi.mock("../channels/plugins/index.js", () => ({ - listChannelPlugins: () => [], + listChannelPlugins: () => pluginRegistry.list, })); import { noteSecurityWarnings } from "./doctor-security.js"; @@ -19,6 +20,7 @@ describe("noteSecurityWarnings gateway exposure", () => { beforeEach(() => { note.mockClear(); + pluginRegistry.list = []; prevToken = process.env.OPENCLAW_GATEWAY_TOKEN; prevPassword = process.env.OPENCLAW_GATEWAY_PASSWORD; delete process.env.OPENCLAW_GATEWAY_TOKEN; @@ -73,4 +75,31 @@ describe("noteSecurityWarnings gateway exposure", () => { expect(message).toContain("No channel security warnings detected"); expect(message).not.toContain("Gateway bound"); }); + + it("shows explicit dmScope config command for multi-user DMs", async () => { + pluginRegistry.list = [ + { + id: "whatsapp", + meta: { label: "WhatsApp" }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), + isEnabled: () => true, + isConfigured: () => true, + }, + security: { + resolveDmPolicy: () => ({ + policy: "allowlist", + allowFrom: ["alice", "bob"], + allowFromPath: "channels.whatsapp.", + approveHint: "approve", + }), + }, + }, + ]; + const cfg = { session: { dmScope: "main" } } as OpenClawConfig; + await noteSecurityWarnings(cfg); + const message = lastMessage(); + expect(message).toContain('config set session.dmScope "per-channel-peer"'); + }); }); diff --git a/src/commands/onboard-channels.e2e.test.ts b/src/commands/onboard-channels.e2e.test.ts index 978b8b51507..25c1c6fc220 100644 --- a/src/commands/onboard-channels.e2e.test.ts +++ b/src/commands/onboard-channels.e2e.test.ts @@ -87,6 +87,48 @@ describe("setupChannels", () => { expect(multiselect).not.toHaveBeenCalled(); }); + it("shows explicit dmScope config command in channel primer", async () => { + const note = vi.fn(async () => {}); + const select = vi.fn(async () => "__done__"); + const multiselect = vi.fn(async () => { + throw new Error("unexpected multiselect"); + }); + const text = vi.fn(async ({ message }: { message: string }) => { + throw new Error(`unexpected text prompt: ${message}`); + }); + + const prompter: WizardPrompter = { + intro: vi.fn(async () => {}), + outro: vi.fn(async () => {}), + note, + select, + multiselect, + text: text as unknown as WizardPrompter["text"], + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })), + }; + + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + + await setupChannels({} as OpenClawConfig, runtime, prompter, { + skipConfirm: true, + }); + + const sawPrimer = note.mock.calls.some( + ([message, title]) => + title === "How channels work" && + String(message).includes('config set session.dmScope "per-channel-peer"'), + ); + expect(sawPrimer).toBe(true); + expect(multiselect).not.toHaveBeenCalled(); + }); + it("prompts for configured channel action and skips configuration when told to skip", async () => { const select = vi.fn(async ({ message }: { message: string }) => { if (message === "Select channel (QuickStart)") { diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index bac80213d06..d13dbff6971 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -589,6 +589,7 @@ describe("security audit", () => { expect.objectContaining({ checkId: "channels.whatsapp.dm.scope_main_multiuser", severity: "warn", + remediation: expect.stringContaining('config set session.dmScope "per-channel-peer"'), }), ]), );