Skip to content

fix(config): restore SecretRef id under sensitive token paths on config round-trip#55034

Closed
D3N1ALL wants to merge 3 commits into
openclaw:mainfrom
D3N1ALL:fix/secret-ref-round-trip-token-paths
Closed

fix(config): restore SecretRef id under sensitive token paths on config round-trip#55034
D3N1ALL wants to merge 3 commits into
openclaw:mainfrom
D3N1ALL:fix/secret-ref-round-trip-token-paths

Conversation

@D3N1ALL

@D3N1ALL D3N1ALL commented Mar 26, 2026

Copy link
Copy Markdown

Problem

When channels.discord.token (or any field whose path ends with a token-like suffix) is stored as an env-source SecretRef object — e.g.:

{
  "channels": {
    "discord": {
      "token": { "source": "env", "provider": "default", "id": "DISCORD_BOT_TOKEN" }
    }
  }
}

saving any setting in the Control UI would fail with:

GatewayRequestError: invalid config: channels.discord.token.id:
Env secret reference id must match /^[A-Z][A-Z0-9_]{0,127}$/

This blocked all saves across the entire Web UI — workspace, tools, identity settings — not just Discord config. Rebuilding the server did not help because the issue was in the gateway's redaction/restore logic, not in stored state.

Root Cause

The redaction layer correctly redacts the id field of a SecretRef at a sensitive path (e.g. channels.discord.token) by replacing it with __OPENCLAW_REDACTED__. On the restore path, the gateway is supposed to substitute the sentinel back with the real value from the on-disk snapshot before validation.

The restore succeeded for paths like models.providers.*.apiKey.id because isSensitivePath matched the full path string against /api.?key/i (unanchored). It silently failed for channels.discord.token.id because the sensitive pattern for token fields is /token$/i (end-anchored), and .id as a suffix breaks the match.

The sentinel was therefore left in place, failing the EnvSecretRefSchema regex validator for id.

Fix

In restoreRedactedValuesWithLookup, when a path is matched in the hints lookup as sensitive and the incoming value is an object, detect the case where that object is a partially-redacted SecretRef (i.e. isSecretRefShape is true and id === REDACTED_SENTINEL). In that case, restore the whole original value from the snapshot rather than recursing into the object and leaving the sentinel unresolved.

This is symmetric with the redact side, which already calls redactSecretRefId to partially redact SecretRef objects at sensitive paths.

Tests

  • Added channels.discord.token and channels.discord.accounts.*.token to the redaction test hints fixture.
  • Added a regression test: round-trips channels.discord.token as env-source SecretRef — verifies that the id field is redacted in the outgoing snapshot, the source/provider structural fields are preserved, and restoreRedactedValues fully reconstructs the original config.

Closes #32445 (partial — the type fix landed in #32490; this fixes the missing restore-side handling that made the UI unusable when a SecretRef was stored for this field).

Related: #50537, #46109

@greptile-apps

greptile-apps Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where saving any config through the Web UI would fail with an EnvSecretRefSchema validation error when channels.discord.token (or any token path) stored a SecretRef object instead of a plain string. The root cause was an asymmetry between the redact and restore sides: redactObjectWithLookup correctly called redactSecretRefId to only redact the id field of a SecretRef at a sensitive path, but restoreRedactedValuesWithLookup would then recurse into that partially-redacted object and try to restore channels.discord.token.id via isSensitivePath, which failed because the /token$/i pattern is end-anchored and the .id suffix broke the match.

Key changes:

  • redact-snapshot.ts: In restoreRedactedValuesWithLookup, before recursing into an object value at a sensitive path, the fix detects a partially-redacted SecretRef (isSecretRefShape && id === REDACTED_SENTINEL) and restores the entire original object — symmetric with the redact side's redactSecretRefId call.
  • redact-snapshot.test-hints.ts: Adds channels.discord.token and channels.discord.accounts.*.token to the test hints fixture so they are recognized as sensitive in tests.
  • redact-snapshot.test.ts: Adds a focused regression test covering the exact failing scenario (env-source SecretRef at channels.discord.token), verifying both redaction of id and full restoration.

