Skip to content

fix(status): resolve SecretRef tolerantly in channel token resolution#34726

Closed
Sid-Qin wants to merge 1 commit intoopenclaw:mainfrom
Sid-Qin:fix/34292-secretref-status-inconsistency
Closed

fix(status): resolve SecretRef tolerantly in channel token resolution#34726
Sid-Qin wants to merge 1 commit intoopenclaw:mainfrom
Sid-Qin:fix/34292-secretref-status-inconsistency

Conversation

@Sid-Qin
Copy link
Copy Markdown
Contributor

@Sid-Qin Sid-Qin commented Mar 4, 2026

Summary

  • Problem: openclaw status crashed on unresolved SecretRef objects in channel token config, while openclaw doctor reported OK because it probed the already-resolved gateway process.
  • Why it matters: Users see confusing "unresolved SecretRef" errors from status while doctor says everything is fine.
  • What changed: Added normalizeSecretInputStringTolerant that returns undefined for unresolved SecretRefs instead of throwing. Used it in Telegram/Discord/Slack token resolution so status degrades gracefully.
  • What did NOT change: Doctor behavior unchanged. Token resolution logic unchanged for properly resolved secrets.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • openclaw status no longer crashes on unresolved SecretRefs — shows "none" or falls back to environment variable

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No (tolerant resolution, same underlying logic)
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Any
  • Runtime: Node.js
  • Integration/channel: Telegram/Discord/Slack with SecretRef tokens

Steps

  1. Configure Telegram botToken as a SecretRef
  2. Start gateway (resolves SecretRef)
  3. Run openclaw status (uses config before resolution)

Expected

  • Status shows channel info without crashing

Actual

  • Before fix: Crash on "unresolved SecretRef"
  • After fix: Graceful degradation

Evidence

Updated tests verify unresolved SecretRef returns undefined instead of throwing

Files changed (7 files)

  1. src/config/types.secrets.ts — Added tolerant normalizer
  2. src/telegram/token.ts — Use tolerant normalizer
  3. src/discord/token.ts — Use tolerant normalizer
  4. src/slack/token.ts — Use tolerant normalizer
  5. src/plugin-sdk/index.ts — Export tolerant normalizer
  6. src/telegram/token.test.ts — Updated tests
  7. src/discord/token.test.ts — Updated tests

Human Verification (required)

  • Verified scenarios: Resolved string secret works, unresolved SecretRef returns undefined, null/undefined returns undefined
  • Edge cases checked: Environment variable fallback still works
  • What I did not verify: Live multi-channel status test

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert: Revert commit
  • Files/config to restore: 7 files
  • Known bad symptoms: Status crashes on SecretRef

Risks and Mitigations

  • Low risk: Tolerant normalizer only affects the error path (unresolved SecretRef → undefined instead of throw). Normal resolved secrets unchanged.

openclaw status crashed on unresolved SecretRef objects because
normalizeResolvedSecretInputString threw for non-string inputs. Meanwhile
openclaw doctor probed the already-resolved gateway process and reported
OK. Add normalizeSecretInputStringTolerant that returns undefined for
unresolved SecretRefs, and use it in Telegram/Discord/Slack token
resolution so status degrades gracefully instead of crashing.

Closes openclaw#34292
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 4, 2026

🔒 Aisle Security Analysis

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

# Severity Title
1 🔵 Low Fail-open token resolution when config contains unresolved SecretRef (falls back to env)
2 🔵 Low Slack tokens: unresolved SecretRef now silently ignored, enabling env-var credential fallback

1. 🔵 Fail-open token resolution when config contains unresolved SecretRef (falls back to env)

Property Value
Severity Low
CWE CWE-703
Location src/discord/token.ts:54-68

Description

Token normalization was changed to use normalizeSecretInputStringTolerant, which suppresses the previous hard failure on unresolved SecretRef values.

Previously, normalizeResolvedSecretInputString called assertSecretInputResolved and threw if a SecretRef was present but the value was not a resolved non-empty string ("unresolved"). See:

  • Throw on unresolved refs: [src/config/types.secrets.ts:120-137]
  • Enforced by strict normalizer: [src/config/types.secrets.ts:139-151]

Now normalizeDiscordToken uses the tolerant normalizer [src/discord/token.ts:12-18], which returns undefined for SecretRef inputs (instead of throwing). This allows resolveDiscordToken to silently fall through to reading process.env.DISCORD_BOT_TOKEN when channels.discord.token is an unresolved SecretRef:

  • Config token attempt then env fallback: [src/discord/token.ts:54-68]

