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/__init__.py +42 -0
- capiscio_sdk/config.py +114 -0
- capiscio_sdk/errors.py +69 -0
- capiscio_sdk/executor.py +216 -0
- capiscio_sdk/infrastructure/__init__.py +5 -0
- capiscio_sdk/infrastructure/cache.py +73 -0
- capiscio_sdk/infrastructure/rate_limiter.py +110 -0
- capiscio_sdk/py.typed +0 -0
- capiscio_sdk/scoring/__init__.py +42 -0
- capiscio_sdk/scoring/availability.py +299 -0
- capiscio_sdk/scoring/compliance.py +314 -0
- capiscio_sdk/scoring/trust.py +340 -0
- capiscio_sdk/scoring/types.py +353 -0
- capiscio_sdk/types.py +234 -0
- capiscio_sdk/validators/__init__.py +18 -0
- capiscio_sdk/validators/agent_card.py +444 -0
- capiscio_sdk/validators/certificate.py +384 -0
- capiscio_sdk/validators/message.py +360 -0
- capiscio_sdk/validators/protocol.py +162 -0
- capiscio_sdk/validators/semver.py +202 -0
- capiscio_sdk/validators/signature.py +234 -0
- capiscio_sdk/validators/url_security.py +269 -0
- capiscio_sdk-0.2.0.dist-info/METADATA +221 -0
- capiscio_sdk-0.2.0.dist-info/RECORD +26 -0
- capiscio_sdk-0.2.0.dist-info/WHEEL +4 -0
- capiscio_sdk-0.2.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Message validation logic."""
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Dict, List
|
|
3
|
+
from ..types import ValidationResult, ValidationIssue, ValidationSeverity
|
|
4
|
+
from ..scoring import TrustScorer, AvailabilityScorer
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ..scoring.types import ComplianceScore
|
|
8
|
+
from .url_security import URLSecurityValidator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MessageValidator:
|
|
12
|
+
"""Validates A2A message structure and content (per official A2A spec)."""
|
|
13
|
+
|
|
14
|
+
# Based on official A2A specification from a2a-python SDK
|
|
15
|
+
REQUIRED_FIELDS = ["messageId", "role", "parts"]
|
|
16
|
+
VALID_ROLES = ["agent", "user"]
|
|
17
|
+
VALID_PART_KINDS = ["text", "file", "data"]
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
"""Initialize message validator."""
|
|
21
|
+
self._url_validator = URLSecurityValidator()
|
|
22
|
+
self._trust_scorer = TrustScorer()
|
|
23
|
+
self._availability_scorer = AvailabilityScorer()
|
|
24
|
+
|
|
25
|
+
def validate(self, message: Dict[str, Any], skip_signature_verification: bool = True) -> ValidationResult:
|
|
26
|
+
"""
|
|
27
|
+
Validate an A2A message against official specification.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
message: The message to validate (dict representation of Message object)
|
|
31
|
+
skip_signature_verification: Whether to skip signature verification
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
ValidationResult with three-dimensional scoring
|
|
35
|
+
"""
|
|
36
|
+
issues: List[ValidationIssue] = []
|
|
37
|
+
|
|
38
|
+
# Check required fields
|
|
39
|
+
for field in self.REQUIRED_FIELDS:
|
|
40
|
+
if field not in message:
|
|
41
|
+
issues.append(
|
|
42
|
+
ValidationIssue(
|
|
43
|
+
severity=ValidationSeverity.ERROR,
|
|
44
|
+
code="MISSING_REQUIRED_FIELD",
|
|
45
|
+
message=f"Required field '{field}' is missing",
|
|
46
|
+
path=field,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Validate messageId
|
|
51
|
+
if "messageId" in message:
|
|
52
|
+
if not isinstance(message["messageId"], str):
|
|
53
|
+
issues.append(
|
|
54
|
+
ValidationIssue(
|
|
55
|
+
severity=ValidationSeverity.ERROR,
|
|
56
|
+
code="INVALID_TYPE",
|
|
57
|
+
message="messageId must be a string",
|
|
58
|
+
path="messageId",
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
elif not message["messageId"]:
|
|
62
|
+
issues.append(
|
|
63
|
+
ValidationIssue(
|
|
64
|
+
severity=ValidationSeverity.ERROR,
|
|
65
|
+
code="EMPTY_FIELD",
|
|
66
|
+
message="messageId cannot be empty",
|
|
67
|
+
path="messageId",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Validate role
|
|
72
|
+
if "role" in message:
|
|
73
|
+
if not isinstance(message["role"], str):
|
|
74
|
+
issues.append(
|
|
75
|
+
ValidationIssue(
|
|
76
|
+
severity=ValidationSeverity.ERROR,
|
|
77
|
+
code="INVALID_TYPE",
|
|
78
|
+
message="role must be a string",
|
|
79
|
+
path="role",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
elif message["role"] not in self.VALID_ROLES:
|
|
83
|
+
issues.append(
|
|
84
|
+
ValidationIssue(
|
|
85
|
+
severity=ValidationSeverity.ERROR,
|
|
86
|
+
code="INVALID_VALUE",
|
|
87
|
+
message=f"role must be one of {self.VALID_ROLES}, got '{message['role']}'",
|
|
88
|
+
path="role",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Validate parts
|
|
93
|
+
if "parts" in message:
|
|
94
|
+
if not isinstance(message["parts"], list):
|
|
95
|
+
issues.append(
|
|
96
|
+
ValidationIssue(
|
|
97
|
+
severity=ValidationSeverity.ERROR,
|
|
98
|
+
code="INVALID_TYPE",
|
|
99
|
+
message="parts must be an array",
|
|
100
|
+
path="parts",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
if len(message["parts"]) == 0:
|
|
105
|
+
issues.append(
|
|
106
|
+
ValidationIssue(
|
|
107
|
+
severity=ValidationSeverity.WARNING,
|
|
108
|
+
code="EMPTY_ARRAY",
|
|
109
|
+
message="parts array is empty",
|
|
110
|
+
path="parts",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
parts_issues = self._validate_parts(message["parts"])
|
|
115
|
+
issues.extend(parts_issues)
|
|
116
|
+
|
|
117
|
+
# Validate optional fields if present
|
|
118
|
+
if "contextId" in message and message["contextId"] is not None:
|
|
119
|
+
if not isinstance(message["contextId"], str):
|
|
120
|
+
issues.append(
|
|
121
|
+
ValidationIssue(
|
|
122
|
+
severity=ValidationSeverity.WARNING,
|
|
123
|
+
code="INVALID_TYPE",
|
|
124
|
+
message="contextId must be a string if provided",
|
|
125
|
+
path="contextId",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if "taskId" in message and message["taskId"] is not None:
|
|
130
|
+
if not isinstance(message["taskId"], str):
|
|
131
|
+
issues.append(
|
|
132
|
+
ValidationIssue(
|
|
133
|
+
severity=ValidationSeverity.WARNING,
|
|
134
|
+
code="INVALID_TYPE",
|
|
135
|
+
message="taskId must be a string if provided",
|
|
136
|
+
path="taskId",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Calculate compliance score (message structure adherence)
|
|
141
|
+
compliance = self._calculate_message_compliance(message, issues)
|
|
142
|
+
|
|
143
|
+
# Trust score not applicable to messages (only for agent cards)
|
|
144
|
+
from ..scoring.types import TrustScore, TrustBreakdown, SignaturesBreakdown, ProviderBreakdown, SecurityBreakdown, DocumentationBreakdown, TrustRating
|
|
145
|
+
trust = TrustScore(
|
|
146
|
+
total=0,
|
|
147
|
+
raw_score=0,
|
|
148
|
+
rating=TrustRating.UNTRUSTED,
|
|
149
|
+
confidence_multiplier=0.6,
|
|
150
|
+
breakdown=TrustBreakdown(
|
|
151
|
+
signatures=SignaturesBreakdown(score=0, max_score=0, tested=False, has_valid_signature=False, multiple_signatures=False, covers_all_fields=False, is_recent=False, has_invalid_signature=False, has_expired_signature=False),
|
|
152
|
+
provider=ProviderBreakdown(score=0, max_score=0, tested=False, has_organization=False, has_url=False, url_reachable=None),
|
|
153
|
+
security=SecurityBreakdown(score=0, max_score=0, https_only=False, has_security_schemes=False, has_strong_auth=False, has_http_urls=False),
|
|
154
|
+
documentation=DocumentationBreakdown(score=0, max_score=0, has_documentation_url=False, has_terms_of_service=False, has_privacy_policy=False)
|
|
155
|
+
),
|
|
156
|
+
issues=["Trust scoring not applicable to runtime messages (only for agent discovery/cards)"],
|
|
157
|
+
partial_validation=True
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Availability not applicable for message validation
|
|
161
|
+
availability = self._availability_scorer.score_not_tested("Not applicable for message validation")
|
|
162
|
+
|
|
163
|
+
# Determine success
|
|
164
|
+
has_errors = any(i.severity == ValidationSeverity.ERROR for i in issues)
|
|
165
|
+
|
|
166
|
+
return ValidationResult(
|
|
167
|
+
success=not has_errors,
|
|
168
|
+
compliance=compliance,
|
|
169
|
+
trust=trust,
|
|
170
|
+
availability=availability,
|
|
171
|
+
issues=issues,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _calculate_message_compliance(self, message: Dict[str, Any], issues: List[ValidationIssue]) -> "ComplianceScore":
|
|
175
|
+
"""Calculate compliance score for message structure per A2A spec.
|
|
176
|
+
|
|
177
|
+
Official A2A message structure scoring:
|
|
178
|
+
- Required fields present (messageId, role, parts): 60 points
|
|
179
|
+
- Valid types and values: 20 points
|
|
180
|
+
- Valid parts structure: 15 points
|
|
181
|
+
- Data quality (non-empty messageId, valid part kinds): 5 points
|
|
182
|
+
"""
|
|
183
|
+
from ..scoring.types import (
|
|
184
|
+
ComplianceScore, ComplianceBreakdown,
|
|
185
|
+
CoreFieldsBreakdown, SkillsQualityBreakdown,
|
|
186
|
+
FormatComplianceBreakdown, DataQualityBreakdown,
|
|
187
|
+
get_compliance_rating
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
score = 100
|
|
191
|
+
|
|
192
|
+
# Check for missing required fields (60 points total, 20 per field)
|
|
193
|
+
missing_fields = [f for f in self.REQUIRED_FIELDS if f not in message]
|
|
194
|
+
score -= len(missing_fields) * 20
|
|
195
|
+
|
|
196
|
+
# Check for type errors (20 points)
|
|
197
|
+
type_errors = [i for i in issues if i.code == "INVALID_TYPE"]
|
|
198
|
+
score -= min(len(type_errors) * 5, 20)
|
|
199
|
+
|
|
200
|
+
# Check for invalid values (role, part kinds) (15 points)
|
|
201
|
+
value_errors = [i for i in issues if i.code in ("INVALID_VALUE", "UNKNOWN_TYPE")]
|
|
202
|
+
score -= min(len(value_errors) * 5, 15)
|
|
203
|
+
|
|
204
|
+
# Check parts errors and data quality (5 points)
|
|
205
|
+
parts_errors = [i for i in issues if i.path and "parts" in i.path and i.severity == ValidationSeverity.ERROR]
|
|
206
|
+
score -= min(len(parts_errors) * 2, 5)
|
|
207
|
+
|
|
208
|
+
score = max(0, min(100, score))
|
|
209
|
+
|
|
210
|
+
# Create simplified breakdown
|
|
211
|
+
present_fields = [f for f in self.REQUIRED_FIELDS if f in message]
|
|
212
|
+
breakdown = ComplianceBreakdown(
|
|
213
|
+
core_fields=CoreFieldsBreakdown(
|
|
214
|
+
score=int(len(present_fields) / len(self.REQUIRED_FIELDS) * 60),
|
|
215
|
+
max_score=60,
|
|
216
|
+
present=present_fields,
|
|
217
|
+
missing=missing_fields
|
|
218
|
+
),
|
|
219
|
+
skills_quality=SkillsQualityBreakdown(score=0, max_score=0), # N/A for messages
|
|
220
|
+
format_compliance=FormatComplianceBreakdown(
|
|
221
|
+
score=20 - min(len(type_errors) * 5, 20),
|
|
222
|
+
max_score=20,
|
|
223
|
+
valid_semver=True, # N/A
|
|
224
|
+
valid_protocol_version=True, # N/A
|
|
225
|
+
valid_url=True, # N/A for messages
|
|
226
|
+
valid_transports=True, # N/A
|
|
227
|
+
valid_mime_types=True # N/A
|
|
228
|
+
),
|
|
229
|
+
data_quality=DataQualityBreakdown(
|
|
230
|
+
score=20 - min(len(value_errors) * 5 + len(parts_errors) * 2, 20),
|
|
231
|
+
max_score=20,
|
|
232
|
+
no_duplicate_skill_ids=True, # N/A
|
|
233
|
+
field_lengths_valid=bool("messageId" in message and message.get("messageId")),
|
|
234
|
+
no_ssrf_risks=len([i for i in issues if "SSRF" in i.code or (i.path and "uri" in i.path.lower())]) == 0
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
issue_messages = [i.message for i in issues if i.severity == ValidationSeverity.ERROR]
|
|
239
|
+
|
|
240
|
+
return ComplianceScore(
|
|
241
|
+
total=score,
|
|
242
|
+
rating=get_compliance_rating(score),
|
|
243
|
+
breakdown=breakdown,
|
|
244
|
+
issues=issue_messages
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _validate_parts(self, parts: List[Any]) -> List[ValidationIssue]:
|
|
248
|
+
"""Validate message parts array (per A2A spec: TextPart, FilePart, DataPart)."""
|
|
249
|
+
issues: List[ValidationIssue] = []
|
|
250
|
+
|
|
251
|
+
for i, part in enumerate(parts):
|
|
252
|
+
if not isinstance(part, dict):
|
|
253
|
+
issues.append(
|
|
254
|
+
ValidationIssue(
|
|
255
|
+
severity=ValidationSeverity.ERROR,
|
|
256
|
+
code="INVALID_TYPE",
|
|
257
|
+
message=f"Part {i} must be an object",
|
|
258
|
+
path=f"parts[{i}]",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Check for 'kind' field (discriminator for Part types)
|
|
264
|
+
if "kind" not in part:
|
|
265
|
+
issues.append(
|
|
266
|
+
ValidationIssue(
|
|
267
|
+
severity=ValidationSeverity.ERROR,
|
|
268
|
+
code="MISSING_FIELD",
|
|
269
|
+
message=f"Part {i} missing 'kind' field",
|
|
270
|
+
path=f"parts[{i}].kind",
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
kind = part["kind"]
|
|
276
|
+
if kind not in self.VALID_PART_KINDS:
|
|
277
|
+
issues.append(
|
|
278
|
+
ValidationIssue(
|
|
279
|
+
severity=ValidationSeverity.WARNING,
|
|
280
|
+
code="UNKNOWN_TYPE",
|
|
281
|
+
message=f"Part {i} has unknown kind '{kind}' (expected: {self.VALID_PART_KINDS})",
|
|
282
|
+
path=f"parts[{i}].kind",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Validate based on part type
|
|
287
|
+
if kind == "text":
|
|
288
|
+
if "text" not in part:
|
|
289
|
+
issues.append(
|
|
290
|
+
ValidationIssue(
|
|
291
|
+
severity=ValidationSeverity.ERROR,
|
|
292
|
+
code="MISSING_FIELD",
|
|
293
|
+
message=f"TextPart {i} must have 'text' field",
|
|
294
|
+
path=f"parts[{i}].text",
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
elif not isinstance(part["text"], str):
|
|
298
|
+
issues.append(
|
|
299
|
+
ValidationIssue(
|
|
300
|
+
severity=ValidationSeverity.ERROR,
|
|
301
|
+
code="INVALID_TYPE",
|
|
302
|
+
message=f"TextPart {i} 'text' must be a string",
|
|
303
|
+
path=f"parts[{i}].text",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
elif kind == "file":
|
|
308
|
+
if "file" not in part:
|
|
309
|
+
issues.append(
|
|
310
|
+
ValidationIssue(
|
|
311
|
+
severity=ValidationSeverity.ERROR,
|
|
312
|
+
code="MISSING_FIELD",
|
|
313
|
+
message=f"FilePart {i} must have 'file' field",
|
|
314
|
+
path=f"parts[{i}].file",
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
file_obj = part["file"]
|
|
319
|
+
if not isinstance(file_obj, dict):
|
|
320
|
+
issues.append(
|
|
321
|
+
ValidationIssue(
|
|
322
|
+
severity=ValidationSeverity.ERROR,
|
|
323
|
+
code="INVALID_TYPE",
|
|
324
|
+
message=f"FilePart {i} 'file' must be an object",
|
|
325
|
+
path=f"parts[{i}].file",
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
# Must have either 'bytes' or 'uri'
|
|
330
|
+
if "bytes" not in file_obj and "uri" not in file_obj:
|
|
331
|
+
issues.append(
|
|
332
|
+
ValidationIssue(
|
|
333
|
+
severity=ValidationSeverity.ERROR,
|
|
334
|
+
code="MISSING_FIELD",
|
|
335
|
+
message=f"FilePart {i} must have either 'bytes' or 'uri'",
|
|
336
|
+
path=f"parts[{i}].file",
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
elif kind == "data":
|
|
341
|
+
if "data" not in part:
|
|
342
|
+
issues.append(
|
|
343
|
+
ValidationIssue(
|
|
344
|
+
severity=ValidationSeverity.ERROR,
|
|
345
|
+
code="MISSING_FIELD",
|
|
346
|
+
message=f"DataPart {i} must have 'data' field",
|
|
347
|
+
path=f"parts[{i}].data",
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
elif not isinstance(part["data"], dict):
|
|
351
|
+
issues.append(
|
|
352
|
+
ValidationIssue(
|
|
353
|
+
severity=ValidationSeverity.ERROR,
|
|
354
|
+
code="INVALID_TYPE",
|
|
355
|
+
message=f"DataPart {i} 'data' must be an object",
|
|
356
|
+
path=f"parts[{i}].data",
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return issues
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Protocol-level validation logic."""
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
from ..types import ValidationResult, ValidationIssue, ValidationSeverity, create_simple_validation_result
|
|
4
|
+
from .semver import SemverValidator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProtocolValidator:
|
|
8
|
+
"""Validates A2A protocol compliance."""
|
|
9
|
+
|
|
10
|
+
SUPPORTED_VERSIONS = ["1.0", "1.0.0", "0.3.0"]
|
|
11
|
+
VALID_MESSAGE_TYPES = ["request", "response", "event", "error"]
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
"""Initialize protocol validator."""
|
|
15
|
+
self._semver_validator = SemverValidator()
|
|
16
|
+
|
|
17
|
+
def validate_protocol_version(self, version: str) -> ValidationResult:
|
|
18
|
+
"""
|
|
19
|
+
Validate A2A protocol version.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
version: Protocol version string
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
ValidationResult indicating if version is supported
|
|
26
|
+
"""
|
|
27
|
+
issues: List[ValidationIssue] = []
|
|
28
|
+
score = 100
|
|
29
|
+
|
|
30
|
+
if not version:
|
|
31
|
+
issues.append(
|
|
32
|
+
ValidationIssue(
|
|
33
|
+
severity=ValidationSeverity.ERROR,
|
|
34
|
+
code="MISSING_VERSION",
|
|
35
|
+
message="Protocol version is required",
|
|
36
|
+
path="version",
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
score = 0
|
|
40
|
+
else:
|
|
41
|
+
# Validate semver format
|
|
42
|
+
semver_result = self._semver_validator.validate_version(version, "protocolVersion")
|
|
43
|
+
issues.extend(semver_result.issues)
|
|
44
|
+
|
|
45
|
+
# Check if version is supported
|
|
46
|
+
if version not in self.SUPPORTED_VERSIONS:
|
|
47
|
+
issues.append(
|
|
48
|
+
ValidationIssue(
|
|
49
|
+
severity=ValidationSeverity.WARNING,
|
|
50
|
+
code="UNSUPPORTED_VERSION",
|
|
51
|
+
message=f"Protocol version '{version}' may not be supported",
|
|
52
|
+
path="version",
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
score = 60
|
|
56
|
+
|
|
57
|
+
return create_simple_validation_result(
|
|
58
|
+
success=score > 0 and not any(i.severity == ValidationSeverity.ERROR for i in issues),
|
|
59
|
+
issues=issues,
|
|
60
|
+
simple_score=score,
|
|
61
|
+
dimension="compliance"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def validate_headers(self, headers: Dict[str, str]) -> ValidationResult:
|
|
65
|
+
"""
|
|
66
|
+
Validate A2A protocol headers.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
headers: HTTP headers dictionary
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
ValidationResult for headers
|
|
73
|
+
"""
|
|
74
|
+
issues: List[ValidationIssue] = []
|
|
75
|
+
score = 100
|
|
76
|
+
|
|
77
|
+
# Check Content-Type
|
|
78
|
+
content_type = headers.get("content-type", "").lower()
|
|
79
|
+
if content_type and "application/json" not in content_type:
|
|
80
|
+
issues.append(
|
|
81
|
+
ValidationIssue(
|
|
82
|
+
severity=ValidationSeverity.WARNING,
|
|
83
|
+
code="UNEXPECTED_CONTENT_TYPE",
|
|
84
|
+
message=f"Expected application/json, got {content_type}",
|
|
85
|
+
path="headers.content-type",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
score -= 10
|
|
89
|
+
|
|
90
|
+
# Check for A2A version header
|
|
91
|
+
if "x-a2a-version" not in headers and "X-A2A-Version" not in headers:
|
|
92
|
+
issues.append(
|
|
93
|
+
ValidationIssue(
|
|
94
|
+
severity=ValidationSeverity.WARNING,
|
|
95
|
+
code="MISSING_HEADER",
|
|
96
|
+
message="X-A2A-Version header is recommended",
|
|
97
|
+
path="headers.x-a2a-version",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
score -= 5
|
|
101
|
+
|
|
102
|
+
# Check for suspicious headers
|
|
103
|
+
for header in headers:
|
|
104
|
+
if header.lower().startswith("x-forwarded-"):
|
|
105
|
+
issues.append(
|
|
106
|
+
ValidationIssue(
|
|
107
|
+
severity=ValidationSeverity.INFO,
|
|
108
|
+
code="PROXY_HEADER",
|
|
109
|
+
message=f"Proxy header detected: {header}",
|
|
110
|
+
path=f"headers.{header}",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
score = max(0, score)
|
|
115
|
+
|
|
116
|
+
return create_simple_validation_result(
|
|
117
|
+
success=score >= 70,
|
|
118
|
+
issues=issues,
|
|
119
|
+
simple_score=score,
|
|
120
|
+
dimension="compliance"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def validate_message_type(self, message_type: Optional[str]) -> ValidationResult:
|
|
124
|
+
"""
|
|
125
|
+
Validate message type.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
message_type: Type of message
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
ValidationResult for message type
|
|
132
|
+
"""
|
|
133
|
+
issues: List[ValidationIssue] = []
|
|
134
|
+
score = 100
|
|
135
|
+
|
|
136
|
+
if not message_type:
|
|
137
|
+
issues.append(
|
|
138
|
+
ValidationIssue(
|
|
139
|
+
severity=ValidationSeverity.WARNING,
|
|
140
|
+
code="MISSING_MESSAGE_TYPE",
|
|
141
|
+
message="Message type not specified",
|
|
142
|
+
path="type",
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
score = 70
|
|
146
|
+
elif message_type not in self.VALID_MESSAGE_TYPES:
|
|
147
|
+
issues.append(
|
|
148
|
+
ValidationIssue(
|
|
149
|
+
severity=ValidationSeverity.WARNING,
|
|
150
|
+
code="UNKNOWN_MESSAGE_TYPE",
|
|
151
|
+
message=f"Unknown message type: {message_type}",
|
|
152
|
+
path="type",
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
score = 80
|
|
156
|
+
|
|
157
|
+
return create_simple_validation_result(
|
|
158
|
+
success=True, # Message type is informational
|
|
159
|
+
issues=issues,
|
|
160
|
+
simple_score=score,
|
|
161
|
+
dimension="compliance"
|
|
162
|
+
)
|