fix: isolate external direct-message runtime policy

This commit is contained in:
Peter Steinberger
2026-04-23 01:39:35 +01:00
parent 67f09ea87a
commit 6b41ef311f
35 changed files with 529 additions and 46 deletions

View File

@@ -76,12 +76,10 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
expect(ctx).not.toBeNull();
expect(ctx?.ctxPayload?.MessageThreadId).toBe(42);
expect(ctx?.ctxPayload?.SessionKey).toBe(
"agent:main:telegram:default:direct:42:thread:1234:42",
);
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:1234:42");
});
it("uses the Telegram direct session key when no thread id", async () => {
it("uses the main session key when no thread id", async () => {
const ctx = await buildContext({
message_id: 2,
chat: { id: 1234, type: "private" },
@@ -92,7 +90,7 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
expect(ctx).not.toBeNull();
expect(ctx?.ctxPayload?.MessageThreadId).toBeUndefined();
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:default:direct:42");
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main");
});
});

View File

@@ -140,7 +140,7 @@ describe("buildTelegramMessageContext named-account DM fallback", () => {
expect(ctx).toBeNull();
});
it("uses a per-account session key for default-account DMs", async () => {
it("uses the main session key for default-account DMs", async () => {
setRuntimeConfigSnapshot(baseCfg);
const ctx = await buildTelegramMessageContextForTest({
@@ -154,7 +154,7 @@ describe("buildTelegramMessageContext named-account DM fallback", () => {
},
});
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:default:direct:42");
expect(getLastUpdateLastRoute()?.sessionKey).toBe("agent:main:telegram:default:direct:42");
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main");
expect(getLastUpdateLastRoute()?.sessionKey).toBe("agent:main:main");
});
});

View File

@@ -6,8 +6,9 @@ import {
import { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
import type { TelegramDirectConfig, TelegramGroupConfig } from "openclaw/plugin-sdk/config-runtime";
import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing";
import { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
import { normalizeAccountId, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { resolveDefaultTelegramAccountId } from "./accounts.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from "./bot-access.js";
import { resolveTelegramInboundBody } from "./bot-message-context.body.js";
@@ -234,7 +235,10 @@ export const buildTelegramMessageContext = async ({
});
const requiresExplicitAccountBinding = (
candidate: ReturnType<typeof resolveTelegramConversationRoute>["route"],
): boolean => candidate.accountId !== DEFAULT_ACCOUNT_ID && candidate.matchedBy === "default";
): boolean =>
normalizeAccountId(candidate.accountId) !==
normalizeAccountId(resolveDefaultTelegramAccountId(freshCfg)) &&
candidate.matchedBy === "default";
const isNamedAccountFallback = requiresExplicitAccountBinding(route);
// Named-account groups still require an explicit binding; DMs get a
// per-account fallback session key below to preserve isolation.

View File

@@ -1697,7 +1697,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expect(payload.AccountId).toBe("opie");
expect(payload.SessionKey).toBe("agent:opie:telegram:opie:direct:999");
expect(payload.SessionKey).toBe("agent:opie:main");
});
it("reloads DM routing bindings between messages without recreating the bot", async () => {
@@ -1705,7 +1705,12 @@ describe("createTelegramBot", () => {
const configForAgent = (agentId: string) => ({
channels: {
telegram: {
defaultAccount: "work",
accounts: {
work: {
botToken: "tok-work",
dmPolicy: "open",
},
opie: {
botToken: "tok-opie",
dmPolicy: "open",
@@ -1809,7 +1814,12 @@ describe("createTelegramBot", () => {
loadConfig.mockReturnValue({
channels: {
telegram: {
defaultAccount: "work",
accounts: {
work: {
botToken: "tok-work",
dmPolicy: "open",
},
opie: {
botToken: "tok-opie",
dmPolicy: "open",

View File

@@ -2190,9 +2190,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expect(payload.CommandTargetSessionKey).toBe(
"agent:main:telegram:default:direct:12345:thread:12345:99",
);
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:12345:99");
});
it("allows native DM commands for paired users", async () => {

View File

@@ -6,7 +6,7 @@ import { resolveTelegramConversationBaseSessionKey } from "./conversation-route.
describe("resolveTelegramConversationBaseSessionKey", () => {
const cfg: OpenClawConfig = {};
it("uses a per-account key for default-account DMs", () => {
it("keeps default-account DMs on the route session key", () => {
expect(
resolveTelegramConversationBaseSessionKey({
cfg,
@@ -20,7 +20,34 @@ describe("resolveTelegramConversationBaseSessionKey", () => {
isGroup: false,
senderId: 12345,
}),
).toBe("agent:main:telegram:default:direct:12345");
).toBe("agent:main:main");
});
it("keeps configured default-account DMs on the route session key", () => {
expect(
resolveTelegramConversationBaseSessionKey({
cfg: {
channels: {
telegram: {
defaultAccount: "work",
accounts: {
work: {},
personal: {},
},
},
},
},
route: {
agentId: "main",
accountId: "work",
matchedBy: "default",
sessionKey: "agent:main:main",
},
chatId: 12345,
isGroup: false,
senderId: 12345,
}),
).toBe("agent:main:main");
});
it("uses the per-account fallback key for named-account DMs without an explicit binding", () => {

View File

@@ -7,11 +7,13 @@ import {
import {
buildAgentSessionKey,
deriveLastRoutePolicy,
normalizeAccountId,
resolveAgentRoute,
} from "openclaw/plugin-sdk/routing";
import { buildAgentMainSessionKey, sanitizeAgentId } from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { resolveDefaultTelegramAccountId } from "./accounts.js";
import {
buildTelegramGroupPeerId,
buildTelegramParentPeer,
@@ -147,10 +149,13 @@ export function resolveTelegramConversationBaseSessionKey(params: {
isGroup: boolean;
senderId?: string | number | null;
}): string {
if (params.isGroup || params.route.matchedBy === "binding.channel") {
const routeAccountId = normalizeAccountId(params.route.accountId);
const defaultAccountId = normalizeAccountId(resolveDefaultTelegramAccountId(params.cfg));
const isNamedAccountFallback =
routeAccountId !== defaultAccountId && params.route.matchedBy === "default";
if (!isNamedAccountFallback || params.isGroup) {
return params.route.sessionKey;
}
const configuredDmScope = params.cfg.session?.dmScope;
return normalizeLowercaseStringOrEmpty(
buildAgentSessionKey({
agentId: params.route.agentId,
@@ -163,10 +168,7 @@ export function resolveTelegramConversationBaseSessionKey(params: {
senderId: params.senderId,
}),
},
dmScope:
configuredDmScope && configuredDmScope !== "main"
? configuredDmScope
: "per-account-channel-peer",
dmScope: "per-account-channel-peer",
identityLinks: params.cfg.session?.identityLinks,
}),
);