Skip to content

fix(gateway/googlechat): handle HTTP endpoint URL events + correct JWT signer email#909

Merged
thepagent merged 2 commits into
openabdev:mainfrom
sebastian-hsu:fix/googlechat-http-endpoint-url
May 24, 2026
Merged

fix(gateway/googlechat): handle HTTP endpoint URL events + correct JWT signer email#909
thepagent merged 2 commits into
openabdev:mainfrom
sebastian-hsu:fix/googlechat-http-endpoint-url

Conversation

@sebastian-hsu
Copy link
Copy Markdown
Contributor

@sebastian-hsu sebastian-hsu commented May 23, 2026

What problem does this solve?

Following docs/google-chat.md end-to-end produces a 100% non-working
Google Chat bot. Two independent bugs in gateway/src/adapters/googlechat.rs
make the documented HTTP endpoint URL connection mode unusable:

  1. Envelope schema mismatchGoogleChatEnvelope only deserializes a
    chat-wrapped payload (Pub/Sub style), but HTTP endpoint URL connections
    deliver a top-level shape (message, user, space, thread,
    common). All real webhooks silently drop at envelope.chat is None
    with a 200 response, so Google considers them "delivered" but no event
    reaches the agent.

  2. JWT email allow-list incorrectGOOGLE_CHAT_EMAIL_SUFFIX requires
    @gcp-sa-gsuiteaddons.iam.gserviceaccount.com (Workspace Add-ons
    signer), but Google Chat HTTP webhooks are signed as
    chat@system.gserviceaccount.com. Setting GOOGLE_CHAT_AUDIENCE per
    docs returns 401 to every webhook.

Together, a fresh setup yields "OpenAB not responding" (audience set) or
zero replies (audience unset).

Closes #899

Discord Discussion URL: N/A — bug discovered during personal Google
Workspace setup; reproduction details and captured webhook body are in
issue #899.

At a Glance

Before:
  Google Chat ──HTTPS POST──▶ gateway
                                │
                                ├─ JWT verify (Bug 2)
                                │   expected: *@gcp-sa-gsuiteaddons...
                                │   got:      chat@system.gserviceaccount.com
                                │   → 401
                                │
                                └─ (audience unset) envelope.chat is None (Bug 1)
                                   → silent 200 drop
  Result: bot never replies

After:
  Google Chat ──HTTPS POST──▶ gateway
                                │
                                ├─ JWT verify
                                │   accept: chat@system.gserviceaccount.com → pass
                                │
                                └─ envelope parse
                                   try wrapped (chat.messagePayload.message),
                                   fall back to top-level (message, user, space)
                                   → event forwarded to OAB → reply via Chat API
  Result: bot replies as expected

Prior Art & Industry Research

OpenClaw: No Google Chat adapter; not applicable.

Hermes Agent: No Google Chat adapter; not applicable.

Other references:

Proposed Solution

Bug 1 — envelope schema:

  • Extend GoogleChatEnvelope with optional top-level message, user,
    space fields.
  • In webhook(), prefer the wrapped path; fall back to top-level when
    envelope.chat is None.
  • Both formats now parse; existing tests covering the wrapped schema
    remain unchanged and still pass.

Bug 2 — JWT email allow-list:

  • Rename GOOGLE_CHAT_EMAIL_SUFFIXGOOGLE_CHAT_SIGNER_EMAIL and set
    the value to chat@system.gserviceaccount.com (Google Chat's actual
    signer for HTTP endpoint URL mode).
  • Tighten the check from ends_with (suffix match) to exact equality.
  • Update the existing email_claim_accepts_* test to assert the new
    signer.

Why this approach?

Schema fallback rather than format-detection by an env var:

  • Both formats are unambiguously distinguishable structurally (presence
    of the chat field), so serde + Option gives us a zero-cost branch
    with no new config surface.
  • Existing Pub/Sub deployments (if any) keep working with zero changes.

For Bug 2, narrowing the email check rather than removing JWT verification
preserves the security posture (proves the request came from Google Chat
specifically, not just any Google service) while matching reality.

Alternatives Considered

  1. GOOGLE_CHAT_EVENT_FORMAT=wrapped|toplevel env var — explicit
    but adds config burden; users would need to know which mode their
    Chat App uses. The schema fallback is zero-config.
  2. Drop JWT verification entirely — what current docs label
    "insecure (local development only)". Bad default; anyone who learns
    the webhook URL can inject events.
  3. Accept any @system.gserviceaccount.com — broader than needed,
    could let other Google internal services spoof Chat events. Narrow
    to chat@system.gserviceaccount.com.
  4. Update docs to recommend Cloud Pub/Sub mode — pushes a different
    deployment model on every user; HTTP endpoint URL is what
    docs/google-chat.md currently recommends and what current users
    follow.

Validation

  • cargo check --release passes (fresh build on rust:1-bookworm)
  • cargo test --release --package openab-gateway170 passed, 0 failed
    (existing wrapped-shape tests untouched; updated email-claim test asserts new signer)
  • Manual end-to-end (production K3s cluster, Cloudflare Tunnel sidecar):
    • Patched binary running in production cc-agent pod
    • 1:1 DM "test" → gateway: googlechat webhook received → googlechat → gateway → OAB spawn → cc-agent reply via Chat API → reply visible in Chat client
    • Space @mention → same flow, reply visible in thread
  • No silent drops or JWT email mismatch warnings in gateway log after the change

