mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
Cross-checked 75 docs pages under user-guide/messaging/, developer-guide/,
guides/, and integrations/ against the live registries and gateway code.
messaging/
- index.md: API Server toolset is hermes-api-server (was 'hermes (default)');
Google Chat slug is hermes-google_chat (underscore — plugin name uses _).
- google_chat.md: drop bogus 'pip install hermes-agent[google_chat]' (no such
extra); list the actual deps (google-cloud-pubsub, google-api-python-client,
google-auth, google-auth-oauthlib).
- qqbot.md: config namespace is platforms.qqbot (was platforms.qq, which is
silently ignored by the adapter); QQ_STT_BASE_URL is not read directly —
baseUrl lives under platforms.qqbot.extra.stt.
- teams-meetings.md: 'hermes teams-pipeline' is plugin-gated (teams_pipeline
plugin must be enabled), not a built-in subcommand.
- sms.md: example log line 0.0.0.0:8080 -> 127.0.0.1:8080 (default
SMS_WEBHOOK_HOST).
- open-webui.md: API_SERVER_* are env vars, not YAML keys — write them to
per-profile .env, not 'hermes config set' (same pattern fixed in
api-server.md last round). Also bumped example ports to 8650+ to dodge the
default webhook (8644)/wecom-callback (8645)/msgraph-webhook (8646)
collision.
developer-guide/
- architecture.md: tool/toolset counts (61/52 -> 70+/~28); LOC stamps for
run_agent.py, cli.py, hermes_cli/main.py, setup.py, mcp_tool.py,
gateway/run.py replaced with 'large file' to stop drifting.
- agent-loop.md: same LOC drift (~13,700 -> 'a large file (15k+ lines)').
- gateway-internals.md: '14+ external messaging platforms' -> '20+'; gateway
platform tree updated (qqbot is a sub-package, not qqbot.py; added
yuanbao.py, feishu_comment.py, msgraph_webhook.py); 'gateway/builtin_hooks/
(always active)' was wrong — it's an empty extension point and
_register_builtin_hooks() is a no-op stub.
- acp-internals.md: drop fictional 'message_callback' from the bridged-
callbacks list; clarify thinking_callback is currently set to None.
- provider-runtime.md: provider list was missing AWS Bedrock, Azure Foundry,
NVIDIA NIM, xAI, Arcee, GMI Cloud, StepFun, Qwen OAuth, Xiaomi, Ollama
Cloud, LM Studio, Tencent TokenHub. Fallback section described only the
legacy single-pair model — corrected to the canonical list-form
fallback_providers chain.
- environments.md: parsers list missing llama4_json and the deepseek_v31
alias; both register via @register_parser.
- browser-supervisor.md: drop reference to scripts/browser_supervisor_e2e.py
which doesn't exist in-repo.
- contributing.md: tinker-atropos is a git submodule — note that
'git submodule update --init' is required if cloning without
--recurse-submodules.
guides/
- operate-teams-meeting-pipeline.md: cron flags were all wrong — schedule is
positional (not --schedule), the script-only flag is --no-agent (not
--script-only), and there's no --command flag. Replaced with a real example
that creates the script under ~/.hermes/scripts/ and uses the actual flags.
Also replaced fictional 'hermes cron show <name>' with 'hermes cron status'.
- automation-templates.md: 'cron create --skills "a,b"' doesn't work —
the flag is --skill (singular, repeatable). Fixed all 5 occurrences via AST
rewrite.
- minimax-oauth.md: 'hermes auth add minimax-oauth --region cn' silently
fails because --region isn't registered on the auth-add argparse spec.
Pointed users at the minimax-cn provider (or MINIMAX_CN_API_KEY env) for
China-region access.
- cron-script-only.md: 'hermes send' is fictional — replaced the comparison-
table mention with a webhook-subscription pointer; also fixed the dead link
to /guides/pipe-script-output (page doesn't exist).
- cron-troubleshooting.md: 'hermes serve' isn't a real subcommand. Pointed
at 'hermes gateway' (foreground) / 'hermes gateway start' (service).
- local-ollama-setup.md: 'agent.api_timeout' is not a config key. The right
knob is the HERMES_API_TIMEOUT env var.
- python-library.md: run_conversation() return dict has only final_response
and messages — task_id is stored on the agent instance, not echoed back.
- use-mcp-with-hermes.md: '--args /c "npx -y …"' wraps the npx command in
one quoted string, so cmd.exe gets a single arg instead of the multi-token
command line it needs. Removed the surrounding quotes — argparse nargs='*'
collects each token correctly.
integrations/
- providers.md: Bedrock guardrail YAML keys were 'id'/'version' (don't exist);
actual keys are guardrail_identifier/guardrail_version (matches DEFAULT_CONFIG
and the run_agent.py reader). GMI default base URL (api.gmi.ai/v1 ->
api.gmi-serving.com/v1) and portal URL (inference.gmi.ai -> www.gmicloud.ai)
refreshed. Fallback section rewritten to lead with the canonical
fallback_providers list form (was leading with the legacy fallback_model
single dict); supported-providers list extended to include azure-foundry,
alibaba-coding-plan, lmstudio.
index.md
- '68 built-in tools' -> '70+'; '15+ platforms' was both inconsistent with
integrations/index.md ('19+') and undercounted — bumped to 20+ and added
Weixin/QQ Bot/Yuanbao/Google Chat to the list.
Validation: 'npm run build' clean (exit 0); broken-link count unchanged at
155 (same as round-1 post-skill-regen baseline). 24 files, +132/-89.
370 lines
14 KiB
Markdown
370 lines
14 KiB
Markdown
---
|
||
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 the dependencies the Google Chat adapter needs (no Hermes extra is currently published — install them directly):
|
||
|
||
```bash
|
||
pip install google-cloud-pubsub google-api-python-client google-auth google-auth-oauthlib
|
||
```
|
||
|
||
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 Chat–specific 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.
|