Skip to content

[Bug]: Onboarding crashes when using exec secret provider for channel tokens (telegram, slack, discord) #37303

Description

@spotshare-nick

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

  1. 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
        }
      }
    }
  2. Run openclaw onboard and select Telegram (or Slack/Discord)
  3. Choose "Use external secret provider" for the bot token
  4. Select the configured provider and enter a secret ID
  5. The reference validates successfully ("Reference validated")
  6. 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:

  • Mattermostextensions/mattermost/src/onboarding.ts:53: resolveMattermostAccount({ cfg, accountId, allowUnresolvedSecretRef: true })
  • Zaloextensions/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.tsresolveTelegramToken() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingregressionBehavior that previously worked and now failsstaleMarked as stale due to inactivity

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions