mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 16:11:25 +00:00
fix: fail-closed shared-session reply routing (#24571) (thanks @brandonwise)
This commit is contained in:
@@ -96,4 +96,41 @@ describe("agent delivery helpers", () => {
|
||||
expect(mocks.resolveOutboundTarget).not.toHaveBeenCalled();
|
||||
expect(resolved.resolvedTo).toBe("+1555");
|
||||
});
|
||||
|
||||
it("prefers turn-source delivery context over session last route", () => {
|
||||
const plan = resolveAgentDeliveryPlan({
|
||||
sessionEntry: {
|
||||
sessionId: "s4",
|
||||
updatedAt: 4,
|
||||
deliveryContext: { channel: "slack", to: "U_WRONG", accountId: "wrong" },
|
||||
},
|
||||
requestedChannel: "last",
|
||||
turnSourceChannel: "whatsapp",
|
||||
turnSourceTo: "+17775550123",
|
||||
turnSourceAccountId: "work",
|
||||
accountId: undefined,
|
||||
wantsDelivery: true,
|
||||
});
|
||||
|
||||
expect(plan.resolvedChannel).toBe("whatsapp");
|
||||
expect(plan.resolvedTo).toBe("+17775550123");
|
||||
expect(plan.resolvedAccountId).toBe("work");
|
||||
});
|
||||
|
||||
it("does not reuse mutable session to when only turnSourceChannel is provided", () => {
|
||||
const plan = resolveAgentDeliveryPlan({
|
||||
sessionEntry: {
|
||||
sessionId: "s5",
|
||||
updatedAt: 5,
|
||||
deliveryContext: { channel: "slack", to: "U_WRONG" },
|
||||
},
|
||||
requestedChannel: "last",
|
||||
turnSourceChannel: "whatsapp",
|
||||
accountId: undefined,
|
||||
wantsDelivery: true,
|
||||
});
|
||||
|
||||
expect(plan.resolvedChannel).toBe("whatsapp");
|
||||
expect(plan.resolvedTo).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,15 @@ export function resolveAgentDeliveryPlan(params: {
|
||||
normalizedTurnSource && isDeliverableMessageChannel(normalizedTurnSource)
|
||||
? normalizedTurnSource
|
||||
: undefined;
|
||||
const turnSourceTo =
|
||||
typeof params.turnSourceTo === "string" && params.turnSourceTo.trim()
|
||||
? params.turnSourceTo.trim()
|
||||
: undefined;
|
||||
const turnSourceAccountId = normalizeAccountId(params.turnSourceAccountId);
|
||||
const turnSourceThreadId =
|
||||
params.turnSourceThreadId != null && params.turnSourceThreadId !== ""
|
||||
? params.turnSourceThreadId
|
||||
: undefined;
|
||||
|
||||
const baseDelivery = resolveSessionDeliveryTarget({
|
||||
entry: params.sessionEntry,
|
||||
@@ -72,9 +81,9 @@ export function resolveAgentDeliveryPlan(params: {
|
||||
explicitTo,
|
||||
explicitThreadId: params.explicitThreadId,
|
||||
turnSourceChannel,
|
||||
turnSourceTo: params.turnSourceTo,
|
||||
turnSourceAccountId: params.turnSourceAccountId,
|
||||
turnSourceThreadId: params.turnSourceThreadId,
|
||||
turnSourceTo,
|
||||
turnSourceAccountId,
|
||||
turnSourceThreadId,
|
||||
});
|
||||
|
||||
const resolvedChannel = (() => {
|
||||
|
||||
@@ -470,4 +470,62 @@ describe("resolveSessionDeliveryTarget — cross-channel reply guard (#24152)",
|
||||
expect(resolved.accountId).toBe("bot-123");
|
||||
expect(resolved.threadId).toBe(42);
|
||||
});
|
||||
|
||||
it("does not fall back to session target metadata when turnSourceChannel is set", () => {
|
||||
const resolved = resolveSessionDeliveryTarget({
|
||||
entry: {
|
||||
sessionId: "sess-no-fallback",
|
||||
updatedAt: 1,
|
||||
lastChannel: "slack",
|
||||
lastTo: "U_WRONG",
|
||||
lastAccountId: "wrong-account",
|
||||
lastThreadId: "1739142736.000100",
|
||||
},
|
||||
requestedChannel: "last",
|
||||
turnSourceChannel: "whatsapp",
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("whatsapp");
|
||||
expect(resolved.to).toBeUndefined();
|
||||
expect(resolved.accountId).toBeUndefined();
|
||||
expect(resolved.threadId).toBeUndefined();
|
||||
expect(resolved.lastTo).toBeUndefined();
|
||||
expect(resolved.lastAccountId).toBeUndefined();
|
||||
expect(resolved.lastThreadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses explicitTo even when turnSourceTo is omitted", () => {
|
||||
const resolved = resolveSessionDeliveryTarget({
|
||||
entry: {
|
||||
sessionId: "sess-explicit-to",
|
||||
updatedAt: 1,
|
||||
lastChannel: "slack",
|
||||
lastTo: "U_WRONG",
|
||||
},
|
||||
requestedChannel: "last",
|
||||
explicitTo: "+15551234567",
|
||||
turnSourceChannel: "whatsapp",
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("whatsapp");
|
||||
expect(resolved.to).toBe("+15551234567");
|
||||
});
|
||||
|
||||
it("still allows mismatched lastTo only from turn-scoped metadata", () => {
|
||||
const resolved = resolveSessionDeliveryTarget({
|
||||
entry: {
|
||||
sessionId: "sess-mismatch-turn",
|
||||
updatedAt: 1,
|
||||
lastChannel: "slack",
|
||||
lastTo: "U_WRONG",
|
||||
},
|
||||
requestedChannel: "telegram",
|
||||
allowMismatchedLastTo: true,
|
||||
turnSourceChannel: "whatsapp",
|
||||
turnSourceTo: "+15550000000",
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("telegram");
|
||||
expect(resolved.to).toBe("+15550000000");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,17 +89,13 @@ export function resolveSessionDeliveryTarget(params: {
|
||||
const sessionLastChannel =
|
||||
context?.channel && isDeliverableMessageChannel(context.channel) ? context.channel : undefined;
|
||||
|
||||
// When a turn-source channel is provided, use it instead of the session's
|
||||
// mutable lastChannel. This prevents a concurrent inbound from a different
|
||||
// channel from hijacking the reply target (cross-channel privacy leak).
|
||||
const lastChannel = params.turnSourceChannel ?? sessionLastChannel;
|
||||
const lastTo = params.turnSourceChannel ? (params.turnSourceTo ?? context?.to) : context?.to;
|
||||
const lastAccountId = params.turnSourceChannel
|
||||
? (params.turnSourceAccountId ?? context?.accountId)
|
||||
: context?.accountId;
|
||||
const lastThreadId = params.turnSourceChannel
|
||||
? (params.turnSourceThreadId ?? context?.threadId)
|
||||
: context?.threadId;
|
||||
// When a turn-source channel is provided, use only turn-scoped metadata.
|
||||
// Falling back to mutable session fields would re-introduce routing races.
|
||||
const hasTurnSourceChannel = params.turnSourceChannel != null;
|
||||
const lastChannel = hasTurnSourceChannel ? params.turnSourceChannel : sessionLastChannel;
|
||||
const lastTo = hasTurnSourceChannel ? params.turnSourceTo : context?.to;
|
||||
const lastAccountId = hasTurnSourceChannel ? params.turnSourceAccountId : context?.accountId;
|
||||
const lastThreadId = hasTurnSourceChannel ? params.turnSourceThreadId : context?.threadId;
|
||||
|
||||
const rawRequested = params.requestedChannel ?? "last";
|
||||
const requested = rawRequested === "last" ? "last" : normalizeMessageChannel(rawRequested);
|
||||
|
||||
Reference in New Issue
Block a user