What & why (CUS-240)
Under user OAuth, Google Chat populates only name (users/{id}) and type on User objects — displayName is always empty. So list_members / get_member return bare numeric ids instead of people. This PR fills displayName best-effort by resolving those ids against the People API (people.getBatchGet) using the connection's directory.readonly scope.
This is a members-only first cut (per scoping decision). Reactions, message senders, and space-event users can follow in a second PR.
⚠️ Empirical findings reviewers must weigh (this is why it's a Draft)
I tested this end-to-end against a real Workspace (palash@composio.dev, People API enabled, directory.readonly granted):
- ✅
displayName is always empty under user auth — verified live, 14/14 human members returned name+type only. Matches the doc note: "if your Chat app authenticates as a user, the output for a User resource only populates name and type."
- ⚠️ Resolution is gated on a customer admin setting, and it's commonly OFF. Resolving the 14 ids via
getBatchGet + directory.readonly returned 1/10 — and the one was the requester's own profile. The other 9 came back 200 with a found DOMAIN_PROFILE but no names (withheld). Cause: the Workspace admin's directory/profile sharing setting — Composio's own org has it off.
- There is no regular-user workaround: reading coworker names regardless of the setting needs the Admin SDK (
admin.directory.user.readonly, admin-consent), which breaks the "user-consented scope" premise.
Implication: this feature delivers names only for customers whose admin enabled directory sharing — frequently not the case, especially for the security-conscious enterprises most likely to request it. It degrades gracefully (leaves displayName empty = same as today) but before merging, confirm the CUS-240 requester's org actually has directory sharing on — otherwise it's a no-op for them.
What this PR does
apps/google_chat/common/enrich.py — shared, best-effort helper. Collects HUMAN users with empty displayName, pre-filters BOTs, dedupes ids, batch-resolves via getBatchGet (≤200/call), selects the primary name entry (never names[0]), writes back. Never raises — enrichment must not break the host action.
get_member.py / list_members.py — one-line hook before _validate_response, plus directory.readonly added to _scopes.
- Mirrors the existing cross-service pattern in
gmail/search_people.py and reuses the getBatchGet shape from googlecontacts/batch_get_people.py.
- Tests:
tests/test_apps/test_google_chat_enrich.py (primary-name selection, dedupe, BOT/pre-named skip, unresolved-left-empty, never-raises). 4/4 pass.
Deliberately NOT included
notices[] — left out; an empty displayName on a HUMAN already signals "unresolved." (And unresolved is the common case, so a structured notice is debatable.)
config.ts default_scopes change — kept the scope in the actions' _scopes only.
Open questions for review
- Scope placement.
directory.readonly is in the actions' flat _scopes. _optional_scopes exists but is unused/not pushed, so there's no "requested-but-optional" path today. Is _scopes right, or should this also/instead live in config.ts default_scopes? Either way it triggers re-consent for existing connections.
- Worth shipping given the admin-sharing dependency? See findings above.
- Breadth — extend to reactions / message senders / space-event users next?
Unrelated bug found
tools.proxy (proxy execute) silently drops repeated query params passed via parameters — only 1 of 10 resourceNames got through until embedded in the URL. Production actions use http_request (list params work there), so this PR is unaffected, but the proxy bug is worth a separate issue.
🤖 Generated with Claude Code