NousResearch/hermes-agent

Google Workspace OAuth disabled_client detection and live checks

Cluster 19570. Data as of

Nothing open on GitHub. Fix merged in #21826 on 8 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #21826. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #21826. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and PRs address how the Google Workspace integration detects and reports disabled or misconfigured OAuth clients, including hardening the --check flow, adding a live-check mode, and correcting misclassification of partial-scope 403 errors as disabled-client errors.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #21826 fix(google-workspace): detect disabled_client in --check and add --check-live (salvage of #19643)

    merged to main

    files: scripts/release.py, skills/productivity/google-workspace/scripts/setup.py

    Show the diff
    • scripts/release.py
      @@ -47,6 +47,7 @@
           "[redacted:email]": "qiyin-code",
           "[redacted:email]": "olisikh",
           "[redacted:email]": "leoneparise",
      +    "[redacted:email]": "ygd58",
           "[redacted:email]": "teknium1",
           "[redacted:email]": "thelumiereguy",
           "[redacted:email]": "counterposition",
    • skills/productivity/google-workspace/scripts/setup.py
      @@ -130,7 +130,33 @@ def _ensure_deps():
                   sys.exit(1)
       
       
      -def check_auth():
      +def check_auth_live():
      +    """Check auth with a real API call to detect disabled_client/account issues."""
      +    # quiet=True suppresses the "AUTHENTICATED" print from check_auth so the
      +    # final status line reflects the live-call outcome (OK or FAILED).
      +    if not check_auth(quiet=True):
      +        return False
      +    try:
      +        from googleapiclient.discovery import build
      +        from google.oauth2.credentials import Credentials
      +        creds = Credentials.from_authorized_user_file(str(TOKEN_PATH))
      +        service = build("calendar", "v3", credentials=[redacted:secret-assignment]
      +        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}")
      +            print("  1. Check Google Cloud Console for disabled OAuth client")
      +            print("  2. Check myaccount.google.com for account status")
      +            print("  3. Do NOT retry with a disabled account")
      +        else:
      +            print(f"LIVE_CHECK_FAILED: {e}")
      +        return False
      +
      +
      +def check_auth(quiet: bool = False):
           """Check if stored credentials are valid. Prints status, exits 0 or 1."""
           if not TOKEN_PATH.exists():
               print(f"NOT_AUTHENTICATED: No token at {TOKEN_PATH}")
      @@ -157,7 +183,8 @@ def check_auth():
                   print(f"AUTHENTICATED (partial): Token valid but missing {len(missing_scopes)} scopes:")
                   for s in missing_scopes:
                       print(f"  - {s}")
      -        print(f"AUTHENTICATED: Token valid at {TOKEN_PATH}")
      +        if not quiet:
      +            print(f"AUTHENTICATED: Token valid at {TOKEN_PATH}")
               return True
       
           if creds.expired and creds.refresh_token:
      @@ -174,10 +201,25 @@ def check_auth():
                       print(f"AUTHENTICATED (partial): Token refreshed but missing {len(missing_scopes)} scopes:")
                       for s in missing_scopes:
                           print(f"  - {s}")
      -            print(f"AUTHENTICATED: Token refreshed at {TOKEN_PATH}")
      +            if not quiet:
      +                print(f"AUTHENTICATED: Token refreshed at {TOKEN_PATH}")
                   return True
               except Exception as e:
      -            print(f"REFRESH_FAILED: {e}")
      +            err_str = str(e).lower()
      +            if "disabled_client" in err_str or "invalid_client" in err_str:
      +                print(f"OAUTH_CLIENT_DISABLED: {e}")
      +                print("  The OAuth client or Google account has been disabled.")
      +                print("  Steps to resolve:")
      +                print("    1. Check your Google Cloud Console — verify the OAuth client is not disabled")
      +                print("    2. Check if your Google account itself has been disabled at myaccount.google.com")
      +                print("    3. If the account is disabled, you can appeal at accounts.google.com/signin/recovery")
      +                print("    4. Do NOT retry API calls with a disabled account — this may worsen the situation")
      +                print("    5. If the OAuth client is disabled, create a new one in Google Cloud Console")
      +            elif "token_revoked" in err_str or "invalid_grant" in err_str:
      +                print(f"TOKEN_REVOKED: {e}")
      +                print("  Re-run setup to re-authenticate.")
      +            else:
      +                print(f"REFRESH_FAILED: {e}")
                   return False
       
           print("TOKEN_INVALID: [redacted:secret-assignment] setup.")
      @@ -384,6 +426,7 @@ def main():
           parser = argparse.ArgumentParser(description="Google Workspace OAuth setup for Hermes")
           group = parser.add_mutually_exclusive_group(required=True)
           group.add_argument("--check", action="store_true", help="Check if auth is valid (exit 0=yes, 1=no)")
      +    group.add_argument("--check-live", action="store_true", help="Check auth with a real API call (detects disabled_client)")
           group.add_argument("--client-secret", metavar="PATH", help="Store OAuth client_secret.json")
           group.add_argument("--auth-url", action="store_true", help="Print OAuth URL for user to visit")
           group.add_argument("--auth-code", metavar="CODE", help="Exchange auth code for token")
      @@ -393,6 +436,8 @@ def main():
       
           if args.check:
               sys.exit(0 if check_auth() else 1)
      +    if getattr(args, "check_live", False):
      +        sys.exit(0 if check_auth_live() else 1)
           elif args.client_secret:
               [redacted:secret-assignment]
           elif args.auth_url:

1 PR closed without merging

From the close comments: merged via another change 1

  • #19643 fix(google-workspace): detect disabled_client in --check and add --check-live · merged via another change → #21826
Closed issues · 1
  • Closed#19570google-workspace: fresh Google account disabled shortly after official OAuth setup; setup.py --check still reports AUTHENTICATED while API call fails disabled_client
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#21826
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
8 May 2026

For your agent

The same facts, typed.