fix: add dmScope guidance regression coverage (#13129) (thanks @VintLin)

This commit is contained in:
Peter Steinberger
2026-02-13 16:42:49 +01:00
parent ca3c83acdf
commit f612e35907
4 changed files with 74 additions and 1 deletions

View File

@@ -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.

View File

@@ -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"');
});
});

View File

@@ -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)") {

View File

@@ -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"'),
}),
]),
);