🤖 Generated with Claude Code

https://discord.com/channels/1491295327620169908/1491365158868619404/1507822388883230721

…T signer email

Two independent bugs prevent the Google Chat adapter from working with the
connection mode recommended by docs/google-chat.md:

1. Envelope schema only deserialized the Pub/Sub-wrapped shape, but HTTP
   endpoint URL connections deliver top-level fields (message, user, space).
   All real webhooks silently dropped at `envelope.chat is None` with a
   200 response.

2. JWT email allow-list expected @gcp-sa-gsuiteaddons.iam.gserviceaccount.com
   (Workspace Add-ons signer), but Google Chat HTTP webhooks are signed by
   chat@system.gserviceaccount.com. Setting GOOGLE_CHAT_AUDIENCE per docs
   returned 401 to every webhook.

Together, following the docs produced a 100% non-working bot.

This change:
- Extends GoogleChatEnvelope with optional top-level fields and adds a
  fallback branch in the webhook handler; existing wrapped-shape tests
  continue to pass unchanged.
- Renames GOOGLE_CHAT_EMAIL_SUFFIX → GOOGLE_CHAT_SIGNER_EMAIL, changes
  the value to chat@system.gserviceaccount.com, and tightens the check
  from `ends_with` to exact equality.
- Updates the existing email-claim test to assert the new signer.

Verified end-to-end on a production cc-agent deployment (1:1 DM + Space
@mention) via Cloudflare Tunnel sidecar; gateway forwards events to OAB
and replies arrive in Chat.

Closes openabdev#899

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sebastian-hsu sebastian-hsu requested a review from thepagent as a code owner May 23, 2026 03:31
@github-actions github-actions Bot added the closing-soon PR missing Discord Discussion URL — will auto-close in 3 days label May 23, 2026
@shaun-agent
Copy link
Copy Markdown
Contributor

shaun-agent commented May 23, 2026

OpenAB PR Screening

This is auto-generated by the OpenAB project-screening flow for context collection and reviewer handoff.
Click 👍 if you find this useful. Human review will be done within 24 hours. We appreciate your support and contribution 🙏

Screening report done.

GitHub comment: #909 (comment)
Project action: moved PVTI_lADOEFbZWM4BUUALzgtljcw to PR-Screening in https://github.com/orgs/openabdev/projects/1

Intent

PR #909 fixes the documented Google Chat HTTP endpoint URL mode, which currently either 401s every webhook or silently 200-drops valid events.

Feat

Fix. Updates gateway/src/adapters/googlechat.rs to accept top-level HTTP event envelopes and validate the actual signer email, chat@system.gserviceaccount.com.

Who It Serves

Deployers and agent runtime operators using Google Chat.

Rewritten Prompt

Fix the Google Chat gateway adapter so HTTP endpoint URL mode works end to end. Support both Pub/Sub wrapped payloads and top-level HTTP payloads, preserve audience verification, require the correct signer email, and add regression tests for both envelope formats and signer validation.

Merge Pitch

This should move forward. The bug breaks a documented setup path completely, the diff is narrow, and reported validation includes gateway tests plus manual Google Chat E2E checks. Main reviewer concern: env var compatibility around GOOGLE_CHAT_EMAIL_SUFFIX -> GOOGLE_CHAT_SIGNER_EMAIL.

Best-Practice Comparison

OpenClaw and Hermes Agent are not directly applicable; this is adapter-boundary correctness, not scheduling or durable execution. The relevant principle is avoiding silent drops at external delivery boundaries.

Implementation Options

  1. Conservative: fix only HTTP top-level shape and signer.
  2. Balanced: support both wrapped and top-level shapes with structural fallback, exact signer validation, and focused tests.
  3. Ambitious: add explicit Google Chat delivery modes, diagnostics, docs split, and compatibility migration.

Comparison Table

Option Speed Complexity Reliability Maintainability User Impact Fit
Conservative High Low Medium Medium High Acceptable
Balanced High Low-Medium High High High Best
Ambitious Low High High Medium-High High Follow-up

Recommendation

Advance the balanced path. Before merge, review signer compatibility, old env var behavior, and regression coverage for both payload shapes.

@github-actions github-actions Bot removed the closing-soon PR missing Discord Discussion URL — will auto-close in 3 days label May 23, 2026
@chaodu-agent
Copy link
Copy Markdown
Collaborator

LGTM ✅ — Both bugs fixed correctly; unit test coverage added for the new code path.

What This PR Does

Fixes two independent bugs that made Google Chat HTTP endpoint URL mode completely non-functional: (1) envelope parsing only recognized Pub/Sub wrapped format, silently dropping all HTTP endpoint URL webhooks; (2) JWT email allow-list referenced the wrong signer, rejecting all valid requests.

