app
Adom Google
Public Made by Adomby adom
Drive your whole Google Workspace from the terminal, run by your AI. Read Gmail, write Google Docs, build Google Sheets and Google Slides, search Google Drive, manage Google Calendar and Google Tasks, and post to Google Chat, plus any Google API and a zero-setup personal Gmail.
← Commit history
v0.8.26: org register (set up once for the whole org)
3 files changed
+167−15
.adomignore+1@@ -1,3 +1,4 @@ # Dev/maintainer-only skills: kept in the source repo, NEVER shipped in the public # install tarball (they carry internal setup notes and customer names). skills/adom-google-connector-oauth-dev/+gateway/
bin/adom-google+162−12@@ -23,7 +23,7 @@ See `adom-google help`. """ import sys, os, json, base64, argparse, urllib.parse, ssl, socket, hashlib, secrets, struct, time, subprocess, shutil -ADOM_GOOGLE_VERSION = "0.8.25"+ADOM_GOOGLE_VERSION = "0.8.26" if sys.argv[1:2] == ["--version"]: print(f"adom-google {ADOM_GOOGLE_VERSION}") sys.exit(0)@@ -238,10 +238,12 @@ Hint: 3. Configure the OAuth consent screen (User type = Internal; app name fr Hint: 4. Create an OAuth client (type = Web application); AUTO-FILL Authorized redirect URI Hint: with the gateway `redirect_uri` (the error-prone field — never make the user type it). Hint: 5. Capture the client_id + client_secret from the final dialog.-Hint: 6. Save them: adom-google onboard finish --org <slug> --client-id <id> --client-secret <secret>+Hint: 6. Register for the whole org: printf '%s\n' "$SECRET" | adom-google org register --org <org> --client-id <id> Hint: 7. Authorize (full Workspace by default, no mode quiz): adom-google auth-Hint: The secret stays on THE USER'S container (0600) and rides Adom's shared callback in RELAY-Hint: mode — Adom never holds it."""+Hint: `org register` hands the client to Adom's gateway ONCE for the whole org: it verifies the caller+Hint: is an org member (wiki login), checks the client with Google, holds the secret (broker mode), and+Hint: writes the non-secret half to <org>/org-settings so teammates need no setup. `onboard finish+Hint: --relay` keeps the old single-box mode (secret 0600 on this box only; teammates cannot use it).""" TOKEN_URL = "https://oauth2.googleapis.com/token" AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"@@ -1667,8 +1669,8 @@ def cmd_onboard(a): print("┌─ adom-google · onboard your org to Google Workspace ─────────────────────────┐") print("│ One-time setup: your team gets its OWN Google OAuth client so the AI can │") print("│ read mail, post Chat on your behalf, build Slides/Sheets/Docs, search Drive. │")- print("│ Your client SECRET stays on YOUR machine (0600). Adom only lends its public │")- print("│ callback URL — it never sees your secret. │")+ print("│ Done ONCE by one person: Adom's gateway verifies you are in the org, checks │")+ print("│ the client with Google and holds the secret, so every member just signs in. │") print("└──────────────────────────────────────────────────────────────────────────────┘\n") print("The AI can drive your real browser through every step below and auto-fill the") print("fields (recommended) — or you can do it by hand. Either way, the exact values:\n")@@ -1685,17 +1687,153 @@ def cmd_onboard(a): print(" " + cfg["console"]["create_client"]) print(" ⚠ Authorized redirect URI — paste EXACTLY (this is the gateway callback):") print(" " + R)- print("\n5) Copy the Client ID + Client secret it shows, then run:")- print(" adom-google onboard finish --org <your-org-slug> \\")- print(" --client-id <client-id> --client-secret <client-secret>")+ print("\n5) Copy the Client ID + Client secret it shows, then register it for the WHOLE org")+ print(" (secret on stdin, so it stays out of shell history):")+ print(" printf '%s\\n' \"$SECRET\" | adom-google org register --org <your-wiki-org> --client-id <client-id>")+ print(" Teammates then just run `adom-google auth`: the non-secret half is on <org>/org-settings.") print("\n6) Authorize (full Workspace access by default):") print(" adom-google auth # one Allow click with your Workspace account (you have ~60 min)") print("\n(Chat also needs a one-time Chat-app config: " + cfg["console"]["chat_config"] + ")") print("\n" + ONBOARD_HINT) +def _wiki_token():+ """The caller's wiki token, resolved the same way adom-wiki does: env, config, container key.+ Sent ONLY to the OAuth gateway, which uses it once to ask the wiki which orgs you belong to."""+ for k in ("ADOM_WIKI_TOKEN", "ADOMPKG_TOKEN"):+ if os.environ.get(k):+ return os.environ[k].strip()+ for path in (os.path.expanduser("~/.config/adom-wiki/config.json"),):+ try:+ t = (json.load(open(path)) or {}).get("token")+ if t:+ return t.strip()+ except Exception:+ pass+ try:+ return open("/var/run/adom/api-key").read().strip() or None+ except Exception:+ return None+++def _gateway_org_call(method, path, gw, body=None, params=None):+ tok = _wiki_token()+ if not tok:+ sys.exit("ERROR: no wiki token found (adom-wiki is not signed in on this box).\n"+ "Hint: check with `adom-wiki whoami`; the gateway uses your wiki login to confirm org membership.")+ try:+ r = requests.request(method, gw.rstrip("/") + path, json=body, params=params, timeout=60,+ headers={"Authorization": "Bearer " + tok})+ except requests.RequestException as e:+ sys.exit(f"ERROR: could not reach the OAuth gateway at {gw}: {e}")+ try:+ return r.status_code, r.json()+ except ValueError:+ return r.status_code, {"error": f"http_{r.status_code}", "detail": r.text[:200]}+++def _org_settings_record(org, client_id, gw, domain=None):+ """The NON-secret half, for <org>/org-settings integrations/google-oauth.json."""+ rec = {"schema": "adom.org-integration/1", "integration": ORG_INTEGRATION, "owner": org,+ "kind": "oauth-gateway-provider", "secret": False, "app": f"org-{org}",+ "client_id": client_id, "gateway_url": gw.rstrip("/"), "mode": "broker",+ "set_up_by": (_wiki_json(["whoami"]) or {}).get("data", {}).get("user", {}).get("username"),+ "note": ("Google OAuth client for adom-google. Public client_id only: the client secret is "+ "held by the OAuth gateway, which does the token exchange, so no member's box ever "+ "holds it. Members just run `adom-google auth`."),+ "updated": time.strftime("%Y-%m-%d")}+ if domain:+ rec["workspace_domains"] = [d.strip() for d in domain.split(",") if d.strip()]+ return rec+++def cmd_org_register(a):+ """Register THIS org's own Google OAuth client with the gateway, once, for every member."""+ org = a.org.strip().lower()+ gw = (a.gateway or ADOM_SHARED_GATEWAY).rstrip("/")+ secret = a.client_secret+ if not secret or secret == "-":+ secret = sys.stdin.readline().strip() # keeps it out of shell history / ps+ print(f"1) Registering {org}'s Google OAuth client with the gateway ({gw}) ...")+ print(" The gateway checks: you are a member of this org (your wiki login), the client id +")+ print(" secret are real (asks Google), and its /callback is an authorized redirect URI.")+ code, d = _gateway_org_call("POST", "/apps/register", gw,+ body={"org": org, "client_id": a.client_id, "client_secret": secret})+ secret = None+ if code != 200 or not d.get("ok"):+ msg = d.get("hint") or d.get("detail") or ""+ sys.exit(f"ERROR: gateway refused the registration: {d.get('error')} {('(' + d['google'] + ')') if d.get('google') else ''}\n"+ + (f"Hint: {msg}\n" if msg else "")+ + ("Hint: redirect URI to add on the OAuth client: " + d["redirect_uri"] + "\n" if d.get("redirect_uri") else ""))+ print(f" OK: {d['app']} registered by {d['checks']['membership']}; client valid, redirect registered"+ + (" (replaced the previous client)" if d.get("replaced") else ""))+ print(f"2) Publishing the non-secret half to {org}/org-settings so every member picks it up ...")+ rec = _org_settings_record(org, a.client_id, gw, a.domain)+ tmp = os.path.join(CFG_DIR, f".org-settings-{org}.json")+ os.makedirs(CFG_DIR, exist_ok=True)+ with open(tmp, "w") as f:+ json.dump(rec, f, indent=2)+ r = subprocess.run(["adom-wiki", "settings", "set", org, ORG_INTEGRATION, "--file", tmp, "-m",+ f"google-oauth: {org}'s adom-google provider (secret held by the gateway)"],+ capture_output=True, text=True, timeout=60)+ os.unlink(tmp)+ if r.returncode != 0 or "SETTINGS_WRITTEN" not in (r.stdout + r.stderr):+ out = (r.stdout + r.stderr).strip().splitlines()+ print(" WARN: could not write org settings; members would not auto-discover yet:", file=sys.stderr)+ print(" " + "\n ".join(out[-4:]), file=sys.stderr)+ print(f" Hint: if {org}/org-settings does not exist yet, the error above prints the one-time create command.", file=sys.stderr)+ else:+ print(f" OK: {org}/org-settings integrations/{ORG_INTEGRATION}.json written (members only, no secret)")+ print("3) Wiring this box ...")+ global _PROVIDER+ _PROVIDER = None+ orgs, found = _discover_org_providers(only_org=org)+ if found:+ _write_org_provider(*found[0])+ else:+ _write_org_provider(org, rec)+ print(f" OK: provider → {PROVIDER_PATH} (0600, broker mode, no secret here)")+ print(f"\n✅ {org} is set up. You and every other member of {org}: `adom-google auth` (one Allow click).")+++def cmd_org_status(a):+ org = a.org.strip().lower()+ gw = (a.gateway or ADOM_SHARED_GATEWAY).rstrip("/")+ code, d = _gateway_org_call("GET", "/apps/status", gw, params={"org": org})+ st = _wiki_json(["settings", "get", org, ORG_INTEGRATION]) or {}+ d["org_settings"] = "present" if st.get("status") == "ok" else ((st.get("hints") or [{}])[0].get("code") or "unreadable")+ if a.json:+ print(json.dumps(d)); return+ if code != 200:+ sys.exit(f"ERROR: {d.get('error')}: {d.get('hint') or d.get('detail') or ''}")+ print(f"org: {org} (gateway app {d.get('app')})")+ print(f"gateway: {'registered by ' + str(d.get('registered_by')) + ' at ' + str(d.get('registered_at')) if d.get('registered') else 'NOT registered'}")+ print(f"client_id: {d.get('client_id') or '-'}")+ print(f"org settings: {d['org_settings']}")+ print(f"redirect_uri: {d.get('redirect_uri')}")+++def cmd_org_unregister(a):+ org = a.org.strip().lower()+ gw = (a.gateway or ADOM_SHARED_GATEWAY).rstrip("/")+ code, d = _gateway_org_call("POST", "/apps/unregister", gw, body={"org": org})+ if code != 200:+ sys.exit(f"ERROR: {d.get('error')}: {d.get('hint') or d.get('detail') or ''}")+ print(f"OK: gateway entry for {org} {'removed' if d.get('removed') else 'was not present'}. "+ f"Members' existing tokens stop refreshing. The {org}/org-settings entry is left as is; "+ f"remove it with the wiki if the org is done with adom-google.")++ def cmd_onboard_finish(a):- """Record the org's freshly-created OAuth client and wire RELAY mode (secret stays local)."""+ """Record the org's freshly-created OAuth client.++ DEFAULT (0.8.26+): register it with the gateway for the WHOLE org (`org register`): the+ gateway holds the secret and brokers, and the non-secret half goes on <org>/org-settings, so+ every member just runs `adom-google auth`. `--relay` keeps the old single-box mode where the+ secret stays 0600 on this box only and teammates cannot use it."""+ if not getattr(a, "relay", False):+ a.domain = getattr(a, "domain", None)+ return cmd_org_register(a) gw = (a.gateway or ADOM_SHARED_GATEWAY).rstrip("/") prov = {} if os.path.exists(PROVIDER_PATH):@@ -1706,7 +1844,7 @@ def cmd_onboard_finish(a): prov = {} prov["gateway_url"] = gw prov["client_id"] = a.client_id- prov["client_secret"] = a.client_secret # the org's OWN secret, on the org's OWN box+ prov["client_secret"] = a.client_secret or sys.stdin.readline().strip() # the org's OWN secret, on the org's OWN box prov["app"] = a.org prov["broker"] = False # relay mode: gateway relays, we exchange locally os.makedirs(CFG_DIR, exist_ok=True)@@ -2155,10 +2293,22 @@ def main(): y = obx.add_parser("finish") y.add_argument("--org", required=True) y.add_argument("--client-id", dest="client_id", required=True)- y.add_argument("--client-secret", dest="client_secret", required=True)+ y.add_argument("--client-secret", dest="client_secret", help="omit or '-' to read from stdin") y.add_argument("--gateway", help="override the shared callback gateway (advanced)")+ y.add_argument("--domain", help="your Google Workspace domain(s), comma separated (recorded in org settings)")+ y.add_argument("--relay", action="store_true", help="old single-box mode: keep the secret 0600 on THIS box only (teammates cannot use it)") y.set_defaults(fn=cmd_onboard_finish) + og = sub.add_parser("org", help="set adom-google up ONCE for a whole wiki org (gateway holds the secret; members auto-discover)")+ ogs = og.add_subparsers(dest="sub")+ z = ogs.add_parser("register", help="hand your org's Google OAuth client to the gateway + publish the non-secret half to <org>/org-settings")+ z.add_argument("--org", required=True); z.add_argument("--client-id", dest="client_id", required=True)+ z.add_argument("--client-secret", dest="client_secret", help="omit or '-' to read it from stdin (keeps it out of shell history)")+ z.add_argument("--domain", help="Google Workspace domain(s), comma separated"); z.add_argument("--gateway")+ z.set_defaults(fn=cmd_org_register)+ z = ogs.add_parser("status"); z.add_argument("--org", required=True); z.add_argument("--gateway"); z.add_argument("--json", action="store_true"); z.set_defaults(fn=cmd_org_status)+ z = ogs.add_parser("unregister"); z.add_argument("--org", required=True); z.add_argument("--gateway"); z.set_defaults(fn=cmd_org_unregister)+ g = sub.add_parser("gmail"); gs = g.add_subparsers(dest="sub") x = gs.add_parser("attachments"); x.add_argument("message_id"); x.set_defaults(fn=cmd_gmail_attachments) x = gs.add_parser("read"); x.add_argument("message_id"); x.add_argument("attachment_id"); x.add_argument("-o", "--out"); x.set_defaults(fn=cmd_gmail_read)
page.json+4−3@@ -94,7 +94,7 @@ "skills": [], "hero_path": "screenshots/hero-google-v5.png", "hero_type": "image",- "description": "Drive your whole Google Workspace from the terminal: Gmail, Contacts, Drive, Sheets, Docs, Slides, Calendar, Tasks, Google Chat, plus a generic `api` passthrough to hit ANY Google API. Connect a personal @gmail (or another org's Google account) with ZERO setup via Adom's hosted Connector (`adom-google connect-personal`), or bring your own token-broker gateway (`provider set`): Adom staff use the private adom-google-adom provider. One-click OAuth, no client secret on your machine.",+ "description": "Drive your whole Google Workspace from the terminal: Gmail, Contacts, Drive, Sheets, Docs, Slides, Calendar, Tasks, Google Chat, plus a generic `api` passthrough to hit ANY Google API. Open source, ships with NO OAuth provider baked in: point it at your own token-broker gateway (`provider set`), buy a managed one, or: Adom staff: install the private adom-google-adom provider. One-click OAuth, no client secret on your machine. First run asks safe mode (no delete) vs full mode (incl. delete).", "keywords": [ "adom-google", "google-workspace",@@ -118,6 +118,8 @@ "uninstall": "./uninstall.sh" }, "files": [+ "bin/adom-google",+ "bin/adom-gmail", "SKILL.md", "skills/adom-google-onboarding/SKILL.md", "README.md",@@ -126,8 +128,7 @@ "provider.example.json", "install.sh", "uninstall.sh",- "package.json",- "VERSION"+ "package.json" ], "org": "adom" }