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,299 @@
|
|
|
1
|
+
"""Availability scorer for operational readiness.
|
|
2
|
+
|
|
3
|
+
Calculates availability score (0-100) based on:
|
|
4
|
+
- Primary endpoint (50 points)
|
|
5
|
+
- Transport protocol support (30 points)
|
|
6
|
+
- Response quality (20 points)
|
|
7
|
+
|
|
8
|
+
Only calculated when network tests are enabled.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
from ..types import ValidationIssue
|
|
13
|
+
from .types import (
|
|
14
|
+
AvailabilityScore,
|
|
15
|
+
AvailabilityBreakdown,
|
|
16
|
+
PrimaryEndpointBreakdown,
|
|
17
|
+
TransportSupportBreakdown,
|
|
18
|
+
ResponseQualityBreakdown,
|
|
19
|
+
get_availability_rating,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AvailabilityScorer:
|
|
24
|
+
"""Calculates availability scores for operational readiness."""
|
|
25
|
+
|
|
26
|
+
def score_not_tested(self, reason: str = "Network tests not enabled") -> AvailabilityScore:
|
|
27
|
+
"""Create availability score for when testing was skipped.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
reason: Why availability wasn't tested
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
AvailabilityScore with tested=False
|
|
34
|
+
"""
|
|
35
|
+
return AvailabilityScore(
|
|
36
|
+
total=None,
|
|
37
|
+
rating=None,
|
|
38
|
+
breakdown=None,
|
|
39
|
+
issues=[],
|
|
40
|
+
tested=False,
|
|
41
|
+
not_tested_reason=reason
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def score_endpoint_test(
|
|
45
|
+
self,
|
|
46
|
+
endpoint_responded: bool,
|
|
47
|
+
response_time: Optional[float] = None,
|
|
48
|
+
has_cors: Optional[bool] = None,
|
|
49
|
+
valid_tls: Optional[bool] = None,
|
|
50
|
+
issues: Optional[List[ValidationIssue]] = None
|
|
51
|
+
) -> AvailabilityScore:
|
|
52
|
+
"""Calculate availability score based on endpoint test results.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
endpoint_responded: Whether primary endpoint responded
|
|
56
|
+
response_time: Response time in seconds (if responded)
|
|
57
|
+
has_cors: Whether CORS headers present
|
|
58
|
+
valid_tls: Whether TLS certificate is valid
|
|
59
|
+
issues: List of validation issues found
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
AvailabilityScore with detailed breakdown
|
|
63
|
+
"""
|
|
64
|
+
if issues is None:
|
|
65
|
+
issues = []
|
|
66
|
+
|
|
67
|
+
# Score primary endpoint (50 points)
|
|
68
|
+
primary = self._score_primary_endpoint(
|
|
69
|
+
endpoint_responded,
|
|
70
|
+
response_time,
|
|
71
|
+
has_cors,
|
|
72
|
+
valid_tls,
|
|
73
|
+
issues
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Score transport support (30 points)
|
|
77
|
+
# For now, basic scoring - can be enhanced with actual transport tests
|
|
78
|
+
transport = self._score_transport_support(endpoint_responded, issues)
|
|
79
|
+
|
|
80
|
+
# Score response quality (20 points)
|
|
81
|
+
response_quality = self._score_response_quality(endpoint_responded, issues)
|
|
82
|
+
|
|
83
|
+
# Calculate total
|
|
84
|
+
total = primary.score + transport.score + response_quality.score
|
|
85
|
+
total = max(0, min(100, total))
|
|
86
|
+
|
|
87
|
+
# Create breakdown
|
|
88
|
+
breakdown = AvailabilityBreakdown(
|
|
89
|
+
primary_endpoint=primary,
|
|
90
|
+
transport_support=transport,
|
|
91
|
+
response_quality=response_quality
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Extract issue messages
|
|
95
|
+
issue_messages = [
|
|
96
|
+
issue.message for issue in issues
|
|
97
|
+
if self._is_availability_issue(issue)
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
return AvailabilityScore(
|
|
101
|
+
total=total,
|
|
102
|
+
rating=get_availability_rating(total),
|
|
103
|
+
breakdown=breakdown,
|
|
104
|
+
issues=issue_messages,
|
|
105
|
+
tested=True
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _score_primary_endpoint(
|
|
109
|
+
self,
|
|
110
|
+
responded: bool,
|
|
111
|
+
response_time: Optional[float],
|
|
112
|
+
has_cors: Optional[bool],
|
|
113
|
+
valid_tls: Optional[bool],
|
|
114
|
+
issues: List[ValidationIssue]
|
|
115
|
+
) -> PrimaryEndpointBreakdown:
|
|
116
|
+
"""Score primary endpoint (50 points).
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
responded: Whether endpoint responded
|
|
120
|
+
response_time: Response time in seconds
|
|
121
|
+
has_cors: Whether CORS present
|
|
122
|
+
valid_tls: Whether TLS valid
|
|
123
|
+
issues: Validation issues
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
PrimaryEndpointBreakdown
|
|
127
|
+
"""
|
|
128
|
+
score = 0
|
|
129
|
+
errors = []
|
|
130
|
+
|
|
131
|
+
# Responds (30 points)
|
|
132
|
+
if responded:
|
|
133
|
+
score += 30
|
|
134
|
+
else:
|
|
135
|
+
errors.append("Endpoint did not respond")
|
|
136
|
+
|
|
137
|
+
# Response time (10 points)
|
|
138
|
+
if response_time is not None:
|
|
139
|
+
if response_time < 2.0: # Under 2 seconds
|
|
140
|
+
score += 10
|
|
141
|
+
elif response_time < 5.0: # Under 5 seconds
|
|
142
|
+
score += 5
|
|
143
|
+
else:
|
|
144
|
+
errors.append(f"Slow response time: {response_time:.2f}s")
|
|
145
|
+
|
|
146
|
+
# Valid TLS (5 points)
|
|
147
|
+
if valid_tls:
|
|
148
|
+
score += 5
|
|
149
|
+
elif valid_tls is False:
|
|
150
|
+
errors.append("Invalid TLS certificate")
|
|
151
|
+
|
|
152
|
+
# CORS support (5 points)
|
|
153
|
+
if has_cors:
|
|
154
|
+
score += 5
|
|
155
|
+
elif has_cors is False:
|
|
156
|
+
errors.append("Missing CORS headers")
|
|
157
|
+
|
|
158
|
+
return PrimaryEndpointBreakdown(
|
|
159
|
+
score=score,
|
|
160
|
+
max_score=50,
|
|
161
|
+
responds=responded,
|
|
162
|
+
response_time=response_time,
|
|
163
|
+
has_cors=has_cors,
|
|
164
|
+
valid_tls=valid_tls,
|
|
165
|
+
errors=errors
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _score_transport_support(
|
|
169
|
+
self,
|
|
170
|
+
endpoint_responded: bool,
|
|
171
|
+
issues: List[ValidationIssue]
|
|
172
|
+
) -> TransportSupportBreakdown:
|
|
173
|
+
"""Score transport protocol support (30 points).
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
endpoint_responded: Whether primary endpoint worked
|
|
177
|
+
issues: Validation issues
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
TransportSupportBreakdown
|
|
181
|
+
"""
|
|
182
|
+
score = 0
|
|
183
|
+
|
|
184
|
+
# Preferred transport works (20 points)
|
|
185
|
+
preferred_works = endpoint_responded and not self._has_issue_code(
|
|
186
|
+
issues,
|
|
187
|
+
"TRANSPORT_FAILED"
|
|
188
|
+
)
|
|
189
|
+
if preferred_works:
|
|
190
|
+
score += 20
|
|
191
|
+
|
|
192
|
+
# Additional interfaces (10 points)
|
|
193
|
+
# This would require testing multiple transports
|
|
194
|
+
# For now, give partial credit if primary works
|
|
195
|
+
if preferred_works:
|
|
196
|
+
score += 10
|
|
197
|
+
additional_working = 1
|
|
198
|
+
additional_failed = 0
|
|
199
|
+
else:
|
|
200
|
+
additional_working = 0
|
|
201
|
+
additional_failed = 0
|
|
202
|
+
|
|
203
|
+
return TransportSupportBreakdown(
|
|
204
|
+
score=score,
|
|
205
|
+
max_score=30,
|
|
206
|
+
preferred_transport_works=preferred_works,
|
|
207
|
+
additional_interfaces_working=additional_working,
|
|
208
|
+
additional_interfaces_failed=additional_failed
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def _score_response_quality(
|
|
212
|
+
self,
|
|
213
|
+
endpoint_responded: bool,
|
|
214
|
+
issues: List[ValidationIssue]
|
|
215
|
+
) -> ResponseQualityBreakdown:
|
|
216
|
+
"""Score response quality (20 points).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
endpoint_responded: Whether endpoint responded
|
|
220
|
+
issues: Validation issues
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
ResponseQualityBreakdown
|
|
224
|
+
"""
|
|
225
|
+
score = 0
|
|
226
|
+
|
|
227
|
+
# Valid structure (10 points)
|
|
228
|
+
valid_structure = endpoint_responded and not self._has_issue_code(
|
|
229
|
+
issues,
|
|
230
|
+
["INVALID_RESPONSE_STRUCTURE", "MALFORMED_JSON"]
|
|
231
|
+
)
|
|
232
|
+
if valid_structure:
|
|
233
|
+
score += 10
|
|
234
|
+
|
|
235
|
+
# Proper content type (5 points)
|
|
236
|
+
proper_content_type = endpoint_responded and not self._has_issue_code(
|
|
237
|
+
issues,
|
|
238
|
+
"INVALID_CONTENT_TYPE"
|
|
239
|
+
)
|
|
240
|
+
if proper_content_type:
|
|
241
|
+
score += 5
|
|
242
|
+
|
|
243
|
+
# Proper error handling (5 points)
|
|
244
|
+
# Check that errors are properly formatted
|
|
245
|
+
proper_errors = endpoint_responded and not self._has_issue_code(
|
|
246
|
+
issues,
|
|
247
|
+
"IMPROPER_ERROR_FORMAT"
|
|
248
|
+
)
|
|
249
|
+
if proper_errors:
|
|
250
|
+
score += 5
|
|
251
|
+
|
|
252
|
+
return ResponseQualityBreakdown(
|
|
253
|
+
score=score,
|
|
254
|
+
max_score=20,
|
|
255
|
+
valid_structure=valid_structure,
|
|
256
|
+
proper_content_type=proper_content_type,
|
|
257
|
+
proper_error_handling=proper_errors
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def _is_availability_issue(self, issue: ValidationIssue) -> bool:
|
|
261
|
+
"""Check if issue is availability-related.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
issue: Validation issue to check
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if availability-related
|
|
268
|
+
"""
|
|
269
|
+
availability_codes = {
|
|
270
|
+
"ENDPOINT_UNREACHABLE",
|
|
271
|
+
"TRANSPORT_FAILED",
|
|
272
|
+
"TIMEOUT",
|
|
273
|
+
"INVALID_RESPONSE_STRUCTURE",
|
|
274
|
+
"MALFORMED_JSON",
|
|
275
|
+
"INVALID_CONTENT_TYPE",
|
|
276
|
+
"IMPROPER_ERROR_FORMAT",
|
|
277
|
+
"TLS_ERROR",
|
|
278
|
+
"CORS_ERROR",
|
|
279
|
+
}
|
|
280
|
+
return issue.code in availability_codes
|
|
281
|
+
|
|
282
|
+
def _has_issue_code(
|
|
283
|
+
self,
|
|
284
|
+
issues: List[ValidationIssue],
|
|
285
|
+
codes: str | List[str]
|
|
286
|
+
) -> bool:
|
|
287
|
+
"""Check if any issue has given code(s).
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
issues: List of validation issues
|
|
291
|
+
codes: Single code or list of codes to check
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
True if any issue matches
|
|
295
|
+
"""
|
|
296
|
+
if isinstance(codes, str):
|
|
297
|
+
codes = [codes]
|
|
298
|
+
code_set = set(codes)
|
|
299
|
+
return any(issue.code in code_set for issue in issues)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Compliance scorer for A2A protocol specification adherence.
|
|
2
|
+
|
|
3
|
+
Calculates compliance score (0-100) based on:
|
|
4
|
+
- Core required fields (60 points)
|
|
5
|
+
- Skills quality (20 points)
|
|
6
|
+
- Format compliance (15 points)
|
|
7
|
+
- Data quality (5 points)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
|
+
from ..types import ValidationIssue
|
|
12
|
+
from .types import (
|
|
13
|
+
ComplianceScore,
|
|
14
|
+
ComplianceBreakdown,
|
|
15
|
+
CoreFieldsBreakdown,
|
|
16
|
+
SkillsQualityBreakdown,
|
|
17
|
+
FormatComplianceBreakdown,
|
|
18
|
+
DataQualityBreakdown,
|
|
19
|
+
get_compliance_rating,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ComplianceScorer:
|
|
24
|
+
"""Calculates compliance scores for A2A protocol adherence."""
|
|
25
|
+
|
|
26
|
+
# Required fields per A2A specification
|
|
27
|
+
REQUIRED_FIELDS = [
|
|
28
|
+
"name",
|
|
29
|
+
"description",
|
|
30
|
+
"url",
|
|
31
|
+
"version",
|
|
32
|
+
"protocolVersion",
|
|
33
|
+
"preferredTransport",
|
|
34
|
+
"capabilities",
|
|
35
|
+
"provider",
|
|
36
|
+
"skills"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
POINTS_PER_CORE_FIELD = 60 / len(REQUIRED_FIELDS) # ~6.67 points each
|
|
40
|
+
|
|
41
|
+
def score_agent_card(
|
|
42
|
+
self,
|
|
43
|
+
card_data: Dict[str, Any],
|
|
44
|
+
issues: List[ValidationIssue]
|
|
45
|
+
) -> ComplianceScore:
|
|
46
|
+
"""Calculate compliance score for an agent card.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
card_data: Agent card data dictionary
|
|
50
|
+
issues: List of validation issues found
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
ComplianceScore with detailed breakdown
|
|
54
|
+
"""
|
|
55
|
+
# Calculate each component
|
|
56
|
+
core_fields = self._score_core_fields(card_data)
|
|
57
|
+
skills_quality = self._score_skills_quality(card_data)
|
|
58
|
+
format_compliance = self._score_format_compliance(card_data, issues)
|
|
59
|
+
data_quality = self._score_data_quality(card_data, issues)
|
|
60
|
+
|
|
61
|
+
# Calculate total
|
|
62
|
+
total = (
|
|
63
|
+
core_fields.score +
|
|
64
|
+
skills_quality.score +
|
|
65
|
+
format_compliance.score +
|
|
66
|
+
data_quality.score
|
|
67
|
+
)
|
|
68
|
+
total = max(0, min(100, total)) # Clamp to 0-100
|
|
69
|
+
|
|
70
|
+
# Create breakdown
|
|
71
|
+
breakdown = ComplianceBreakdown(
|
|
72
|
+
core_fields=core_fields,
|
|
73
|
+
skills_quality=skills_quality,
|
|
74
|
+
format_compliance=format_compliance,
|
|
75
|
+
data_quality=data_quality
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Extract issue messages
|
|
79
|
+
issue_messages = [
|
|
80
|
+
issue.message for issue in issues
|
|
81
|
+
if self._is_compliance_issue(issue)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
return ComplianceScore(
|
|
85
|
+
total=total,
|
|
86
|
+
rating=get_compliance_rating(total),
|
|
87
|
+
breakdown=breakdown,
|
|
88
|
+
issues=issue_messages
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _score_core_fields(self, card_data: Dict[str, Any]) -> CoreFieldsBreakdown:
|
|
92
|
+
"""Score core required fields (60 points).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
card_data: Agent card data
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
CoreFieldsBreakdown with present/missing fields
|
|
99
|
+
"""
|
|
100
|
+
present = []
|
|
101
|
+
missing = []
|
|
102
|
+
|
|
103
|
+
for field in self.REQUIRED_FIELDS:
|
|
104
|
+
if field in card_data and card_data[field]:
|
|
105
|
+
present.append(field)
|
|
106
|
+
else:
|
|
107
|
+
missing.append(field)
|
|
108
|
+
|
|
109
|
+
score = int(len(present) * self.POINTS_PER_CORE_FIELD)
|
|
110
|
+
|
|
111
|
+
return CoreFieldsBreakdown(
|
|
112
|
+
score=score,
|
|
113
|
+
max_score=60,
|
|
114
|
+
present=present,
|
|
115
|
+
missing=missing
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _score_skills_quality(self, card_data: Dict[str, Any]) -> SkillsQualityBreakdown:
|
|
119
|
+
"""Score skills quality (20 points).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
card_data: Agent card data
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
SkillsQualityBreakdown with quality metrics
|
|
126
|
+
"""
|
|
127
|
+
skills = card_data.get("skills", [])
|
|
128
|
+
score = 0
|
|
129
|
+
issue_count = 0
|
|
130
|
+
|
|
131
|
+
# Ensure skills is a list (defensive coding for validation errors)
|
|
132
|
+
if not isinstance(skills, list):
|
|
133
|
+
skills = []
|
|
134
|
+
|
|
135
|
+
skills_present = bool(skills)
|
|
136
|
+
all_have_required = True
|
|
137
|
+
all_have_tags = True
|
|
138
|
+
|
|
139
|
+
if skills_present:
|
|
140
|
+
score += 5 # Skills array present
|
|
141
|
+
|
|
142
|
+
# Check all skills have required fields
|
|
143
|
+
required_skill_fields = ["id", "name", "description"]
|
|
144
|
+
for skill in skills:
|
|
145
|
+
# Defensive: ensure skill is a dict
|
|
146
|
+
if not isinstance(skill, dict):
|
|
147
|
+
all_have_required = False
|
|
148
|
+
issue_count += 1
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
if not all(field in skill for field in required_skill_fields):
|
|
152
|
+
all_have_required = False
|
|
153
|
+
issue_count += 1
|
|
154
|
+
|
|
155
|
+
if all_have_required:
|
|
156
|
+
score += 10
|
|
157
|
+
|
|
158
|
+
# Check all skills have tags
|
|
159
|
+
for skill in skills:
|
|
160
|
+
# Defensive: ensure skill is a dict
|
|
161
|
+
if not isinstance(skill, dict):
|
|
162
|
+
all_have_tags = False
|
|
163
|
+
issue_count += 1
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if not skill.get("tags"):
|
|
167
|
+
all_have_tags = False
|
|
168
|
+
issue_count += 1
|
|
169
|
+
|
|
170
|
+
if all_have_tags:
|
|
171
|
+
score += 5
|
|
172
|
+
else:
|
|
173
|
+
all_have_required = False
|
|
174
|
+
all_have_tags = False
|
|
175
|
+
|
|
176
|
+
return SkillsQualityBreakdown(
|
|
177
|
+
score=score,
|
|
178
|
+
max_score=20,
|
|
179
|
+
skills_present=skills_present,
|
|
180
|
+
all_skills_have_required_fields=all_have_required,
|
|
181
|
+
all_skills_have_tags=all_have_tags,
|
|
182
|
+
issue_count=issue_count
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _score_format_compliance(
|
|
186
|
+
self,
|
|
187
|
+
card_data: Dict[str, Any],
|
|
188
|
+
issues: List[ValidationIssue]
|
|
189
|
+
) -> FormatComplianceBreakdown:
|
|
190
|
+
"""Score format compliance (15 points).
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
card_data: Agent card data
|
|
194
|
+
issues: Validation issues to check
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
FormatComplianceBreakdown with format checks
|
|
198
|
+
"""
|
|
199
|
+
score = 0
|
|
200
|
+
|
|
201
|
+
# Check for format-related issues (3 points each)
|
|
202
|
+
valid_semver = not self._has_issue_code(issues, "INVALID_SEMVER")
|
|
203
|
+
if valid_semver:
|
|
204
|
+
score += 3
|
|
205
|
+
|
|
206
|
+
valid_protocol_version = not self._has_issue_code(issues, "INVALID_PROTOCOL_VERSION")
|
|
207
|
+
if valid_protocol_version:
|
|
208
|
+
score += 3
|
|
209
|
+
|
|
210
|
+
valid_url = not self._has_issue_code(issues, ["INVALID_URL", "INSECURE_URL"])
|
|
211
|
+
if valid_url:
|
|
212
|
+
score += 3
|
|
213
|
+
|
|
214
|
+
valid_transports = not self._has_issue_code(issues, "INVALID_TRANSPORT")
|
|
215
|
+
if valid_transports:
|
|
216
|
+
score += 3
|
|
217
|
+
|
|
218
|
+
valid_mime_types = not self._has_issue_code(issues, "INVALID_MIME_TYPE")
|
|
219
|
+
if valid_mime_types:
|
|
220
|
+
score += 3
|
|
221
|
+
|
|
222
|
+
return FormatComplianceBreakdown(
|
|
223
|
+
score=score,
|
|
224
|
+
max_score=15,
|
|
225
|
+
valid_semver=valid_semver,
|
|
226
|
+
valid_protocol_version=valid_protocol_version,
|
|
227
|
+
valid_url=valid_url,
|
|
228
|
+
valid_transports=valid_transports,
|
|
229
|
+
valid_mime_types=valid_mime_types
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def _score_data_quality(
|
|
233
|
+
self,
|
|
234
|
+
card_data: Dict[str, Any],
|
|
235
|
+
issues: List[ValidationIssue]
|
|
236
|
+
) -> DataQualityBreakdown:
|
|
237
|
+
"""Score data quality (5 points).
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
card_data: Agent card data
|
|
241
|
+
issues: Validation issues to check
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
DataQualityBreakdown with quality checks
|
|
245
|
+
"""
|
|
246
|
+
score = 0
|
|
247
|
+
|
|
248
|
+
# Check for duplicate skill IDs (2 points)
|
|
249
|
+
skills = card_data.get("skills", [])
|
|
250
|
+
# Ensure skills is a list (defensive coding)
|
|
251
|
+
if not isinstance(skills, list):
|
|
252
|
+
skills = []
|
|
253
|
+
# Ensure each skill is a dict
|
|
254
|
+
skill_ids = [s.get("id") for s in skills if isinstance(s, dict) and s.get("id")]
|
|
255
|
+
no_duplicates = len(skill_ids) == len(set(skill_ids))
|
|
256
|
+
if no_duplicates:
|
|
257
|
+
score += 2
|
|
258
|
+
|
|
259
|
+
# Check field lengths (2 points)
|
|
260
|
+
field_lengths_valid = not self._has_issue_code(issues, "FIELD_LENGTH_EXCEEDED")
|
|
261
|
+
if field_lengths_valid:
|
|
262
|
+
score += 2
|
|
263
|
+
|
|
264
|
+
# Check for SSRF risks (1 point)
|
|
265
|
+
no_ssrf_risks = not self._has_issue_code(issues, ["SSRF_RISK", "PRIVATE_IP"])
|
|
266
|
+
if no_ssrf_risks:
|
|
267
|
+
score += 1
|
|
268
|
+
|
|
269
|
+
return DataQualityBreakdown(
|
|
270
|
+
score=score,
|
|
271
|
+
max_score=5,
|
|
272
|
+
no_duplicate_skill_ids=no_duplicates,
|
|
273
|
+
field_lengths_valid=field_lengths_valid,
|
|
274
|
+
no_ssrf_risks=no_ssrf_risks
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _is_compliance_issue(self, issue: ValidationIssue) -> bool:
|
|
278
|
+
"""Check if issue is compliance-related.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
issue: Validation issue to check
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
True if compliance-related
|
|
285
|
+
"""
|
|
286
|
+
compliance_codes = {
|
|
287
|
+
"MISSING_REQUIRED_FIELD",
|
|
288
|
+
"INVALID_SEMVER",
|
|
289
|
+
"INVALID_PROTOCOL_VERSION",
|
|
290
|
+
"INVALID_TRANSPORT",
|
|
291
|
+
"INVALID_MIME_TYPE",
|
|
292
|
+
"FIELD_LENGTH_EXCEEDED",
|
|
293
|
+
"DUPLICATE_SKILL_ID",
|
|
294
|
+
}
|
|
295
|
+
return issue.code in compliance_codes
|
|
296
|
+
|
|
297
|
+
def _has_issue_code(
|
|
298
|
+
self,
|
|
299
|
+
issues: List[ValidationIssue],
|
|
300
|
+
codes: str | List[str]
|
|
301
|
+
) -> bool:
|
|
302
|
+
"""Check if any issue has given code(s).
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
issues: List of validation issues
|
|
306
|
+
codes: Single code or list of codes to check
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True if any issue matches
|
|
310
|
+
"""
|
|
311
|
+
if isinstance(codes, str):
|
|
312
|
+
codes = [codes]
|
|
313
|
+
code_set = set(codes)
|
|
314
|
+
return any(issue.code in code_set for issue in issues)
|