From 4e626e91dbcae2996295b484ce0052e013bf13e7 Mon Sep 17 00:00:00 2001 From: zeroaltitude Date: Tue, 3 Mar 2026 21:46:59 -0700 Subject: [PATCH] fix(channels): require full token override for independence, trim whitespace tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Named accounts must now override ALL base token fields (not just any one) to be considered independent. Partial overrides (e.g. only appToken when base has both botToken+appToken) are correctly treated as inheriting the base identity. - Base token check now trims whitespace — ' ' is not a valid token. - Added 2 test cases: partial override, whitespace-only tokens. Addresses codex-connector P1+P2 on PR #30310. --- src/channels/plugins/account-helpers.test.ts | 28 +++++++++++++++++++- src/channels/plugins/account-helpers.ts | 16 ++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/channels/plugins/account-helpers.test.ts b/src/channels/plugins/account-helpers.test.ts index 8262c8795c6..2d4a40088e9 100644 --- a/src/channels/plugins/account-helpers.test.ts +++ b/src/channels/plugins/account-helpers.test.ts @@ -88,7 +88,7 @@ describe("createAccountListHelpers", () => { testchannel: { botToken: "xoxb-base", appToken: "xapp-base", - accounts: { tank: { botToken: "xoxb-tank" } }, + accounts: { tank: { botToken: "xoxb-tank", appToken: "xapp-tank" } }, }, }, } as unknown as OpenClawConfig; @@ -108,6 +108,32 @@ describe("createAccountListHelpers", () => { expect(listAccountIds(config)).toEqual(["teamA", "teamB"]); }); + it("does NOT inject default when account partially overrides (only appToken)", () => { + const config = { + channels: { + testchannel: { + botToken: "xoxb-base", + appToken: "xapp-base", + accounts: { teamA: { appToken: "xapp-own" } }, + }, + }, + } as unknown as OpenClawConfig; + // teamA overrides appToken but inherits botToken — still same bot identity + expect(listAccountIds(config)).toEqual(["teamA"]); + }); + + it("does NOT inject default when base token is whitespace-only", () => { + const config = { + channels: { + testchannel: { + botToken: " ", + accounts: { teamA: {} }, + }, + }, + } as unknown as OpenClawConfig; + expect(listAccountIds(config)).toEqual(["teamA"]); + }); + it("does NOT inject default when named accounts inherit base tokens (avoids duplicates)", () => { const config = { channels: { diff --git a/src/channels/plugins/account-helpers.ts b/src/channels/plugins/account-helpers.ts index 210ca7a230f..f3e66537b82 100644 --- a/src/channels/plugins/account-helpers.ts +++ b/src/channels/plugins/account-helpers.ts @@ -55,12 +55,22 @@ export function createAccountListHelpers( // default would start a duplicate provider on the same credentials. const channel = cfg.channels?.[channelKey]; const base = channel as Record | undefined; - const hasBaseTokens = Boolean(base?.botToken || base?.appToken || base?.token); - if (hasBaseTokens) { + const isTruthy = (v: unknown): boolean => + typeof v === "string" ? v.trim().length > 0 : Boolean(v); + // Identify which token fields the base config provides. + const baseTokenFields = (["botToken", "appToken", "token"] as const).filter((f) => + isTruthy(base?.[f]), + ); + if (baseTokenFields.length > 0) { const accounts = (base?.accounts ?? {}) as Record>; const everyAccountHasOwnTokens = ids.every((id) => { const acct = accounts[id]; - return acct && Boolean(acct.botToken || acct.appToken || acct.token); + if (!acct) { + return false; + } + // An account is only independent if it overrides *every* base token + // field. Partial overrides still inherit the remaining base tokens. + return baseTokenFields.every((f) => isTruthy(acct[f])); }); if (everyAccountHasOwnTokens) { // Every named account has distinct credentials, so default won't