Security impact:

  • A configuration that explicitly configured a SecretRef (expecting gateway/runtime secret resolution and fail-closed behavior if not resolved) will now authenticate using an environment variable if present.
  • This can hide misconfiguration and may cause the service to run with unintended credentials (e.g., stale/host-level env token), undermining separation between secret-resolution mechanisms and direct env access.

Vulnerable flow:

  • input: cfg.channels.discord.token can be a SecretRef object
  • normalization: tolerant normalizer returns undefined
  • fallback: resolveDiscordToken uses process.env.DISCORD_BOT_TOKEN instead of failing

Recommendation

Fail closed when a SecretRef is configured but unresolved in authentication/token paths.

Options:

  1. Revert to strict normalization for tokens (preferred):
import { normalizeResolvedSecretInputString } from "../config/types.secrets.js";

export function normalizeDiscordToken(raw: unknown, path: string): string | undefined {
  const trimmed = normalizeResolvedSecretInputString({ value: raw, path });
  return trimmed?.replace(/^Bot\s+/i, "");
}
  1. If fallback is desired only when no SecretRef is configured, make callers distinguish between “missing” and “unresolved ref”. For example, change tolerant API to return a tuple/object:
const { value, hasRef } = normalizeSecretInputStringTolerantWithRefInfo(...);
if (hasRef) throw new Error(`${path}: unresolved SecretRef`);

Also keep/use the path parameter so operators get actionable error messages when misconfigured.


2. 🔵 Slack tokens: unresolved SecretRef now silently ignored, enabling env-var credential fallback

Property Value
Severity Low
CWE CWE-15
Location src/slack/token.ts:7-9

Description

Slack token parsing was changed from strict normalizeResolvedSecretInputString(...path) (throws on unresolved SecretRef) to tolerant normalizeSecretInputStringTolerant (returns undefined). This can cause a fail-open credential source fallback:

  • If channels.slack.accounts.<id>.botToken/appToken/userToken is configured as a SecretRef object but not resolved at runtime, resolveSlack*Token() now returns undefined instead of throwing.
  • For the default Slack account, resolveSlackAccount() falls back to environment variables when the config-derived token is undefined (configBot ?? envBot). As a result, a misconfigured/unresolved secret reference can silently cause the system to authenticate using SLACK_BOT_TOKEN / SLACK_APP_TOKEN / SLACK_USER_TOKEN from the process environment, rather than the intended secret provider.
  • This both reduces observability (no path-specific unresolved SecretRef error) and can enable credential injection via environment variables in deployments where environment variables are less trusted than the config/secret provider.

Vulnerable code (new behavior):

export function resolveSlackBotToken(raw?: unknown, _path?: string): string | undefined {
  return normalizeSecretInputStringTolerant({ value: raw });
}

Security impact example (data flow):

  • input: merged.botToken set to a SecretRef object in config
  • token resolution now returns undefined (no exception)
  • sink: resolveSlackAccount() chooses botToken = configBot ?? envBot, so process.env.SLACK_BOT_TOKEN may be used instead of the configured secret ref

Recommendation

Avoid tolerant normalization for runtime authentication credentials, or explicitly detect unresolved SecretRefs and fail closed.

Option A (recommended): keep strict behavior for Slack tokens

import { normalizeResolvedSecretInputString } from "../config/types.secrets.js";

export function resolveSlackBotToken(raw?: unknown, path = "channels.slack.botToken") {
  return normalizeResolvedSecretInputString({ value: raw, path });
}

Option B: if env fallback is desired, only allow it when the config value is truly absent (not a SecretRef)

import { normalizeSecretInputString, resolveSecretInputRef } from "../config/types.secrets.js";

export function resolveSlackBotToken(raw?: unknown, path = "channels.slack.botToken") {
  const direct = normalizeSecretInputString(raw);
  if (direct) return direct;
  const { ref } = resolveSecretInputRef({ value: raw });
  if (ref) throw new Error(`${path}: unresolved SecretRef`);
  return undefined;
}

This prevents silently switching credential sources when a SecretRef is present but unresolved.


Analyzed PR: #34726 at commit e66c6d9

Last updated on: 2026-03-04T17:16:36Z

@openclaw-barnacle openclaw-barnacle bot added channel: discord Channel integration: discord channel: slack Channel integration: slack channel: telegram Channel integration: telegram size: S experienced-contributor labels Mar 4, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 4, 2026

Greptile Summary

This PR fixes a crash in openclaw status caused by unresolved SecretRef objects in Telegram, Discord, and Slack token configuration. It introduces normalizeSecretInputStringTolerant, a variant of normalizeResolvedSecretInputString that returns undefined instead of throwing when a SecretRef has not yet been resolved by the gateway. The three channel token files switch to the new tolerant function, and tests are updated accordingly.

