Skip to content
Draft
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
14 changes: 14 additions & 0 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,25 @@ jobs:
- name: Run manifest validation without dependencies
run: python -S ./conformance/bin/conformance-test manifest ./conformance/examples/basic/ard.json

- name: Run conformance tool tests without dependencies
run: python -S ./conformance/tests/test_media_type_diagnostics.py -v

- name: Validate extension media-type fixture without dependencies
run: python -S ./conformance/bin/conformance-test manifest ./conformance/tests/fixtures/extension-media-types.json

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install jsonschema

- name: Validate extension media-type fixture with JSON Schema
run: python ./conformance/bin/conformance-test manifest ./conformance/tests/fixtures/extension-media-types.json

- name: Run conformance tool tests with JSON Schema
run: python ./conformance/tests/test_media_type_diagnostics.py -v
env:
ARD_REQUIRE_JSONSCHEMA: "1"

- name: Make scripts executable
run: |
chmod +x ./conformance/bin/run-conformance-demo
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ lib-cov
coverage
*.lcov

# Python bytecode
__pycache__/
*.py[cod]

# nyc test coverage
.nyc_output

Expand Down
7 changes: 7 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ When checking an ARD manifest (`ard.json`), the tool executes the following vali
* **Strict URN Pattern Matching**: Enforces that each entry's `identifier` adheres strictly to the domain-anchored URN namespace format defined in the spec:
`urn:air:<publisher>:<namespace>:<agent-name>` (RFC 8141).
* **Value-or-Reference Delivery**: Enforces the mutual exclusivity constraint of the specification. Each entry **MUST** contain precisely one of either `"url"` (remote reference) or `"data"` (embedded payload), and will fail if both or neither are provided.
* **Media Type Diagnostics**:
* Accepts the standard discovery media types without a diagnostic.
* Warns clearly when a standard discovery type is missing required parameters or carries unrecognized parameters.
* Reports well-formed, unrecognized `application/*` extension types as informational. ARD's `type` term is intentionally open, so extension types do not require registration in the conformance tool.
* Warns when the deprecated `application/mcp-server+json` form from **ADR-0008** is used and names `application/mcp-server-card+json` as its replacement.
* Gives a distinct syntax warning for malformed media types.
* Retains the existing warning for well-formed, unrecognized types outside the `application` top-level type.
* **Discovery Constraints (§D.2)**:
* Warns when `"representativeQueries"` is **absent** — the semantic index is built from it, so such an entry is a valid catalog entry but not a discoverable ARD entry.
* Warns when it is present but does not contain **2 to 5** natural-language queries.
Expand Down
168 changes: 154 additions & 14 deletions conformance/bin/conformance-test
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,68 @@ if hasattr(sys.stdout, 'reconfigure'):
# Strict URN Regex matching urn:air:<publisher>:<namespace>:<agent-name>
URN_REGEX = re.compile(r"^urn:air:([a-zA-Z0-9.-]+)(?::([a-zA-Z0-9._:-]+))?:([a-zA-Z0-9._-]+)$")

# RFC 6838 restricted-name for type/subtype plus RFC 9110 parameters.
RESTRICTED_NAME = r"[0-9A-Za-z][0-9A-Za-z!#$&^_.+\-]{0,126}"
TOKEN = r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+"
QUOTED_STRING = r'"(?:[\t !#-\[\]-~]|\\[\t !-~])*"'
MEDIA_TYPE_REGEX = re.compile(
rf"(?P<type>{RESTRICTED_NAME})/(?P<subtype>{RESTRICTED_NAME})"
rf"(?P<parameters>(?:[ \t]*;[ \t]*{TOKEN}[ \t]*=[ \t]*(?:{TOKEN}|{QUOTED_STRING}))*)"
)
MEDIA_TYPE_PARAMETER_REGEX = re.compile(
rf"[ \t]*;[ \t]*(?P<name>{TOKEN})[ \t]*=[ \t]*"
rf"(?P<value>{TOKEN}|{QUOTED_STRING})"
)

def parse_media_type(media_type):
match = MEDIA_TYPE_REGEX.fullmatch(media_type)
if match is None:
return None

base_media_type = (
f"{match.group('type').lower()}/{match.group('subtype').lower()}"
)
parameters = tuple(
sorted(
(
parameter.group("name").lower(),
parameter.group("value"),
)
for parameter in MEDIA_TYPE_PARAMETER_REGEX.finditer(
match.group("parameters")
)
)
)
parameter_names = [name for name, _ in parameters]
if len(parameter_names) != len(set(parameter_names)):
return None
return base_media_type, parameters

