fix(auth-profiles): preserve secret refs and OAuth fields during doctor auth migration#97881
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 29, 2026, 12:59 PM ET / 16:59 UTC. Summary PR surface: Source -46, Tests +136. Total +90 across 4 files. Reproducibility: yes. Source inspection shows current main drops the cited fields in both migration writers, and the PR body plus tests exercise the actual migration functions with before/after credential-field output. Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the focused migration fix that reuses the canonical credential parser for both legacy writers, keeping coverage on both JSON migration paths and avoiding any new runtime fallback. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main drops the cited fields in both migration writers, and the PR body plus tests exercise the actual migration functions with before/after credential-field output. Is this the best way to solve the issue? Yes. Reusing the canonical credential parser is the best local fix because it removes duplicate field-copy logic in both writers instead of adding another manual preservation list that can drift again. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 6de357ad4772. Label changesLabel justifications:
Evidence reviewedPR surface: Source -46, Tests +136. Total +90 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
This pull request has been automatically marked as stale due to inactivity. |
…or auth migration Both legacy auth migration writers rebuilt each credential from a hardcoded field subset, dropping keyRef/tokenRef and the OAuth clientId/idToken/ chatgptPlanType, even though the canonical reader already preserves them. applyLegacyAuthStore now keeps the already-parsed credential as-is, and coerceLegacyFlatCredential delegates field extraction to the shared parseLegacyCredentialEntry while keeping its existing type and usability gates. The flat writer also no longer drops a secret-ref-only credential. This routes both migration paths through one normalization path so doctor --fix stops silently degrading or destroying credentials before removing the legacy files.
800a873 to
ccb3811
Compare
|
@jalehman Seems like the checks are passing can this be merged?? |
|
Merged via squash.
|
Sorry about the delay — thought I had merged this! |
All good!! |
…or auth migration (openclaw#97881) Both legacy auth migration writers rebuilt each credential from a hardcoded field subset, dropping keyRef/tokenRef and the OAuth clientId/idToken/ chatgptPlanType, even though the canonical reader already preserves them. applyLegacyAuthStore now keeps the already-parsed credential as-is, and coerceLegacyFlatCredential delegates field extraction to the shared parseLegacyCredentialEntry while keeping its existing type and usability gates. The flat writer also no longer drops a secret-ref-only credential. This routes both migration paths through one normalization path so doctor --fix stops silently degrading or destroying credentials before removing the legacy files.
What Problem This Solves
openclaw doctor --fixmigrates a user's legacy auth credentials into the canonical per-agent SQLite store and then backs up and removes the old JSON files. Both migration writers rebuild each credential from a hardcoded subset of fields and silently drop the rest, so the migration permanently loses refresh-critical and secret material:applyLegacyAuthStore(src/agents/auth-profiles/persisted.ts), which imports the legacyauth.jsonfile, dropskeyRefandtokenRef(secret-ref indirection) and the OAuthclientId,idToken, andchatgptPlanTypefields.coerceLegacyFlatCredential(src/commands/doctor-auth-flat-profiles.ts), which imports a flat-shapedauth-profiles.json, drops the identical fields and returnsnullfor a secret-ref-only credential, so that credential is not imported at all.The post-migration verification only checks that profile ids exist, so the corruption passes silently. The legacy file is then removed, and the user later hits auth failures (OAuth that can no longer refresh, or an API key with no secret) with no upgrade-time warning. The two writers read different files (
auth.jsonvs a flatauth-profiles.json), so each is independently reachable; fixing one is not enough.This is distinct from #97522/#97541 (doctor overwrites a second OAuth account), #96544 (map-key collision), and #93814 (trajectory export). The open #93352 adds a runtime read-path importer in different functions and does not address the dropped fields.
Why This Change Was Made
The asymmetry is reader vs writer. The canonical credential reader
parseCredentialEntry(used byloadLegacyAuthProfileStoreandcoercePersistedAuthProfileStore) already normalizes and preserves the full credential, includingkeyRef,tokenRef,clientId,idToken, andchatgptPlanType. The two migration writers hand-rebuilt a lossy copy of that normalization instead of reusing it.applyLegacyAuthStorereceives an already-parsedAuthProfileCredential(LegacyAuthStore = Record<string, AuthProfileCredential>), so it now preserves that credential as-is and only defaults the provider from the map key. This deletes the duplicate field-by-field rebuild.coerceLegacyFlatCredentialnow delegates field extraction to the same canonical parser via the newparseLegacyCredentialEntry, keeping its existing flat-shape type inference as the acceptance gate andhasUsableAuthProfileCredentialas the usability gate. This keeps every prior accept/reject decision identical while preserving all fields, and removes the duplicate parser.Net production code drops by 46 lines, and both migration paths now share one normalization path, so they cannot drift apart again.
User Impact
Users running
openclaw doctor --fixwith a legacyauth.jsonor a flatauth-profiles.jsonkeep their full credentials after migration: OAuth refresh material (clientId,idToken,chatgptPlanType) survives, secret-ref-backed API keys and tokens survive, and secret-ref-only credentials are no longer dropped. No config or behavior change for credentials that already migrated cleanly.Evidence
Both migration legs were driven with their real production entry points on pristine
origin/main(6de357ad47) and on this patch, with identical inputs.The legacy
auth.jsonleg was driven throughloadLegacyAuthProfileStore+applyLegacyAuthStore(the exact functions the doctor import calls). The flatauth-profiles.jsonleg was driven end to end throughmaybeMigrateAuthProfileJsonStoresToSqlite(the real doctor migration), then read back from the real SQLite store vialoadPersistedAuthProfileStore.Verification
node scripts/run-vitest.mjs src/agents/auth-profiles/persisted-boundary.test.ts src/commands/doctor-auth-flat-profiles.test.tspasses (38 tests). The 3 new tests fail on pristineorigin/mainand pass with this patch.oxlintandoxfmt --checkclean on the 4 changed files.node scripts/run-tsgo.mjs -p tsconfig.core.jsonand-p test/tsconfig/tsconfig.core.test.jsonboth clean.Real behavior proof
Behavior addressed: openclaw doctor --fix migration dropped OAuth refresh fields (clientId/idToken/chatgptPlanType) and secret refs (keyRef/tokenRef) from both the legacy auth.json writer and the flat auth-profiles.json writer, silently degrading or destroying credentials.
Real environment tested: drove the real migration functions on pristine origin/main 6de357a and on the patched tree with identical inputs. The legacy leg ran loadLegacyAuthProfileStore + applyLegacyAuthStore against an on-disk auth.json. The flat leg ran maybeMigrateAuthProfileJsonStoresToSqlite end to end and read the result back from the real per-agent SQLite store via loadPersistedAuthProfileStore. Everything stayed real; nothing stubbed.
Exact steps or command run after this patch: wrote a legacy auth.json and a flat auth-profiles.json holding OAuth extras and secret-ref credentials, ran the real migration, then printed the resulting credential fields.
Evidence after fix:
Observed result after fix: on identical inputs the migrated credentials now retain clientId, idToken, chatgptPlanType, and the keyRef/tokenRef secret references on both the legacy auth.json path and the flat auth-profiles.json path, where before they were undefined or the credential was dropped entirely.
What was not tested: no live provider auth request was made against a real OAuth or API endpoint; full build was not run.
Open question for maintainers
The code-level loss is proven and unmasked on both writers. The real-world blast radius depends on whether shipped flat auth.json / auth-profiles.json files actually carried keyRef, tokenRef, clientId, idToken, or chatgptPlanType. The canonical reader contains explicit parsing for exactly these fields out of the legacy flat shape, which is strong evidence they shipped, but a maintainer who knows the shipped on-disk history can confirm severity.