capiscio-sdk 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
capiscio_sdk/types.py ADDED
@@ -0,0 +1,234 @@
1
+ """Core types for Capiscio A2A Security."""
2
+ from typing import List, Dict, Any, Optional
3
+ from pydantic import BaseModel, Field
4
+ from enum import Enum
5
+ import warnings
6
+
7
+
8
+ class ValidationSeverity(str, Enum):
9
+ """Severity level for validation issues."""
10
+
11
+ ERROR = "error"
12
+ WARNING = "warning"
13
+ INFO = "info"
14
+
15
+
16
+ class ValidationIssue(BaseModel):
17
+ """A single validation issue."""
18
+
19
+ severity: ValidationSeverity
20
+ code: str
21
+ message: str
22
+ path: Optional[str] = None
23
+ details: Optional[Dict[str, Any]] = None
24
+
25
+
26
+ class ValidationResult(BaseModel):
27
+ """Result of a validation operation with multi-dimensional scoring.
28
+
29
+ Provides three independent score dimensions:
30
+ - compliance: A2A protocol specification adherence (0-100)
31
+ - trust: Security and authenticity signals (0-100)
32
+ - availability: Operational readiness (0-100, optional)
33
+
34
+ The legacy `score` field is maintained for backward compatibility
35
+ and returns the compliance.total value.
36
+ """
37
+
38
+ success: bool
39
+ issues: List[ValidationIssue] = Field(default_factory=list)
40
+ metadata: Dict[str, Any] = Field(default_factory=dict)
41
+
42
+ # Three-dimensional scores
43
+ compliance: Optional["ComplianceScore"] = None
44
+ trust: Optional["TrustScore"] = None
45
+ availability: Optional["AvailabilityScore"] = None
46
+
47
+ # Legacy single score (deprecated, for backward compatibility)
48
+ legacy_score: Optional[int] = Field(default=None, alias="score")
49
+
50
+ @property
51
+ def score(self) -> int:
52
+ """Legacy score property for backward compatibility.
53
+
54
+ Returns the compliance score total. This property is deprecated.
55
+ Use result.compliance.total instead.
56
+
57
+ Returns:
58
+ Compliance score (0-100)
59
+ """
60
+ warnings.warn(
61
+ "The 'score' property is deprecated. Use 'compliance.total', "
62
+ "'trust.total', or 'availability.total' instead for more specific scoring.",
63
+ DeprecationWarning,
64
+ stacklevel=2
65
+ )
66
+
67
+ # Return compliance score if available, otherwise legacy score, otherwise 0
68
+ if self.compliance:
69
+ return self.compliance.total
70
+ if self.legacy_score is not None:
71
+ return self.legacy_score
72
+ return 0
73
+
74
+ @property
75
+ def recommendation(self) -> str:
76
+ """Get overall recommendation based on all score dimensions.
77
+
78
+ Returns:
79
+ Human-readable recommendation string
80
+ """
81
+ if not self.compliance or not self.trust:
82
+ return "Incomplete validation - run full validation for recommendations"
83
+
84
+ compliance_total = self.compliance.total
85
+ trust_total = self.trust.total
86
+
87
+ # Both high
88
+ if compliance_total >= 90 and trust_total >= 80:
89
+ return "Excellent - Highly compliant and trusted agent"
90
+
91
+ # High compliance, lower trust
92
+ if compliance_total >= 90 and trust_total < 60:
93
+ return "Good compliance but low trust - verify signatures and security"
94
+
95
+ # High trust, lower compliance
96
+ if trust_total >= 80 and compliance_total < 75:
97
+ return "Trusted but not fully compliant - fix spec violations"
98
+
99
+ # Both moderate
100
+ if compliance_total >= 60 and trust_total >= 40:
101
+ return "Acceptable - improvements recommended"
102
+
103
+ # Low scores
104
+ return "Not recommended - significant issues found"
105
+
106
+ @property
107
+ def errors(self) -> List[ValidationIssue]:
108
+ """Get only error-level issues."""
109
+ return [i for i in self.issues if i.severity == ValidationSeverity.ERROR]
110
+
111
+ @property
112
+ def warnings(self) -> List[ValidationIssue]:
113
+ """Get only warning-level issues."""
114
+ return [i for i in self.issues if i.severity == ValidationSeverity.WARNING]
115
+
116
+
117
+ # Import here to avoid circular dependency
118
+ from .scoring.types import ComplianceScore, TrustScore, AvailabilityScore # noqa: E402
119
+
120
+ # Update forward references
121
+ ValidationResult.model_rebuild()
122
+
123
+
124
+ # Helper function for simple validators
125
+ def create_simple_validation_result(
126
+ success: bool,
127
+ issues: List[ValidationIssue],
128
+ simple_score: int = 100,
129
+ dimension: str = "compliance"
130
+ ) -> ValidationResult:
131
+ """Create a ValidationResult for simple validators that don't need full scoring.
132
+
133
+ Args:
134
+ success: Whether validation succeeded
135
+ issues: List of validation issues
136
+ simple_score: Simple 0-100 score
137
+ dimension: Which dimension to apply score to ("compliance" or "trust")
138
+
139
+ Returns:
140
+ ValidationResult with simplified scoring
141
+ """
142
+ from .scoring.types import (
143
+ ComplianceScore, TrustScore, ComplianceBreakdown, TrustBreakdown,
144
+ CoreFieldsBreakdown, SkillsQualityBreakdown,
145
+ FormatComplianceBreakdown, DataQualityBreakdown,
146
+ SignaturesBreakdown, ProviderBreakdown,
147
+ SecurityBreakdown, DocumentationBreakdown,
148
+ get_compliance_rating, get_trust_rating
149
+ )
150
+
151
+ issue_messages = [i.message for i in issues if i.severity == ValidationSeverity.ERROR]
152
+
153
+ if dimension == "compliance":
154
+ # Create minimal compliance score
155
+ compliance = ComplianceScore(
156
+ total=simple_score,
157
+ rating=get_compliance_rating(simple_score),
158
+ breakdown=ComplianceBreakdown(
159
+ core_fields=CoreFieldsBreakdown(score=simple_score, present=[], missing=[]),
160
+ skills_quality=SkillsQualityBreakdown(score=0),
161
+ format_compliance=FormatComplianceBreakdown(score=0),
162
+ data_quality=DataQualityBreakdown(score=0)
163
+ ),
164
+ issues=issue_messages
165
+ )
166
+ trust = TrustScore(
167
+ total=0,
168
+ raw_score=0,
169
+ confidence_multiplier=0.6,
170
+ rating=get_trust_rating(0),
171
+ breakdown=TrustBreakdown(
172
+ signatures=SignaturesBreakdown(score=0),
173
+ provider=ProviderBreakdown(score=0),
174
+ security=SecurityBreakdown(score=0),
175
+ documentation=DocumentationBreakdown(score=0)
176
+ )
177
+ )
178
+ else: # trust
179
+ compliance = ComplianceScore(
180
+ total=0,
181
+ rating=get_compliance_rating(0),
182
+ breakdown=ComplianceBreakdown(
183
+ core_fields=CoreFieldsBreakdown(score=0, present=[], missing=[]),
184
+ skills_quality=SkillsQualityBreakdown(score=0),
185
+ format_compliance=FormatComplianceBreakdown(score=0),
186
+ data_quality=DataQualityBreakdown(score=0)
187
+ )
188
+ )
189
+ trust = TrustScore(
190
+ total=simple_score,
191
+ raw_score=simple_score,
192
+ confidence_multiplier=1.0,
193
+ rating=get_trust_rating(simple_score),
194
+ breakdown=TrustBreakdown(
195
+ signatures=SignaturesBreakdown(score=simple_score),
196
+ provider=ProviderBreakdown(score=0),
197
+ security=SecurityBreakdown(score=0),
198
+ documentation=DocumentationBreakdown(score=0)
199
+ ),
200
+ issues=issue_messages
201
+ )
202
+
203
+ from .scoring import AvailabilityScorer
204
+ availability_scorer = AvailabilityScorer()
205
+ availability = availability_scorer.score_not_tested("Not applicable")
206
+
207
+ return ValidationResult(
208
+ success=success,
209
+ compliance=compliance,
210
+ trust=trust,
211
+ availability=availability,
212
+ issues=issues
213
+ )
214
+
215
+
216
+ class CacheEntry(BaseModel):
217
+ """Cached validation result with TTL."""
218
+
219
+ result: ValidationResult
220
+ cached_at: float # Unix timestamp
221
+ ttl: int # Seconds
222
+
223
+
224
+ class RateLimitInfo(BaseModel):
225
+ """Rate limit information."""
226
+
227
+ requests_allowed: int
228
+ requests_used: int
229
+ reset_at: float # Unix timestamp
230
+
231
+ @property
232
+ def requests_remaining(self) -> int:
233
+ """Remaining requests."""
234
+ return max(0, self.requests_allowed - self.requests_used)
@@ -0,0 +1,18 @@
1
+ """Validators for A2A message components."""
2
+ from .message import MessageValidator
3
+ from .protocol import ProtocolValidator
4
+ from .url_security import URLSecurityValidator
5
+ from .signature import SignatureValidator
6
+ from .semver import SemverValidator
7
+ from .agent_card import AgentCardValidator
8
+ from .certificate import CertificateValidator
9
+
10
+ __all__ = [
11
+ "MessageValidator",
12
+ "ProtocolValidator",
13
+ "URLSecurityValidator",
14
+ "SignatureValidator",
15
+ "SemverValidator",
16
+ "AgentCardValidator",
17
+ "CertificateValidator",
18
+ ]
@@ -0,0 +1,444 @@
1
+ """Agent Card validation for A2A protocol.
2
+
3
+ This module provides validation for Agent Card discovery documents as specified
4
+ in the A2A protocol. It validates schema structure, required fields, capabilities,
5
+ skills, and provider information.
6
+
7
+ Validates:
8
+ - Schema structure and required fields
9
+ - URL validity and security
10
+ - Protocol version compatibility
11
+ - Capabilities configuration
12
+ - Skills definitions
13
+ - Provider information
14
+ - Transport configuration
15
+ """
16
+
17
+ from typing import Any, Dict, List, Optional
18
+ import httpx
19
+ from ..types import ValidationResult, ValidationIssue, ValidationSeverity
20
+ from ..scoring import ComplianceScorer, TrustScorer, AvailabilityScorer
21
+ from .url_security import URLSecurityValidator
22
+ from .semver import SemverValidator
23
+
24
+
25
+ class AgentCardValidator:
26
+ """Validates Agent Card discovery documents per A2A specification.
27
+
28
+ Agent Cards are static metadata documents used for agent discovery.
29
+ This validator checks schema compliance, security requirements, and
30
+ configuration correctness.
31
+ """
32
+
33
+ # Required fields per A2A specification
34
+ REQUIRED_FIELDS = [
35
+ "name",
36
+ "description",
37
+ "url",
38
+ "version",
39
+ "protocolVersion",
40
+ "preferredTransport",
41
+ "capabilities",
42
+ "provider",
43
+ "skills"
44
+ ]
45
+
46
+ # Valid transport protocols per A2A spec
47
+ VALID_TRANSPORTS = ["JSONRPC", "GRPC", "HTTP+JSON"]
48
+
49
+ def __init__(
50
+ self,
51
+ http_client: Optional[httpx.AsyncClient] = None,
52
+ url_validator: Optional[URLSecurityValidator] = None,
53
+ semver_validator: Optional[SemverValidator] = None
54
+ ):
55
+ """Initialize agent card validator.
56
+
57
+ Args:
58
+ http_client: Optional HTTP client for fetching cards
59
+ url_validator: Optional URL security validator
60
+ semver_validator: Optional semantic version validator
61
+ """
62
+ self.http_client = http_client or httpx.AsyncClient(timeout=10.0)
63
+ self.url_validator = url_validator or URLSecurityValidator()
64
+ self.semver_validator = semver_validator or SemverValidator()
65
+
66
+ # Initialize scorers
67
+ self.compliance_scorer = ComplianceScorer()
68
+ self.trust_scorer = TrustScorer()
69
+ self.availability_scorer = AvailabilityScorer()
70
+
71
+ async def fetch_and_validate(self, agent_url: str) -> ValidationResult:
72
+ """Fetch agent card from URL and validate it.
73
+
74
+ Args:
75
+ agent_url: Base URL of the agent
76
+
77
+ Returns:
78
+ ValidationResult with issues and score
79
+ """
80
+ issues: List[ValidationIssue] = []
81
+
82
+ try:
83
+ # Validate the agent URL first
84
+ url_result = self.url_validator.validate_url(agent_url, field_name="agent_url", require_https=True)
85
+ if not url_result.success:
86
+ issues.extend(url_result.issues)
87
+ # Return with zero scores for all dimensions
88
+ return ValidationResult(
89
+ success=False,
90
+ compliance=self.compliance_scorer.score_agent_card({}, issues),
91
+ trust=self.trust_scorer.score_agent_card({}, issues),
92
+ availability=self.availability_scorer.score_not_tested("URL validation failed"),
93
+ issues=issues
94
+ )
95
+
96
+ # Fetch agent card from well-known location
97
+ card_url = f"{agent_url.rstrip('/')}/.well-known/agent-card.json"
98
+ response = await self.http_client.get(card_url)
99
+ response.raise_for_status()
100
+
101
+ card_data = response.json()
102
+
103
+ # Validate the fetched card
104
+ return self.validate_agent_card(card_data)
105
+
106
+ except httpx.HTTPStatusError as e:
107
+ issues.append(ValidationIssue(
108
+ severity=ValidationSeverity.ERROR,
109
+ code="AGENT_CARD_HTTP_ERROR",
110
+ message=f"Failed to fetch agent card (HTTP {e.response.status_code}): {str(e)}",
111
+ path="agent_card"
112
+ ))
113
+ return ValidationResult(
114
+ success=False,
115
+ compliance=self.compliance_scorer.score_agent_card({}, issues),
116
+ trust=self.trust_scorer.score_agent_card({}, issues),
117
+ availability=self.availability_scorer.score_not_tested("HTTP error fetching card"),
118
+ issues=issues
119
+ )
120
+ except httpx.RequestError as e:
121
+ issues.append(ValidationIssue(
122
+ severity=ValidationSeverity.ERROR,
123
+ code="AGENT_CARD_FETCH_FAILED",
124
+ message=f"Failed to fetch agent card: {str(e)}",
125
+ path="agent_card"
126
+ ))
127
+ return ValidationResult(
128
+ success=False,
129
+ compliance=self.compliance_scorer.score_agent_card({}, issues),
130
+ trust=self.trust_scorer.score_agent_card({}, issues),
131
+ availability=self.availability_scorer.score_not_tested("Network error fetching card"),
132
+ issues=issues
133
+ )
134
+ except Exception as e:
135
+ issues.append(ValidationIssue(
136
+ severity=ValidationSeverity.ERROR,
137
+ code="AGENT_CARD_VALIDATION_ERROR",
138
+ message=f"Agent card validation error: {str(e)}",
139
+ path="agent_card"
140
+ ))
141
+ return ValidationResult(
142
+ success=False,
143
+ compliance=self.compliance_scorer.score_agent_card({}, issues),
144
+ trust=self.trust_scorer.score_agent_card({}, issues),
145
+ availability=self.availability_scorer.score_not_tested("Validation error"),
146
+ issues=issues
147
+ )
148
+
149
+ def validate_agent_card(self, card: Dict[str, Any], skip_signature_verification: bool = True) -> ValidationResult:
150
+ """Validate agent card structure and content.
151
+
152
+ Args:
153
+ card: Agent card dictionary
154
+ skip_signature_verification: Whether to skip signature verification (default True for now)
155
+
156
+ Returns:
157
+ ValidationResult with three-dimensional scoring
158
+ """
159
+ issues: List[ValidationIssue] = []
160
+
161
+ # 1. Validate required fields
162
+ issues.extend(self._validate_required_fields(card))
163
+
164
+ # 2. Validate URL
165
+ if "url" in card:
166
+ url_result = self.url_validator.validate_url(card["url"], field_name="agent_card.url", require_https=True)
167
+ issues.extend(url_result.issues)
168
+
169
+ # 3. Validate protocol version
170
+ if "protocolVersion" in card:
171
+ version_result = self.semver_validator.validate_version(card["protocolVersion"])
172
+ issues.extend(version_result.issues)
173
+
174
+ # 4. Validate transport
175
+ if "preferredTransport" in card:
176
+ issues.extend(self._validate_transport(card["preferredTransport"]))
177
+
178
+ # 5. Validate capabilities
179
+ if "capabilities" in card:
180
+ issues.extend(self._validate_capabilities(card["capabilities"]))
181
+
182
+ # 6. Validate provider
183
+ if "provider" in card:
184
+ issues.extend(self._validate_provider(card["provider"]))
185
+
186
+ # 7. Validate skills
187
+ if "skills" in card:
188
+ issues.extend(self._validate_skills(card["skills"]))
189
+
190
+ # 8. Validate additional interfaces (if present)
191
+ if "additionalInterfaces" in card:
192
+ issues.extend(self._validate_additional_interfaces(card["additionalInterfaces"], card))
193
+
194
+ # Calculate three-dimensional scores
195
+ compliance = self.compliance_scorer.score_agent_card(card, issues)
196
+ trust = self.trust_scorer.score_agent_card(card, issues, skip_signature_verification)
197
+ availability = self.availability_scorer.score_not_tested("Schema-only validation")
198
+
199
+ # Determine success based on error presence
200
+ has_errors = any(i.severity == ValidationSeverity.ERROR for i in issues)
201
+
202
+ return ValidationResult(
203
+ success=not has_errors,
204
+ compliance=compliance,
205
+ trust=trust,
206
+ availability=availability,
207
+ issues=issues
208
+ )
209
+
210
+ def _validate_required_fields(self, card: Dict[str, Any]) -> List[ValidationIssue]:
211
+ """Validate that all required fields are present."""
212
+ issues: List[ValidationIssue] = []
213
+
214
+ for field in self.REQUIRED_FIELDS:
215
+ if field not in card or card[field] is None:
216
+ issues.append(ValidationIssue(
217
+ severity=ValidationSeverity.ERROR,
218
+ code="MISSING_REQUIRED_FIELD",
219
+ message=f"Agent card missing required field: {field}",
220
+ path=f"agent_card.{field}"
221
+ ))
222
+ elif isinstance(card[field], str) and not card[field].strip():
223
+ issues.append(ValidationIssue(
224
+ severity=ValidationSeverity.ERROR,
225
+ code="EMPTY_REQUIRED_FIELD",
226
+ message=f"Agent card required field is empty: {field}",
227
+ path=f"agent_card.{field}"
228
+ ))
229
+
230
+ return issues
231
+
232
+ def _validate_transport(self, transport: str) -> List[ValidationIssue]:
233
+ """Validate transport protocol."""
234
+ issues: List[ValidationIssue] = []
235
+
236
+ if transport not in self.VALID_TRANSPORTS:
237
+ issues.append(ValidationIssue(
238
+ severity=ValidationSeverity.ERROR,
239
+ code="INVALID_TRANSPORT",
240
+ message=f"Invalid transport protocol: {transport}. Valid options: {', '.join(self.VALID_TRANSPORTS)}",
241
+ path="agent_card.preferredTransport"
242
+ ))
243
+
244
+ return issues
245
+
246
+ def _validate_capabilities(self, capabilities: Dict[str, Any]) -> List[ValidationIssue]:
247
+ """Validate agent capabilities."""
248
+ issues: List[ValidationIssue] = []
249
+
250
+ if not isinstance(capabilities, dict):
251
+ issues.append(ValidationIssue(
252
+ severity=ValidationSeverity.ERROR,
253
+ code="INVALID_CAPABILITIES_TYPE",
254
+ message="Capabilities must be an object",
255
+ path="agent_card.capabilities"
256
+ ))
257
+ return issues
258
+
259
+ # Validate boolean capability flags
260
+ boolean_capabilities = ["streaming", "pushNotifications", "batchProcessing"]
261
+ for cap in boolean_capabilities:
262
+ if cap in capabilities and not isinstance(capabilities[cap], bool):
263
+ issues.append(ValidationIssue(
264
+ severity=ValidationSeverity.WARNING,
265
+ code="INVALID_CAPABILITY_TYPE",
266
+ message=f"Capability {cap} should be boolean",
267
+ path=f"agent_card.capabilities.{cap}"
268
+ ))
269
+
270
+ # Check for empty capabilities
271
+ if not capabilities:
272
+ issues.append(ValidationIssue(
273
+ severity=ValidationSeverity.WARNING,
274
+ code="EMPTY_CAPABILITIES",
275
+ message="Agent card has empty capabilities object",
276
+ path="agent_card.capabilities"
277
+ ))
278
+
279
+ return issues
280
+
281
+ def _validate_provider(self, provider: Dict[str, Any]) -> List[ValidationIssue]:
282
+ """Validate provider information."""
283
+ issues: List[ValidationIssue] = []
284
+
285
+ if not isinstance(provider, dict):
286
+ issues.append(ValidationIssue(
287
+ severity=ValidationSeverity.ERROR,
288
+ code="INVALID_PROVIDER_TYPE",
289
+ message="Provider must be an object",
290
+ path="agent_card.provider"
291
+ ))
292
+ return issues
293
+
294
+ # Check required provider fields
295
+ required_provider_fields = ["name"]
296
+ for field in required_provider_fields:
297
+ if field not in provider or not provider[field]:
298
+ issues.append(ValidationIssue(
299
+ severity=ValidationSeverity.ERROR,
300
+ code="MISSING_PROVIDER_FIELD",
301
+ message=f"Provider missing required field: {field}",
302
+ path=f"agent_card.provider.{field}"
303
+ ))
304
+
305
+ # Validate provider URL if present
306
+ if "url" in provider and provider["url"]:
307
+ url_result = self.url_validator.validate_url(provider["url"], field_name="agent_card.provider.url", require_https=False)
308
+ for issue in url_result.issues:
309
+ issues.append(ValidationIssue(
310
+ severity=issue.severity,
311
+ code=issue.code,
312
+ message=f"Provider URL: {issue.message}",
313
+ path="agent_card.provider.url"
314
+ ))
315
+
316
+ return issues
317
+
318
+ def _validate_skills(self, skills: List[Dict[str, Any]]) -> List[ValidationIssue]:
319
+ """Validate skills array."""
320
+ issues: List[ValidationIssue] = []
321
+
322
+ if not isinstance(skills, list):
323
+ issues.append(ValidationIssue(
324
+ severity=ValidationSeverity.ERROR,
325
+ code="INVALID_SKILLS_TYPE",
326
+ message="Skills must be an array",
327
+ path="agent_card.skills"
328
+ ))
329
+ return issues
330
+
331
+ if not skills:
332
+ issues.append(ValidationIssue(
333
+ severity=ValidationSeverity.WARNING,
334
+ code="EMPTY_SKILLS",
335
+ message="Agent card has empty skills array",
336
+ path="agent_card.skills"
337
+ ))
338
+ return issues
339
+
340
+ # Validate each skill
341
+ for idx, skill in enumerate(skills):
342
+ if not isinstance(skill, dict):
343
+ issues.append(ValidationIssue(
344
+ severity=ValidationSeverity.ERROR,
345
+ code="INVALID_SKILL_TYPE",
346
+ message=f"Skill at index {idx} must be an object",
347
+ path=f"agent_card.skills[{idx}]"
348
+ ))
349
+ continue
350
+
351
+ # Check required skill fields
352
+ if "name" not in skill or not skill["name"]:
353
+ issues.append(ValidationIssue(
354
+ severity=ValidationSeverity.ERROR,
355
+ code="MISSING_SKILL_NAME",
356
+ message=f"Skill at index {idx} missing name",
357
+ path=f"agent_card.skills[{idx}].name"
358
+ ))
359
+
360
+ if "description" not in skill or not skill["description"]:
361
+ issues.append(ValidationIssue(
362
+ severity=ValidationSeverity.WARNING,
363
+ code="MISSING_SKILL_DESCRIPTION",
364
+ message=f"Skill '{skill.get('name', idx)}' missing description",
365
+ path=f"agent_card.skills[{idx}].description"
366
+ ))
367
+
368
+ return issues
369
+
370
+ def _validate_additional_interfaces(
371
+ self,
372
+ interfaces: List[Dict[str, Any]],
373
+ card: Dict[str, Any]
374
+ ) -> List[ValidationIssue]:
375
+ """Validate additional interfaces configuration."""
376
+ issues: List[ValidationIssue] = []
377
+
378
+ if not isinstance(interfaces, list):
379
+ issues.append(ValidationIssue(
380
+ severity=ValidationSeverity.ERROR,
381
+ code="INVALID_INTERFACES_TYPE",
382
+ message="additionalInterfaces must be an array",
383
+ path="agent_card.additionalInterfaces"
384
+ ))
385
+ return issues
386
+
387
+ # Track URLs and transports to detect conflicts
388
+ url_transport_map: Dict[str, str] = {}
389
+ main_url = card.get("url", "")
390
+ preferred_transport = card.get("preferredTransport", "JSONRPC")
391
+
392
+ if main_url:
393
+ url_transport_map[main_url] = preferred_transport
394
+
395
+ for idx, interface in enumerate(interfaces):
396
+ if not isinstance(interface, dict):
397
+ issues.append(ValidationIssue(
398
+ severity=ValidationSeverity.ERROR,
399
+ code="INVALID_INTERFACE_TYPE",
400
+ message=f"Interface at index {idx} must be an object",
401
+ path=f"agent_card.additionalInterfaces[{idx}]"
402
+ ))
403
+ continue
404
+
405
+ # Validate required interface fields
406
+ if "url" not in interface or not interface["url"]:
407
+ issues.append(ValidationIssue(
408
+ severity=ValidationSeverity.ERROR,
409
+ code="MISSING_INTERFACE_URL",
410
+ message=f"Interface at index {idx} missing URL",
411
+ path=f"agent_card.additionalInterfaces[{idx}].url"
412
+ ))
413
+ else:
414
+ # Validate interface URL
415
+ url_result = self.url_validator.validate_url(interface["url"], field_name=f"agent_card.additionalInterfaces[{idx}].url", require_https=True)
416
+ issues.extend(url_result.issues)
417
+
418
+ if "transport" not in interface or not interface["transport"]:
419
+ issues.append(ValidationIssue(
420
+ severity=ValidationSeverity.ERROR,
421
+ code="MISSING_INTERFACE_TRANSPORT",
422
+ message=f"Interface at index {idx} missing transport",
423
+ path=f"agent_card.additionalInterfaces[{idx}].transport"
424
+ ))
425
+ else:
426
+ # Validate transport
427
+ transport_issues = self._validate_transport(interface["transport"])
428
+ issues.extend(transport_issues)
429
+
430
+ # Check for transport conflicts on same URL
431
+ url = interface.get("url", "")
432
+ transport = interface["transport"]
433
+ if url and url in url_transport_map:
434
+ if url_transport_map[url] != transport:
435
+ issues.append(ValidationIssue(
436
+ severity=ValidationSeverity.ERROR,
437
+ code="TRANSPORT_URL_CONFLICT",
438
+ message=f"Conflicting transport protocols for URL {url}: {url_transport_map[url]} vs {transport}",
439
+ path=f"agent_card.additionalInterfaces[{idx}]"
440
+ ))
441
+ elif url:
442
+ url_transport_map[url] = transport
443
+
444
+ return issues