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.
@@ -0,0 +1,340 @@
1
+ """Trust scorer for security and authenticity signals.
2
+
3
+ Calculates trust score (0-100) based on:
4
+ - Cryptographic signatures (40 points)
5
+ - Provider information (25 points)
6
+ - Security configuration (20 points)
7
+ - Documentation and transparency (15 points)
8
+
9
+ Applies confidence multiplier based on signature state:
10
+ - Valid signature: 1.0x (full confidence)
11
+ - No signature: 0.6x (unverified claims)
12
+ - Invalid signature: 0.4x (active distrust)
13
+ """
14
+
15
+ from typing import Any, Dict, List
16
+ from ..types import ValidationIssue
17
+ from .types import (
18
+ TrustScore,
19
+ TrustBreakdown,
20
+ SignaturesBreakdown,
21
+ ProviderBreakdown,
22
+ SecurityBreakdown,
23
+ DocumentationBreakdown,
24
+ get_trust_rating,
25
+ get_trust_confidence_multiplier,
26
+ )
27
+
28
+
29
+ class TrustScorer:
30
+ """Calculates trust scores for security and authenticity signals."""
31
+
32
+ def score_agent_card(
33
+ self,
34
+ card_data: Dict[str, Any],
35
+ issues: List[ValidationIssue],
36
+ skip_signature_verification: bool = False
37
+ ) -> TrustScore:
38
+ """Calculate trust score for an agent card.
39
+
40
+ Args:
41
+ card_data: Agent card data dictionary
42
+ issues: List of validation issues found
43
+ skip_signature_verification: Whether signature verification was skipped
44
+
45
+ Returns:
46
+ TrustScore with detailed breakdown
47
+ """
48
+ # Calculate each component
49
+ signatures = self._score_signatures(card_data, issues, skip_signature_verification)
50
+ provider = self._score_provider(card_data, issues)
51
+ security = self._score_security(card_data, issues)
52
+ documentation = self._score_documentation(card_data)
53
+
54
+ # Calculate raw score
55
+ raw_score = (
56
+ signatures.score +
57
+ provider.score +
58
+ security.score +
59
+ documentation.score
60
+ )
61
+ raw_score = max(0, min(100, raw_score))
62
+
63
+ # Apply confidence multiplier
64
+ confidence_multiplier = get_trust_confidence_multiplier(
65
+ has_valid_signature=signatures.has_valid_signature,
66
+ has_invalid_signature=signatures.has_invalid_signature
67
+ )
68
+ total = int(raw_score * confidence_multiplier)
69
+
70
+ # Create breakdown
71
+ breakdown = TrustBreakdown(
72
+ signatures=signatures,
73
+ provider=provider,
74
+ security=security,
75
+ documentation=documentation
76
+ )
77
+
78
+ # Extract issue messages
79
+ issue_messages = [
80
+ issue.message for issue in issues
81
+ if self._is_trust_issue(issue)
82
+ ]
83
+
84
+ return TrustScore(
85
+ total=total,
86
+ raw_score=raw_score,
87
+ confidence_multiplier=confidence_multiplier,
88
+ rating=get_trust_rating(total),
89
+ breakdown=breakdown,
90
+ issues=issue_messages,
91
+ partial_validation=skip_signature_verification
92
+ )
93
+
94
+ def _score_signatures(
95
+ self,
96
+ card_data: Dict[str, Any],
97
+ issues: List[ValidationIssue],
98
+ skip_verification: bool
99
+ ) -> SignaturesBreakdown:
100
+ """Score cryptographic signatures (40 points).
101
+
102
+ Args:
103
+ card_data: Agent card data
104
+ issues: Validation issues
105
+ skip_verification: Whether verification was skipped
106
+
107
+ Returns:
108
+ SignaturesBreakdown with signature metrics
109
+ """
110
+ score = 0
111
+ tested = not skip_verification
112
+
113
+ has_valid = False
114
+ multiple_sigs = False
115
+ covers_all = False
116
+ is_recent = False
117
+ has_invalid = False
118
+ has_expired = False
119
+
120
+ if tested:
121
+ # Check for valid signature (25 points)
122
+ has_valid = not self._has_issue_code(issues, [
123
+ "SIGNATURE_VERIFICATION_FAILED",
124
+ "MISSING_SIGNATURE"
125
+ ])
126
+ if has_valid:
127
+ score += 25
128
+
129
+ # Check for invalid signature
130
+ has_invalid = self._has_issue_code(issues, "SIGNATURE_VERIFICATION_FAILED")
131
+
132
+ # Check for expired signature
133
+ has_expired = self._has_issue_code(issues, "SIGNATURE_EXPIRED")
134
+
135
+ # Check for multiple signatures (5 points)
136
+ signatures = card_data.get("signatures", [])
137
+ if isinstance(signatures, list) and len(signatures) > 1:
138
+ multiple_sigs = True
139
+ score += 5
140
+
141
+ # Check if signature covers all fields (5 points)
142
+ # In practice, this would verify the JWS payload includes all card fields
143
+ if has_valid and not self._has_issue_code(issues, "INCOMPLETE_SIGNATURE_COVERAGE"):
144
+ covers_all = True
145
+ score += 5
146
+
147
+ # Check if signature is recent (5 points)
148
+ # Signatures less than 90 days old
149
+ if has_valid and not has_expired:
150
+ is_recent = True
151
+ score += 5
152
+
153
+ return SignaturesBreakdown(
154
+ score=score,
155
+ max_score=40,
156
+ tested=tested,
157
+ has_valid_signature=has_valid,
158
+ multiple_signatures=multiple_sigs,
159
+ covers_all_fields=covers_all,
160
+ is_recent=is_recent,
161
+ has_invalid_signature=has_invalid,
162
+ has_expired_signature=has_expired
163
+ )
164
+
165
+ def _score_provider(
166
+ self,
167
+ card_data: Dict[str, Any],
168
+ issues: List[ValidationIssue]
169
+ ) -> ProviderBreakdown:
170
+ """Score provider information (25 points).
171
+
172
+ Args:
173
+ card_data: Agent card data
174
+ issues: Validation issues
175
+
176
+ Returns:
177
+ ProviderBreakdown with provider metrics
178
+ """
179
+ score = 0
180
+ provider = card_data.get("provider", {})
181
+
182
+ # Ensure provider is a dict (defensive coding for validation errors)
183
+ if not isinstance(provider, dict):
184
+ provider = {}
185
+
186
+ # Check for organization (10 points)
187
+ has_org = bool(provider.get("organization"))
188
+ if has_org:
189
+ score += 10
190
+
191
+ # Check for provider URL (10 points)
192
+ has_url = bool(provider.get("url"))
193
+ if has_url:
194
+ score += 10
195
+
196
+ # Check if URL is reachable (5 points)
197
+ # This would require network test, so we check if no related errors
198
+ url_reachable = None
199
+ if has_url and not self._has_issue_code(issues, ["PROVIDER_URL_UNREACHABLE"]):
200
+ url_reachable = True
201
+ score += 5
202
+ elif has_url:
203
+ url_reachable = False
204
+
205
+ return ProviderBreakdown(
206
+ score=score,
207
+ max_score=25,
208
+ tested=True,
209
+ has_organization=has_org,
210
+ has_url=has_url,
211
+ url_reachable=url_reachable
212
+ )
213
+
214
+ def _score_security(
215
+ self,
216
+ card_data: Dict[str, Any],
217
+ issues: List[ValidationIssue]
218
+ ) -> SecurityBreakdown:
219
+ """Score security configuration (20 points).
220
+
221
+ Args:
222
+ card_data: Agent card data
223
+ issues: Validation issues
224
+
225
+ Returns:
226
+ SecurityBreakdown with security metrics
227
+ """
228
+ score = 0
229
+
230
+ # Check HTTPS only (10 points)
231
+ has_http = self._has_issue_code(issues, ["INSECURE_URL", "HTTP_URL_FOUND"])
232
+ https_only = not has_http
233
+ if https_only:
234
+ score += 10
235
+
236
+ # Check for security schemes (5 points)
237
+ capabilities = card_data.get("capabilities", {})
238
+ # Ensure capabilities is a dict (defensive coding for validation errors)
239
+ if not isinstance(capabilities, dict):
240
+ capabilities = {}
241
+ security_schemes = capabilities.get("securitySchemes", [])
242
+ has_security_schemes = bool(security_schemes)
243
+ if has_security_schemes:
244
+ score += 5
245
+
246
+ # Check for strong auth (5 points)
247
+ # OAuth2, API Key, or other authentication
248
+ has_strong_auth = False
249
+ if has_security_schemes:
250
+ for scheme in security_schemes:
251
+ scheme_type = scheme.get("type", "").lower()
252
+ if scheme_type in ["oauth2", "apikey", "http"]:
253
+ has_strong_auth = True
254
+ break
255
+ if has_strong_auth:
256
+ score += 5
257
+
258
+ return SecurityBreakdown(
259
+ score=score,
260
+ max_score=20,
261
+ https_only=https_only,
262
+ has_security_schemes=has_security_schemes,
263
+ has_strong_auth=has_strong_auth,
264
+ has_http_urls=has_http
265
+ )
266
+
267
+ def _score_documentation(self, card_data: Dict[str, Any]) -> DocumentationBreakdown:
268
+ """Score documentation and transparency (15 points).
269
+
270
+ Args:
271
+ card_data: Agent card data
272
+
273
+ Returns:
274
+ DocumentationBreakdown with documentation metrics
275
+ """
276
+ score = 0
277
+
278
+ # Check for documentation URL (5 points)
279
+ has_docs = bool(card_data.get("documentationUrl"))
280
+ if has_docs:
281
+ score += 5
282
+
283
+ # Check for terms of service (5 points)
284
+ has_tos = bool(card_data.get("termsOfService"))
285
+ if has_tos:
286
+ score += 5
287
+
288
+ # Check for privacy policy (5 points)
289
+ has_privacy = bool(card_data.get("privacyPolicy"))
290
+ if has_privacy:
291
+ score += 5
292
+
293
+ return DocumentationBreakdown(
294
+ score=score,
295
+ max_score=15,
296
+ has_documentation_url=has_docs,
297
+ has_terms_of_service=has_tos,
298
+ has_privacy_policy=has_privacy
299
+ )
300
+
301
+ def _is_trust_issue(self, issue: ValidationIssue) -> bool:
302
+ """Check if issue is trust-related.
303
+
304
+ Args:
305
+ issue: Validation issue to check
306
+
307
+ Returns:
308
+ True if trust-related
309
+ """
310
+ trust_codes = {
311
+ "SIGNATURE_VERIFICATION_FAILED",
312
+ "MISSING_SIGNATURE",
313
+ "SIGNATURE_EXPIRED",
314
+ "INCOMPLETE_SIGNATURE_COVERAGE",
315
+ "INSECURE_URL",
316
+ "HTTP_URL_FOUND",
317
+ "PROVIDER_URL_UNREACHABLE",
318
+ "SSRF_RISK",
319
+ "PRIVATE_IP",
320
+ }
321
+ return issue.code in trust_codes
322
+
323
+ def _has_issue_code(
324
+ self,
325
+ issues: List[ValidationIssue],
326
+ codes: str | List[str]
327
+ ) -> bool:
328
+ """Check if any issue has given code(s).
329
+
330
+ Args:
331
+ issues: List of validation issues
332
+ codes: Single code or list of codes to check
333
+
334
+ Returns:
335
+ True if any issue matches
336
+ """
337
+ if isinstance(codes, str):
338
+ codes = [codes]
339
+ code_set = set(codes)
340
+ return any(issue.code in code_set for issue in issues)
@@ -0,0 +1,353 @@
1
+ """Type definitions for multi-dimensional scoring system.
2
+
3
+ Defines the three core score types (Compliance, Trust, Availability),
4
+ their breakdown structures, rating enums, and helper functions.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from typing import List, Optional
10
+
11
+
12
+ # ============================================================================
13
+ # Rating Enums
14
+ # ============================================================================
15
+
16
+
17
+ class ComplianceRating(str, Enum):
18
+ """Compliance score rating levels."""
19
+ PERFECT = "Perfect"
20
+ EXCELLENT = "Excellent"
21
+ GOOD = "Good"
22
+ FAIR = "Fair"
23
+ POOR = "Poor"
24
+
25
+
26
+ class TrustRating(str, Enum):
27
+ """Trust score rating levels."""
28
+ HIGHLY_TRUSTED = "Highly Trusted"
29
+ TRUSTED = "Trusted"
30
+ MODERATE_TRUST = "Moderate Trust"
31
+ LOW_TRUST = "Low Trust"
32
+ UNTRUSTED = "Untrusted"
33
+
34
+
35
+ class AvailabilityRating(str, Enum):
36
+ """Availability score rating levels."""
37
+ FULLY_AVAILABLE = "Fully Available"
38
+ AVAILABLE = "Available"
39
+ DEGRADED = "Degraded"
40
+ UNSTABLE = "Unstable"
41
+ UNAVAILABLE = "Unavailable"
42
+
43
+
44
+ # ============================================================================
45
+ # Breakdown Structures
46
+ # ============================================================================
47
+
48
+
49
+ @dataclass
50
+ class CoreFieldsBreakdown:
51
+ """Breakdown for core required fields scoring."""
52
+ score: int
53
+ max_score: int = 60
54
+ present: List[str] = field(default_factory=list)
55
+ missing: List[str] = field(default_factory=list)
56
+
57
+
58
+ @dataclass
59
+ class SkillsQualityBreakdown:
60
+ """Breakdown for skills quality scoring."""
61
+ score: int
62
+ max_score: int = 20
63
+ skills_present: bool = False
64
+ all_skills_have_required_fields: bool = False
65
+ all_skills_have_tags: bool = False
66
+ issue_count: int = 0
67
+
68
+
69
+ @dataclass
70
+ class FormatComplianceBreakdown:
71
+ """Breakdown for format compliance scoring."""
72
+ score: int
73
+ max_score: int = 15
74
+ valid_semver: bool = False
75
+ valid_protocol_version: bool = False
76
+ valid_url: bool = False
77
+ valid_transports: bool = False
78
+ valid_mime_types: bool = False
79
+
80
+
81
+ @dataclass
82
+ class DataQualityBreakdown:
83
+ """Breakdown for data quality scoring."""
84
+ score: int
85
+ max_score: int = 5
86
+ no_duplicate_skill_ids: bool = False
87
+ field_lengths_valid: bool = False
88
+ no_ssrf_risks: bool = False
89
+
90
+
91
+ @dataclass
92
+ class ComplianceBreakdown:
93
+ """Complete compliance score breakdown (100 points total)."""
94
+ core_fields: CoreFieldsBreakdown
95
+ skills_quality: SkillsQualityBreakdown
96
+ format_compliance: FormatComplianceBreakdown
97
+ data_quality: DataQualityBreakdown
98
+
99
+
100
+ @dataclass
101
+ class SignaturesBreakdown:
102
+ """Breakdown for signature validation scoring."""
103
+ score: int
104
+ max_score: int = 40
105
+ tested: bool = False
106
+ has_valid_signature: bool = False
107
+ multiple_signatures: bool = False
108
+ covers_all_fields: bool = False
109
+ is_recent: bool = False
110
+ has_invalid_signature: bool = False
111
+ has_expired_signature: bool = False
112
+
113
+
114
+ @dataclass
115
+ class ProviderBreakdown:
116
+ """Breakdown for provider information scoring."""
117
+ score: int
118
+ max_score: int = 25
119
+ tested: bool = False
120
+ has_organization: bool = False
121
+ has_url: bool = False
122
+ url_reachable: Optional[bool] = None
123
+
124
+
125
+ @dataclass
126
+ class SecurityBreakdown:
127
+ """Breakdown for security configuration scoring."""
128
+ score: int
129
+ max_score: int = 20
130
+ https_only: bool = False
131
+ has_security_schemes: bool = False
132
+ has_strong_auth: bool = False
133
+ has_http_urls: bool = False
134
+
135
+
136
+ @dataclass
137
+ class DocumentationBreakdown:
138
+ """Breakdown for documentation and transparency scoring."""
139
+ score: int
140
+ max_score: int = 15
141
+ has_documentation_url: bool = False
142
+ has_terms_of_service: bool = False
143
+ has_privacy_policy: bool = False
144
+
145
+
146
+ @dataclass
147
+ class TrustBreakdown:
148
+ """Complete trust score breakdown (100 points before multiplier)."""
149
+ signatures: SignaturesBreakdown
150
+ provider: ProviderBreakdown
151
+ security: SecurityBreakdown
152
+ documentation: DocumentationBreakdown
153
+
154
+
155
+ @dataclass
156
+ class PrimaryEndpointBreakdown:
157
+ """Breakdown for primary endpoint scoring."""
158
+ score: int
159
+ max_score: int = 50
160
+ responds: bool = False
161
+ response_time: Optional[float] = None
162
+ has_cors: Optional[bool] = None
163
+ valid_tls: Optional[bool] = None
164
+ errors: List[str] = field(default_factory=list)
165
+
166
+
167
+ @dataclass
168
+ class TransportSupportBreakdown:
169
+ """Breakdown for transport protocol support scoring."""
170
+ score: int
171
+ max_score: int = 30
172
+ preferred_transport_works: bool = False
173
+ additional_interfaces_working: int = 0
174
+ additional_interfaces_failed: int = 0
175
+
176
+
177
+ @dataclass
178
+ class ResponseQualityBreakdown:
179
+ """Breakdown for response quality scoring."""
180
+ score: int
181
+ max_score: int = 20
182
+ valid_structure: bool = False
183
+ proper_content_type: bool = False
184
+ proper_error_handling: bool = False
185
+
186
+
187
+ @dataclass
188
+ class AvailabilityBreakdown:
189
+ """Complete availability score breakdown (100 points total)."""
190
+ primary_endpoint: PrimaryEndpointBreakdown
191
+ transport_support: TransportSupportBreakdown
192
+ response_quality: ResponseQualityBreakdown
193
+
194
+
195
+ # ============================================================================
196
+ # Core Score Types
197
+ # ============================================================================
198
+
199
+
200
+ @dataclass
201
+ class ComplianceScore:
202
+ """Compliance score (0-100): Measures A2A specification adherence.
203
+
204
+ Always calculated consistently regardless of validation flags.
205
+ """
206
+ total: int
207
+ rating: ComplianceRating
208
+ breakdown: ComplianceBreakdown
209
+ issues: List[str] = field(default_factory=list)
210
+
211
+ def __post_init__(self) -> None:
212
+ """Validate score is in range."""
213
+ assert 0 <= self.total <= 100, f"Invalid compliance score: {self.total}"
214
+
215
+
216
+ @dataclass
217
+ class TrustScore:
218
+ """Trust score (0-100): Measures security and authenticity signals.
219
+
220
+ Includes confidence multiplier based on signature presence.
221
+ """
222
+ total: int # After confidence multiplier
223
+ raw_score: int # Before multiplier
224
+ confidence_multiplier: float # 1.0x, 0.6x, or 0.4x
225
+ rating: TrustRating
226
+ breakdown: TrustBreakdown
227
+ issues: List[str] = field(default_factory=list)
228
+ partial_validation: bool = False
229
+
230
+ def __post_init__(self) -> None:
231
+ """Validate score is in range."""
232
+ assert 0 <= self.total <= 100, f"Invalid trust score: {self.total}"
233
+ assert 0 <= self.raw_score <= 100, f"Invalid raw trust score: {self.raw_score}"
234
+ assert self.confidence_multiplier in (0.4, 0.6, 1.0), \
235
+ f"Invalid confidence multiplier: {self.confidence_multiplier}"
236
+
237
+
238
+ @dataclass
239
+ class AvailabilityScore:
240
+ """Availability score (0-100): Measures operational readiness.
241
+
242
+ Only calculated when network tests are enabled (not schema-only mode).
243
+ """
244
+ total: Optional[int] # None if not tested
245
+ rating: Optional[AvailabilityRating]
246
+ breakdown: Optional[AvailabilityBreakdown]
247
+ issues: List[str] = field(default_factory=list)
248
+ tested: bool = False
249
+ not_tested_reason: Optional[str] = None
250
+
251
+ def __post_init__(self) -> None:
252
+ """Validate score is in range if present."""
253
+ if self.total is not None:
254
+ assert 0 <= self.total <= 100, f"Invalid availability score: {self.total}"
255
+
256
+
257
+ # ============================================================================
258
+ # Context & Helpers
259
+ # ============================================================================
260
+
261
+
262
+ @dataclass
263
+ class ScoringContext:
264
+ """Context about what validation was performed."""
265
+ schema_only: bool = False
266
+ skip_signature_verification: bool = False
267
+ test_live: bool = False
268
+ strict_mode: bool = False
269
+
270
+
271
+ # ============================================================================
272
+ # Rating Helper Functions
273
+ # ============================================================================
274
+
275
+
276
+ def get_compliance_rating(score: int) -> ComplianceRating:
277
+ """Get compliance rating based on score.
278
+
279
+ Args:
280
+ score: Compliance score (0-100)
281
+
282
+ Returns:
283
+ ComplianceRating enum value
284
+ """
285
+ if score == 100:
286
+ return ComplianceRating.PERFECT
287
+ if score >= 90:
288
+ return ComplianceRating.EXCELLENT
289
+ if score >= 75:
290
+ return ComplianceRating.GOOD
291
+ if score >= 60:
292
+ return ComplianceRating.FAIR
293
+ return ComplianceRating.POOR
294
+
295
+
296
+ def get_trust_rating(score: int) -> TrustRating:
297
+ """Get trust rating based on score.
298
+
299
+ Args:
300
+ score: Trust score (0-100, after confidence multiplier)
301
+
302
+ Returns:
303
+ TrustRating enum value
304
+ """
305
+ if score >= 80:
306
+ return TrustRating.HIGHLY_TRUSTED
307
+ if score >= 60:
308
+ return TrustRating.TRUSTED
309
+ if score >= 40:
310
+ return TrustRating.MODERATE_TRUST
311
+ if score >= 20:
312
+ return TrustRating.LOW_TRUST
313
+ return TrustRating.UNTRUSTED
314
+
315
+
316
+ def get_availability_rating(score: int) -> AvailabilityRating:
317
+ """Get availability rating based on score.
318
+
319
+ Args:
320
+ score: Availability score (0-100)
321
+
322
+ Returns:
323
+ AvailabilityRating enum value
324
+ """
325
+ if score >= 95:
326
+ return AvailabilityRating.FULLY_AVAILABLE
327
+ if score >= 80:
328
+ return AvailabilityRating.AVAILABLE
329
+ if score >= 60:
330
+ return AvailabilityRating.DEGRADED
331
+ if score >= 40:
332
+ return AvailabilityRating.UNSTABLE
333
+ return AvailabilityRating.UNAVAILABLE
334
+
335
+
336
+ def get_trust_confidence_multiplier(
337
+ has_valid_signature: bool,
338
+ has_invalid_signature: bool
339
+ ) -> float:
340
+ """Get trust confidence multiplier based on signature state.
341
+
342
+ Args:
343
+ has_valid_signature: Whether a valid signature exists
344
+ has_invalid_signature: Whether an invalid signature exists
345
+
346
+ Returns:
347
+ Confidence multiplier: 1.0x (valid), 0.6x (none), or 0.4x (invalid)
348
+ """
349
+ if has_invalid_signature:
350
+ return 0.4 # Active distrust
351
+ if has_valid_signature:
352
+ return 1.0 # Full confidence
353
+ return 0.6 # Unverified claims