STANDARD_MEDIA_TYPES = (
"application/ai-catalog+json",
"application/agent-card+json",
"application/a2a-agent-card+json",
"application/mcp-server-card+json",
"application/agent-skills+zip",
"application/agent-skills+gzip",
'text/markdown; profile="urn:air:agent-skills"',
"application/ai-registry",
"application/ai-registry+json",
)
STANDARD_MEDIA_TYPE_KEYS = frozenset(
parse_media_type(media_type) for media_type in STANDARD_MEDIA_TYPES
)
STANDARD_BASE_MEDIA_TYPES = frozenset(
base_media_type for base_media_type, _ in STANDARD_MEDIA_TYPE_KEYS
)
STANDARD_PARAMETERS_BY_BASE = {
base_media_type: dict(parameters)
for base_media_type, parameters in STANDARD_MEDIA_TYPE_KEYS
}
DEPRECATED_MEDIA_TYPES = {
"application/mcp-server+json": "application/mcp-server-card+json",
}

# Colors for beautiful CLI output
COLOR_RESET = "\033[0m"
COLOR_BOLD = "\033[1m"
Expand All @@ -40,9 +102,82 @@ def print_failure(msg):
def print_warning(msg):
print(f" {COLOR_YELLOW}⚠{COLOR_RESET} {msg}")

def print_info(msg):
print(f" {COLOR_CYAN}ℹ{COLOR_RESET} {msg}")

def print_bullet(msg):
print(f" • {msg}")

def classify_media_type(media_type):
if not isinstance(media_type, str):
return (
"warning",
f"Media type must be a string; got {type(media_type).__name__}.",
)

parsed_media_type = parse_media_type(media_type)
if parsed_media_type is None:
return (
"warning",
f"Media type '{media_type}' is not a valid IANA media type. "
"Expected 'type/subtype' with optional parameters.",
)

base_media_type, parameters = parsed_media_type
if parsed_media_type in STANDARD_MEDIA_TYPE_KEYS:
return None

replacement = DEPRECATED_MEDIA_TYPES.get(base_media_type)
if replacement is not None:
return (
"warning",
f"Media type '{media_type}' was renamed by ADR-0008. "
f"Use '{replacement}'.",
)

if base_media_type in STANDARD_BASE_MEDIA_TYPES:
expected_parameters = STANDARD_PARAMETERS_BY_BASE[base_media_type]
actual_parameters = dict(parameters)
missing_parameters = [
f"{name}={value}"
for name, value in expected_parameters.items()
if actual_parameters.get(name) != value
]
unrecognized_parameters = [
name
for name, value in parameters
if expected_parameters.get(name) != value
]
differences = []
if missing_parameters:
differences.append(
"missing required parameters: " + ", ".join(missing_parameters)
)
if unrecognized_parameters:
differences.append(
"unrecognized parameters: "
+ ", ".join(unrecognized_parameters)
)
return (
"warning",
f"Media type '{media_type}' is based on standard discovery type "
f"'{base_media_type}' but has "
f"{'; '.join(differences)}.",
)

if base_media_type.startswith("application/"):
return (
"info",
f"Media type '{media_type}' is a valid application extension media type. "
"ARD permits extension types without core registration.",
)

return (
"warning",
f"Media type '{media_type}' is not one of standard discovery types: "
f"{list(STANDARD_MEDIA_TYPES)}.",
)

def parse_request_headers(header_args):
headers = {}
for raw_header in header_args:
Expand All @@ -66,6 +201,7 @@ class ConformanceTester:
def __init__(self):
self.errors = []
self.warnings = []
self.infos = []
self.jsonschema_available = False
try:
import jsonschema
Expand All @@ -81,6 +217,10 @@ class ConformanceTester:
self.warnings.append(msg)
print_warning(msg)

def add_info(self, msg):
self.infos.append(msg)
print_info(msg)

