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/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
+ ]