fix: resolve all 17 playbook findings (P0–P3)

P0 fixes:
- agent register: upsert by agent_id (no duplicate rows)
- daemon poll-once: extract _gmail_poll_cycle, run synchronously
- garc_core.py: suppress urllib3/googleapiclient DeprecationWarnings

P1 fixes:
- OAuth: detect RefreshError → delete stale token → re-auth flow
- OAuth: scope coverage check before returning valid creds
- ingress: add stale-reset subcommand (reset in_progress > N min)
- sheets: trim-sheet / clean-all — deleteDimension for empty rows
- approval gate: send Gmail notification to GARC_APPROVAL_EMAIL

P2 additions:
- Google Chat: garc-chat-helper.py + garc send chat subcommands
- Service Account: garc auth service-account verify + DWD support
- Audit log: Sheets audit tab + garc audit list + bin/garc async hook
- garc auth revoke: POST /revoke + delete token file
- kg: pagination fix, shell injection fix, garc-kg-query.py
- docs: _doc_insert_text / append_doc / garc drive append-doc

P3 additions:
- Multi-tenant: lib/profile.sh (list/use/add/show/remove/current)
  bin/garc: auto-load profile config.env and token.json
- Google Forms pipeline: garc-forms-helper.py + lib/forms.sh
  garc forms list/responses/watch
- systemd: _daemon_install_service OS-detect → launchd or systemd units
- Python version gate (>=3.10) in bin/garc + pyproject.toml
- garc doctor command for environment diagnostics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
林 駿甫 (Shunsuke Hayashi) 2026-04-15 09:55:33 +09:00
parent 680bd433f4
commit 7b5951a1d5
21 changed files with 2078 additions and 144 deletions

View file

@ -7,11 +7,19 @@ import json
import os
import sys
import time
import warnings
import functools
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, Any
# Suppress noisy deprecation warnings from urllib3 / requests bundled
# inside google-auth and google-api-python-client on Python 3.12+
warnings.filterwarnings("ignore", message=".*urllib3.*", category=DeprecationWarning)
warnings.filterwarnings("ignore", message=".*ssl.wrap_socket.*", category=DeprecationWarning)
warnings.filterwarnings("ignore", message=".*imp module.*", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning, module="googleapiclient")
GARC_CONFIG_DIR = Path(os.environ.get("GARC_CONFIG_DIR", Path.home() / ".garc"))
TOKEN_FILE = Path(os.environ.get("GARC_TOKEN_FILE", GARC_CONFIG_DIR / "token.json"))
CREDENTIALS_FILE = Path(os.environ.get("GARC_CREDENTIALS_FILE", GARC_CONFIG_DIR / "credentials.json"))
@ -82,21 +90,36 @@ def get_credentials(scopes: Optional[list] = None, use_service_account: bool = F
try:
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from google.auth.exceptions import RefreshError
creds = None
if TOKEN_FILE.exists():
creds = Credentials.from_authorized_user_file(str(TOKEN_FILE), scopes)
if creds and creds.valid:
return creds
# Verify the token covers the requested scopes
if _scopes_covered(creds, scopes):
return creds
# Scope mismatch — need re-auth
print("⚠️ Token scopes insufficient for requested operation.", file=sys.stderr)
print(" Re-authenticating to add required scopes...", file=sys.stderr)
creds = None
if creds and creds.expired and creds.refresh_token:
try:
creds.refresh(Request())
_save_token(creds)
return creds
except RefreshError as e:
# Token revoked or expired beyond refresh — delete and re-auth
print(f"⚠️ Token refresh failed (revoked or expired): {e}", file=sys.stderr)
print(" Deleting stale token. You will be prompted to log in again.", file=sys.stderr)
TOKEN_FILE.unlink(missing_ok=True)
creds = None
except Exception as e:
print(f"⚠️ Token refresh failed: {e}", file=sys.stderr)
print(f"⚠️ Token refresh error: {e}", file=sys.stderr)
TOKEN_FILE.unlink(missing_ok=True)
creds = None
# Need fresh OAuth flow
if not CREDENTIALS_FILE.exists():
@ -117,6 +140,17 @@ def get_credentials(scopes: Optional[list] = None, use_service_account: bool = F
sys.exit(1)
def _scopes_covered(creds, requested_scopes: list) -> bool:
"""Return True if the credential's granted scopes cover all requested scopes."""
if not requested_scopes:
return True
granted = set(getattr(creds, "scopes", None) or [])
if not granted:
# Token file may not carry scope info — assume OK to avoid spurious re-auth
return True
return all(s in granted for s in requested_scopes)
def _save_token(creds):
"""Save credentials to token file."""
GARC_CONFIG_DIR.mkdir(parents=True, exist_ok=True)