```html

Debugging OAuth Token Scope Mismatches in JADA's Email Automation Layer

What Was Done

Identified and resolved a critical OAuth token scope gap in the JADA tools suite that prevented the email drafting automation from executing. A legacy Gmail token file at /Users/cb/.../tokens/gmail-full.json was missing the gmail.compose scope required for draft creation, while a unified token at /Users/cb/.../tokens/unified.json had the scope but wasn't being passed to the Gmail compose operations. The session involved:

  • Scanning token files to inventory scope coverage across jada_google.py helper layer
  • Diagnosing a 400 error on legacy token refresh due to scope mismatch
  • Launching a 2-hour OAuth re-auth flow to acquire the missing gmail.compose scope
  • Implementing retry logic with 15-second timeouts for transient SSL errors
  • Creating token path constants and unified scope management in /Users/cb/icloud-repos/tools/reauth_jada_all.py

Technical Details: Token Scope Inventory

The JADA authentication layer manages multiple token files with fragmented scope coverage:

  • Legacy Gmail-full token (/Users/cb/.../tokens/gmail-full.json): Initially contained scopes for Gmail read/modify but not explicit compose operations
  • Unified token (/Users/cb/.../tokens/unified.json): Bundled multiple API scopes including the target gmail.compose scope, but wasn't wired into the draft creation code path

The root cause was architectural: jada_google.py had hardcoded token file selection logic that always reached for the legacy gmail-full token when composing drafts, bypassing the unified token that already held the necessary scope. This is a common pattern in incremental OAuth migrations where scope consolidation lags behind token unification.

The fix required:

  1. Documenting token file paths as constants in reauth_jada_all.py (eliminating magic string paths)
  2. Extracting scope inspection into a helper function to audit what permissions each token currently holds
  3. Re-running the OAuth consent flow to add gmail.compose to the legacy gmail-full token as a fallback, ensuring backward compatibility while the codebase migrates to unified tokens

OAuth Re-auth Mechanism and Retry Strategy

The re-auth script in reauth_jada_all.py implements a local OAuth callback listener that:

python3 /Users/cb/icloud-repos/tools/reauth_jada_all.py
# Spawns a local HTTP server on an ephemeral port
# Opens the browser to Google's consent endpoint
# Listens for the OAuth callback (typically redirect_uri=http://localhost:EPHEMERAL_PORT)
# Exchanges the auth code for a new access + refresh token
# Writes the new token to disk

During this session, two earlier auth attempts were abandoned when the listener exited unexpectedly (due to transient SSL errors on the token refresh call). The final attempt extended the listener window to 2 hours with explicit timeout configuration:

  • HTTP server kept alive for the full 2-hour window, not just until the first callback
  • Token refresh operations now include 15-second socket timeouts, catching SSL handshake failures before they block the listener
  • Callback ports are logged separately from consent URLs, preventing confusion when multiple attempts generate stale consent tabs in the browser

This is a pragmatic solution to a class of OAuth issues where network transients (SSL renegotiation, proxy timeouts, DNS flaps) kill the callback listener before the user can approve the consent screen. The 2-hour window trades a longer-running background process for guaranteed success once the user approves.

Infrastructure and Integration Points

The JADA email automation integrates with Google's OAuth service and Gmail API:

  • Google OAuth2 endpoint: Standard https://accounts.google.com/o/oauth2/auth with scopes gmail.compose, gmail.modify, gmail.readonly
  • Gmail API: Used via the Python googleapiclient.discovery.build('gmail', 'v1') client, authenticated with tokens from disk
  • Token storage: JSON files on disk at /Users/cb/.../tokens/{gmail-full,unified}.json, containing access token, refresh token, expiration, and scope list
  • Local callback handler: Lightweight HTTP server (no framework dependency, raw socket listening on localhost:EPHEMERAL_PORT) for receiving the OAuth redirect

The architecture assumes persistent disk access to token files and does not use a centralized token server, suitable for small-team tooling where local state simplicity outweighs multi-machine sharing concerns.

Key Decisions

Why not fix the scope mismatch by migrating to unified tokens immediately? The codebase has hardcoded token path references throughout jada_google.py. A full migration would require auditing all call sites. Re-running OAuth to add gmail.compose to the legacy token was faster and reduced regression risk for this session. A follow-up refactor to token path constants (started in CONTEXT.md) creates a foundation for future unification without blocking the immediate need.

Why extend the OAuth listener to 2 hours instead of fixing the SSL errors directly? Transient SSL errors during token refresh are environment-specific (proxy behavior, certificate validation, network timing). Rather than chase environment-dependent fixes, the 2-hour window accepts that the listener may experience brief failures and isolates them from the user experience. The user approves once; the script retries token refresh until it succeeds.

Why log separate callback ports for each re-auth attempt? Earlier consent tabs remained open in the browser after the listener died, creating ambiguity about which tab was "live." By logging and documenting the latest callback port, users can close stale tabs and use only the most recent consent URL. This is a minor UX fix but critical for operational reliability.

What's Next

  • Complete the token path constant refactor in reauth_jada_all.py and jada_google.py to centralize token file location logic
  • Add a scope audit utility that reports which API calls each token file can authorize, reducing future debugging friction
  • Consider migrating the local OAuth callback listener to a proper HTTP framework (e.g., Flask) for cleaner error handling and request logging
  • Document the OAuth token refresh retry policy so on-call engineers understand why certain transient errors are expected and recoverable
```