Skip to content

fix(channels): include default account when base config has tokens [claude, human developer oversight]#30310

Closed
zeroaltitude wants to merge 19 commits intoopenclaw:mainfrom
zeroaltitude:fix/channel-account-helpers
Closed

fix(channels): include default account when base config has tokens [claude, human developer oversight]#30310
zeroaltitude wants to merge 19 commits intoopenclaw:mainfrom
zeroaltitude:fix/channel-account-helpers

Conversation

@zeroaltitude
Copy link
Copy Markdown
Contributor

@zeroaltitude zeroaltitude commented Mar 1, 2026

Summary

Fix multi-account channel startup when a channel config includes base-level auth tokens and an accounts map. Previously listAccountIds() 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)

channels:
  slack:
    botToken: "xoxb-base"
    appToken: "xapp-base"
    accounts:
      teamA:
        botToken: "xoxb-teamA"
        appToken: "xapp-teamA"

Before: only teamA provider started → base bot never connected.
After: both default + teamA start (two distinct credentials).

Behavior / Contract (after)

listAccountIds() now:

  • Returns ["default"] when there are no named accounts (unchanged)
  • Does not inject another default when an account id already normalizes to "default" (case-insensitive)
  • If the base config has auth token fields (botToken / appToken / token):
    • Injects "default" only when every named account fully overrides all base token fields (ensures distinct credentials, avoids duplicate provider connections)
    • Otherwise returns only named accounts (prevents starting two providers on the same credentials)
  • Whitespace-only tokens are treated as "not present"

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 injected only when all accounts are independent
  • Mixed configs (some accounts inherit) → no injection
  • Partial override (only some token fields) → treated as inheriting
  • Whitespace-only base tokens → treated as absent
  • "default" present (case-insensitive) → no duplication

AI-Assisted Development 🤖

This PR was developed with AI assistance (Claude/OpenClaw agent "Tank").

  • Degree of testing: Well-tested — unit tests covering all injection/inheritance edge cases. pnpm check clean.
  • Human oversight: All changes reviewed and directed by Eddie Abrams
  • Understanding confirmed: The contributor understands multi-account channel startup and token inheritance semantics

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 1, 2026

Greptile Summary

This PR fixes multi-account channel startup when a base channel config carries auth tokens alongside an accounts map. listAccountIds() in src/channels/plugins/account-helpers.ts now injects the implicit "default" account only when every enabled named account fully overrides every base-level token field, preventing duplicate providers while ensuring the base bot starts. Test coverage is thorough for the documented cases.

Confidence Score: 5/5

Safe 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 AI
This 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

@zeroaltitude zeroaltitude changed the title fix(channels): include default account when base config has tokens fix(channels): include default account when base config has tokens [claude, human developer oversight] Mar 1, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
Comment thread src/channels/plugins/account-helpers.ts Outdated
zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 3, 2026
…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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 4, 2026
…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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
Comment thread src/channels/plugins/account-helpers.ts Outdated
zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 4, 2026
…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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
@zeroaltitude zeroaltitude force-pushed the fix/channel-account-helpers branch from e6db4f9 to 62ed6ea Compare March 7, 2026 17:57
zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 7, 2026
…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.
zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 7, 2026
…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.
zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 7, 2026
…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.
@zeroaltitude zeroaltitude force-pushed the fix/channel-account-helpers branch from 62ed6ea to 1c94b59 Compare March 7, 2026 19:49
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
@zeroaltitude
Copy link
Copy Markdown
Contributor Author

zeroaltitude commented Mar 8, 2026

