mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(sessions): enforce channel send policy for account-scoped DMs
Derive the channel from canonical account-scoped DM session keys when resolving session.sendPolicy, so channel-scoped allow/deny rules apply to per-account-channel-peer sessions. Keep derivation limited to canonical channel peer key shapes and add malformed-key regressions so incomplete or non-channel keys do not accidentally match channel rules. Compatibility note: existing channel-scoped send-policy rules can now block account-scoped DM sends that were previously allowed by this bug. Thanks @yetval for the fix.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { buildAgentPeerSessionKey } from "../routing/session-key.js";
|
||||
import { resolveSendPolicy } from "./send-policy.js";
|
||||
|
||||
describe("resolveSendPolicy", () => {
|
||||
@@ -73,6 +74,31 @@ describe("resolveSendPolicy", () => {
|
||||
sessionKey: "demo-channel:direct:user-1",
|
||||
expected: "deny",
|
||||
},
|
||||
{
|
||||
name: "channel-scoped deny fires for per-account-channel-peer DM key without explicit channel field",
|
||||
cfg: cfgWithRules([{ action: "deny", match: { channel: "demo-channel" } }]),
|
||||
sessionKey: buildAgentPeerSessionKey({
|
||||
agentId: "main",
|
||||
channel: "demo-channel",
|
||||
accountId: "acct-1",
|
||||
peerKind: "direct",
|
||||
peerId: "user-1",
|
||||
dmScope: "per-account-channel-peer",
|
||||
}),
|
||||
expected: "deny",
|
||||
},
|
||||
{
|
||||
name: "channel-scoped deny ignores later peer-kind-looking tokens in non-channel keys",
|
||||
cfg: cfgWithRules([{ action: "deny", match: { channel: "demo-channel" } }]),
|
||||
sessionKey: "demo-channel:not-a-peer-kind:user-1:direct",
|
||||
expected: "allow",
|
||||
},
|
||||
{
|
||||
name: "channel-scoped deny ignores incomplete account-scoped keys",
|
||||
cfg: cfgWithRules([{ action: "deny", match: { channel: "demo-channel" } }]),
|
||||
sessionKey: "demo-channel:acct-1:direct",
|
||||
expected: "allow",
|
||||
},
|
||||
])("$name", ({ cfg, entry, sessionKey, expected }) => {
|
||||
expect(resolveSendPolicy({ cfg, entry, sessionKey })).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -40,18 +40,22 @@ function stripAgentSessionKeyPrefix(key?: string): string | undefined {
|
||||
return key;
|
||||
}
|
||||
|
||||
const CHANNEL_SESSION_KEY_PEER_KINDS = new Set(["group", "channel", "direct", "dm"]);
|
||||
|
||||
function deriveChannelFromKey(key?: string) {
|
||||
const normalizedKey = stripAgentSessionKeyPrefix(key);
|
||||
if (!normalizedKey) {
|
||||
return undefined;
|
||||
}
|
||||
const parts = normalizedKey.split(":").filter(Boolean);
|
||||
// Canonical key layout is <channel>:<peerKind>:<peerId>; parts[0] is the channel
|
||||
// for direct/dm peers too, so channel-scoped rules also fire for direct chats.
|
||||
if (
|
||||
parts.length >= 3 &&
|
||||
(parts[1] === "group" || parts[1] === "channel" || parts[1] === "direct" || parts[1] === "dm")
|
||||
) {
|
||||
// Key layout is <channel>:[<accountId>:]<peerKind>:<peerId>; parts[0] is the
|
||||
// channel for account-scoped DM keys too, so channel-scoped rules also fire
|
||||
// for per-account-channel-peer sessions, not just 3-part direct/group keys.
|
||||
const hasChannelPeerShape =
|
||||
parts.length >= 3 && CHANNEL_SESSION_KEY_PEER_KINDS.has(parts[1] ?? "");
|
||||
const hasAccountScopedPeerShape =
|
||||
parts.length >= 4 && CHANNEL_SESSION_KEY_PEER_KINDS.has(parts[2] ?? "");
|
||||
if (hasChannelPeerShape || hasAccountScopedPeerShape) {
|
||||
return normalizeMatchValue(parts[0]);
|
||||
}
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user