fix(channels): include default account when base config has tokens [claude, human developer oversight]#30310
fix(channels): include default account when base config has tokens [claude, human developer oversight]#30310zeroaltitude wants to merge 19 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes multi-account channel startup when a base channel config carries auth tokens alongside an Confidence Score: 5/5Safe to merge — the core logic is correct, well-tested, and backward-compatible; remaining findings are minor suggestions. All findings are P2. The injection logic, disabled-account filtering, normalizedToRaw reverse-mapping, whitespace guard, and duplicate-default prevention all behave as specified. The hardcoded token field list is a reasonable scope constraint for the targeted channels, and the minor test coverage gap for the alreadyHasDefault branch does not hide a bug. No files require special attention before merging. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/channels/plugins/account-helpers.ts
Line: 64-66
Comment:
**Hardcoded token fields limit scope to Slack/Discord**
The list `["botToken", "appToken", "token"]` only covers channels that use exactly these field names. Any channel plugin whose auth fields differ (e.g. `apiKey`, `accessToken`, `webhookSecret`) will never have `baseTokenFields.length > 0`, so the default injection will silently not fire for those channels.
Consider threading an optional `tokenFields` parameter through `createAccountListHelpers` options so individual channel helpers can declare their own auth field names. If the intention is to keep this scoped to Slack/Discord for now, a brief comment calling that out would prevent the next channel author from wondering why injection doesn't work for them.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/channels/plugins/account-helpers.test.ts
Line: 221-243
Comment:
**`alreadyHasDefault` guard not exercised by these tests**
Both the `Default` and `default` variants have no own `botToken`, so `everyAccountHasOwnTokens` is `false` before the `alreadyHasDefault` check is ever reached. The guard itself is correct, but it's dead code from these tests' perspective — removing the guard wouldn't break either test.
The case that actually exercises it is a config where every account provides its own token values AND one of the account keys normalizes to `"default"`. Worth adding a targeted test for that shape to lock in the guard's behavior.
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "fix: guard implicit default against norm..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dced57a85b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5297354d8e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…erit base tokens - Don't inject 'default' when all named accounts lack per-account auth overrides (they'd use the same base credentials, causing duplicate provider connections and event processing) - Use normalizeAccountId for case-insensitive default detection - Applies to Slack (botToken/appToken) and Discord (token) alike Addresses review feedback from codex-connector on PR openclaw#30310.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 12007aa7e9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…okens Mixed configs (some accounts with own tokens, some inheriting base) would still duplicate credentials between default and inheriting accounts. Now requires every named account to carry per-account auth before injecting default. Addresses codex-connector P1 on PR openclaw#30310.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 474db9690a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…tespace tokens - 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 openclaw#30310.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6db4f95d8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
e6db4f9 to
62ed6ea
Compare
…erit base tokens - Don't inject 'default' when all named accounts lack per-account auth overrides (they'd use the same base credentials, causing duplicate provider connections and event processing) - Use normalizeAccountId for case-insensitive default detection - Applies to Slack (botToken/appToken) and Discord (token) alike Addresses review feedback from codex-connector on PR openclaw#30310.
…okens Mixed configs (some accounts with own tokens, some inheriting base) would still duplicate credentials between default and inheriting accounts. Now requires every named account to carry per-account auth before injecting default. Addresses codex-connector P1 on PR openclaw#30310.
…tespace tokens - 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 openclaw#30310.
62ed6ea to
1c94b59
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c94b594ef
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@joshavant — small account/channel/gateway fix, would appreciate your eyes on when you have a moment! thanks in advance! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82c8957fbd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
82c8957 to
9c3ff07
Compare
…counts Two fixes in response to review: 1. Use options.normalizeAccountId (caller-provided) instead of the imported normalizeAccountId when building the reverse map in everyAccountHasOwnTokens. Ensures the reverse map keys match the IDs returned by listConfiguredAccountIds() when a custom normalizer is used. 2. Filter disabled accounts before the everyAccountHasOwnTokens gate. Disabled accounts are skipped at channel startup; allowing them to block default injection would prevent the base account from starting in mixed enabled/disabled configs where all enabled accounts are independent.
f086588 to
1ec775e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ec775e361
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const isTruthy = (v: unknown): boolean => | ||
| typeof v === "string" ? v.trim().length > 0 : Boolean(v); |
There was a problem hiding this comment.
Count explicit blank token overrides as account-local auth
If a named account explicitly clears a token field, this gate now suppresses default even though runtime resolution no longer falls back to the base credentials. For example, with channels.discord.token plus channels.discord.accounts.ops.token: "", extensions/discord/src/token.ts:39-52 returns source none for ops because hasAccountToken blocks base fallback, and extensions/discord/src/shared.ts:74-80 then treats ops as unconfigured. listAccountIds() therefore returns only ops, so gateway startup never starts the working base account anymore.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid edge case. The current predicate only checks for truthy per-account tokens, so an explicit blank override (token: "") is treated as "inherits base" rather than "no fallback". In practice, users would simply omit the field rather than blank it out, so this is a very niche scenario. Deferring to a follow-up.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: faa4b76667
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38b11f96a8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| } | ||
| const enabledIds = configuredIds.filter((id) => { | ||
| const rawKey = normalizedToRaw.get(id) ?? id; |
There was a problem hiding this comment.
Stop remapping raw IDs through normalizedToRaw in token checks
Looking up each configured id via normalizedToRaw.get(id) can alias distinct raw account keys during everyAccountHasOwnTokens evaluation and produce false positives. Fresh evidence: when no normalizeAccountId option is provided, listConfiguredAccountIds returns raw keys unchanged, so a config containing both A and a is valid here; this line remaps a to the first normalized match, so the token-override check can read the same account twice, incorrectly inject default, and start an extra base-credential runtime alongside an inheriting account.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. The concern is real in theory but requires a config with case-distinct raw keys that collapse under normalization (e.g. accounts: { A: {}, a: {} }) — a config that's already broken at the channel-routing level since account lookups are case-insensitive. Deferring to a follow-up if this surfaces in practice; the mainline path is correct.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eea31ab468
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| } | ||
| const enabledIds = configuredIds.filter((id) => { | ||
| const rawKey = normalizedToRaw.get(id) ?? id; |
There was a problem hiding this comment.
Avoid aliasing distinct raw account keys in token checks
Using normalizedToRaw.get(id) ?? id here aliases case-distinct raw keys when no custom normalizer is provided (for example accounts: { A: {...}, a: {} }), so the everyAccountHasOwnTokens gate can read the same account twice and incorrectly inject default. Fresh evidence versus the prior thread: listConfiguredAccountIds() returns raw IDs unchanged when normalizeAccountId is not set, so both A and a do reach this remap path and can be conflated.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. The concern is real in theory but requires a config with case-distinct raw keys that collapse under normalization (e.g. accounts: { A: {}, a: {} }) — a config that's already broken at the channel-routing level since account lookups are case-insensitive. Deferring to a follow-up if this surfaces in practice; the mainline path is correct.
|
This pull request has been automatically marked as stale due to inactivity. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd8498565f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| } | ||
| const enabledIds = configuredIds.filter((id) => { | ||
| const rawKey = normalizedToRaw.get(id) ?? id; |
There was a problem hiding this comment.
Avoid aliasing raw account keys in token-ownership checks
This remaps each configured id through normalizedToRaw, but when no custom normalizer is provided listConfiguredAccountIds returns raw keys unchanged, so distinct raw keys that normalize the same (e.g. "Router D" and "router-d") are conflated here. The ownership check can then read the wrong account config and incorrectly conclude all accounts have independent tokens, injecting default and starting an extra base-credential runtime alongside an inheriting account. Fresh evidence against the earlier rebuttal is that raw IDs do reach this remap path when options.normalizeAccountId is absent.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. The concern is real in theory but requires a config with case-distinct raw keys that collapse under normalization (e.g. accounts: { A: {}, a: {} }) — a config that's already broken at the channel-routing level since account lookups are case-insensitive. Deferring to a follow-up if this surfaces in practice; the mainline path is correct.
When a configured account ID (e.g. 'Default') normalizes to 'default', skip injecting the implicit default to avoid duplicates in listAccountIds().
@openclaw-bot Hi! This branch continues to receive my improvements. Is there a reason to keep the stale label? |
|
@greptile-apps: I would appreciate a re-review after several sets of changes. Also, if you have the power to remove the stale label, I'd appreciate it. Or, if you think it's still appropriate, please explain why so I can attempt to do the right thing. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b7314e539
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const rawKey = normalizedToRaw.get(id) ?? id; | ||
| const acct = accounts[rawKey]; |
There was a problem hiding this comment.
Stop aliasing raw ids through normalizedToRaw lookups
When no normalizeAccountId option is provided, listConfiguredAccountIds returns raw account keys, but this remap still routes each id through normalizedToRaw. With case-distinct keys like accounts: { A: { token: "own" }, a: {} }, id === "a" resolves to raw key "A", so everyAccountHasOwnTokens can read the same account twice and incorrectly inject default; startup then runs both a and default, which can duplicate base-credential runtimes (for example Discord/Slack inheriting base tokens). Fresh evidence after the earlier thread: production callers such as extensions/discord/src/accounts.ts:21 and extensions/slack/src/accounts.ts:29 call createAccountListHelpers without a normalizer, so this path is reachable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. The concern is real in theory but requires a config with case-distinct raw keys that collapse under normalization (e.g. accounts: { A: {}, a: {} }) — a config that's already broken at the channel-routing level since account lookups are case-insensitive. Deferring to a follow-up if this surfaces in practice; the mainline path is correct.
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Normalized account ID collision can select wrong account credentials in listAccountIds
Description
Impact:
Vulnerable logic: const normalizedToRaw = new Map<string, string>();
for (const key of Object.keys(accounts)) {
const normalized = normalizeId(key);
if (normalized && !normalizedToRaw.has(normalized)) {
normalizedToRaw.set(normalized, key);
}
}
...
const rawKey = normalizedToRaw.get(id) ?? id;
const acct = accounts[rawKey];This is an ambiguity bug: a colliding normalized ID silently resolves to one of multiple possible raw keys based on object key order. RecommendationFail closed on collisions, or make collisions explicit. Option A (recommended): detect collisions and throw a configuration error so operators must fix ambiguous account IDs: const normalizedToRaw = new Map<string, string>();
for (const key of Object.keys(accounts)) {
const normalized = normalizeId(key);
if (!normalized) continue;
const existing = normalizedToRaw.get(normalized);
if (existing && existing !== key) {
throw new Error(
`Ambiguous account id: ${JSON.stringify(existing)} and ${JSON.stringify(key)} both normalize to ${JSON.stringify(normalized)}`,
);
}
normalizedToRaw.set(normalized, key);
}Option B: store all raw keys per normalized ID (e.g., Also consider applying the same collision handling anywhere 2. 🟡 Implicit injection of default account ID can start an unintended provider instance using base channel credentials
DescriptionIn
Vulnerable behavior (injected ID): if (everyAccountHasOwnTokens) {
const alreadyHasDefault = configuredIds.some((id) => normalizeId(id) === DEFAULT_ACCOUNT_ID);
if (!alreadyHasDefault) {
implicitId = DEFAULT_ACCOUNT_ID;
}
}
...
return listCombinedAccountIds({
configuredAccountIds: configuredIds,
implicitAccountId: implicitId,
fallbackAccountIdWhenEmpty: DEFAULT_ACCOUNT_ID,
});This is security-relevant because RecommendationRequire explicit operator intent before starting a Options (choose one):
Example safer logic (explicit default only): const configuredIds = listConfiguredAccountIds(cfg);
const hasExplicitDefault = configuredIds.some((id) => normalizeId(id) === DEFAULT_ACCOUNT_ID);
return listCombinedAccountIds({
configuredAccountIds: configuredIds,
// do not add implicit default; only fallback when empty
fallbackAccountIdWhenEmpty: DEFAULT_ACCOUNT_ID,
});If the goal is to run both base and named accounts, require Analyzed PR: #30310 at commit Last updated on: 2026-04-26T03:47:36Z |
|
Closing this as implemented after Codex automated review. Current main already handles the mixed legacy config shape through the supported repair/setup path: top-level single-account credentials such as Best possible solution: Close PR #30310 and keep the shipped doctor/setup promotion behavior as the product path. Mixed legacy configs should be repaired into explicit What I checked:
So I’m closing this as already implemented rather than keeping a duplicate issue open. Codex Review notes: model gpt-5.5, reasoning high; reviewed against e672b61417af; fix evidence: release v2026.4.24, commit cbcfdf62c729. |
Summary
Fix multi-account channel startup when a channel config includes base-level auth tokens and an
accountsmap. PreviouslylistAccountIds()could omit the implicit"default"account, so the base token provider never started. Users saw their named accounts connect, but the base bot stayed disconnected.Review focus:
src/channels/plugins/account-helpers.ts— default injection rules + token-field independence check.Repro (before)
Before: only
teamAprovider started → base bot never connected.After: both
default+teamAstart (two distinct credentials).Behavior / Contract (after)
listAccountIds()now:["default"]when there are no named accounts (unchanged)"default"(case-insensitive)botToken/appToken/token):"default"only when every named account fully overrides all base token fields (ensures distinct credentials, avoids duplicate provider connections)Why this shape?
Always injecting
"default"when base tokens exist causes duplicate providers/event handling when named accounts inherit base tokens (or only partially override them). This change makes the "inject default" behavior safe and deterministic.Tests
Adds targeted unit coverage for:
"default"present (case-insensitive) → no duplicationAI-Assisted Development 🤖
This PR was developed with AI assistance (Claude/OpenClaw agent "Tank").
pnpm checkclean.