fix(auth): load legacy Codex OAuth sidecars in embedded secrets-runtime loaders#85074
Conversation
|
Codex review: needs maintainer review before merge. Workflow note: Future ClawSweeper reviews update this same comment in place. How this review workflow works
Summary Reproducibility: yes. from source and linked live proof, though I did not run a local live gateway repro in this read-only review. Current main leaves embedded loaders opted out of legacy sidecar hydration, and the source PR's redacted gateway proof shows the failing embedded Codex paths recovering after this auth change. PR rating 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. Real behavior proof Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest possible solution: Land the scoped loader-default fix if maintainers accept the auth-provider upgrade behavior, while keeping the Keychain-only self-heal design in the separate follow-up. Do we have a high-confidence way to reproduce the issue? Yes from source and linked live proof, though I did not run a local live gateway repro in this read-only review. Current main leaves embedded loaders opted out of legacy sidecar hydration, and the source PR's redacted gateway proof shows the failing embedded Codex paths recovering after this auth change. Is this the best way to solve the issue? Yes. The PR is the narrow owner-boundary fix: reuse the existing legacy sidecar resolver and refresh migration in the embedded loader defaults instead of adding a second migration or enabling Keychain prompts in headless paths. Label changes:
Label justifications:
Acceptance criteria:
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 016c34ff1d2a. |
474a56f to
fc23cd8
Compare
|
ClawSweeper PR egg ✨ Hatched: 🥚 common Mossy Crabkin Hatch commandComment Hatchability rules:
Rarity: 🥚 common. What is this egg doing here?
|
fc23cd8 to
ac608c7
Compare
|
Filed #85083 as the follow-up for the Keychain-only sidecar case explicitly called out as out-of-scope here. Lands the same self-heal experience for the smaller bucket of users whose legacy seed is in macOS Keychain, with a safe-prompting design (interactive: migrate; headless: fail clean) instead of just flipping |
…me loaders The auto-migration introduced in #83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing #83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR #84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
ac608c7 to
17eb0b3
Compare
…first interactive CLI run Closes the remaining bucket from #85083: macOS users whose legacy `oauthRef` sidecar seed lives only in the Keychain still had to know to run `openclaw doctor --fix` after upgrade, because the embedded-runtime self-heal added in #85074 intentionally runs with `allowKeychainPrompt: false`. This change adds a tiny pre-command trigger in `runCli` that, on darwin + TTY only, reuses the existing `maybeRepairLegacyOAuthSidecarProfiles` doctor flow when a sidecar dir is present. Fast bail-outs make the cost a single `readdirSync` for users without legacy sidecars. Declining writes a 24h cool-off marker in the state dir so we never pester the user every command. Headless paths (cron, systemd, Telegram polling, embedded sub-agent dispatch) still skip the prompt, but the legacy sidecar loader now emits a one-shot warn that names `openclaw doctor --fix` and macOS Keychain, instead of letting the credential silently fall through to a downstream "No API key found". Also drops the user-confusing `sidecar-backed` phrasing from the doctor prompt/notes/changes; existing internal identifier names are left intact.
…first interactive CLI run Closes the remaining bucket from #85083: macOS users whose legacy `oauthRef` sidecar seed lives only in the Keychain still had to know to run `openclaw doctor --fix` after upgrade, because the embedded-runtime self-heal added in #85074 intentionally runs with `allowKeychainPrompt: false`. This change adds a tiny pre-command trigger in `runCli` that, on darwin + TTY only, reuses the existing `maybeRepairLegacyOAuthSidecarProfiles` doctor flow when a sidecar dir is present. Fast bail-outs make the cost a single `readdirSync` for users without legacy sidecars. Declining writes a 24h cool-off marker in the state dir so we never pester the user every command. Headless paths (cron, systemd, Telegram polling, embedded sub-agent dispatch) still skip the prompt, but the legacy sidecar loader now emits a one-shot warn that names `openclaw doctor --fix` and macOS Keychain, instead of letting the credential silently fall through to a downstream "No API key found". Also drops the user-confusing `sidecar-backed` phrasing from the doctor prompt/notes/changes; existing internal identifier names are left intact.
…first interactive CLI run Closes the remaining bucket from #85083: macOS users whose legacy `oauthRef` sidecar seed lives only in the Keychain still had to know to run `openclaw doctor --fix` after upgrade, because the embedded-runtime self-heal added in #85074 intentionally runs with `allowKeychainPrompt: false`. This change adds a tiny pre-command trigger in `runCli` that, on darwin + TTY only, reuses the existing `maybeRepairLegacyOAuthSidecarProfiles` doctor flow when a sidecar dir is present. Fast bail-outs make the cost a single `readdirSync` for users without legacy sidecars. Declining writes a 24h cool-off marker in the state dir so we never pester the user every command. Headless paths (cron, systemd, Telegram polling, embedded sub-agent dispatch) still skip the prompt, but the legacy sidecar loader now emits a one-shot warn that names `openclaw doctor --fix` and macOS Keychain, instead of letting the credential silently fall through to a downstream "No API key found". Also drops the user-confusing `sidecar-backed` phrasing from the doctor prompt/notes/changes; existing internal identifier names are left intact.
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
…me loaders (openclaw#85074) The auto-migration introduced in openclaw#83312 only fires when a credential is loaded via a path that reads its sidecar tokens. The OAuth refresh manager's internal loader does (so direct CLI inference works and self-heals on first refresh). The embedded runner's secrets-runtime loaders did not: - loadAuthProfileStoreForSecretsRuntime - loadAuthProfileStoreWithoutExternalProfiles - ensureAuthProfileStoreWithoutExternalProfiles All three opted out of sidecar resolution. So for an upgraded user with a legacy oauthRef-backed openai-codex profile, the credential loaded with no access/refresh material, evaluateStoredCredentialEligibility marked it ineligible, resolveAuthProfileOrder filtered it out, and resolveApiKeyForProvider threw "No API key found for provider 'openai-codex'" before the OAuth manager (and its migration path) was ever consulted. CLI worked, Telegram/cron/embedded turns broke — only doctor-or-bust would fix it. Flip the three embedded loaders to default resolveLegacyOAuthSidecars to true (matching loadStoredOAuthRefreshStore). The existing openclaw#83312 refresh-and-rewrite then fires on the first embedded turn for these users and persists tokens inline, removing the legacy sidecar from disk on the next doctor pass. Cherry-picked and squashed from PR openclaw#84752 (commits 85f36e8 and 4624e34). Comments noting local-fork bookkeeping stripped per repo policy. Co-authored-by: Will <totalsolutionspm@gmail.com>
Credit
This fix originates from @Totalsolutionsync's work in #84752 (P1, with live self-hosted-gateway proof attached there). I cherry-picked it into a single-purpose PR because #84752 bundles three orthogonal fixes (diagnostic lane, auth, Telegram polling) and the author explicitly offered to split. This PR takes them up on the auth half so the high-priority Codex-OAuth-on-upgrade fix can land on its own.
Commits cherry-picked:
85f36e8d2b4e+4624e34c0671. Adjustments on top: stripped local-fork bookkeeping comments per repo policy, fixed aunicorn(no-useless-fallback-in-spread)lint error that surfaced oncheck-lint, added a focused regression test for the three flipped loaders, and added a doc note about Keychain-only legacy sidecars still requiring doctor.What the bug is
#83312 added a runtime auto-migration for legacy
oauthRef-backedopenai-codexprofiles: when the OAuth refresh manager touches one, it persists the rotated tokens inline. CLI inference hits that path, so CLI users self-heal. The embedded runner (Telegram, cron, sub-agent dispatch) goes through three different loaders that hardcodedresolveLegacyOAuthSidecars: false, so the credential loads token-less, gets filtered as ineligible, and the OAuth manager — and therefore the migration — is never reached. Result:No API key found for provider "openai-codex"on every embedded turn until the user runsopenclaw doctor.Users who upgrade via
openclaw updateare fine (it runs doctor). Users who upgrade vianpm i -g openclaw@latesthit this — I did.The fix
Flip the three embedded loaders' default to
true, matching whatloadStoredOAuthRefreshStorealready does inside the refresh lock. The existing #83312 migration then fires on the first embedded turn and persists tokens inline.Scope note: Keychain-only sidecars still need doctor
The embedded runtime path runs with
allowKeychainPrompt: false, andreadLegacyMacOAuthSecretKeychainKeyearly-bails when that flag is false. So this PR fully self-heals users whose legacy sidecar seed lives inOPENCLAW_AUTH_PROFILE_SECRET_KEYor the seed file under~/.openclaw/oauth/..., but does not self-heal users whose seed lives only in macOS Keychain. Those users still need a one-timeopenclaw doctor --fixto migrate inline. A doc note has been added indocs/gateway/doctor.mdcovering this, and the Keychain follow-up is tracked in #85083.Regression test
src/agents/auth-profiles/store.sidecar-runtime-defaults.test.tsbuilds a fixture with a legacyoauthRef-backed Codex profile and a sidecar file encrypted with a file/env-based seed, then asserts each of the three flipped loaders returns the credential with inlineaccess/refresh/idTokenhydrated by default — and that explicitresolveLegacyOAuthSidecars: falsestill opts out.Verification
node scripts/run-vitest.mjs src/agents/auth-profiles/store.sidecar-runtime-defaults.test.ts src/agents/auth-profiles/oauth-manager.test.ts src/agents/auth-profiles/persisted-boundary.test.ts— 50 passed across 6 test-file shards.node scripts/run-oxlint.mjs src/agents/auth-profiles/store.ts— clean after the spread fix.Companion
Pairs with #85028 (fail-fast when an OAuth credential has no refresh token) as defense-in-depth.
Co-authored-by: @Totalsolutionsync.