from __future__ import annotations import re from typing import Any def run_diagnostics(config: dict[str, Any] | None) -> list[dict[str, Any]]: if not config: return [] issues: list[dict[str, Any]] = [] issues.extend(_check_mutable_allowlist(config)) issues.extend(_check_deprecated_formats(config)) issues.extend(_check_legacy_stream_mode(config)) issues.extend(_check_credential_readiness(config)) return issues def _check_mutable_allowlist(config: dict[str, Any]) -> list[dict[str, Any]]: issues: list[dict[str, Any]] = [] allow_from = _normalize_allowlist(config.get("allowFrom", config.get("allow_from", []))) for entry in allow_from: if "@" in entry and not _is_users_prefix(entry): issues.append( { "severity": "warning", "category": "mutable_allowlist", "field": "allowFrom", "value": entry, "message": ( f"allowFrom contains email '{entry}' without 'users/' prefix. " "Email-based allowlists are mutable; consider migrating to 'users/' format" ), } ) group_allow_from = _normalize_allowlist(config.get("groupAllowFrom", config.get("group_allow_from", []))) for entry in group_allow_from: if "@" in entry and not entry.startswith("spaces/"): issues.append( { "severity": "warning", "category": "mutable_allowlist", "field": "groupAllowFrom", "value": entry, "message": ( f"groupAllowFrom contains email '{entry}'. " "Group allowlists should use 'spaces/' format as emails are mutable" ), } ) return issues def _check_deprecated_formats(config: dict[str, Any]) -> list[dict[str, Any]]: issues: list[dict[str, Any]] = [] allow_from = _normalize_allowlist(config.get("allowFrom", config.get("allow_from", []))) for entry in allow_from: if _is_users_prefix_with_email_legacy(entry): issues.append( { "severity": "warning", "category": "deprecated_format", "field": "allowFrom", "value": entry, "message": ( f"allowFrom entry '{entry}' uses deprecated format. " "Remove the trailing '/' portion: use 'users/' instead" ), } ) if config.get("streamMode"): issues.append( { "severity": "info", "category": "deprecated_config", "field": "streamMode", "value": config["streamMode"], "message": ( "streamMode config is deprecated; " "streaming is configured via 'supports_streaming' capability" ), } ) if config.get("enableThreadReply"): issues.append( { "severity": "info", "category": "deprecated_config", "field": "enableThreadReply", "value": config["enableThreadReply"], "message": "enableThreadReply is deprecated; use replyToMode instead", } ) return issues def _check_legacy_stream_mode(config: dict[str, Any]) -> list[dict[str, Any]]: issues: list[dict[str, Any]] = [] if config.get("streamMode"): issues.append( { "severity": "info", "category": "legacy_config", "field": "streamMode", "value": config["streamMode"], "message": "Remove 'streamMode' config key; streaming is now controlled by capability flags", } ) return issues def _check_credential_readiness(config: dict[str, Any]) -> list[dict[str, Any]]: issues: list[dict[str, Any]] = [] has_inline = bool(config.get("service_account")) has_file = bool(config.get("service_account_file")) has_secret_ref = bool(config.get("serviceAccountRef", config.get("service_account_ref"))) has_env = bool( __import__("os").getenv("GOOGLE_CHAT_SERVICE_ACCOUNT") or __import__("os").getenv("GOOGLE_SERVICE_ACCOUNT_FILE") or __import__("os").getenv("GOOGLE_CHAT_SERVICE_ACCOUNT_FILE") ) if not (has_inline or has_file or has_secret_ref or has_env): issues.append( { "severity": "error", "category": "credential_readiness", "field": "serviceAccount", "message": ( "No Google Chat service account configured. " "Provide one via service_account, service_account_file, " "serviceAccountRef, or environment variables" ), } ) return issues def _normalize_allowlist(raw: Any) -> list[str]: if isinstance(raw, str): return [x.strip() for x in raw.split(",") if x.strip()] if isinstance(raw, (list, tuple)): return [str(x).strip() for x in raw if x] return [] def _is_users_prefix(entry: str) -> bool: return entry.startswith("users/") or entry.startswith("user:") def _is_users_prefix_with_email_legacy(entry: str) -> bool: if not entry.startswith("users/"): return False inner = entry.removeprefix("users/") return bool(re.match(r"^[\w.+-]+@[\w-]+\.[\w.-]+/", inner))