From e9b4ab06216b625d753112aaf9759b2751d26117 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Mon, 10 Aug 2026 20:15:33 +0800 Subject: [PATCH 1/5] fix: enforce per-task push notification config limit --- .../InMemoryPushNotificationConfigStore.java | 22 ++++++++ ...MemoryPushNotificationConfigStoreTest.java | 53 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java index 0d239758f..f51659657 100644 --- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java +++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java @@ -10,6 +10,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.a2aproject.sdk.spec.InvalidParamsError; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; @@ -24,6 +25,13 @@ @ApplicationScoped public class InMemoryPushNotificationConfigStore implements PushNotificationConfigStore { + /** + * Maximum number of push notification configs allowed per task. + * Prevents a single task from accumulating an unbounded list of configs + * (each config consumes memory and can trigger outbound HTTP requests). + */ + public static final int MAX_PUSH_CONFIGS_PER_TASK = 100; + private final Map> pushNotificationInfos = Collections.synchronizedMap(new HashMap<>()); private final Map protocolVersions = Collections.synchronizedMap(new HashMap<>()); @@ -41,6 +49,20 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio } notificationConfig = builder.build(); + // Enforce the per-task limit (BUG-42). Re-registering/updating an already-registered + // config ID is allowed; only genuinely new configs count against the limit. + boolean isExistingConfig = false; + for (TaskPushNotificationConfig existing : notificationConfigList) { + if (existing.id() != null && existing.id().equals(notificationConfig.id())) { + isExistingConfig = true; + break; + } + } + if (!isExistingConfig && notificationConfigList.size() >= MAX_PUSH_CONFIGS_PER_TASK) { + throw new InvalidParamsError("Too many push notification configs for task " + taskId + + " (max " + MAX_PUSH_CONFIGS_PER_TASK + ")"); + } + Iterator notificationConfigIterator = notificationConfigList.iterator(); while (notificationConfigIterator.hasNext()) { TaskPushNotificationConfig config = notificationConfigIterator.next(); diff --git a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java index 110168603..69bbb5286 100644 --- a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java +++ b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; @@ -17,6 +18,7 @@ import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.spec.AgentInterface; +import org.a2aproject.sdk.spec.InvalidParamsError; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; import org.a2aproject.sdk.spec.Task; @@ -659,4 +661,55 @@ public void testPaginationFullIteration() { assertEquals(3, pageCount, "Should have exactly 3 pages (3+3+1)"); } + @Test + public void testSetInfoAtLimitExactlyAllowed() { + String taskId = "task_limit_exact"; + for (int i = 0; i < InMemoryPushNotificationConfigStore.MAX_PUSH_CONFIGS_PER_TASK; i++) { + configStore.setInfo(createSamplePushConfig(taskId, + "http://url" + i + ".com/callback", "cfg" + i, null)); + } + + ListTaskPushNotificationConfigsResult result = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + assertEquals(InMemoryPushNotificationConfigStore.MAX_PUSH_CONFIGS_PER_TASK, result.configs().size()); + } + + @Test + public void testSetInfoRejectsExceedingPerTaskLimit() { + String taskId = "task_limit_exceed"; + for (int i = 0; i < InMemoryPushNotificationConfigStore.MAX_PUSH_CONFIGS_PER_TASK; i++) { + configStore.setInfo(createSamplePushConfig(taskId, + "http://url" + i + ".com/callback", "cfg" + i, null)); + } + + // The (MAX+1)-th distinct config for the same task must be rejected (BUG-42) + TaskPushNotificationConfig overflow = createSamplePushConfig(taskId, + "http://url-overflow.com/callback", "cfg-overflow", null); + assertThrows(InvalidParamsError.class, () -> configStore.setInfo(overflow)); + + // The store is unchanged + ListTaskPushNotificationConfigsResult result = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + assertEquals(InMemoryPushNotificationConfigStore.MAX_PUSH_CONFIGS_PER_TASK, result.configs().size()); + assertTrue(result.configs().stream().noneMatch(c -> "cfg-overflow".equals(c.id()))); + } + + @Test + public void testSetInfoUpdateExistingConfigAtLimitAllowed() { + String taskId = "task_limit_update"; + for (int i = 0; i < InMemoryPushNotificationConfigStore.MAX_PUSH_CONFIGS_PER_TASK; i++) { + configStore.setInfo(createSamplePushConfig(taskId, + "http://url" + i + ".com/callback", "cfg" + i, null)); + } + + // Updating an existing config at the limit must still be allowed + TaskPushNotificationConfig updated = createSamplePushConfig(taskId, + "http://url-updated.com/callback", "cfg0", "new-token"); + TaskPushNotificationConfig result = configStore.setInfo(updated); + + assertEquals("cfg0", result.id()); + ListTaskPushNotificationConfigsResult configs = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + assertEquals(InMemoryPushNotificationConfigStore.MAX_PUSH_CONFIGS_PER_TASK, configs.configs().size()); + assertEquals("http://url-updated.com/callback", + configs.configs().stream().filter(c -> "cfg0".equals(c.id())).findFirst().orElseThrow().url()); + } + } From 356e81baa2d074828fc31f3d4f5fb3cb137fc0f1 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Tue, 11 Aug 2026 21:28:39 +0800 Subject: [PATCH 2/5] feat: make the per-task push config limit configurable and enforce it in the JPA store - Add a2a.push-notification-config.max-per-task (default 100) to a2a-defaults.properties, read via A2AConfigProvider. - Add PushNotificationConfigStore.maxPushConfigsPerTask() helper and document the per-task limit in the interface javadoc. - InMemoryPushNotificationConfigStore now reads the configured limit (null-safe for direct construction in tests). - JpaDatabasePushNotificationConfigStore enforces the same limit before persisting a new config. --- ...paDatabasePushNotificationConfigStore.java | 17 +++++++++ .../InMemoryPushNotificationConfigStore.java | 18 +++++++--- .../tasks/PushNotificationConfigStore.java | 35 +++++++++++++++++++ .../META-INF/a2a-defaults.properties | 6 ++++ ...MemoryPushNotificationConfigStoreTest.java | 2 +- 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java index a651d7634..dee737d4b 100644 --- a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java +++ b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java @@ -7,12 +7,15 @@ import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Alternative; +import jakarta.inject.Inject; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; +import org.a2aproject.sdk.server.config.A2AConfigProvider; import org.a2aproject.sdk.server.tasks.PushNotificationConfigStore; +import org.a2aproject.sdk.spec.InvalidParamsError; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; @@ -34,6 +37,9 @@ public class JpaDatabasePushNotificationConfigStore implements PushNotificationC @PersistenceContext(unitName = "a2a-java") EntityManager em; + @Inject + A2AConfigProvider config; + @Transactional @Override public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificationConfig) { @@ -66,6 +72,17 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio LOGGER.debug("Updated existing PushNotificationConfig for Task '{}' with ID: {}", taskId, notificationConfig.id()); } else { + // Enforce the per-task limit (configurable via + // a2a.push-notification-config.max-per-task); only genuinely new + // configs count against it. + int maxPerTask = PushNotificationConfigStore.maxPushConfigsPerTask(config); + Long existingCount = em.createQuery( + "SELECT COUNT(c) FROM JpaPushNotificationConfig c WHERE c.id.taskId = :taskId", + Long.class).setParameter("taskId", taskId).getSingleResult(); + if (existingCount >= maxPerTask) { + throw new InvalidParamsError("Too many push notification configs for task " + taskId + + " (max " + maxPerTask + ")"); + } // Create new entity JpaPushNotificationConfig jpaConfig = JpaPushNotificationConfig.createFromConfig(taskId, notificationConfig, resolvedVersion); em.persist(jpaConfig); diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java index f51659657..dca85aa94 100644 --- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java +++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java @@ -10,6 +10,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.a2aproject.sdk.server.config.A2AConfigProvider; import org.a2aproject.sdk.spec.InvalidParamsError; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; @@ -26,15 +27,19 @@ public class InMemoryPushNotificationConfigStore implements PushNotificationConfigStore { /** - * Maximum number of push notification configs allowed per task. + * Default maximum number of push notification configs allowed per task. * Prevents a single task from accumulating an unbounded list of configs * (each config consumes memory and can trigger outbound HTTP requests). + * Overridable via {@code a2a.push-notification-config.max-per-task}. */ public static final int MAX_PUSH_CONFIGS_PER_TASK = 100; private final Map> pushNotificationInfos = Collections.synchronizedMap(new HashMap<>()); private final Map protocolVersions = Collections.synchronizedMap(new HashMap<>()); + @Inject + @Nullable A2AConfigProvider config; + @Inject public InMemoryPushNotificationConfigStore() { } @@ -49,8 +54,10 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio } notificationConfig = builder.build(); - // Enforce the per-task limit (BUG-42). Re-registering/updating an already-registered - // config ID is allowed; only genuinely new configs count against the limit. + // Enforce the per-task limit (configurable via + // a2a.push-notification-config.max-per-task). Re-registering/updating an + // already-registered config ID is allowed; only genuinely new configs count + // against the limit. boolean isExistingConfig = false; for (TaskPushNotificationConfig existing : notificationConfigList) { if (existing.id() != null && existing.id().equals(notificationConfig.id())) { @@ -58,9 +65,10 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio break; } } - if (!isExistingConfig && notificationConfigList.size() >= MAX_PUSH_CONFIGS_PER_TASK) { + int maxPerTask = PushNotificationConfigStore.maxPushConfigsPerTask(config); + if (!isExistingConfig && notificationConfigList.size() >= maxPerTask) { throw new InvalidParamsError("Too many push notification configs for task " + taskId - + " (max " + MAX_PUSH_CONFIGS_PER_TASK + ")"); + + " (max " + maxPerTask + ")"); } Iterator notificationConfigIterator = notificationConfigList.iterator(); diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java index 5d2bf3392..c7fb5ddce 100644 --- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java +++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java @@ -1,5 +1,6 @@ package org.a2aproject.sdk.server.tasks; +import org.a2aproject.sdk.server.config.A2AConfigProvider; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; @@ -21,6 +22,13 @@ *
  • Used for retrieval and deletion of specific configurations
  • * * + *

    Per-task Limit

    + * Implementations MUST reject registering more than + * {@code a2a.push-notification-config.max-per-task} (default 100) distinct configs for + * a single task, throwing {@code InvalidParamsError}. Re-registering an already-registered + * config ID does not count against the limit. See + * {@link #maxPushConfigsPerTask(A2AConfigProvider)}. + * *

    Pagination Support

    * {@link #getInfo(ListTaskPushNotificationConfigsParams)} supports pagination for tasks * with many push notification configurations: @@ -105,6 +113,33 @@ static String resolveProtocolVersion(@Nullable String protocolVersion) { return protocolVersion != null ? protocolVersion : org.a2aproject.sdk.spec.AgentInterface.CURRENT_PROTOCOL_VERSION; } + /** + * Maximum number of push notification configs allowed per task. + * + *

    Reads the {@code a2a.push-notification-config.max-per-task} configuration value; + * a {@code null} provider, or a missing, non-numeric, or non-positive value, falls + * back to 100. See {@code META-INF/a2a-defaults.properties}.

    + * + * @param config the configuration provider, or {@code null} when the store is used + * without CDI injection (e.g., in tests) + * @return the per-task limit (always positive) + */ + static int maxPushConfigsPerTask(@Nullable A2AConfigProvider config) { + if (config == null) { + return 100; + } + return config.getOptionalValue("a2a.push-notification-config.max-per-task") + .map(value -> { + try { + int parsed = Integer.parseInt(value.trim()); + return parsed > 0 ? parsed : 100; + } catch (NumberFormatException e) { + return 100; + } + }) + .orElse(100); + } + /** * Retrieves push notification configurations for a task with pagination support. *

    diff --git a/server-common/src/main/resources/META-INF/a2a-defaults.properties b/server-common/src/main/resources/META-INF/a2a-defaults.properties index e1a71fe24..0ab5b660d 100644 --- a/server-common/src/main/resources/META-INF/a2a-defaults.properties +++ b/server-common/src/main/resources/META-INF/a2a-defaults.properties @@ -32,3 +32,9 @@ a2a.executor.queue-capacity=100 # When true, referenced task IDs in messages are resolved from the TaskStore # and made available via RequestContext.getRelatedTasks() a2a.request-context.populate-referred-tasks=true + +# PushNotificationConfigStore - per-task configuration limit +# Maximum number of push notification configs a single task may register. +# Prevents a task from accumulating an unbounded list of configs (each config +# consumes memory and can trigger outbound HTTP requests). +a2a.push-notification-config.max-per-task=100 diff --git a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java index 69bbb5286..7c27efb9b 100644 --- a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java +++ b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java @@ -681,7 +681,7 @@ public void testSetInfoRejectsExceedingPerTaskLimit() { "http://url" + i + ".com/callback", "cfg" + i, null)); } - // The (MAX+1)-th distinct config for the same task must be rejected (BUG-42) + // The (MAX+1)-th distinct config for the same task must be rejected TaskPushNotificationConfig overflow = createSamplePushConfig(taskId, "http://url-overflow.com/callback", "cfg-overflow", null); assertThrows(InvalidParamsError.class, () -> configStore.setInfo(overflow)); From 8622f32584d7f7d254d695684974b7c5199df334 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Wed, 12 Aug 2026 00:15:33 +0800 Subject: [PATCH 3/5] test: add JPA limit enforcement test and share the default limit constant - Define DEFAULT_MAX_PUSH_CONFIGS_PER_TASK on PushNotificationConfigStore and use it in maxPushConfigsPerTask() (removes the repeated magic 100); InMemoryPushNotificationConfigStore.MAX_PUSH_CONFIGS_PER_TASK now references it. - Add JpaDatabasePushNotificationConfigStoreIntegrationTest coverage for the per-task limit: registering up to the limit succeeds, the next distinct config throws InvalidParamsError, and updating an existing config ID at the limit is still allowed. --- ...otificationConfigStoreIntegrationTest.java | 26 +++++++++++++++++++ .../InMemoryPushNotificationConfigStore.java | 2 +- .../tasks/PushNotificationConfigStore.java | 15 ++++++++--- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java index 4ff600268..0d51bd039 100644 --- a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java +++ b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java @@ -28,6 +28,7 @@ import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams; import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams; +import org.a2aproject.sdk.spec.InvalidParamsError; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; import org.a2aproject.sdk.spec.Message; @@ -581,4 +582,29 @@ private void createSamples(String taskId, int size) { } } } + + @Test + @Transactional + public void testPushConfigLimitEnforced() { + String taskId = "task_limit_" + System.currentTimeMillis(); + int limit = org.a2aproject.sdk.server.tasks.PushNotificationConfigStore.DEFAULT_MAX_PUSH_CONFIGS_PER_TASK; + + // Register configs up to the limit. + for (int i = 0; i < limit; i++) { + TaskPushNotificationConfig config = createSamplePushConfig( + "http://limit" + i + ".com/callback", "limit-cfg" + i, "token" + i); + pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); + } + + // The (limit+1)-th distinct config must be rejected. + TaskPushNotificationConfig extra = createSamplePushConfig( + "http://extra.com/callback", "limit-extra", "token-extra"); + assertThrows(InvalidParamsError.class, () -> + pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(extra).taskId(taskId).build())); + + // Re-registering an existing config ID at the limit is still allowed. + TaskPushNotificationConfig existing = createSamplePushConfig( + "http://limit0.com/callback-updated", "limit-cfg0", "token-updated"); + pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(existing).taskId(taskId).build()); + } } diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java index dca85aa94..051805fa7 100644 --- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java +++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java @@ -32,7 +32,7 @@ public class InMemoryPushNotificationConfigStore implements PushNotificationConf * (each config consumes memory and can trigger outbound HTTP requests). * Overridable via {@code a2a.push-notification-config.max-per-task}. */ - public static final int MAX_PUSH_CONFIGS_PER_TASK = 100; + public static final int MAX_PUSH_CONFIGS_PER_TASK = PushNotificationConfigStore.DEFAULT_MAX_PUSH_CONFIGS_PER_TASK; private final Map> pushNotificationInfos = Collections.synchronizedMap(new HashMap<>()); private final Map protocolVersions = Collections.synchronizedMap(new HashMap<>()); diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java index c7fb5ddce..64138f3aa 100644 --- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java +++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/PushNotificationConfigStore.java @@ -78,6 +78,13 @@ */ public interface PushNotificationConfigStore { + /** + * Default maximum number of push notification configs allowed per task, + * used when {@code a2a.push-notification-config.max-per-task} is not + * configured. See {@code META-INF/a2a-defaults.properties}. + */ + int DEFAULT_MAX_PUSH_CONFIGS_PER_TASK = 100; + /** * Sets or updates the push notification configuration for a task. *

    @@ -126,18 +133,18 @@ static String resolveProtocolVersion(@Nullable String protocolVersion) { */ static int maxPushConfigsPerTask(@Nullable A2AConfigProvider config) { if (config == null) { - return 100; + return DEFAULT_MAX_PUSH_CONFIGS_PER_TASK; } return config.getOptionalValue("a2a.push-notification-config.max-per-task") .map(value -> { try { int parsed = Integer.parseInt(value.trim()); - return parsed > 0 ? parsed : 100; + return parsed > 0 ? parsed : DEFAULT_MAX_PUSH_CONFIGS_PER_TASK; } catch (NumberFormatException e) { - return 100; + return DEFAULT_MAX_PUSH_CONFIGS_PER_TASK; } }) - .orElse(100); + .orElse(DEFAULT_MAX_PUSH_CONFIGS_PER_TASK); } /** From 7f25eae2d243a7ee631fb1ffdc7584d14396d9f2 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Wed, 12 Aug 2026 00:16:10 +0800 Subject: [PATCH 4/5] docs: restore push config limit doc in dev/configuration.md Restore the documentation for a2a.push-notification-config.max-per-task in docs/content/dev/configuration.md (previously contributed via the fork PR that was inadvertently dropped during a force-push). --- docs/content/dev/configuration.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/content/dev/configuration.md b/docs/content/dev/configuration.md index 2458b9c09..8b3fea787 100644 --- a/docs/content/dev/configuration.md +++ b/docs/content/dev/configuration.md @@ -65,6 +65,15 @@ a2a.request-context.populate-referred-tasks=true When enabled, task IDs referenced in incoming messages are looked up in the `TaskStore` and made available to the `AgentExecutor` via `RequestContext.getRelatedTasks()`. This is useful for multi-task conversations where the agent needs access to state from related tasks. Enabled by default; set to `false` to avoid extra `TaskStore` lookups when not needed. +### Push Notification Config Store + +```properties +# Maximum push notification configs per task (default: 100) +a2a.push-notification-config.max-per-task=100 +``` + +Limits the number of distinct push notification configurations a single task may register. Each config consumes memory and can trigger an outbound HTTP request on every task event. Re-registering an existing config ID (updating it) does not count against the limit. Both `InMemoryPushNotificationConfigStore` and `JpaDatabasePushNotificationConfigStore` enforce this limit, throwing `InvalidParamsError` when exceeded. + ### Tuning Guidelines - **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load. From 1595f3c13ad2005d2fd572f08fd8a3fc8ae22428 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Tue, 11 Aug 2026 17:50:30 +0100 Subject: [PATCH 5/5] refactor: simplify setInfo limit check and fix field shadowing Rename `config` field to `configProvider` to avoid shadowing by local variables of the same name in setInfo and deleteInfo. Combine the existing-config check and the remove-old-config loop into a single `removeIf` call, eliminating a redundant iteration. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../InMemoryPushNotificationConfigStore.java | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java index 051805fa7..5bc1c7ac8 100644 --- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java +++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java @@ -38,7 +38,7 @@ public class InMemoryPushNotificationConfigStore implements PushNotificationConf private final Map protocolVersions = Collections.synchronizedMap(new HashMap<>()); @Inject - @Nullable A2AConfigProvider config; + @Nullable A2AConfigProvider configProvider; @Inject public InMemoryPushNotificationConfigStore() { @@ -54,31 +54,18 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio } notificationConfig = builder.build(); - // Enforce the per-task limit (configurable via - // a2a.push-notification-config.max-per-task). Re-registering/updating an - // already-registered config ID is allowed; only genuinely new configs count - // against the limit. - boolean isExistingConfig = false; - for (TaskPushNotificationConfig existing : notificationConfigList) { - if (existing.id() != null && existing.id().equals(notificationConfig.id())) { - isExistingConfig = true; - break; - } - } - int maxPerTask = PushNotificationConfigStore.maxPushConfigsPerTask(config); + // Enforce the per-task limit and remove any existing config with the same + // ID in a single pass. Re-registering/updating an already-registered config + // ID is allowed; only genuinely new configs count against the limit. + String configId = notificationConfig.id(); + boolean isExistingConfig = notificationConfigList.removeIf( + existing -> existing.id() != null && existing.id().equals(configId)); + int maxPerTask = PushNotificationConfigStore.maxPushConfigsPerTask(configProvider); if (!isExistingConfig && notificationConfigList.size() >= maxPerTask) { throw new InvalidParamsError("Too many push notification configs for task " + taskId + " (max " + maxPerTask + ")"); } - Iterator notificationConfigIterator = notificationConfigList.iterator(); - while (notificationConfigIterator.hasNext()) { - TaskPushNotificationConfig config = notificationConfigIterator.next(); - if (config.id() != null && config.id().equals(notificationConfig.id())) { - notificationConfigIterator.remove(); - break; - } - } notificationConfigList.add(notificationConfig); pushNotificationInfos.put(taskId, notificationConfigList); return notificationConfig;