From fbc1585b3fba596a3185ab7cdef0151db6f6237a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 1 Mar 2026 23:24:33 -0800 Subject: [PATCH] fix(pairing): handle missing accountId in allowFrom reads (#31369) * pairing: honor default account in allowFrom read when accountId omitted * changelog: credit pairing allowFrom fallback fix --- CHANGELOG.md | 1 + src/pairing/pairing-store.test.ts | 17 +++++++++++++++++ src/pairing/pairing-store.ts | 14 ++++++++------ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b3b9f75447..c3991404d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc. - Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204. - BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204. - Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204. diff --git a/src/pairing/pairing-store.test.ts b/src/pairing/pairing-store.test.ts index 9f0ba535711..34752372090 100644 --- a/src/pairing/pairing-store.test.ts +++ b/src/pairing/pairing-store.test.ts @@ -395,4 +395,21 @@ describe("pairing store", () => { expect(scoped).toEqual(["1002", "1001"]); }); }); + + it("uses default-account allowFrom when account id is omitted", async () => { + await withTempStateDir(async (stateDir) => { + await writeAllowFromFixture({ stateDir, channel: "telegram", allowFrom: ["1001"] }); + await writeAllowFromFixture({ + stateDir, + channel: "telegram", + accountId: DEFAULT_ACCOUNT_ID, + allowFrom: ["1002"], + }); + + const asyncScoped = await readChannelAllowFromStore("telegram", process.env); + const syncScoped = readChannelAllowFromStoreSync("telegram", process.env); + expect(asyncScoped).toEqual(["1002", "1001"]); + expect(syncScoped).toEqual(["1002", "1001"]); + }); + }); }); diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts index fe373b3ea1f..467a52d0572 100644 --- a/src/pairing/pairing-store.ts +++ b/src/pairing/pairing-store.ts @@ -225,6 +225,10 @@ function shouldIncludeLegacyAllowFromEntries(normalizedAccountId: string): boole return !normalizedAccountId || normalizedAccountId === DEFAULT_ACCOUNT_ID; } +function resolveAllowFromAccountId(accountId?: string): string { + return normalizePairingAccountId(accountId) || DEFAULT_ACCOUNT_ID; +} + function normalizeId(value: string | number): string { return String(value).trim(); } @@ -395,10 +399,9 @@ export async function readLegacyChannelAllowFromStore( export async function readChannelAllowFromStore( channel: PairingChannel, env: NodeJS.ProcessEnv = process.env, - accountId: string, + accountId?: string, ): Promise { - const normalizedAccountId = accountId.trim().toLowerCase(); - const resolvedAccountId = normalizedAccountId || DEFAULT_ACCOUNT_ID; + const resolvedAccountId = resolveAllowFromAccountId(accountId); if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) { return await readNonDefaultAccountAllowFrom({ @@ -427,10 +430,9 @@ export function readLegacyChannelAllowFromStoreSync( export function readChannelAllowFromStoreSync( channel: PairingChannel, env: NodeJS.ProcessEnv = process.env, - accountId: string, + accountId?: string, ): string[] { - const normalizedAccountId = accountId.trim().toLowerCase(); - const resolvedAccountId = normalizedAccountId || DEFAULT_ACCOUNT_ID; + const resolvedAccountId = resolveAllowFromAccountId(accountId); if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) { return readNonDefaultAccountAllowFromSync({