diff --git a/src/gaia/connectors/formatting.py b/src/gaia/connectors/formatting.py index fc58363f6..edb7dd13f 100644 --- a/src/gaia/connectors/formatting.py +++ b/src/gaia/connectors/formatting.py @@ -30,14 +30,20 @@ ) # Per-agent override messages for ``AGENT_NOT_GRANTED``. Keyed by -# ``namespaced_agent_id`` (matches the ``agent_id`` field on -# ``AuthRequiredError``). Used to surface a more specific upgrade-path -# message when an existing user's grant predates a scope addition (e.g. a -# user who connected Google before #962 has no ``gmail.modify`` grant; the -# first organize/trash/send tool call raises ``AGENT_NOT_GRANTED`` and we -# want them to see exactly how to fix it). -_AGENT_GRANT_MIGRATION_MESSAGES: Dict[str, str] = { - "installed:email": ( +# ``(namespaced_agent_id, provider)`` (matching the ``agent_id`` and +# ``provider`` fields on ``AuthRequiredError``). Used to surface a more +# specific upgrade-path message when an existing user's grant predates a +# scope addition (e.g. a user who connected Google before #962 has no +# ``gmail.modify`` grant; the first organize/trash/send tool call raises +# ``AGENT_NOT_GRANTED`` and we want them to see exactly how to fix it). +# +# The provider is part of the key so a per-provider override never shadows +# another provider's failure: the email agent now serves both Google and +# Microsoft mailboxes, and a Microsoft grant gap must NOT show Google +# "Reconnect" instructions. Providers without a tailored entry fall through +# to the generic provider-aware branch below. +_AGENT_GRANT_MIGRATION_MESSAGES: Dict[tuple[str, str], str] = { + ("installed:email", "google"): ( "Email agent needs additional Google permissions " "(gmail.modify, gmail.send, calendar.events). " "Open Settings → Connectors → Google → Reconnect to grant the " @@ -53,13 +59,17 @@ def format_connector_error(e: BaseException) -> str: Settings → Connections (``AGENT_NOT_GRANTED`` and ``NOT_CONNECTED``) are surfaced explicitly so agents can tell the user where to go. - For agents registered in ``_AGENT_GRANT_MIGRATION_MESSAGES``, the - ``AGENT_NOT_GRANTED`` reason returns the agent-specific upgrade - message instead of the generic one. + For ``(agent_id, provider)`` pairs registered in + ``_AGENT_GRANT_MIGRATION_MESSAGES``, the ``AGENT_NOT_GRANTED`` reason + returns the agent-specific upgrade message instead of the generic one; + any provider without a tailored entry falls through to the generic + provider-aware message. """ if isinstance(e, AuthRequiredError): if e.reason is AuthRequiredError.Reason.AGENT_NOT_GRANTED: - override = _AGENT_GRANT_MIGRATION_MESSAGES.get(e.agent_id or "") + override = _AGENT_GRANT_MIGRATION_MESSAGES.get( + (e.agent_id or "", e.provider or "") + ) if override: return override scopes = ", ".join(e.missing_scopes) or "(none reported)" diff --git a/tests/unit/connectors/test_formatting.py b/tests/unit/connectors/test_formatting.py index 13bd14601..8be952c66 100644 --- a/tests/unit/connectors/test_formatting.py +++ b/tests/unit/connectors/test_formatting.py @@ -88,6 +88,30 @@ def test_format_connector_error_email_agent_grant_migration(): assert "Reconnect" in msg +def test_format_connector_error_email_agent_microsoft_falls_through(): + """ + A Microsoft grant gap for ``installed:email`` must NOT show the Google + "Reconnect" migration text (issue #1751). The override is keyed by + ``(agent_id, provider)``, so a Microsoft failure falls through to the + generic provider-aware branch — naming Microsoft and the missing Graph + scopes, not Google. + """ + err = AuthRequiredError( + AuthRequiredError.Reason.AGENT_NOT_GRANTED, + provider="microsoft", + agent_id="installed:email", + missing_scopes=["Mail.ReadWrite", "Mail.Send"], + ) + msg = format_connector_error(err) + assert "AGENT_NOT_GRANTED" in msg + assert "microsoft" in msg + assert "Mail.ReadWrite" in msg + assert "Mail.Send" in msg + # The Google migration string must not leak onto a Microsoft failure. + assert "Google" not in msg + assert "gmail.modify" not in msg + + def test_format_connector_error_unknown_agent_falls_back_to_generic(): """An agent without a registered override gets the generic message.""" err = AuthRequiredError(