hermes - 💡(How to fix) Fix google-workspace: check_auth_live() misreports partial-scope (403) as disabled client; use typed exception split or tokeninfo endpoint [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Follow-up to #19570 / PR #19643. The shipped check_auth_live() in skills/productivity/google-workspace/scripts/setup.py probes the Google API by calling service.calendarList().list(maxResults=1).execute() and catches everything under a single except Exception:

def check_auth_live():
    if not check_auth(quiet=True):
        return False
    try:
        ...
        service = build("calendar", "v3", credentials=creds)
        service.calendarList().list(maxResults=1).execute()
        print("LIVE_CHECK_OK: Real API call succeeded.")
        return True
    except Exception as e:
        err_str = str(e).lower()
        if "disabled_client" in err_str or "invalid_client" in err_str:
            print(f"LIVE_CHECK_FAILED: OAuth client or account disabled: {e}")
            ...
        else:
            print(f"LIVE_CHECK_FAILED: {e}")
        return False

Error Message

def check_auth_live(): if not check_auth(quiet=True): return False try: ... service = build("calendar", "v3", credentials=creds) service.calendarList().list(maxResults=1).execute() print("LIVE_CHECK_OK: Real API call succeeded.") return True except Exception as e: err_str = str(e).lower() if "disabled_client" in err_str or "invalid_client" in err_str: print(f"LIVE_CHECK_FAILED: OAuth client or account disabled: {e}") ... else: print(f"LIVE_CHECK_FAILED: {e}") return False

Root Cause

Follow-up to #19570 / PR #19643. The shipped check_auth_live() in skills/productivity/google-workspace/scripts/setup.py probes the Google API by calling service.calendarList().list(maxResults=1).execute() and catches everything under a single except Exception:

def check_auth_live():
    if not check_auth(quiet=True):
        return False
    try:
        ...
        service = build("calendar", "v3", credentials=creds)
        service.calendarList().list(maxResults=1).execute()
        print("LIVE_CHECK_OK: Real API call succeeded.")
        return True
    except Exception as e:
        err_str = str(e).lower()
        if "disabled_client" in err_str or "invalid_client" in err_str:
            print(f"LIVE_CHECK_FAILED: OAuth client or account disabled: {e}")
            ...
        else:
            print(f"LIVE_CHECK_FAILED: {e}")
        return False

Fix Action

Fixed

Code Example

def check_auth_live():
    if not check_auth(quiet=True):
        return False
    try:
        ...
        service = build("calendar", "v3", credentials=creds)
        service.calendarList().list(maxResults=1).execute()
        print("LIVE_CHECK_OK: Real API call succeeded.")
        return True
    except Exception as e:
        err_str = str(e).lower()
        if "disabled_client" in err_str or "invalid_client" in err_str:
            print(f"LIVE_CHECK_FAILED: OAuth client or account disabled: {e}")
            ...
        else:
            print(f"LIVE_CHECK_FAILED: {e}")
        return False

---

from google.auth.exceptions import RefreshError
from googleapiclient.errors import HttpError

try:
    service.calendarList().list(maxResults=1).execute()
    print("LIVE_CHECK_OK: Real API call succeeded.")
    return True
except RefreshError as e:
    # Token refresh failed during API call — reuse the OAuth error code helper
    ...
except HttpError as e:
    status = getattr(e.resp, "status", None)
    if status == 403 and "scope" in str(e).lower():
        print(f"LIVE_CHECK_PARTIAL: Calendar scope not granted, but token is valid: {e}")
        return True  # Token works; user just didn't grant Calendar
    print(f"LIVE_CHECK_FAILED: HTTP {status}: {e}")
    return False
except Exception as e:
    print(f"LIVE_CHECK_FAILED: {type(e).__name__}: {e}")
    return False
RAW_BUFFERClick to expand / collapse

Context

Follow-up to #19570 / PR #19643. The shipped check_auth_live() in skills/productivity/google-workspace/scripts/setup.py probes the Google API by calling service.calendarList().list(maxResults=1).execute() and catches everything under a single except Exception:

def check_auth_live():
    if not check_auth(quiet=True):
        return False
    try:
        ...
        service = build("calendar", "v3", credentials=creds)
        service.calendarList().list(maxResults=1).execute()
        print("LIVE_CHECK_OK: Real API call succeeded.")
        return True
    except Exception as e:
        err_str = str(e).lower()
        if "disabled_client" in err_str or "invalid_client" in err_str:
            print(f"LIVE_CHECK_FAILED: OAuth client or account disabled: {e}")
            ...
        else:
            print(f"LIVE_CHECK_FAILED: {e}")
        return False

Problem

setup.py is intentionally permissive about partial scope grants — see the long comment at check_auth() lines 144–147 and the _missing_scopes_from_payload helper. A user who unchecks Calendar at the OAuth consent screen has a token that:

  • Passes check_auth() (valid, refreshable, we don't reject missing scopes)
  • Fails calendarList().list() with 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT

The current live-probe path falls into the generic LIVE_CHECK_FAILED: <HttpError ...> arm and returns False. The agent (or the user) sees a failure on what is actually a working token — just scoped narrower than Calendar.

This is the one failure mode where check_auth_live is supposed to help distinguish real account issues from local-only validation gaps, so conflating it with scope problems makes the new flag actively less useful than the stale --check in that case.

Proposed fix

Two options, not mutually exclusive:

Option 1: Split exception types (minimal)

from google.auth.exceptions import RefreshError
from googleapiclient.errors import HttpError

try:
    service.calendarList().list(maxResults=1).execute()
    print("LIVE_CHECK_OK: Real API call succeeded.")
    return True
except RefreshError as e:
    # Token refresh failed during API call — reuse the OAuth error code helper
    ...
except HttpError as e:
    status = getattr(e.resp, "status", None)
    if status == 403 and "scope" in str(e).lower():
        print(f"LIVE_CHECK_PARTIAL: Calendar scope not granted, but token is valid: {e}")
        return True  # Token works; user just didn't grant Calendar
    print(f"LIVE_CHECK_FAILED: HTTP {status}: {e}")
    return False
except Exception as e:
    print(f"LIVE_CHECK_FAILED: {type(e).__name__}: {e}")
    return False

Option 2: Swap the probe to tokeninfo (cleanest)

`https://oauth2.googleapis.com/tokeninfo?access_token=*** is Google's purpose-built endpoint for exactly this validation. Properties:

  • Free and unmetered (not billed against any API quota).
  • Requires zero specific scopes.
  • Returns 200 with token details if the access token is live and the OAuth client is enabled.
  • Returns 400 with {"error": "invalid_token"} for disabled-client, revoked-token, or account-disabled cases.
  • Doesn't leave an entry in Calendar audit logs.

This avoids the Calendar-scope coupling entirely and is the answer the PR was trying to get at with its Calendar probe.

Acceptance criteria

  • check_auth_live() no longer reports LIVE_CHECK_FAILED for users with valid tokens that simply lack the Calendar scope.
  • Test in tests/skills/test_google_oauth_setup.py that injects an HttpError with status 403 and "insufficientPermissions" / "ACCESS_TOKEN_SCOPE_INSUFFICIENT" body, asserts the code does NOT treat this as a disabled client and does NOT exit 1 from that failure mode.

References

Related

See also #21860 (unstructured RefreshError classification) and #21862 (missing tests). Worth bundling into a single PR.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING