{"repo":"NousResearch/hermes-agent","public_id":"19570","display_label":"Google Workspace OAuth disabled_client detection and live checks","summary":"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.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":21826,"merged_at":"2026-05-08T11:50:43+00:00"},"resolution":{"state":"resolved","by":"pr-21826"},"freshness":{"state":"verified","last_verified_at":"2026-05-08T11:50:43+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":19570,"pr_number":21826,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-08T11:50:43+00:00"}],"members":[{"kind":"issue","number":19570,"title":"google-workspace: fresh Google account disabled shortly after official OAuth setup; setup.py --check still reports AUTHENTICATED while API call fails disabled_client","url":null,"state":"closed"},{"kind":"pr","number":19643,"title":"fix(google-workspace): detect disabled_client in --check and add --check-live","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#21826","number":21826}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":21826,"title":"fix(google-workspace): detect disabled_client in --check and add --check-live (salvage of #19643)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/release.py","skills/productivity/google-workspace/scripts/setup.py"],"fix_hunks":[{"file":"scripts/release.py","hunks":["@@ -47,6 +47,7 @@\n     \"[redacted:email]\": \"qiyin-code\",\n     \"[redacted:email]\": \"olisikh\",\n     \"[redacted:email]\": \"leoneparise\",\n+    \"[redacted:email]\": \"ygd58\",\n     \"[redacted:email]\": \"teknium1\",\n     \"[redacted:email]\": \"thelumiereguy\",\n     \"[redacted:email]\": \"counterposition\","]},{"file":"skills/productivity/google-workspace/scripts/setup.py","hunks":["@@ -130,7 +130,33 @@ def _ensure_deps():\n             sys.exit(1)\n \n \n-def check_auth():\n+def check_auth_live():\n+    \"\"\"Check auth with a real API call to detect disabled_client/account issues.\"\"\"\n+    # quiet=True suppresses the \"AUTHENTICATED\" print from check_auth so the\n+    # final status line reflects the live-call outcome (OK or FAILED).\n+    if not check_auth(quiet=True):\n+        return False\n+    try:\n+        from googleapiclient.discovery import build\n+        from google.oauth2.credentials import Credentials\n+        creds = Credentials.from_authorized_user_file(str(TOKEN_PATH))\n+        service = build(\"calendar\", \"v3\", credentials=[redacted:secret-assignment]\n+        service.calendarList().list(maxResults=1).execute()\n+        print(\"LIVE_CHECK_OK: Real API call succeeded.\")\n+        return True\n+    except Exception as e:\n+        err_str = str(e).lower()\n+        if \"disabled_client\" in err_str or \"invalid_client\" in err_str:\n+            print(f\"LIVE_CHECK_FAILED: OAuth client or account disabled: {e}\")\n+            print(\"  1. Check Google Cloud Console for disabled OAuth client\")\n+            print(\"  2. Check myaccount.google.com for account status\")\n+            print(\"  3. Do NOT retry with a disabled account\")\n+        else:\n+            print(f\"LIVE_CHECK_FAILED: {e}\")\n+        return False\n+\n+\n+def check_auth(quiet: bool = False):\n     \"\"\"Check if stored credentials are valid. Prints status, exits 0 or 1.\"\"\"\n     if not TOKEN_PATH.exists():\n         print(f\"NOT_AUTHENTICATED: No token at {TOKEN_PATH}\")","@@ -157,7 +183,8 @@ def check_auth():\n             print(f\"AUTHENTICATED (partial): Token valid but missing {len(missing_scopes)} scopes:\")\n             for s in missing_scopes:\n                 print(f\"  - {s}\")\n-        print(f\"AUTHENTICATED: Token valid at {TOKEN_PATH}\")\n+        if not quiet:\n+            print(f\"AUTHENTICATED: Token valid at {TOKEN_PATH}\")\n         return True\n \n     if creds.expired and creds.refresh_token:","@@ -174,10 +201,25 @@ def check_auth():\n                 print(f\"AUTHENTICATED (partial): Token refreshed but missing {len(missing_scopes)} scopes:\")\n                 for s in missing_scopes:\n                     print(f\"  - {s}\")\n-            print(f\"AUTHENTICATED: Token refreshed at {TOKEN_PATH}\")\n+            if not quiet:\n+                print(f\"AUTHENTICATED: Token refreshed at {TOKEN_PATH}\")\n             return True\n         except Exception as e:\n-            print(f\"REFRESH_FAILED: {e}\")\n+            err_str = str(e).lower()\n+            if \"disabled_client\" in err_str or \"invalid_client\" in err_str:\n+                print(f\"OAUTH_CLIENT_DISABLED: {e}\")\n+                print(\"  The OAuth client or Google account has been disabled.\")\n+                print(\"  Steps to resolve:\")\n+                print(\"    1. Check your Google Cloud Console — verify the OAuth client is not disabled\")\n+                print(\"    2. Check if your Google account itself has been disabled at myaccount.google.com\")\n+                print(\"    3. If the account is disabled, you can appeal at accounts.google.com/signin/recovery\")\n+                print(\"    4. Do NOT retry API calls with a disabled account — this may worsen the situation\")\n+                print(\"    5. If the OAuth client is disabled, create a new one in Google Cloud Console\")\n+            elif \"token_revoked\" in err_str or \"invalid_grant\" in err_str:\n+                print(f\"TOKEN_REVOKED: {e}\")\n+                print(\"  Re-run setup to re-authenticate.\")\n+            else:\n+                print(f\"REFRESH_FAILED: {e}\")\n             return False\n \n     print(\"TOKEN_INVALID: [redacted:secret-assignment] setup.\")","@@ -384,6 +426,7 @@ def main():\n     parser = argparse.ArgumentParser(description=\"Google Workspace OAuth setup for Hermes\")\n     group = parser.add_mutually_exclusive_group(required=True)\n     group.add_argument(\"--check\", action=\"store_true\", help=\"Check if auth is valid (exit 0=yes, 1=no)\")\n+    group.add_argument(\"--check-live\", action=\"store_true\", help=\"Check auth with a real API call (detects disabled_client)\")\n     group.add_argument(\"--client-secret\", metavar=\"PATH\", help=\"Store OAuth client_secret.json\")\n     group.add_argument(\"--auth-url\", action=\"store_true\", help=\"Print OAuth URL for user to visit\")\n     group.add_argument(\"--auth-code\", metavar=\"CODE\", help=\"Exchange auth code for token\")","@@ -393,6 +436,8 @@ def main():\n \n     if args.check:\n         sys.exit(0 if check_auth() else 1)\n+    if getattr(args, \"check_live\", False):\n+        sys.exit(0 if check_auth_live() else 1)\n     elif args.client_secret:\n         [redacted:secret-assignment]\n     elif args.auth_url:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}