How It Works

  • Envelope: Adds optional top-level message/user/space fields to GoogleChatEnvelope. Handler prefers the wrapped (chat.messagePayload) path and falls back to top-level when chat is absent. Zero-config, structurally unambiguous.
  • JWT email: Narrows from suffix match (*@gcp-sa-gsuiteaddons...) to exact match (chat@system.gserviceaccount.com). This is correct because Pub/Sub mode uses subscription-level auth, not HTTP bearer JWT — the old suffix was wrong for both modes.

四問 (Four Questions)

Question Answer
What problem? HTTP endpoint URL mode 100% broken — bot never replies (silent 200 drop or 401)
How solved? Envelope fallback + JWT signer correction
Alternatives considered? Env var format switch (config burden), drop JWT (insecure), broader suffix (over-permissive), change docs to Pub/Sub (wrong default) — all rejected with good rationale
Best approach? Yes — zero-config, backward-compat with Pub/Sub deployments, tightens security posture

Findings

# Severity Finding Location
1 🟢 Envelope fallback is zero-config, structurally unambiguous (chat field presence) googlechat.rs:495-510
2 🟢 JWT exact match is correct — Pub/Sub uses subscription auth, not this path googlechat.rs:146-157
3 🟢 sender/space fallback chain consistent across both paths googlechat.rs:525-526
4 🟢 Unit test added for HTTP endpoint URL top-level envelope parsing (commit db3fcc3) googlechat.rs:2444-2471
Baseline Check
  • PR opened: 2026-05-23
  • Main already has: Google Chat adapter with Pub/Sub wrapped envelope support
  • Net-new value: HTTP endpoint URL mode support + correct JWT signer
Reviewers
  • 超渡法師 (coordinator): LGTM ✅
  • 普渡法師: LGTM ✅ (identified missing test → fixed)
  • 覺渡法師: not reviewed (rate limited)

@thepagent thepagent merged commit 15dc2cc into openabdev:main May 24, 2026
5 checks passed
@canyugs
Copy link
Copy Markdown
Contributor

canyugs commented May 24, 2026

Hi @sebastian-hsu — thanks for digging into this. I reproduced PR #909 end-to-end in a separate Google Workspace, and want to flag findings + propose a small change before this merges.

TL;DR: the envelope fix is solid; the signer change as written breaks every existing deployment that PR #718 was built against. There are (at least) two legitimate Chat App configurations in the wild that use different signer/envelope combinations, so the signer check needs to be an env-var allow-list, not a single hardcoded value.

Findings

Two distinct, both-official Chat App configurations produce different signer + envelope combinations:

Configuration Envelope Signer (JWT email claim)
A. Registered via Workspace Add-ons SDK (or otherwise triggers gsuiteaddons.googleapis.com) wrapped (chat.messagePayload.message) service-{PROJECT_NUMBER}@gcp-sa-gsuiteaddons.iam.gserviceaccount.com
B. Registered via Google Chat API → Configuration directly top-level (message, user, space) chat@system.gserviceaccount.com

Config A is what PR #718 was built and tested against — the original ends_with("@gcp-sa-gsuiteaddons.iam.gserviceaccount.com") check accepts any project-scoped variant of this signer.

Config B is what PR #909 targets.

PR #909 changes the check from a suffix match (ends_with) to an exact match against chat@system.gserviceaccount.com, which silently breaks every Config A deployment.

Evidence

Proposal

Replace the hardcoded const with an env-var allow-list that ships with sane defaults covering both configurations:

const DEFAULT_SIGNERS: &str = "chat@system.gserviceaccount.com,\
                               *@gcp-sa-gsuiteaddons.iam.gserviceaccount.com";

// GOOGLE_CHAT_SIGNERS: comma-separated list, supports exact match or `*@suffix` glob.
fn signer_allowlist() -> Vec<String> {
    std::env::var("GOOGLE_CHAT_SIGNERS")
        .unwrap_or_else(|_| DEFAULT_SIGNERS.into())
        .split(',')
        .map(|s| s.trim().to_owned())
        .filter(|s| !s.is_empty())
        .collect()
}

Both known-good signers ship in the default; future Google additions can be configured without a code change.

Asks

  1. Could you share your environment details? This helps us pin down the exact rule that selects between Config A and B, so we can document it:

    • Workspace tier (Business / Enterprise / Individual / Education)?
    • Was your Chat App registered via Google Chat API → Configuration only, or did you also configure anything via the Workspace Add-ons SDK or the Marketplace SDK?
    • In APIs & Services → Enabled APIs, is gsuiteaddons.googleapis.com enabled in your GCP project?
    • Authentication Audience setting on the Chat API Configuration page: HTTP endpoint URL or Project Number?
  2. The "Other references" link returns 404:

    https://developers.google.com/workspace/chat/format-events
    

    Could you share the URL you actually read? I tried a few redirect paths but couldn't recover the page documenting the top-level envelope + chat@system.gserviceaccount.com claim.

Happy to push the allow-list change as a follow-up commit on this PR or as a separate PR — whichever you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(gateway/googlechat): adapter silently drops events from docs-recommended HTTP endpoint URL setup

5 participants