Skip to content

fix(auth+shell): PR #215 review remediation and running-prompt ghost fix - #217

Merged
elkaix merged 6 commits into
mainfrom
consolidated-auth-tui-fix
Jul 19, 2026
Merged

fix(auth+shell): PR #215 review remediation and running-prompt ghost fix#217
elkaix merged 6 commits into
mainfrom
consolidated-auth-tui-fix

Conversation

@elkaix

@elkaix elkaix commented Jul 19, 2026

Copy link
Copy Markdown
Member

Summary

Consolidates two independent fixes onto one branch off main:

  1. PR feat(auth): add xAI, GitHub Copilot, DigitalOcean, and Snowflake Cortex OAuth login providers #215 auth review remediation — hardens the xAI / GitHub Copilot / DigitalOcean / Snowflake Cortex OAuth login providers merged in feat(auth): add xAI, GitHub Copilot, DigitalOcean, and Snowflake Cortex OAuth login providers #215: scope token refresh to the active/selected provider, serialize credential persistence under a lock with atomic config replacement, roll back replaced credentials on a save failure, fail closed on credential migration, validate OAuth token and implicit-state responses, bound implicit callback bodies, and preserve router-discovery / catalog-degradation truthfulness. Adds comprehensive OAuth persistence, refresh, platform, and provider test coverage.

  2. Running-prompt "ghost" fix — on long streaming turns the running-prompt block (agent tree, spinner, tip) could render a doubled/ghost copy. run_in_terminal re-requests an absolute cursor position (an async CPR round-trip) on teardown; the suppressed live body re-expanded before that settled, diffing against a provisional cursor model and stranding the old rows. It now awaits the CPR settle while the handoff is still suppressed, so the body re-expands against a correct cursor model. Unlike a renderer reset, this touches no static scrollback, so the fix(shell): remove queued-input bordered ghost in running-turn TUI #216 queued-input ghost fix stays intact (verified by its e2e test).

Verification

Notes

  • The running-prompt ghost is a real-terminal cursor-timing race; the automated tests assert the structural fix and guard against re-regressing fix(shell): remove queued-input bordered ghost in running-turn TUI #216, but final visual confirmation is best done in a live terminal (Ctrl+L clears any residual — same renderer.clear() mechanism).
  • Also gitignores /tasks/ working files.

Summary by CodeRabbit

  • Security & Authentication
    • Hardened OAuth handling with strict token validation, provider-scoped refresh, fail-closed locking, and atomic persistence with rollback (including keyring migration).
    • Improved implicit-flow callback safety: validates Content-Length and rejects blank access tokens.
  • Bug Fixes
    • Fixed intermittent duplicated/“ghost” running-prompt content on long streaming turns by settling cursor state after scrollback handoff.
    • Improved managed-provider refresh so updates apply only when provider state still matches.
    • DigitalOcean router discovery now distinguishes and reports empty, malformed, and partial catalog results.
  • Reliability
    • File locking now supports bounded waits and raises a dedicated timeout error when contention persists.

elkaix added 3 commits July 19, 2026 11:49
Hardens the xAI / GitHub Copilot / DigitalOcean / Snowflake Cortex OAuth
login providers merged in #215: scope OAuth refresh to the active/selected
provider, serialize credential persistence under a lock with atomic config
replacement, roll back replaced credentials on failure, fail closed on
credential migration, validate OAuth token and implicit-state responses,
bound implicit callback bodies, and preserve router-discovery and catalog
degradation truthfulness. Adds comprehensive OAuth persistence, refresh,
platform, and provider test coverage.
…mpt ghost

On long streaming turns the running-prompt block (agent tree, spinner, tip)
could render a doubled "ghost" copy. run_in_terminal re-requests an absolute
cursor position (async CPR round-trip) on teardown; the suppressed live body
re-expanded before that settled, diffing against a provisional cursor model
and stranding the old rows. Await the CPR settle while the handoff is still
suppressed so the body re-expands against a correct cursor model. Unlike a
renderer reset this touches no static scrollback, so it does not re-fossilize
committed content (the queued-input ghost fix stays intact).
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 20 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5bf4cf7-9ebc-4614-a2fc-0c26c5adf7e8

