operation) {
+ Span span = tracer.spanBuilder(name).startSpan();
+ try (Scope scope = span.makeCurrent()) {
+ return operation.get();
+ } catch (Exception e) {
+ span.recordException(e);
+ throw e;
+ } finally {
+ span.end();
+ }
+ }
+
+ public void withSpan(String name, Runnable operation) {
+ Span span = tracer.spanBuilder(name).startSpan();
+ try (Scope scope = span.makeCurrent()) {
+ operation.run();
+ } catch (Exception e) {
+ span.recordException(e);
+ throw e;
+ } finally {
+ span.end();
+ }
+ }
+}
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/AgentExecutor.java b/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/AgentExecutor.java
index aba33913a..4f5b8fcdb 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/AgentExecutor.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/AgentExecutor.java
@@ -4,6 +4,8 @@
import org.a2aproject.sdk.server.tasks.AgentEmitter;
import org.a2aproject.sdk.spec.A2AError;
+import io.opentelemetry.instrumentation.annotations.WithSpan;
+
/**
* Core business logic interface for implementing A2A agent functionality.
*
@@ -116,6 +118,7 @@ public interface AgentExecutor {
* @param emitter the agent emitter for sending messages, updating status, and streaming artifacts
* @throws A2AError if execution fails catastrophically (exception propagates to client)
*/
+ @WithSpan("AgentExecutor.execute")
void execute(RequestContext context, AgentEmitter emitter) throws A2AError;
/**
@@ -147,5 +150,6 @@ public interface AgentExecutor {
* @throws org.a2aproject.sdk.spec.TaskNotCancelableError if this agent does not support cancellation
* @throws A2AError if cancellation is supported but failed to execute
*/
+ @WithSpan("AgentExecutor.cancel")
void cancel(RequestContext context, AgentEmitter emitter) throws A2AError;
}
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/RequestContext.java b/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/RequestContext.java
index ee8253bac..867548515 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/RequestContext.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/RequestContext.java
@@ -4,10 +4,12 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.UUID;
+import java.util.Objects;
import java.util.stream.Collectors;
import org.a2aproject.sdk.server.ServerCallContext;
+import org.a2aproject.sdk.server.util.IdGenerator;
+import org.a2aproject.sdk.server.util.UUIDIdGenerator;
import org.a2aproject.sdk.spec.InvalidParamsError;
import org.a2aproject.sdk.spec.Message;
import org.a2aproject.sdk.spec.MessageSendConfiguration;
@@ -313,7 +315,7 @@ private List getTextParts(List> parts) {
* The builder handles ID generation and validation automatically:
*
*
- * - TaskId and ContextId are auto-generated (UUID) if not provided
+ * - TaskId and ContextId are auto-generated if not provided (via {@link IdGenerator})
* - IDs are validated against message parameters if both are present
* - Message parameters are updated with generated IDs
* - Related tasks list is initialized to empty list if null
@@ -326,6 +328,7 @@ public static class Builder {
private @Nullable Task task;
private @Nullable List relatedTasks;
private @Nullable ServerCallContext serverCallContext;
+ private IdGenerator idGenerator = new UUIDIdGenerator();
public Builder setParams(@Nullable MessageSendParams params) {
this.params = params;
@@ -357,6 +360,11 @@ public Builder setServerCallContext(@Nullable ServerCallContext serverCallContex
return this;
}
+ public Builder setIdGenerator(IdGenerator idGenerator) {
+ this.idGenerator = Objects.requireNonNull(idGenerator);
+ return this;
+ }
+
public @Nullable MessageSendParams getParams() {
return params;
}
@@ -406,11 +414,11 @@ public RequestContext build() throws InvalidParamsError {
// 4. Determine final IDs using coalesce pattern: builder → message → generate
String finalTaskId = taskId != null ? taskId :
messageTaskId != null ? messageTaskId :
- UUID.randomUUID().toString();
+ idGenerator.generateId();
String finalContextId = contextId != null ? contextId :
messageContextId != null ? messageContextId :
- UUID.randomUUID().toString();
+ idGenerator.generateId();
// 5. Update params if message needs to be updated with final IDs
MessageSendParams finalParams = params;
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/SimpleRequestContextBuilder.java b/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/SimpleRequestContextBuilder.java
index a340ee451..9d25cb82c 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/SimpleRequestContextBuilder.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/SimpleRequestContextBuilder.java
@@ -4,6 +4,8 @@
import java.util.List;
import org.a2aproject.sdk.server.tasks.TaskStore;
+import org.a2aproject.sdk.server.util.IdGenerator;
+import org.a2aproject.sdk.server.util.UUIDIdGenerator;
import org.a2aproject.sdk.spec.Task;
public class SimpleRequestContextBuilder extends RequestContext.Builder {
@@ -11,8 +13,14 @@ public class SimpleRequestContextBuilder extends RequestContext.Builder {
private final boolean shouldPopulateReferredTasks;
public SimpleRequestContextBuilder(TaskStore taskStore, boolean shouldPopulateReferredTasks) {
+ this(taskStore, shouldPopulateReferredTasks, new UUIDIdGenerator());
+ }
+
+ public SimpleRequestContextBuilder(TaskStore taskStore, boolean shouldPopulateReferredTasks,
+ IdGenerator idGenerator) {
this.taskStore = taskStore;
this.shouldPopulateReferredTasks = shouldPopulateReferredTasks;
+ setIdGenerator(idGenerator);
}
@Override
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java b/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
index 8184d7baf..8f01d914f 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
@@ -43,6 +43,8 @@
import org.a2aproject.sdk.server.tasks.ResultAggregator;
import org.a2aproject.sdk.server.tasks.TaskManager;
import org.a2aproject.sdk.server.tasks.TaskStore;
+import org.a2aproject.sdk.server.util.IdGenerator;
+import org.a2aproject.sdk.server.util.UUIDIdGenerator;
import org.a2aproject.sdk.server.util.async.EventConsumerExecutorProducer.EventConsumerExecutor;
import org.a2aproject.sdk.server.util.async.Internal;
import org.a2aproject.sdk.spec.A2AError;
@@ -255,7 +257,8 @@ public DefaultRequestHandler(AgentExecutor agentExecutor, TaskStore taskStore,
QueueManager queueManager, PushNotificationConfigStore pushConfigStore,
MainEventBusProcessor mainEventBusProcessor,
@Internal Executor executor,
- @EventConsumerExecutor Executor eventConsumerExecutor) {
+ @EventConsumerExecutor Executor eventConsumerExecutor,
+ IdGenerator idGenerator) {
this.agentExecutor = agentExecutor;
this.taskStore = taskStore;
this.queueManager = queueManager;
@@ -267,7 +270,7 @@ public DefaultRequestHandler(AgentExecutor agentExecutor, TaskStore taskStore,
// implementation if the parameter is null. Skip that for now, since otherwise I get CDI errors, and
// I am unsure about the correct scope.
// Also reworked to make a Supplier since otherwise the builder gets polluted with wrong tasks
- this.requestContextBuilder = () -> new SimpleRequestContextBuilder(taskStore, false);
+ this.requestContextBuilder = () -> new SimpleRequestContextBuilder(taskStore, false, idGenerator);
}
@PostConstruct
@@ -288,7 +291,7 @@ public static DefaultRequestHandler create(AgentExecutor agentExecutor, TaskStor
Executor executor, Executor eventConsumerExecutor) {
DefaultRequestHandler handler =
new DefaultRequestHandler(agentExecutor, taskStore, queueManager, pushConfigStore,
- mainEventBusProcessor, executor, eventConsumerExecutor);
+ mainEventBusProcessor, executor, eventConsumerExecutor, new UUIDIdGenerator());
handler.agentCompletionTimeoutSeconds = 5;
handler.consumptionCompletionTimeoutSeconds = 2;
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/util/IdGenerator.java b/server-common/src/main/java/org/a2aproject/sdk/server/util/IdGenerator.java
new file mode 100644
index 000000000..d5d3ab90e
--- /dev/null
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/util/IdGenerator.java
@@ -0,0 +1,10 @@
+package org.a2aproject.sdk.server.util;
+
+/**
+ * SPI for generating unique identifiers (tasks, messages, events).
+ */
+@FunctionalInterface
+public interface IdGenerator {
+
+ String generateId();
+}
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/util/UUIDIdGenerator.java b/server-common/src/main/java/org/a2aproject/sdk/server/util/UUIDIdGenerator.java
new file mode 100644
index 000000000..94f820be0
--- /dev/null
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/util/UUIDIdGenerator.java
@@ -0,0 +1,19 @@
+package org.a2aproject.sdk.server.util;
+
+import java.util.UUID;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Default;
+
+/**
+ * Default {@link IdGenerator} using random UUID strings.
+ */
+@ApplicationScoped
+@Default
+public class UUIDIdGenerator implements IdGenerator {
+
+ @Override
+ public String generateId() {
+ return UUID.randomUUID().toString();
+ }
+}
diff --git a/server-common/src/test/java/io/a2a/server/security/SigningServiceTest.java b/server-common/src/test/java/io/a2a/server/security/SigningServiceTest.java
new file mode 100644
index 000000000..43de99f62
--- /dev/null
+++ b/server-common/src/test/java/io/a2a/server/security/SigningServiceTest.java
@@ -0,0 +1,115 @@
+package io.a2a.server.security;
+
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import org.a2aproject.sdk.spec.AgentCapabilities;
+import org.a2aproject.sdk.spec.AgentCard;
+import org.a2aproject.sdk.spec.AgentInterface;
+import org.a2aproject.sdk.spec.TransportProtocol;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for SigningService.
+ */
+class SigningServiceTest {
+
+ private SigningService signingService;
+
+ @BeforeEach
+ void setUp() {
+ signingService = new SigningService();
+ }
+
+ private AgentCard.Builder createTestAgentCardBuilder() {
+ return AgentCard.builder()
+ .name("Test Agent")
+ .description("Test Description")
+ .supportedInterfaces(Collections.singletonList(
+ new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999")))
+ .version("1.0.0")
+ .capabilities(AgentCapabilities.builder().build())
+ .defaultInputModes(Collections.singletonList("text"))
+ .defaultOutputModes(Collections.singletonList("text"))
+ .skills(Collections.emptyList());
+ }
+
+ @Test
+ void testSignAndVerifyRSA() throws Exception {
+ RSAKey rsaJWK = new RSAKeyGenerator(2048)
+ .keyID(UUID.randomUUID().toString())
+ .keyUse(KeyUse.SIGNATURE)
+ .generate();
+
+ RSAKey rsaPublicJWK = rsaJWK.toPublicJWK();
+
+ AgentCard card = createTestAgentCardBuilder().build();
+
+ AgentCard signedCard = signingService.sign(card, rsaJWK);
+
+ assertNotNull(signedCard.signatures());
+ assertEquals(1, signedCard.signatures().size());
+
+ boolean verified = signingService.verify(signedCard, rsaPublicJWK);
+ assertTrue(verified, "Signature should be verified");
+ }
+
+ @Test
+ void testSignAndVerifyEC() throws Exception {
+ ECKey ecJWK = new ECKeyGenerator(Curve.P_256)
+ .keyID(UUID.randomUUID().toString())
+ .generate();
+
+ ECKey ecPublicJWK = ecJWK.toPublicJWK();
+
+ AgentCard card = createTestAgentCardBuilder().build();
+
+ AgentCard signedCard = signingService.sign(card, ecJWK);
+
+ assertNotNull(signedCard.signatures());
+ assertEquals(1, signedCard.signatures().size());
+
+ boolean verified = signingService.verify(signedCard, ecPublicJWK);
+ assertTrue(verified, "Signature should be verified");
+ }
+
+ @Test
+ void testVerifyFailsWithModifiedCard() throws Exception {
+ RSAKey rsaJWK = new RSAKeyGenerator(2048)
+ .keyID(UUID.randomUUID().toString())
+ .generate();
+ RSAKey rsaPublicJWK = rsaJWK.toPublicJWK();
+
+ AgentCard card = createTestAgentCardBuilder().build();
+ AgentCard signedCard = signingService.sign(card, rsaJWK);
+
+ // Modify the card content (e.g., description)
+ AgentCard modifiedCard = AgentCard.builder(signedCard)
+ .description("Modified Description")
+ .build();
+
+ boolean verified = signingService.verify(modifiedCard, rsaPublicJWK);
+ assertFalse(verified, "Verification should fail for modified card");
+ }
+
+ @Test
+ void testVerifyFailsWithWrongKey() throws Exception {
+ RSAKey rsaJWK1 = new RSAKeyGenerator(2048).generate();
+ RSAKey rsaJWK2 = new RSAKeyGenerator(2048).generate();
+
+ AgentCard card = createTestAgentCardBuilder().build();
+ AgentCard signedCard = signingService.sign(card, rsaJWK1);
+
+ boolean verified = signingService.verify(signedCard, rsaJWK2.toPublicJWK());
+ assertFalse(verified, "Verification should fail with wrong key");
+ }
+}