fix(config): restore SecretRef id field when redacted (#44357)#44455
fix(config): restore SecretRef id field when redacted (#44357)#44455claw-explorer wants to merge 1 commit into
Conversation
## Summary Fixes Control UI config.set failures when SecretRef fields are present. Previously, the gateway would redact SecretRef id values to __OPENCLAW_REDACTED__ during config.get, but the restore logic didn't handle SecretRef objects specifically, causing validation to reject the sentinel value. ## Root Cause When a SecretRef object is redacted: The restore logic checked for `value === REDACTED_SENTINEL`, but for SecretRef objects, the `value` is the entire object, not the sentinel. The sentinel is inside the `id` field, so it was never restored. ## Fix Added logic to both `restoreRedactedValuesWithLookup` and `restoreRedactedValuesGuessing` to detect SecretRef shapes and restore the `id` field specifically when it contains the sentinel. ## Testing Added two test cases: 1. Verifies SecretRef id restoration preserves structure 2. Verifies users can still explicitly change SecretRef id values Closes openclaw#44357
Greptile SummaryThis PR fixes a real bug where SecretRef objects ( The fix is implemented correctly for the primary code path (
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/config/redact-snapshot.ts
Line: 703-715
Comment:
**Missing sensitivity guard on SecretRef restoration in guessing path**
In `restoreRedactedValuesGuessing`, the string-sentinel restoration (lines 697–702) is guarded by `!isExplicitlyNonSensitivePath(hints, [...]) && isSensitivePath(path)`, but the new SecretRef restoration block immediately below has no equivalent guard. This means any incoming object that happens to match the `isSecretRefShape` predicate (i.e. has `source: string` and `id: "__OPENCLAW_REDACTED__"`) at *any* path — including non-sensitive ones — will have its `id` silently replaced from the original config, even if the path is explicitly marked non-sensitive in `hints`.
The same inconsistency exists in the `!matched` branch of `restoreRedactedValuesWithLookup` (lines 651–662), where the string check includes `!markedNonSensitive && isSensitivePath(path)` but the new SecretRef block does not.
In practice the impact is low (a non-sensitive path would never have `id` set to `REDACTED_SENTINEL` by the redaction logic itself), but for consistency with the surrounding code the guard should be mirrored:
```
} else if (typeof value === "object" && value !== null) {
// Check if this is a SecretRef with redacted id field (#44357)
const incomingObj = value as Record<string, unknown>;
if (
!isExplicitlyNonSensitivePath(hints, [path, wildcardPath]) &&
isSensitivePath(path) &&
isSecretRefShape(incomingObj) &&
incomingObj.id === REDACTED_SENTINEL
) {
const originalObj = toObjectRecord(orig[key]);
if (isSecretRefShape(originalObj)) {
result[key] = { ...incomingObj, id: originalObj.id };
} else {
result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints);
}
} else {
result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints);
}
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/config/redact-snapshot.ts
Line: 630-641
Comment:
**Triplicated SecretRef restoration logic**
The same SecretRef detection + restoration block appears verbatim in three separate places:
1. `restoreRedactedValuesWithLookup` — matched branch (here, lines 630–641)
2. `restoreRedactedValuesWithLookup` — `!matched` / guessing fallback branch (lines 651–662)
3. `restoreRedactedValuesGuessing` (lines 704–715)
Extracting it into a small helper (e.g. `restoreSecretRefId`) would reduce the surface area for future divergence and make each call site easier to read:
```typescript
function restoreSecretRefId(
incomingObj: Record<string, unknown>,
originalValue: unknown,
): Record<string, unknown> | null {
if (!isSecretRefShape(incomingObj) || incomingObj.id !== REDACTED_SENTINEL) {
return null;
}
const originalObj = toObjectRecord(originalValue);
if (!isSecretRefShape(originalObj)) {
return null;
}
return { ...incomingObj, id: originalObj.id };
}
```
Each call site then becomes a simple `restoreSecretRefId(incomingObj, orig[key]) ?? <fallback>` check, making it easy to apply (or audit) the sensitivity guard mentioned in the adjacent comment uniformly.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: c9c73ec |
| } else if (typeof value === "object" && value !== null) { | ||
| result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints); | ||
| // Check if this is a SecretRef with redacted id field (#44357) | ||
| const incomingObj = value as Record<string, unknown>; | ||
| if (isSecretRefShape(incomingObj) && incomingObj.id === REDACTED_SENTINEL) { | ||
| const originalObj = toObjectRecord(orig[key]); | ||
| if (isSecretRefShape(originalObj)) { | ||
| result[key] = { ...incomingObj, id: originalObj.id }; | ||
| } else { | ||
| result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints); | ||
| } | ||
| } else { | ||
| result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints); | ||
| } |
There was a problem hiding this comment.
Missing sensitivity guard on SecretRef restoration in guessing path
In restoreRedactedValuesGuessing, the string-sentinel restoration (lines 697–702) is guarded by !isExplicitlyNonSensitivePath(hints, [...]) && isSensitivePath(path), but the new SecretRef restoration block immediately below has no equivalent guard. This means any incoming object that happens to match the isSecretRefShape predicate (i.e. has source: string and id: "__OPENCLAW_REDACTED__") at any path — including non-sensitive ones — will have its id silently replaced from the original config, even if the path is explicitly marked non-sensitive in hints.
The same inconsistency exists in the !matched branch of restoreRedactedValuesWithLookup (lines 651–662), where the string check includes !markedNonSensitive && isSensitivePath(path) but the new SecretRef block does not.
In practice the impact is low (a non-sensitive path would never have id set to REDACTED_SENTINEL by the redaction logic itself), but for consistency with the surrounding code the guard should be mirrored:
} else if (typeof value === "object" && value !== null) {
// Check if this is a SecretRef with redacted id field (#44357)
const incomingObj = value as Record<string, unknown>;
if (
!isExplicitlyNonSensitivePath(hints, [path, wildcardPath]) &&
isSensitivePath(path) &&
isSecretRefShape(incomingObj) &&
incomingObj.id === REDACTED_SENTINEL
) {
const originalObj = toObjectRecord(orig[key]);
if (isSecretRefShape(originalObj)) {
result[key] = { ...incomingObj, id: originalObj.id };
} else {
result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints);
}
} else {
result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints);
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/redact-snapshot.ts
Line: 703-715
Comment:
**Missing sensitivity guard on SecretRef restoration in guessing path**
In `restoreRedactedValuesGuessing`, the string-sentinel restoration (lines 697–702) is guarded by `!isExplicitlyNonSensitivePath(hints, [...]) && isSensitivePath(path)`, but the new SecretRef restoration block immediately below has no equivalent guard. This means any incoming object that happens to match the `isSecretRefShape` predicate (i.e. has `source: string` and `id: "__OPENCLAW_REDACTED__"`) at *any* path — including non-sensitive ones — will have its `id` silently replaced from the original config, even if the path is explicitly marked non-sensitive in `hints`.
The same inconsistency exists in the `!matched` branch of `restoreRedactedValuesWithLookup` (lines 651–662), where the string check includes `!markedNonSensitive && isSensitivePath(path)` but the new SecretRef block does not.
In practice the impact is low (a non-sensitive path would never have `id` set to `REDACTED_SENTINEL` by the redaction logic itself), but for consistency with the surrounding code the guard should be mirrored:
```
} else if (typeof value === "object" && value !== null) {
// Check if this is a SecretRef with redacted id field (#44357)
const incomingObj = value as Record<string, unknown>;
if (
!isExplicitlyNonSensitivePath(hints, [path, wildcardPath]) &&
isSensitivePath(path) &&
isSecretRefShape(incomingObj) &&
incomingObj.id === REDACTED_SENTINEL
) {
const originalObj = toObjectRecord(orig[key]);
if (isSecretRefShape(originalObj)) {
result[key] = { ...incomingObj, id: originalObj.id };
} else {
result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints);
}
} else {
result[key] = restoreRedactedValuesGuessing(value, orig[key], path, hints);
}
}
```
How can I resolve this? If you propose a fix, please make it concise.| // Check if this is a SecretRef with redacted id field (#44357) | ||
| const incomingObj = value as Record<string, unknown>; | ||
| if (isSecretRefShape(incomingObj) && incomingObj.id === REDACTED_SENTINEL) { | ||
| const originalObj = toObjectRecord(orig[key]); | ||
| if (isSecretRefShape(originalObj)) { | ||
| result[key] = { ...incomingObj, id: originalObj.id }; | ||
| } else { | ||
| result[key] = restoreRedactedValuesWithLookup(value, orig[key], lookup, candidate, hints); | ||
| } | ||
| } else { | ||
| result[key] = restoreRedactedValuesWithLookup(value, orig[key], lookup, candidate, hints); | ||
| } |
There was a problem hiding this comment.
Triplicated SecretRef restoration logic
The same SecretRef detection + restoration block appears verbatim in three separate places:
restoreRedactedValuesWithLookup— matched branch (here, lines 630–641)restoreRedactedValuesWithLookup—!matched/ guessing fallback branch (lines 651–662)restoreRedactedValuesGuessing(lines 704–715)
Extracting it into a small helper (e.g. restoreSecretRefId) would reduce the surface area for future divergence and make each call site easier to read:
function restoreSecretRefId(
incomingObj: Record<string, unknown>,
originalValue: unknown,
): Record<string, unknown> | null {
if (!isSecretRefShape(incomingObj) || incomingObj.id !== REDACTED_SENTINEL) {
return null;
}
const originalObj = toObjectRecord(originalValue);
if (!isSecretRefShape(originalObj)) {
return null;
}
return { ...incomingObj, id: originalObj.id };
}Each call site then becomes a simple restoreSecretRefId(incomingObj, orig[key]) ?? <fallback> check, making it easy to apply (or audit) the sensitivity guard mentioned in the adjacent comment uniformly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/redact-snapshot.ts
Line: 630-641
Comment:
**Triplicated SecretRef restoration logic**
The same SecretRef detection + restoration block appears verbatim in three separate places:
1. `restoreRedactedValuesWithLookup` — matched branch (here, lines 630–641)
2. `restoreRedactedValuesWithLookup` — `!matched` / guessing fallback branch (lines 651–662)
3. `restoreRedactedValuesGuessing` (lines 704–715)
Extracting it into a small helper (e.g. `restoreSecretRefId`) would reduce the surface area for future divergence and make each call site easier to read:
```typescript
function restoreSecretRefId(
incomingObj: Record<string, unknown>,
originalValue: unknown,
): Record<string, unknown> | null {
if (!isSecretRefShape(incomingObj) || incomingObj.id !== REDACTED_SENTINEL) {
return null;
}
const originalObj = toObjectRecord(originalValue);
if (!isSecretRefShape(originalObj)) {
return null;
}
return { ...incomingObj, id: originalObj.id };
}
```
Each call site then becomes a simple `restoreSecretRefId(incomingObj, orig[key]) ?? <fallback>` check, making it easy to apply (or audit) the sensitivity guard mentioned in the adjacent comment uniformly.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9c73ecef2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (isSecretRefShape(incomingObj) && incomingObj.id === REDACTED_SENTINEL) { | ||
| const originalObj = toObjectRecord(orig[key]); | ||
| if (isSecretRefShape(originalObj)) { | ||
| result[key] = { ...incomingObj, id: originalObj.id }; |
There was a problem hiding this comment.
Respect sensitive:false before restoring SecretRef IDs
This branch restores incomingObj.id from the original config whenever it sees a SecretRef sentinel, but it runs even in the !matched path where markedNonSensitive was already computed. That means paths explicitly marked sensitive:false can no longer keep a literal __OPENCLAW_REDACTED__ value for SecretRef id, unlike other fields (the existing non-sensitive override behavior), so user input is silently overwritten during config.set instead of preserved.
Useful? React with 👍 / 👎.
|
Thanks @claw-explorer for the focused fix direction on this path. This is now fully covered by #58044 (merged), which landed the restore-path hardening in the final code path with additional guardrails and regression coverage. Closing this PR as superseded by #58044. |
Summary
Fixes Control UI config.set failures when SecretRef fields are present (#44357). Previously, the gateway would redact SecretRef id values to
__OPENCLAW_REDACTED__during config.get, but the restore logic didn't handle SecretRef objects specifically, causing validation to reject the sentinel value.Root Cause
When a SecretRef object is redacted:
The restore logic checked for
value === REDACTED_SENTINEL, but for SecretRef objects, thevalueis the entire object, not the sentinel. The sentinel is inside theidfield, so it was never restored before validation.Fix
Added logic to both
restoreRedactedValuesWithLookupandrestoreRedactedValuesGuessingto:isSecretRefShape)idfield contains__OPENCLAW_REDACTED__idfrom the original configThis happens before Zod validation, so the restored SecretRef passes validation.
Testing
Added two test cases:
Impact
Workaround (No Longer Needed)
Users previously had to edit
~/.openclaw/openclaw.jsondirectly instead of using the Control UI.Closes #44357