From 5991bed32e67f57c8dfcfb71d5371262ce27f1eb Mon Sep 17 00:00:00 2001 From: iHildy Date: Sat, 24 Jan 2026 01:12:48 +0000 Subject: [PATCH] feat(googlechat): support Google Workspace Add-on event format --- docs/channels/googlechat.md | 37 +++++++++++++++++- extensions/googlechat/src/auth.ts | 7 +++- extensions/googlechat/src/monitor.ts | 57 +++++++++++++++++++++++----- pnpm-lock.yaml | 6 +-- 4 files changed, 91 insertions(+), 16 deletions(-) diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md index fa8529b19..f8886b63d 100644 --- a/docs/channels/googlechat.md +++ b/docs/channels/googlechat.md @@ -162,9 +162,42 @@ Notes: - Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`). ## Troubleshooting + +### 405 Method Not Allowed +If Google Cloud Logs Explorer shows errors like: +``` +status code: 405, reason phrase: HTTP error response: HTTP/1.1 405 Method Not Allowed +``` + +This means the webhook handler isn't registered. Common causes: +1. **Channel not configured**: The `channels.googlechat` section is missing from your config. Verify with: + ```bash + clawdbot config get channels.googlechat + ``` + If it returns "Config path not found", add the configuration (see [Config highlights](#config-highlights)). + +2. **Plugin not enabled**: Check plugin status: + ```bash + clawdbot plugins list | grep googlechat + ``` + If it shows "disabled", add `plugins.entries.googlechat.enabled: true` to your config. + +3. **Gateway not restarted**: After adding config, restart the gateway: + ```bash + clawdbot gateway restart + ``` + +Verify the channel is running: +```bash +clawdbot channels status +# Should show: Google Chat default: enabled, configured, ... +``` + +### Other issues - 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`. +- 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`. +- Use `clawdbot logs --follow` while sending a test message to see if requests reach the gateway. Related docs: - [Gateway configuration](/gateway/configuration) diff --git a/extensions/googlechat/src/auth.ts b/extensions/googlechat/src/auth.ts index 4d1b7281d..681ea6c22 100644 --- a/extensions/googlechat/src/auth.ts +++ b/extensions/googlechat/src/auth.ts @@ -4,6 +4,8 @@ import type { ResolvedGoogleChatAccount } from "./accounts.js"; const CHAT_SCOPE = "https://www.googleapis.com/auth/chat.bot"; const CHAT_ISSUER = "chat@system.gserviceaccount.com"; +// Google Workspace Add-ons use a different service account pattern +const ADDON_ISSUER_PATTERN = /^service-\d+@gcp-sa-gsuiteaddons\.iam\.gserviceaccount\.com$/; const CHAT_CERTS_URL = "https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com"; @@ -87,8 +89,9 @@ export async function verifyGoogleChatRequest(params: { audience, }); const payload = ticket.getPayload(); - const ok = Boolean(payload?.email_verified) && payload?.email === CHAT_ISSUER; - return ok ? { ok: true } : { ok: false, reason: "invalid issuer" }; + const email = payload?.email ?? ""; + const ok = payload?.email_verified && (email === CHAT_ISSUER || ADDON_ISSUER_PATTERN.test(email)); + return ok ? { ok: true } : { ok: false, reason: `invalid issuer: ${email}` }; } catch (err) { return { ok: false, reason: err instanceof Error ? err.message : "invalid token" }; } diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index ac22ad827..d9ae70b13 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -12,7 +12,14 @@ import { } from "./api.js"; import { verifyGoogleChatRequest, type GoogleChatAudienceType } from "./auth.js"; import { getGoogleChatRuntime } from "./runtime.js"; -import type { GoogleChatAnnotation, GoogleChatAttachment, GoogleChatEvent } from "./types.js"; +import type { + GoogleChatAnnotation, + GoogleChatAttachment, + GoogleChatEvent, + GoogleChatSpace, + GoogleChatMessage, + GoogleChatUser, +} from "./types.js"; export type GoogleChatRuntimeEnv = { log?: (message: string) => void; @@ -168,44 +175,76 @@ export async function handleGoogleChatWebhookRequest( return true; } - const raw = body.value; + let raw = body.value; if (!raw || typeof raw !== "object" || Array.isArray(raw)) { res.statusCode = 400; res.end("invalid payload"); return true; } - const eventType = (raw as { type?: string; eventType?: string }).type ?? - (raw as { type?: string; eventType?: string }).eventType; + // Transform Google Workspace Add-on format to standard Chat API format + const rawObj = raw as { + commonEventObject?: { hostApp?: string }; + chat?: { + messagePayload?: { space?: GoogleChatSpace; message?: GoogleChatMessage }; + user?: GoogleChatUser; + eventTime?: string; + }; + authorizationEventObject?: { systemIdToken?: string }; + }; + + if (rawObj.commonEventObject?.hostApp === "CHAT" && rawObj.chat?.messagePayload) { + const chat = rawObj.chat; + const messagePayload = chat.messagePayload; + raw = { + type: "MESSAGE", + space: messagePayload?.space, + message: messagePayload?.message, + user: chat.user, + eventTime: chat.eventTime, + }; + + // For Add-ons, the bearer token may be in authorizationEventObject.systemIdToken + const systemIdToken = rawObj.authorizationEventObject?.systemIdToken; + if (!bearer && systemIdToken) { + Object.assign(req.headers, { authorization: `Bearer ${systemIdToken}` }); + } + } + + const event = raw as GoogleChatEvent; + const eventType = event.type ?? (raw as { eventType?: string }).eventType; if (typeof eventType !== "string") { res.statusCode = 400; res.end("invalid payload"); return true; } - const rawObj = raw as Record; - if (!rawObj.space || typeof rawObj.space !== "object" || Array.isArray(rawObj.space)) { + if (!event.space || typeof event.space !== "object" || Array.isArray(event.space)) { res.statusCode = 400; res.end("invalid payload"); return true; } if (eventType === "MESSAGE") { - if (!rawObj.message || typeof rawObj.message !== "object" || Array.isArray(rawObj.message)) { + if (!event.message || typeof event.message !== "object" || Array.isArray(event.message)) { res.statusCode = 400; res.end("invalid payload"); return true; } } - const event = raw as GoogleChatEvent; + // Re-extract bearer in case it was updated from Add-on format + const authHeaderNow = String(req.headers.authorization ?? ""); + const effectiveBearer = authHeaderNow.toLowerCase().startsWith("bearer ") + ? authHeaderNow.slice("bearer ".length) + : bearer; let selected: WebhookTarget | undefined; for (const target of targets) { const audienceType = target.audienceType; const audience = target.audience; const verification = await verifyGoogleChatRequest({ - bearer, + bearer: effectiveBearer, audienceType, audience, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d6838e55..0df307cd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -300,7 +300,9 @@ importers: extensions/google-antigravity-auth: {} - extensions/google-chat: + extensions/google-gemini-cli-auth: {} + + extensions/googlechat: dependencies: clawdbot: specifier: workspace:* @@ -309,8 +311,6 @@ importers: specifier: ^10.5.0 version: 10.5.0 - extensions/google-gemini-cli-auth: {} - extensions/imessage: {} extensions/llm-task: {}