fix(config): harden SecretRef round-trip handling in Control UI and RPC writes#58044
Conversation
Greptile SummaryThis PR hardens the SecretRef round-trip path across Control UI and the gateway write RPCs ( Key changes:
Two minor P2 observations: the placeholder Confidence Score: 5/5Safe to merge — all remaining findings are P2 style/UX suggestions with no correctness impact. All P0/P1 concerns are absent. The core restore logic correctly rejects rather than corrupts. The two P2 findings are a slightly misleading UX message when raw mode is simultaneously forced off AND a SecretRef object is rendered, and a non-critical error message improvement for provider-less SecretRef round-trips. Neither affects data integrity or security. ui/src/ui/views/config-form.node.ts (placeholder text when rawAvailable=false) and src/config/redact-snapshot.ts (error message for no-provider SecretRefs) Prompt To Fix All With AIThis is a comment left during a code review.
Path: ui/src/ui/views/config-form.node.ts
Line: 646
Comment:
**Misleading placeholder when Raw mode is unavailable**
The placeholder text `"Structured value (SecretRef) - use Raw mode to edit"` tells the user to switch to Raw mode, but `renderTextInput` doesn't have visibility into whether `rawAvailable` is `false` for the current snapshot. When the overlap-fallback triggers (`raw: null`) and a SecretRef object is present in the form, the user sees an uneditable field pointing them to a mode that is also disabled — the Raw button would be greyed out with the tooltip `"Raw mode unavailable for this snapshot"`.
For a minimal fix, thread `rawAvailable` into the text input renderer and conditionally render `"Structured value (SecretRef) — edit the config file directly"` when raw mode is disabled.
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: 569-575
Comment:
**Misleading error message for SecretRefs without `provider`**
`isSecretRefWithProvider` returns `false` for any SecretRef that lacks a `provider` field, even if the original config has the ref with a legitimate `id`. Since `isSecretRefShape` only requires `source` and `id`, configs with providerless SecretRefs at sensitive paths (e.g. `{source: "env", id: "OPENAI_KEY"}`) will hit this branch and receive `"contains a redacted id placeholder with no matching original value"` — despite a matching original value existing.
The behavior is correct (reject-instead-of-corrupt is safer), but the error message is misleading. A more precise message like `"SecretRef at ${params.path} requires a 'provider' field to restore the redacted id automatically (original ref lacks provider)."` would better guide users debugging this.
```suggestion
throw new RedactionError(
params.path,
`SecretRef at ${params.path} requires a 'provider' field to restore the redacted id automatically (original ref lacks provider).`,
);
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Docs: align SecretRef write semantics wi..." | Re-trigger Greptile |
| const effectiveRedacted = sensitiveState.isRedacted || isStructuredValue; | ||
| const placeholder = effectiveRedacted | ||
| ? isStructuredValue | ||
| ? "Structured value (SecretRef) - use Raw mode to edit" |
There was a problem hiding this comment.
Misleading placeholder when Raw mode is unavailable
The placeholder text "Structured value (SecretRef) - use Raw mode to edit" tells the user to switch to Raw mode, but renderTextInput doesn't have visibility into whether rawAvailable is false for the current snapshot. When the overlap-fallback triggers (raw: null) and a SecretRef object is present in the form, the user sees an uneditable field pointing them to a mode that is also disabled — the Raw button would be greyed out with the tooltip "Raw mode unavailable for this snapshot".
For a minimal fix, thread rawAvailable into the text input renderer and conditionally render "Structured value (SecretRef) — edit the config file directly" when raw mode is disabled.
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/views/config-form.node.ts
Line: 646
Comment:
**Misleading placeholder when Raw mode is unavailable**
The placeholder text `"Structured value (SecretRef) - use Raw mode to edit"` tells the user to switch to Raw mode, but `renderTextInput` doesn't have visibility into whether `rawAvailable` is `false` for the current snapshot. When the overlap-fallback triggers (`raw: null`) and a SecretRef object is present in the form, the user sees an uneditable field pointing them to a mode that is also disabled — the Raw button would be greyed out with the tooltip `"Raw mode unavailable for this snapshot"`.
For a minimal fix, thread `rawAvailable` into the text input renderer and conditionally render `"Structured value (SecretRef) — edit the config file directly"` when raw mode is disabled.
How can I resolve this? If you propose a fix, please make it concise.| const originalObj = toObjectRecord(params.original); | ||
| if (!isSecretRefWithProvider(originalObj)) { | ||
| throw new RedactionError( | ||
| params.path, | ||
| `SecretRef at ${params.path} contains a redacted id placeholder with no matching original value.`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Misleading error message for SecretRefs without
provider
isSecretRefWithProvider returns false for any SecretRef that lacks a provider field, even if the original config has the ref with a legitimate id. Since isSecretRefShape only requires source and id, configs with providerless SecretRefs at sensitive paths (e.g. {source: "env", id: "OPENAI_KEY"}) will hit this branch and receive "contains a redacted id placeholder with no matching original value" — despite a matching original value existing.
The behavior is correct (reject-instead-of-corrupt is safer), but the error message is misleading. A more precise message like "SecretRef at ${params.path} requires a 'provider' field to restore the redacted id automatically (original ref lacks provider)." would better guide users debugging this.
| const originalObj = toObjectRecord(params.original); | |
| if (!isSecretRefWithProvider(originalObj)) { | |
| throw new RedactionError( | |
| params.path, | |
| `SecretRef at ${params.path} contains a redacted id placeholder with no matching original value.`, | |
| ); | |
| } | |
| throw new RedactionError( | |
| params.path, | |
| `SecretRef at ${params.path} requires a 'provider' field to restore the redacted id automatically (original ref lacks provider).`, | |
| ); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/redact-snapshot.ts
Line: 569-575
Comment:
**Misleading error message for SecretRefs without `provider`**
`isSecretRefWithProvider` returns `false` for any SecretRef that lacks a `provider` field, even if the original config has the ref with a legitimate `id`. Since `isSecretRefShape` only requires `source` and `id`, configs with providerless SecretRefs at sensitive paths (e.g. `{source: "env", id: "OPENAI_KEY"}`) will hit this branch and receive `"contains a redacted id placeholder with no matching original value"` — despite a matching original value existing.
The behavior is correct (reject-instead-of-corrupt is safer), but the error message is misleading. A more precise message like `"SecretRef at ${params.path} requires a 'provider' field to restore the redacted id automatically (original ref lacks provider)."` would better guide users debugging this.
```suggestion
throw new RedactionError(
params.path,
`SecretRef at ${params.path} requires a 'provider' field to restore the redacted id automatically (original ref lacks provider).`,
);
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Addressed in 4d2589c.\n\nI added the provider-specific restore error branch in src/config/redact-snapshot.ts so providerless SecretRefs now surface a precise message instead of "no matching original value."\n\nAlso added regression coverage in src/config/redact-snapshot.restore.test.ts (reports a provider-focused error when original SecretRefs lack provider).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b40a949aea
ℹ️ 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".
| const isStructuredValue = | ||
| value !== null && value !== undefined && typeof value === "object" && !Array.isArray(value); | ||
| const rawAvailable = params.rawAvailable ?? true; | ||
| const effectiveRedacted = sensitiveState.isRedacted || isStructuredValue; |
There was a problem hiding this comment.
Restrict structured-value lockout to SecretRef fields
This treats any object value in a text input as redacted/read-only (effectiveRedacted), not just SecretRef payloads. In practice, if a string field is malformed as a non-SecretRef object (for example from manual edits or older config drift), the form can no longer be used to correct it; and when raw mode is unavailable for that snapshot, the UI offers no in-app recovery path. The lockout should be scoped to actual SecretRef shapes (or at least sensitive SecretRef paths) instead of all object values.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 77da09a.\n\nI narrowed the text-input lockout to SecretRef-shaped structured values only (instead of all object values) in ui/src/ui/views/config-form.node.ts.\n\nFor malformed non-SecretRef object values on string fields, the input now stays editable (rendered as JSON text) so users can recover in-app even when Raw mode is unavailable.\n\nAdded regression coverage in ui/src/ui/views/config.browser.test.ts (keeps malformed non-SecretRef object values editable when raw mode is unavailable).
4d2589c to
77da09a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2fe9a41386
ℹ️ 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".
| respond: RespondFn; | ||
| }): Promise<boolean> { | ||
| try { | ||
| await prepareSecretsRuntimeSnapshot({ config: params.config }); |
There was a problem hiding this comment.
Restrict SecretRef preflight to config-sourced refs
This preflight now calls prepareSecretsRuntimeSnapshot, which resolves SecretRefs from auth-profile stores in addition to the submitted config (see the auth-store collection path in src/secrets/runtime.ts and src/secrets/runtime-auth-collectors.ts). That means config.set/config.apply/config.patch can be rejected for unrelated stale keyRef/tokenRef values under ~/.openclaw/... even when the submitted config is valid, effectively blocking config edits because of external auth-profile drift. The write-path preflight should validate only refs reachable from the candidate config payload (or separately classify auth-store failures) so unrelated profile state does not hard-fail config writes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 54ed0dee52.
The config write preflight now scopes SecretRef resolvability checks to the submitted config only:
- Added
includeAuthStoreRefs?: booleantoprepareSecretsRuntimeSnapshot(default remainstrueso startup/reload behavior is unchanged). config.set/config.patch/config.applypreflight now calls it withincludeAuthStoreRefs: false, so unrelated stale auth-profile refs no longer block config writes.
Regression coverage added:
src/secrets/runtime.test.ts:can skip auth-profile SecretRef resolution when includeAuthStoreRefs is falsesrc/gateway/server.config-patch.test.ts:does not reject config.set for unresolved auth-profile refs outside submitted configsrc/gateway/server.config-apply.test.ts:does not reject config.apply for unresolved auth-profile refs outside submitted config
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aea521bb1c
ℹ️ 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".
| respond: RespondFn; | ||
| }): Promise<boolean> { | ||
| try { | ||
| await prepareSecretsRuntimeSnapshot({ config: params.config }); |
There was a problem hiding this comment.
Limit SecretRef preflight to submitted config refs
Restrict this write-path preflight to SecretRefs reachable from the submitted config payload; calling prepareSecretsRuntimeSnapshot here also resolves refs from auth-profile stores, so an unrelated stale keyRef/tokenRef under ~/.openclaw/agents/*/auth-profiles can make config.set/config.apply/config.patch fail even when the proposed config is valid. That blocks routine config edits due to external auth-store drift rather than a defect in the candidate config.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 54ed0dee52.
Confirmed and fixed the preflight scope issue: config write preflight no longer includes auth-profile store refs.
Implementation details:
prepareSecretsRuntimeSnapshotnow acceptsincludeAuthStoreRefs?: boolean(defaulttrueto preserve startup/reload behavior).- Gateway config write preflight for
config.set/config.patch/config.applynow passesincludeAuthStoreRefs: false.
So an unrelated stale tokenRef/keyRef in auth profiles no longer blocks valid config writes.
Added regressions:
src/secrets/runtime.test.ts:can skip auth-profile SecretRef resolution when includeAuthStoreRefs is falsesrc/gateway/server.config-patch.test.ts:does not reject config.set for unresolved auth-profile refs outside submitted configsrc/gateway/server.config-apply.test.ts:does not reject config.apply for unresolved auth-profile refs outside submitted config
54ed0de to
6d8d9ff
Compare
77b8af7 to
074c0e8
Compare
…PC writes (openclaw#58044) * Config: harden SecretRef round-trip handling * Gateway: test SecretRef preflight on config writes * Agents: align skill loader with upstream Skill type * Docs: align SecretRef write semantics with Control UI and RPC behavior * Config: add UI and gateway regression evidence for SecretRef hardening * Config: add token SecretRef restore regression and skill sourceInfo compat * UI: scope structured-value lockout to SecretRef fields * Agents: remove out-of-scope skill loader compat edits * UI: reduce app-render churn to rawAvailable-only changes * Gateway: scope SecretRef preflight to submitted config * Docs: clarify config write SecretRef preflight scope * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…PC writes (openclaw#58044) * Config: harden SecretRef round-trip handling * Gateway: test SecretRef preflight on config writes * Agents: align skill loader with upstream Skill type * Docs: align SecretRef write semantics with Control UI and RPC behavior * Config: add UI and gateway regression evidence for SecretRef hardening * Config: add token SecretRef restore regression and skill sourceInfo compat * UI: scope structured-value lockout to SecretRef fields * Agents: remove out-of-scope skill loader compat edits * UI: reduce app-render churn to rawAvailable-only changes * Gateway: scope SecretRef preflight to submitted config * Docs: clarify config write SecretRef preflight scope * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…PC writes (openclaw#58044) * Config: harden SecretRef round-trip handling * Gateway: test SecretRef preflight on config writes * Agents: align skill loader with upstream Skill type * Docs: align SecretRef write semantics with Control UI and RPC behavior * Config: add UI and gateway regression evidence for SecretRef hardening * Config: add token SecretRef restore regression and skill sourceInfo compat * UI: scope structured-value lockout to SecretRef fields * Agents: remove out-of-scope skill loader compat edits * UI: reduce app-render churn to rawAvailable-only changes * Gateway: scope SecretRef preflight to submitted config * Docs: clarify config write SecretRef preflight scope * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…PC writes (openclaw#58044) * Config: harden SecretRef round-trip handling * Gateway: test SecretRef preflight on config writes * Agents: align skill loader with upstream Skill type * Docs: align SecretRef write semantics with Control UI and RPC behavior * Config: add UI and gateway regression evidence for SecretRef hardening * Config: add token SecretRef restore regression and skill sourceInfo compat * UI: scope structured-value lockout to SecretRef fields * Agents: remove out-of-scope skill loader compat edits * UI: reduce app-render churn to rawAvailable-only changes * Gateway: scope SecretRef preflight to submitted config * Docs: clarify config write SecretRef preflight scope * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…PC writes (openclaw#58044) * Config: harden SecretRef round-trip handling * Gateway: test SecretRef preflight on config writes * Agents: align skill loader with upstream Skill type * Docs: align SecretRef write semantics with Control UI and RPC behavior * Config: add UI and gateway regression evidence for SecretRef hardening * Config: add token SecretRef restore regression and skill sourceInfo compat * UI: scope structured-value lockout to SecretRef fields * Agents: remove out-of-scope skill loader compat edits * UI: reduce app-render churn to rawAvailable-only changes * Gateway: scope SecretRef preflight to submitted config * Docs: clarify config write SecretRef preflight scope * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
…PC writes (openclaw#58044) * Config: harden SecretRef round-trip handling * Gateway: test SecretRef preflight on config writes * Agents: align skill loader with upstream Skill type * Docs: align SecretRef write semantics with Control UI and RPC behavior * Config: add UI and gateway regression evidence for SecretRef hardening * Config: add token SecretRef restore regression and skill sourceInfo compat * UI: scope structured-value lockout to SecretRef fields * Agents: remove out-of-scope skill loader compat edits * UI: reduce app-render churn to rawAvailable-only changes * Gateway: scope SecretRef preflight to submitted config * Docs: clarify config write SecretRef preflight scope * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
Summary
This PR unifies the C1 SecretRef round-trip hardening work into one branch.
It fixes Control UI + gateway write-path corruption where redacted SecretRef object IDs (
__OPENCLAW_REDACTED__) could survive restore, fail schema validation, or corrupt form state.Scope
__OPENCLAW_REDACTED__) as submitted config data.raw: nullinstead of risky reconstructed raw textconfig.set,config.apply,config.patch) before persisting edits.C1 Coverage Matrix
id: "value"round-trip now restores correctly instead of validating the sentinel literal.Tests
pnpm test -- src/config/redact-snapshot.restore.test.ts src/config/redact-snapshot.test.ts src/gateway/server.config-apply.test.ts src/gateway/server.config-patch.test.ts