diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f9e8a6a79..a79c30baba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Security/Config writes: block reserved prototype keys in account-id normalization and route account config resolution through own-key lookups, hardening `/allowlist` and account-scoped config paths against prototype-chain pollution. - Security/Exec: harden `safeBins` long-option validation by rejecting unknown/ambiguous GNU long-option abbreviations and denying sort filesystem-dependent flags (`--random-source`, `--temporary-directory`, `-T`), closing safe-bin denylist bypasses. Thanks @jiseoung. ## 2026.2.23 (Unreleased) diff --git a/src/auto-reply/chunk.ts b/src/auto-reply/chunk.ts index a40eebb82cb..780d57a1f5b 100644 --- a/src/auto-reply/chunk.ts +++ b/src/auto-reply/chunk.ts @@ -5,6 +5,7 @@ import type { ChannelId } from "../channels/plugins/types.js"; import type { OpenClawConfig } from "../config/config.js"; import { findFenceSpanAt, isSafeFenceBreak, parseFenceSpans } from "../markdown/fences.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; import { chunkTextByBreakResolver } from "../shared/text-chunking.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js"; @@ -39,17 +40,10 @@ function resolveChunkLimitForProvider( const normalizedAccountId = normalizeAccountId(accountId); const accounts = cfgSection.accounts; if (accounts && typeof accounts === "object") { - const direct = accounts[normalizedAccountId]; + const direct = resolveAccountEntry(accounts, normalizedAccountId); if (typeof direct?.textChunkLimit === "number") { return direct.textChunkLimit; } - const matchKey = Object.keys(accounts).find( - (key) => key.toLowerCase() === normalizedAccountId.toLowerCase(), - ); - const match = matchKey ? accounts[matchKey] : undefined; - if (typeof match?.textChunkLimit === "number") { - return match.textChunkLimit; - } } return cfgSection.textChunkLimit; } @@ -89,17 +83,10 @@ function resolveChunkModeForProvider( const normalizedAccountId = normalizeAccountId(accountId); const accounts = cfgSection.accounts; if (accounts && typeof accounts === "object") { - const direct = accounts[normalizedAccountId]; + const direct = resolveAccountEntry(accounts, normalizedAccountId); if (direct?.chunkMode) { return direct.chunkMode; } - const matchKey = Object.keys(accounts).find( - (key) => key.toLowerCase() === normalizedAccountId.toLowerCase(), - ); - const match = matchKey ? accounts[matchKey] : undefined; - if (match?.chunkMode) { - return match.chunkMode; - } } return cfgSection.chunkMode; } diff --git a/src/auto-reply/reply/block-streaming.ts b/src/auto-reply/reply/block-streaming.ts index 4dfd5bb92df..318da982238 100644 --- a/src/auto-reply/reply/block-streaming.ts +++ b/src/auto-reply/reply/block-streaming.ts @@ -2,6 +2,7 @@ import { getChannelDock } from "../../channels/dock.js"; import { normalizeChannelId } from "../../channels/plugins/index.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { BlockStreamingCoalesceConfig } from "../../config/types.js"; +import { resolveAccountEntry } from "../../routing/account-lookup.js"; import { normalizeAccountId } from "../../routing/session-key.js"; import { INTERNAL_MESSAGE_CHANNEL, @@ -45,7 +46,7 @@ function resolveProviderBlockStreamingCoalesce(params: { } const normalizedAccountId = normalizeAccountId(accountId); const typed = providerCfg as ProviderBlockStreamingConfig; - const accountCfg = typed.accounts?.[normalizedAccountId]; + const accountCfg = resolveAccountEntry(typed.accounts, normalizedAccountId); return accountCfg?.blockStreamingCoalesce ?? typed.blockStreamingCoalesce; } diff --git a/src/auto-reply/reply/commands-allowlist.ts b/src/auto-reply/reply/commands-allowlist.ts index 8bc5efb5152..1ba35827f0c 100644 --- a/src/auto-reply/reply/commands-allowlist.ts +++ b/src/auto-reply/reply/commands-allowlist.ts @@ -12,12 +12,17 @@ import { import { resolveDiscordAccount } from "../../discord/accounts.js"; import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js"; import { resolveIMessageAccount } from "../../imessage/accounts.js"; +import { isBlockedObjectKey } from "../../infra/prototype-keys.js"; import { addChannelAllowFromStoreEntry, readChannelAllowFromStore, removeChannelAllowFromStoreEntry, } from "../../pairing/pairing-store.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; +import { + DEFAULT_ACCOUNT_ID, + normalizeAccountId, + normalizeOptionalAccountId, +} from "../../routing/session-key.js"; import { resolveSignalAccount } from "../../signal/accounts.js"; import { resolveSlackAccount } from "../../slack/accounts.js"; import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js"; @@ -199,13 +204,22 @@ function resolveAccountTarget( const channels = (parsed.channels ??= {}) as Record; const channel = (channels[channelId] ??= {}) as Record; const normalizedAccountId = normalizeAccountId(accountId); + if (isBlockedObjectKey(normalizedAccountId)) { + return { target: channel, pathPrefix: `channels.${channelId}`, accountId: DEFAULT_ACCOUNT_ID }; + } const hasAccounts = Boolean(channel.accounts && typeof channel.accounts === "object"); const useAccount = normalizedAccountId !== DEFAULT_ACCOUNT_ID || hasAccounts; if (!useAccount) { return { target: channel, pathPrefix: `channels.${channelId}`, accountId: normalizedAccountId }; } const accounts = (channel.accounts ??= {}) as Record; - const account = (accounts[normalizedAccountId] ??= {}) as Record; + const existingAccount = Object.hasOwn(accounts, normalizedAccountId) + ? accounts[normalizedAccountId] + : undefined; + if (!existingAccount || typeof existingAccount !== "object") { + accounts[normalizedAccountId] = {}; + } + const account = accounts[normalizedAccountId] as Record; return { target: account, pathPrefix: `channels.${channelId}.accounts.${normalizedAccountId}`, @@ -361,6 +375,14 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo reply: { text: "⚠️ Unknown channel. Add channel= to the command." }, }; } + if (parsed.account?.trim() && !normalizeOptionalAccountId(parsed.account)) { + return { + shouldContinue: false, + reply: { + text: "⚠️ Invalid account id. Reserved keys (__proto__, constructor, prototype) are blocked.", + }, + }; + } const accountId = normalizeAccountId(parsed.account ?? params.ctx.AccountId); const scope = parsed.scope; diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index a402f8dd42b..0c4d40ec7eb 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -645,6 +645,22 @@ describe("handleCommands /allowlist", () => { expect(result.reply?.text).toContain("DM allowlist added"); }); + it("rejects blocked account ids and keeps Object.prototype clean", async () => { + delete (Object.prototype as Record).allowFrom; + + const cfg = { + commands: { text: true, config: true }, + channels: { telegram: { allowFrom: ["123"] } }, + } as OpenClawConfig; + const params = buildPolicyParams("/allowlist add dm --account __proto__ 789", cfg); + const result = await handleCommands(params); + + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("Invalid account id"); + expect((Object.prototype as Record).allowFrom).toBeUndefined(); + expect(writeConfigFileMock).not.toHaveBeenCalled(); + }); + it("removes DM allowlist entries from canonical allowFrom and deletes legacy dm.allowFrom", async () => { const cases = [ { diff --git a/src/channels/plugins/config-writes.ts b/src/channels/plugins/config-writes.ts index 6b86bdd495a..87e220d7029 100644 --- a/src/channels/plugins/config-writes.ts +++ b/src/channels/plugins/config-writes.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../../config/config.js"; +import { resolveAccountEntry } from "../../routing/account-lookup.js"; import { normalizeAccountId } from "../../routing/session-key.js"; import type { ChannelId } from "./types.js"; @@ -8,16 +9,7 @@ type ChannelConfigWithAccounts = { }; function resolveAccountConfig(accounts: ChannelConfigWithAccounts["accounts"], accountId: string) { - if (!accounts || typeof accounts !== "object") { - return undefined; - } - if (accountId in accounts) { - return accounts[accountId]; - } - const matchKey = Object.keys(accounts).find( - (key) => key.toLowerCase() === accountId.toLowerCase(), - ); - return matchKey ? accounts[matchKey] : undefined; + return resolveAccountEntry(accounts, accountId); } export function resolveChannelConfigWrites(params: { diff --git a/src/config/channel-capabilities.ts b/src/config/channel-capabilities.ts index 7e5bd75461c..0e66f755e3b 100644 --- a/src/config/channel-capabilities.ts +++ b/src/config/channel-capabilities.ts @@ -1,4 +1,5 @@ import { normalizeChannelId } from "../channels/plugins/index.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; import type { OpenClawConfig } from "./config.js"; import type { TelegramCapabilitiesConfig } from "./types.telegram.js"; @@ -32,14 +33,7 @@ function resolveAccountCapabilities(params: { const accounts = cfg.accounts; if (accounts && typeof accounts === "object") { - const direct = accounts[normalizedAccountId]; - if (direct) { - return normalizeCapabilities(direct.capabilities) ?? normalizeCapabilities(cfg.capabilities); - } - const matchKey = Object.keys(accounts).find( - (key) => key.toLowerCase() === normalizedAccountId.toLowerCase(), - ); - const match = matchKey ? accounts[matchKey] : undefined; + const match = resolveAccountEntry(accounts, normalizedAccountId); if (match) { return normalizeCapabilities(match.capabilities) ?? normalizeCapabilities(cfg.capabilities); } diff --git a/src/config/group-policy.ts b/src/config/group-policy.ts index 2c5c4b7aa62..fe8b1542a12 100644 --- a/src/config/group-policy.ts +++ b/src/config/group-policy.ts @@ -1,4 +1,5 @@ import type { ChannelId } from "../channels/plugins/types.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; import type { OpenClawConfig } from "./config.js"; import { @@ -293,13 +294,7 @@ function resolveChannelGroups( if (!channelConfig) { return undefined; } - const accountGroups = - channelConfig.accounts?.[normalizedAccountId]?.groups ?? - channelConfig.accounts?.[ - Object.keys(channelConfig.accounts ?? {}).find( - (key) => key.toLowerCase() === normalizedAccountId.toLowerCase(), - ) ?? "" - ]?.groups; + const accountGroups = resolveAccountEntry(channelConfig.accounts, normalizedAccountId)?.groups; return accountGroups ?? channelConfig.groups; } @@ -320,13 +315,10 @@ function resolveChannelGroupPolicyMode( if (!channelConfig) { return undefined; } - const accountPolicy = - channelConfig.accounts?.[normalizedAccountId]?.groupPolicy ?? - channelConfig.accounts?.[ - Object.keys(channelConfig.accounts ?? {}).find( - (key) => key.toLowerCase() === normalizedAccountId.toLowerCase(), - ) ?? "" - ]?.groupPolicy; + const accountPolicy = resolveAccountEntry( + channelConfig.accounts, + normalizedAccountId, + )?.groupPolicy; return accountPolicy ?? channelConfig.groupPolicy; } diff --git a/src/config/markdown-tables.ts b/src/config/markdown-tables.ts index 8815a90b139..2095cd87b33 100644 --- a/src/config/markdown-tables.ts +++ b/src/config/markdown-tables.ts @@ -1,4 +1,5 @@ import { normalizeChannelId } from "../channels/plugins/index.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; import type { OpenClawConfig } from "./config.js"; import type { MarkdownTableMode } from "./types.base.js"; @@ -31,15 +32,7 @@ function resolveMarkdownModeFromSection( const normalizedAccountId = normalizeAccountId(accountId); const accounts = section.accounts; if (accounts && typeof accounts === "object") { - const direct = accounts[normalizedAccountId]; - const directMode = direct?.markdown?.tables; - if (isMarkdownTableMode(directMode)) { - return directMode; - } - const matchKey = Object.keys(accounts).find( - (key) => key.toLowerCase() === normalizedAccountId.toLowerCase(), - ); - const match = matchKey ? accounts[matchKey] : undefined; + const match = resolveAccountEntry(accounts, normalizedAccountId); const matchMode = match?.markdown?.tables; if (isMarkdownTableMode(matchMode)) { return matchMode; diff --git a/src/config/prototype-keys.ts b/src/config/prototype-keys.ts index 9762aae019a..3ba47c293ef 100644 --- a/src/config/prototype-keys.ts +++ b/src/config/prototype-keys.ts @@ -1,5 +1 @@ -const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]); - -export function isBlockedObjectKey(key: string): boolean { - return BLOCKED_OBJECT_KEYS.has(key); -} +export { isBlockedObjectKey } from "../infra/prototype-keys.js"; diff --git a/src/discord/accounts.ts b/src/discord/accounts.ts index a5810de247d..33731b4260d 100644 --- a/src/discord/accounts.ts +++ b/src/discord/accounts.ts @@ -2,6 +2,7 @@ import { createAccountActionGate } from "../channels/plugins/account-action-gate import { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; import type { OpenClawConfig } from "../config/config.js"; import type { DiscordAccountConfig, DiscordActionConfig } from "../config/types.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; import { resolveDiscordToken } from "./token.js"; @@ -22,11 +23,7 @@ function resolveAccountConfig( cfg: OpenClawConfig, accountId: string, ): DiscordAccountConfig | undefined { - const accounts = cfg.channels?.discord?.accounts; - if (!accounts || typeof accounts !== "object") { - return undefined; - } - return accounts[accountId] as DiscordAccountConfig | undefined; + return resolveAccountEntry(cfg.channels?.discord?.accounts, accountId); } function mergeDiscordAccountConfig(cfg: OpenClawConfig, accountId: string): DiscordAccountConfig { diff --git a/src/discord/draft-chunking.ts b/src/discord/draft-chunking.ts index f238ed472af..76231bc8397 100644 --- a/src/discord/draft-chunking.ts +++ b/src/discord/draft-chunking.ts @@ -1,6 +1,7 @@ import { resolveTextChunkLimit } from "../auto-reply/chunk.js"; import { getChannelDock } from "../channels/dock.js"; import type { OpenClawConfig } from "../config/config.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; const DEFAULT_DISCORD_DRAFT_STREAM_MIN = 200; @@ -19,9 +20,8 @@ export function resolveDiscordDraftStreamingChunking( fallbackLimit: providerChunkLimit, }); const normalizedAccountId = normalizeAccountId(accountId); - const draftCfg = - cfg?.channels?.discord?.accounts?.[normalizedAccountId]?.draftChunk ?? - cfg?.channels?.discord?.draftChunk; + const accountCfg = resolveAccountEntry(cfg?.channels?.discord?.accounts, normalizedAccountId); + const draftCfg = accountCfg?.draftChunk ?? cfg?.channels?.discord?.draftChunk; const maxRequested = Math.max( 1, diff --git a/src/imessage/accounts.ts b/src/imessage/accounts.ts index 6c812ee68a8..d0ed6a9218c 100644 --- a/src/imessage/accounts.ts +++ b/src/imessage/accounts.ts @@ -1,6 +1,7 @@ import { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; import type { OpenClawConfig } from "../config/config.js"; import type { IMessageAccountConfig } from "../config/types.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; export type ResolvedIMessageAccount = { @@ -19,11 +20,7 @@ function resolveAccountConfig( cfg: OpenClawConfig, accountId: string, ): IMessageAccountConfig | undefined { - const accounts = cfg.channels?.imessage?.accounts; - if (!accounts || typeof accounts !== "object") { - return undefined; - } - return accounts[accountId] as IMessageAccountConfig | undefined; + return resolveAccountEntry(cfg.channels?.imessage?.accounts, accountId); } function mergeIMessageAccountConfig(cfg: OpenClawConfig, accountId: string): IMessageAccountConfig { diff --git a/src/infra/prototype-keys.ts b/src/infra/prototype-keys.ts new file mode 100644 index 00000000000..9762aae019a --- /dev/null +++ b/src/infra/prototype-keys.ts @@ -0,0 +1,5 @@ +const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]); + +export function isBlockedObjectKey(key: string): boolean { + return BLOCKED_OBJECT_KEYS.has(key); +} diff --git a/src/line/accounts.ts b/src/line/accounts.ts index c46fcff1791..28a65667342 100644 --- a/src/line/accounts.ts +++ b/src/line/accounts.ts @@ -4,6 +4,7 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId as normalizeSharedAccountId, } from "../routing/account-id.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import type { LineConfig, LineAccountConfig, @@ -104,10 +105,12 @@ export function resolveLineAccount(params: { cfg: OpenClawConfig; accountId?: string; }): ResolvedLineAccount { - const { cfg, accountId = DEFAULT_ACCOUNT_ID } = params; + const cfg = params.cfg; + const accountId = normalizeSharedAccountId(params.accountId); const lineConfig = cfg.channels?.line as LineConfig | undefined; const accounts = lineConfig?.accounts; - const accountConfig = accountId !== DEFAULT_ACCOUNT_ID ? accounts?.[accountId] : undefined; + const accountConfig = + accountId !== DEFAULT_ACCOUNT_ID ? resolveAccountEntry(accounts, accountId) : undefined; const { token, tokenSource } = resolveToken({ accountId, diff --git a/src/routing/account-id.test.ts b/src/routing/account-id.test.ts index bdaa45bbaac..4d9250e77ff 100644 --- a/src/routing/account-id.test.ts +++ b/src/routing/account-id.test.ts @@ -20,6 +20,15 @@ describe("account id normalization", () => { expect(normalizeAccountId(" Prod/US East ")).toBe("prod-us-east"); }); + it("rejects prototype-pollution key vectors", () => { + expect(normalizeAccountId("__proto__")).toBe(DEFAULT_ACCOUNT_ID); + expect(normalizeAccountId("constructor")).toBe(DEFAULT_ACCOUNT_ID); + expect(normalizeAccountId("prototype")).toBe(DEFAULT_ACCOUNT_ID); + expect(normalizeOptionalAccountId("__proto__")).toBeUndefined(); + expect(normalizeOptionalAccountId("constructor")).toBeUndefined(); + expect(normalizeOptionalAccountId("prototype")).toBeUndefined(); + }); + it("preserves optional semantics without forcing default", () => { expect(normalizeOptionalAccountId(undefined)).toBeUndefined(); expect(normalizeOptionalAccountId(" ")).toBeUndefined(); diff --git a/src/routing/account-id.ts b/src/routing/account-id.ts index 9488efebb80..aa561c0bbca 100644 --- a/src/routing/account-id.ts +++ b/src/routing/account-id.ts @@ -1,3 +1,5 @@ +import { isBlockedObjectKey } from "../infra/prototype-keys.js"; + export const DEFAULT_ACCOUNT_ID = "default"; const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i; @@ -17,12 +19,20 @@ function canonicalizeAccountId(value: string): string { .slice(0, 64); } +function normalizeCanonicalAccountId(value: string): string | undefined { + const canonical = canonicalizeAccountId(value); + if (!canonical || isBlockedObjectKey(canonical)) { + return undefined; + } + return canonical; +} + export function normalizeAccountId(value: string | undefined | null): string { const trimmed = (value ?? "").trim(); if (!trimmed) { return DEFAULT_ACCOUNT_ID; } - return canonicalizeAccountId(trimmed) || DEFAULT_ACCOUNT_ID; + return normalizeCanonicalAccountId(trimmed) || DEFAULT_ACCOUNT_ID; } export function normalizeOptionalAccountId(value: string | undefined | null): string | undefined { @@ -30,5 +40,5 @@ export function normalizeOptionalAccountId(value: string | undefined | null): st if (!trimmed) { return undefined; } - return canonicalizeAccountId(trimmed) || undefined; + return normalizeCanonicalAccountId(trimmed) || undefined; } diff --git a/src/routing/account-lookup.test.ts b/src/routing/account-lookup.test.ts new file mode 100644 index 00000000000..1960c8dd692 --- /dev/null +++ b/src/routing/account-lookup.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { resolveAccountEntry } from "./account-lookup.js"; + +describe("resolveAccountEntry", () => { + it("resolves direct and case-insensitive account keys", () => { + const accounts = { + default: { id: "default" }, + Business: { id: "business" }, + }; + expect(resolveAccountEntry(accounts, "default")).toEqual({ id: "default" }); + expect(resolveAccountEntry(accounts, "business")).toEqual({ id: "business" }); + }); + + it("ignores prototype-chain values", () => { + const inherited = { default: { id: "polluted" } }; + const accounts = Object.create(inherited) as Record; + expect(resolveAccountEntry(accounts, "default")).toBeUndefined(); + }); +}); diff --git a/src/routing/account-lookup.ts b/src/routing/account-lookup.ts new file mode 100644 index 00000000000..fc891306f67 --- /dev/null +++ b/src/routing/account-lookup.ts @@ -0,0 +1,14 @@ +export function resolveAccountEntry( + accounts: Record | undefined, + accountId: string, +): T | undefined { + if (!accounts || typeof accounts !== "object") { + return undefined; + } + if (Object.hasOwn(accounts, accountId)) { + return accounts[accountId]; + } + const normalized = accountId.toLowerCase(); + const matchKey = Object.keys(accounts).find((key) => key.toLowerCase() === normalized); + return matchKey ? accounts[matchKey] : undefined; +} diff --git a/src/signal/accounts.ts b/src/signal/accounts.ts index 09267f6c5c1..ed5732b9155 100644 --- a/src/signal/accounts.ts +++ b/src/signal/accounts.ts @@ -1,6 +1,7 @@ import { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; import type { OpenClawConfig } from "../config/config.js"; import type { SignalAccountConfig } from "../config/types.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; export type ResolvedSignalAccount = { @@ -20,11 +21,7 @@ function resolveAccountConfig( cfg: OpenClawConfig, accountId: string, ): SignalAccountConfig | undefined { - const accounts = cfg.channels?.signal?.accounts; - if (!accounts || typeof accounts !== "object") { - return undefined; - } - return accounts[accountId] as SignalAccountConfig | undefined; + return resolveAccountEntry(cfg.channels?.signal?.accounts, accountId); } function mergeSignalAccountConfig(cfg: OpenClawConfig, accountId: string): SignalAccountConfig { diff --git a/src/slack/accounts.ts b/src/slack/accounts.ts index f5d54b50980..65c49cfaa44 100644 --- a/src/slack/accounts.ts +++ b/src/slack/accounts.ts @@ -2,6 +2,7 @@ import { normalizeChatType } from "../channels/chat-type.js"; import { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; import type { OpenClawConfig } from "../config/config.js"; import type { SlackAccountConfig } from "../config/types.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; import { resolveSlackAppToken, resolveSlackBotToken } from "./token.js"; @@ -37,11 +38,7 @@ function resolveAccountConfig( cfg: OpenClawConfig, accountId: string, ): SlackAccountConfig | undefined { - const accounts = cfg.channels?.slack?.accounts; - if (!accounts || typeof accounts !== "object") { - return undefined; - } - return accounts[accountId] as SlackAccountConfig | undefined; + return resolveAccountEntry(cfg.channels?.slack?.accounts, accountId); } function mergeSlackAccountConfig(cfg: OpenClawConfig, accountId: string): SlackAccountConfig { diff --git a/src/telegram/accounts.ts b/src/telegram/accounts.ts index c608eac1987..9df2971801e 100644 --- a/src/telegram/accounts.ts +++ b/src/telegram/accounts.ts @@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/config.js"; import type { TelegramAccountConfig, TelegramActionConfig } from "../config/types.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { listBoundAccountIds, resolveDefaultAgentBoundAccountId } from "../routing/bindings.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; import { resolveTelegramToken } from "./token.js"; @@ -78,17 +79,8 @@ function resolveAccountConfig( cfg: OpenClawConfig, accountId: string, ): TelegramAccountConfig | undefined { - const accounts = cfg.channels?.telegram?.accounts; - if (!accounts || typeof accounts !== "object") { - return undefined; - } - const direct = accounts[accountId] as TelegramAccountConfig | undefined; - if (direct) { - return direct; - } const normalized = normalizeAccountId(accountId); - const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized); - return matchKey ? (accounts[matchKey] as TelegramAccountConfig | undefined) : undefined; + return resolveAccountEntry(cfg.channels?.telegram?.accounts, normalized); } function mergeTelegramAccountConfig(cfg: OpenClawConfig, accountId: string): TelegramAccountConfig { diff --git a/src/telegram/draft-chunking.ts b/src/telegram/draft-chunking.ts index e73a76ae8cc..3b4d5e30afb 100644 --- a/src/telegram/draft-chunking.ts +++ b/src/telegram/draft-chunking.ts @@ -1,6 +1,7 @@ import { resolveTextChunkLimit } from "../auto-reply/chunk.js"; import { getChannelDock } from "../channels/dock.js"; import type { OpenClawConfig } from "../config/config.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; const DEFAULT_TELEGRAM_DRAFT_STREAM_MIN = 200; @@ -19,9 +20,8 @@ export function resolveTelegramDraftStreamingChunking( fallbackLimit: providerChunkLimit, }); const normalizedAccountId = normalizeAccountId(accountId); - const draftCfg = - cfg?.channels?.telegram?.accounts?.[normalizedAccountId]?.draftChunk ?? - cfg?.channels?.telegram?.draftChunk; + const accountCfg = resolveAccountEntry(cfg?.channels?.telegram?.accounts, normalizedAccountId); + const draftCfg = accountCfg?.draftChunk ?? cfg?.channels?.telegram?.draftChunk; const maxRequested = Math.max( 1, diff --git a/src/web/accounts.ts b/src/web/accounts.ts index 41f9f2bd915..52fb5caabeb 100644 --- a/src/web/accounts.ts +++ b/src/web/accounts.ts @@ -4,6 +4,7 @@ import { createAccountListHelpers } from "../channels/plugins/account-helpers.js import type { OpenClawConfig } from "../config/config.js"; import { resolveOAuthDir } from "../config/paths.js"; import type { DmPolicy, GroupPolicy, WhatsAppAccountConfig } from "../config/types.js"; +import { resolveAccountEntry } from "../routing/account-lookup.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; import { resolveUserPath } from "../utils.js"; import { hasWebCredsSync } from "./auth-store.js"; @@ -68,12 +69,7 @@ function resolveAccountConfig( cfg: OpenClawConfig, accountId: string, ): WhatsAppAccountConfig | undefined { - const accounts = cfg.channels?.whatsapp?.accounts; - if (!accounts || typeof accounts !== "object") { - return undefined; - } - const entry = accounts[accountId] as WhatsAppAccountConfig | undefined; - return entry; + return resolveAccountEntry(cfg.channels?.whatsapp?.accounts, accountId); } function resolveDefaultAuthDir(accountId: string): string {