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
capiscio_sdk/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Capiscio SDK - Runtime security middleware for A2A agents.
|
|
2
|
+
|
|
3
|
+
This package provides always-on protection for A2A protocol agents through
|
|
4
|
+
validation, signature verification, and protocol compliance checking.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from capiscio_sdk import secure
|
|
8
|
+
>>> agent = secure(MyAgentExecutor())
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.0"
|
|
12
|
+
|
|
13
|
+
# Core exports
|
|
14
|
+
from .executor import CapiscioSecurityExecutor, secure, secure_agent
|
|
15
|
+
from .config import SecurityConfig, DownstreamConfig, UpstreamConfig
|
|
16
|
+
from .errors import (
|
|
17
|
+
CapiscioSecurityError,
|
|
18
|
+
CapiscioValidationError,
|
|
19
|
+
CapiscioSignatureError,
|
|
20
|
+
CapiscioRateLimitError,
|
|
21
|
+
CapiscioUpstreamError,
|
|
22
|
+
)
|
|
23
|
+
from .types import ValidationResult, ValidationIssue, ValidationSeverity
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"__version__",
|
|
27
|
+
"CapiscioSecurityExecutor",
|
|
28
|
+
"secure",
|
|
29
|
+
"secure_agent",
|
|
30
|
+
"SecurityConfig",
|
|
31
|
+
"DownstreamConfig",
|
|
32
|
+
"UpstreamConfig",
|
|
33
|
+
"CapiscioSecurityError",
|
|
34
|
+
"CapiscioValidationError",
|
|
35
|
+
"CapiscioSignatureError",
|
|
36
|
+
"CapiscioRateLimitError",
|
|
37
|
+
"CapiscioUpstreamError",
|
|
38
|
+
"ValidationResult",
|
|
39
|
+
"ValidationIssue",
|
|
40
|
+
"ValidationSeverity",
|
|
41
|
+
]
|
|
42
|
+
|
capiscio_sdk/config.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Configuration for Capiscio A2A Security."""
|
|
2
|
+
import os
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DownstreamConfig(BaseModel):
|
|
8
|
+
"""Configuration for downstream protection (agents calling you)."""
|
|
9
|
+
|
|
10
|
+
validate_schema: bool = True
|
|
11
|
+
verify_signatures: bool = True
|
|
12
|
+
require_signatures: bool = False
|
|
13
|
+
check_protocol_compliance: bool = True
|
|
14
|
+
enable_rate_limiting: bool = True
|
|
15
|
+
rate_limit_requests_per_minute: int = 60
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UpstreamConfig(BaseModel):
|
|
19
|
+
"""Configuration for upstream protection (calling other agents)."""
|
|
20
|
+
|
|
21
|
+
validate_agent_cards: bool = True
|
|
22
|
+
verify_signatures: bool = True
|
|
23
|
+
require_signatures: bool = False
|
|
24
|
+
test_endpoints: bool = False # Performance impact
|
|
25
|
+
cache_validation: bool = True
|
|
26
|
+
cache_timeout: int = 3600 # seconds
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SecurityConfig(BaseModel):
|
|
30
|
+
"""Main security configuration."""
|
|
31
|
+
|
|
32
|
+
downstream: DownstreamConfig = Field(default_factory=DownstreamConfig)
|
|
33
|
+
upstream: UpstreamConfig = Field(default_factory=UpstreamConfig)
|
|
34
|
+
strict_mode: bool = False
|
|
35
|
+
fail_mode: Literal["block", "monitor", "log"] = "block"
|
|
36
|
+
log_validation_failures: bool = True
|
|
37
|
+
timeout_ms: int = 5000
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def development(cls) -> "SecurityConfig":
|
|
41
|
+
"""Development preset - permissive."""
|
|
42
|
+
return cls(
|
|
43
|
+
downstream=DownstreamConfig(
|
|
44
|
+
require_signatures=False,
|
|
45
|
+
enable_rate_limiting=False,
|
|
46
|
+
),
|
|
47
|
+
upstream=UpstreamConfig(
|
|
48
|
+
require_signatures=False,
|
|
49
|
+
test_endpoints=False,
|
|
50
|
+
),
|
|
51
|
+
strict_mode=False,
|
|
52
|
+
fail_mode="log",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def production(cls) -> "SecurityConfig":
|
|
57
|
+
"""Production preset - balanced."""
|
|
58
|
+
return cls(
|
|
59
|
+
downstream=DownstreamConfig(
|
|
60
|
+
require_signatures=False,
|
|
61
|
+
enable_rate_limiting=True,
|
|
62
|
+
),
|
|
63
|
+
upstream=UpstreamConfig(
|
|
64
|
+
require_signatures=False,
|
|
65
|
+
test_endpoints=False,
|
|
66
|
+
),
|
|
67
|
+
strict_mode=False,
|
|
68
|
+
fail_mode="block",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def strict(cls) -> "SecurityConfig":
|
|
73
|
+
"""Strict preset - maximum security."""
|
|
74
|
+
return cls(
|
|
75
|
+
downstream=DownstreamConfig(
|
|
76
|
+
require_signatures=True,
|
|
77
|
+
enable_rate_limiting=True,
|
|
78
|
+
),
|
|
79
|
+
upstream=UpstreamConfig(
|
|
80
|
+
require_signatures=True,
|
|
81
|
+
test_endpoints=True,
|
|
82
|
+
),
|
|
83
|
+
strict_mode=True,
|
|
84
|
+
fail_mode="block",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_env(cls) -> "SecurityConfig":
|
|
89
|
+
"""Load configuration from environment variables."""
|
|
90
|
+
return cls(
|
|
91
|
+
downstream=DownstreamConfig(
|
|
92
|
+
validate_schema=os.getenv("CAPISCIO_VALIDATE_SCHEMA", "true").lower()
|
|
93
|
+
== "true",
|
|
94
|
+
verify_signatures=os.getenv("CAPISCIO_VERIFY_SIGNATURES", "true").lower()
|
|
95
|
+
== "true",
|
|
96
|
+
require_signatures=os.getenv("CAPISCIO_REQUIRE_SIGNATURES", "false").lower()
|
|
97
|
+
== "true",
|
|
98
|
+
enable_rate_limiting=os.getenv("CAPISCIO_RATE_LIMITING", "true").lower()
|
|
99
|
+
== "true",
|
|
100
|
+
rate_limit_requests_per_minute=int(os.getenv("CAPISCIO_RATE_LIMIT_RPM", "60")),
|
|
101
|
+
),
|
|
102
|
+
upstream=UpstreamConfig(
|
|
103
|
+
validate_agent_cards=os.getenv("CAPISCIO_VALIDATE_UPSTREAM", "true").lower()
|
|
104
|
+
== "true",
|
|
105
|
+
verify_signatures=os.getenv(
|
|
106
|
+
"CAPISCIO_VERIFY_UPSTREAM_SIGNATURES", "true"
|
|
107
|
+
).lower()
|
|
108
|
+
== "true",
|
|
109
|
+
cache_validation=os.getenv("CAPISCIO_CACHE_VALIDATION", "true").lower()
|
|
110
|
+
== "true",
|
|
111
|
+
),
|
|
112
|
+
fail_mode=os.getenv("CAPISCIO_FAIL_MODE", "block"), # type: ignore
|
|
113
|
+
timeout_ms=int(os.getenv("CAPISCIO_TIMEOUT_MS", "5000")),
|
|
114
|
+
)
|
capiscio_sdk/errors.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Error types for Capiscio A2A Security."""
|
|
2
|
+
from typing import Optional, List, Dict, Any
|
|
3
|
+
from .types import ValidationResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CapiscioSecurityError(Exception):
|
|
7
|
+
"""Base error for Capiscio security."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.details = details or {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CapiscioValidationError(CapiscioSecurityError):
|
|
16
|
+
"""Schema or protocol validation failed."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
validation_result: ValidationResult,
|
|
22
|
+
errors: Optional[List[str]] = None,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.validation_result = validation_result
|
|
26
|
+
self.errors = errors or [issue.message for issue in validation_result.errors]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CapiscioSignatureError(CapiscioSecurityError):
|
|
30
|
+
"""Signature verification failed."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str, agent_url: str, reason: str):
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.agent_url = agent_url
|
|
35
|
+
self.reason = reason
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CapiscioRateLimitError(CapiscioSecurityError):
|
|
39
|
+
"""Rate limit exceeded."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str, retry_after_seconds: int):
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.retry_after_seconds = retry_after_seconds
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CapiscioUpstreamError(CapiscioSecurityError):
|
|
47
|
+
"""Upstream agent validation failed."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
message: str,
|
|
52
|
+
agent_url: str,
|
|
53
|
+
validation_result: ValidationResult,
|
|
54
|
+
):
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
self.agent_url = agent_url
|
|
57
|
+
self.validation_result = validation_result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CapiscioConfigError(CapiscioSecurityError):
|
|
61
|
+
"""Configuration error."""
|
|
62
|
+
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CapiscioTimeoutError(CapiscioSecurityError):
|
|
67
|
+
"""Operation timed out."""
|
|
68
|
+
|
|
69
|
+
pass
|
capiscio_sdk/executor.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Security executor wrapper for A2A agents."""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, Optional, Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from a2a.server.agent_execution import RequestContext
|
|
8
|
+
except ImportError:
|
|
9
|
+
RequestContext = Any # type: ignore[misc,assignment]
|
|
10
|
+
|
|
11
|
+
from .config import SecurityConfig
|
|
12
|
+
from .validators import MessageValidator, ProtocolValidator
|
|
13
|
+
from .infrastructure import ValidationCache, RateLimiter
|
|
14
|
+
from .types import ValidationResult
|
|
15
|
+
from .errors import (
|
|
16
|
+
CapiscioValidationError,
|
|
17
|
+
CapiscioRateLimitError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CapiscioSecurityExecutor:
|
|
24
|
+
"""
|
|
25
|
+
Security wrapper for A2A agent executors.
|
|
26
|
+
|
|
27
|
+
Provides runtime validation, rate limiting, and security checks
|
|
28
|
+
for A2A agent interactions. Implements the AgentExecutor interface.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
delegate: Any,
|
|
34
|
+
config: Optional[SecurityConfig] = None,
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize security executor.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
delegate: The agent executor to wrap (must implement AgentExecutor interface)
|
|
41
|
+
config: Security configuration (defaults to production preset)
|
|
42
|
+
"""
|
|
43
|
+
self.delegate = delegate
|
|
44
|
+
self.config = config or SecurityConfig.production()
|
|
45
|
+
|
|
46
|
+
# Initialize components
|
|
47
|
+
self._message_validator = MessageValidator()
|
|
48
|
+
self._protocol_validator = ProtocolValidator()
|
|
49
|
+
|
|
50
|
+
# Initialize infrastructure
|
|
51
|
+
self._cache: Optional[ValidationCache]
|
|
52
|
+
self._rate_limiter: Optional[RateLimiter]
|
|
53
|
+
|
|
54
|
+
if self.config.upstream.cache_validation:
|
|
55
|
+
self._cache = ValidationCache(
|
|
56
|
+
max_size=1000,
|
|
57
|
+
ttl=self.config.upstream.cache_timeout,
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
self._cache = None
|
|
61
|
+
|
|
62
|
+
if self.config.downstream.enable_rate_limiting:
|
|
63
|
+
self._rate_limiter = RateLimiter(
|
|
64
|
+
requests_per_minute=self.config.downstream.rate_limit_requests_per_minute
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
self._rate_limiter = None
|
|
68
|
+
|
|
69
|
+
async def execute(self, context: RequestContext, event_queue: Any) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Execute agent with security checks.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
context: RequestContext with message and task information
|
|
75
|
+
event_queue: EventQueue for publishing events
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
CapiscioValidationError: If validation fails in block mode
|
|
79
|
+
CapiscioRateLimitError: If rate limit exceeded in block mode
|
|
80
|
+
"""
|
|
81
|
+
# Extract message for validation
|
|
82
|
+
message = context.message
|
|
83
|
+
if not message:
|
|
84
|
+
logger.warning("No message in context")
|
|
85
|
+
await self.delegate.execute(context, event_queue)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Convert message to dict for validation (our validators expect dict format)
|
|
89
|
+
message_dict = message.model_dump() if hasattr(message, 'model_dump') else {}
|
|
90
|
+
|
|
91
|
+
# Extract identifier for rate limiting
|
|
92
|
+
identifier = message_dict.get("message_id") or message.message_id
|
|
93
|
+
|
|
94
|
+
# Check rate limit
|
|
95
|
+
if self._rate_limiter and identifier:
|
|
96
|
+
try:
|
|
97
|
+
self._rate_limiter.consume(identifier)
|
|
98
|
+
except CapiscioRateLimitError as e:
|
|
99
|
+
if self.config.fail_mode == "block":
|
|
100
|
+
raise
|
|
101
|
+
elif self.config.fail_mode == "monitor":
|
|
102
|
+
logger.warning(f"Rate limit exceeded for {identifier}: {e}")
|
|
103
|
+
# Continue execution in log/monitor mode
|
|
104
|
+
|
|
105
|
+
# Validate message
|
|
106
|
+
if self.config.downstream.validate_schema:
|
|
107
|
+
validation_result = self._validate_message(message_dict)
|
|
108
|
+
|
|
109
|
+
if not validation_result.success:
|
|
110
|
+
error = CapiscioValidationError(
|
|
111
|
+
"Message validation failed", validation_result
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if self.config.fail_mode == "block":
|
|
115
|
+
raise error
|
|
116
|
+
elif self.config.fail_mode == "monitor":
|
|
117
|
+
logger.warning(f"Validation failed: {error.errors}")
|
|
118
|
+
elif self.config.fail_mode == "log":
|
|
119
|
+
logger.info(f"Validation issues detected: {validation_result.issues}")
|
|
120
|
+
|
|
121
|
+
# Execute delegate
|
|
122
|
+
try:
|
|
123
|
+
await self.delegate.execute(context, event_queue)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
if self.config.fail_mode != "log":
|
|
126
|
+
raise
|
|
127
|
+
logger.error(f"Delegate execution failed: {e}")
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
async def cancel(self, context: RequestContext, event_queue: Any) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Cancel task with passthrough to delegate.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
context: RequestContext with task to cancel
|
|
136
|
+
event_queue: EventQueue for publishing cancellation event
|
|
137
|
+
"""
|
|
138
|
+
# Cancellation just passes through - no security checks needed
|
|
139
|
+
await self.delegate.cancel(context, event_queue)
|
|
140
|
+
|
|
141
|
+
def _validate_message(self, message: Dict[str, Any]) -> ValidationResult:
|
|
142
|
+
"""Validate message with caching."""
|
|
143
|
+
# Try cache first
|
|
144
|
+
if self._cache:
|
|
145
|
+
message_id = message.get("id")
|
|
146
|
+
if message_id:
|
|
147
|
+
cached = self._cache.get(message_id)
|
|
148
|
+
if cached:
|
|
149
|
+
logger.debug(f"Using cached validation for message {message_id}")
|
|
150
|
+
return cached
|
|
151
|
+
|
|
152
|
+
# Validate
|
|
153
|
+
result = self._message_validator.validate(message)
|
|
154
|
+
|
|
155
|
+
# Cache result
|
|
156
|
+
if self._cache and message.get("id"):
|
|
157
|
+
msg_id = message.get("id")
|
|
158
|
+
if isinstance(msg_id, str):
|
|
159
|
+
self._cache.set(msg_id, result)
|
|
160
|
+
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
def __getattr__(self, name: str) -> Any:
|
|
164
|
+
"""Delegate attribute access to wrapped executor."""
|
|
165
|
+
return getattr(self.delegate, name)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def secure(
|
|
169
|
+
agent: Any,
|
|
170
|
+
config: Optional[SecurityConfig] = None,
|
|
171
|
+
) -> CapiscioSecurityExecutor:
|
|
172
|
+
"""
|
|
173
|
+
Wrap an agent executor with security middleware (minimal pattern).
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
agent: Agent executor to wrap
|
|
177
|
+
config: Security configuration (defaults to production)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Secured agent executor
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
```python
|
|
184
|
+
agent = secure(MyAgentExecutor())
|
|
185
|
+
```
|
|
186
|
+
"""
|
|
187
|
+
return CapiscioSecurityExecutor(agent, config)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def secure_agent(
|
|
191
|
+
config: Optional[SecurityConfig] = None,
|
|
192
|
+
) -> Callable[[type], Callable[..., CapiscioSecurityExecutor]]:
|
|
193
|
+
"""
|
|
194
|
+
Decorator to secure an agent executor class (decorator pattern).
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
config: Security configuration (defaults to production)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Decorator function
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
```python
|
|
204
|
+
@secure_agent(config=SecurityConfig.strict())
|
|
205
|
+
class MyAgent:
|
|
206
|
+
def execute(self, message):
|
|
207
|
+
# ... agent logic
|
|
208
|
+
```
|
|
209
|
+
"""
|
|
210
|
+
def decorator(cls: type) -> Callable[..., CapiscioSecurityExecutor]:
|
|
211
|
+
@wraps(cls)
|
|
212
|
+
def wrapper(*args: Any, **kwargs: Any) -> CapiscioSecurityExecutor:
|
|
213
|
+
instance = cls(*args, **kwargs)
|
|
214
|
+
return CapiscioSecurityExecutor(instance, config)
|
|
215
|
+
return wrapper
|
|
216
|
+
return decorator
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Validation result caching."""
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from cachetools import TTLCache
|
|
5
|
+
from ..types import ValidationResult, CacheEntry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ValidationCache:
|
|
9
|
+
"""In-memory cache for validation results with TTL."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, max_size: int = 1000, ttl: int = 300):
|
|
12
|
+
"""
|
|
13
|
+
Initialize validation cache.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
max_size: Maximum number of entries to cache
|
|
17
|
+
ttl: Time-to-live in seconds (default 5 minutes)
|
|
18
|
+
"""
|
|
19
|
+
self._cache: TTLCache[str, CacheEntry] = TTLCache(maxsize=max_size, ttl=ttl)
|
|
20
|
+
self._ttl = ttl
|
|
21
|
+
|
|
22
|
+
def get(self, key: str) -> Optional[ValidationResult]:
|
|
23
|
+
"""
|
|
24
|
+
Get validation result from cache.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
key: Cache key (e.g., agent URL or message ID)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Cached ValidationResult or None if not found
|
|
31
|
+
"""
|
|
32
|
+
entry = self._cache.get(key)
|
|
33
|
+
if entry is None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
return entry.result
|
|
37
|
+
|
|
38
|
+
def set(self, key: str, result: ValidationResult) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Store validation result in cache.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
key: Cache key
|
|
44
|
+
result: ValidationResult to cache
|
|
45
|
+
"""
|
|
46
|
+
entry = CacheEntry(
|
|
47
|
+
result=result,
|
|
48
|
+
cached_at=time.time(),
|
|
49
|
+
ttl=self._ttl,
|
|
50
|
+
)
|
|
51
|
+
self._cache[key] = entry
|
|
52
|
+
|
|
53
|
+
def invalidate(self, key: str) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Remove entry from cache.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
key: Cache key to invalidate
|
|
59
|
+
"""
|
|
60
|
+
self._cache.pop(key, None)
|
|
61
|
+
|
|
62
|
+
def clear(self) -> None:
|
|
63
|
+
"""Clear all entries from cache."""
|
|
64
|
+
self._cache.clear()
|
|
65
|
+
|
|
66
|
+
def size(self) -> int:
|
|
67
|
+
"""
|
|
68
|
+
Get current cache size.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Number of entries in cache
|
|
72
|
+
"""
|
|
73
|
+
return len(self._cache)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Rate limiting implementation."""
|
|
2
|
+
import time
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from ..types import RateLimitInfo
|
|
5
|
+
from ..errors import CapiscioRateLimitError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RateLimiter:
|
|
9
|
+
"""Token bucket rate limiter."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, requests_per_minute: int = 60):
|
|
12
|
+
"""
|
|
13
|
+
Initialize rate limiter.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
requests_per_minute: Maximum requests allowed per minute
|
|
17
|
+
"""
|
|
18
|
+
self._requests_per_minute = requests_per_minute
|
|
19
|
+
self._buckets: Dict[str, Dict[str, float]] = {}
|
|
20
|
+
self._bucket_capacity = requests_per_minute
|
|
21
|
+
self._refill_rate = requests_per_minute / 60.0 # tokens per second
|
|
22
|
+
|
|
23
|
+
def check(self, identifier: str) -> RateLimitInfo:
|
|
24
|
+
"""
|
|
25
|
+
Check rate limit for identifier without consuming tokens.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
identifier: Unique identifier (e.g., agent URL or IP address)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
RateLimitInfo with current rate limit status
|
|
32
|
+
"""
|
|
33
|
+
now = time.time()
|
|
34
|
+
bucket = self._get_or_create_bucket(identifier, now)
|
|
35
|
+
|
|
36
|
+
# Calculate current tokens (don't actually refill yet)
|
|
37
|
+
time_elapsed = now - bucket["last_refill"]
|
|
38
|
+
tokens_to_add = time_elapsed * self._refill_rate
|
|
39
|
+
current_tokens = min(
|
|
40
|
+
self._bucket_capacity, bucket["tokens"] + tokens_to_add
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Calculate reset time (when bucket will be full again)
|
|
44
|
+
tokens_needed = self._bucket_capacity - current_tokens
|
|
45
|
+
seconds_to_full = tokens_needed / self._refill_rate if tokens_needed > 0 else 0
|
|
46
|
+
reset_at = now + seconds_to_full
|
|
47
|
+
|
|
48
|
+
return RateLimitInfo(
|
|
49
|
+
requests_allowed=self._requests_per_minute,
|
|
50
|
+
requests_used=int(self._bucket_capacity - current_tokens),
|
|
51
|
+
reset_at=reset_at,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def consume(self, identifier: str, tokens: float = 1.0) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Consume tokens from bucket.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
identifier: Unique identifier
|
|
60
|
+
tokens: Number of tokens to consume (default 1.0)
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
CapiscioRateLimitError: If rate limit exceeded
|
|
64
|
+
"""
|
|
65
|
+
now = time.time()
|
|
66
|
+
bucket = self._get_or_create_bucket(identifier, now)
|
|
67
|
+
|
|
68
|
+
# Refill tokens first
|
|
69
|
+
time_elapsed = now - bucket["last_refill"]
|
|
70
|
+
tokens_to_add = time_elapsed * self._refill_rate
|
|
71
|
+
bucket["tokens"] = min(
|
|
72
|
+
self._bucket_capacity, bucket["tokens"] + tokens_to_add
|
|
73
|
+
)
|
|
74
|
+
bucket["last_refill"] = now
|
|
75
|
+
|
|
76
|
+
# Check if enough tokens available
|
|
77
|
+
if bucket["tokens"] < tokens:
|
|
78
|
+
# Calculate retry after time
|
|
79
|
+
tokens_needed = tokens - bucket["tokens"]
|
|
80
|
+
retry_after = tokens_needed / self._refill_rate
|
|
81
|
+
|
|
82
|
+
raise CapiscioRateLimitError(
|
|
83
|
+
f"Rate limit exceeded for {identifier}",
|
|
84
|
+
retry_after_seconds=int(retry_after) + 1,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Consume tokens
|
|
88
|
+
bucket["tokens"] -= tokens
|
|
89
|
+
|
|
90
|
+
def reset(self, identifier: str) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Reset rate limit for identifier.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
identifier: Unique identifier to reset
|
|
96
|
+
"""
|
|
97
|
+
self._buckets.pop(identifier, None)
|
|
98
|
+
|
|
99
|
+
def clear(self) -> None:
|
|
100
|
+
"""Clear all rate limit buckets."""
|
|
101
|
+
self._buckets.clear()
|
|
102
|
+
|
|
103
|
+
def _get_or_create_bucket(self, identifier: str, now: float) -> Dict[str, float]:
|
|
104
|
+
"""Get or create bucket for identifier."""
|
|
105
|
+
if identifier not in self._buckets:
|
|
106
|
+
self._buckets[identifier] = {
|
|
107
|
+
"tokens": float(self._bucket_capacity),
|
|
108
|
+
"last_refill": now,
|
|
109
|
+
}
|
|
110
|
+
return self._buckets[identifier]
|
capiscio_sdk/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Multi-dimensional scoring system for A2A validation.
|
|
2
|
+
|
|
3
|
+
This module provides three independent scoring dimensions:
|
|
4
|
+
- Compliance: Protocol specification adherence (0-100)
|
|
5
|
+
- Trust: Security and authenticity signals (0-100)
|
|
6
|
+
- Availability: Operational readiness (0-100)
|
|
7
|
+
|
|
8
|
+
Each dimension has its own rating scale and breakdown structure,
|
|
9
|
+
allowing users to make nuanced decisions based on their priorities.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .types import (
|
|
13
|
+
ComplianceScore,
|
|
14
|
+
TrustScore,
|
|
15
|
+
AvailabilityScore,
|
|
16
|
+
ComplianceBreakdown,
|
|
17
|
+
TrustBreakdown,
|
|
18
|
+
AvailabilityBreakdown,
|
|
19
|
+
ComplianceRating,
|
|
20
|
+
TrustRating,
|
|
21
|
+
AvailabilityRating,
|
|
22
|
+
ScoringContext,
|
|
23
|
+
)
|
|
24
|
+
from .compliance import ComplianceScorer
|
|
25
|
+
from .trust import TrustScorer
|
|
26
|
+
from .availability import AvailabilityScorer
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ComplianceScore",
|
|
30
|
+
"TrustScore",
|
|
31
|
+
"AvailabilityScore",
|
|
32
|
+
"ComplianceBreakdown",
|
|
33
|
+
"TrustBreakdown",
|
|
34
|
+
"AvailabilityBreakdown",
|
|
35
|
+
"ComplianceRating",
|
|
36
|
+
"TrustRating",
|
|
37
|
+
"AvailabilityRating",
|
|
38
|
+
"ScoringContext",
|
|
39
|
+
"ComplianceScorer",
|
|
40
|
+
"TrustScorer",
|
|
41
|
+
"AvailabilityScorer",
|
|
42
|
+
]
|