Skip to content

Preserve externally refreshed Gemini credentials - #75

Merged
HemSoft merged 5 commits into
mainfrom
fix/issue-66-preserve-gemini-credentials
Jul 23, 2026
Merged

Preserve externally refreshed Gemini credentials#75
HemSoft merged 5 commits into
mainfrom
fix/issue-66-preserve-gemini-credentials

Conversation

@HemSoft

@HemSoft HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Closes #66

Summary

  • re-read Gemini CLI credentials immediately before persisting a token refresh
  • adopt a valid external refresh when Gemini CLI updates the file during the request
  • preserve unrelated credential metadata and owner-only file permissions

Verification

  • ./test.sh (210 tests, 0 failures; release artifact and changelog smoke tests passed)
  • git diff --check

Note

Preserve externally refreshed Gemini credentials during in-flight token refresh

  • Before writing refreshed tokens, GeminiUsageProvider.resolveAccessToken checks whether the Gemini credentials file was updated externally (e.g. by the Gemini CLI) and adopts the external tokens instead of overwriting them.
  • A new optimistic write path in GeminiAuthFileStore (writeCredentials(ifUnchangedFrom:)) uses NSFileCoordinator to serialize writes and returns the newer on-disk credentials if the file changed since the expected snapshot, rather than clobbering them.
  • When an external update is adopted, refresh and ID tokens from the original are merged in if absent from the external credentials, and the result is persisted atomically with 0600 permissions.
  • Behavioral Change: writeCredentials now throws additional errors (invalidCredentialFile, unableToCoordinateWrite) and all credential writes go through NSFileCoordinator.

Macroscope summarized 7b3190d.

@cursor

cursor Bot commented Jul 23, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Gemini token refresh now re-reads credentials before persistence and preserves newer externally written tokens. A deterministic test covers the in-flight update, token usage, metadata, and permissions. The changelog documents the fix.

Changes

Gemini credential refresh

Layer / File(s) Summary
Preserve externally refreshed credentials
CodexBarMac/Services/GeminiUsageProvider.swift, CodexBarMacTests/CodexBarMacTests.swift, CHANGELOG.md
The provider detects credential-file changes during refresh, uses a valid external access token, and avoids overwriting newer credentials. Tests verify tokens, metadata, and 0o600 permissions, while the changelog records the fix.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • HemSoft/codexbar-mac#34: Updates Gemini token refresh handling and adds concurrency coverage for externally refreshed credentials.

Suggested labels: bug

Poem

A rabbit watched the tokens race,
Then guarded every newer trace.
Access, refresh, ID stayed bright,
Metadata tucked safe and tight.
“No stale overwrite tonight!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy the issue goals: re-read before persistence, preserve newer tokens and metadata, add a deterministic test, and update the changelog.
Out of Scope Changes check ✅ Passed The PR stays focused on Gemini refresh handling, its test, and the changelog with no obvious unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: keeping externally refreshed Gemini credentials from being overwritten.
Description check ✅ Passed The description matches the PR and explains the refresh handling, metadata, permissions, and verification work.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-66-preserve-gemini-credentials

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 0489715e1b

ℹ️ 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 on lines +246 to +249
guard let latestCredentials = GeminiAuthFileStore.readCredentials(at: oauthFilePath) else {
return .transient
}
if latestCredentials != credentials {

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 Re-read credentials before rejecting refresh errors

When the Gemini CLI refreshes this OAuth file while CodexBar's refresh request is in flight, the POST made with the old refresh token can come back as 400/invalid_grant after the file already contains a fresh access/refresh token. Because this re-read only happens after a successful token response, that race still returns rejected and tells the user to re-authenticate instead of preferring the updated local CLI credentials.

AGENTS.md reference: AGENTS.md:L20-L24

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in 4674ec9. The provider now re-reads credentials immediately after the awaited token request, before interpreting HTTP or OAuth errors, and adopts a fresh external access token. It still re-checks immediately before persistence to cover a later write. Added a deterministic 400/invalid_grant race test; the full ./test.sh suite passes (207 tests).

@coderabbitai coderabbitai Bot added the bug Something isn't working label Jul 23, 2026
@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 4674ec9a14

ℹ️ 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 on lines +297 to +302
guard let accessToken = latest.accessToken,
!accessToken.isEmpty,
!latest.shouldRefresh(at: now()) else {
return .unusable
}
return .valid(accessToken)

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 Require a new access token before adopting external refresh

When this refresh is forced after a quota 401, the original.accessToken has already been rejected, but this helper treats any parsed credential change as usable as long as the on-disk access token is non-expired. If Gemini CLI or another credential writer changes only id_token, client metadata, refresh token, or expiry while leaving access_token unchanged during the token request, we return the same bearer that just got a 401, discard the successful refresh response, and the retry cannot refresh again because canRefresh is already false. Please only adopt the external update when it provides a different access token in the forced/rejected-token path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in 2ae3932. Forced refreshes now adopt an external update only when it supplies a different valid access token, so metadata-only or refresh-token-only changes cannot reuse the bearer that just received a 401. Added a regression test covering that forced-refresh path; ./test.sh passes 208 tests.

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 2ae393202c

ℹ️ 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 on lines +264 to +267
switch externalCredentialUpdate(
since: credentials,
requiresDifferentAccessToken: force
) {

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 Make the credential recheck atomic with the write

When Gemini CLI writes oauth_creds.json in the window after this re-read returns .unchanged but before the following writeCredentials call, the newer tokens are still overwritten: writeCredentials rereads the file only to merge metadata, then unconditionally replaces the token fields with updated built from the old snapshot. This leaves the same in-flight refresh race this change is trying to prevent; the compare/merge needs to happen in the write path, or otherwise be made atomic with the write, so a post-check external update is not clobbered.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in 0ab86fd. The provider now uses a conditional credential-store write: under NSFileCoordinator, the store re-reads and compares the on-disk credentials with the request snapshot immediately in the write path. If they changed, it returns and preserves the external credentials instead of writing the stale refresh result. Added a deterministic post-check update regression; ./test.sh passes 209 tests.

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 0ab86fdc8a

ℹ️ 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 on lines +325 to +328
guard let accessToken = latest.accessToken,
!accessToken.isEmpty,
!latest.shouldRefresh(at: now()),
!requiresDifferentAccessToken || accessToken != original.accessToken else {

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 Keep refresh tokens when adopting external Gemini updates

When the Gemini CLI refreshes during this request and writes a fresh access_token/expiry_date but no refresh_token, this guard classifies that file as .valid, so the callers return early and skip the merge path that preserves credentials.refreshToken. The app then leaves an access-only credential file behind; after that access token expires, resolveAccessToken rejects the account because there is no refresh token, forcing the user to re-authenticate even though the original refresh token was still available.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in the latest commit. External credential adoption now conditionally writes back only missing refresh and ID tokens from the original snapshot while preserving the new external access token, expiry, and unrelated metadata. A regression covers an external access-token refresh that omits both fields; ./test.sh passes 210 tests.

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@HemSoft

HemSoft commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 7b3190d8e3

ℹ️ 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".

@HemSoft
HemSoft merged commit 806590d into main Jul 23, 2026
3 checks passed
@HemSoft
HemSoft deleted the fix/issue-66-preserve-gemini-credentials branch July 23, 2026 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Preserve externally refreshed Gemini CLI credentials

1 participant