The fix is minimal, well-scoped, and backward-compatible. The core logic change is correct: diagnostic/status code paths now degrade gracefully rather than crashing.

  • Dead code in normalizeSecretInputStringTolerant (src/config/types.secrets.ts lines 168–176): the resolveSecretInputRef call and the if (ref) check both unconditionally return undefined, making that entire block a no-op. The function could be simplified to a single return normalizeSecretInputString(params.value). The current implementation is misleading for future maintainers.
  • _path parameters are retained in normalizeDiscordToken and the Slack helpers for API compatibility — this is correct practice.
  • No Slack token test file exists; the Slack token changes are covered only by the shared logic tests for normalizeSecretInputStringTolerant. This is acceptable given the simplicity of the Slack wrappers, but adding a src/slack/token.test.ts analogous to the Telegram/Discord tests would improve coverage parity.

Confidence Score: 4/5

  • This PR is safe to merge; it only widens a narrow error path to return undefined instead of throwing, leaving all normal resolved-secret paths unchanged.
  • The behavioural change is strictly additive (turns a throw into a graceful undefined), tests cover the new paths for Telegram and Discord, and the affected code is limited to diagnostic/status call sites. The only notable concern is dead code inside normalizeSecretInputStringTolerant which does not affect correctness.
  • src/config/types.secrets.ts — contains dead code in the new normalizeSecretInputStringTolerant function that should be cleaned up.

Last reviewed commit: e66c6d9

Comment on lines +168 to +176
const { ref } = resolveSecretInputRef({
value: params.value,
refValue: params.refValue,
defaults: params.defaults,
});
if (ref) {
return undefined;
}
return undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dead code — both branches of if (ref) return undefined

The resolveSecretInputRef call and the if (ref) branch below it serve no purpose: both paths return undefined. This means the entire function is functionally equivalent to return normalizeSecretInputString(params.value). The extra branching is misleading — a future reader might assume the ref-truthy and ref-falsy paths behave differently when they do not.

Suggested change
const { ref } = resolveSecretInputRef({
value: params.value,
refValue: params.refValue,
defaults: params.defaults,
});
if (ref) {
return undefined;
}
return undefined;
return undefined;
}

Or simplify the whole function body to:

export function normalizeSecretInputStringTolerant(params: {
  value: unknown;
  refValue?: unknown;
  defaults?: SecretDefaults;
}): string | undefined {
  return normalizeSecretInputString(params.value);
}

If the resolveSecretInputRef call is kept intentionally (e.g. for future logging of which ref was skipped), that intent should be documented with a comment so the dead-code appearance is explained.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/types.secrets.ts
Line: 168-176

Comment:
**Dead code — both branches of `if (ref)` return `undefined`**

The `resolveSecretInputRef` call and the `if (ref)` branch below it serve no purpose: both paths return `undefined`. This means the entire function is functionally equivalent to `return normalizeSecretInputString(params.value)`. The extra branching is misleading — a future reader might assume the `ref`-truthy and `ref`-falsy paths behave differently when they do not.

```suggestion
  return undefined;
}
```

Or simplify the whole function body to:

```ts
export function normalizeSecretInputStringTolerant(params: {
  value: unknown;
  refValue?: unknown;
  defaults?: SecretDefaults;
}): string | undefined {
  return normalizeSecretInputString(params.value);
}
```

If the `resolveSecretInputRef` call is kept intentionally (e.g. for future logging of which ref was skipped), that intent should be documented with a comment so the dead-code appearance is explained.

How can I resolve this? If you propose a fix, please make it concise.

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: e66c6d95fb

ℹ️ 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 +69 to 71
const accountToken = normalizeSecretInputStringTolerant({
value: accountCfg?.botToken,
path: `channels.telegram.accounts.${accountId}.botToken`,
});
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 Preserve account token override when SecretRef is unresolved

Using normalizeSecretInputStringTolerant here makes an unresolved per-account botToken look the same as “not configured”, so resolveTelegramToken now falls through to top-level channels.telegram.botToken for non-default accounts instead of stopping on the explicit account override. In the context I checked (src/telegram/token.ts resolution order), a config like accounts.work.botToken: {source:"env",...} plus a top-level token will now run work with the wrong bot token, whereas before this commit it threw on unresolved SecretRef; that can misroute sends/status for multi-account Telegram setups.

Useful? React with 👍 / 👎.

@joshavant
Copy link
Copy Markdown
Contributor

Thanks for the report and the work here. This was addressed as part of a broader fix in #37023, which consolidated the related read-only SecretRef/status handling work into one implementation.

I’m closing this out as superseded by #37023 so the follow-up history stays in one place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord channel: slack Channel integration: slack channel: telegram Channel integration: telegram size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

status: unresolved SecretRef for Telegram botToken while doctor reports Telegram OK

2 participants