Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ private ArtifactType() {
public static final String WSDL = "WSDL";
public static final String XSD = "XSD";
public static final String XML = "XML";
public static final String AGENT_CARD = "AGENT_CARD";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.apicurio.registry.content;

import com.fasterxml.jackson.databind.JsonNode;
import io.apicurio.registry.content.util.ContentTypeUtil;

import java.util.Map;

/**
* Content accepter for A2A Agent Card artifacts.
*
* Agent Cards are JSON documents that describe AI agents following the A2A (Agent2Agent) protocol.
* This accepter validates that the content is valid JSON and contains required Agent Card fields.
*
* @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
*/
public class AgentCardContentAccepter implements ContentAccepter {

@Override
public boolean acceptsContent(TypedContent content, Map<String, TypedContent> resolvedReferences) {
try {
// Must be parseable JSON
if (content.getContentType() != null && content.getContentType().toLowerCase().contains("json")
&& !ContentTypeUtil.isParsableJson(content.getContent())) {
return false;
}

JsonNode tree = ContentTypeUtil.parseJson(content.getContent());

// Check for A2A Agent Card structure
// An Agent Card must have a "name" field at minimum
// Optional but common fields: "description", "version", "url", "capabilities", "skills"
if (tree.isObject() && tree.has("name")) {
// Additional heuristics to identify an Agent Card vs regular JSON
// Look for A2A-specific fields
if (tree.has("capabilities") || tree.has("skills") || tree.has("url")
|| tree.has("authentication") || tree.has("defaultInputModes")
|| tree.has("defaultOutputModes") || tree.has("provider")) {
return true;
}
// If it has name and description, and looks like a service descriptor, accept it
if (tree.has("description") && tree.has("version")) {
return true;
}
}
} catch (Exception e) {
// Error - invalid syntax
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.apicurio.registry.content.extract;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.apicurio.registry.content.ContentHandle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
* Performs metadata extraction for A2A Agent Card content.
*
* Extracts the agent name and description from the Agent Card JSON structure.
*
* @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
*/
public class AgentCardContentExtractor implements ContentExtractor {

private static final Logger log = LoggerFactory.getLogger(AgentCardContentExtractor.class);

private static final ObjectMapper mapper = new ObjectMapper();

@Override
public ExtractedMetaData extract(ContentHandle content) {
try {
JsonNode agentCard = mapper.readTree(content.bytes());
JsonNode name = agentCard.get("name");
JsonNode description = agentCard.get("description");

ExtractedMetaData metaData = null;

if (name != null && !name.isNull() && name.isTextual()) {
metaData = new ExtractedMetaData();
metaData.setName(name.asText());
}

if (description != null && !description.isNull() && description.isTextual()) {
if (metaData == null) {
metaData = new ExtractedMetaData();
}
metaData.setDescription(description.asText());
}

return metaData;
} catch (IOException e) {
log.warn("Error extracting metadata from Agent Card: {}", e.getMessage());
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.apicurio.registry.rules.validity;

import com.fasterxml.jackson.databind.JsonNode;
import io.apicurio.registry.content.TypedContent;
import io.apicurio.registry.content.util.ContentTypeUtil;
import io.apicurio.registry.rest.v3.beans.ArtifactReference;
import io.apicurio.registry.rules.violation.RuleViolation;
import io.apicurio.registry.rules.violation.RuleViolationException;
import io.apicurio.registry.types.RuleType;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Content validator for A2A Agent Card artifacts.
*
* Validates that the content is a valid Agent Card JSON document following the A2A protocol specification.
*
* @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
*/
public class AgentCardContentValidator implements ContentValidator {

@Override
public void validate(ValidityLevel level, TypedContent content,
Map<String, TypedContent> resolvedReferences) throws RuleViolationException {

if (level == ValidityLevel.NONE) {
return;
}

Set<RuleViolation> violations = new HashSet<>();

try {
JsonNode tree = ContentTypeUtil.parseJson(content.getContent());

if (!tree.isObject()) {
throw new RuleViolationException("Agent Card must be a JSON object",
RuleType.VALIDITY, level.name(),
Collections.singleton(new RuleViolation("Agent Card must be a JSON object", "")));
}

// SYNTAX_ONLY level: just check it's valid JSON (already done by parsing)
if (level == ValidityLevel.SYNTAX_ONLY) {
return;
}

// FULL level: validate required fields
if (!tree.has("name") || tree.get("name").asText().trim().isEmpty()) {
violations.add(new RuleViolation("Agent Card must have a non-empty 'name' field", "/name"));
}

// Validate optional but typed fields if present
if (tree.has("version") && !tree.get("version").isTextual()) {
violations.add(new RuleViolation("'version' field must be a string", "/version"));
}

if (tree.has("url") && !tree.get("url").isTextual()) {
violations.add(new RuleViolation("'url' field must be a string", "/url"));
}

if (tree.has("capabilities") && !tree.get("capabilities").isObject()) {
violations.add(new RuleViolation("'capabilities' field must be an object", "/capabilities"));
}

if (tree.has("skills") && !tree.get("skills").isArray()) {
violations.add(new RuleViolation("'skills' field must be an array", "/skills"));
}

if (tree.has("defaultInputModes") && !tree.get("defaultInputModes").isArray()) {
violations.add(
new RuleViolation("'defaultInputModes' field must be an array", "/defaultInputModes"));
}

if (tree.has("defaultOutputModes") && !tree.get("defaultOutputModes").isArray()) {
violations.add(new RuleViolation("'defaultOutputModes' field must be an array",
"/defaultOutputModes"));
}

if (!violations.isEmpty()) {
throw new RuleViolationException("Invalid Agent Card", RuleType.VALIDITY, level.name(),
violations);
}

} catch (RuleViolationException e) {
throw e;
} catch (Exception e) {
throw new RuleViolationException("Invalid Agent Card JSON: " + e.getMessage(),
RuleType.VALIDITY, level.name(), e);
}
}

@Override
public void validateReferences(TypedContent content, List<ArtifactReference> references)
throws RuleViolationException {
// Agent Cards don't support references in MVP
if (references != null && !references.isEmpty()) {
throw new RuleViolationException("Agent Cards do not support references",
RuleType.INTEGRITY, "NONE",
Collections.singleton(new RuleViolation("References are not supported for Agent Cards", "")));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.apicurio.registry.types.provider;

import io.apicurio.registry.content.AgentCardContentAccepter;
import io.apicurio.registry.content.ContentAccepter;
import io.apicurio.registry.content.canon.ContentCanonicalizer;
import io.apicurio.registry.json.content.canon.JsonContentCanonicalizer;
import io.apicurio.registry.content.dereference.ContentDereferencer;
import io.apicurio.registry.content.dereference.NoopContentDereferencer;
import io.apicurio.registry.content.extract.AgentCardContentExtractor;
import io.apicurio.registry.content.extract.ContentExtractor;
import io.apicurio.registry.content.refs.DefaultReferenceArtifactIdentifierExtractor;
import io.apicurio.registry.content.refs.NoOpReferenceFinder;
import io.apicurio.registry.content.refs.ReferenceArtifactIdentifierExtractor;
import io.apicurio.registry.content.refs.ReferenceFinder;
import io.apicurio.registry.rules.compatibility.CompatibilityChecker;
import io.apicurio.registry.rules.compatibility.NoopCompatibilityChecker;
import io.apicurio.registry.rules.validity.AgentCardContentValidator;
import io.apicurio.registry.rules.validity.ContentValidator;
import io.apicurio.registry.types.ArtifactType;
import io.apicurio.registry.types.ContentTypes;

import java.util.Set;

/**
* Artifact type utility provider for A2A Agent Card artifacts.
*
* Agent Cards are JSON documents that describe AI agents following the A2A (Agent2Agent) protocol.
* They contain metadata about an agent's capabilities, skills, and communication endpoints.
*
* @see <a href="https://a2a-protocol.org/">A2A Protocol</a>
*/
public class AgentCardArtifactTypeUtilProvider extends AbstractArtifactTypeUtilProvider {

@Override
public String getArtifactType() {
return ArtifactType.AGENT_CARD;
}

@Override
public Set<String> getContentTypes() {
return Set.of(ContentTypes.APPLICATION_JSON);
}

@Override
protected ContentAccepter createContentAccepter() {
return new AgentCardContentAccepter();
}

@Override
protected CompatibilityChecker createCompatibilityChecker() {
// For MVP, no compatibility checking
// Future: implement Agent Card compatibility rules
return NoopCompatibilityChecker.INSTANCE;
}

@Override
protected ContentCanonicalizer createContentCanonicalizer() {
// Use JSON canonicalizer for consistent formatting
return new JsonContentCanonicalizer();
}

@Override
protected ContentValidator createContentValidator() {
return new AgentCardContentValidator();
}

@Override
protected ContentExtractor createContentExtractor() {
return new AgentCardContentExtractor();
}

@Override
protected ContentDereferencer createContentDereferencer() {
// Agent Cards don't support references in MVP
return NoopContentDereferencer.INSTANCE;
}

@Override
protected ReferenceFinder createReferenceFinder() {
// Agent Cards don't support references in MVP
return NoOpReferenceFinder.INSTANCE;
}

@Override
public boolean supportsReferencesWithContext() {
return false;
}

@Override
protected ReferenceArtifactIdentifierExtractor createReferenceArtifactIdentifierExtractor() {
return new DefaultReferenceArtifactIdentifierExtractor();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public class DefaultArtifactTypeUtilProviderImpl implements ArtifactTypeUtilProv
new AsyncApiArtifactTypeUtilProvider(), new JsonArtifactTypeUtilProvider(),
new AvroArtifactTypeUtilProvider(), new GraphQLArtifactTypeUtilProvider(),
new KConnectArtifactTypeUtilProvider(), new WsdlArtifactTypeUtilProvider(),
new XsdArtifactTypeUtilProvider(), new XmlArtifactTypeUtilProvider()));
new XsdArtifactTypeUtilProvider(), new XmlArtifactTypeUtilProvider(),
new AgentCardArtifactTypeUtilProvider()));

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

Expand Down
Loading
Loading