capiscio-sdk 0.3.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 +67 -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/dv.py +296 -0
- capiscio_sdk/executor.py +5 -5
- capiscio_sdk/integrations/fastapi.py +3 -2
- capiscio_sdk/scoring/__init__.py +73 -3
- capiscio_sdk/simple_guard.py +196 -204
- 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-0.3.0.dist-info → capiscio_sdk-2.3.0.dist-info}/RECORD +18 -10
- {capiscio_sdk-0.3.0.dist-info → capiscio_sdk-2.3.0.dist-info}/WHEEL +1 -1
- capiscio_sdk-0.3.0.dist-info/METADATA +0 -126
- {capiscio_sdk-0.3.0.dist-info → capiscio_sdk-2.3.0.dist-info}/licenses/LICENSE +0 -0
capiscio_sdk/badge.py
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
"""Trust Badge API for CapiscIO agents.
|
|
2
|
+
|
|
3
|
+
This module provides a user-friendly API for working with Trust Badges,
|
|
4
|
+
which are signed JWS tokens that provide portable, verifiable identity
|
|
5
|
+
for agents. See RFC-002 for the full specification.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
|
|
9
|
+
from capiscio_sdk.badge import verify_badge, parse_badge, VerifyOptions
|
|
10
|
+
|
|
11
|
+
# Verify a badge with full validation
|
|
12
|
+
result = verify_badge(
|
|
13
|
+
token,
|
|
14
|
+
trusted_issuers=["https://registry.capisc.io"],
|
|
15
|
+
audience="https://my-service.example.com",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if result.valid:
|
|
19
|
+
print(f"Badge valid for agent: {result.claims.agent_id}")
|
|
20
|
+
print(f"Trust level: {result.claims.trust_level}")
|
|
21
|
+
else:
|
|
22
|
+
print(f"Verification failed: {result.error}")
|
|
23
|
+
|
|
24
|
+
# Parse without verification (for inspection)
|
|
25
|
+
claims = parse_badge(token)
|
|
26
|
+
print(f"Issuer: {claims.issuer}")
|
|
27
|
+
print(f"Expires: {claims.expires_at}")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
from enum import Enum
|
|
33
|
+
from typing import List, Optional, Union
|
|
34
|
+
|
|
35
|
+
from capiscio_sdk._rpc.client import CapiscioRPCClient
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _utc_now() -> datetime:
|
|
39
|
+
"""Get current UTC time as naive datetime (for comparison with JWT timestamps)."""
|
|
40
|
+
# JWT timestamps are typically naive UTC, so we compare with naive UTC
|
|
41
|
+
from datetime import timezone
|
|
42
|
+
|
|
43
|
+
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _from_utc_timestamp(ts: float) -> datetime:
|
|
47
|
+
"""Convert UTC timestamp to naive datetime."""
|
|
48
|
+
from datetime import timezone
|
|
49
|
+
|
|
50
|
+
return datetime.fromtimestamp(ts, timezone.utc).replace(tzinfo=None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class VerifyMode(Enum):
|
|
54
|
+
"""Badge verification mode."""
|
|
55
|
+
|
|
56
|
+
ONLINE = "online"
|
|
57
|
+
"""Perform real-time checks against the registry (revocation, agent status)."""
|
|
58
|
+
|
|
59
|
+
OFFLINE = "offline"
|
|
60
|
+
"""Use only local trust store and revocation cache."""
|
|
61
|
+
|
|
62
|
+
HYBRID = "hybrid"
|
|
63
|
+
"""Try online checks, fall back to cache if unavailable."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TrustLevel(Enum):
|
|
67
|
+
"""Trust level as defined in RFC-002."""
|
|
68
|
+
|
|
69
|
+
LEVEL_1 = "1"
|
|
70
|
+
"""Domain Validated (DV) - Basic verification."""
|
|
71
|
+
|
|
72
|
+
LEVEL_2 = "2"
|
|
73
|
+
"""Organization Validated (OV) - Business verification."""
|
|
74
|
+
|
|
75
|
+
LEVEL_3 = "3"
|
|
76
|
+
"""Extended Validation (EV) - Rigorous vetting."""
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_string(cls, value: str) -> "TrustLevel":
|
|
80
|
+
"""Create TrustLevel from string value."""
|
|
81
|
+
for level in cls:
|
|
82
|
+
if level.value == value:
|
|
83
|
+
return level
|
|
84
|
+
raise ValueError(f"Unknown trust level: {value}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class BadgeClaims:
|
|
89
|
+
"""Parsed badge claims from a Trust Badge token.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
jti: Unique badge identifier (UUID).
|
|
93
|
+
issuer: Badge issuer URL (CA).
|
|
94
|
+
subject: Agent DID (did:web format).
|
|
95
|
+
audience: Optional list of intended audience URLs.
|
|
96
|
+
issued_at: When the badge was issued.
|
|
97
|
+
expires_at: When the badge expires.
|
|
98
|
+
trust_level: Trust level (1=DV, 2=OV, 3=EV).
|
|
99
|
+
domain: Agent's verified domain.
|
|
100
|
+
agent_name: Human-readable agent name.
|
|
101
|
+
agent_id: Extracted agent ID from subject DID.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
jti: str
|
|
105
|
+
issuer: str
|
|
106
|
+
subject: str
|
|
107
|
+
issued_at: datetime
|
|
108
|
+
expires_at: datetime
|
|
109
|
+
trust_level: TrustLevel
|
|
110
|
+
domain: str
|
|
111
|
+
agent_name: str = ""
|
|
112
|
+
audience: List[str] = field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def agent_id(self) -> str:
|
|
116
|
+
"""Extract agent ID from subject DID."""
|
|
117
|
+
# did:web:registry.capisc.io:agents:my-agent -> my-agent
|
|
118
|
+
parts = self.subject.split(":")
|
|
119
|
+
if len(parts) >= 5 and parts[3] == "agents":
|
|
120
|
+
return parts[4]
|
|
121
|
+
return ""
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def is_expired(self) -> bool:
|
|
125
|
+
"""Check if the badge has expired."""
|
|
126
|
+
return _utc_now() > self.expires_at
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def is_not_yet_valid(self) -> bool:
|
|
130
|
+
"""Check if the badge is not yet valid."""
|
|
131
|
+
return _utc_now() < self.issued_at
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_dict(cls, data: dict) -> "BadgeClaims":
|
|
135
|
+
"""Create BadgeClaims from a dictionary."""
|
|
136
|
+
return cls(
|
|
137
|
+
jti=data.get("jti", ""),
|
|
138
|
+
issuer=data.get("iss", ""),
|
|
139
|
+
subject=data.get("sub", ""),
|
|
140
|
+
issued_at=_from_utc_timestamp(data.get("iat", 0)),
|
|
141
|
+
expires_at=_from_utc_timestamp(data.get("exp", 0)),
|
|
142
|
+
trust_level=TrustLevel.from_string(data.get("trust_level", "1")),
|
|
143
|
+
domain=data.get("domain", ""),
|
|
144
|
+
agent_name=data.get("agent_name", ""),
|
|
145
|
+
audience=data.get("aud", []),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def to_dict(self) -> dict:
|
|
149
|
+
"""Convert to dictionary."""
|
|
150
|
+
return {
|
|
151
|
+
"jti": self.jti,
|
|
152
|
+
"iss": self.issuer,
|
|
153
|
+
"sub": self.subject,
|
|
154
|
+
"iat": int(self.issued_at.timestamp()),
|
|
155
|
+
"exp": int(self.expires_at.timestamp()),
|
|
156
|
+
"trust_level": self.trust_level.value,
|
|
157
|
+
"domain": self.domain,
|
|
158
|
+
"agent_name": self.agent_name,
|
|
159
|
+
"aud": self.audience,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class VerifyOptions:
|
|
165
|
+
"""Options for badge verification.
|
|
166
|
+
|
|
167
|
+
Attributes:
|
|
168
|
+
mode: Verification mode (online, offline, hybrid).
|
|
169
|
+
trusted_issuers: List of trusted issuer URLs.
|
|
170
|
+
audience: Expected audience (your service URL).
|
|
171
|
+
skip_revocation_check: Skip revocation check (testing only).
|
|
172
|
+
skip_agent_status_check: Skip agent status check (testing only).
|
|
173
|
+
public_key_jwk: Override public key (for offline verification).
|
|
174
|
+
fail_open: Allow stale cache for levels 2-4 (WARNING: violates RFC-002 default).
|
|
175
|
+
stale_threshold_seconds: Cache staleness threshold in seconds (default: 300 per RFC-002 §7.5).
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
mode: VerifyMode = VerifyMode.ONLINE
|
|
179
|
+
trusted_issuers: List[str] = field(default_factory=list)
|
|
180
|
+
audience: Optional[str] = None
|
|
181
|
+
skip_revocation_check: bool = False
|
|
182
|
+
skip_agent_status_check: bool = False
|
|
183
|
+
public_key_jwk: Optional[str] = None
|
|
184
|
+
fail_open: bool = False # RFC-002 v1.3 §7.5: Default is fail-closed
|
|
185
|
+
stale_threshold_seconds: int = 300 # RFC-002 v1.3 §7.5: REVOCATION_CACHE_MAX_STALENESS
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# RFC-002 v1.3 §7.5 named constant
|
|
189
|
+
REVOCATION_CACHE_MAX_STALENESS = 300 # 5 minutes
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class VerifyResult:
|
|
194
|
+
"""Result of badge verification.
|
|
195
|
+
|
|
196
|
+
Attributes:
|
|
197
|
+
valid: Whether the badge is valid.
|
|
198
|
+
claims: Parsed badge claims (if valid or parseable).
|
|
199
|
+
error: Error message if verification failed.
|
|
200
|
+
error_code: RFC-002 error code if applicable.
|
|
201
|
+
warnings: Non-fatal issues encountered.
|
|
202
|
+
mode: Verification mode that was used.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
valid: bool
|
|
206
|
+
claims: Optional[BadgeClaims] = None
|
|
207
|
+
error: Optional[str] = None
|
|
208
|
+
error_code: Optional[str] = None
|
|
209
|
+
warnings: List[str] = field(default_factory=list)
|
|
210
|
+
mode: VerifyMode = VerifyMode.ONLINE
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Module-level client (lazy initialization)
|
|
214
|
+
_client: Optional[CapiscioRPCClient] = None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _get_client() -> CapiscioRPCClient:
|
|
218
|
+
"""Get or create the module-level gRPC client."""
|
|
219
|
+
global _client
|
|
220
|
+
if _client is None:
|
|
221
|
+
_client = CapiscioRPCClient()
|
|
222
|
+
_client.connect()
|
|
223
|
+
return _client
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def verify_badge(
|
|
227
|
+
token: str,
|
|
228
|
+
*,
|
|
229
|
+
trusted_issuers: Optional[List[str]] = None,
|
|
230
|
+
audience: Optional[str] = None,
|
|
231
|
+
mode: Union[VerifyMode, str] = VerifyMode.ONLINE,
|
|
232
|
+
skip_revocation_check: bool = False,
|
|
233
|
+
skip_agent_status_check: bool = False,
|
|
234
|
+
public_key_jwk: Optional[str] = None,
|
|
235
|
+
fail_open: bool = False,
|
|
236
|
+
stale_threshold_seconds: int = REVOCATION_CACHE_MAX_STALENESS,
|
|
237
|
+
options: Optional[VerifyOptions] = None,
|
|
238
|
+
) -> VerifyResult:
|
|
239
|
+
"""Verify a Trust Badge token.
|
|
240
|
+
|
|
241
|
+
Performs full RFC-002 verification including:
|
|
242
|
+
- JWS signature verification
|
|
243
|
+
- Claims validation (exp, iat, iss, sub, aud)
|
|
244
|
+
- Revocation check (online/hybrid modes)
|
|
245
|
+
- Agent status check (online/hybrid modes)
|
|
246
|
+
- RFC-002 v1.3 §7.5: Staleness fail-closed for levels 2-4
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
token: The badge JWT/JWS token to verify.
|
|
250
|
+
trusted_issuers: List of trusted issuer URLs. If empty, all issuers accepted.
|
|
251
|
+
audience: Your service URL for audience validation.
|
|
252
|
+
mode: Verification mode (online, offline, hybrid).
|
|
253
|
+
skip_revocation_check: Skip revocation check (testing only).
|
|
254
|
+
skip_agent_status_check: Skip agent status check (testing only).
|
|
255
|
+
public_key_jwk: Override public key JWK for offline verification.
|
|
256
|
+
fail_open: Allow stale cache for levels 2-4 (WARNING: violates RFC-002 default).
|
|
257
|
+
stale_threshold_seconds: Cache staleness threshold (default: 300 per RFC-002 §7.5).
|
|
258
|
+
options: VerifyOptions object (alternative to individual args).
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
VerifyResult with validation status and claims.
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
result = verify_badge(
|
|
265
|
+
token,
|
|
266
|
+
trusted_issuers=["https://registry.capisc.io"],
|
|
267
|
+
audience="https://my-service.example.com",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if result.valid:
|
|
271
|
+
print(f"Agent {result.claims.agent_id} verified at level {result.claims.trust_level}")
|
|
272
|
+
"""
|
|
273
|
+
# Build options from individual args or use provided options
|
|
274
|
+
if options is None:
|
|
275
|
+
if isinstance(mode, str):
|
|
276
|
+
mode = VerifyMode(mode)
|
|
277
|
+
options = VerifyOptions(
|
|
278
|
+
mode=mode,
|
|
279
|
+
trusted_issuers=trusted_issuers or [],
|
|
280
|
+
audience=audience,
|
|
281
|
+
skip_revocation_check=skip_revocation_check,
|
|
282
|
+
skip_agent_status_check=skip_agent_status_check,
|
|
283
|
+
public_key_jwk=public_key_jwk,
|
|
284
|
+
fail_open=fail_open,
|
|
285
|
+
stale_threshold_seconds=stale_threshold_seconds,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
client = _get_client()
|
|
290
|
+
|
|
291
|
+
# Map SDK mode to gRPC mode string
|
|
292
|
+
mode_map = {
|
|
293
|
+
VerifyMode.ONLINE: "online",
|
|
294
|
+
VerifyMode.OFFLINE: "offline",
|
|
295
|
+
VerifyMode.HYBRID: "hybrid",
|
|
296
|
+
}
|
|
297
|
+
grpc_mode = mode_map.get(options.mode, "online")
|
|
298
|
+
|
|
299
|
+
# If public_key_jwk is provided, use the simpler verify_badge RPC
|
|
300
|
+
# which supports offline verification with a specific key
|
|
301
|
+
if options.public_key_jwk:
|
|
302
|
+
valid, claims_dict, error = client.badge.verify_badge(
|
|
303
|
+
token=token,
|
|
304
|
+
public_key_jwk=options.public_key_jwk,
|
|
305
|
+
)
|
|
306
|
+
warnings = []
|
|
307
|
+
else:
|
|
308
|
+
# Use verify_badge_with_options for online/hybrid verification
|
|
309
|
+
valid, claims_dict, warnings, error = client.badge.verify_badge_with_options(
|
|
310
|
+
token=token,
|
|
311
|
+
accept_self_signed=False, # SDK handles this separately
|
|
312
|
+
trusted_issuers=options.trusted_issuers,
|
|
313
|
+
audience=options.audience or "",
|
|
314
|
+
skip_revocation=options.skip_revocation_check,
|
|
315
|
+
skip_agent_status=options.skip_agent_status_check,
|
|
316
|
+
mode=grpc_mode,
|
|
317
|
+
fail_open=options.fail_open,
|
|
318
|
+
stale_threshold_seconds=options.stale_threshold_seconds,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Convert claims if available
|
|
322
|
+
claims = None
|
|
323
|
+
if claims_dict:
|
|
324
|
+
claims = BadgeClaims.from_dict(claims_dict)
|
|
325
|
+
|
|
326
|
+
# Build result
|
|
327
|
+
if valid:
|
|
328
|
+
# Additional client-side validation (server may not enforce all)
|
|
329
|
+
# Check trusted issuers
|
|
330
|
+
if options.trusted_issuers and claims:
|
|
331
|
+
if claims.issuer not in options.trusted_issuers:
|
|
332
|
+
return VerifyResult(
|
|
333
|
+
valid=False,
|
|
334
|
+
claims=claims,
|
|
335
|
+
error=f"Issuer {claims.issuer} not in trusted list",
|
|
336
|
+
error_code="BADGE_ISSUER_UNTRUSTED",
|
|
337
|
+
mode=options.mode,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Check audience
|
|
341
|
+
if options.audience and claims and claims.audience:
|
|
342
|
+
if options.audience not in claims.audience:
|
|
343
|
+
return VerifyResult(
|
|
344
|
+
valid=False,
|
|
345
|
+
claims=claims,
|
|
346
|
+
error=f"Audience {options.audience} not in badge audience",
|
|
347
|
+
error_code="BADGE_AUDIENCE_MISMATCH",
|
|
348
|
+
mode=options.mode,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return VerifyResult(
|
|
352
|
+
valid=True,
|
|
353
|
+
claims=claims,
|
|
354
|
+
warnings=warnings or [],
|
|
355
|
+
mode=options.mode,
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
# Verification failed
|
|
359
|
+
error_code = None
|
|
360
|
+
if error:
|
|
361
|
+
# Extract error code from message if present
|
|
362
|
+
# RFC-002 v1.3: Added REVOCATION_CHECK_FAILED for staleness failures
|
|
363
|
+
for code in [
|
|
364
|
+
"REVOCATION_CHECK_FAILED", # RFC-002 v1.3 §7.5 staleness error
|
|
365
|
+
"BADGE_MALFORMED",
|
|
366
|
+
"BADGE_SIGNATURE_INVALID",
|
|
367
|
+
"BADGE_EXPIRED",
|
|
368
|
+
"BADGE_NOT_YET_VALID",
|
|
369
|
+
"BADGE_ISSUER_UNTRUSTED",
|
|
370
|
+
"BADGE_AUDIENCE_MISMATCH",
|
|
371
|
+
"BADGE_REVOKED",
|
|
372
|
+
"BADGE_CLAIMS_INVALID",
|
|
373
|
+
"BADGE_AGENT_DISABLED",
|
|
374
|
+
]:
|
|
375
|
+
if code in error:
|
|
376
|
+
error_code = code
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
return VerifyResult(
|
|
380
|
+
valid=False,
|
|
381
|
+
claims=claims,
|
|
382
|
+
error=error,
|
|
383
|
+
error_code=error_code,
|
|
384
|
+
mode=options.mode,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
except Exception as e:
|
|
388
|
+
return VerifyResult(
|
|
389
|
+
valid=False,
|
|
390
|
+
error=str(e),
|
|
391
|
+
mode=options.mode if options else VerifyMode.ONLINE,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def parse_badge(token: str) -> BadgeClaims:
|
|
396
|
+
"""Parse badge claims without verification.
|
|
397
|
+
|
|
398
|
+
Use this to inspect badge contents before full verification,
|
|
399
|
+
or to extract claims for display purposes.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
token: The badge JWT/JWS token to parse.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
BadgeClaims object with parsed claims.
|
|
406
|
+
|
|
407
|
+
Raises:
|
|
408
|
+
ValueError: If the token cannot be parsed.
|
|
409
|
+
|
|
410
|
+
Example:
|
|
411
|
+
claims = parse_badge(token)
|
|
412
|
+
print(f"Badge for: {claims.agent_id}")
|
|
413
|
+
print(f"Issued by: {claims.issuer}")
|
|
414
|
+
print(f"Expires: {claims.expires_at}")
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
417
|
+
client = _get_client()
|
|
418
|
+
claims_dict, error = client.badge.parse_badge(token)
|
|
419
|
+
|
|
420
|
+
if error:
|
|
421
|
+
raise ValueError(f"Failed to parse badge: {error}")
|
|
422
|
+
|
|
423
|
+
if claims_dict is None:
|
|
424
|
+
raise ValueError("No claims returned from parser")
|
|
425
|
+
|
|
426
|
+
return BadgeClaims.from_dict(claims_dict)
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
if isinstance(e, ValueError):
|
|
430
|
+
raise
|
|
431
|
+
raise ValueError(f"Failed to parse badge: {e}") from e
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
async def request_badge(
|
|
435
|
+
agent_id: str,
|
|
436
|
+
*,
|
|
437
|
+
ca_url: str = "https://registry.capisc.io",
|
|
438
|
+
api_key: Optional[str] = None,
|
|
439
|
+
domain: Optional[str] = None,
|
|
440
|
+
trust_level: Union[TrustLevel, str] = TrustLevel.LEVEL_1,
|
|
441
|
+
audience: Optional[List[str]] = None,
|
|
442
|
+
timeout: float = 30.0,
|
|
443
|
+
) -> str:
|
|
444
|
+
"""Request a new Trust Badge from a Certificate Authority.
|
|
445
|
+
|
|
446
|
+
This sends a request to the CA to issue a new badge for the agent.
|
|
447
|
+
The CA will verify the agent's identity based on the trust level
|
|
448
|
+
and return a signed badge token.
|
|
449
|
+
|
|
450
|
+
Uses the capiscio-core gRPC service for the actual request.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
agent_id: The agent identifier to request a badge for.
|
|
454
|
+
ca_url: Certificate Authority URL (default: CapiscIO registry).
|
|
455
|
+
api_key: API key for authentication with the CA.
|
|
456
|
+
domain: Agent's domain (required for verification).
|
|
457
|
+
trust_level: Requested trust level (1=DV, 2=OV, 3=EV).
|
|
458
|
+
audience: Optional audience restrictions for the badge.
|
|
459
|
+
timeout: Request timeout in seconds (not used with gRPC).
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
The signed badge JWT token.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
ValueError: If the CA returns an error.
|
|
466
|
+
|
|
467
|
+
Example:
|
|
468
|
+
token = await request_badge(
|
|
469
|
+
agent_id="my-agent",
|
|
470
|
+
ca_url="https://registry.capisc.io",
|
|
471
|
+
api_key=os.environ["CAPISCIO_API_KEY"],
|
|
472
|
+
domain="example.com",
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Save the token for later use
|
|
476
|
+
with open("badge.jwt", "w") as f:
|
|
477
|
+
f.write(token)
|
|
478
|
+
"""
|
|
479
|
+
# Use sync version since gRPC client is sync
|
|
480
|
+
return request_badge_sync(
|
|
481
|
+
agent_id,
|
|
482
|
+
ca_url=ca_url,
|
|
483
|
+
api_key=api_key,
|
|
484
|
+
domain=domain,
|
|
485
|
+
trust_level=trust_level,
|
|
486
|
+
audience=audience,
|
|
487
|
+
timeout=timeout,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def request_badge_sync(
|
|
492
|
+
agent_id: str,
|
|
493
|
+
*,
|
|
494
|
+
ca_url: str = "https://registry.capisc.io",
|
|
495
|
+
api_key: Optional[str] = None,
|
|
496
|
+
domain: Optional[str] = None,
|
|
497
|
+
trust_level: Union[TrustLevel, str] = TrustLevel.LEVEL_1,
|
|
498
|
+
audience: Optional[List[str]] = None,
|
|
499
|
+
timeout: float = 30.0,
|
|
500
|
+
) -> str:
|
|
501
|
+
"""Synchronous version of request_badge.
|
|
502
|
+
|
|
503
|
+
Uses the capiscio-core gRPC service for the actual request.
|
|
504
|
+
See request_badge for full documentation.
|
|
505
|
+
"""
|
|
506
|
+
if isinstance(trust_level, str):
|
|
507
|
+
trust_level = TrustLevel.from_string(trust_level)
|
|
508
|
+
|
|
509
|
+
# Convert TrustLevel enum to int for gRPC
|
|
510
|
+
trust_level_int = int(trust_level.value)
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
client = _get_client()
|
|
514
|
+
success, result, error = client.badge.request_badge(
|
|
515
|
+
agent_id=agent_id,
|
|
516
|
+
api_key=api_key or "",
|
|
517
|
+
ca_url=ca_url,
|
|
518
|
+
domain=domain or "",
|
|
519
|
+
ttl_seconds=300, # Default per RFC-002
|
|
520
|
+
trust_level=trust_level_int,
|
|
521
|
+
audience=audience,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
if not success:
|
|
525
|
+
raise ValueError(f"CA rejected badge request: {error}")
|
|
526
|
+
|
|
527
|
+
token = result.get("token")
|
|
528
|
+
if not token:
|
|
529
|
+
raise ValueError("CA response missing token")
|
|
530
|
+
|
|
531
|
+
return token
|
|
532
|
+
|
|
533
|
+
except Exception as e:
|
|
534
|
+
if isinstance(e, ValueError):
|
|
535
|
+
raise
|
|
536
|
+
raise ValueError(f"Failed to request badge: {e}") from e
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
async def request_pop_badge(
|
|
540
|
+
agent_did: str,
|
|
541
|
+
private_key_jwk: str,
|
|
542
|
+
*,
|
|
543
|
+
ca_url: str = "https://registry.capisc.io",
|
|
544
|
+
api_key: Optional[str] = None,
|
|
545
|
+
ttl_seconds: int = 300,
|
|
546
|
+
audience: Optional[List[str]] = None,
|
|
547
|
+
timeout: float = 30.0,
|
|
548
|
+
) -> str:
|
|
549
|
+
"""Request a Trust Badge using Proof of Possession (RFC-003).
|
|
550
|
+
|
|
551
|
+
This requests a badge using the PoP challenge-response protocol,
|
|
552
|
+
providing IAL-1 assurance with cryptographic key binding. The agent
|
|
553
|
+
must prove possession of the private key associated with their DID.
|
|
554
|
+
|
|
555
|
+
Uses the capiscio-core gRPC service for the actual PoP flow.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
agent_did: The agent DID (did:web:... or did:key:...).
|
|
559
|
+
private_key_jwk: Private key in JWK format (JSON string).
|
|
560
|
+
ca_url: Certificate Authority URL (default: CapiscIO registry).
|
|
561
|
+
api_key: API key for authentication with the CA.
|
|
562
|
+
ttl_seconds: Requested badge TTL in seconds (default: 300).
|
|
563
|
+
audience: Optional audience restrictions for the badge.
|
|
564
|
+
timeout: Request timeout in seconds (not used with gRPC).
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
The signed badge JWT token with IAL-1 assurance.
|
|
568
|
+
|
|
569
|
+
Raises:
|
|
570
|
+
ValueError: If the CA returns an error or PoP verification fails.
|
|
571
|
+
|
|
572
|
+
Example:
|
|
573
|
+
import json
|
|
574
|
+
|
|
575
|
+
# Load private key
|
|
576
|
+
with open("private.jwk") as f:
|
|
577
|
+
private_key_jwk = json.dumps(json.load(f))
|
|
578
|
+
|
|
579
|
+
token = await request_pop_badge(
|
|
580
|
+
agent_did="did:web:registry.capisc.io:agents:my-agent",
|
|
581
|
+
private_key_jwk=private_key_jwk,
|
|
582
|
+
ca_url="https://registry.capisc.io",
|
|
583
|
+
api_key=os.environ["CAPISCIO_API_KEY"],
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Verify the badge has IAL-1
|
|
587
|
+
claims = parse_badge(token)
|
|
588
|
+
# Note: IAL-1 badges have a 'cnf' claim with key binding
|
|
589
|
+
"""
|
|
590
|
+
# Use sync version since gRPC client is sync
|
|
591
|
+
return request_pop_badge_sync(
|
|
592
|
+
agent_did,
|
|
593
|
+
private_key_jwk,
|
|
594
|
+
ca_url=ca_url,
|
|
595
|
+
api_key=api_key,
|
|
596
|
+
ttl_seconds=ttl_seconds,
|
|
597
|
+
audience=audience,
|
|
598
|
+
timeout=timeout,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def request_pop_badge_sync(
|
|
603
|
+
agent_did: str,
|
|
604
|
+
private_key_jwk: str,
|
|
605
|
+
*,
|
|
606
|
+
ca_url: str = "https://registry.capisc.io",
|
|
607
|
+
api_key: Optional[str] = None,
|
|
608
|
+
ttl_seconds: int = 300,
|
|
609
|
+
audience: Optional[List[str]] = None,
|
|
610
|
+
timeout: float = 30.0,
|
|
611
|
+
) -> str:
|
|
612
|
+
"""Synchronous version of request_pop_badge.
|
|
613
|
+
|
|
614
|
+
Uses the capiscio-core gRPC service for the actual PoP flow.
|
|
615
|
+
See request_pop_badge for full documentation.
|
|
616
|
+
"""
|
|
617
|
+
try:
|
|
618
|
+
client = _get_client()
|
|
619
|
+
success, result, error = client.badge.request_pop_badge(
|
|
620
|
+
agent_did=agent_did,
|
|
621
|
+
private_key_jwk=private_key_jwk,
|
|
622
|
+
api_key=api_key or "",
|
|
623
|
+
ca_url=ca_url,
|
|
624
|
+
ttl_seconds=ttl_seconds,
|
|
625
|
+
audience=audience,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
if not success:
|
|
629
|
+
raise ValueError(f"PoP badge request failed: {error}")
|
|
630
|
+
|
|
631
|
+
token = result.get("token")
|
|
632
|
+
if not token:
|
|
633
|
+
raise ValueError("CA response missing token")
|
|
634
|
+
|
|
635
|
+
return token
|
|
636
|
+
except Exception as e:
|
|
637
|
+
if isinstance(e, ValueError):
|
|
638
|
+
raise
|
|
639
|
+
raise ValueError(f"Failed to request PoP badge: {e}") from e
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def start_badge_keeper(
|
|
643
|
+
mode: str,
|
|
644
|
+
*,
|
|
645
|
+
agent_id: str = "",
|
|
646
|
+
api_key: str = "",
|
|
647
|
+
ca_url: str = "https://registry.capisc.io",
|
|
648
|
+
private_key_path: str = "",
|
|
649
|
+
output_file: str = "badge.jwt",
|
|
650
|
+
domain: str = "",
|
|
651
|
+
ttl_seconds: int = 300,
|
|
652
|
+
renew_before_seconds: int = 60,
|
|
653
|
+
check_interval_seconds: int = 30,
|
|
654
|
+
trust_level: Union[TrustLevel, str, int] = TrustLevel.LEVEL_1,
|
|
655
|
+
):
|
|
656
|
+
"""Start a badge keeper daemon (RFC-002 §7.3).
|
|
657
|
+
|
|
658
|
+
The keeper automatically renews badges before they expire, ensuring
|
|
659
|
+
continuous operation. Returns a generator of keeper events.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
mode: 'ca' for CA mode, 'self-sign' for development
|
|
663
|
+
agent_id: Agent UUID (required for CA mode)
|
|
664
|
+
api_key: API key (required for CA mode)
|
|
665
|
+
ca_url: CA URL (default: https://registry.capisc.io)
|
|
666
|
+
private_key_path: Path to private key JWK (required for self-sign)
|
|
667
|
+
output_file: Path to write badge file
|
|
668
|
+
domain: Agent domain
|
|
669
|
+
ttl_seconds: Badge TTL (default: 300)
|
|
670
|
+
renew_before_seconds: Renew this many seconds before expiry (default: 60)
|
|
671
|
+
check_interval_seconds: Check interval (default: 30)
|
|
672
|
+
trust_level: Trust level for CA mode (1-4, default: 1)
|
|
673
|
+
|
|
674
|
+
Yields:
|
|
675
|
+
KeeperEvent dicts with: type, badge_jti, subject, trust_level,
|
|
676
|
+
expires_at, error, error_code, timestamp, token
|
|
677
|
+
|
|
678
|
+
Example:
|
|
679
|
+
# CA mode - production
|
|
680
|
+
for event in start_badge_keeper(
|
|
681
|
+
mode="ca",
|
|
682
|
+
agent_id="my-agent-uuid",
|
|
683
|
+
api_key=os.environ["CAPISCIO_API_KEY"],
|
|
684
|
+
):
|
|
685
|
+
if event["type"] == "renewed":
|
|
686
|
+
print(f"Badge renewed: {event['badge_jti']}")
|
|
687
|
+
elif event["type"] == "error":
|
|
688
|
+
print(f"Error: {event['error']}")
|
|
689
|
+
|
|
690
|
+
# Self-sign mode - development
|
|
691
|
+
for event in start_badge_keeper(
|
|
692
|
+
mode="self-sign",
|
|
693
|
+
private_key_path="private.jwk",
|
|
694
|
+
):
|
|
695
|
+
print(f"Event: {event['type']}")
|
|
696
|
+
"""
|
|
697
|
+
# Convert trust level to int if needed
|
|
698
|
+
if isinstance(trust_level, TrustLevel):
|
|
699
|
+
trust_level_int = int(trust_level.value)
|
|
700
|
+
elif isinstance(trust_level, str):
|
|
701
|
+
trust_level_int = int(TrustLevel.from_string(trust_level).value)
|
|
702
|
+
else:
|
|
703
|
+
trust_level_int = trust_level
|
|
704
|
+
|
|
705
|
+
client = _get_client()
|
|
706
|
+
yield from client.badge.start_keeper(
|
|
707
|
+
mode=mode,
|
|
708
|
+
agent_id=agent_id,
|
|
709
|
+
api_key=api_key,
|
|
710
|
+
ca_url=ca_url,
|
|
711
|
+
private_key_path=private_key_path,
|
|
712
|
+
output_file=output_file,
|
|
713
|
+
domain=domain,
|
|
714
|
+
ttl_seconds=ttl_seconds,
|
|
715
|
+
renew_before_seconds=renew_before_seconds,
|
|
716
|
+
check_interval_seconds=check_interval_seconds,
|
|
717
|
+
trust_level=trust_level_int,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# Export public API
|
|
722
|
+
__all__ = [
|
|
723
|
+
# Constants (RFC-002 v1.3 §7.5)
|
|
724
|
+
"REVOCATION_CACHE_MAX_STALENESS",
|
|
725
|
+
# Types
|
|
726
|
+
"BadgeClaims",
|
|
727
|
+
"VerifyOptions",
|
|
728
|
+
"VerifyResult",
|
|
729
|
+
"VerifyMode",
|
|
730
|
+
"TrustLevel",
|
|
731
|
+
# Functions
|
|
732
|
+
"verify_badge",
|
|
733
|
+
"parse_badge",
|
|
734
|
+
"request_badge",
|
|
735
|
+
"request_badge_sync",
|
|
736
|
+
"start_badge_keeper",
|
|
737
|
+
]
|