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,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
@@ -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,5 @@
1
+ """Infrastructure components."""
2
+ from .cache import ValidationCache
3
+ from .rate_limiter import RateLimiter
4
+
5
+ __all__ = ["ValidationCache", "RateLimiter"]
@@ -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
+ ]