Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions src/gaia/connectors/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand All @@ -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)"
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/connectors/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down