From 124c626152b4cfa644016305385ad129b00c5a1f Mon Sep 17 00:00:00 2001 From: meraklbz Date: Mon, 10 Aug 2026 20:09:02 +0800 Subject: [PATCH 1/3] fix: make get_info_for_dispatch abstract to prevent silent notification drops --- .../tasks/push_notification_config_store.py | 42 ++++++------------- .../tasks/test_inmemory_push_notifications.py | 40 ++++++++++++++++++ 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/a2a/server/tasks/push_notification_config_store.py b/src/a2a/server/tasks/push_notification_config_store.py index e1e65c3fb..414897703 100644 --- a/src/a2a/server/tasks/push_notification_config_store.py +++ b/src/a2a/server/tasks/push_notification_config_store.py @@ -1,14 +1,9 @@ -import logging - from abc import ABC, abstractmethod from a2a.server.context import ServerCallContext from a2a.types.a2a_pb2 import TaskPushNotificationConfig -logger = logging.getLogger(__name__) - - class PushNotificationConfigStore(ABC): """Interface for storing and retrieving push notification configurations for tasks.""" @@ -34,6 +29,7 @@ async def get_info( context). """ + @abstractmethod async def get_info_for_dispatch( self, task_id: str, @@ -41,32 +37,18 @@ async def get_info_for_dispatch( """Retrieves all push notification configurations for a task, across all owners. This is the internal read path used by the push-notification - dispatch loop. Implementations SHOULD override this method to - return every configuration registered for task_id regardless of - which user registered it. Authorization already happened at - registration time and the dispatch path fires every registered - webhook for the task. - - The default implementation falls back to calling get_info with - a synthetic empty ServerCallContext. This preserves 1.0 - behavior for subclasses that have not implemented the override - but is INCORRECT for any deployment with multiple owners: the - empty context resolves to the empty-string owner partition and - returns no configs (silently dropping every notification). A - warning is logged on every call to flag the misconfiguration. - Custom subclasses MUST override this method to deliver - notifications correctly in multi-owner deployments. + dispatch loop. Implementations MUST return every configuration + registered for task_id regardless of which user registered it. + Authorization already happened at registration time and the + dispatch path fires every registered webhook for the task. + + The previous non-abstract default fell back to ``get_info`` with a + synthetic empty ``ServerCallContext``, which resolves to the + empty-string owner partition and silently dropped every + notification in any deployment with multiple owners. Making this + method abstract forces every store implementation to provide the + cross-owner read path explicitly instead of failing silently. """ - logger.warning( - '%s does not override ' - 'PushNotificationConfigStore.get_info_for_dispatch; falling back ' - 'to a context-less get_info call which silently drops ' - 'notifications in any deployment with multiple owners. Override ' - 'get_info_for_dispatch to return all configs for task_id across ' - 'every owner.', - type(self).__name__, - ) - return await self.get_info(task_id, ServerCallContext()) @abstractmethod async def delete_info( diff --git a/tests/server/tasks/test_inmemory_push_notifications.py b/tests/server/tasks/test_inmemory_push_notifications.py index f204e2181..cf40f5e96 100644 --- a/tests/server/tasks/test_inmemory_push_notifications.py +++ b/tests/server/tasks/test_inmemory_push_notifications.py @@ -572,3 +572,43 @@ async def test_cross_user_dispatch_alice_registers_bob_triggers( if __name__ == '__main__': unittest.main() + + +class TestPushNotificationConfigStoreContract(unittest.TestCase): + """The dispatch read path must be implemented explicitly (BUG-48).""" + + def test_get_info_for_dispatch_is_abstract(self): + """A store that forgets get_info_for_dispatch cannot be instantiated. + + Previously the base class silently fell back to an owner-scoped + get_info call that drops every notification in multi-owner + deployments; now the method is abstract so the failure is loud. + """ + from a2a.server.tasks.push_notification_config_store import ( + PushNotificationConfigStore, + ) + + class IncompleteStore(PushNotificationConfigStore): + async def set_info(self, task_id, notification_config, context): + pass + + async def get_info(self, task_id, context): + return [] + + async def delete_info(self, task_id, context, config_id=None): + pass + + with self.assertRaises(TypeError): + IncompleteStore() + + def test_builtin_stores_implement_dispatch_read_path(self): + """Both shipped stores provide the cross-owner dispatch read path.""" + from a2a.server.tasks.inmemory_push_notification_config_store import ( + InMemoryPushNotificationConfigStore, + ) + + store = InMemoryPushNotificationConfigStore() + self.assertTrue( + hasattr(store, 'get_info_for_dispatch') + and callable(store.get_info_for_dispatch) + ) From 99cf07681c2283c12641a9f76969dcc61c129bcf Mon Sep 17 00:00:00 2001 From: meraklbz Date: Tue, 11 Aug 2026 00:41:03 +0800 Subject: [PATCH 2/3] ci: retrigger upstream checks From cd4c73533d1cd12fdf6d88cda1d9c7698764d9b3 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Tue, 11 Aug 2026 21:34:43 +0800 Subject: [PATCH 3/3] chore: remove internal tracking ids from comments --- tests/server/tasks/test_inmemory_push_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/server/tasks/test_inmemory_push_notifications.py b/tests/server/tasks/test_inmemory_push_notifications.py index cf40f5e96..f4467a439 100644 --- a/tests/server/tasks/test_inmemory_push_notifications.py +++ b/tests/server/tasks/test_inmemory_push_notifications.py @@ -575,7 +575,7 @@ async def test_cross_user_dispatch_alice_registers_bob_triggers( class TestPushNotificationConfigStoreContract(unittest.TestCase): - """The dispatch read path must be implemented explicitly (BUG-48).""" + """The dispatch read path must be implemented explicitly.""" def test_get_info_for_dispatch_is_abstract(self): """A store that forgets get_info_for_dispatch cannot be instantiated.