From fef100d2c185b20b286389691424b3b2eb832753 Mon Sep 17 00:00:00 2001 From: witbrock Date: Sun, 14 Jun 2026 08:14:08 -0400 Subject: [PATCH] Enforce authoritative Gmail profile over model-supplied profile in tool calls apply_runtime_defaults_to_mcp_payload only filled a blank or "default"/"primary" placeholder profile for gmail_* tools, so an explicit profile the MODEL chose passed straight through. Observed live: qwen3:8b emitted gmail_list_messages with profile="zhan-gmail" (an account whose OAuth refresh token is expired/revoked) even though the valid "vonwitbrock-gmail" profile was selected and tests green in the settings UI. The call hard-failed with invalid_grant and the whole turn failed. Which Gmail account to use is an identity/credential decision the caller's selection owns, not the model. Enforce the authoritative profile (request- selected, resolved request-first at orchestrator merge, else the configured default) over ANY supplied profile that differs. When no authoritative default is resolved (default_gmail_profile is None, e.g. deterministic/scheduled workflows) the supplied value is left untouched, so this does not disturb workflow steps that set a profile deterministically. Telemetry distinguishes blank-fill / placeholder-replacement / enforced-override and records the overridden profile. Tests: tests/backend/test_mcp_tool_bridge_gmail_profile.py. Co-Authored-By: Claude Fable 5 --- src/backend/workflows/mcp_tool_bridge.py | 45 ++++++---- .../test_mcp_tool_bridge_gmail_profile.py | 84 +++++++++++++++++++ 2 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 tests/backend/test_mcp_tool_bridge_gmail_profile.py diff --git a/src/backend/workflows/mcp_tool_bridge.py b/src/backend/workflows/mcp_tool_bridge.py index ad40437cd..e1a1acca2 100644 --- a/src/backend/workflows/mcp_tool_bridge.py +++ b/src/backend/workflows/mcp_tool_bridge.py @@ -216,25 +216,34 @@ def apply_runtime_defaults_to_mcp_payload( if isinstance(current_profile, str) else "" ) - if default_profile and ( - not current_profile_text - or ( - current_profile_text in {"default", "primary"} - and default_profile != current_profile_text - ) - ): + # Enforce the authoritative Gmail profile (request-selected, else the + # configured default) over whatever was supplied, INCLUDING an explicit + # profile the model chose itself. Which Gmail account to use is an + # identity/credential decision the caller's selection owns, not the + # model: models have picked stale/unauthorised profiles (e.g. an account + # with an expired/revoked token) even when a valid profile was selected, + # which hard-fails the whole turn (invalid_grant). When no authoritative + # default is resolved (e.g. deterministic/scheduled workflows where it is + # None) the supplied value is left untouched — nothing to enforce. + if default_profile and current_profile_text != default_profile: payload["profile"] = default_profile - bindings.append( - { - "field": "profile", - "source": ( - "default_gmail_profile_placeholder_replacement" - if current_profile_text in {"default", "primary"} - else "default_gmail_profile" - ), - "value_present": True, - } - ) + if not current_profile_text: + profile_source = "default_gmail_profile" + elif current_profile_text in {"default", "primary"}: + profile_source = "default_gmail_profile_placeholder_replacement" + else: + profile_source = "default_gmail_profile_enforced_override" + profile_binding: dict[str, Any] = { + "field": "profile", + "source": profile_source, + "value_present": True, + } + if ( + current_profile_text + and current_profile_text not in {"default", "primary"} + ): + profile_binding["overridden_profile"] = current_profile_text + bindings.append(profile_binding) namespace_binding = apply_namespace_to_mcp_payload( payload, diff --git a/tests/backend/test_mcp_tool_bridge_gmail_profile.py b/tests/backend/test_mcp_tool_bridge_gmail_profile.py new file mode 100644 index 000000000..9c22acdb6 --- /dev/null +++ b/tests/backend/test_mcp_tool_bridge_gmail_profile.py @@ -0,0 +1,84 @@ +"""Gmail profile enforcement in apply_runtime_defaults_to_mcp_payload. + +Which Gmail account a tool call uses is an identity/credential decision the +caller's selection owns — not the model. A turn was observed failing because +qwen3:8b chose an explicit stale profile ("zhan-gmail", expired/revoked token) +even though a valid profile was selected; the old defaulting only filled blank +or "default"/"primary" placeholders, so the model's explicit (wrong) profile +won and the turn hard-failed with invalid_grant. The authoritative profile must +override any supplied one. +""" + +from __future__ import annotations + +from src.backend.workflows.mcp_tool_bridge import ( + apply_runtime_defaults_to_mcp_payload, +) + +_AUTH = "vonwitbrock-gmail" + + +def _apply(payload: dict, *, default: str | None): + return apply_runtime_defaults_to_mcp_payload( + payload, + tool_name="gmail_list_messages", + input_schema=None, # None => schema accepts any field (profile included) + user_namespace="#V#michael_witbrock@university_of_auckland_strong_ai_lab", + default_gmail_profile=default, + ) + + +def _binding_sources(bindings) -> set[str]: + return {b.get("source") for b in bindings if b.get("field") == "profile"} + + +def test_model_supplied_wrong_profile_is_overridden() -> None: + payload = {"profile": "zhan-gmail", "max_results": 8} + bindings = _apply(payload, default=_AUTH) + assert payload["profile"] == _AUTH # the bug: previously stayed zhan-gmail + assert "default_gmail_profile_enforced_override" in _binding_sources(bindings) + override = next(b for b in bindings if b.get("field") == "profile") + assert override["overridden_profile"] == "zhan-gmail" + + +def test_blank_profile_is_filled() -> None: + payload = {"max_results": 5} + _apply(payload, default=_AUTH) + assert payload["profile"] == _AUTH + + +def test_placeholder_profile_is_replaced() -> None: + payload = {"profile": "default"} + bindings = _apply(payload, default=_AUTH) + assert payload["profile"] == _AUTH + assert ( + "default_gmail_profile_placeholder_replacement" + in _binding_sources(bindings) + ) + + +def test_profile_matching_default_is_untouched() -> None: + payload = {"profile": _AUTH} + bindings = _apply(payload, default=_AUTH) + assert payload["profile"] == _AUTH + assert _binding_sources(bindings) == set() # no profile binding emitted + + +def test_no_authoritative_default_leaves_supplied_profile() -> None: + # Deterministic/scheduled contexts pass default_gmail_profile=None; there is + # nothing to enforce, so an explicit profile must be preserved. + payload = {"profile": "zhan-gmail"} + _apply(payload, default=None) + assert payload["profile"] == "zhan-gmail" + + +def test_non_gmail_tool_profile_is_untouched() -> None: + payload = {"profile": "zhan-gmail"} + apply_runtime_defaults_to_mcp_payload( + payload, + tool_name="search_web", + input_schema=None, + user_namespace=None, + default_gmail_profile=_AUTH, + ) + assert payload["profile"] == "zhan-gmail" # enforcement is gmail-only