fix(discord): tolerate unresolved SecretRef in resolveDiscordToken (fixes #76371)#76385
fix(discord): tolerate unresolved SecretRef in resolveDiscordToken (fixes #76371)#76385mogglemoss wants to merge 3 commits intoopenclaw:mainfrom
Conversation
The externalized @openclaw/discord plugin (2026.5.2) crashes channel
startup when channels.discord.token is configured as a SecretRef object,
because normalizeDiscordToken used the strict normalizeResolvedSecretInputString
which throws when the value is an unresolved SecretRef. The channel-startup
path reads raw config (before the gateway's secret resolver has materialized
SecretRefs into a runtime snapshot), so the strict assertion deterministically
fails the channel start with:
[discord] channel startup failed: channels.discord.token: unresolved
SecretRef "env:default:DISCORD_BOT_TOKEN". Resolve this command against
an active gateway runtime snapshot before reading it.
Switch to the gentle normalizeSecretInputString so unresolved SecretRefs
return undefined and let the existing fall-through chain (account-token →
top-level token → process.env.DISCORD_BOT_TOKEN → source=none) handle the
common case where the SecretRef intent matches the env fallback. Configurations
without an env fallback still surface a loud user-actionable error from
createDiscordRestClient ("Discord bot token missing for account ...") rather
than the internal SecretRef contract error.
Updates the existing throw assertions to reflect the new graceful behavior:
- token.test.ts: SecretRef + env present → returns env token (source=env);
SecretRef + no env → returns source=none without throwing; account-level
SecretRef respects the existing "explicit blank means no inheritance"
semantics (source=none, no inherit from base).
- client.test.ts: createDiscordRestClient surfaces the user-actionable
"bot token missing" error instead of the raw SecretRef contract error.
Refs openclaw#76371
AI-assisted (Claude / Sonnet 4.6, fully tested via `pnpm test:extension discord`,
1315/1315 tests passing).
|
Codex review: needs changes before merge. Summary Reproducibility: yes. Current main has a high-confidence source path: raw Discord token SecretRefs reach Next step before merge Security Review findings
Review detailsBest possible solution: Make Discord mirror Telegram-style inspected env SecretRef resolution for both account and top-level tokens, with provider/default/allowlist enforcement and the bare Do we have a high-confidence way to reproduce the issue? Yes. Current main has a high-confidence source path: raw Discord token SecretRefs reach Is this the best way to solve the issue? No. The provider-policy gate addresses the earlier security concern, but the patch should resolve configured env SecretRefs by their own Full review comments:
Overall correctness: patch is incorrect Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against dda2cf4e73b3. |
Addresses Codex review feedback on the prior commit:
- [P1] Previously the gentle normalize made *every* SecretRef look absent,
so a configured non-env SecretRef ({source:"file"}, {source:"exec"},
or {source:"env",id:"DISCORD_WORK_TOKEN"}) at top-level config could
silently fall through to process.env.DISCORD_BOT_TOKEN, potentially
starting the wrong bot account when DISCORD_BOT_TOKEN happened to be
set for a different deployment.
Refined resolveDiscordToken to inspect the unresolved SecretRef via
coerceSecretRef before allowing fallthrough: only refs whose intent
matches the existing env fallback ({source:"env", id:"DISCORD_BOT_TOKEN"})
fall through. All other SecretRef shapes preserve operator intent and
return {token:"", source:"none"} so the user-actionable "Discord bot
token missing" error from createDiscordRestClient surfaces cleanly
instead of an unrelated env token being substituted.
- [P3] Added Unreleased changelog entry under the Fixes section per repo
policy for user-facing fixes.
New test coverage:
- file-source SecretRef + DISCORD_BOT_TOKEN env present -> source=none
(no env substitution)
- env-source SecretRef with different id (DISCORD_PROD_BOT_TOKEN) +
DISCORD_BOT_TOKEN env present -> source=none
- exec-source SecretRef + DISCORD_BOT_TOKEN env present -> source=none
All earlier-added test cases still pass (matching env SecretRef ->
source=env; matching SecretRef + no env -> source=none; account-level
SecretRef -> no inheritance).
Refs openclaw#76371, openclaw#76385.
|
@clawsweeper Thanks for the review — both findings addressed in [P1] Preserve configured SecretRefs before using env fallback — addressed.
Three new unit cases lock this in:
[P3] Add the required changelog entry — addressed. Added under Re-validation: Diff is now |
Addresses Codex review [P1] on the previous commit (validate SecretRef
providers before env fallback): the prior matchesEnvFallback gate only
checked source==="env" and id===DISCORD_BOT_TOKEN. A SecretRef wired to
a misconfigured provider (e.g. provider source mismatch, missing
non-default provider, or env provider with an allowlist that excludes
the id) could still bypass operator secret-provider policy and read
process.env.DISCORD_BOT_TOKEN, potentially starting the wrong bot
account under operator-rejected credentials.
Mirror Telegram's resolveEnvSecretRefValue policy gate
(extensions/telegram/src/token.ts) inline, returning false (which now
short-circuits to source=none) when:
1. The provider is configured but its source is not "env".
2. The provider is configured with an allowlist that excludes the id.
3. The provider is not configured AND is not the runtime-resolved
default env-provider alias.
Only refs that pass all three checks (in addition to the source/id match)
fall through to process.env. Operator policy is preserved end-to-end.
Refactored helper kept inline (extension-local) rather than promoted to
plugin-sdk per CONTRIBUTING ("refactor-only PRs ... not accepted"). If a
maintainer wants this consolidated with telegram's helper as part of a
follow-up, happy to do it as a separate change.
New tests (4):
- env SecretRef + provider source mismatch -> source=none
- env SecretRef + provider allowlist excludes id -> source=none
- env SecretRef + provider not configured and not default alias -> source=none
- env SecretRef + provider configured with matching allowlist -> source=env
Updated changelog entry to call out the provider-policy gate.
Refs openclaw#76371, openclaw#76385.
|
@clawsweeper Round 2 addressed in [P1] Validate SecretRef providers before env fallback — implemented inline as
Anything that fails any of those returns I kept the helper inline (extension-local) rather than promoting Tests (
Updated changelog entry to call out the provider-policy gate. Branch diff vs |
|
Closing in favor of #76383 (@neeravmakwana, opened 4 minutes earlier for the same bug originally filed as #76371). Their PR uses the broader Telegram-style approach — resolves env SecretRefs by their own Cleaner to consolidate on one PR than to have maintainers triage two converging implementations of the same fix. Cross-linked from the upstream issue (#76371) and from #76383 itself. Thanks @clawsweeper for the careful three rounds of review — they tightened my approach (broad fallback → matching-id only → matching-id + provider policy gate) even though the final landing spot is to defer. The provider-policy gate I added in |
|
Thanks @mogglemoss for the patch. The merged fix in #76449 took the broader external channel contract approach, so Discord SecretRef-backed startup tokens are resolved before the token path needs to handle unresolved refs. This PR had already been closed, and the merged fix is now in place. |
Summary
@openclaw/discord2026.5.2 channel startup crashes deterministically whenchannels.discord.tokenis configured as a SecretRef object (e.g.{ id: "DISCORD_BOT_TOKEN", provider: "default", source: "env" }), failing every gateway restart with[discord] channel startup failed: channels.discord.token: unresolved SecretRef "env:default:DISCORD_BOT_TOKEN". Resolve this command against an active gateway runtime snapshot before reading it.dist/extensions/discord) for SecretRef users. Filed as [Bug] @openclaw/discord 2026.5.2 channel startup crashes on SecretRef-backed token (env:default:DISCORD_BOT_TOKEN unresolved) #76371.normalizeDiscordTokennow uses the gentlenormalizeSecretInputStringinstead of the strictnormalizeResolvedSecretInputString. Unresolved SecretRefs returnundefinedand let the existing fall-through chain (account-token → top-level token →process.env.DISCORD_BOT_TOKEN→source=none) handle the common case where the env-source SecretRef's intent matches the existing env fallback. Two existing throw-assertions updated; two new tests added covering the SecretRef-without-env path and the account-level SecretRef + base-token interaction.resolveDiscordToken's fall-through ordering, theBotprefix stripping, theinspectConfiguredToken"configured but unavailable" path, thehasAccountToken"explicit blank means no inheritance" semantics, any caller ofnormalizeDiscordTokenoutsideresolveDiscordTokenitself, any code outsideextensions/discord/. No SDK contract change.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
resolved-snapshot-vs-raw-configfamily)@openclaw/bluebubbles2026.5.2 webhook auth SecretRef crash, same 2026.5.2 externalization wave)Root Cause (if applicable)
extensions/discord/src/token.ts:normalizeDiscordTokeninvokednormalizeResolvedSecretInputString(strict mode → callsassertSecretInputResolved→ throwscreateUnresolvedSecretInputErroron any unresolved SecretRef). The channel-startup path (startChannel(plugin.id)→ discord plugin →resolveDiscordAccount→resolveDiscordToken→normalizeDiscordToken) reads rawOpenClawConfigbefore the gateway has materialized channel SecretRefs into a runtime snapshot, so the assertion fires deterministically and crashes the channel start.channels.discord.token. The existingtoken.test.ts > "throws when token is an unresolved SecretRef object"documented the throw as intentional, which made it look like a contract — but in practice the throw is unreachable from the runtime-snapshot consumers (which do pre-resolve) and only ever fires from raw-config consumers like channel startup, where it is the bug rather than the guard.dist/extensions/discord(bundled) to@openclaw/discord(npm). The bundled 2026.4.29 startup path apparently happened to feedresolveDiscordTokena config where SecretRefs had already been coerced upstream; the externalized startup path receives raw config. Telegram dodges the same family because its token resolver does not call the strict variant.Regression Test Plan (if applicable)
extensions/discord/src/token.test.ts(and existingextensions/discord/src/client.test.tsupdated to assert the new user-actionable error).resolveDiscordToken({ channels: { discord: { token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" } } } })withprocess.env.DISCORD_BOT_TOKEN="..."returns{ token: "...", source: "env" }and does not throw. Without the env, returns{ token: "", source: "none" }. With an account-level SecretRef + base token, returns{ token: "", source: "none" }(preserves "explicit blank means no inheritance")."throws when token is an unresolved SecretRef object"test asserted the opposite — the bug as a feature).User-visible / Behavior Changes
channels.discord.tokenis a SecretRef object whose intent matches the env fallback (the very common{ source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" }shape)."Discord bot token missing for account "default" (set discord.accounts.default.token or DISCORD_BOT_TOKEN for default)."(fromcreateDiscordRestClient) instead of the internal"channels.discord.token: unresolved SecretRef …. Resolve this command against an active gateway runtime snapshot before reading it."— clearer guidance, same loud failure.inspectConfiguredToken"configured but unavailable" status all behave as before.Diagram (if applicable)
Security Impact (required)
NoYes(in spirit only — no new disclosure path; previously a SecretRef-shaped token crashed the channel before any token was used; now it falls through to the same env fallback that already existed in the same function for the no-config case. No new sink for token bytes;Botprefix stripping preserved; explicit-blank account semantics preserved.)NoNoNoenv:default:DISCORD_BOT_TOKENpreviously crashed; it will now silently fall through to the env fallback ifDISCORD_BOT_TOKENhappens to be set. This is the same fall-through behavior that already exists for the "no config token" case, so it does not introduce a new path — but it does mean the intent expressed in a non-env SecretRef (e.g.source: "exec") becomes invisible at the channel-start site. Mitigation: callers that need strict resolution semantics (i.e. consumers of an actual runtime snapshot) should callnormalizeResolvedSecretInputStringdirectly; this PR only changes the channel-start consumer's behavior. The user-actionable "bot token missing" error still fires loudly when no token resolves.Repro + Verification
Environment
launchd(gui/502/ai.openclaw.gateway), local mode~/.openclaw/.envcontainsDISCORD_BOT_TOKEN=...(autoloaded via dotenv; verified inprocess.envof the gateway child process).Steps
2026.5.2(theopenclaw doctorstep ofopenclaw updateauto-installs@openclaw/discord@2026.5.2from npm).channels.discord.tokenas the SecretRef object above; ensureDISCORD_BOT_TOKENis in env (any of:~/.openclaw/.env, the LaunchAgent env file, or the parent shell).launchctl kickstart -k gui/502/ai.openclaw.gateway)./tmp/openclaw/openclaw-<date>.logfordiscordevents around startup.Expected
discord channels resolved→discord client initialized as <bot-id>→gateway ready(nochannel startup failedlines).Actual (before this PR)
Evidence
normalizeResolvedSecretInputString→assertSecretInputResolved(dist/types.secrets-CyuYZNvB.js).Human Verification (required)
2026.5.2install with the SecretRef config above (multiple gateway restarts, deterministic).channels.discord.tokenso the env fallback fires unconditionally) and confirmed Discord channel starts cleanly — same code path this PR makes available with the SecretRef present.pnpm install+pnpm test:extension discordonupstream/main→ 1313/1313 pass (baseline).pnpm test:extension discord→ 1315/1315 pass.normalizeDiscordTokencall sites (accounts.ts,setup-account-state.ts,setup-surface.ts,directory-live.ts,probe.ts,resolve-allowlist-common.ts,client.tsviaresolveDiscordToken); none rely on the strict-throw behavior other than the one test that asserted it directly.inspectConfiguredTokencodepath — the gentle pattern this PR adopts is already used there for the same reason ("configured but unavailable").DISCORD_BOT_TOKENset in env → returnssource=env, channel starts.source=none, channel reports user-actionable "bot token missing" error (no internal SecretRef contract error).source=nonefor that account (preserves existing "explicit blank means no inheritance" semantics).Bot <token>prefix stripping → preserved (covered by the unchanged regex).pnpm build && pnpm check && pnpm test(the full repo lane) locally — left to CI. Nosrc/**,src/plugin-sdk/**, orpackages/**files were touched, so the import-boundary inventories listed inCONTRIBUTING.mdshould not be required.codex review --base origin/mainlocally (no Codex subscription on this box). Will address Codex/bot review comments after CI runs them.Review Conversations
Compatibility / Migration
Yes— string-token configs unchanged; SecretRef configs that previously crashed now succeed via the env fallback (or surface a user-actionable error if no env).No— existing configs work; the docstring inextensions/discord/src/doctor.ts:256("verify DISCORD_BOT_TOKEN is present in the state-dir .env or configure channels.discord.token / channels.discord.accounts.default.token as a SecretRef") becomes accurate again instead of misleading.No.Risks and Mitigations
{ source: "exec", … }) previously threw at channel start; with this PR it falls through to env. If the operator intended an exec-resolved token but only hasDISCORD_BOT_TOKENset in env, the operator's stated intent is silently overridden by the env fallback.tokenSecretRefs in core beforestartChannel) would handle this more cleanly; seeextensions/discord/src/token.tscomment added in this PR. A future PR can add a "configured but unresolved" warning log if this surprises operators in practice.normalizeResolvedSecretInputStringandassertSecretInputResolvedfor runtime-snapshot consumers. This PR only changes the one call site that was systematically misusing the strict variant.AI/Vibe-coded disclosure
codex review --base origin/mainand address findings before requesting review (no Codex subscription on this box; will rely on CI Codex review).Session origin
This PR was authored mid-incident while debugging an actual production gateway hitting #76371 on a fresh
2026.5.2install. Condensed session log:launchctl kickstart -k) cleared transient saturation but the channel-startup error reproduced deterministically across multiple restarts.DISCORD_BOT_TOKENdirectly to the LaunchAgent env file (so it was inprocess.envfrom the very first instruction). Same crash. Conclusion: the bug is in the SecretRef strict-resolver code path, not env availability.@openclaw/discordsource for the literal error message:extensions/discord/src/token.ts:normalizeDiscordToken→normalizeResolvedSecretInputString(strict).inspectConfiguredTokeninsetup-account-state.ts— same plugin, same author intent, gentle handling of SecretRef objects vianormalizeSecretInputString.@openclaw/bluebubbleswebhook path), confirmed no existing report for the channel-startup case, filed [Bug] @openclaw/discord 2026.5.2 channel startup crashes on SecretRef-backed token (env:default:DISCORD_BOT_TOKEN unresolved) #76371.normalizeDiscordTokenwas wrong because the existing test ("throws when token is an unresolved SecretRef object") explicitly asserted the throw — but on closer reading, the throw was a debug aid, not a feature, and the gentle path already exists in the same plugin. Updated the assertion to lock in the new graceful behavior.channels.discord.tokenfrom config so env fallback unconditionally fires") on the real instance — Discord channel starts cleanly. That validates the same code path this PR enables with the SecretRef config preserved.The session prompt thread is available on request; happy to share if useful for the AI-PR-tracking maintainers do.