def _load_entry_schema(self):
"""The ARD entry schema is authoritative for entry structure (spec Appendix D.1)."""
schema_path = os.path.join(os.path.dirname(__file__), "../../spec/schemas/ard-entry.schema.json")
Expand Down Expand Up @@ -195,19 +335,13 @@ class ConformanceTester:
if not media_type:
self.add_error(f"[{label}] Missing required 'type' (mediaType).")
else:
valid_types = [
"application/ai-catalog+json",
"application/agent-card+json",
"application/a2a-agent-card+json",
"application/mcp-server-card+json",
"application/agent-skills+zip",
"application/agent-skills+gzip",
"text/markdown; profile=\"urn:air:agent-skills\"",
"application/ai-registry",
"application/ai-registry+json"
]
if media_type not in valid_types:
self.add_warning(f"[{label}] Media type '{media_type}' is not one of standard discovery types: {valid_types}.")
diagnostic = classify_media_type(media_type)
if diagnostic is not None:
severity, message = diagnostic
if severity == "info":
self.add_info(f"[{label}] {message}")
else:
self.add_warning(f"[{label}] {message}")

# Strict Value-or-Reference checks
has_url = "url" in entry
Expand Down Expand Up @@ -524,7 +658,13 @@ def main():
print_header("Conformance Validation Summary")
if success:
print(f"{COLOR_BOLD}{COLOR_GREEN}CONFORMANCE STATUS: PASS{COLOR_RESET}")
print(f"Validated with 0 critical specification errors and {len(tester.warnings)} warnings.")
summary = (
f"Validated with 0 critical specification errors and "
f"{len(tester.warnings)} warnings"
)
if tester.infos:
summary += f" and {len(tester.infos)} informational messages"
print(f"{summary}.")
sys.exit(0)
else:
print(f"{COLOR_BOLD}{COLOR_RED}CONFORMANCE STATUS: FAIL{COLOR_RESET}")
Expand Down
89 changes: 89 additions & 0 deletions conformance/tests/fixtures/extension-media-types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"specVersion": "1.0",
"host": {
"displayName": "Extension media type conformance fixture",
"identifier": "https://media-types.test"
},
"entries": [
{
"identifier": "urn:air:media-types.test:fixture:standard-mcp",
"displayName": "Standard MCP card",
"type": "application/mcp-server-card+json",
"url": "https://media-types.test/artifacts/standard-mcp",
"representativeQueries": [
"find the standard MCP fixture",
"locate the standard protocol card"
]
},
{
"identifier": "urn:air:media-types.test:fixture:standard-a2a",
"displayName": "Standard A2A card",
"type": "application/a2a-agent-card+json",
"url": "https://media-types.test/artifacts/standard-a2a",
"representativeQueries": [
"find the standard A2A fixture",
"locate the standard agent card"
]
},
{
"identifier": "urn:air:media-types.test:fixture:vendor-card",
"displayName": "Vendor extension card",
"type": "application/vnd.example.tool-manifest+json",
"url": "https://media-types.test/artifacts/vendor-card",
"representativeQueries": [
"find a vendor extension card",
"locate an example tool manifest"
]
},
{
"identifier": "urn:air:media-types.test:fixture:extension-bundle",
"displayName": "Extension bundle",
"type": "application/example-bundle+zip",
"url": "https://media-types.test/artifacts/extension-bundle",
"representativeQueries": [
"find an extension bundle",
"locate an example archive"
]
},
{
"identifier": "urn:air:media-types.test:fixture:extension-widget",
"displayName": "Extension widget",
"type": "application/x-example-widget",
"url": "https://media-types.test/artifacts/extension-widget",
"representativeQueries": [
"find an extension widget",
"locate a custom application artifact"
]
},
{
"identifier": "urn:air:media-types.test:fixture:deprecated-mcp",
"displayName": "Deprecated MCP card",
"type": "application/mcp-server+json",
"url": "https://media-types.test/artifacts/deprecated-mcp",
"representativeQueries": [
"find the deprecated MCP fixture",
"locate the transition media type"
]
},
{
"identifier": "urn:air:media-types.test:fixture:other-top-level",
"displayName": "Other top-level type",
"type": "text/x-example-notes",
"url": "https://media-types.test/artifacts/other-top-level",
"representativeQueries": [
"find text extension notes",
"locate a non-application artifact"
]
},
{
"identifier": "urn:air:media-types.test:fixture:malformed-type",
"displayName": "Malformed media type",
"type": "application/ example",
"url": "https://media-types.test/artifacts/malformed-type",
"representativeQueries": [
"find the malformed syntax fixture",
"locate an invalid media type example"
]
}
]
}
Loading