#!/usr/bin/env python3 """connect_next.py — deterministic state-machine for the Google connect flow. The agent runs this and RELAYS `message` (Vietnamese) to the user verbatim — it must NOT improvise console steps, explore the filesystem, or pick the python interpreter. This wraps the builtin `setup.py` and always calls it with the venv python (has google deps). python3 lib/connect_next.py -> {stage, message} (the next step to say) python3 lib/connect_next.py --secret -> load OAuth client secret, advance python3 lib/connect_next.py --code "" -> exchange auth code, advance Stages: connected | need_oauth_client | need_consent | error """ import os import re import sys import json import subprocess HOME = os.path.expanduser("~") SETUP = os.path.join(HOME, ".hermes", "skills", "productivity", "google-workspace", "scripts", "setup.py") CLIENT_SECRET = os.path.join(HOME, ".hermes", "google_client_secret.json") PENDING = os.path.join(HOME, ".hermes", "google_oauth_pending.json") LAST_URL = os.path.join(HOME, ".hermes", "google_oauth_last_url.txt") def _venv_python(): p = "/usr/local/lib/hermes-agent/venv/bin/python" if os.path.exists(p): return p try: d = subprocess.run(["bash", "-lc", "dirname $(readlink -f $(command -v hermes))"], capture_output=True, text=True, timeout=10).stdout.strip() cand = os.path.join(d, "python") if os.path.exists(cand): return cand except Exception: pass return "python3" VENV = _venv_python() def _setup(*args, timeout=60): try: p = subprocess.run([VENV, SETUP, *args], capture_output=True, text=True, timeout=timeout) return p.returncode, ((p.stdout or "") + (p.stderr or "")) except Exception as e: return 1, str(e) def _authenticated(): _, out = _setup("--check", timeout=30) return ("NOT_AUTHENTICATED" not in out) and ("AUTHENTICATED" in out) def _find_url(text): m = re.search(r"https://accounts\.google\.com/o/oauth2/\S+", text) return m.group(0) if m else None GUIDE_OAUTH = ( "Để em đọc được Gmail/Drive của anh/chị, mình cần tạo một 'chìa khoá' Google — làm 1 lần, ~5 phút. " "Anh/chị làm lần lượt 4 bước (mỗi bước có link), xong **gửi em file/khoá ở bước 4**:\n\n" "1) Tạo dự án: https://console.cloud.google.com/projectcreate — đặt tên (vd CEO OS) → Create → chọn dự án vừa tạo.\n" "2) Bật API (1 link bật cả 6 cùng lúc — chỉ bấm Enable): " "https://console.cloud.google.com/flows/enableapi?apiid=gmail.googleapis.com,calendar-json.googleapis.com,drive.googleapis.com,sheets.googleapis.com,docs.googleapis.com,people.googleapis.com\n" "3) Màn hình đồng ý: https://console.cloud.google.com/auth/overview — Get started → tên app + email → Audience: External → tạo. " "Rồi vào https://console.cloud.google.com/auth/audience — Test users → Add → email của anh/chị.\n" "4) Tạo khoá: https://console.cloud.google.com/auth/clients — Create client → Application type: **Desktop app** → Create → " "tải **file JSON** (hoặc copy Client ID + Client Secret) rồi **gửi cho em**.\n\n" "Em chờ anh/chị gửi file/khoá ở bước 4 nhé." ) def _consent_message(url): return ("Gần xong! Anh/chị mở link này, đăng nhập:\n" + url + "\n\nRồi: nếu hiện 'chưa xác minh' → **Nâng cao → Tiếp tục**; tới màn hình chọn quyền thì " "**TICK ô 'Select all' (chọn TẤT CẢ quyền)** → **Continue/Cho phép**. " "⚠️ Không tick Select all thì em sẽ không đọc được gì. " "Trang cuối hiện **localhost:1** báo lỗi không tải được là **ĐÚNG** — anh/chị copy **TOÀN BỘ đường link** " "trên thanh địa chỉ rồi **dán lại cho em**.") def state(): if _authenticated(): return {"stage": "connected", "message": "Đã nối Google rồi ✅. Anh/chị thử: 'đọc 3 email gần nhất' nhé."} if not os.path.isfile(CLIENT_SECRET): return {"stage": "need_oauth_client", "message": GUIDE_OAUTH} # have secret, not connected → reuse pending consent URL if present, else generate once if os.path.isfile(PENDING) and os.path.isfile(LAST_URL): url = open(LAST_URL, encoding="utf-8").read().strip() if url: return {"stage": "need_consent", "url": url, "message": _consent_message(url)} _, out = _setup("--auth-url", timeout=60) url = (open(LAST_URL, encoding="utf-8").read().strip() if os.path.isfile(LAST_URL) else None) or _find_url(out) if url: return {"stage": "need_consent", "url": url, "message": _consent_message(url)} return {"stage": "error", "message": "Có trục trặc khi tạo link đồng ý. Anh/chị gửi lại em **file khoá** (Client OAuth JSON) để thử lại nhé.", "debug": out[:300]} def load_secret(arg): path = arg if not os.path.isfile(arg): s = (arg or "").strip() if s.startswith("{"): path = "/tmp/_gclient_secret.json" open(path, "w", encoding="utf-8").write(s) else: return {"stage": "need_oauth_client", "message": "Em chưa nhận được file khoá hợp lệ. Anh/chị gửi lại **file JSON** (hoặc dán nội dung JSON) từ bước 4 nhé."} rc, out = _setup("--client-secret", path, timeout=30) if rc != 0 and "OK" not in out and "saved" not in out: return {"stage": "error", "message": "File khoá chưa đúng. Anh/chị tải lại đúng **Client OAuth (Desktop app)** dạng JSON rồi gửi em nhé.", "debug": out[:200]} return state() # advances → consent URL def exchange(code): _, out = _setup("--auth-code", code, timeout=60) if "fresh_auth_url" in out: url = _find_url(out) or (open(LAST_URL, encoding="utf-8").read().strip() if os.path.isfile(LAST_URL) else None) if url: return {"stage": "need_consent", "url": url, "message": "Mã vừa rồi hết hạn. Anh/chị mở **LẠI** link này, Cho phép, rồi dán lại đường link localhost **ngay** nhé:\n" + url} if _authenticated(): return {"stage": "connected", "message": "Đã nối Google thành công ✅! Anh/chị thử ngay: 'đọc 3 email gần nhất' nhé."} return {"stage": "error", "message": "Chưa đổi được mã. Anh/chị dán lại **đường link localhost mới nhất** giúp em nhé.", "debug": out[:200]} def main(): a = sys.argv[1:] if a and a[0] == "--secret": out = load_secret(a[1] if len(a) > 1 else "") elif a and a[0] == "--code": out = exchange(a[1] if len(a) > 1 else "") else: out = state() print(json.dumps(out, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": sys.exit(main())