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.25: provider discovery from wiki org settings
3 files changed
+134−12
bin/adom-google+130−9@@ -21,9 +21,9 @@ Auth (one consent, long-lived refresh token): See `adom-google help`. """-import sys, os, json, base64, argparse, urllib.parse, ssl, socket, hashlib, secrets, struct, time+import sys, os, json, base64, argparse, urllib.parse, ssl, socket, hashlib, secrets, struct, time, subprocess, shutil -ADOM_GOOGLE_VERSION = "0.8.24"+ADOM_GOOGLE_VERSION = "0.8.25" if sys.argv[1:2] == ["--version"]: print(f"adom-google {ADOM_GOOGLE_VERSION}") sys.exit(0)@@ -272,6 +272,79 @@ BUNDLED_PROVIDER = os.path.join(HERE, "provider.json") LEGACY_CREDS = os.path.join(HERE, "credentials.json") MANUAL_REDIRECT = "http://localhost" # manual desktop-client flow: loopback, user copies the code +ORG_INTEGRATION = "google-oauth" # file integrations/google-oauth.json on <org>/org-settings+++def _wiki_json(args, timeout=25):+ """Run `adom-wiki <args> --json`; return the envelope dict, or None on any failure."""+ if not shutil.which("adom-wiki"):+ return None+ try:+ out = subprocess.run(["adom-wiki", *args, "--json"], capture_output=True, text=True,+ timeout=timeout).stdout+ return json.loads(out)+ except Exception:+ return None+++def _discover_org_providers(only_org=None):+ """Find the Google OAuth provider in the signed-in wiki user's ORG SETTINGS.++ Every wiki org can keep shared, NON-secret integration config on its members-only+ `<org>/org-settings` page (`adom-wiki settings`). An org that has set adom-google up stores+ `integrations/google-oauth.json` there: {client_id, gateway_url, app, mode:"broker", ...}.+ The client secret is never in it; in broker mode it lives on the OAuth gateway, which does+ the token exchange. So any member's first `adom-google auth` just works, with no extra+ package. Returns (orgs_checked, [(org, data), ...])."""+ who = _wiki_json(["whoami"])+ orgs = [o.get("name") for o in (((who or {}).get("data") or {}).get("orgs") or []) if o.get("name")]+ if only_org:+ orgs = [only_org]+ found = []+ for org in orgs:+ d = _wiki_json(["settings", "get", org, ORG_INTEGRATION])+ if not d or d.get("status") != "ok":+ continue+ data = d.get("data") or {}+ if data.get("client_secret") or data.get("secret") is True:+ continue # never accept a secret from shared settings; it does not belong there+ if data.get("client_id") and data.get("gateway_url"):+ found.append((org, data))+ return orgs, found+++def _write_org_provider(org, data):+ """Cache an org-settings provider as provider.json (0600), tagged with its source."""+ prov = {"gateway_url": data["gateway_url"].rstrip("/"), "client_id": data["client_id"],+ "app": data.get("app") or org, "broker": (data.get("mode") or "broker") == "broker",+ "_source": f"org-settings:{org}", "_fetched": time.strftime("%Y-%m-%d")}+ if data.get("gateway_ws"):+ prov["gateway_ws"] = data["gateway_ws"]+ os.makedirs(CFG_DIR, exist_ok=True)+ fd = os.open(PROVIDER_PATH, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)+ with os.fdopen(fd, "w") as f:+ json.dump(prov, f, indent=2)+ os.chmod(PROVIDER_PATH, 0o600)+ return prov+++_DISCOVERY = {"tried": False, "candidates": [], "orgs": []}+++def _maybe_discover():+ """If NOTHING configures a provider, try the user's org settings once per process.+ An explicit provider (provider set, env, a bundled file) always wins and is never touched."""+ if _DISCOVERY["tried"] or os.environ.get("ADOM_GOOGLE_NO_DISCOVER"):+ return False+ _DISCOVERY["tried"] = True+ orgs, found = _discover_org_providers()+ _DISCOVERY["orgs"], _DISCOVERY["candidates"] = orgs, [o for o, _ in found]+ if len(found) == 1:+ _write_org_provider(*found[0])+ return True+ return False # none, or several orgs: never guess which org a Google login belongs to++ _PROVIDER = None def _provider(): """Resolve {app, gateway_url, gateway_ws, gateway_redirect, client_id, client_secret}."""@@ -321,16 +394,19 @@ def _provider(): p["gateway_redirect"] = p["gateway_url"] + "/callback" else: p["gateway_redirect"] = None+ if _maybe_discover(): # wrote provider.json from org settings: resolve again+ return _provider() _PROVIDER = p return p NO_PROVIDER_HINT = """ERROR: adom-google has no OAuth provider configured (gateway URL + client_id).-The open-source CLI ships without one on purpose — the provider is account-specific. Three ways:-Hint: • Adom employee → install the private `adom-google-adom` package (drops the Adom provider).-Hint: • Managed (any org, ~$5/mo) → email [email protected] for a hosted OAuth gateway container.-Hint: • Self-host (DIY) → run your own gateway (john/service-oauth) + Google OAuth client, then:-Hint: adom-google provider set --gateway https://your-gateway.example --client-id <id>+Hint: adom-google looks for one in your wiki org's settings first (adom-wiki settings get <org> google-oauth).+Hint: • Your org already set adom-google up (Adom does)? Run: adom-google provider discover+Hint: If you belong to SEVERAL such orgs it will not guess: adom-google provider discover --org <org>+Hint: • First person in your org? Set it up once for everyone: adom-google onboard --org <org>+Hint: • Personal @gmail or any org, zero setup: adom-google connect-personal (Adom's hosted Connector)+Hint: • Self-host: adom-google provider set --gateway https://your-gateway.example --client-id <id> Hint: Check the current provider any time with: adom-google provider show""" # AI hint emitted whenever adom-google is unconfigured. Per ai-hints-in-clis: this is how the@@ -1284,6 +1360,15 @@ def _cmd_auth_impl(a, as_json, real_out): if a.manual: return _auth_manual(cfg, scopes, mode) # Gateway flow — one static redirect, code routed back over WebSocket. Needs a provider.+ try:+ tag = json.load(open(PROVIDER_PATH)).get("_source", "") if os.path.exists(PROVIDER_PATH) else ""+ except Exception:+ tag = ""+ if tag.startswith("org-settings:") and not os.environ.get("ADOM_GOOGLE_NO_DISCOVER"):+ _o, _f = _discover_org_providers(only_org=tag.split(":", 1)[1])+ if _f:+ global _PROVIDER+ _write_org_provider(*_f[0]); _PROVIDER = None pr = _provider() if not pr["gateway_url"]: sys.exit(NO_PROVIDER_HINT)@@ -1518,14 +1603,49 @@ def cmd_provider_set(a): print("Next: ask the user safe vs full, then `adom-google auth`.") +def cmd_provider_discover(a):+ """Fetch the provider from the user's wiki org settings and cache it (0600)."""+ orgs, found = _discover_org_providers(only_org=getattr(a, "org", None))+ res = {"orgs_checked": orgs, "found": [o for o, _ in found]}+ if len(found) == 1:+ org, data = found[0]+ prov = _write_org_provider(org, data)+ res.update(ok=True, org=org, app=prov["app"], gateway_url=prov["gateway_url"],+ mode="broker" if prov["broker"] else "relay", provider_path=PROVIDER_PATH)+ if a.json:+ print(json.dumps(res)); return+ print(f"OK: provider from {org}/org-settings → {PROVIDER_PATH} (0600)")+ print(f" app={prov['app']} gateway={prov['gateway_url']} mode={res['mode']}")+ print("Next: adom-google auth (one Allow click)")+ return+ res["ok"] = False+ if a.json:+ print(json.dumps(res)); sys.exit(1)+ if not orgs:+ sys.exit("ERROR: could not read your wiki orgs (is adom-wiki installed and signed in? try: adom-wiki whoami).")+ if not found:+ sys.exit(f"ERROR: none of your orgs ({', '.join(orgs)}) has set adom-google up in its org settings.\n"+ "Hint: first person in the org? adom-google onboard --org <org>\n"+ "Hint: personal or zero-setup: adom-google connect-personal")+ sys.exit(f"ERROR: several of your orgs have adom-google set up ({', '.join(res['found'])}); pick one:\n"+ + "".join(f" adom-google provider discover --org {o}\n" for o in res["found"]))++ def cmd_provider_show(a): pr = _provider() if os.path.exists(PROVIDER_PATH): src = PROVIDER_PATH elif os.path.exists(BASE_PROVIDER_PATH): src = BASE_PROVIDER_PATH + " (inherited from default account)" elif os.path.exists(BUNDLED_PROVIDER): src = BUNDLED_PROVIDER + " (bundled)"- elif os.path.exists(LEGACY_CREDS): src = LEGACY_CREDS + " (legacy)"+ elif os.path.exists(LEGACY_CREDS) and "clientId" in open(LEGACY_CREDS).read():+ src = LEGACY_CREDS + " (legacy)" elif os.environ.get("OAUTH_GATEWAY_URL"): src = "environment"- else: src = "(none — run `provider set`)"+ else: src = "(none: run `provider discover`, or `provider set`)"+ try:+ tag = json.load(open(PROVIDER_PATH)).get("_source", "") if os.path.exists(PROVIDER_PATH) else ""+ if tag.startswith("org-settings:"):+ src += f" [from {tag.split(':',1)[1]}/org-settings, `provider discover` refreshes it]"+ except Exception:+ pass print(f"app: {pr['app']}") print(f"gateway_url: {pr['gateway_url'] or '(UNSET)'}") print(f"gateway_ws: {pr['gateway_ws'] or '(derived from gateway_url)'}")@@ -2023,6 +2143,7 @@ def main(): x.add_argument("--client-id", dest="client_id"); x.add_argument("--client-secret", dest="client_secret") x.add_argument("--app"); x.set_defaults(fn=cmd_provider_set) x = pvs.add_parser("show"); x.set_defaults(fn=cmd_provider_show)+ x = pvs.add_parser("discover", help="load the provider from your wiki org's settings (adom-wiki settings get <org> google-oauth)"); x.add_argument("--org", help="pick this org when you belong to several"); x.add_argument("--json", action="store_true"); x.set_defaults(fn=cmd_provider_discover) s = sub.add_parser("status"); s.add_argument("--json", action="store_true", help="one JSON object with authorized:true|false (+ account, mode, scopes, gateway); exit 1 when NOT authorized"); s.set_defaults(fn=cmd_status) s = sub.add_parser("accounts"); s.add_argument("--json", action="store_true"); s.set_defaults(fn=cmd_accounts) s = sub.add_parser("setup"); s.set_defaults(fn=cmd_setup)
install.shpage.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" }