capiscio-sdk 0.2.0__py3-none-any.whl → 2.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 +69 -1
- capiscio_sdk/_rpc/__init__.py +7 -0
- capiscio_sdk/_rpc/client.py +1321 -0
- capiscio_sdk/_rpc/gen/__init__.py +1 -0
- capiscio_sdk/_rpc/process.py +232 -0
- capiscio_sdk/badge.py +737 -0
- capiscio_sdk/badge_keeper.py +304 -0
- capiscio_sdk/config.py +1 -1
- capiscio_sdk/dv.py +296 -0
- capiscio_sdk/errors.py +11 -1
- capiscio_sdk/executor.py +17 -0
- capiscio_sdk/integrations/fastapi.py +74 -0
- capiscio_sdk/scoring/__init__.py +73 -3
- capiscio_sdk/simple_guard.py +346 -0
- capiscio_sdk/types.py +1 -1
- capiscio_sdk/validators/__init__.py +59 -2
- capiscio_sdk/validators/_core.py +376 -0
- capiscio_sdk-2.3.0.dist-info/METADATA +532 -0
- capiscio_sdk-2.3.0.dist-info/RECORD +36 -0
- {capiscio_sdk-0.2.0.dist-info → capiscio_sdk-2.3.0.dist-info}/WHEEL +1 -1
- capiscio_sdk-0.2.0.dist-info/METADATA +0 -221
- capiscio_sdk-0.2.0.dist-info/RECORD +0 -26
- {capiscio_sdk-0.2.0.dist-info → capiscio_sdk-2.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
# RFC-002 §9.1: X-Capiscio-Badge header
|
|
36
|
+
auth_header = request.headers.get("X-Capiscio-Badge")
|
|
37
|
+
|
|
38
|
+
# If no header, we might let it pass but mark as unverified?
|
|
39
|
+
# The mandate says: "Returns 401 (missing) or 403 (invalid)."
|
|
40
|
+
if not auth_header:
|
|
41
|
+
return JSONResponse(
|
|
42
|
+
{"error": "Missing X-Capiscio-Badge header. This endpoint is protected by CapiscIO."},
|
|
43
|
+
status_code=401
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
start_time = time.perf_counter()
|
|
47
|
+
try:
|
|
48
|
+
# Read the body for integrity check
|
|
49
|
+
body_bytes = await request.body()
|
|
50
|
+
|
|
51
|
+
# Verify the JWS with body
|
|
52
|
+
payload = self.guard.verify_inbound(auth_header, body=body_bytes)
|
|
53
|
+
|
|
54
|
+
# Reset the receive channel so downstream can read the body
|
|
55
|
+
async def receive() -> Dict[str, Any]:
|
|
56
|
+
return {"type": "http.request", "body": body_bytes, "more_body": False}
|
|
57
|
+
request._receive = receive
|
|
58
|
+
|
|
59
|
+
# Inject claims into request.state
|
|
60
|
+
request.state.agent = payload
|
|
61
|
+
request.state.agent_id = payload.get("iss")
|
|
62
|
+
|
|
63
|
+
except VerificationError as e:
|
|
64
|
+
return JSONResponse({"error": f"Access Denied: {str(e)}"}, status_code=403)
|
|
65
|
+
|
|
66
|
+
verification_duration = (time.perf_counter() - start_time) * 1000
|
|
67
|
+
|
|
68
|
+
response = await call_next(request)
|
|
69
|
+
|
|
70
|
+
# Add Server-Timing header (standard for performance metrics)
|
|
71
|
+
# Syntax: metric_name;dur=123.4;desc="Description"
|
|
72
|
+
response.headers["Server-Timing"] = f"capiscio-auth;dur={verification_duration:.3f};desc=\"CapiscIO Verification\""
|
|
73
|
+
|
|
74
|
+
return response
|
capiscio_sdk/scoring/__init__.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""Multi-dimensional scoring system for A2A validation.
|
|
2
2
|
|
|
3
|
+
.. deprecated:: 0.3.0
|
|
4
|
+
The pure Python scoring module is deprecated. Scoring is now handled by
|
|
5
|
+
capiscio-core via gRPC. Use :func:`capiscio_sdk.validate_agent_card` for
|
|
6
|
+
Agent Card validation with scoring, which delegates to Go core.
|
|
7
|
+
|
|
3
8
|
This module provides three independent scoring dimensions:
|
|
4
9
|
- Compliance: Protocol specification adherence (0-100)
|
|
5
10
|
- Trust: Security and authenticity signals (0-100)
|
|
@@ -7,8 +12,14 @@ This module provides three independent scoring dimensions:
|
|
|
7
12
|
|
|
8
13
|
Each dimension has its own rating scale and breakdown structure,
|
|
9
14
|
allowing users to make nuanced decisions based on their priorities.
|
|
15
|
+
|
|
16
|
+
NOTE: The types in this module are still used for API compatibility.
|
|
17
|
+
The scorer classes (ComplianceScorer, TrustScorer, AvailabilityScorer)
|
|
18
|
+
are deprecated and should not be used directly.
|
|
10
19
|
"""
|
|
11
20
|
|
|
21
|
+
import warnings as _warnings
|
|
22
|
+
|
|
12
23
|
from .types import (
|
|
13
24
|
ComplianceScore,
|
|
14
25
|
TrustScore,
|
|
@@ -21,11 +32,69 @@ from .types import (
|
|
|
21
32
|
AvailabilityRating,
|
|
22
33
|
ScoringContext,
|
|
23
34
|
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
from .
|
|
35
|
+
|
|
36
|
+
# Import deprecated scorers with warnings
|
|
37
|
+
from .compliance import ComplianceScorer as _LegacyComplianceScorer
|
|
38
|
+
from .trust import TrustScorer as _LegacyTrustScorer
|
|
39
|
+
from .availability import AvailabilityScorer as _LegacyAvailabilityScorer
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ComplianceScorer(_LegacyComplianceScorer):
|
|
43
|
+
"""Compliance scorer (DEPRECATED - scoring now handled by Go core).
|
|
44
|
+
|
|
45
|
+
.. deprecated:: 0.3.0
|
|
46
|
+
Use :func:`capiscio_sdk.validate_agent_card` which delegates
|
|
47
|
+
scoring to capiscio-core. This class will be removed in v1.0.0.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, *args, **kwargs):
|
|
51
|
+
_warnings.warn(
|
|
52
|
+
"ComplianceScorer is deprecated. Scoring is now handled by Go core. "
|
|
53
|
+
"Use capiscio_sdk.validate_agent_card() instead.",
|
|
54
|
+
DeprecationWarning,
|
|
55
|
+
stacklevel=2
|
|
56
|
+
)
|
|
57
|
+
super().__init__(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TrustScorer(_LegacyTrustScorer):
|
|
61
|
+
"""Trust scorer (DEPRECATED - scoring now handled by Go core).
|
|
62
|
+
|
|
63
|
+
.. deprecated:: 0.3.0
|
|
64
|
+
Use :func:`capiscio_sdk.validate_agent_card` which delegates
|
|
65
|
+
scoring to capiscio-core. This class will be removed in v1.0.0.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *args, **kwargs):
|
|
69
|
+
_warnings.warn(
|
|
70
|
+
"TrustScorer is deprecated. Scoring is now handled by Go core. "
|
|
71
|
+
"Use capiscio_sdk.validate_agent_card() instead.",
|
|
72
|
+
DeprecationWarning,
|
|
73
|
+
stacklevel=2
|
|
74
|
+
)
|
|
75
|
+
super().__init__(*args, **kwargs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AvailabilityScorer(_LegacyAvailabilityScorer):
|
|
79
|
+
"""Availability scorer (DEPRECATED - scoring now handled by Go core).
|
|
80
|
+
|
|
81
|
+
.. deprecated:: 0.3.0
|
|
82
|
+
Use :func:`capiscio_sdk.validate_agent_card` which delegates
|
|
83
|
+
scoring to capiscio-core. This class will be removed in v1.0.0.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, *args, **kwargs):
|
|
87
|
+
_warnings.warn(
|
|
88
|
+
"AvailabilityScorer is deprecated. Scoring is now handled by Go core. "
|
|
89
|
+
"Use capiscio_sdk.validate_agent_card() instead.",
|
|
90
|
+
DeprecationWarning,
|
|
91
|
+
stacklevel=2
|
|
92
|
+
)
|
|
93
|
+
super().__init__(*args, **kwargs)
|
|
94
|
+
|
|
27
95
|
|
|
28
96
|
__all__ = [
|
|
97
|
+
# Types (still used)
|
|
29
98
|
"ComplianceScore",
|
|
30
99
|
"TrustScore",
|
|
31
100
|
"AvailabilityScore",
|
|
@@ -36,6 +105,7 @@ __all__ = [
|
|
|
36
105
|
"TrustRating",
|
|
37
106
|
"AvailabilityRating",
|
|
38
107
|
"ScoringContext",
|
|
108
|
+
# Deprecated scorers (kept for backward compatibility)
|
|
39
109
|
"ComplianceScorer",
|
|
40
110
|
"TrustScorer",
|
|
41
111
|
"AvailabilityScorer",
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""SimpleGuard - Zero-config security for A2A agents.
|
|
2
|
+
|
|
3
|
+
This module provides signing and verification of A2A messages using
|
|
4
|
+
the capiscio-core Go library via gRPC. All cryptographic operations
|
|
5
|
+
are delegated to the Go core for consistency across SDKs.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Dict, Any, Union
|
|
12
|
+
|
|
13
|
+
from .errors import ConfigurationError, VerificationError
|
|
14
|
+
from ._rpc.client import CapiscioRPCClient
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SimpleGuard:
|
|
20
|
+
"""
|
|
21
|
+
Zero-config security middleware for A2A agents.
|
|
22
|
+
|
|
23
|
+
SimpleGuard handles message signing and verification using Ed25519
|
|
24
|
+
keys. All cryptographic operations are performed by the capiscio-core
|
|
25
|
+
Go library via gRPC, ensuring consistent behavior across all SDKs.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> guard = SimpleGuard(dev_mode=True)
|
|
29
|
+
>>> token = guard.sign_outbound({"sub": "test"}, body=b"hello")
|
|
30
|
+
>>> claims = guard.verify_inbound(token, body=b"hello")
|
|
31
|
+
|
|
32
|
+
# With explicit agent_id:
|
|
33
|
+
>>> guard = SimpleGuard(agent_id="did:web:example.com:agents:myagent")
|
|
34
|
+
|
|
35
|
+
# In dev mode, did:key is auto-generated:
|
|
36
|
+
>>> guard = SimpleGuard(dev_mode=True)
|
|
37
|
+
>>> print(guard.agent_id) # did:key:z6Mk...
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
base_dir: Optional[Union[str, Path]] = None,
|
|
43
|
+
dev_mode: bool = False,
|
|
44
|
+
rpc_address: Optional[str] = None,
|
|
45
|
+
agent_id: Optional[str] = None,
|
|
46
|
+
badge_token: Optional[str] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Initialize SimpleGuard.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
base_dir: Starting directory to search for config (defaults to cwd).
|
|
53
|
+
dev_mode: If True, auto-generates keys with did:key identity (RFC-002 §6.1).
|
|
54
|
+
rpc_address: gRPC server address. If None, auto-starts local server.
|
|
55
|
+
agent_id: Explicit agent DID. If None:
|
|
56
|
+
- In dev_mode: Auto-generates did:key from keypair
|
|
57
|
+
- Otherwise: Loaded from agent-card.json
|
|
58
|
+
badge_token: Pre-obtained badge token to use for identity. When set,
|
|
59
|
+
make_headers() will use this token instead of signing.
|
|
60
|
+
"""
|
|
61
|
+
self.dev_mode = dev_mode
|
|
62
|
+
self._explicit_agent_id = agent_id
|
|
63
|
+
self._badge_token = badge_token
|
|
64
|
+
|
|
65
|
+
# 1. Safety Check
|
|
66
|
+
if self.dev_mode and os.getenv("CAPISCIO_ENV") == "prod":
|
|
67
|
+
logger.critical(
|
|
68
|
+
"CRITICAL: SimpleGuard initialized in dev_mode=True but CAPISCIO_ENV=prod. "
|
|
69
|
+
"This is insecure! Disable dev_mode in production."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# 2. Resolve base_dir
|
|
73
|
+
self.project_root = self._resolve_project_root(base_dir)
|
|
74
|
+
self.keys_dir = self.project_root / "capiscio_keys"
|
|
75
|
+
self.trusted_dir = self.keys_dir / "trusted"
|
|
76
|
+
self.agent_card_path = self.project_root / "agent-card.json"
|
|
77
|
+
|
|
78
|
+
# 3. Connect to gRPC server
|
|
79
|
+
self._client = CapiscioRPCClient(address=rpc_address)
|
|
80
|
+
self._client.connect()
|
|
81
|
+
|
|
82
|
+
# 4. Load or generate agent identity
|
|
83
|
+
self.agent_id: str
|
|
84
|
+
self.signing_kid: str
|
|
85
|
+
self._load_or_generate_card()
|
|
86
|
+
|
|
87
|
+
# 5. Load or generate keys via gRPC (may update agent_id with did:key)
|
|
88
|
+
self._load_or_generate_keys()
|
|
89
|
+
|
|
90
|
+
# 6. Load trust store
|
|
91
|
+
self._setup_trust_store()
|
|
92
|
+
|
|
93
|
+
def sign_outbound(self, payload: Dict[str, Any], body: Optional[bytes] = None) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Sign a payload for outbound transmission.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
payload: The JSON payload to sign.
|
|
99
|
+
body: Optional HTTP body bytes to bind to the signature.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Compact JWS string.
|
|
103
|
+
"""
|
|
104
|
+
# Inject issuer if missing
|
|
105
|
+
if "iss" not in payload:
|
|
106
|
+
payload["iss"] = self.agent_id
|
|
107
|
+
|
|
108
|
+
# Use body for binding if provided
|
|
109
|
+
body_bytes = body or b""
|
|
110
|
+
|
|
111
|
+
# Sign via gRPC - use SignAttached which handles timestamps and body hash
|
|
112
|
+
jws, error = self._client.simpleguard.sign_attached(
|
|
113
|
+
payload=body_bytes, # This gets hashed into 'bh' claim
|
|
114
|
+
key_id=self.signing_kid,
|
|
115
|
+
headers={"iss": self.agent_id},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if error:
|
|
119
|
+
raise ConfigurationError(f"Failed to sign payload: {error}")
|
|
120
|
+
|
|
121
|
+
return jws
|
|
122
|
+
|
|
123
|
+
def verify_inbound(self, jws: str, body: Optional[bytes] = None) -> Dict[str, Any]:
|
|
124
|
+
"""
|
|
125
|
+
Verify an inbound JWS.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
jws: The compact JWS string.
|
|
129
|
+
body: Optional HTTP body bytes to verify against 'bh' claim.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The verified payload.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
VerificationError: If signature is invalid, key is untrusted, or integrity check fails.
|
|
136
|
+
"""
|
|
137
|
+
valid, payload_bytes, key_id, error = self._client.simpleguard.verify_attached(
|
|
138
|
+
jws=jws,
|
|
139
|
+
body=body,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if error:
|
|
143
|
+
logger.warning(f'{{"event": "agent_call_denied", "reason": "{error}"}}')
|
|
144
|
+
raise VerificationError(error)
|
|
145
|
+
|
|
146
|
+
if not valid:
|
|
147
|
+
raise VerificationError("Verification failed")
|
|
148
|
+
|
|
149
|
+
# Parse payload
|
|
150
|
+
try:
|
|
151
|
+
payload = json.loads(payload_bytes) if payload_bytes else {}
|
|
152
|
+
except json.JSONDecodeError:
|
|
153
|
+
payload = {}
|
|
154
|
+
|
|
155
|
+
iss = payload.get("iss", "unknown")
|
|
156
|
+
logger.info(f'{{"event": "agent_call_allowed", "iss": "{iss}", "kid": "{key_id}"}}')
|
|
157
|
+
|
|
158
|
+
return payload
|
|
159
|
+
|
|
160
|
+
def make_headers(self, payload: Dict[str, Any], body: Optional[bytes] = None) -> Dict[str, str]:
|
|
161
|
+
"""
|
|
162
|
+
Generate headers containing the Badge (RFC-002 §9.1).
|
|
163
|
+
|
|
164
|
+
If a badge_token was provided at construction, it will be used.
|
|
165
|
+
Otherwise, signs the payload to create a token.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
payload: The JSON payload to sign (ignored if using badge_token).
|
|
169
|
+
body: Optional HTTP body bytes to bind to the signature.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dict with X-Capiscio-Badge header.
|
|
173
|
+
"""
|
|
174
|
+
if self._badge_token:
|
|
175
|
+
return {"X-Capiscio-Badge": self._badge_token}
|
|
176
|
+
|
|
177
|
+
token = self.sign_outbound(payload, body=body)
|
|
178
|
+
return {"X-Capiscio-Badge": token}
|
|
179
|
+
|
|
180
|
+
def set_badge_token(self, token: str) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Update the badge token used for outbound requests.
|
|
183
|
+
|
|
184
|
+
This is typically called by the badge keeper when a new token is obtained.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
token: The new badge token (compact JWS).
|
|
188
|
+
"""
|
|
189
|
+
self._badge_token = token
|
|
190
|
+
logger.debug(f"Updated badge token for agent {self.agent_id}")
|
|
191
|
+
|
|
192
|
+
def close(self) -> None:
|
|
193
|
+
"""Close the gRPC connection."""
|
|
194
|
+
if self._client:
|
|
195
|
+
self._client.close()
|
|
196
|
+
|
|
197
|
+
def __enter__(self) -> "SimpleGuard":
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
201
|
+
self.close()
|
|
202
|
+
|
|
203
|
+
def _resolve_project_root(self, base_dir: Optional[Union[str, Path]]) -> Path:
|
|
204
|
+
"""Walk up the directory tree to find agent-card.json or stop at root."""
|
|
205
|
+
current = Path(base_dir or os.getcwd()).resolve()
|
|
206
|
+
|
|
207
|
+
search_path = current
|
|
208
|
+
while search_path != search_path.parent:
|
|
209
|
+
if (search_path / "agent-card.json").exists():
|
|
210
|
+
return search_path
|
|
211
|
+
search_path = search_path.parent
|
|
212
|
+
|
|
213
|
+
return current
|
|
214
|
+
|
|
215
|
+
def _load_or_generate_card(self) -> None:
|
|
216
|
+
"""Load agent-card.json or generate a minimal one in dev_mode."""
|
|
217
|
+
# If explicit agent_id was provided, use it
|
|
218
|
+
if self._explicit_agent_id:
|
|
219
|
+
self.agent_id = self._explicit_agent_id
|
|
220
|
+
self.signing_kid = "key-0" # Will be updated when keys are generated/loaded
|
|
221
|
+
logger.info(f"Using explicit agent_id: {self.agent_id}")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
if self.agent_card_path.exists():
|
|
225
|
+
try:
|
|
226
|
+
with open(self.agent_card_path, "r") as f:
|
|
227
|
+
data = json.load(f)
|
|
228
|
+
self.agent_id = data.get("agent_id")
|
|
229
|
+
keys = data.get("public_keys", [])
|
|
230
|
+
if not keys:
|
|
231
|
+
raise ConfigurationError("agent-card.json missing 'public_keys'.")
|
|
232
|
+
self.signing_kid = keys[0].get("kid")
|
|
233
|
+
|
|
234
|
+
if not self.agent_id or not self.signing_kid:
|
|
235
|
+
raise ConfigurationError("agent-card.json missing 'agent_id' or 'public_keys[0].kid'.")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
raise ConfigurationError(f"Failed to load agent-card.json: {e}")
|
|
238
|
+
elif self.dev_mode:
|
|
239
|
+
logger.info("Dev Mode: Will generate did:key identity from keypair")
|
|
240
|
+
# Placeholder - will be updated with did:key after key generation
|
|
241
|
+
self.agent_id = "local-dev-agent" # Temporary, replaced in _load_or_generate_keys
|
|
242
|
+
self.signing_kid = "local-dev-key"
|
|
243
|
+
else:
|
|
244
|
+
raise ConfigurationError(f"agent-card.json not found at {self.project_root}")
|
|
245
|
+
|
|
246
|
+
def _load_or_generate_keys(self) -> None:
|
|
247
|
+
"""Load keys or generate them in dev_mode via gRPC.
|
|
248
|
+
|
|
249
|
+
In dev_mode, if no explicit agent_id was provided, updates self.agent_id
|
|
250
|
+
with the did:key derived from the generated keypair (RFC-002 §6.1).
|
|
251
|
+
"""
|
|
252
|
+
private_key_path = self.keys_dir / "private.pem"
|
|
253
|
+
public_key_path = self.keys_dir / "public.pem"
|
|
254
|
+
|
|
255
|
+
if not self.keys_dir.exists():
|
|
256
|
+
if self.dev_mode:
|
|
257
|
+
self.keys_dir.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
self.trusted_dir.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
else:
|
|
260
|
+
raise ConfigurationError(f"capiscio_keys directory not found at {self.keys_dir}")
|
|
261
|
+
|
|
262
|
+
if private_key_path.exists():
|
|
263
|
+
# Load existing key via gRPC
|
|
264
|
+
key_info, error = self._client.simpleguard.load_key(str(private_key_path))
|
|
265
|
+
if error:
|
|
266
|
+
raise ConfigurationError(f"Failed to load private.pem: {error}")
|
|
267
|
+
# Update signing kid to match the loaded key
|
|
268
|
+
self.signing_kid = key_info["key_id"]
|
|
269
|
+
logger.info(f"Loaded key: {self.signing_kid}")
|
|
270
|
+
elif self.dev_mode:
|
|
271
|
+
logger.info("Dev Mode: Generating Ed25519 keypair via gRPC")
|
|
272
|
+
|
|
273
|
+
# Generate via gRPC
|
|
274
|
+
key_info, error = self._client.simpleguard.generate_key_pair(
|
|
275
|
+
key_id=self.signing_kid,
|
|
276
|
+
)
|
|
277
|
+
if error:
|
|
278
|
+
raise ConfigurationError(f"Failed to generate keypair: {error}")
|
|
279
|
+
|
|
280
|
+
# Update agent_id with did:key if not explicitly set (RFC-002 §6.1)
|
|
281
|
+
did_key = key_info.get("did_key")
|
|
282
|
+
if did_key and not self._explicit_agent_id:
|
|
283
|
+
self.agent_id = did_key
|
|
284
|
+
logger.info(f"Dev Mode: Using did:key identity: {self.agent_id}")
|
|
285
|
+
|
|
286
|
+
# Save private key
|
|
287
|
+
with open(private_key_path, "w") as f:
|
|
288
|
+
f.write(key_info["private_key_pem"])
|
|
289
|
+
|
|
290
|
+
# Save public key
|
|
291
|
+
with open(public_key_path, "w") as f:
|
|
292
|
+
f.write(key_info["public_key_pem"])
|
|
293
|
+
|
|
294
|
+
# Update agent-card.json with JWK
|
|
295
|
+
self._update_agent_card_with_pem(key_info["public_key_pem"])
|
|
296
|
+
else:
|
|
297
|
+
raise ConfigurationError(f"private.pem not found at {private_key_path}")
|
|
298
|
+
|
|
299
|
+
def _update_agent_card_with_pem(self, public_key_pem: str) -> None:
|
|
300
|
+
"""Helper to write agent-card.json with the generated key."""
|
|
301
|
+
# For simplicity, just create a minimal card
|
|
302
|
+
# In production, would convert PEM to JWK
|
|
303
|
+
card_data = {
|
|
304
|
+
"agent_id": self.agent_id,
|
|
305
|
+
"public_keys": [{
|
|
306
|
+
"kty": "OKP",
|
|
307
|
+
"crv": "Ed25519",
|
|
308
|
+
"kid": self.signing_kid,
|
|
309
|
+
"use": "sig",
|
|
310
|
+
# Note: x would need to be extracted from PEM
|
|
311
|
+
}],
|
|
312
|
+
"protocolVersion": "0.3.0",
|
|
313
|
+
"name": "Local Dev Agent",
|
|
314
|
+
"description": "Auto-generated by SimpleGuard",
|
|
315
|
+
"url": "http://localhost:8000",
|
|
316
|
+
"version": "0.1.0",
|
|
317
|
+
"provider": {
|
|
318
|
+
"organization": "Local Dev"
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
with open(self.agent_card_path, "w") as f:
|
|
323
|
+
json.dump(card_data, f, indent=2)
|
|
324
|
+
logger.info(f"Created agent-card.json at {self.agent_card_path}")
|
|
325
|
+
|
|
326
|
+
def _setup_trust_store(self) -> None:
|
|
327
|
+
"""Ensure trust store exists and add self-trust in dev_mode."""
|
|
328
|
+
if not self.trusted_dir.exists() and self.dev_mode:
|
|
329
|
+
self.trusted_dir.mkdir(parents=True, exist_ok=True)
|
|
330
|
+
|
|
331
|
+
if self.dev_mode:
|
|
332
|
+
# Self-Trust: Load public key into gRPC server's trust store
|
|
333
|
+
# Use a different key_id for trust to avoid overwriting the signing key
|
|
334
|
+
public_key_path = self.keys_dir / "public.pem"
|
|
335
|
+
trust_key_id = f"{self.signing_kid}-trust"
|
|
336
|
+
self_trust_path = self.trusted_dir / f"{trust_key_id}.pem"
|
|
337
|
+
|
|
338
|
+
if public_key_path.exists() and not self_trust_path.exists():
|
|
339
|
+
import shutil
|
|
340
|
+
shutil.copy(public_key_path, self_trust_path)
|
|
341
|
+
logger.info(f"Dev Mode: Added self-trust for kid {trust_key_id}")
|
|
342
|
+
|
|
343
|
+
# Load into gRPC server trust store for verification
|
|
344
|
+
# (signing key is loaded separately during guard initialization)
|
|
345
|
+
if self_trust_path.exists():
|
|
346
|
+
self._client.simpleguard.load_key(str(self_trust_path))
|
capiscio_sdk/types.py
CHANGED
|
@@ -1,13 +1,70 @@
|
|
|
1
|
-
"""Validators for A2A message components.
|
|
1
|
+
"""Validators for A2A message components.
|
|
2
|
+
|
|
3
|
+
This module provides validation for A2A protocol messages and Agent Cards.
|
|
4
|
+
|
|
5
|
+
RECOMMENDED: Use `CoreValidator` for Agent Card validation - it delegates
|
|
6
|
+
to Go core for consistent behavior across all CapiscIO SDKs.
|
|
7
|
+
|
|
8
|
+
The pure Python validators (AgentCardValidator, MessageValidator, etc.) are
|
|
9
|
+
DEPRECATED and will be removed in a future version. They are maintained for
|
|
10
|
+
backward compatibility only.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
# Recommended: Use Go core-backed validator
|
|
14
|
+
from capiscio_sdk.validators import CoreValidator, validate_agent_card
|
|
15
|
+
|
|
16
|
+
result = validate_agent_card(card_dict)
|
|
17
|
+
# Or for repeated validations:
|
|
18
|
+
with CoreValidator() as validator:
|
|
19
|
+
result = validator.validate_agent_card(card_dict)
|
|
20
|
+
|
|
21
|
+
# Deprecated: Pure Python validator
|
|
22
|
+
from capiscio_sdk.validators import AgentCardValidator
|
|
23
|
+
validator = AgentCardValidator() # Will show deprecation warning
|
|
24
|
+
"""
|
|
25
|
+
import warnings as _warnings
|
|
26
|
+
|
|
27
|
+
# Go core-backed validators (RECOMMENDED)
|
|
28
|
+
from ._core import CoreValidator, validate_agent_card
|
|
29
|
+
|
|
30
|
+
# Legacy pure Python validators (DEPRECATED)
|
|
31
|
+
# These are imported with deprecation tracking
|
|
2
32
|
from .message import MessageValidator
|
|
3
33
|
from .protocol import ProtocolValidator
|
|
4
34
|
from .url_security import URLSecurityValidator
|
|
5
35
|
from .signature import SignatureValidator
|
|
6
36
|
from .semver import SemverValidator
|
|
7
|
-
from .agent_card import AgentCardValidator
|
|
37
|
+
from .agent_card import AgentCardValidator as _LegacyAgentCardValidator
|
|
8
38
|
from .certificate import CertificateValidator
|
|
9
39
|
|
|
40
|
+
|
|
41
|
+
class AgentCardValidator(_LegacyAgentCardValidator):
|
|
42
|
+
"""Agent Card validator (DEPRECATED - use CoreValidator instead).
|
|
43
|
+
|
|
44
|
+
This class is deprecated. Use `CoreValidator` for Go core-backed
|
|
45
|
+
validation with consistent behavior across all CapiscIO SDKs.
|
|
46
|
+
|
|
47
|
+
.. deprecated:: 0.3.0
|
|
48
|
+
Use :class:`CoreValidator` instead. This pure Python implementation
|
|
49
|
+
will be removed in version 1.0.0.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, *args, **kwargs):
|
|
53
|
+
_warnings.warn(
|
|
54
|
+
"AgentCardValidator is deprecated and will be removed in v1.0.0. "
|
|
55
|
+
"Use CoreValidator for Go core-backed validation: "
|
|
56
|
+
"from capiscio_sdk.validators import CoreValidator, validate_agent_card",
|
|
57
|
+
DeprecationWarning,
|
|
58
|
+
stacklevel=2
|
|
59
|
+
)
|
|
60
|
+
super().__init__(*args, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
10
63
|
__all__ = [
|
|
64
|
+
# Recommended (Go core-backed)
|
|
65
|
+
"CoreValidator",
|
|
66
|
+
"validate_agent_card",
|
|
67
|
+
# Legacy (deprecated)
|
|
11
68
|
"MessageValidator",
|
|
12
69
|
"ProtocolValidator",
|
|
13
70
|
"URLSecurityValidator",
|