From b76cd6695d11bd3cb5776e7c038abe9dbaa5f4a5 Mon Sep 17 00:00:00 2001
From: iHildy
` (then the sender is added to a local allowlist store).
- Public inbound DMs require an explicit opt-in: set `dmPolicy="open"` and include `"*"` in the channel allowlist (`allowFrom` / `channels.discord.dm.allowFrom` / `channels.slack.dm.allowFrom`).
@@ -116,7 +116,7 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
## Highlights
- **[Local-first Gateway](https://docs.clawd.bot/gateway)** — single control plane for sessions, channels, tools, and events.
-- **[Multi-channel inbox](https://docs.clawd.bot/channels)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, BlueBubbles, Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android.
+- **[Multi-channel inbox](https://docs.clawd.bot/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android.
- **[Multi-agent routing](https://docs.clawd.bot/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
- **[Voice Wake](https://docs.clawd.bot/nodes/voicewake) + [Talk Mode](https://docs.clawd.bot/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs.
- **[Live Canvas](https://docs.clawd.bot/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui).
@@ -138,7 +138,7 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
- [Media pipeline](https://docs.clawd.bot/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawd.bot/nodes/audio).
### Channels
-- [Channels](https://docs.clawd.bot/channels): [WhatsApp](https://docs.clawd.bot/channels/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/channels/telegram) (grammY), [Slack](https://docs.clawd.bot/channels/slack) (Bolt), [Discord](https://docs.clawd.bot/channels/discord) (discord.js), [Signal](https://docs.clawd.bot/channels/signal) (signal-cli), [iMessage](https://docs.clawd.bot/channels/imessage) (imsg), [BlueBubbles](https://docs.clawd.bot/channels/bluebubbles) (extension), [Microsoft Teams](https://docs.clawd.bot/channels/msteams) (extension), [Matrix](https://docs.clawd.bot/channels/matrix) (extension), [Zalo](https://docs.clawd.bot/channels/zalo) (extension), [Zalo Personal](https://docs.clawd.bot/channels/zalouser) (extension), [WebChat](https://docs.clawd.bot/web/webchat).
+- [Channels](https://docs.clawd.bot/channels): [WhatsApp](https://docs.clawd.bot/channels/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/channels/telegram) (grammY), [Slack](https://docs.clawd.bot/channels/slack) (Bolt), [Discord](https://docs.clawd.bot/channels/discord) (discord.js), [Google Chat](https://docs.clawd.bot/channels/googlechat) (Chat API), [Signal](https://docs.clawd.bot/channels/signal) (signal-cli), [iMessage](https://docs.clawd.bot/channels/imessage) (imsg), [BlueBubbles](https://docs.clawd.bot/channels/bluebubbles) (extension), [Microsoft Teams](https://docs.clawd.bot/channels/msteams) (extension), [Matrix](https://docs.clawd.bot/channels/matrix) (extension), [Zalo](https://docs.clawd.bot/channels/zalo) (extension), [Zalo Personal](https://docs.clawd.bot/channels/zalouser) (extension), [WebChat](https://docs.clawd.bot/web/webchat).
- [Group routing](https://docs.clawd.bot/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.clawd.bot/channels).
### Apps + nodes
@@ -169,7 +169,7 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
## How it works (short)
```
-WhatsApp / Telegram / Slack / Discord / Signal / iMessage / BlueBubbles / Microsoft Teams / Matrix / Zalo / Zalo Personal / WebChat
+WhatsApp / Telegram / Slack / Discord / Google Chat / Signal / iMessage / BlueBubbles / Microsoft Teams / Matrix / Zalo / Zalo Personal / WebChat
│
▼
┌───────────────────────────────┐
@@ -252,7 +252,7 @@ ClawdHub is a minimal skill registry. With ClawdHub enabled, the agent can searc
## Chat commands
-Send these in WhatsApp/Telegram/Slack/Microsoft Teams/WebChat (group commands are owner-only):
+Send these in WhatsApp/Telegram/Slack/Google Chat/Microsoft Teams/WebChat (group commands are owner-only):
- `/status` — compact session status (model + tokens, cost when available)
- `/new` or `/reset` — reset the session
diff --git a/apps/macos/Sources/Clawdbot/ChannelsSettings+ChannelState.swift b/apps/macos/Sources/Clawdbot/ChannelsSettings+ChannelState.swift
index 79dd97cf9..efce42781 100644
--- a/apps/macos/Sources/Clawdbot/ChannelsSettings+ChannelState.swift
+++ b/apps/macos/Sources/Clawdbot/ChannelsSettings+ChannelState.swift
@@ -40,6 +40,16 @@ extension ChannelsSettings {
return .orange
}
+ var googlechatTint: Color {
+ guard let status = self.channelStatus("googlechat", as: ChannelsStatusSnapshot.GoogleChatStatus.self)
+ else { return .secondary }
+ if !status.configured { return .secondary }
+ if status.lastError != nil { return .orange }
+ if status.probe?.ok == false { return .orange }
+ if status.running { return .green }
+ return .orange
+ }
+
var signalTint: Color {
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
else { return .secondary }
@@ -85,6 +95,14 @@ extension ChannelsSettings {
return "Configured"
}
+ var googlechatSummary: String {
+ guard let status = self.channelStatus("googlechat", as: ChannelsStatusSnapshot.GoogleChatStatus.self)
+ else { return "Checking…" }
+ if !status.configured { return "Not configured" }
+ if status.running { return "Running" }
+ return "Configured"
+ }
+
var signalSummary: String {
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
else { return "Checking…" }
@@ -193,6 +211,37 @@ extension ChannelsSettings {
return lines.isEmpty ? nil : lines.joined(separator: " · ")
}
+ var googlechatDetails: String? {
+ guard let status = self.channelStatus("googlechat", as: ChannelsStatusSnapshot.GoogleChatStatus.self)
+ else { return nil }
+ var lines: [String] = []
+ if let source = status.credentialSource {
+ lines.append("Credential: \(source)")
+ }
+ if let audienceType = status.audienceType {
+ let audience = status.audience ?? ""
+ let label = audience.isEmpty ? audienceType : "\(audienceType) \(audience)"
+ lines.append("Audience: \(label)")
+ }
+ if let probe = status.probe {
+ if probe.ok {
+ if let elapsed = probe.elapsedMs {
+ lines.append("Probe \(Int(elapsed))ms")
+ }
+ } else {
+ let code = probe.status.map { String($0) } ?? "unknown"
+ lines.append("Probe failed (\(code))")
+ }
+ }
+ if let last = self.date(fromMs: status.lastProbeAt) {
+ lines.append("Last probe \(relativeAge(from: last))")
+ }
+ if let err = status.lastError, !err.isEmpty {
+ lines.append("Error: \(err)")
+ }
+ return lines.isEmpty ? nil : lines.joined(separator: " · ")
+ }
+
var signalDetails: String? {
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
else { return nil }
@@ -244,7 +293,7 @@ extension ChannelsSettings {
}
var orderedChannels: [ChannelItem] {
- let fallback = ["whatsapp", "telegram", "discord", "slack", "signal", "imessage"]
+ let fallback = ["whatsapp", "telegram", "discord", "googlechat", "slack", "signal", "imessage"]
let order = self.store.snapshot?.channelOrder ?? fallback
let channels = order.enumerated().map { index, id in
ChannelItem(
@@ -307,6 +356,8 @@ extension ChannelsSettings {
return self.telegramTint
case "discord":
return self.discordTint
+ case "googlechat":
+ return self.googlechatTint
case "signal":
return self.signalTint
case "imessage":
@@ -326,6 +377,8 @@ extension ChannelsSettings {
return self.telegramSummary
case "discord":
return self.discordSummary
+ case "googlechat":
+ return self.googlechatSummary
case "signal":
return self.signalSummary
case "imessage":
@@ -345,6 +398,8 @@ extension ChannelsSettings {
return self.telegramDetails
case "discord":
return self.discordDetails
+ case "googlechat":
+ return self.googlechatDetails
case "signal":
return self.signalDetails
case "imessage":
@@ -377,6 +432,10 @@ extension ChannelsSettings {
return self
.date(fromMs: self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)?
.lastProbeAt)
+ case "googlechat":
+ return self
+ .date(fromMs: self.channelStatus("googlechat", as: ChannelsStatusSnapshot.GoogleChatStatus.self)?
+ .lastProbeAt)
case "signal":
return self
.date(fromMs: self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)?.lastProbeAt)
@@ -411,6 +470,10 @@ extension ChannelsSettings {
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
else { return false }
return status.lastError?.isEmpty == false || status.probe?.ok == false
+ case "googlechat":
+ guard let status = self.channelStatus("googlechat", as: ChannelsStatusSnapshot.GoogleChatStatus.self)
+ else { return false }
+ return status.lastError?.isEmpty == false || status.probe?.ok == false
case "signal":
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
else { return false }
diff --git a/apps/macos/Sources/Clawdbot/ChannelsStore.swift b/apps/macos/Sources/Clawdbot/ChannelsStore.swift
index e62e737a4..1cbeca381 100644
--- a/apps/macos/Sources/Clawdbot/ChannelsStore.swift
+++ b/apps/macos/Sources/Clawdbot/ChannelsStore.swift
@@ -85,6 +85,28 @@ struct ChannelsStatusSnapshot: Codable {
let lastProbeAt: Double?
}
+ struct GoogleChatProbe: Codable {
+ let ok: Bool
+ let status: Int?
+ let error: String?
+ let elapsedMs: Double?
+ }
+
+ struct GoogleChatStatus: Codable {
+ let configured: Bool
+ let credentialSource: String?
+ let audienceType: String?
+ let audience: String?
+ let webhookPath: String?
+ let webhookUrl: String?
+ let running: Bool
+ let lastStartAt: Double?
+ let lastStopAt: Double?
+ let lastError: String?
+ let probe: GoogleChatProbe?
+ let lastProbeAt: Double?
+ }
+
struct SignalProbe: Codable {
let ok: Bool
let status: Int?
diff --git a/apps/macos/Sources/Clawdbot/GatewayConnection.swift b/apps/macos/Sources/Clawdbot/GatewayConnection.swift
index 9feb98ba9..7facc6d61 100644
--- a/apps/macos/Sources/Clawdbot/GatewayConnection.swift
+++ b/apps/macos/Sources/Clawdbot/GatewayConnection.swift
@@ -11,6 +11,7 @@ enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable {
case whatsapp
case telegram
case discord
+ case googlechat
case slack
case signal
case imessage
diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayAgentChannelTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayAgentChannelTests.swift
index bf72af7e5..5db5476cb 100644
--- a/apps/macos/Tests/ClawdbotIPCTests/GatewayAgentChannelTests.swift
+++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayAgentChannelTests.swift
@@ -11,6 +11,7 @@ import Testing
#expect(GatewayAgentChannel.last.shouldDeliver(true) == true)
#expect(GatewayAgentChannel.whatsapp.shouldDeliver(true) == true)
#expect(GatewayAgentChannel.telegram.shouldDeliver(true) == true)
+ #expect(GatewayAgentChannel.googlechat.shouldDeliver(true) == true)
#expect(GatewayAgentChannel.bluebubbles.shouldDeliver(true) == true)
#expect(GatewayAgentChannel.last.shouldDeliver(false) == false)
}
@@ -19,6 +20,7 @@ import Testing
#expect(GatewayAgentChannel(raw: nil) == .last)
#expect(GatewayAgentChannel(raw: " ") == .last)
#expect(GatewayAgentChannel(raw: "WEBCHAT") == .webchat)
+ #expect(GatewayAgentChannel(raw: "googlechat") == .googlechat)
#expect(GatewayAgentChannel(raw: "BLUEBUBBLES") == .bluebubbles)
#expect(GatewayAgentChannel(raw: "unknown") == .last)
}
diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md
new file mode 100644
index 000000000..fa8529b19
--- /dev/null
+++ b/docs/channels/googlechat.md
@@ -0,0 +1,172 @@
+---
+summary: "Google Chat app support status, capabilities, and configuration"
+read_when:
+ - Working on Google Chat channel features
+---
+# Google Chat (Chat API)
+
+Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only).
+
+## Quick setup (beginner)
+1) Create a Google Cloud project and enable the **Google Chat API**.
+ - Go to: [Google Chat API Credentials](https://console.cloud.google.com/apis/api/chat.googleapis.com/credentials)
+ - Enable the API if it is not already enabled.
+2) Create a **Service Account**:
+ - Press **Create Credentials** > **Service Account**.
+ - Name it whatever you want (e.g., `clawdbot-chat`).
+ - Leave permissions blank (press **Continue**).
+ - Leave principals with access blank (press **Done**).
+3) Create and download the **JSON Key**:
+ - In the list of service accounts, click on the one you just created.
+ - Go to the **Keys** tab.
+ - Click **Add Key** > **Create new key**.
+ - Select **JSON** and press **Create**.
+4) Store the downloaded JSON file on your gateway host (e.g., `~/.clawdbot/googlechat-service-account.json`).
+5) Create a Google Chat app in the [Google Cloud Console Chat Configuration](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat):
+ - Fill in the **Application info**:
+ - **App name**: (e.g. `Clawdbot`)
+ - **Avatar URL**: (e.g. `https://clawd.bot/logo.png`)
+ - **Description**: (e.g. `Personal AI Assistant`)
+ - Enable **Interactive features**.
+ - Under **Functionality**, check **Join spaces and group conversations**.
+ - Under **Connection settings**, select **HTTP endpoint URL**.
+ - Under **Triggers**, select **Use a common HTTP endpoint URL for all triggers** and set it to your gateway's public URL followed by `/googlechat`.
+ - *Tip: Run `clawdbot status` to find your gateway's public URL.*
+ - Under **Visibility**, check **Make this Chat app available to specific people and groups in **.
+ - Enter your email address (e.g. `user@example.com`) in the text box.
+ - Click **Save** at the bottom.
+6) **Enable the app status**:
+ - After saving, **refresh the page**.
+ - Look for the **App status** section (usually near the top or bottom after saving).
+ - Change the status to **Live - available to users**.
+ - Click **Save** again.
+7) Configure Clawdbot with the service account path + webhook audience:
+ - Env: `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=/path/to/service-account.json`
+ - Or config: `channels.googlechat.serviceAccountFile: "/path/to/service-account.json"`.
+8) Set the webhook audience type + value (matches your Chat app config).
+9) Start the gateway. Google Chat will POST to your webhook path.
+
+## Add to Google Chat
+Once the gateway is running and your email is added to the visibility list:
+1) Go to [Google Chat](https://chat.google.com/).
+2) Click the **+** (plus) icon next to **Direct Messages**.
+3) In the search bar (where you usually add people), type the **App name** you configured in the Google Cloud Console.
+ - **Note**: The bot will *not* appear in the "Marketplace" browse list because it is a private app. You must search for it by name.
+4) Select your bot from the results.
+5) Click **Add** or **Chat** to start a 1:1 conversation.
+6) Send "Hello" to trigger the assistant!
+
+## Public URL (Webhook-only)
+Google Chat webhooks require a public HTTPS endpoint. For security, **only expose the `/googlechat` path** to the internet. Keep the Clawdbot dashboard and other sensitive endpoints on your private network.
+
+### Option A: Tailscale Funnel (Recommended)
+If you use Tailscale, you can expose **only** the webhook path using Tailscale Funnel. This keeps your dashboard private while allowing Google Chat to reach your gateway.
+
+1. **Check what address your gateway is bound to:**
+ ```bash
+ ss -tlnp | grep 18789
+ ```
+ Note the IP address (e.g., `127.0.0.1`, `0.0.0.0`, or your Tailscale IP like `100.x.x.x`).
+
+2. **Configure the path mapping** (use the IP from step 1):
+ ```bash
+ # If bound to localhost (127.0.0.1 or 0.0.0.0):
+ tailscale funnel --bg --set-path /googlechat http://127.0.0.1:18789/googlechat
+
+ # If bound to Tailscale IP only (e.g., 100.106.161.80):
+ tailscale funnel --bg --set-path /googlechat http://100.106.161.80:18789/googlechat
+ ```
+
+3. **Authorize the node for Funnel access:**
+ If prompted, visit the authorization URL shown in the output to enable Funnel for this node in your tailnet policy.
+
+4. **Verify the configuration:**
+ ```bash
+ tailscale funnel status
+ ```
+
+Your public webhook URL will be:
+`https://..ts.net/googlechat`
+
+The rest of your gateway (like the dashboard at `/`) remains inaccessible from the public web unless you explicitly add it.
+
+> Note: This configuration persists across reboots. To remove it later, run `tailscale funnel reset`.
+
+### Option B: Reverse Proxy (Caddy)
+If you use a reverse proxy like Caddy, only proxy the specific path:
+```caddy
+your-domain.com {
+ reverse_proxy /googlechat* localhost:18789
+}
+```
+With this config, any request to `your-domain.com/` will be ignored or returned as 404, while `your-domain.com/googlechat` is safely routed to Clawdbot.
+
+### Option C: Cloudflare Tunnel
+Configure your tunnel's ingress rules to only route the webhook path:
+- **Path**: `/googlechat` -> `http://localhost:18789/googlechat`
+- **Default Rule**: HTTP 404 (Not Found)
+
+## How it works
+
+1. Google Chat sends webhook POSTs to the gateway. Each request includes an `Authorization: Bearer ` header.
+2. Clawdbot verifies the token against the configured `audienceType` + `audience`:
+ - `audienceType: "app-url"` → audience is your HTTPS webhook URL.
+ - `audienceType: "project-number"` → audience is the Cloud project number.
+3. Messages are routed by space:
+ - DMs use session key `agent::googlechat:dm:`.
+ - Spaces use session key `agent::googlechat:group:`.
+4. DM access is pairing by default. Unknown senders receive a pairing code; approve with:
+ - `clawdbot pairing approve googlechat `
+5. Group spaces require @-mention by default. Use `botUser` if mention detection needs the app’s user name.
+
+## Targets
+Use these identifiers for delivery and allowlists:
+- Direct messages: `users/` (Clawdbot resolves to a DM space automatically).
+- Spaces: `spaces/`.
+
+## Config highlights
+```json5
+{
+ channels: {
+ "googlechat": {
+ enabled: true,
+ serviceAccountFile: "/path/to/service-account.json",
+ audienceType: "app-url",
+ audience: "https://gateway.example.com/googlechat",
+ webhookPath: "/googlechat",
+ botUser: "users/1234567890", // optional; helps mention detection
+ dm: {
+ policy: "pairing",
+ allowFrom: ["users/1234567890", "name@example.com"]
+ },
+ groupPolicy: "allowlist",
+ groups: {
+ "spaces/AAAA": {
+ allow: true,
+ requireMention: true,
+ users: ["users/1234567890"],
+ systemPrompt: "Short answers only."
+ }
+ },
+ actions: { reactions: true },
+ mediaMaxMb: 20
+ }
+ }
+}
+```
+
+Notes:
+- Service account credentials can also be passed inline with `serviceAccount` (JSON string).
+- Default webhook path is `/googlechat` if `webhookPath` isn’t set.
+- Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled.
+- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
+
+## Troubleshooting
+- Check `clawdbot channels status --probe` for auth errors or missing audience config.
+- If no messages arrive, confirm the Chat app’s webhook URL + event subscriptions.
+- If mention gating blocks replies, set `botUser` to the app’s user resource name and verify `requireMention`.
+
+Related docs:
+- [Gateway configuration](/gateway/configuration)
+- [Security](/gateway/security)
+- [Reactions](/tools/reactions)
diff --git a/docs/channels/index.md b/docs/channels/index.md
index f8fd860c3..ee8a281d1 100644
--- a/docs/channels/index.md
+++ b/docs/channels/index.md
@@ -15,6 +15,7 @@ Text is supported everywhere; media and reactions vary by channel.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
+- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
- [Signal](/channels/signal) — signal-cli; privacy-focused.
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
diff --git a/docs/cli/channels.md b/docs/cli/channels.md
index 2fe9e90df..dcb95b0f7 100644
--- a/docs/cli/channels.md
+++ b/docs/cli/channels.md
@@ -1,7 +1,7 @@
---
summary: "CLI reference for `clawdbot channels` (accounts, status, login/logout, logs)"
read_when:
- - You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage)
+ - You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage)
- You want to check channel status or tail channel logs
---
diff --git a/docs/cli/index.md b/docs/cli/index.md
index ce1c619d5..d10942dc8 100644
--- a/docs/cli/index.md
+++ b/docs/cli/index.md
@@ -355,7 +355,7 @@ Options:
## Channel helpers
### `channels`
-Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams).
+Manage chat channel accounts (WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams).
Subcommands:
- `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included).
@@ -368,7 +368,7 @@ Subcommands:
- `channels logout`: log out of a channel session (if supported).
Common options:
-- `--channel `: `whatsapp|telegram|discord|slack|mattermost|signal|imessage|msteams`
+- `--channel `: `whatsapp|telegram|discord|googlechat|slack|mattermost|signal|imessage|msteams`
- `--account `: channel account id (default `default`)
- `--name