@joshavant — small account/channel/gateway fix, would appreciate your eyes on when you have a moment! thanks in advance!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts
@zeroaltitude zeroaltitude force-pushed the fix/channel-account-helpers branch from 82c8957 to 9c3ff07 Compare March 8, 2026 19:57
…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.
@zeroaltitude zeroaltitude force-pushed the fix/channel-account-helpers branch from f086588 to 1ec775e Compare March 22, 2026 06:00
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +58 to +59
const isTruthy = (v: unknown): boolean =>
typeof v === "string" ? v.trim().length > 0 : Boolean(v);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/channels/plugins/account-helpers.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
}
}
const enabledIds = configuredIds.filter((id) => {
const rawKey = normalizedToRaw.get(id) ?? id;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
}
}
const enabledIds = configuredIds.filter((id) => {
const rawKey = normalizedToRaw.get(id) ?? id;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 12, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/channels/plugins/account-helpers.ts Outdated
}
}
const enabledIds = configuredIds.filter((id) => {
const rawKey = normalizedToRaw.get(id) ?? id;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().
@zeroaltitude
Copy link
Copy Markdown
Contributor Author

This pull request has been automatically marked as stale due to inactivity. Please add updates or it will be closed.

@openclaw-bot Hi! This branch continues to receive my improvements. Is there a reason to keep the stale label?

@zeroaltitude
Copy link
Copy Markdown
Contributor Author

@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.

@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Apr 16, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +87 to +88
const rawKey = normalizedToRaw.get(id) ?? id;
const acct = accounts[rawKey];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 22, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Normalized account ID collision can select wrong account credentials in listAccountIds
2 🟡 Medium Implicit injection of default account ID can start an unintended provider instance using base channel credentials
1. 🟡 Normalized account ID collision can select wrong account credentials in listAccountIds
Property Value
Severity Medium
CWE CWE-287
Location src/channels/plugins/account-helpers.ts:72-93

Description

listAccountIds builds a normalizedToRaw map that stores only the first raw account key for a given normalized ID. If two configured account keys normalize to the same value (e.g., "Router D" and "router-d"), subsequent colliding keys are ignored.

Impact:

  • The enabledIds and everyAccountHasOwnTokens checks may consult the wrong account object (the first raw key encountered), because lookups go through normalizedToRaw.get(id).
  • This can cause incorrect implicit default injection decisions and, more importantly, can lead to starting/resolving a provider under the wrong account configuration (credentials/auth confusion) when normalized IDs are used.

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.

Recommendation

Fail 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., Map<string,string[]>) and require an exact raw-key match when ambiguity exists.

Also consider applying the same collision handling anywhere resolveNormalizedAccountEntry is used so resolution is deterministic and safe.

2. 🟡 Implicit injection of default account ID can start an unintended provider instance using base channel credentials
Property Value
Severity Medium
CWE CWE-840
Location src/channels/plugins/account-helpers.ts:84-109

Description

In createAccountListHelpers().listAccountIds, a default account ID may be added even when the operator did not configure an explicit accounts.default entry.

  • Input/control: The operator configures one or more named accounts under channels.<channel>.accounts.
  • Condition: If the base channel config also contains token fields (botToken/appToken/token) and every enabled named account has its own token fields, the code injects DEFAULT_ACCOUNT_ID ("default") into the account ID list.
  • Security impact: The gateway uses plugin.config.listAccountIds(cfg) to decide which channel accounts to boot. Adding an implicit default causes an additional provider instance to start, and that instance resolves credentials from the base channel config (since accounts.default may not exist). This can:
    • create an unexpected connection using the base credentials (which might be broader-scope/legacy credentials),
    • bypass per-account lifecycle controls that live under accounts.<id> (e.g., accounts.default.enabled: false cannot be set if default is not configured / is injected),
    • lead to duplicate/extra providers that the operator did not intend to run.

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 listAccountIds() feeds directly into channel startup (see src/gateway/server-channels.ts), where each returned account ID is started as a runtime instance.

Recommendation

Require explicit operator intent before starting a default account runtime.

Options (choose one):

  1. Only include default when it is explicitly configured (accounts.default exists) or when no accounts are configured (current fallback behavior).

  2. Add a dedicated flag (e.g., channels.<channel>.startBaseAccount: true) that must be set to allow booting a base-credential runtime alongside named accounts.

  3. If you keep implicit injection, at minimum respect an explicit disable control at the channel root (e.g., channels.<channel>.enabled === false) and/or allow disabling the implicit default via config.

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 accounts.default to be present (even empty) so operators can set accounts.default.enabled: false when needed.


Analyzed PR: #30310 at commit c37f2b1

Last updated on: 2026-04-26T03:47:36Z

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 26, 2026

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 botToken, appToken, and token are moved into accounts.default when named accounts exist, so the default/base account becomes explicit instead of being synthesized at runtime. This also avoids the implicit-default startup risks raised in the PR review discussion.

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 channels.<channel>.accounts.default credentials rather than changing shared startup listing to synthesize an implicit default runtime; any remaining channel-specific edge cases should be tracked as focused doctor/setup repair issues.

What I checked:

  • Doctor repair seeds explicit default account: seedMissingDefaultAccountsFromSingleAccountBase() detects channel configs with named accounts and no default, moves top-level single-account values into accounts.default, deletes those keys from the channel root, and records the repair change. (src/commands/doctor/shared/legacy-config-core-normalizers.ts:121, e672b61417af)
  • Credential keys are covered by promotion contract: The shared single-account promotion key set includes token, tokenFile, botToken, and appToken, which covers the Slack/Discord token fields from the PR repro and related review comments. (src/channels/plugins/setup-promotion-helpers.ts:11, e672b61417af)
  • Doctor flow applies the compatibility migration: runDoctorConfigFlow() calls normalizeCompatibilityConfigValues() and applies the resulting mutation under the openclaw doctor --fix path, so the repair is reachable through the documented doctor workflow. (src/commands/doctor-config-flow.ts:115, e672b61417af)
  • Setup path preserves original account explicitly: moveSingleAccountChannelSectionToDefaultAccount() moves top-level account settings into the promoted account before adding or patching a non-default account, keeping the original account working without leaving duplicate account credentials at the channel root. (src/channels/plugins/setup-helpers.ts:432, e672b61417af)
  • Regression coverage for setup promotion: The setup wizard helper test verifies that patching a non-default account moves legacy top-level channel credentials/settings into accounts.default and removes them from the root channel section. (src/channels/plugins/setup-wizard-helpers.test.ts:1020, e672b61417af)
  • User-facing docs describe repair workflow: The channel configuration docs state that adding a non-default account promotes top-level single-account values into the account map, and that openclaw doctor --fix repairs already-mixed configs by moving those values into the promoted account, usually accounts.default. Public docs: docs/gateway/config-channels.md. (docs/gateway/config-channels.md:741, e672b61417af)

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.

@clawsweeper clawsweeper Bot closed this Apr 26, 2026
@zeroaltitude zeroaltitude deleted the fix/channel-account-helpers branch April 26, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant