feat(plugins/google_chat): Google Chat platform adapter as a bundled plugin

Adds Google Chat as a new gateway platform, shipped under
plugins/platforms/google_chat/ following the canonical bundled-plugin
pattern (Teams, IRC).  Rewired from the original PR #18425 to use the
new env_enablement_fn + cron_deliver_env_var plugin interfaces landed
in the preceding commit, so the adapter touches ZERO core files.

What it does:
- Inbound DM + group messages via Cloud Pub/Sub pull subscription (no
  public URL needed), with attachments (PDFs, images, audio, video)
  downloaded through an SSRF-guarded Google-host allowlist.
- Outbound text replies with the 'Hermes is thinking…' patch-in-place
  pattern — no tombstones.
- Native file attachment delivery via per-user OAuth.  Google Chat's
  media.upload endpoint rejects service-account auth, so each user
  runs /setup-files once in their own DM to grant
  chat.messages.create for themselves; the adapter then uploads as
  them.  Tokens stored per email at
  ~/.hermes/google_chat_user_tokens/<email>.json.
- Thread isolation: side-threads get isolated sessions, top-level DM
  messages share one continuous session.  Persistent thread-count
  store survives gateway restart.
- Supervisor reconnect with exponential backoff.
- Multi-user out of the box.

How it plugs in (no core edits):
- env_enablement_fn seeds PlatformConfig.extra with project_id,
  subscription_name, service_account_json, and the home_channel dict
  (which the core hook turns into a HomeChannel dataclass).  Reads
  GOOGLE_CHAT_PROJECT_ID (falls back to GOOGLE_CLOUD_PROJECT),
  GOOGLE_CHAT_SUBSCRIPTION_NAME (falls back to GOOGLE_CHAT_SUBSCRIPTION),
  GOOGLE_CHAT_SERVICE_ACCOUNT_JSON (falls back to
  GOOGLE_APPLICATION_CREDENTIALS), GOOGLE_CHAT_HOME_CHANNEL.
- cron_deliver_env_var='GOOGLE_CHAT_HOME_CHANNEL' gets cron delivery
  for free — cron/scheduler.py consults the platform registry for any
  name not in its hardcoded built-in sets.
- plugin.yaml's rich requires_env / optional_env blocks auto-populate
  OPTIONAL_ENV_VARS via the new hermes_cli/config.py injector, so
  'hermes config' UI surfaces them with description / url / prompt /
  password metadata.
- Module-level Platform('google_chat') call in adapter.py triggers the
  Platform._missing_() registration so Platform.GOOGLE_CHAT attribute
  access works without an enum entry.

Distribution: ships inside the existing hermes-agent package.  Users
opt in via 'pip install hermes-agent[google_chat]' and follow the
8-step GCP walkthrough at
website/docs/user-guide/messaging/google_chat.md.

Test coverage: 153 tests in tests/gateway/test_google_chat.py, all
passing.  Spans platform registration, env config loading, Pub/Sub
envelope routing, outbound send + chunking + typing patch-in-place,
attachment send paths, SSRF guard, thread/session model,
supervisor reconnect, authorization, per-user OAuth, and the new
plugin-registry cron delivery wiring.

Credit: adapter + OAuth + tests + docs authored by @donramon77
(PR #18425).  Rewire onto the new plugin hooks + salvage commit by
Teknium.

Co-Authored-By: Ramón Fernández <112875006+donramon77@users.noreply.github.com>
This commit is contained in:
Ramón Fernández 2026-05-07 06:41:48 -07:00 committed by Teknium
parent af9336d575
commit 44cd79e798
12 changed files with 6781 additions and 1 deletions

View file

@ -0,0 +1,370 @@
---
sidebar_position: 12
title: "Google Chat"
description: "Set up Hermes Agent as a Google Chat bot using Cloud Pub/Sub"
---
# Google Chat Setup
Connect Hermes Agent to Google Chat as a bot. The integration uses Cloud Pub/Sub
pull subscriptions for inbound events and the Chat REST API for outbound messages.
Equivalent ergonomics to Slack Socket Mode or Telegram long-polling: your Hermes
process does not need a public URL, a tunnel, or a TLS certificate. It connects,
authenticates, and listens on a subscription — the same way a Telegram bot listens
on a token.
:::note Workspace edition
Google Chat is part of Google Workspace. You can use this integration with a
personal Workspace (`@yourdomain.com` registered through Google) or a work
Workspace where you have the Admin rights to publish an app. Gmail-only accounts
cannot host Chat apps.
:::
## Overview
| Component | Value |
|-----------|-------|
| **Libraries** | `google-cloud-pubsub`, `google-api-python-client`, `google-auth` |
| **Inbound transport** | Cloud Pub/Sub pull subscription (no public endpoint) |
| **Outbound transport** | Chat REST API (`chat.googleapis.com`) |
| **Authentication** | Service Account JSON with `roles/pubsub.subscriber` on the subscription |
| **User identification** | Chat resource names (`users/{id}`) + email |
---
## Step 1: Create or pick a GCP project
You need a Google Cloud project to host the Pub/Sub topic. If you don't have one,
create it at [console.cloud.google.com](https://console.cloud.google.com) —
personal accounts get a free tier that easily covers bot traffic.
Note the project ID (e.g., `my-chat-bot-123`). You'll use it in every subsequent
step.
---
## Step 2: Enable two APIs
In the console, go to **APIs & Services → Library** and enable:
- **Google Chat API**
- **Cloud Pub/Sub API**
Both are free for the volumes a personal bot generates.
---
## Step 3: Create a Service Account
**IAM & Admin → Service Accounts → Create Service Account.**
- Name: `hermes-chat-bot`
- Skip the "Grant this service account access to project" step. IAM on the specific
subscription is all you need — do **NOT** grant project-level Pub/Sub roles.
After creation, open the SA, go to **Keys → Add Key → Create new key → JSON** and
download the file. Save it somewhere only Hermes can read (e.g.,
`~/.hermes/google-chat-sa.json`, `chmod 600`).
:::caution There is NO "Chat Bot Caller" role
A common mistake is to search for a Chat-specific IAM role and grant it at the
project level. That role doesn't exist. Chat bot authority comes from being
installed in a space, not from IAM. All your SA needs is Pub/Sub subscriber on
the subscription you create in the next step.
:::
---
## Step 4: Create the Pub/Sub topic and subscription
**Pub/Sub → Topics → Create topic.**
- Topic ID: `hermes-chat-events`
- Leave the defaults for everything else.
After creation, the topic's detail page has a **Subscriptions** tab. Create one:
- Subscription ID: `hermes-chat-events-sub`
- Delivery type: **Pull**
- Message retention: **7 days** (so backlog survives a hermes restart)
- Leave the rest default.
---
## Step 5: IAM binding on the topic (critical)
On the **topic** (not the subscription), add an IAM principal:
- Principal: `chat-api-push@system.gserviceaccount.com`
- Role: `Pub/Sub Publisher`
Without this, Google Chat cannot publish events to your topic and your bot will
never receive anything.
---
## Step 6: IAM binding on the subscription
On the **subscription**, add your own Service Account as a principal:
- Principal: `hermes-chat-bot@<your-project>.iam.gserviceaccount.com`
- Role: `Pub/Sub Subscriber`
Also grant `Pub/Sub Viewer` on the same subscription — Hermes calls
`subscription.get()` at startup as a reachability check.
---
## Step 7: Configure the Chat app
Go to **APIs & Services → Google Chat API → Configuration**.
- **App name**: whatever you want users to see ("Hermes" is reasonable).
- **Avatar URL**: any public PNG (Google has some defaults).
- **Description**: a short sentence shown in the app directory.
- **Functionality**: enable **Receive 1:1 messages** and **Join spaces and group
conversations**.
- **Connection settings**: select **Cloud Pub/Sub**, enter the topic name
`projects/<your-project>/topics/hermes-chat-events`.
- **Visibility**: restrict to your workspace (or specific users) — do not publish
to everyone while you're testing.
Save.
---
## Step 8: Install the bot in a test space
Open Google Chat in a browser. Start a DM with your app by searching for its name
in the **+ New Chat** menu. The first time you message it, Google sends an
`ADDED_TO_SPACE` event that Hermes uses to cache the bot's own `users/{id}` for
self-message filtering.
---
## Step 9: Configure Hermes
Add the Google Chat section to `~/.hermes/.env`:
```bash
# Required
GOOGLE_CHAT_PROJECT_ID=my-chat-bot-123
GOOGLE_CHAT_SUBSCRIPTION_NAME=projects/my-chat-bot-123/subscriptions/hermes-chat-events-sub
GOOGLE_CHAT_SERVICE_ACCOUNT_JSON=/home/you/.hermes/google-chat-sa.json
# Authorization — paste the emails of people allowed to talk to the bot
GOOGLE_CHAT_ALLOWED_USERS=you@yourdomain.com,coworker@yourdomain.com
# Optional
GOOGLE_CHAT_HOME_CHANNEL=spaces/AAAA... # default delivery destination for cron jobs
GOOGLE_CHAT_MAX_MESSAGES=1 # Pub/Sub FlowControl; 1 serializes commands per session
GOOGLE_CHAT_MAX_BYTES=16777216 # 16 MiB — cap on in-flight message bytes
```
The project ID also falls back to `GOOGLE_CLOUD_PROJECT`, and the SA path falls
back to `GOOGLE_APPLICATION_CREDENTIALS` — use whichever convention you prefer.
Install Hermes with the optional dependencies:
```bash
pip install 'hermes-agent[google_chat]'
```
Start the gateway:
```bash
hermes gateway
```
You should see a log line like:
```
[GoogleChat] Connected; project=my-chat-bot-123, subscription=<redacted>,
bot_user_id=users/XXXX, flow_control(msgs=1, bytes=16777216)
```
Send "hola" in the test DM. The bot posts a "Hermes is thinking…" marker, then
edits that same message in place with the real response — no "message deleted"
tombstones.
---
## Formatting and capabilities
Google Chat renders a limited markdown subset:
| Supported | Not supported |
|-----------|---------------|
| `*bold*`, `_italic_`, `~strike~`, `` `code` `` | Headings, lists |
| Inline images via URL | Interactive Card v2 buttons (v1 of this gateway) |
| Native file attachments (after `/setup-files` — see Step 10) | Native voice notes / circular video notes |
The agent's system prompt includes a Google Chatspecific hint so it knows these
limits and avoids formatting that won't render.
Message size limit: 4000 characters per message. Longer agent responses are
automatically split across multiple messages.
Thread support: when a user replies inside a thread, Hermes detects the
`thread.name` and posts its reply in the same thread, so each thread gets a
separate Hermes session.
---
## Step 10: Native attachment delivery (optional)
Out of the box the bot can post text, inline images via URL, and download cards
for audio/video/documents. To deliver **native** Chat attachments — the same
file widget you get when a human drags-and-drops a file — each user authorizes
the bot once via a per-user OAuth flow.
### Why a separate flow
Google Chat's `media.upload` endpoint hard-rejects service-account auth:
> This method doesn't support app authentication with a service account.
> Authenticate with a user account.
There's no IAM role or scope that fixes this. The endpoint only accepts user
credentials. So the bot has to act *as a user* whenever it uploads a file —
specifically, as the user who asked for the file.
### One-time host setup
1. Go to **APIs & Services → Credentials** in the same GCP project.
2. **Create credentials → OAuth client ID → Desktop app**.
3. Download the JSON. Move it onto the host that runs Hermes.
4. On the host, register the client with Hermes:
```bash
python -m gateway.platforms.google_chat_user_oauth \
--client-secret /path/to/client_secret.json
```
That writes `~/.hermes/google_chat_user_client_secret.json`. This is shared
infrastructure — it identifies the OAuth *app*, not any individual user. One
file per host is enough no matter how many users authorize later.
### Per-user authorization (in chat)
Each user runs the flow once, in their own DM with the bot:
1. They send `/setup-files` to the bot. It replies with status and the next
step.
2. They send `/setup-files start`. The bot replies with an OAuth URL.
3. They open the URL, click **Allow**, and watch the browser fail to load
`http://localhost:1/?...&code=...`. That failure is expected — the auth
code is in the URL bar.
4. They copy the failed URL (or just the `code=...` value) and paste it back
into chat as `/setup-files <PASTED_URL>`. The bot exchanges it for a
refresh token.
The token lands at `~/.hermes/google_chat_user_tokens/<sanitized_email>.json`.
Subsequent file requests in that user's DM use *their* token, so the bot
uploads as them and the message lands in their space.
To revoke later: `/setup-files revoke` deletes only that user's token. Other
users' tokens are untouched.
### Scope
The flow requests exactly one scope: `chat.messages.create`. That covers both
`media.upload` and the `messages.create` that references the uploaded
`attachmentDataRef`. No Drive, no broader Chat scopes — this is least-privilege
on purpose.
### Multi-user behavior
When the asker has no per-user token yet, the bot falls back to a legacy
single-user token at `~/.hermes/google_chat_user_token.json` (if present from
a pre-multi-user install). When neither is available, the bot posts a clear
text notice telling the asker to run `/setup-files`.
A user revoking only clears their own slot. A 401/403 from one user's token
evicts only that user's cache. Users don't disrupt each other.
---
## Troubleshooting
**Bot stays silent after sending "hola."**
1. Check the Pub/Sub subscription has undelivered messages in the console.
If it does, Hermes isn't authenticated — verify `GOOGLE_CHAT_SERVICE_ACCOUNT_JSON`
and that the SA is listed as `Pub/Sub Subscriber` on the subscription.
2. If the subscription has zero messages, Google Chat isn't publishing.
Double-check the IAM binding on the **topic**:
`chat-api-push@system.gserviceaccount.com` must have `Pub/Sub Publisher`.
3. Check `hermes gateway` logs for `[GoogleChat] Connected`. If you see
`[GoogleChat] Config validation failed`, the error message tells you which
env var to fix.
**Bot replies but an error message appears instead of the agent's answer.**
Check logs for `[GoogleChat] Pub/Sub stream died` — if these repeat, your SA
credentials may have been rotated or the subscription deleted. After 10 attempts
the adapter marks itself fatal.
**"403 Forbidden" on every outbound message.**
The bot was removed from the space, or you revoked it in the Chat API console.
Re-install it in the space (the next `ADDED_TO_SPACE` event will re-enable
messaging automatically).
**Too many "Rate limit hit" warnings.**
The Chat API's default quotas allow 60 messages per space per minute. If your
agent produces long streaming responses that exceed that, the adapter retries
with exponential backoff — but you'll still see user-visible latency. Consider
concise responses or raising the quota in the GCP console.
**Bot keeps posting the "/setup-files" notice instead of files.**
The asker has no per-user OAuth token and there's no legacy fallback. Run
`/setup-files` in their DM and follow Step 10. After the exchange completes
the next file request uploads natively without a gateway restart.
**`/setup-files start` says "No client credentials stored on the host."**
The one-time host setup wasn't done. From a terminal on the host that runs
Hermes:
```bash
python -m gateway.platforms.google_chat_user_oauth \
--client-secret /path/to/client_secret.json
```
Then send `/setup-files start` again.
**`/setup-files <PASTED_URL>` says "Token exchange failed."**
The auth code is single-use and short-lived (typically a few minutes). Send
`/setup-files start` to get a fresh URL and retry.
---
## Security notes
- **Service Account scope**: the adapter requests `chat.bot` and `pubsub` scopes.
IAM should be the actual enforcement — grant your SA the minimum
(`roles/pubsub.subscriber` + `roles/pubsub.viewer` on the subscription), not
project-level or org-level Pub/Sub roles.
- **Attachment download protection**: Hermes will only attach the SA bearer
token to URLs whose host matches a short allowlist of Google-owned domains
(`googleapis.com`, `drive.google.com`, `lh[3-6].googleusercontent.com`, and
a few others). Any other host is rejected before the HTTP request, to
protect against SSRF scenarios where a crafted event could redirect the
bearer token to the GCE metadata service.
- **Redaction**: Service Account emails, subscription paths, and topic paths
are stripped from log output by `agent/redact.py`. The debug envelope dump
(`GOOGLE_CHAT_DEBUG_RAW=1`) routes through the same redaction filter and
logs at DEBUG level.
- **Compliance**: if you plan to connect this bot to a regulated workspace
(anything with a data-residency or AI-governance policy), get that approval
before the first install.
- **User OAuth scope**: the per-user attachment flow requests *only*
`chat.messages.create` — the minimum that covers `media.upload` plus the
follow-up `messages.create`. Tokens are persisted as plain JSON at
`~/.hermes/google_chat_user_tokens/<sanitized_email>.json` (filesystem
permissions are the protection — same model as the SA key file). Each
token is owned by exactly one user; revoke is scoped to that user.

View file

@ -17,6 +17,7 @@ For the full voice feature set — including CLI microphone mode, spoken replies
| Telegram | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ |
| Discord | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Slack | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Google Chat | — | ✅ | ✅ | ✅ | — | ✅ | — |
| WhatsApp | — | ✅ | ✅ | — | — | ✅ | ✅ |
| Signal | — | ✅ | ✅ | — | — | ✅ | ✅ |
| SMS | — | — | — | — | — | — | — |
@ -46,6 +47,7 @@ flowchart TB
dc[Discord]
wa[WhatsApp]
sl[Slack]
gc[Google Chat]
sig[Signal]
sms[SMS]
em[Email]
@ -74,6 +76,7 @@ flowchart TB
dc --> store
wa --> store
sl --> store
gc --> store
sig --> store
sms --> store
em --> store
@ -383,6 +386,7 @@ Each platform has its own toolset:
| Discord | `hermes-discord` | Full tools including terminal |
| WhatsApp | `hermes-whatsapp` | Full tools including terminal |
| Slack | `hermes-slack` | Full tools including terminal |
| Google Chat | `hermes-google-chat` | Full tools including terminal |
| Signal | `hermes-signal` | Full tools including terminal |
| SMS | `hermes-sms` | Full tools including terminal |
| Email | `hermes-email` | Full tools including terminal |
@ -406,6 +410,7 @@ Each platform has its own toolset:
- [Telegram Setup](telegram.md)
- [Discord Setup](discord.md)
- [Slack Setup](slack.md)
- [Google Chat Setup](google_chat.md)
- [WhatsApp Setup](whatsapp.md)
- [Signal Setup](signal.md)
- [SMS Setup (Twilio)](sms.md)