capiscio-sdk 0.2.0__py3-none-any.whl → 0.3.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 CHANGED
@@ -12,6 +12,7 @@ __version__ = "0.2.0"
12
12
 
13
13
  # Core exports
14
14
  from .executor import CapiscioSecurityExecutor, secure, secure_agent
15
+ from .simple_guard import SimpleGuard
15
16
  from .config import SecurityConfig, DownstreamConfig, UpstreamConfig
16
17
  from .errors import (
17
18
  CapiscioSecurityError,
@@ -25,6 +26,7 @@ from .types import ValidationResult, ValidationIssue, ValidationSeverity
25
26
  __all__ = [
26
27
  "__version__",
27
28
  "CapiscioSecurityExecutor",
29
+ "SimpleGuard",
28
30
  "secure",
29
31
  "secure_agent",
30
32
  "SecurityConfig",
capiscio_sdk/config.py CHANGED
@@ -1,4 +1,4 @@
1
- """Configuration for Capiscio A2A Security."""
1
+ """Configuration for Capiscio Python SDK."""
2
2
  import os
3
3
  from typing import Literal
4
4
  from pydantic import BaseModel, Field
capiscio_sdk/errors.py CHANGED
@@ -1,4 +1,4 @@
1
- """Error types for Capiscio A2A Security."""
1
+ """Error types for Capiscio Python SDK."""
2
2
  from typing import Optional, List, Dict, Any
3
3
  from .types import ValidationResult
4
4
 
@@ -67,3 +67,13 @@ class CapiscioTimeoutError(CapiscioSecurityError):
67
67
  """Operation timed out."""
68
68
 
69
69
  pass
70
+
71
+
72
+ class ConfigurationError(CapiscioSecurityError):
73
+ """Missing keys or invalid paths (SimpleGuard)."""
74
+ pass
75
+
76
+
77
+ class VerificationError(CapiscioSecurityError):
78
+ """Invalid signature, expired token, or untrusted key (SimpleGuard)."""
79
+ pass
capiscio_sdk/executor.py CHANGED
@@ -138,6 +138,23 @@ class CapiscioSecurityExecutor:
138
138
  # Cancellation just passes through - no security checks needed
139
139
  await self.delegate.cancel(context, event_queue)
140
140
 
141
+ async def validate_agent_card(self, url: str) -> ValidationResult:
142
+ """
143
+ Validate an agent card from a URL.
144
+
145
+ Args:
146
+ url: URL to the agent card or agent root
147
+
148
+ Returns:
149
+ ValidationResult with scores
150
+ """
151
+ from .validators.agent_card import AgentCardValidator
152
+ validator = AgentCardValidator()
153
+ try:
154
+ return await validator.fetch_and_validate(url)
155
+ finally:
156
+ await validator.http_client.aclose()
157
+
141
158
  def _validate_message(self, message: Dict[str, Any]) -> ValidationResult:
142
159
  """Validate message with caching."""
143
160
  # Try cache first
@@ -0,0 +1,73 @@
1
+ """FastAPI integration for Capiscio SimpleGuard."""
2
+ from typing import Callable, Awaitable, Any, Dict
3
+ try:
4
+ from starlette.middleware.base import BaseHTTPMiddleware
5
+ from starlette.requests import Request
6
+ from starlette.responses import JSONResponse, Response
7
+ from starlette.types import ASGIApp
8
+ except ImportError:
9
+ raise ImportError("FastAPI/Starlette is required for this integration. Install with 'pip install fastapi'.")
10
+
11
+ from ..simple_guard import SimpleGuard
12
+ from ..errors import VerificationError
13
+ import time
14
+
15
+ class CapiscioMiddleware(BaseHTTPMiddleware):
16
+ """
17
+ Middleware to enforce A2A identity verification on incoming requests.
18
+ """
19
+ def __init__(self, app: ASGIApp, guard: SimpleGuard) -> None:
20
+ super().__init__(app)
21
+ self.guard = guard
22
+
23
+ async def dispatch(
24
+ self,
25
+ request: Request,
26
+ call_next: Callable[[Request], Awaitable[Response]]
27
+ ) -> Response:
28
+ # Allow health checks or public endpoints if needed
29
+ # For now, we assume everything under /agent/ needs protection
30
+ # But let's just check for the header.
31
+
32
+ if request.method == "OPTIONS":
33
+ return await call_next(request)
34
+
35
+ auth_header = request.headers.get("X-Capiscio-JWS")
36
+
37
+ # If no header, we might let it pass but mark as unverified?
38
+ # The mandate says: "Returns 401 (missing) or 403 (invalid)."
39
+ if not auth_header:
40
+ return JSONResponse(
41
+ {"error": "Missing X-Capiscio-JWS header. This endpoint is protected by CapiscIO."},
42
+ status_code=401
43
+ )
44
+
45
+ start_time = time.perf_counter()
46
+ try:
47
+ # Read the body for integrity check
48
+ body_bytes = await request.body()
49
+
50
+ # Verify the JWS with body
51
+ payload = self.guard.verify_inbound(auth_header, body=body_bytes)
52
+
53
+ # Reset the receive channel so downstream can read the body
54
+ async def receive() -> Dict[str, Any]:
55
+ return {"type": "http.request", "body": body_bytes, "more_body": False}
56
+ request._receive = receive
57
+
58
+ # Inject claims into request.state
59
+ request.state.agent = payload
60
+ request.state.agent_id = payload.get("iss")
61
+
62
+ except VerificationError as e:
63
+ return JSONResponse({"error": f"Access Denied: {str(e)}"}, status_code=403)
64
+
65
+ verification_duration = (time.perf_counter() - start_time) * 1000
66
+
67
+ response = await call_next(request)
68
+
69
+ # Add Server-Timing header (standard for performance metrics)
70
+ # Syntax: metric_name;dur=123.4;desc="Description"
71
+ response.headers["Server-Timing"] = f"capiscio-auth;dur={verification_duration:.3f};desc=\"CapiscIO Verification\""
72
+
73
+ return response
@@ -0,0 +1,354 @@
1
+ """SimpleGuard: Local, zero-config security for A2A agents."""
2
+ import os
3
+ import json
4
+ import logging
5
+ import base64
6
+ import hashlib
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Optional, Dict, Any, Union, cast
10
+
11
+ import jwt
12
+ from cryptography.hazmat.primitives import serialization
13
+ from cryptography.hazmat.primitives.asymmetric import ed25519
14
+
15
+ from .errors import ConfigurationError, VerificationError
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ MAX_TOKEN_AGE = 60
20
+ CLOCK_SKEW_LEEWAY = 5
21
+
22
+ class SimpleGuard:
23
+ """
24
+ The "Customs Officer" for your Agent.
25
+
26
+ Enforces Identity (JWS) and Protocol (A2A) validation locally.
27
+ Prioritizes local utility and zero-configuration.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ base_dir: Optional[Union[str, Path]] = None,
33
+ dev_mode: bool = False
34
+ ) -> None:
35
+ """
36
+ Initialize SimpleGuard.
37
+
38
+ Args:
39
+ base_dir: Starting directory to search for config (defaults to cwd).
40
+ dev_mode: If True, auto-generates keys and agent-card.json.
41
+ """
42
+ self.dev_mode = dev_mode
43
+
44
+ # 1. Safety Check
45
+ if self.dev_mode and os.getenv("CAPISCIO_ENV") == "prod":
46
+ logger.critical(
47
+ "CRITICAL: SimpleGuard initialized in dev_mode=True but CAPISCIO_ENV=prod. "
48
+ "This is insecure! Disable dev_mode in production."
49
+ )
50
+
51
+ # 2. Resolve base_dir (Walk up logic)
52
+ self.project_root = self._resolve_project_root(base_dir)
53
+ self.keys_dir = self.project_root / "capiscio_keys"
54
+ self.trusted_dir = self.keys_dir / "trusted"
55
+ self.agent_card_path = self.project_root / "agent-card.json"
56
+ self.private_key_path = self.keys_dir / "private.pem"
57
+ self.public_key_path = self.keys_dir / "public.pem"
58
+
59
+ # 3. Load or Generate agent-card.json
60
+ self.agent_id: str
61
+ self.signing_kid: str
62
+ self._load_or_generate_card()
63
+
64
+ # 4. Load or Generate Keys
65
+ self._private_key: ed25519.Ed25519PrivateKey
66
+ self._load_or_generate_keys()
67
+
68
+ # 5. Load Trust Store (and self-trust in dev mode)
69
+ self._setup_trust_store()
70
+
71
+ def sign_outbound(self, payload: Dict[str, Any], body: Optional[bytes] = None) -> str:
72
+ """
73
+ Sign a payload for outbound transmission.
74
+
75
+ Args:
76
+ payload: The JSON payload to sign.
77
+ body: Optional HTTP body bytes to bind to the signature.
78
+
79
+ Returns:
80
+ Compact JWS string.
81
+ """
82
+ # Inject issuer if missing
83
+ if "iss" not in payload:
84
+ payload["iss"] = self.agent_id
85
+
86
+ # Replay Protection: Inject timestamps
87
+ now = int(time.time())
88
+ payload["iat"] = now
89
+ payload["exp"] = now + MAX_TOKEN_AGE
90
+
91
+ # Integrity: Calculate Body Hash if body is provided
92
+ if body is not None:
93
+ # SHA-256 hash
94
+ sha256_hash = hashlib.sha256(body).digest()
95
+ # Base64Url encode (no padding)
96
+ bh = base64.urlsafe_b64encode(sha256_hash).decode('utf-8').rstrip('=')
97
+ payload["bh"] = bh
98
+
99
+ # Prepare headers
100
+ headers = {
101
+ "kid": self.signing_kid,
102
+ "typ": "JWT",
103
+ "alg": "EdDSA"
104
+ }
105
+
106
+ # Sign
107
+ try:
108
+ token = jwt.encode(
109
+ payload,
110
+ self._private_key,
111
+ algorithm="EdDSA",
112
+ headers=headers
113
+ )
114
+ return token
115
+ except Exception as e:
116
+ raise ConfigurationError(f"Failed to sign payload: {e}")
117
+
118
+ def verify_inbound(self, jws: str, body: Optional[bytes] = None) -> Dict[str, Any]:
119
+ """
120
+ Verify an inbound JWS.
121
+
122
+ Args:
123
+ jws: The compact JWS string.
124
+ body: Optional HTTP body bytes to verify against 'bh' claim.
125
+
126
+ Returns:
127
+ The verified payload.
128
+
129
+ Raises:
130
+ VerificationError: If signature is invalid, key is untrusted, or integrity check fails.
131
+ """
132
+ try:
133
+ # 1. Parse Header to get kid (without verifying yet)
134
+ header = jwt.get_unverified_header(jws)
135
+ kid = header.get("kid")
136
+
137
+ if not kid:
138
+ raise VerificationError("Missing 'kid' in JWS header.")
139
+
140
+ # 2. Resolution: Look for trusted key
141
+ trusted_key_path = self.trusted_dir / f"{kid}.pem"
142
+ if not trusted_key_path.exists():
143
+ logger.warning(f'{{"event": "agent_call_denied", "kid": "{kid}", "reason": "untrusted_key"}}')
144
+ raise VerificationError(f"Untrusted key ID: {kid}")
145
+
146
+ # Load the trusted public key
147
+ with open(trusted_key_path, "rb") as f:
148
+ public_key = serialization.load_pem_public_key(f.read())
149
+ # Ensure it is a key type compatible with jwt.decode (Ed25519PublicKey is supported)
150
+ # We cast to Any to satisfy mypy's strict check against the specific union
151
+ public_key = cast(Any, public_key)
152
+
153
+ # 3. Verify Signature
154
+ payload = jwt.decode(
155
+ jws,
156
+ public_key,
157
+ algorithms=["EdDSA"],
158
+ options={"verify_aud": False} # Audience verification depends on context, skipping for generic guard
159
+ )
160
+
161
+ # Cast payload to Dict[str, Any]
162
+ payload = cast(Dict[str, Any], payload)
163
+
164
+ # 4. Integrity Check (Body Hash)
165
+ if "bh" in payload:
166
+ if body is None:
167
+ raise VerificationError("JWS contains 'bh' claim but no body provided for verification.")
168
+
169
+ # Calculate hash of received body
170
+ sha256_hash = hashlib.sha256(body).digest()
171
+ calculated_bh = base64.urlsafe_b64encode(sha256_hash).decode('utf-8').rstrip('=')
172
+
173
+ if calculated_bh != payload["bh"]:
174
+ logger.warning(f'{{"event": "agent_call_denied", "kid": "{kid}", "reason": "integrity_check_failed"}}')
175
+ raise VerificationError("Integrity Check Failed: Body modified")
176
+
177
+ # 5. Replay Protection (Timestamp Enforcement)
178
+ now = int(time.time())
179
+ exp = payload.get("exp")
180
+ iat = payload.get("iat")
181
+
182
+ if exp is None or iat is None:
183
+ raise VerificationError("Missing timestamp claims (exp, iat).")
184
+
185
+ if now > (exp + CLOCK_SKEW_LEEWAY):
186
+ logger.warning(f'{{"event": "agent_call_denied", "kid": "{kid}", "reason": "token_expired"}}')
187
+ raise VerificationError("Token expired.")
188
+
189
+ if now < (iat - CLOCK_SKEW_LEEWAY):
190
+ logger.warning(f'{{"event": "agent_call_denied", "kid": "{kid}", "reason": "clock_skew"}}')
191
+ raise VerificationError("Token not yet valid (Clock skew).")
192
+
193
+ # 6. Observability
194
+ iss = payload.get("iss", "unknown")
195
+ logger.info(f'{{"event": "agent_call_allowed", "iss": "{iss}", "kid": "{kid}"}}')
196
+
197
+ return payload
198
+
199
+ except jwt.InvalidSignatureError:
200
+ logger.warning(f'{{"event": "agent_call_denied", "kid": "{kid}", "reason": "invalid_signature"}}')
201
+ raise VerificationError("Invalid signature.")
202
+ except jwt.ExpiredSignatureError:
203
+ logger.warning(f'{{"event": "agent_call_denied", "kid": "{kid}", "reason": "token_expired"}}')
204
+ raise VerificationError("Token expired.")
205
+ except jwt.DecodeError:
206
+ raise VerificationError("Invalid JWS format.")
207
+ except Exception as e:
208
+ if isinstance(e, VerificationError):
209
+ raise
210
+ raise VerificationError(f"Verification failed: {e}")
211
+
212
+ def make_headers(self, payload: Dict[str, Any], body: Optional[bytes] = None) -> Dict[str, str]:
213
+ """Helper to generate the headers containing the JWS."""
214
+ token = self.sign_outbound(payload, body=body)
215
+ return {"X-Capiscio-JWS": token}
216
+
217
+ def _resolve_project_root(self, base_dir: Optional[Union[str, Path]]) -> Path:
218
+ """Walk up the directory tree to find agent-card.json or stop at root."""
219
+ current = Path(base_dir or os.getcwd()).resolve()
220
+
221
+ # If we are in dev mode and nothing exists, we might just use cwd
222
+ # But let's try to find an existing project structure first
223
+ search_path = current
224
+ while search_path != search_path.parent:
225
+ if (search_path / "agent-card.json").exists():
226
+ return search_path
227
+ search_path = search_path.parent
228
+
229
+ # If not found, default to cwd
230
+ return current
231
+
232
+ def _load_or_generate_card(self) -> None:
233
+ """Load agent-card.json or generate a minimal one in dev_mode."""
234
+ if self.agent_card_path.exists():
235
+ try:
236
+ with open(self.agent_card_path, "r") as f:
237
+ data = json.load(f)
238
+ self.agent_id = data.get("agent_id")
239
+ # Assuming the first key is the signing key for now, or looking for a specific structure
240
+ # The mandate says: "Cache self.agent_id and self.signing_kid"
241
+ # We need to find the kid from the keys array.
242
+ keys = data.get("public_keys", [])
243
+ if not keys:
244
+ raise ConfigurationError("agent-card.json missing 'public_keys'.")
245
+ self.signing_kid = keys[0].get("kid")
246
+
247
+ if not self.agent_id or not self.signing_kid:
248
+ raise ConfigurationError("agent-card.json missing 'agent_id' or 'public_keys[0].kid'.")
249
+ except Exception as e:
250
+ raise ConfigurationError(f"Failed to load agent-card.json: {e}")
251
+ elif self.dev_mode:
252
+ # Generate minimal card
253
+ logger.info("Dev Mode: Generating minimal agent-card.json")
254
+ self.agent_id = "local-dev-agent"
255
+ self.signing_kid = "local-dev-key"
256
+
257
+ # We will populate the JWK part after generating the key in the next step
258
+ # For now, just set the basics, we'll write the file after key gen
259
+ else:
260
+ raise ConfigurationError(f"agent-card.json not found at {self.project_root}")
261
+
262
+ def _load_or_generate_keys(self) -> None:
263
+ """Load private.pem or generate it in dev_mode."""
264
+ if not self.keys_dir.exists():
265
+ if self.dev_mode:
266
+ self.keys_dir.mkdir(parents=True, exist_ok=True)
267
+ self.trusted_dir.mkdir(parents=True, exist_ok=True)
268
+ else:
269
+ raise ConfigurationError(f"capiscio_keys directory not found at {self.keys_dir}")
270
+
271
+ if self.private_key_path.exists():
272
+ try:
273
+ with open(self.private_key_path, "rb") as f:
274
+ loaded_key = serialization.load_pem_private_key(
275
+ f.read(), password=None
276
+ )
277
+ if not isinstance(loaded_key, ed25519.Ed25519PrivateKey):
278
+ raise ConfigurationError("Private key is not an Ed25519 key.")
279
+ self._private_key = loaded_key
280
+ except Exception as e:
281
+ raise ConfigurationError(f"Failed to load private.pem: {e}")
282
+ elif self.dev_mode:
283
+ logger.info("Dev Mode: Generating Ed25519 keypair")
284
+ self._private_key = ed25519.Ed25519PrivateKey.generate()
285
+
286
+ # Save Private Key
287
+ with open(self.private_key_path, "wb") as f:
288
+ f.write(self._private_key.private_bytes(
289
+ encoding=serialization.Encoding.PEM,
290
+ format=serialization.PrivateFormat.PKCS8,
291
+ encryption_algorithm=serialization.NoEncryption()
292
+ ))
293
+
294
+ # Save Public Key
295
+ public_key = self._private_key.public_key()
296
+ pem_public = public_key.public_bytes(
297
+ encoding=serialization.Encoding.PEM,
298
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
299
+ )
300
+ with open(self.public_key_path, "wb") as f:
301
+ f.write(pem_public)
302
+
303
+ # Now update agent-card.json with the JWK
304
+ self._update_agent_card_with_jwk(public_key)
305
+ else:
306
+ raise ConfigurationError(f"private.pem not found at {self.private_key_path}")
307
+
308
+ def _update_agent_card_with_jwk(self, public_key: ed25519.Ed25519PublicKey) -> None:
309
+ """Helper to write the agent-card.json with the generated key."""
310
+ # Convert Ed25519 public key to JWK parameters
311
+ # Ed25519 keys are simple: x is the raw bytes
312
+ raw_bytes = public_key.public_bytes(
313
+ encoding=serialization.Encoding.Raw,
314
+ format=serialization.PublicFormat.Raw
315
+ )
316
+ x_b64 = base64.urlsafe_b64encode(raw_bytes).decode('utf-8').rstrip('=')
317
+
318
+ jwk = {
319
+ "kty": "OKP",
320
+ "crv": "Ed25519",
321
+ "x": x_b64,
322
+ "kid": self.signing_kid,
323
+ "use": "sig"
324
+ }
325
+
326
+ card_data = {
327
+ "agent_id": self.agent_id,
328
+ "public_keys": [jwk],
329
+ "protocolVersion": "0.3.0",
330
+ "name": "Local Dev Agent",
331
+ "description": "Auto-generated by SimpleGuard",
332
+ "url": "http://localhost:8000",
333
+ "version": "0.1.0",
334
+ "provider": {
335
+ "organization": "Local Dev"
336
+ }
337
+ }
338
+
339
+ with open(self.agent_card_path, "w") as f:
340
+ json.dump(card_data, f, indent=2)
341
+ logger.info(f"Created agent-card.json at {self.agent_card_path}")
342
+
343
+ def _setup_trust_store(self) -> None:
344
+ """Ensure trust store exists and add self-trust in dev_mode."""
345
+ if not self.trusted_dir.exists() and self.dev_mode:
346
+ self.trusted_dir.mkdir(parents=True, exist_ok=True)
347
+
348
+ if self.dev_mode:
349
+ # Self-Trust: Copy public.pem to trusted/{kid}.pem
350
+ self_trust_path = self.trusted_dir / f"{self.signing_kid}.pem"
351
+ if not self_trust_path.exists() and self.public_key_path.exists():
352
+ import shutil
353
+ shutil.copy(self.public_key_path, self_trust_path)
354
+ logger.info(f"Dev Mode: Added self-trust for kid {self.signing_kid}")
capiscio_sdk/types.py CHANGED
@@ -1,4 +1,4 @@
1
- """Core types for Capiscio A2A Security."""
1
+ """Core types for Capiscio Python SDK."""
2
2
  from typing import List, Dict, Any, Optional
3
3
  from pydantic import BaseModel, Field
4
4
  from enum import Enum
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: capiscio-sdk
3
+ Version: 0.3.0
4
+ Summary: Runtime security middleware for A2A agents
5
+ Project-URL: Homepage, https://capisc.io
6
+ Project-URL: Documentation, https://docs.capisc.io/sdk-python
7
+ Project-URL: Repository, https://github.com/capiscio/capiscio-sdk-python
8
+ Project-URL: Issues, https://github.com/capiscio/capiscio-sdk-python/issues
9
+ Author-email: Capiscio Team <team@capisc.io>
10
+ License: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: a2a,agent,agent-to-agent,middleware,security,validation
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: a2a-sdk>=0.1.0
25
+ Requires-Dist: cachetools>=5.3.0
26
+ Requires-Dist: cryptography>=42.0.0
27
+ Requires-Dist: httpx>=0.27.0
28
+ Requires-Dist: pydantic>=2.0.0
29
+ Requires-Dist: pyjwt[crypto]>=2.8.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: black>=24.0.0; extra == 'dev'
32
+ Requires-Dist: fastapi>=0.100.0; extra == 'dev'
33
+ Requires-Dist: mypy>=1.9.0; extra == 'dev'
34
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
35
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
36
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
37
+ Requires-Dist: ruff>=0.3.0; extra == 'dev'
38
+ Requires-Dist: starlette>=0.27.0; extra == 'dev'
39
+ Requires-Dist: types-cachetools>=5.3.0; extra == 'dev'
40
+ Provides-Extra: web
41
+ Requires-Dist: fastapi>=0.100.0; extra == 'web'
42
+ Requires-Dist: starlette>=0.27.0; extra == 'web'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # CapiscIO SDK (Python)
46
+
47
+ **Enforcement-First Security for A2A Agents.**
48
+
49
+ [![PyPI version](https://badge.fury.io/py/capiscio-sdk.svg)](https://badge.fury.io/py/capiscio-sdk)
50
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
51
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
52
+
53
+ **CapiscIO** is the "Customs Officer" for your AI Agent. It provides military-grade Identity and Integrity enforcement for the [Agent-to-Agent (A2A) Protocol](https://github.com/google/A2A) with **zero configuration**.
54
+
55
+ ## 🚀 The 60-Second Upgrade
56
+
57
+ Turn any FastAPI application into a Verified A2A Agent in 3 lines of code.
58
+
59
+ ```python
60
+ from fastapi import FastAPI
61
+ from capiscio_sdk.simple_guard import SimpleGuard
62
+ from capiscio_sdk.integrations.fastapi import CapiscioMiddleware
63
+
64
+ # 1. Initialize Guard (Auto-generates keys in dev_mode)
65
+ guard = SimpleGuard(dev_mode=True)
66
+
67
+ app = FastAPI()
68
+
69
+ # 2. Add Enforcement Middleware
70
+ app.add_middleware(CapiscioMiddleware, guard=guard)
71
+
72
+ @app.post("/agent/task")
73
+ async def handle_task(request: Request):
74
+ # 🔒 Only reachable if Identity + Integrity are verified
75
+ caller = request.state.agent_id
76
+ return {"status": "accepted", "verified_caller": caller}
77
+ ```
78
+
79
+ ## 🛡️ What You Get (Out of the Box)
80
+
81
+ 1. **Zero-Config Identity**:
82
+ * Auto-generates **Ed25519** keys and `agent-card.json` on first run.
83
+ * No manual key management required for development.
84
+
85
+ 2. **Payload Integrity**:
86
+ * Enforces **SHA-256 Body Hash (`bh`)** verification.
87
+ * Blocks tampered payloads instantly (returns `403 Forbidden`).
88
+
89
+ 3. **Replay Protection**:
90
+ * Enforces strict **60-second** token expiration (`exp`).
91
+ * Prevents replay attacks and ensures freshness.
92
+
93
+ 4. **Performance Telemetry**:
94
+ * Adds `<1ms` overhead.
95
+ * Includes `Server-Timing` headers for transparent monitoring.
96
+
97
+ ## Installation
98
+
99
+ ```bash
100
+ pip install capiscio-sdk
101
+ ```
102
+
103
+ ## How It Works
104
+
105
+ ### 1. The Handshake
106
+ CapiscIO enforces the **A2A Trust Protocol**:
107
+ * **Sender**: Signs the request body (JWS + Body Hash).
108
+ * **Receiver**: Verifies the signature and re-hashes the body to ensure integrity.
109
+
110
+ ### 2. The "Customs Officer"
111
+ The `SimpleGuard` acts as a local authority. It manages your agent's "Passport" (Agent Card) and verifies the "Visas" (Tokens) of incoming requests.
112
+
113
+ ### 3. Telemetry
114
+ Every response includes a `Server-Timing` header showing exactly how fast the verification was:
115
+ ```http
116
+ Server-Timing: capiscio-auth;dur=0.618;desc="CapiscIO Verification"
117
+ ```
118
+
119
+ ## Documentation
120
+
121
+ - [Official Documentation](https://docs.capisc.io)
122
+ - [A2A Protocol Spec](https://github.com/google/A2A)
123
+
124
+ ## License
125
+
126
+ Apache License 2.0 - see [LICENSE](LICENSE) for details.
@@ -1,12 +1,14 @@
1
- capiscio_sdk/__init__.py,sha256=FeQaj_zfpzATbqqFP1vsRbJ_XT5NcBFm4vpR7efoqbE,1108
2
- capiscio_sdk/config.py,sha256=S5iclYkxG-29UY4ipVU-7DhmeTR2zell6TiX3yrRvE0,3974
3
- capiscio_sdk/errors.py,sha256=MCDh1fAAswXPpKoZEqVG_eD8rOmHv38GFEFFvz1GRjI,1859
4
- capiscio_sdk/executor.py,sha256=EKM_93thP5utyLEH2DZNTL1KY9YZRyOvWNBMzIgpTsU,7226
1
+ capiscio_sdk/__init__.py,sha256=d1tXC6Mh3yogY-KLPLSJPzAe5lx-GsnR1EPfAalBv00,1165
2
+ capiscio_sdk/config.py,sha256=-1KNubnRE06SENRXcnGvrj-y1EfKz2uAMdz7s0n6ark,3972
3
+ capiscio_sdk/errors.py,sha256=vGk7fhf07MM4-p7tvMXZlIz0yHQw9J03yIGGqRAEWDY,2107
4
+ capiscio_sdk/executor.py,sha256=jalft0M0PvyfN-5sxuPlD5hExC8EW_Af92pR2KwEtyc,7753
5
5
  capiscio_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- capiscio_sdk/types.py,sha256=mMh_4VTYRy0xE3Cng1WJEeHigAFUVroRG1GVHtzTN2E,7980
6
+ capiscio_sdk/simple_guard.py,sha256=0s85UBDvNw8zG0y-gqZSibWEV_4LaykMATs-tIPCD5M,14516
7
+ capiscio_sdk/types.py,sha256=A_0SLhrbX7apkH5yYGTkG-_284xRjCK0N3lEseVRJRs,7978
7
8
  capiscio_sdk/infrastructure/__init__.py,sha256=5yK7kbk1tcHqlnFIN0lR5N_4ZWWquNivLfHkXOAtso0,152
8
9
  capiscio_sdk/infrastructure/cache.py,sha256=FjIRRzNbsPxFZrxdeek51IwFSmR682Wpj1bvvmgqwiU,1841
9
10
  capiscio_sdk/infrastructure/rate_limiter.py,sha256=7Q_GfcooWKjioDeB46PpPb00laDmkLpUYGM0Ui8nXqI,3642
11
+ capiscio_sdk/integrations/fastapi.py,sha256=op8Bwib0IoebIOihUzDEimY1Z_wmX2b3I374Io5bVcU,2896
10
12
  capiscio_sdk/scoring/__init__.py,sha256=OjHeqLe6aZ7yjbpmtt_upqXDwYUnX5CDXTVNngOMHcg,1083
11
13
  capiscio_sdk/scoring/availability.py,sha256=CzXA1ED48U1Cc06sh0Mtl_kxZP6af-9cceBumTXQhO8,9130
12
14
  capiscio_sdk/scoring/compliance.py,sha256=JZyYuT18A_eiDNdOz-doTIYwW6YhVPvfRj_siNAkkTY,9780
@@ -20,7 +22,7 @@ capiscio_sdk/validators/protocol.py,sha256=bkaJJXseulTJ4Sdiio8gE8Q_Pyqj0BRsJe6BG
20
22
  capiscio_sdk/validators/semver.py,sha256=mlF3GO5ZPA-w6FzSxhjcr56sgCdS0YVVAd1dUr1bxWs,6385
21
23
  capiscio_sdk/validators/signature.py,sha256=lI8XzaKfG_dXSOQXZ40Lda0ntga9EqqC4zAId2kOt6g,8072
22
24
  capiscio_sdk/validators/url_security.py,sha256=SdpOrB48hrfgAMuLvpWH2P0LLCJtg6QBohGDIye8f1E,9802
23
- capiscio_sdk-0.2.0.dist-info/METADATA,sha256=k3oDfOrYLqCpPighcDEnKPMNX3SRVsBxHfiAO6H7Oyc,7639
24
- capiscio_sdk-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- capiscio_sdk-0.2.0.dist-info/licenses/LICENSE,sha256=AMM_E-ILcCpX0JALqX3BL9yfgSx654BtkhX-CBFYp1Q,10758
26
- capiscio_sdk-0.2.0.dist-info/RECORD,,
25
+ capiscio_sdk-0.3.0.dist-info/METADATA,sha256=MMJh_eCqXM-DyX1bkj3CDJG3UqxBE-Ie81peUbEnV-0,4641
26
+ capiscio_sdk-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ capiscio_sdk-0.3.0.dist-info/licenses/LICENSE,sha256=AMM_E-ILcCpX0JALqX3BL9yfgSx654BtkhX-CBFYp1Q,10758
28
+ capiscio_sdk-0.3.0.dist-info/RECORD,,
@@ -1,221 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: capiscio-sdk
3
- Version: 0.2.0
4
- Summary: Runtime security middleware for A2A agents
5
- Project-URL: Homepage, https://capisc.io
6
- Project-URL: Documentation, https://docs.capisc.io/sdk-python
7
- Project-URL: Repository, https://github.com/capiscio/capiscio-sdk-python
8
- Project-URL: Issues, https://github.com/capiscio/capiscio-sdk-python/issues
9
- Author-email: Capiscio Team <team@capisc.io>
10
- License: Apache-2.0
11
- License-File: LICENSE
12
- Keywords: a2a,agent,agent-to-agent,middleware,security,validation
13
- Classifier: Development Status :: 3 - Alpha
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: Apache Software License
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Topic :: Security
22
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
- Requires-Python: >=3.10
24
- Requires-Dist: a2a-sdk>=0.1.0
25
- Requires-Dist: cachetools>=5.3.0
26
- Requires-Dist: cryptography>=42.0.0
27
- Requires-Dist: httpx>=0.27.0
28
- Requires-Dist: pydantic>=2.0.0
29
- Requires-Dist: pyjwt[crypto]>=2.8.0
30
- Provides-Extra: dev
31
- Requires-Dist: black>=24.0.0; extra == 'dev'
32
- Requires-Dist: mypy>=1.9.0; extra == 'dev'
33
- Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
34
- Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
35
- Requires-Dist: pytest>=8.0.0; extra == 'dev'
36
- Requires-Dist: ruff>=0.3.0; extra == 'dev'
37
- Requires-Dist: types-cachetools>=5.3.0; extra == 'dev'
38
- Description-Content-Type: text/markdown
39
-
40
- # CapiscIO SDK (Python)
41
-
42
- **Runtime security middleware for A2A (Agent-to-Agent) protocol agents**
43
-
44
- [![PyPI version](https://badge.fury.io/py/capiscio-sdk.svg)](https://badge.fury.io/py/capiscio-sdk)
45
- [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
46
- [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
47
-
48
- ## What is CapiscIO SDK?
49
-
50
- CapiscIO SDK provides **always-on runtime protection** for agents using the [A2A (Agent-to-Agent) protocol](https://github.com/google/A2A). It wraps your agent executor to validate incoming requests, verify signatures, and protect against malicious actors—all without requiring peer cooperation.
51
-
52
- ### Key Features
53
-
54
- - ✅ **Message validation** - Schema and protocol compliance checking
55
- - ✅ **Signature verification** - JWS/JWKS cryptographic validation (RFC 7515)
56
- - ✅ **Upstream protection** - Validate agents you call
57
- - ✅ **Downstream protection** - Validate agents calling you
58
- - ✅ **Rate limiting** - Token bucket algorithm
59
- - ✅ **Caching** - Performance-optimized validation results
60
- - ✅ **Three integration patterns** - Minimal, explicit, or decorator
61
-
62
- ## Installation
63
-
64
- ```bash
65
- pip install capiscio-sdk
66
- ```
67
-
68
- ## Quick Start
69
-
70
-
71
- ### Pattern 1: Minimal (One-liner with Preset)
72
-
73
- ```python
74
- from capiscio_sdk import secure, SecurityConfig
75
- from a2a.server.request_handlers import DefaultRequestHandler
76
- from a2a.server.tasks import InMemoryTaskStore
77
-
78
- # Wrap your agent with security (production defaults)
79
- agent = secure(MyAgentExecutor(), SecurityConfig.production())
80
-
81
- # Use in A2A request handler
82
- handler = DefaultRequestHandler(
83
- agent_executor=agent,
84
- task_store=InMemoryTaskStore()
85
- )
86
-
87
- # Access validation results (three-dimensional scoring)
88
- result = await agent.validate_agent_card(card_url)
89
- print(result.compliance.total, result.trust.total, result.availability.total)
90
- ```
91
-
92
- ### Pattern 2: Granular Control
93
-
94
- ```python
95
- from capiscio_sdk import CapiscIOSecurityExecutor, SecurityConfig
96
-
97
- # Start with a preset, customize what matters to you
98
- config = SecurityConfig.production()
99
- config.downstream.rate_limit_requests_per_minute = 100 # Higher rate limit
100
- config.downstream.require_signatures = True # Enforce signatures
101
- config.upstream.test_endpoints = True # Test before calling
102
- config.fail_mode = "monitor" # Log but don't block yet
103
-
104
- secure_agent = CapiscIOSecurityExecutor(
105
- delegate=MyAgentExecutor(),
106
- config=config
107
- )
108
- ```
109
-
110
- ### Pattern 3: Environment-Driven (12-Factor App)
111
-
112
- ```python
113
- from capiscio_sdk import secure_agent, SecurityConfig
114
- from a2a import AgentExecutor, RequestContext, EventQueue
115
-
116
- @secure_agent(config=SecurityConfig.from_env())
117
- class MyAgentExecutor(AgentExecutor):
118
- async def execute(self, context: RequestContext, event_queue: EventQueue):
119
- # Your agent logic - config loaded from env vars
120
- pass
121
-
122
- # Already secured - use directly!
123
- handler = DefaultRequestHandler(agent_executor=MyAgentExecutor())
124
- ```
125
-
126
- **All 16 configuration options documented in the [Configuration Guide](https://docs.capisc.io/sdk-python/guides/configuration/).**
127
-
128
- ## Why CapiscIO?
129
-
130
- ### The Problem
131
-
132
- When building A2A agents, you face security risks from:
133
- - **Malicious downstream agents** sending invalid/malicious requests
134
- - **Broken upstream dependencies** with invalid agent cards
135
- - **Protocol violations** causing runtime failures
136
- - **Missing signatures** with no authenticity verification
137
-
138
- ### The Solution
139
-
140
- CapiscIO wraps your agent executor and provides:
141
-
142
- 1. **Downstream Protection** - Validates all incoming requests
143
- 2. **Upstream Protection** - Validates agents you call
144
- 3. **Always-On** - Works without peer cooperation
145
- 4. **Performance** - Caching and parallel validation
146
- 5. **Three-Dimensional Scoring** - Compliance, trust, and availability insights
147
-
148
- ## Configuration
149
-
150
- ### Presets
151
-
152
- ```python
153
- # Development - Permissive, verbose logging
154
- SecurityConfig.development()
155
-
156
- # Production - Balanced (default)
157
- SecurityConfig.production()
158
-
159
- # Strict - Maximum security
160
- SecurityConfig.strict()
161
-
162
- # From environment variables
163
- SecurityConfig.from_env()
164
- ```
165
-
166
- ### Custom Configuration
167
-
168
- ```python
169
- from capiscio_sdk import SecurityConfig, DownstreamConfig, UpstreamConfig
170
-
171
- config = SecurityConfig(
172
- downstream=DownstreamConfig(
173
- validate_schema=True,
174
- verify_signatures=True,
175
- require_signatures=False,
176
- enable_rate_limiting=True,
177
- rate_limit_requests_per_minute=100
178
- ),
179
- upstream=UpstreamConfig(
180
- validate_agent_cards=True,
181
- verify_signatures=True,
182
- cache_validation=True,
183
- cache_timeout=3600 # seconds
184
- ),
185
- fail_mode="block", # "block" | "monitor" | "log"
186
- timeout_ms=5000
187
- )
188
- ```
189
-
190
- ## Documentation
191
-
192
- - [Quickstart Guide](docs/quickstart.md)
193
- - [Configuration Reference](docs/configuration.md)
194
- - [API Documentation](docs/api-reference.md)
195
- - [Examples](examples/)
196
-
197
- ## Roadmap
198
-
199
- - **V1.0** (Q4 2025) - Core middleware (this package)
200
- - **V2.0** (Q2 2026) - Extension protocol (validation feedback)
201
- - **V3.0** (Q3 2026) - Platform integration (trust network)
202
- - **V4.0** (Q4 2026) - Enterprise features (policies, audit logs)
203
-
204
- ## Contributing
205
-
206
- We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
207
-
208
- ## License
209
-
210
- Apache License 2.0 - see [LICENSE](LICENSE) for details.
211
-
212
- ## About A2A
213
-
214
- The [Agent-to-Agent (A2A) protocol](https://github.com/google/A2A) is an open standard for agent interoperability, supported by Google and 50+ partners including Salesforce, ServiceNow, SAP, Intuit, and more. CapiscIO provides the security layer for production A2A deployments.
215
-
216
- ## Support
217
-
218
- - **Issues:** [GitHub Issues](https://github.com/capiscio/capiscio-sdk-python/issues)
219
- - **Discussions:** [GitHub Discussions](https://github.com/capiscio/capiscio-sdk-python/discussions)
220
- - **Documentation:** [docs.capisc.io](https://docs.capisc.io)
221
- - **Website:** [capisc.io](https://capisc.io)