Bug type
Regression (worked before, now fails)
Summary
When configuring a channel (Telegram, Slack, or Discord) during openclaw onboard and choosing an exec secret provider for the bot token, the onboarding wizard crashes immediately after the "Reference validated" step with:
Error: channels.telegram.botToken: unresolved SecretRef “exec:xxx:telegram/apiKey".
Resolve this command against an active gateway runtime snapshot before reading it.
The onboarding never completes, so the channel configuration is never written to openclaw.json. The exec provider (e.g. 1Password op) is called once during validation and works correctly, but is never called again for actual resolution.
Steps to reproduce
- Configure an exec secret provider (e.g. 1Password CLI) in
openclaw.json:
"secrets": {
"providers": {
"my_provider": {
"source": "exec",
"command": "/opt/homebrew/bin/op",
"args": ["read", "op://Vault/Item/credential"],
"passEnv": ["HOME", "OP_SERVICE_ACCOUNT_TOKEN"],
"jsonOnly": false
}
}
}
- Run
openclaw onboard and select Telegram (or Slack/Discord)
- Choose "Use external secret provider" for the bot token
- Select the configured provider and enter a secret ID
- The reference validates successfully ("Reference validated")
- Crash occurs immediately after
Expected behavior
After validating the secret reference, onboarding should complete the remaining steps (allowFrom configuration, dmPolicy, etc.) and write the channel config to openclaw.json. The exec provider should either:
- Be called to resolve the token for immediate use during onboarding, or
- The onboarding flow should gracefully handle unresolved SecretRefs in contexts where only "is a token configured?" matters (not the actual token value)
Actual behavior
Crash:
Error: channels.telegram.botToken: unresolved SecretRef “exec:xxx:telegram/apiKey".
Resolve this command against an active gateway runtime snapshot before reading it.
OpenClaw version
2026.3.2
Operating system
macOS (Darwin 25.3.0)
Install method
install script.sh
Logs, screenshots, and evidence
applyOnboardingResult (onboard-channels)
→ refreshStatus("telegram")
→ telegramOnboardingAdapter.getStatus({ cfg }) # telegram.ts:154
→ listTelegramAccountIds(cfg).some(...) # telegram.ts:155
→ resolveTelegramAccount({ cfg, accountId }) # telegram.ts:156
→ resolveTelegramToken(cfg, { accountId }) # accounts.ts (via token.ts)
→ normalizeResolvedSecretInputString({ value }) # token.ts:95
→ assertSecretInputResolved(params) # types.secrets.ts:126
→ throw Error("unresolved SecretRef") # types.secrets.ts:134
Impact and severity
This is not Telegram-specific. All channel getStatus() implementations follow the same pattern of calling resolveXxxAccount() which eagerly resolves the token:
| Channel |
getStatus |
Token resolution that throws |
| Telegram |
src/channels/plugins/onboarding/telegram.ts:154 |
resolveTelegramToken() in src/telegram/token.ts:69,95 |
| Slack |
src/channels/plugins/onboarding/slack.ts:200 |
resolveSlackBotToken() in src/slack/token.ts:4 |
| Discord |
src/channels/plugins/onboarding/discord.ts:149 |
normalizeDiscordToken() in src/discord/token.ts:13 |
Any channel using an exec (or file) secret provider for its token will hit this during onboarding.
Additional information
Two extension adapters already handle this by passing allowUnresolvedSecretRef: true to their account resolution:
- Mattermost —
extensions/mattermost/src/onboarding.ts:53: resolveMattermostAccount({ cfg, accountId, allowUnresolvedSecretRef: true })
- Zalo —
extensions/zalo/src/onboarding.ts:220: resolveZaloAccount({ cfg, accountId, allowUnresolvedSecretRef: true })
The core channels (Telegram, Slack, Discord) do not use this option and their resolveXxxAccount() functions do not support it.
Proposed Area of Concern
Primary fix: getStatus() in each channel onboarding adapter
The getStatus() implementations should not call resolveXxxAccount() (which eagerly resolves tokens) when they only need to check if a token is configured. The fix should either:
Option A (targeted): Wrap the resolveXxxAccount() call in each getStatus() with a try/catch. If the call throws due to an unresolved SecretRef, the throw itself proves a token IS configured (just not resolvable outside the gateway runtime), so the catch block simply returns true — no fallback lookup needed.
Current getStatus() callback (Telegram, representative of all three):
const configured = listTelegramAccountIds(cfg).some((accountId) => {
const account = resolveTelegramAccount({ cfg, accountId }); // ← throws on unresolved SecretRef
return Boolean(account.token) || Boolean(account.config.tokenFile?.trim())
|| hasConfiguredSecretInput(account.config.botToken); // ← never reached
});
Fix:
const configured = listTelegramAccountIds(cfg).some((accountId) => {
try {
const account = resolveTelegramAccount({ cfg, accountId });
return Boolean(account.token) || Boolean(account.config.tokenFile?.trim())
|| hasConfiguredSecretInput(account.config.botToken);
} catch {
return true; // unresolved SecretRef proves a token is configured
}
});
Note: resolveXxxAccount() and hasConfiguredSecretInput() are in the same .some() callback — not separate code paths. The existing hasConfiguredSecretInput() check already handles SecretRef objects correctly; it just never executes because the resolve call throws first.
Files (all three getStatus implementations compile into the same dist bundle — plugin-sdk/index.js):
src/channels/plugins/onboarding/telegram.ts:154-162
src/channels/plugins/onboarding/slack.ts:200-208
src/channels/plugins/onboarding/discord.ts:149-153
Option B (deeper): Add a "check-only" mode to the token resolution functions that returns whether a token source exists without actually resolving SecretRef values. This would avoid the try/catch approach and make the intent clearer:
src/telegram/token.ts — resolveTelegramToken() or a new hasTelegramToken()
src/slack/token.ts — similar
src/discord/token.ts — similar
Option C (broadest): Make normalizeResolvedSecretInputString() in src/config/types.secrets.ts:139 aware of the onboarding context — e.g. accept an option to return undefined instead of throwing when encountering an unresolved SecretRef. This would make all callers safe without individual fixes.
Secondary concern (defense-in-depth): refreshStatus() in the onboarding framework
src/commands/onboarding/onboard-channels.ts (the refreshStatus / applyOnboardingResult flow)
The onboarding framework calls refreshStatus() immediately after applying a config patch that may contain unresolved SecretRefs. Even if individual adapters are fixed, this pattern is fragile — any future adapter could introduce the same bug.
However, note that catching errors in refreshStatus() alone is not a correct fix: if getStatus() throws, the adapter would silently report "not configured" instead of "configured" — a wrong answer rather than a crash. The real fix belongs in getStatus(). Adding error handling in refreshStatus() is only useful as defense-in-depth to prevent future adapters from crashing the entire onboarding flow.
Regression
This is a regression introduced in commit 806803b7e (2026-03-02, "feat(secrets): expand SecretRef coverage across user-supplied credentials", #29580).
- Not present in v2026.3.1 or earlier —
resolveTelegramToken() and equivalents did not call normalizeResolvedSecretInputString() before this commit.
- Present in v2026.3.2 and v2026.3.2-beta.1.
Bug type
Regression (worked before, now fails)
Summary
When configuring a channel (Telegram, Slack, or Discord) during
openclaw onboardand choosing an exec secret provider for the bot token, the onboarding wizard crashes immediately after the "Reference validated" step with:The onboarding never completes, so the channel configuration is never written to
openclaw.json. The exec provider (e.g. 1Passwordop) is called once during validation and works correctly, but is never called again for actual resolution.Steps to reproduce
openclaw.json:openclaw onboardand select Telegram (or Slack/Discord)Expected behavior
After validating the secret reference, onboarding should complete the remaining steps (allowFrom configuration, dmPolicy, etc.) and write the channel config to
openclaw.json. The exec provider should either:Actual behavior
Crash:
OpenClaw version
2026.3.2
Operating system
macOS (Darwin 25.3.0)
Install method
install script.sh
Logs, screenshots, and evidence
Impact and severity
This is not Telegram-specific. All channel
getStatus()implementations follow the same pattern of callingresolveXxxAccount()which eagerly resolves the token:src/channels/plugins/onboarding/telegram.ts:154resolveTelegramToken()insrc/telegram/token.ts:69,95src/channels/plugins/onboarding/slack.ts:200resolveSlackBotToken()insrc/slack/token.ts:4src/channels/plugins/onboarding/discord.ts:149normalizeDiscordToken()insrc/discord/token.ts:13Any channel using an exec (or file) secret provider for its token will hit this during onboarding.
Additional information
Two extension adapters already handle this by passing
allowUnresolvedSecretRef: trueto their account resolution:extensions/mattermost/src/onboarding.ts:53:resolveMattermostAccount({ cfg, accountId, allowUnresolvedSecretRef: true })extensions/zalo/src/onboarding.ts:220:resolveZaloAccount({ cfg, accountId, allowUnresolvedSecretRef: true })The core channels (Telegram, Slack, Discord) do not use this option and their
resolveXxxAccount()functions do not support it.Proposed Area of Concern
Primary fix:
getStatus()in each channel onboarding adapterThe
getStatus()implementations should not callresolveXxxAccount()(which eagerly resolves tokens) when they only need to check if a token is configured. The fix should either:Option A (targeted): Wrap the
resolveXxxAccount()call in eachgetStatus()with a try/catch. If the call throws due to an unresolved SecretRef, the throw itself proves a token IS configured (just not resolvable outside the gateway runtime), so the catch block simply returnstrue— no fallback lookup needed.Current
getStatus()callback (Telegram, representative of all three):Fix:
Note:
resolveXxxAccount()andhasConfiguredSecretInput()are in the same.some()callback — not separate code paths. The existinghasConfiguredSecretInput()check already handles SecretRef objects correctly; it just never executes because the resolve call throws first.Files (all three
getStatusimplementations compile into the same dist bundle —plugin-sdk/index.js):src/channels/plugins/onboarding/telegram.ts:154-162src/channels/plugins/onboarding/slack.ts:200-208src/channels/plugins/onboarding/discord.ts:149-153Option B (deeper): Add a "check-only" mode to the token resolution functions that returns whether a token source exists without actually resolving SecretRef values. This would avoid the try/catch approach and make the intent clearer:
src/telegram/token.ts—resolveTelegramToken()or a newhasTelegramToken()src/slack/token.ts— similarsrc/discord/token.ts— similarOption C (broadest): Make
normalizeResolvedSecretInputString()insrc/config/types.secrets.ts:139aware of the onboarding context — e.g. accept an option to returnundefinedinstead of throwing when encountering an unresolved SecretRef. This would make all callers safe without individual fixes.Secondary concern (defense-in-depth):
refreshStatus()in the onboarding frameworksrc/commands/onboarding/onboard-channels.ts(therefreshStatus/applyOnboardingResultflow)The onboarding framework calls
refreshStatus()immediately after applying a config patch that may contain unresolved SecretRefs. Even if individual adapters are fixed, this pattern is fragile — any future adapter could introduce the same bug.However, note that catching errors in
refreshStatus()alone is not a correct fix: ifgetStatus()throws, the adapter would silently report "not configured" instead of "configured" — a wrong answer rather than a crash. The real fix belongs ingetStatus(). Adding error handling inrefreshStatus()is only useful as defense-in-depth to prevent future adapters from crashing the entire onboarding flow.Regression
This is a regression introduced in commit
806803b7e(2026-03-02, "feat(secrets): expand SecretRef coverage across user-supplied credentials", #29580).resolveTelegramToken()and equivalents did not callnormalizeResolvedSecretInputString()before this commit.