mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:20:42 +00:00
fix(routing): match bound peers by kind before applying wildcard precedence
An agent with bindings that differed only by peer kind (for example direct/* and channel/*, or the same peer id across kinds) could pick the wrong sender account in resolveFirstBoundAccountId because the lookup compared peer.id only and dropped peer.kind. Combined with the peerId now always being forwarded from subagent spawns, an unrelated binding could win purely by config order and route child messages from the wrong identity. - src/routing/bound-account-read.ts: preserve peer.kind in the normalized match and accept an optional peerKind on resolveFirstBoundAccountId. When both caller and binding declare a kind, they must match or the binding is skipped. If either side omits the kind, kind is not used as a filter (preserves prior behavior for callers that do not know the kind, such as cron delivery resolution). - src/agents/subagent-spawn.ts: derive peerKind for the lookup via the active channel plugin's inferTargetChatType helper and pass it through. Same-agent spawns still short-circuit the lookup entirely. Regression coverage in src/routing/bound-account-read.test.ts: - Filters bindings by peer kind when caller supplies peerKind — direct/* and channel/* wildcards resolve to distinct accounts. - Skips peer-specific bindings whose kind does not match the caller's peerKind, falling through to the channel-only binding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Gustavo Madeira Santana
parent
46708707f6
commit
6faff0c343
@@ -147,4 +147,75 @@ describe("resolveFirstBoundAccountId", () => {
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("filters bindings by peer kind when caller supplies peerKind", () => {
|
||||
const cfg = cfgWithBindings([
|
||||
{
|
||||
type: "route",
|
||||
agentId: "bot-alpha",
|
||||
match: {
|
||||
channel: "matrix",
|
||||
peer: { kind: "direct", id: "*" },
|
||||
accountId: "bot-alpha-dm",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
agentId: "bot-alpha",
|
||||
match: {
|
||||
channel: "matrix",
|
||||
peer: { kind: "channel", id: "*" },
|
||||
accountId: "bot-alpha-room",
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
resolveFirstBoundAccountId({
|
||||
cfg,
|
||||
channelId: "matrix",
|
||||
agentId: "bot-alpha",
|
||||
peerId: "!room:example.org",
|
||||
peerKind: "channel",
|
||||
}),
|
||||
).toBe("bot-alpha-room");
|
||||
expect(
|
||||
resolveFirstBoundAccountId({
|
||||
cfg,
|
||||
channelId: "matrix",
|
||||
agentId: "bot-alpha",
|
||||
peerId: "@user:example.org",
|
||||
peerKind: "direct",
|
||||
}),
|
||||
).toBe("bot-alpha-dm");
|
||||
});
|
||||
|
||||
it("skips peer-specific bindings whose kind does not match the caller's peerKind", () => {
|
||||
const cfg = cfgWithBindings([
|
||||
{
|
||||
type: "route",
|
||||
agentId: "bot-alpha",
|
||||
match: {
|
||||
channel: "matrix",
|
||||
peer: { kind: "direct", id: "!room:example.org" },
|
||||
accountId: "bot-alpha-wrong-kind",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
agentId: "bot-alpha",
|
||||
match: { channel: "matrix", accountId: "bot-alpha-default" },
|
||||
},
|
||||
]);
|
||||
// Caller peerKind=channel: the direct-kind binding is ineligible even though
|
||||
// its peerId would match — falls through to the channel-only binding.
|
||||
expect(
|
||||
resolveFirstBoundAccountId({
|
||||
cfg,
|
||||
channelId: "matrix",
|
||||
agentId: "bot-alpha",
|
||||
peerId: "!room:example.org",
|
||||
peerKind: "channel",
|
||||
}),
|
||||
).toBe("bot-alpha-default");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeChatType, type ChatType } from "../channels/chat-type.js";
|
||||
import { normalizeChatChannelId } from "../channels/ids.js";
|
||||
import { listRouteBindings } from "../config/bindings.js";
|
||||
import type { AgentRouteBinding } from "../config/types.agents.js";
|
||||
@@ -19,6 +20,7 @@ function resolveNormalizedBindingMatch(binding: AgentRouteBinding): {
|
||||
accountId: string;
|
||||
channelId: string;
|
||||
peerId?: string;
|
||||
peerKind?: ChatType;
|
||||
} | null {
|
||||
if (!binding || typeof binding !== "object") {
|
||||
return null;
|
||||
@@ -36,11 +38,13 @@ function resolveNormalizedBindingMatch(binding: AgentRouteBinding): {
|
||||
return null;
|
||||
}
|
||||
const peerId = match.peer && typeof match.peer.id === "string" ? match.peer.id.trim() : undefined;
|
||||
const peerKind = match.peer ? normalizeChatType(match.peer.kind) : undefined;
|
||||
return {
|
||||
agentId: normalizeAgentId(binding.agentId),
|
||||
accountId: normalizeAccountId(accountId),
|
||||
channelId,
|
||||
peerId: peerId || undefined,
|
||||
peerKind: peerKind ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,6 +53,7 @@ export function resolveFirstBoundAccountId(params: {
|
||||
channelId: string;
|
||||
agentId: string;
|
||||
peerId?: string;
|
||||
peerKind?: ChatType;
|
||||
}): string | undefined {
|
||||
const normalizedChannel = normalizeBindingChannelId(params.channelId);
|
||||
if (!normalizedChannel) {
|
||||
@@ -56,6 +61,7 @@ export function resolveFirstBoundAccountId(params: {
|
||||
}
|
||||
const normalizedAgentId = normalizeAgentId(params.agentId);
|
||||
const normalizedPeerId = params.peerId?.trim() || undefined;
|
||||
const normalizedPeerKind = normalizeChatType(params.peerKind) ?? undefined;
|
||||
let wildcardPeerMatch: string | undefined;
|
||||
let channelOnlyFallback: string | undefined;
|
||||
let peerlessPeerSpecificFallback: string | undefined;
|
||||
@@ -68,6 +74,13 @@ export function resolveFirstBoundAccountId(params: {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// When the caller knows the peer kind and the binding declares a peer kind,
|
||||
// they must match — a direct/* binding must not win for a channel caller,
|
||||
// and vice versa. If either side omits the kind, we do not filter on it
|
||||
// (preserves backward-compat for peerless cron callers).
|
||||
if (resolved.peerKind && normalizedPeerKind && resolved.peerKind !== normalizedPeerKind) {
|
||||
continue;
|
||||
}
|
||||
if (resolved.peerId === "*") {
|
||||
if (normalizedPeerId) {
|
||||
wildcardPeerMatch ??= resolved.accountId;
|
||||
|
||||
Reference in New Issue
Block a user