📥 Commits

Reviewing files that changed from the base of the PR and between 5b808da and 7966bd7.

📒 Files selected for processing (4)
  • src/pythinker_code/auth/oauth.py
  • src/pythinker_code/auth/platforms.py
  • tests/auth/test_oauth_persist.py
  • tests/ui_and_conv/test_visualize_running_prompt.py
📝 Walkthrough

Walkthrough

OAuth handling now validates provider responses, uses scoped refreshes, bounded locks, atomic persistence, rollback, and guarded managed-model commits. DigitalOcean discovery and terminal scrollback handoffs gain dedicated edge-case handling and regression coverage.

Changes

OAuth and provider flows

Layer / File(s) Summary
OAuth response and provider validation
CHANGELOG.md, src/pythinker_code/auth/{digitalocean,oauth_flows,snowflake,xai}.py, tests/auth/*
OAuth payloads, callback bodies, access tokens, and router catalogs now reject or classify malformed data without persisting invalid credentials or exposing sensitive fields.
Credential storage and atomic persistence
src/pythinker_code/auth/oauth.py, src/pythinker_code/utils/io.py, tests/auth/test_oauth_{persist,cross_process,refresh}.py
Credential paths, keyring migration, file locks, cancellation, rollback, and cross-process refresh now use bounded, atomic, fail-closed persistence behavior.
Scoped OAuth refresh and managed-model commits
src/pythinker_code/auth/{oauth,platforms}.py, src/pythinker_code/web/api/sessions.py, tests/auth/test_{platforms,oauth_refresh}.py, tests/web/test_sessions_api.py
Provider-specific refresh references and snapshot-guarded working-config updates prevent stale discovery results from overwriting committed provider changes.

Terminal rendering

Layer / File(s) Summary
Cursor settlement after scrollback handoff
src/pythinker_code/ui/shell/visualize/_interactive.py, tests/ui_and_conv/test_visualize_running_prompt.py
Scrollback handoffs await cursor-position responses before prompt re-expansion and verify the ordering with regression tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant refresh_managed_models
  participant OAuthManager
  participant persist_config_change
  refresh_managed_models->>OAuthManager: ensure_fresh(oauth_ref)
  OAuthManager-->>refresh_managed_models: refreshed provider credentials
  refresh_managed_models->>persist_config_change: apply snapshot-guarded model updates
  persist_config_change-->>refresh_managed_models: authoritative committed configuration
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it omits the required Related Issue section and the checklist from the template. Add a Related Issue section with Resolve #(issue_number) and include the checklist items, especially tests/changelog/docs confirmations.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.60% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Uses conventional-commit form fix(auth+shell): ... and matches the auth/shell changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch consolidated-auth-tui-fix

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

Comment thread tests/auth/test_oauth_persist.py
Comment thread tests/auth/test_oauth_persist.py
Comment thread tests/auth/test_oauth_persist.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pythinker_code/auth/oauth.py`:
- Around line 642-645: Update load_tokens and the keyring migration flow around
_migrate_keyring_token so its read-copy-delete operation is serialized with
persist_login, persist_logout, and other credential writes via
_credential_transaction_locks. Ensure callers such as resolve_api_key,
get_chatgpt_account_id, and _refresh_tokens cannot perform keyring migration
outside the same lock, while preserving normal file-token loading.

In `@src/pythinker_code/auth/platforms.py`:
- Around line 353-356: Update the OAuthManager construction in the
provider.oauth branch to run off the event loop, using asyncio.to_thread around
OAuthManager(working_config) so its synchronous initialization and
_migrate_oauth_storage file lock cannot block async refresh handling.

In `@src/pythinker_code/ui/shell/visualize/_interactive.py`:
- Around line 249-250: Replace the suppress(Exception) block in
_settle_cursor_after_handoff with explicit exception handling that records the
exception through _handoff_trace and logger.debug, matching
_reset_prompt_renderer and _safe_prompt_invalidate. Preserve the existing
wait_for_cpr_responses behavior while ensuring CPR-settling failures are logged.

In `@tests/ui_and_conv/test_visualize_running_prompt.py`:
- Around line 217-273: Add a failure-path regression test alongside
test_scrollback_handoff_settles_cursor_before_reexpanding, making
run_in_terminal raise during _emit_incremental_content_commits. Assert the
RuntimeError propagates, _settle_cursor_after_handoff runs while
_scrollback_handoff_depth is 1, _reset_prompt_renderer is invoked, and the
handoff depth returns to zero.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f9a4760d-1e92-4f0e-b002-9a6b55e679fc

📥 Commits

Reviewing files that changed from the base of the PR and between 887401e and 12bc0c0.

⛔ Files ignored due to path filters (2)
  • docs/en/release-notes/changelog.md is excluded by !docs/**
  • docs/superpowers/specs/2026-07-18-pr-215-auth-review-fixes-design.md is excluded by !docs/**
📒 Files selected for processing (21)
  • .gitignore
  • CHANGELOG.md
  • src/pythinker_code/auth/digitalocean.py
  • src/pythinker_code/auth/oauth.py
  • src/pythinker_code/auth/oauth_flows.py
  • src/pythinker_code/auth/platforms.py
  • src/pythinker_code/auth/snowflake.py
  • src/pythinker_code/auth/xai.py
  • src/pythinker_code/ui/shell/visualize/_interactive.py
  • src/pythinker_code/utils/io.py
  • src/pythinker_code/web/api/sessions.py
  • tests/auth/test_digitalocean_auth.py
  • tests/auth/test_oauth_cross_process.py
  • tests/auth/test_oauth_flows.py
  • tests/auth/test_oauth_persist.py
  • tests/auth/test_oauth_refresh.py
  • tests/auth/test_platforms.py
  • tests/auth/test_snowflake_auth.py
  • tests/auth/test_xai_auth.py
  • tests/ui_and_conv/test_visualize_running_prompt.py
  • tests/web/test_sessions_api.py

Comment thread src/pythinker_code/auth/oauth.py
Comment thread src/pythinker_code/auth/platforms.py Outdated
Comment thread src/pythinker_code/ui/shell/visualize/_interactive.py Outdated
Comment thread tests/ui_and_conv/test_visualize_running_prompt.py
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

- Log CPR-settle failures in `_settle_cursor_after_handoff` via `_handoff_trace`
  + `logger.debug` instead of silently suppressing, matching the sibling
  `_reset_prompt_renderer` / `_safe_prompt_invalidate` handlers (no silent
  swallow).
- Construct `OAuthManager` off the event loop in `refresh_managed_models` via
  `asyncio.to_thread`, so its synchronous `_migrate_oauth_storage()` file lock
  cannot stall async refresh.
- Add a failure-path test asserting the scrollback-handoff `finally` still
  settles the cursor (and the fail path resets the renderer) when emit raises.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pythinker_code/ui/shell/visualize/_interactive.py (1)

311-313: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make handoff cleanup cancellation-safe. Cancellation at the CPR wait can leave _scrollback_handoff_depth stuck > 0 and keep the prompt in finalizing mode; move the decrement and invalidate into an inner finally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pythinker_code/ui/shell/visualize/_interactive.py` around lines 311 -
313, Update the handoff cleanup around `_settle_cursor_after_handoff()` so
cancellation during its wait still executes cleanup by placing
`_scrollback_handoff_depth` decrement and `_safe_prompt_invalidate()` in an
inner finally block. Preserve the existing ordering and ensure both cleanup
actions run exactly once after the handoff attempt.

Source: Coding guidelines

src/pythinker_code/auth/platforms.py (1)

548-568: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return the guarded commit result, not the local working-copy diff.

changed is computed while mutating working_config, but apply_updates may skip every update when providers_match fails—or may apply a change to the latest committed config even when changed is False. The function can therefore report success or no change incorrectly on concurrent refreshes. Track whether guarded applications actually changed committed and return that status.

As per coding guidelines, observable results must distinguish success, failure, and partial success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pythinker_code/auth/platforms.py` around lines 548 - 568, Update the
surrounding function that defines apply_updates and returns changed so it tracks
whether guarded applications actually modify committed. Have each successful
_apply_models, apply_opencode_go_models, apply_minimax_models,
apply_z_ai_models, and apply_kimi_models operation contribute to a
committed-change flag, while skipped provider_match guards do not; return the
persist_config_change result or equivalent guarded commit status instead of the
local working_config diff, preserving distinct success, failure, and
partial-success outcomes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pythinker_code/auth/platforms.py`:
- Around line 357-360: Update the OAuthManager construction in the
refresh_managed_models flow to retain the asyncio.to_thread task, and on
cancellation await that task to completion before re-raising CancelledError.
Preserve the existing OAuthManager construction and ensure
_migrate_oauth_storage cannot continue writing shared state after the caller has
unwound.

In `@tests/ui_and_conv/test_visualize_running_prompt.py`:
- Around line 311-314: Update the test around the view handoff to exercise the
renderer boundary instead of monkeypatching private helpers such as
_settle_cursor_after_handoff and _reset_prompt_renderer. Fake get_app_or_none()
with a renderer test double, have run_in_terminal invoke func() while making
emission fail, and assert the renderer’s CPR wait/reset calls and their
ordering.

---

Outside diff comments:
In `@src/pythinker_code/auth/platforms.py`:
- Around line 548-568: Update the surrounding function that defines
apply_updates and returns changed so it tracks whether guarded applications
actually modify committed. Have each successful _apply_models,
apply_opencode_go_models, apply_minimax_models, apply_z_ai_models, and
apply_kimi_models operation contribute to a committed-change flag, while skipped
provider_match guards do not; return the persist_config_change result or
equivalent guarded commit status instead of the local working_config diff,
preserving distinct success, failure, and partial-success outcomes.

In `@src/pythinker_code/ui/shell/visualize/_interactive.py`:
- Around line 311-313: Update the handoff cleanup around
`_settle_cursor_after_handoff()` so cancellation during its wait still executes
cleanup by placing `_scrollback_handoff_depth` decrement and
`_safe_prompt_invalidate()` in an inner finally block. Preserve the existing
ordering and ensure both cleanup actions run exactly once after the handoff
attempt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7b91e679-9e4e-4158-bf9a-a6edd362db82

📥 Commits

Reviewing files that changed from the base of the PR and between 12bc0c0 and 5b808da.

📒 Files selected for processing (3)
  • src/pythinker_code/auth/platforms.py
  • src/pythinker_code/ui/shell/visualize/_interactive.py
  • tests/ui_and_conv/test_visualize_running_prompt.py

Comment thread src/pythinker_code/auth/platforms.py Outdated
Comment thread tests/ui_and_conv/test_visualize_running_prompt.py
elkaix added 2 commits July 19, 2026 13:01
load_tokens()'s keyring read-copy-delete migration ran outside
_credential_transaction_locks, so it could race persist_login/persist_logout
and overwrite a newer file token with the older keyring value. Serialize it
under the credential lock. The lock is now reentrant per thread (fcntl.flock /
msvcrt.locking deny a second same-file acquisition within one process), so
callers already inside a credential transaction — e.g. persist_login loading
the previous token at the same key (oauth.py:804) — re-enter instead of
self-deadlocking. Adds reentrancy and migration-under-held-lock regression tests.
- Make the off-loop OAuthManager construction cancellation-safe: drain the
  asyncio.to_thread worker on cancellation so _migrate_oauth_storage cannot
  keep writing shared OAuth state (or hold its file lock) after the caller
  unwinds.
- Add a behavior-level test exercising the real _settle_cursor_after_handoff
  against a renderer double (awaits CPR when supported, no-op otherwise, failing
  wait swallowed) rather than only mocking the helper.
@elkaix
elkaix merged commit b5cfe09 into main Jul 19, 2026
38 checks passed
@elkaix
elkaix deleted the consolidated-auth-tui-fix branch July 19, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant