Skip to content

Commit e042993

Browse files
authored
Prototype a2a card artifact type - fixes #7003 (#7007)
1 parent a279789 commit e042993

File tree

12 files changed

+456
-1
lines changed

12 files changed

+456
-1
lines changed

common/src/main/java/io/apicurio/registry/types/ArtifactType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ private ArtifactType() {
1818
public static final String WSDL = "WSDL";
1919
public static final String XSD = "XSD";
2020
public static final String XML = "XML";
21+
public static final String AGENT_CARD = "AGENT_CARD";
2122

2223
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.apicurio.registry.content;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.apicurio.registry.content.util.ContentTypeUtil;
5+
6+
import java.util.Map;
7+
8+
/**
9+
* Content accepter for A2A Agent Card artifacts.
10+
*
11+
* Agent Cards are JSON documents that describe AI agents following the A2A (Agent2Agent) protocol.
12+
* This accepter validates that the content is valid JSON and contains required Agent Card fields.
13+
*
14+
* @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
15+
*/
16+
public class AgentCardContentAccepter implements ContentAccepter {
17+
18+
@Override
19+
public boolean acceptsContent(TypedContent content, Map<String, TypedContent> resolvedReferences) {
20+
try {
21+
// Must be parseable JSON
22+
if (content.getContentType() != null && content.getContentType().toLowerCase().contains("json")
23+
&& !ContentTypeUtil.isParsableJson(content.getContent())) {
24+
return false;
25+
}
26+
27+
JsonNode tree = ContentTypeUtil.parseJson(content.getContent());
28+
29+
// Check for A2A Agent Card structure
30+
// An Agent Card must have a "name" field at minimum
31+
// Optional but common fields: "description", "version", "url", "capabilities", "skills"
32+
if (tree.isObject() && tree.has("name")) {
33+
// Additional heuristics to identify an Agent Card vs regular JSON
34+
// Look for A2A-specific fields
35+
if (tree.has("capabilities") || tree.has("skills") || tree.has("url")
36+
|| tree.has("authentication") || tree.has("defaultInputModes")
37+
|| tree.has("defaultOutputModes") || tree.has("provider")) {
38+
return true;
39+
}
40+
// If it has name and description, and looks like a service descriptor, accept it
41+
if (tree.has("description") && tree.has("version")) {
42+
return true;
43+
}
44+
}
45+
} catch (Exception e) {
46+
// Error - invalid syntax
47+
}
48+
return false;
49+
}
50+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.apicurio.registry.content.extract;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import io.apicurio.registry.content.ContentHandle;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
import java.io.IOException;
10+
11+
/**
12+
* Performs metadata extraction for A2A Agent Card content.
13+
*
14+
* Extracts the agent name and description from the Agent Card JSON structure.
15+
*
16+
* @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
17+
*/
18+
public class AgentCardContentExtractor implements ContentExtractor {
19+
20+
private static final Logger log = LoggerFactory.getLogger(AgentCardContentExtractor.class);
21+
22+
private static final ObjectMapper mapper = new ObjectMapper();
23+
24+
@Override
25+
public ExtractedMetaData extract(ContentHandle content) {
26+
try {
27+
JsonNode agentCard = mapper.readTree(content.bytes());
28+
JsonNode name = agentCard.get("name");
29+
JsonNode description = agentCard.get("description");
30+
31+
ExtractedMetaData metaData = null;
32+
33+
if (name != null && !name.isNull() && name.isTextual()) {
34+
metaData = new ExtractedMetaData();
35+
metaData.setName(name.asText());
36+
}
37+
38+
if (description != null && !description.isNull() && description.isTextual()) {
39+
if (metaData == null) {
40+
metaData = new ExtractedMetaData();
41+
}
42+
metaData.setDescription(description.asText());
43+
}
44+
45+
return metaData;
46+
} catch (IOException e) {
47+
log.warn("Error extracting metadata from Agent Card: {}", e.getMessage());
48+
return null;
49+
}
50+
}
51+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package io.apicurio.registry.rules.validity;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.apicurio.registry.content.TypedContent;
5+
import io.apicurio.registry.content.util.ContentTypeUtil;
6+
import io.apicurio.registry.rest.v3.beans.ArtifactReference;
7+
import io.apicurio.registry.rules.violation.RuleViolation;
8+
import io.apicurio.registry.rules.violation.RuleViolationException;
9+
import io.apicurio.registry.types.RuleType;
10+
11+
import java.util.Collections;
12+
import java.util.HashSet;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.Set;
16+
17+
/**
18+
* Content validator for A2A Agent Card artifacts.
19+
*
20+
* Validates that the content is a valid Agent Card JSON document following the A2A protocol specification.
21+
*
22+
* @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
23+
*/
24+
public class AgentCardContentValidator implements ContentValidator {
25+
26+
@Override
27+
public void validate(ValidityLevel level, TypedContent content,
28+
Map<String, TypedContent> resolvedReferences) throws RuleViolationException {
29+
30+
if (level == ValidityLevel.NONE) {
31+
return;
32+
}
33+
34+
Set<RuleViolation> violations = new HashSet<>();
35+
36+
try {
37+
JsonNode tree = ContentTypeUtil.parseJson(content.getContent());
38+
39+
if (!tree.isObject()) {
40+
throw new RuleViolationException("Agent Card must be a JSON object",
41+
RuleType.VALIDITY, level.name(),
42+
Collections.singleton(new RuleViolation("Agent Card must be a JSON object", "")));
43+
}
44+
45+
// SYNTAX_ONLY level: just check it's valid JSON (already done by parsing)
46+
if (level == ValidityLevel.SYNTAX_ONLY) {
47+
return;
48+
}
49+
50+
// FULL level: validate required fields
51+
if (!tree.has("name") || tree.get("name").asText().trim().isEmpty()) {
52+
violations.add(new RuleViolation("Agent Card must have a non-empty 'name' field", "/name"));
53+
}
54+
55+
// Validate optional but typed fields if present
56+
if (tree.has("version") && !tree.get("version").isTextual()) {
57+
violations.add(new RuleViolation("'version' field must be a string", "/version"));
58+
}
59+
60+
if (tree.has("url") && !tree.get("url").isTextual()) {
61+
violations.add(new RuleViolation("'url' field must be a string", "/url"));
62+
}
63+
64+
if (tree.has("capabilities") && !tree.get("capabilities").isObject()) {
65+
violations.add(new RuleViolation("'capabilities' field must be an object", "/capabilities"));
66+
}
67+
68+
if (tree.has("skills") && !tree.get("skills").isArray()) {
69+
violations.add(new RuleViolation("'skills' field must be an array", "/skills"));
70+
}
71+
72+
if (tree.has("defaultInputModes") && !tree.get("defaultInputModes").isArray()) {
73+
violations.add(
74+
new RuleViolation("'defaultInputModes' field must be an array", "/defaultInputModes"));
75+
}
76+
77+
if (tree.has("defaultOutputModes") && !tree.get("defaultOutputModes").isArray()) {
78+
violations.add(new RuleViolation("'defaultOutputModes' field must be an array",
79+
"/defaultOutputModes"));
80+
}
81+
82+
if (!violations.isEmpty()) {
83+
throw new RuleViolationException("Invalid Agent Card", RuleType.VALIDITY, level.name(),
84+
violations);
85+
}
86+
87+
} catch (RuleViolationException e) {
88+
throw e;
89+
} catch (Exception e) {
90+
throw new RuleViolationException("Invalid Agent Card JSON: " + e.getMessage(),
91+
RuleType.VALIDITY, level.name(), e);
92+
}
93+
}
94+
95+
@Override
96+
public void validateReferences(TypedContent content, List<ArtifactReference> references)
97+
throws RuleViolationException {
98+
// Agent Cards don't support references in MVP
99+
if (references != null && !references.isEmpty()) {
100+
throw new RuleViolationException("Agent Cards do not support references",
101+
RuleType.INTEGRITY, "NONE",
102+
Collections.singleton(new RuleViolation("References are not supported for Agent Cards", "")));
103+
}
104+
}
105+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package io.apicurio.registry.types.provider;
2+
3+
import io.apicurio.registry.content.AgentCardContentAccepter;
4+
import io.apicurio.registry.content.ContentAccepter;
5+
import io.apicurio.registry.content.canon.ContentCanonicalizer;
6+
import io.apicurio.registry.json.content.canon.JsonContentCanonicalizer;
7+
import io.apicurio.registry.content.dereference.ContentDereferencer;
8+
import io.apicurio.registry.content.dereference.NoopContentDereferencer;
9+
import io.apicurio.registry.content.extract.AgentCardContentExtractor;
10+
import io.apicurio.registry.content.extract.ContentExtractor;
11+
import io.apicurio.registry.content.refs.DefaultReferenceArtifactIdentifierExtractor;
12+
import io.apicurio.registry.content.refs.NoOpReferenceFinder;
13+
import io.apicurio.registry.content.refs.ReferenceArtifactIdentifierExtractor;
14+
import io.apicurio.registry.content.refs.ReferenceFinder;
15+
import io.apicurio.registry.rules.compatibility.CompatibilityChecker;
16+
import io.apicurio.registry.rules.compatibility.NoopCompatibilityChecker;
17+
import io.apicurio.registry.rules.validity.AgentCardContentValidator;
18+
import io.apicurio.registry.rules.validity.ContentValidator;
19+
import io.apicurio.registry.types.ArtifactType;
20+
import io.apicurio.registry.types.ContentTypes;
21+
22+
import java.util.Set;
23+
24+
/**
25+
* Artifact type utility provider for A2A Agent Card artifacts.
26+
*
27+
* Agent Cards are JSON documents that describe AI agents following the A2A (Agent2Agent) protocol.
28+
* They contain metadata about an agent's capabilities, skills, and communication endpoints.
29+
*
30+
* @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
31+
*/
32+
public class AgentCardArtifactTypeUtilProvider extends AbstractArtifactTypeUtilProvider {
33+
34+
@Override
35+
public String getArtifactType() {
36+
return ArtifactType.AGENT_CARD;
37+
}
38+
39+
@Override
40+
public Set<String> getContentTypes() {
41+
return Set.of(ContentTypes.APPLICATION_JSON);
42+
}
43+
44+
@Override
45+
protected ContentAccepter createContentAccepter() {
46+
return new AgentCardContentAccepter();
47+
}
48+
49+
@Override
50+
protected CompatibilityChecker createCompatibilityChecker() {
51+
// For MVP, no compatibility checking
52+
// Future: implement Agent Card compatibility rules
53+
return NoopCompatibilityChecker.INSTANCE;
54+
}
55+
56+
@Override
57+
protected ContentCanonicalizer createContentCanonicalizer() {
58+
// Use JSON canonicalizer for consistent formatting
59+
return new JsonContentCanonicalizer();
60+
}
61+
62+
@Override
63+
protected ContentValidator createContentValidator() {
64+
return new AgentCardContentValidator();
65+
}
66+
67+
@Override
68+
protected ContentExtractor createContentExtractor() {
69+
return new AgentCardContentExtractor();
70+
}
71+
72+
@Override
73+
protected ContentDereferencer createContentDereferencer() {
74+
// Agent Cards don't support references in MVP
75+
return NoopContentDereferencer.INSTANCE;
76+
}
77+
78+
@Override
79+
protected ReferenceFinder createReferenceFinder() {
80+
// Agent Cards don't support references in MVP
81+
return NoOpReferenceFinder.INSTANCE;
82+
}
83+
84+
@Override
85+
public boolean supportsReferencesWithContext() {
86+
return false;
87+
}
88+
89+
@Override
90+
protected ReferenceArtifactIdentifierExtractor createReferenceArtifactIdentifierExtractor() {
91+
return new DefaultReferenceArtifactIdentifierExtractor();
92+
}
93+
}

schema-util/util-provider/src/main/java/io/apicurio/registry/types/provider/DefaultArtifactTypeUtilProviderImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public class DefaultArtifactTypeUtilProviderImpl implements ArtifactTypeUtilProv
1515
new AsyncApiArtifactTypeUtilProvider(), new JsonArtifactTypeUtilProvider(),
1616
new AvroArtifactTypeUtilProvider(), new GraphQLArtifactTypeUtilProvider(),
1717
new KConnectArtifactTypeUtilProvider(), new WsdlArtifactTypeUtilProvider(),
18-
new XsdArtifactTypeUtilProvider(), new XmlArtifactTypeUtilProvider()));
18+
new XsdArtifactTypeUtilProvider(), new XmlArtifactTypeUtilProvider(),
19+
new AgentCardArtifactTypeUtilProvider()));
1920

2021
protected List<ArtifactTypeUtilProvider> providers = new ArrayList<>();
2122

0 commit comments

Comments
 (0)