Confidence Score: 5/5

Safe to merge — targeted fix with a regression test, no risky side-effects identified.

The fix is minimal, well-scoped, and symmetric with the existing redact-side logic. The new condition only triggers when hints[candidate]?.sensitive === true && isSecretRefShape(value) && value.id === REDACTED_SENTINEL, which is precisely the state produced by redactSecretRefId on the redact side. Scenarios outside this condition (user supplying a real new id, non-SecretRef nested objects, the guessing-path code) are unaffected. The regression test closes the loop end-to-end.

No files require special attention.

Important Files Changed

Filename Overview
src/config/redact-snapshot.ts Core fix: detects partially-redacted SecretRef at a sensitive hints path and restores the whole original object, symmetric with the redact side's redactSecretRefId call.
src/config/redact-snapshot.test-hints.ts Adds channels.discord.token and channels.discord.accounts.*.token as sensitive paths in the test hints fixture.
src/config/redact-snapshot.test.ts Adds regression test verifying env-source SecretRef at channels.discord.token is correctly redacted and fully restored on round-trip.

Reviews (1): Last reviewed commit: "fix(config): restore SecretRef id under ..." | Re-trigger Greptile

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

Copy link
Copy Markdown

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

ℹ️ 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/config/redact-snapshot.ts Outdated
Comment on lines +667 to +671
hints[candidate]?.sensitive === true &&
isSecretRefShape(objValue) &&
objValue.id === REDACTED_SENTINEL
) {
result[key] = restoreOriginalValueOrThrow({ key, path: candidate, original: orig });

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 edited SecretRef metadata when id is redacted

This branch restores the entire original object whenever a sensitive SecretRef arrives with id === REDACTED_SENTINEL, which silently drops user edits to non-secret fields like source or provider. A common case is editing only channels.discord.token.provider in the config UI/raw editor while leaving the redacted id placeholder; after this code runs, the provider change is discarded because result[key] is replaced with orig[key]. Restore should merge and only substitute the secret id field so non-sensitive updates are preserved.

Useful? React with 👍 / 👎.

@D3N1ALL

D3N1ALL commented Mar 26, 2026

Copy link
Copy Markdown
Author

The checks-node-test-2 failure is in src/infra/restart.test.ts — unrelated to this PR (touches only src/config/redact-snapshot*). The test passes locally and in 3/4 Linux shards + all 9 Windows shards. This is a known flaky isolation issue in that test suite (see recent commits: test: harden ci isolated mocks, test: preserve child_process exports in restart bun mock). Happy to rebase/re-push to trigger a fresh run if needed.

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

Copy link
Copy Markdown

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: 99198718bb

ℹ️ 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/config/redact-snapshot.ts Outdated
Comment on lines +667 to +671
hints[candidate]?.sensitive === true &&
isSecretRefShape(objValue) &&
objValue.id === REDACTED_SENTINEL
) {
result[key] = restoreOriginalValueOrThrow({ key, path: candidate, original: orig });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve non-secret SecretRef fields during restore

When a sensitive SecretRef comes back with id === REDACTED_SENTINEL, this branch restores orig[key] wholesale, which discards legitimate edits to non-secret fields (source, provider, etc.) made in the same save request. For example, changing channels.discord.token.provider while leaving the redacted id placeholder will silently revert provider to its old value. Restore should only substitute the secret id from the original object (or merge field-by-field), not replace the entire object.

Useful? React with 👍 / 👎.

@joshavant

Copy link
Copy Markdown
Contributor

Thanks @D3N1ALL for the contribution on sensitive token-path SecretRef restore behavior.

This is now fully covered by #58044 (merged), including the sensitive-path restore hardening and associated regression coverage.

Closing this PR as superseded by #58044.

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.

[Feature]: channels.discord.token should support SecretRef (env source)

2 participants