capiscio-sdk 0.3.0__py3-none-any.whl → 2.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1321 @@
1
+ """gRPC client wrapper for capiscio-core."""
2
+
3
+ from typing import Optional
4
+
5
+ import grpc
6
+
7
+ from capiscio_sdk._rpc.process import ProcessManager, get_process_manager
8
+
9
+ # Import generated stubs
10
+ from capiscio_sdk._rpc.gen.capiscio.v1 import badge_pb2, badge_pb2_grpc
11
+ from capiscio_sdk._rpc.gen.capiscio.v1 import did_pb2, did_pb2_grpc
12
+ from capiscio_sdk._rpc.gen.capiscio.v1 import trust_pb2, trust_pb2_grpc
13
+ from capiscio_sdk._rpc.gen.capiscio.v1 import revocation_pb2, revocation_pb2_grpc
14
+ from capiscio_sdk._rpc.gen.capiscio.v1 import scoring_pb2, scoring_pb2_grpc
15
+ from capiscio_sdk._rpc.gen.capiscio.v1 import simpleguard_pb2, simpleguard_pb2_grpc
16
+ from capiscio_sdk._rpc.gen.capiscio.v1 import registry_pb2, registry_pb2_grpc
17
+
18
+
19
+ class CapiscioRPCClient:
20
+ """High-level gRPC client for capiscio-core.
21
+
22
+ This client manages the connection to the capiscio-core gRPC server
23
+ and provides access to all services.
24
+
25
+ Usage:
26
+ # Auto-start local server
27
+ client = CapiscioRPCClient()
28
+ client.connect()
29
+
30
+ # Use DID service
31
+ result = client.did.parse("did:web:example.com:agents:my-agent")
32
+
33
+ # Use badge service
34
+ badge = client.badge.parse_badge(token)
35
+
36
+ client.close()
37
+
38
+ # Or use as context manager
39
+ with CapiscioRPCClient() as client:
40
+ result = client.did.parse("did:web:example.com")
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ address: Optional[str] = None,
46
+ auto_start: bool = True,
47
+ ) -> None:
48
+ """Initialize the client.
49
+
50
+ Args:
51
+ address: gRPC server address. If None, auto-starts local server.
52
+ auto_start: Whether to auto-start local server if address is None.
53
+ """
54
+ self._address = address
55
+ self._auto_start = auto_start and address is None
56
+ self._channel: Optional[grpc.Channel] = None
57
+ self._process_manager: Optional[ProcessManager] = None
58
+
59
+ # Service stubs (initialized on connect)
60
+ self._badge_stub: Optional[badge_pb2_grpc.BadgeServiceStub] = None
61
+ self._did_stub: Optional[did_pb2_grpc.DIDServiceStub] = None
62
+ self._trust_stub: Optional[trust_pb2_grpc.TrustStoreServiceStub] = None
63
+ self._revocation_stub: Optional[revocation_pb2_grpc.RevocationServiceStub] = None
64
+ self._scoring_stub: Optional[scoring_pb2_grpc.ScoringServiceStub] = None
65
+ self._simpleguard_stub: Optional[simpleguard_pb2_grpc.SimpleGuardServiceStub] = None
66
+ self._registry_stub: Optional[registry_pb2_grpc.RegistryServiceStub] = None
67
+
68
+ # Service wrappers
69
+ self._badge: Optional["BadgeClient"] = None
70
+ self._did: Optional["DIDClient"] = None
71
+ self._trust: Optional["TrustStoreClient"] = None
72
+ self._revocation: Optional["RevocationClient"] = None
73
+ self._scoring: Optional["ScoringClient"] = None
74
+ self._simpleguard: Optional["SimpleGuardClient"] = None
75
+ self._registry: Optional["RegistryClient"] = None
76
+
77
+ def connect(self, timeout: float = 10.0) -> "CapiscioRPCClient":
78
+ """Connect to the gRPC server.
79
+
80
+ Args:
81
+ timeout: Connection timeout in seconds
82
+
83
+ Returns:
84
+ self for chaining
85
+ """
86
+ if self._channel is not None:
87
+ return self # Already connected
88
+
89
+ # Determine address
90
+ address = self._address
91
+ if address is None and self._auto_start:
92
+ self._process_manager = get_process_manager()
93
+ address = self._process_manager.ensure_running(timeout=timeout)
94
+ elif address is None:
95
+ address = "unix:///tmp/capiscio.sock"
96
+
97
+ # Create channel
98
+ if address.startswith("unix://"):
99
+ self._channel = grpc.insecure_channel(address)
100
+ else:
101
+ self._channel = grpc.insecure_channel(address)
102
+
103
+ # Initialize stubs
104
+ self._badge_stub = badge_pb2_grpc.BadgeServiceStub(self._channel)
105
+ self._did_stub = did_pb2_grpc.DIDServiceStub(self._channel)
106
+ self._trust_stub = trust_pb2_grpc.TrustStoreServiceStub(self._channel)
107
+ self._revocation_stub = revocation_pb2_grpc.RevocationServiceStub(self._channel)
108
+ self._scoring_stub = scoring_pb2_grpc.ScoringServiceStub(self._channel)
109
+ self._simpleguard_stub = simpleguard_pb2_grpc.SimpleGuardServiceStub(self._channel)
110
+ self._registry_stub = registry_pb2_grpc.RegistryServiceStub(self._channel)
111
+
112
+ # Initialize service wrappers
113
+ self._badge = BadgeClient(self._badge_stub)
114
+ self._did = DIDClient(self._did_stub)
115
+ self._trust = TrustStoreClient(self._trust_stub)
116
+ self._revocation = RevocationClient(self._revocation_stub)
117
+ self._scoring = ScoringClient(self._scoring_stub)
118
+ self._simpleguard = SimpleGuardClient(self._simpleguard_stub)
119
+ self._registry = RegistryClient(self._registry_stub)
120
+
121
+ return self
122
+
123
+ def close(self) -> None:
124
+ """Close the connection."""
125
+ if self._channel is not None:
126
+ self._channel.close()
127
+ self._channel = None
128
+
129
+ # Clear stubs
130
+ self._badge_stub = None
131
+ self._did_stub = None
132
+ self._trust_stub = None
133
+ self._revocation_stub = None
134
+ self._scoring_stub = None
135
+ self._simpleguard_stub = None
136
+ self._registry_stub = None
137
+
138
+ def __enter__(self) -> "CapiscioRPCClient":
139
+ return self.connect()
140
+
141
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
142
+ self.close()
143
+
144
+ def _ensure_connected(self) -> None:
145
+ if self._channel is None:
146
+ self.connect()
147
+
148
+ @property
149
+ def badge(self) -> "BadgeClient":
150
+ """Access the BadgeService."""
151
+ self._ensure_connected()
152
+ assert self._badge is not None
153
+ return self._badge
154
+
155
+ @property
156
+ def did(self) -> "DIDClient":
157
+ """Access the DIDService."""
158
+ self._ensure_connected()
159
+ assert self._did is not None
160
+ return self._did
161
+
162
+ @property
163
+ def trust(self) -> "TrustStoreClient":
164
+ """Access the TrustStoreService."""
165
+ self._ensure_connected()
166
+ assert self._trust is not None
167
+ return self._trust
168
+
169
+ @property
170
+ def revocation(self) -> "RevocationClient":
171
+ """Access the RevocationService."""
172
+ self._ensure_connected()
173
+ assert self._revocation is not None
174
+ return self._revocation
175
+
176
+ @property
177
+ def scoring(self) -> "ScoringClient":
178
+ """Access the ScoringService."""
179
+ self._ensure_connected()
180
+ assert self._scoring is not None
181
+ return self._scoring
182
+
183
+ @property
184
+ def simpleguard(self) -> "SimpleGuardClient":
185
+ """Access the SimpleGuardService."""
186
+ self._ensure_connected()
187
+ assert self._simpleguard is not None
188
+ return self._simpleguard
189
+
190
+ @property
191
+ def registry(self) -> "RegistryClient":
192
+ """Access the RegistryService."""
193
+ self._ensure_connected()
194
+ assert self._registry is not None
195
+ return self._registry
196
+
197
+
198
+ class BadgeClient:
199
+ """Client wrapper for BadgeService."""
200
+
201
+ def __init__(self, stub: badge_pb2_grpc.BadgeServiceStub) -> None:
202
+ self._stub = stub
203
+
204
+ def sign_badge(
205
+ self,
206
+ claims: dict,
207
+ private_key_jwk: str,
208
+ key_id: str = "",
209
+ ) -> tuple[str, dict]:
210
+ """Sign a new badge.
211
+
212
+ Args:
213
+ claims: Badge claims dictionary
214
+ private_key_jwk: Private key in JWK JSON format
215
+ key_id: Optional key ID
216
+
217
+ Returns:
218
+ Tuple of (token, claims)
219
+ """
220
+ pb_claims = badge_pb2.BadgeClaims(
221
+ jti=claims.get("jti", ""),
222
+ iss=claims.get("iss", ""),
223
+ sub=claims.get("sub", ""),
224
+ iat=claims.get("iat", 0),
225
+ exp=claims.get("exp", 0),
226
+ aud=claims.get("aud", []),
227
+ domain=claims.get("domain", ""),
228
+ agent_name=claims.get("agent_name", ""),
229
+ )
230
+
231
+ request = badge_pb2.SignBadgeRequest(
232
+ claims=pb_claims,
233
+ private_key_jwk=private_key_jwk,
234
+ key_id=key_id,
235
+ )
236
+
237
+ response = self._stub.SignBadge(request)
238
+ return response.token, _claims_to_dict(response.claims)
239
+
240
+ def verify_badge(
241
+ self,
242
+ token: str,
243
+ public_key_jwk: str = "",
244
+ ) -> tuple[bool, Optional[dict], Optional[str]]:
245
+ """Verify a badge token.
246
+
247
+ Args:
248
+ token: Badge JWT token
249
+ public_key_jwk: Public key in JWK JSON format
250
+
251
+ Returns:
252
+ Tuple of (valid, claims, error_message)
253
+ """
254
+ request = badge_pb2.VerifyBadgeRequest(
255
+ token=token,
256
+ public_key_jwk=public_key_jwk,
257
+ )
258
+
259
+ response = self._stub.VerifyBadge(request)
260
+ claims = _claims_to_dict(response.claims) if response.claims else None
261
+ error = response.error_message if response.error_message else None
262
+ return response.valid, claims, error
263
+
264
+ def verify_badge_with_options(
265
+ self,
266
+ token: str,
267
+ accept_self_signed: bool = False,
268
+ trusted_issuers: Optional[list[str]] = None,
269
+ audience: str = "",
270
+ skip_revocation: bool = False,
271
+ skip_agent_status: bool = False,
272
+ mode: str = "online",
273
+ fail_open: bool = False,
274
+ stale_threshold_seconds: int = 300,
275
+ ) -> tuple[bool, Optional[dict], list[str], Optional[str]]:
276
+ """Verify a badge token with full options.
277
+
278
+ This is the recommended method for verifying badges, especially
279
+ self-signed (Level 0) badges from did:key issuers.
280
+
281
+ Args:
282
+ token: Badge JWT token
283
+ accept_self_signed: Accept Level 0 self-signed badges (did:key issuer)
284
+ trusted_issuers: List of trusted issuer DIDs
285
+ audience: Expected audience (your service's identifier)
286
+ skip_revocation: Skip revocation check
287
+ skip_agent_status: Skip agent status check
288
+ mode: Verification mode ('online', 'offline', 'hybrid')
289
+ fail_open: RFC-002 v1.3 §7.5 - Allow verification when cache is stale (default: False)
290
+ stale_threshold_seconds: RFC-002 v1.3 §7.5 - Max cache staleness in seconds (default: 300)
291
+
292
+ Returns:
293
+ Tuple of (valid, claims, warnings, error_message)
294
+
295
+ Example:
296
+ # Verify a self-signed badge
297
+ valid, claims, warnings, error = client.badge.verify_badge_with_options(
298
+ token,
299
+ accept_self_signed=True,
300
+ )
301
+ if valid:
302
+ print(f"Badge valid for {claims['sub']}")
303
+ print(f"Trust level: {claims['trust_level']}")
304
+ """
305
+ # Map mode string to proto enum
306
+ mode_map = {
307
+ "online": badge_pb2.VerifyMode.VERIFY_MODE_ONLINE,
308
+ "offline": badge_pb2.VerifyMode.VERIFY_MODE_OFFLINE,
309
+ "hybrid": badge_pb2.VerifyMode.VERIFY_MODE_HYBRID,
310
+ }
311
+ verify_mode = mode_map.get(mode.lower(), badge_pb2.VerifyMode.VERIFY_MODE_ONLINE)
312
+
313
+ options = badge_pb2.VerifyOptions(
314
+ mode=verify_mode,
315
+ trusted_issuers=trusted_issuers or [],
316
+ audience=audience,
317
+ skip_revocation=skip_revocation,
318
+ skip_agent_status=skip_agent_status,
319
+ accept_self_signed=accept_self_signed,
320
+ fail_open=fail_open,
321
+ stale_threshold_seconds=stale_threshold_seconds,
322
+ )
323
+
324
+ request = badge_pb2.VerifyBadgeWithOptionsRequest(
325
+ token=token,
326
+ options=options,
327
+ )
328
+
329
+ response = self._stub.VerifyBadgeWithOptions(request)
330
+ claims = _claims_to_dict(response.claims) if response.claims else None
331
+ warnings = list(response.warnings) if response.warnings else []
332
+ error = response.error_message if response.error_message else None
333
+ return response.valid, claims, warnings, error
334
+
335
+ def parse_badge(self, token: str) -> tuple[Optional[dict], Optional[str]]:
336
+ """Parse badge claims without verification.
337
+
338
+ Args:
339
+ token: Badge JWT token
340
+
341
+ Returns:
342
+ Tuple of (claims, error_message)
343
+ """
344
+ request = badge_pb2.ParseBadgeRequest(token=token)
345
+ response = self._stub.ParseBadge(request)
346
+ claims = _claims_to_dict(response.claims) if response.claims else None
347
+ error = response.error_message if response.error_message else None
348
+ return claims, error
349
+
350
+ def request_badge(
351
+ self,
352
+ agent_id: str,
353
+ api_key: str,
354
+ ca_url: str = "",
355
+ domain: str = "",
356
+ ttl_seconds: int = 300,
357
+ trust_level: int = 1,
358
+ audience: Optional[list[str]] = None,
359
+ ) -> tuple[bool, Optional[dict], Optional[str]]:
360
+ """Request a badge from a Certificate Authority (RFC-002 §12.1).
361
+
362
+ This requests a new badge from the CapiscIO registry CA. The badge
363
+ is signed by the CA and can be used for authenticated A2A communication.
364
+
365
+ Args:
366
+ agent_id: Agent UUID to request badge for
367
+ api_key: API key for authentication (sk_live_... or sk_test_...)
368
+ ca_url: CA URL (default: https://registry.capisc.io)
369
+ domain: Agent domain (optional, uses registered domain)
370
+ ttl_seconds: Badge TTL in seconds (default: 300 per RFC-002)
371
+ trust_level: Trust level 1-4 (default: 1=DV)
372
+ audience: Optional audience restrictions
373
+
374
+ Returns:
375
+ Tuple of (success, result_dict, error_message)
376
+ result_dict contains: token, jti, subject, trust_level, expires_at
377
+
378
+ Example:
379
+ success, result, error = client.badge.request_badge(
380
+ agent_id="my-agent-uuid",
381
+ api_key=os.environ["CAPISCIO_API_KEY"],
382
+ )
383
+ if success:
384
+ token = result["token"]
385
+ print(f"Badge expires: {result['expires_at']}")
386
+ """
387
+ # Map trust level int to proto enum
388
+ trust_level_map = {
389
+ 0: badge_pb2.TrustLevel.TRUST_LEVEL_SELF_SIGNED,
390
+ 1: badge_pb2.TrustLevel.TRUST_LEVEL_DV,
391
+ 2: badge_pb2.TrustLevel.TRUST_LEVEL_OV,
392
+ 3: badge_pb2.TrustLevel.TRUST_LEVEL_EV,
393
+ 4: badge_pb2.TrustLevel.TRUST_LEVEL_CV,
394
+ }
395
+ pb_trust_level = trust_level_map.get(
396
+ trust_level, badge_pb2.TrustLevel.TRUST_LEVEL_DV
397
+ )
398
+
399
+ request = badge_pb2.RequestBadgeRequest(
400
+ agent_id=agent_id,
401
+ ca_url=ca_url,
402
+ api_key=api_key,
403
+ domain=domain,
404
+ ttl_seconds=ttl_seconds,
405
+ trust_level=pb_trust_level,
406
+ audience=audience or [],
407
+ )
408
+
409
+ response = self._stub.RequestBadge(request)
410
+
411
+ if not response.success:
412
+ return False, None, response.error
413
+
414
+ result = {
415
+ "token": response.token,
416
+ "jti": response.jti,
417
+ "subject": response.subject,
418
+ "trust_level": _trust_level_to_string(response.trust_level),
419
+ "expires_at": response.expires_at,
420
+ }
421
+ return True, result, None
422
+
423
+ def request_pop_badge(
424
+ self,
425
+ agent_did: str,
426
+ private_key_jwk: str,
427
+ api_key: str,
428
+ ca_url: str = "",
429
+ ttl_seconds: int = 300,
430
+ audience: Optional[list[str]] = None,
431
+ ) -> tuple[bool, Optional[dict], Optional[str]]:
432
+ """Request a badge using Proof of Possession (RFC-003).
433
+
434
+ This requests a badge using the PoP challenge-response protocol,
435
+ providing IAL-1 assurance with cryptographic key binding.
436
+
437
+ Args:
438
+ agent_did: Agent DID (did:web:... or did:key:...)
439
+ private_key_jwk: Private key in JWK format (JSON string)
440
+ api_key: API key for authentication
441
+ ca_url: CA URL (default: https://registry.capisc.io)
442
+ ttl_seconds: Badge TTL in seconds (default: 300 per RFC-002)
443
+ audience: Optional audience restrictions
444
+
445
+ Returns:
446
+ Tuple of (success, result_dict, error_message)
447
+ result_dict contains: token, jti, subject, trust_level,
448
+ assurance_level, expires_at, cnf
449
+
450
+ Example:
451
+ import json
452
+
453
+ # Load private key
454
+ with open("private.jwk") as f:
455
+ private_key_jwk = json.dumps(json.load(f))
456
+
457
+ success, result, error = client.badge.request_pop_badge(
458
+ agent_did="did:web:registry.capisc.io:agents:my-agent",
459
+ private_key_jwk=private_key_jwk,
460
+ api_key=os.environ["CAPISCIO_API_KEY"],
461
+ )
462
+ if success:
463
+ token = result["token"]
464
+ print(f"IAL-1 badge with cnf: {result['cnf']}")
465
+ """
466
+ request = badge_pb2.RequestPoPBadgeRequest(
467
+ agent_did=agent_did,
468
+ private_key_jwk=private_key_jwk,
469
+ ca_url=ca_url,
470
+ api_key=api_key,
471
+ ttl_seconds=ttl_seconds,
472
+ audience=audience or [],
473
+ )
474
+
475
+ response = self._stub.RequestPoPBadge(request)
476
+
477
+ if not response.success:
478
+ return False, None, response.error
479
+
480
+ result = {
481
+ "token": response.token,
482
+ "jti": response.jti,
483
+ "subject": response.subject,
484
+ "trust_level": response.trust_level,
485
+ "assurance_level": response.assurance_level,
486
+ "expires_at": response.expires_at,
487
+ "cnf": dict(response.cnf),
488
+ }
489
+ return True, result, None
490
+
491
+ def start_keeper(
492
+ self,
493
+ mode: str,
494
+ output_file: str = "badge.jwt",
495
+ agent_id: str = "",
496
+ api_key: str = "",
497
+ ca_url: str = "",
498
+ private_key_path: str = "",
499
+ domain: str = "",
500
+ ttl_seconds: int = 300,
501
+ renew_before_seconds: int = 60,
502
+ check_interval_seconds: int = 30,
503
+ trust_level: int = 1,
504
+ ):
505
+ """Start a badge keeper daemon (RFC-002 §7.3).
506
+
507
+ The keeper automatically renews badges before they expire, ensuring
508
+ continuous operation. Returns a generator of keeper events.
509
+
510
+ Args:
511
+ mode: 'ca' for CA mode, 'self-sign' for development
512
+ output_file: Path to write badge file
513
+ agent_id: Agent UUID (required for CA mode)
514
+ api_key: API key (required for CA mode)
515
+ ca_url: CA URL (default: https://registry.capisc.io)
516
+ private_key_path: Path to private key JWK (required for self-sign)
517
+ domain: Agent domain
518
+ ttl_seconds: Badge TTL (default: 300)
519
+ renew_before_seconds: Renew this many seconds before expiry (default: 60)
520
+ check_interval_seconds: Check interval (default: 30)
521
+ trust_level: Trust level for CA mode (1-4, default: 1)
522
+
523
+ Yields:
524
+ KeeperEvent dicts with: type, badge_jti, subject, trust_level,
525
+ expires_at, error, error_code, timestamp, token
526
+
527
+ Example:
528
+ # CA mode
529
+ for event in client.badge.start_keeper(
530
+ mode="ca",
531
+ agent_id="my-agent-uuid",
532
+ api_key=os.environ["CAPISCIO_API_KEY"],
533
+ ):
534
+ if event["type"] == "renewed":
535
+ print(f"Badge renewed: {event['badge_jti']}")
536
+ elif event["type"] == "error":
537
+ print(f"Error: {event['error']}")
538
+ """
539
+ # Map mode string to proto enum
540
+ mode_map = {
541
+ "ca": badge_pb2.KeeperMode.KEEPER_MODE_CA,
542
+ "self-sign": badge_pb2.KeeperMode.KEEPER_MODE_SELF_SIGN,
543
+ "self_sign": badge_pb2.KeeperMode.KEEPER_MODE_SELF_SIGN,
544
+ }
545
+ pb_mode = mode_map.get(mode.lower(), badge_pb2.KeeperMode.KEEPER_MODE_CA)
546
+
547
+ # Map trust level int to proto enum
548
+ trust_level_map = {
549
+ 0: badge_pb2.TrustLevel.TRUST_LEVEL_SELF_SIGNED,
550
+ 1: badge_pb2.TrustLevel.TRUST_LEVEL_DV,
551
+ 2: badge_pb2.TrustLevel.TRUST_LEVEL_OV,
552
+ 3: badge_pb2.TrustLevel.TRUST_LEVEL_EV,
553
+ 4: badge_pb2.TrustLevel.TRUST_LEVEL_CV,
554
+ }
555
+ pb_trust_level = trust_level_map.get(
556
+ trust_level, badge_pb2.TrustLevel.TRUST_LEVEL_DV
557
+ )
558
+
559
+ request = badge_pb2.StartKeeperRequest(
560
+ mode=pb_mode,
561
+ agent_id=agent_id,
562
+ ca_url=ca_url,
563
+ api_key=api_key,
564
+ output_file=output_file,
565
+ ttl_seconds=ttl_seconds,
566
+ renew_before_seconds=renew_before_seconds,
567
+ check_interval_seconds=check_interval_seconds,
568
+ private_key_path=private_key_path,
569
+ domain=domain,
570
+ trust_level=pb_trust_level,
571
+ )
572
+
573
+ # Stream events from keeper
574
+ for event in self._stub.StartKeeper(request):
575
+ yield _keeper_event_to_dict(event)
576
+
577
+ def create_dv_order(
578
+ self,
579
+ domain: str,
580
+ challenge_type: str,
581
+ jwk: str,
582
+ ca_url: str = "",
583
+ ) -> tuple[bool, Optional[dict], Optional[str]]:
584
+ """Create a Domain Validated badge order (RFC-002 v1.2).
585
+
586
+ Args:
587
+ domain: Domain to validate
588
+ challenge_type: Challenge type ('http-01' or 'dns-01')
589
+ jwk: Public key in JWK format (JSON string)
590
+ ca_url: CA URL (default: https://registry.capisc.io)
591
+
592
+ Returns:
593
+ Tuple of (success, order_dict, error_message)
594
+ order_dict contains: order_id, domain, challenge_type,
595
+ challenge_token, status, validation_url,
596
+ dns_record, expires_at
597
+
598
+ Example:
599
+ import json
600
+
601
+ # Load public key
602
+ with open("public.jwk") as f:
603
+ jwk = json.dumps(json.load(f))
604
+
605
+ success, order, error = client.badge.create_dv_order(
606
+ domain="example.com",
607
+ challenge_type="http-01",
608
+ jwk=jwk,
609
+ )
610
+ if success:
611
+ print(f"Order ID: {order['order_id']}")
612
+ print(f"Validation URL: {order['validation_url']}")
613
+ """
614
+ request = badge_pb2.CreateDVOrderRequest(
615
+ domain=domain,
616
+ challenge_type=challenge_type,
617
+ jwk=jwk,
618
+ ca_url=ca_url,
619
+ )
620
+
621
+ response = self._stub.CreateDVOrder(request)
622
+
623
+ if not response.success:
624
+ return False, None, response.error
625
+
626
+ from datetime import datetime, timezone
627
+
628
+ order = {
629
+ "order_id": response.order_id,
630
+ "domain": response.domain,
631
+ "challenge_type": response.challenge_type,
632
+ "challenge_token": response.challenge_token,
633
+ "status": response.status,
634
+ "validation_url": response.validation_url,
635
+ "dns_record": response.dns_record,
636
+ "expires_at": datetime.fromtimestamp(response.expires_at, timezone.utc).isoformat() if response.expires_at else "",
637
+ }
638
+ return True, order, None
639
+
640
+ def get_dv_order(
641
+ self,
642
+ order_id: str,
643
+ ca_url: str = "",
644
+ ) -> tuple[bool, Optional[dict], Optional[str]]:
645
+ """Get the status of a DV badge order (RFC-002 v1.2).
646
+
647
+ Args:
648
+ order_id: Order ID from create_dv_order
649
+ ca_url: CA URL (default: https://registry.capisc.io)
650
+
651
+ Returns:
652
+ Tuple of (success, order_dict, error_message)
653
+ order_dict contains: order_id, domain, challenge_type,
654
+ challenge_token, status, validation_url,
655
+ dns_record, expires_at, finalized_at (optional)
656
+
657
+ Example:
658
+ success, order, error = client.badge.get_dv_order(
659
+ order_id="550e8400-e29b-41d4-a716-446655440000",
660
+ )
661
+ if success:
662
+ print(f"Status: {order['status']}")
663
+ if order.get('finalized_at'):
664
+ print(f"Finalized at: {order['finalized_at']}")
665
+ """
666
+ request = badge_pb2.GetDVOrderRequest(
667
+ order_id=order_id,
668
+ ca_url=ca_url,
669
+ )
670
+
671
+ response = self._stub.GetDVOrder(request)
672
+
673
+ if not response.success:
674
+ return False, None, response.error
675
+
676
+ from datetime import datetime, timezone
677
+
678
+ order = {
679
+ "order_id": response.order_id,
680
+ "domain": response.domain,
681
+ "challenge_type": response.challenge_type,
682
+ "challenge_token": response.challenge_token,
683
+ "status": response.status,
684
+ "validation_url": response.validation_url,
685
+ "dns_record": response.dns_record,
686
+ "expires_at": datetime.fromtimestamp(response.expires_at, timezone.utc).isoformat() if response.expires_at else "",
687
+ }
688
+
689
+ if response.finalized_at:
690
+ order["finalized_at"] = datetime.fromtimestamp(response.finalized_at, timezone.utc).isoformat()
691
+
692
+ return True, order, None
693
+
694
+ def finalize_dv_order(
695
+ self,
696
+ order_id: str,
697
+ ca_url: str = "",
698
+ ) -> tuple[bool, Optional[dict], Optional[str]]:
699
+ """Finalize a DV badge order and receive a grant (RFC-002 v1.2).
700
+
701
+ Args:
702
+ order_id: Order ID from create_dv_order
703
+ ca_url: CA URL (default: https://registry.capisc.io)
704
+
705
+ Returns:
706
+ Tuple of (success, grant_dict, error_message)
707
+ grant_dict contains: grant (JWT), expires_at
708
+
709
+ Example:
710
+ success, grant, error = client.badge.finalize_dv_order(
711
+ order_id="550e8400-e29b-41d4-a716-446655440000",
712
+ )
713
+ if success:
714
+ print(f"Grant JWT: {grant['grant']}")
715
+ print(f"Expires at: {grant['expires_at']}")
716
+
717
+ # Save grant for later use
718
+ with open("grant.jwt", "w") as f:
719
+ f.write(grant['grant'])
720
+ """
721
+ request = badge_pb2.FinalizeDVOrderRequest(
722
+ order_id=order_id,
723
+ ca_url=ca_url,
724
+ )
725
+
726
+ response = self._stub.FinalizeDVOrder(request)
727
+
728
+ if not response.success:
729
+ return False, None, response.error
730
+
731
+ from datetime import datetime, timezone
732
+
733
+ grant = {
734
+ "grant": response.grant,
735
+ "expires_at": datetime.fromtimestamp(response.expires_at, timezone.utc).isoformat() if response.expires_at else "",
736
+ }
737
+ return True, grant, None
738
+
739
+
740
+ def _keeper_event_to_dict(event) -> dict:
741
+ """Convert KeeperEvent proto to dict."""
742
+ event_type_map = {
743
+ badge_pb2.KeeperEventType.KEEPER_EVENT_STARTED: "started",
744
+ badge_pb2.KeeperEventType.KEEPER_EVENT_RENEWED: "renewed",
745
+ badge_pb2.KeeperEventType.KEEPER_EVENT_ERROR: "error",
746
+ badge_pb2.KeeperEventType.KEEPER_EVENT_STOPPED: "stopped",
747
+ }
748
+ return {
749
+ "type": event_type_map.get(event.type, "unknown"),
750
+ "badge_jti": event.badge_jti,
751
+ "subject": event.subject,
752
+ "trust_level": _trust_level_to_string(event.trust_level),
753
+ "expires_at": event.expires_at,
754
+ "error": event.error,
755
+ "error_code": event.error_code,
756
+ "timestamp": event.timestamp,
757
+ "token": event.token,
758
+ }
759
+
760
+
761
+ def _trust_level_to_string(trust_level) -> str:
762
+ """Convert TrustLevel proto enum to string."""
763
+ level_map = {
764
+ badge_pb2.TrustLevel.TRUST_LEVEL_SELF_SIGNED: "0",
765
+ badge_pb2.TrustLevel.TRUST_LEVEL_DV: "1",
766
+ badge_pb2.TrustLevel.TRUST_LEVEL_OV: "2",
767
+ badge_pb2.TrustLevel.TRUST_LEVEL_EV: "3",
768
+ badge_pb2.TrustLevel.TRUST_LEVEL_CV: "4",
769
+ }
770
+ return level_map.get(trust_level, "")
771
+
772
+
773
+ class DIDClient:
774
+ """Client wrapper for DIDService."""
775
+
776
+ def __init__(self, stub: did_pb2_grpc.DIDServiceStub) -> None:
777
+ self._stub = stub
778
+
779
+ def parse(self, did: str) -> tuple[Optional[dict], Optional[str]]:
780
+ """Parse a did:web identifier.
781
+
782
+ Args:
783
+ did: DID string to parse
784
+
785
+ Returns:
786
+ Tuple of (parsed_did, error_message)
787
+ """
788
+ request = did_pb2.ParseDIDRequest(did=did)
789
+ response = self._stub.Parse(request)
790
+
791
+ if response.error_message:
792
+ return None, response.error_message
793
+
794
+ parsed = {
795
+ "raw": response.did.raw,
796
+ "method": response.did.method,
797
+ "domain": response.did.domain,
798
+ "path": list(response.did.path),
799
+ }
800
+ return parsed, None
801
+
802
+ def new_agent_did(self, domain: str, agent_id: str) -> tuple[str, Optional[str]]:
803
+ """Create a new agent DID.
804
+
805
+ Args:
806
+ domain: Domain for the DID
807
+ agent_id: Agent identifier
808
+
809
+ Returns:
810
+ Tuple of (did, error_message)
811
+ """
812
+ request = did_pb2.NewAgentDIDRequest(domain=domain, agent_id=agent_id)
813
+ response = self._stub.NewAgentDID(request)
814
+ error = response.error_message if response.error_message else None
815
+ return response.did, error
816
+
817
+ def new_capiscio_agent_did(self, agent_id: str) -> tuple[str, Optional[str]]:
818
+ """Create a CapiscIO registry agent DID.
819
+
820
+ Args:
821
+ agent_id: Agent identifier
822
+
823
+ Returns:
824
+ Tuple of (did, error_message)
825
+ """
826
+ request = did_pb2.NewCapiscIOAgentDIDRequest(agent_id=agent_id)
827
+ response = self._stub.NewCapiscIOAgentDID(request)
828
+ error = response.error_message if response.error_message else None
829
+ return response.did, error
830
+
831
+ def document_url(self, did: str) -> tuple[str, Optional[str]]:
832
+ """Get the document URL for a DID.
833
+
834
+ Args:
835
+ did: DID string
836
+
837
+ Returns:
838
+ Tuple of (url, error_message)
839
+ """
840
+ request = did_pb2.DocumentURLRequest(did=did)
841
+ response = self._stub.DocumentURL(request)
842
+ error = response.error_message if response.error_message else None
843
+ return response.url, error
844
+
845
+ def is_agent_did(self, did: str) -> tuple[bool, str]:
846
+ """Check if a DID is an agent DID.
847
+
848
+ Args:
849
+ did: DID string
850
+
851
+ Returns:
852
+ Tuple of (is_agent_did, agent_id)
853
+ """
854
+ request = did_pb2.IsAgentDIDRequest(did=did)
855
+ response = self._stub.IsAgentDID(request)
856
+ return response.is_agent_did, response.agent_id
857
+
858
+
859
+ class TrustStoreClient:
860
+ """Client wrapper for TrustStoreService."""
861
+
862
+ def __init__(self, stub: trust_pb2_grpc.TrustStoreServiceStub) -> None:
863
+ self._stub = stub
864
+
865
+ def add_key(self, did: str, public_key: bytes, format: str = "JWK") -> tuple[str, Optional[str]]:
866
+ """Add a trusted public key."""
867
+ # TODO: Implement when Go service is complete
868
+ raise NotImplementedError("TrustStoreService not yet implemented")
869
+
870
+ def is_trusted(self, did: str) -> bool:
871
+ """Check if a DID is trusted."""
872
+ request = trust_pb2.IsTrustedRequest(did=did)
873
+ response = self._stub.IsTrusted(request)
874
+ return response.is_trusted
875
+
876
+
877
+ class RevocationClient:
878
+ """Client wrapper for RevocationService."""
879
+
880
+ def __init__(self, stub: revocation_pb2_grpc.RevocationServiceStub) -> None:
881
+ self._stub = stub
882
+
883
+ def is_revoked(self, subject: str) -> bool:
884
+ """Check if a subject is revoked."""
885
+ request = revocation_pb2.IsRevokedRequest(subject=subject)
886
+ response = self._stub.IsRevoked(request)
887
+ return response.is_revoked
888
+
889
+
890
+ class ScoringClient:
891
+ """Client wrapper for ScoringService."""
892
+
893
+ def __init__(self, stub: scoring_pb2_grpc.ScoringServiceStub) -> None:
894
+ self._stub = stub
895
+
896
+ def score_agent_card(self, agent_card_json: str) -> tuple[Optional[dict], Optional[str]]:
897
+ """Score an agent card.
898
+
899
+ Args:
900
+ agent_card_json: Agent card as JSON string
901
+
902
+ Returns:
903
+ Tuple of (scoring_result_dict, error_message)
904
+ """
905
+ request = scoring_pb2.ScoreAgentCardRequest(agent_card_json=agent_card_json)
906
+ response = self._stub.ScoreAgentCard(request)
907
+
908
+ if response.error_message:
909
+ return None, response.error_message
910
+
911
+ if not response.result:
912
+ return None, "No result returned"
913
+
914
+ # Convert protobuf result to dict
915
+ result = response.result
916
+ return {
917
+ "overall_score": result.overall_score,
918
+ "rating": result.rating,
919
+ "categories": [
920
+ {
921
+ "category": cat.category,
922
+ "score": cat.score,
923
+ "rules_passed": cat.rules_passed,
924
+ "rules_failed": cat.rules_failed,
925
+ }
926
+ for cat in result.categories
927
+ ],
928
+ "rule_results": [
929
+ {
930
+ "rule_id": r.rule_id,
931
+ "passed": r.passed,
932
+ "message": r.message,
933
+ "details": dict(r.details),
934
+ }
935
+ for r in result.rule_results
936
+ ],
937
+ "validation": {
938
+ "valid": result.validation.valid if result.validation else True,
939
+ "issues": [
940
+ {
941
+ "code": i.code,
942
+ "message": i.message,
943
+ "severity": i.severity,
944
+ "field": i.field,
945
+ }
946
+ for i in (result.validation.issues if result.validation else [])
947
+ ],
948
+ },
949
+ "scored_at": result.scored_at.value if result.scored_at else None,
950
+ "rule_set_id": result.rule_set_id,
951
+ "rule_set_version": result.rule_set_version,
952
+ }, None
953
+
954
+ def validate_rule(self, rule_id: str, agent_card_json: str) -> tuple[Optional[dict], Optional[str]]:
955
+ """Validate a single rule against an agent card.
956
+
957
+ Args:
958
+ rule_id: ID of the rule to validate
959
+ agent_card_json: Agent card as JSON string
960
+
961
+ Returns:
962
+ Tuple of (rule_result_dict, error_message)
963
+ """
964
+ request = scoring_pb2.ValidateRuleRequest(
965
+ rule_id=rule_id,
966
+ agent_card_json=agent_card_json,
967
+ )
968
+ response = self._stub.ValidateRule(request)
969
+
970
+ if response.error_message:
971
+ return None, response.error_message
972
+
973
+ if not response.result:
974
+ return None, "No result returned"
975
+
976
+ return {
977
+ "rule_id": response.result.rule_id,
978
+ "passed": response.result.passed,
979
+ "message": response.result.message,
980
+ "details": dict(response.result.details),
981
+ }, None
982
+
983
+ def list_rule_sets(self) -> tuple[list[dict], Optional[str]]:
984
+ """List available rule sets.
985
+
986
+ Returns:
987
+ Tuple of (list_of_rule_sets, error_message)
988
+ """
989
+ request = scoring_pb2.ListRuleSetsRequest()
990
+ response = self._stub.ListRuleSets(request)
991
+
992
+ rule_sets = []
993
+ for rs in response.rule_sets:
994
+ rule_sets.append({
995
+ "id": rs.id,
996
+ "name": rs.name,
997
+ "version": rs.version,
998
+ "description": rs.description,
999
+ "rules": [
1000
+ {
1001
+ "id": r.id,
1002
+ "name": r.name,
1003
+ "description": r.description,
1004
+ "category": r.category,
1005
+ "severity": r.severity,
1006
+ "weight": r.weight,
1007
+ }
1008
+ for r in rs.rules
1009
+ ],
1010
+ })
1011
+
1012
+ return rule_sets, None
1013
+
1014
+ def get_rule_set(self, rule_set_id: str) -> tuple[Optional[dict], Optional[str]]:
1015
+ """Get details of a specific rule set.
1016
+
1017
+ Args:
1018
+ rule_set_id: ID of the rule set
1019
+
1020
+ Returns:
1021
+ Tuple of (rule_set_dict, error_message)
1022
+ """
1023
+ request = scoring_pb2.GetRuleSetRequest(id=rule_set_id)
1024
+ response = self._stub.GetRuleSet(request)
1025
+
1026
+ if response.error_message:
1027
+ return None, response.error_message
1028
+
1029
+ if not response.rule_set:
1030
+ return None, "No rule set returned"
1031
+
1032
+ rs = response.rule_set
1033
+ return {
1034
+ "id": rs.id,
1035
+ "name": rs.name,
1036
+ "version": rs.version,
1037
+ "description": rs.description,
1038
+ "rules": [
1039
+ {
1040
+ "id": r.id,
1041
+ "name": r.name,
1042
+ "description": r.description,
1043
+ "category": r.category,
1044
+ "severity": r.severity,
1045
+ "weight": r.weight,
1046
+ }
1047
+ for r in rs.rules
1048
+ ],
1049
+ }, None
1050
+
1051
+ def aggregate_scores(
1052
+ self,
1053
+ results: list[dict],
1054
+ method: str = "average"
1055
+ ) -> tuple[Optional[dict], Optional[str]]:
1056
+ """Aggregate multiple scoring results.
1057
+
1058
+ Args:
1059
+ results: List of scoring result dicts with 'overall_score' key
1060
+ method: Aggregation method ('average', 'min', 'max')
1061
+
1062
+ Returns:
1063
+ Tuple of (aggregate_result, error_message)
1064
+ """
1065
+ # Convert dicts to protobuf messages
1066
+ pb_results = []
1067
+ for r in results:
1068
+ pb_results.append(scoring_pb2.ScoringResult(
1069
+ overall_score=r.get("overall_score", 0),
1070
+ ))
1071
+
1072
+ request = scoring_pb2.AggregateScoresRequest(
1073
+ results=pb_results,
1074
+ aggregation_method=method,
1075
+ )
1076
+ response = self._stub.AggregateScores(request)
1077
+
1078
+ return {
1079
+ "aggregate_score": response.aggregate_score,
1080
+ "aggregate_rating": response.aggregate_rating,
1081
+ "category_aggregates": dict(response.category_aggregates),
1082
+ }, None
1083
+
1084
+
1085
+ class SimpleGuardClient:
1086
+ """Client wrapper for SimpleGuardService."""
1087
+
1088
+ def __init__(self, stub: simpleguard_pb2_grpc.SimpleGuardServiceStub) -> None:
1089
+ self._stub = stub
1090
+
1091
+ def sign(self, payload: bytes, key_id: str) -> tuple[bytes, Optional[str]]:
1092
+ """Sign a message (raw signature).
1093
+
1094
+ Args:
1095
+ payload: Message bytes to sign
1096
+ key_id: Key ID to use for signing
1097
+
1098
+ Returns:
1099
+ Tuple of (signature_bytes, error_message)
1100
+ """
1101
+ request = simpleguard_pb2.SignRequest(payload=payload, key_id=key_id)
1102
+ response = self._stub.Sign(request)
1103
+ error = response.error_message if response.error_message else None
1104
+ return response.signature, error
1105
+
1106
+ def verify(
1107
+ self, payload: bytes, signature: bytes, expected_signer: str = ""
1108
+ ) -> tuple[bool, str, Optional[str]]:
1109
+ """Verify a signed message.
1110
+
1111
+ Args:
1112
+ payload: Original message bytes
1113
+ signature: Signature bytes to verify
1114
+ expected_signer: Optional expected signer key ID
1115
+
1116
+ Returns:
1117
+ Tuple of (valid, key_id, error_message)
1118
+ """
1119
+ request = simpleguard_pb2.VerifyRequest(
1120
+ payload=payload,
1121
+ signature=signature,
1122
+ expected_signer=expected_signer,
1123
+ )
1124
+ response = self._stub.Verify(request)
1125
+ error = response.error_message if response.error_message else None
1126
+ return response.valid, response.key_id, error
1127
+
1128
+ def sign_attached(
1129
+ self,
1130
+ payload: bytes,
1131
+ key_id: str,
1132
+ headers: Optional[dict] = None,
1133
+ ) -> tuple[str, Optional[str]]:
1134
+ """Sign with attached payload (creates JWS).
1135
+
1136
+ Args:
1137
+ payload: Payload bytes
1138
+ key_id: Key ID to use
1139
+ headers: Optional additional JWS headers
1140
+
1141
+ Returns:
1142
+ Tuple of (jws_token, error_message)
1143
+ """
1144
+ request = simpleguard_pb2.SignAttachedRequest(
1145
+ payload=payload,
1146
+ key_id=key_id,
1147
+ headers=headers or {},
1148
+ )
1149
+ response = self._stub.SignAttached(request)
1150
+ error = response.error_message if response.error_message else None
1151
+ return response.jws, error
1152
+
1153
+ def verify_attached(
1154
+ self,
1155
+ jws: str,
1156
+ body: Optional[bytes] = None,
1157
+ ) -> tuple[bool, Optional[bytes], str, Optional[str]]:
1158
+ """Verify JWS with optional body hash check.
1159
+
1160
+ Args:
1161
+ jws: JWS compact token
1162
+ body: Optional body bytes to verify against 'bh' claim
1163
+
1164
+ Returns:
1165
+ Tuple of (valid, payload, key_id, error_message)
1166
+ """
1167
+ request = simpleguard_pb2.VerifyAttachedRequest(
1168
+ jws=jws,
1169
+ detached_payload=body or b"",
1170
+ )
1171
+ response = self._stub.VerifyAttached(request)
1172
+ error = response.error_message if response.error_message else None
1173
+ payload = response.payload if response.payload else None
1174
+ return response.valid, payload, response.key_id, error
1175
+
1176
+ def generate_key_pair(
1177
+ self, key_id: str = "", metadata: Optional[dict] = None
1178
+ ) -> tuple[Optional[dict], Optional[str]]:
1179
+ """Generate a new Ed25519 key pair.
1180
+
1181
+ Args:
1182
+ key_id: Optional specific key ID
1183
+ metadata: Optional metadata to associate with key
1184
+
1185
+ Returns:
1186
+ Tuple of (key_info, error_message)
1187
+ key_info contains: key_id, public_key_pem, private_key_pem, did_key
1188
+ """
1189
+ request = simpleguard_pb2.GenerateKeyPairRequest(
1190
+ algorithm=trust_pb2.KEY_ALGORITHM_ED25519,
1191
+ key_id=key_id,
1192
+ metadata=metadata or {},
1193
+ )
1194
+ response = self._stub.GenerateKeyPair(request)
1195
+ error = response.error_message if response.error_message else None
1196
+ if error:
1197
+ return None, error
1198
+ return {
1199
+ "key_id": response.key_id,
1200
+ "public_key_pem": response.public_key_pem,
1201
+ "private_key_pem": response.private_key_pem,
1202
+ "did_key": response.did_key, # did:key URI (RFC-002 §6.1)
1203
+ }, None
1204
+
1205
+ def load_key(self, file_path: str) -> tuple[Optional[dict], Optional[str]]:
1206
+ """Load key from PEM file.
1207
+
1208
+ Args:
1209
+ file_path: Path to PEM file
1210
+
1211
+ Returns:
1212
+ Tuple of (key_info, error_message)
1213
+ """
1214
+ request = simpleguard_pb2.LoadKeyRequest(file_path=file_path)
1215
+ response = self._stub.LoadKey(request)
1216
+ error = response.error_message if response.error_message else None
1217
+ if error:
1218
+ return None, error
1219
+ return {
1220
+ "key_id": response.key_id,
1221
+ "has_private_key": response.has_private_key,
1222
+ }, None
1223
+
1224
+ def export_key(
1225
+ self, key_id: str, file_path: str, include_private: bool = False
1226
+ ) -> tuple[bool, Optional[str]]:
1227
+ """Export key to PEM file.
1228
+
1229
+ Args:
1230
+ key_id: Key to export
1231
+ file_path: Destination path
1232
+ include_private: Whether to include private key
1233
+
1234
+ Returns:
1235
+ Tuple of (success, error_message)
1236
+ """
1237
+ request = simpleguard_pb2.ExportKeyRequest(
1238
+ key_id=key_id,
1239
+ file_path=file_path,
1240
+ include_private=include_private,
1241
+ )
1242
+ response = self._stub.ExportKey(request)
1243
+ error = response.error_message if response.error_message else None
1244
+ return error is None, error
1245
+
1246
+ def get_key_info(self, key_id: str) -> tuple[Optional[dict], Optional[str]]:
1247
+ """Get info about a loaded key.
1248
+
1249
+ Args:
1250
+ key_id: Key to query
1251
+
1252
+ Returns:
1253
+ Tuple of (key_info, error_message)
1254
+ """
1255
+ request = simpleguard_pb2.GetKeyInfoRequest(key_id=key_id)
1256
+ response = self._stub.GetKeyInfo(request)
1257
+ error = response.error_message if response.error_message else None
1258
+ if error:
1259
+ return None, error
1260
+ return {
1261
+ "key_id": response.key_id,
1262
+ "has_private_key": response.has_private_key,
1263
+ "public_key_pem": response.public_key_pem,
1264
+ }, None
1265
+
1266
+
1267
+ class RegistryClient:
1268
+ """Client wrapper for RegistryService."""
1269
+
1270
+ def __init__(self, stub: registry_pb2_grpc.RegistryServiceStub) -> None:
1271
+ self._stub = stub
1272
+
1273
+ def ping(self) -> dict:
1274
+ """Ping the registry."""
1275
+ request = registry_pb2.PingRequest()
1276
+ response = self._stub.Ping(request)
1277
+ return {
1278
+ "status": response.status,
1279
+ "version": response.version,
1280
+ "server_time": response.server_time.value if response.server_time else None,
1281
+ }
1282
+
1283
+ def get_agent(self, did: str) -> tuple[Optional[dict], Optional[str]]:
1284
+ """Get an agent by DID."""
1285
+ request = registry_pb2.GetAgentRequest(did=did)
1286
+ response = self._stub.GetAgent(request)
1287
+
1288
+ if response.error_message:
1289
+ return None, response.error_message
1290
+
1291
+ # TODO: Convert response.agent to dict
1292
+ return None, "not yet implemented"
1293
+
1294
+
1295
+ def _claims_to_dict(claims) -> dict:
1296
+ """Convert protobuf BadgeClaims to dict."""
1297
+ if claims is None:
1298
+ return {}
1299
+
1300
+ # Map proto enum to human-readable trust level string
1301
+ # Note: UNSPECIFIED (0) defaults to "1" (DV) for compatibility
1302
+ trust_level_map = {
1303
+ 0: "1", # TRUST_LEVEL_UNSPECIFIED -> default to DV (1)
1304
+ 1: "0", # TRUST_LEVEL_SELF_SIGNED (Level 0)
1305
+ 2: "1", # TRUST_LEVEL_DV (Level 1)
1306
+ 3: "2", # TRUST_LEVEL_OV (Level 2)
1307
+ 4: "3", # TRUST_LEVEL_EV (Level 3)
1308
+ 5: "4", # TRUST_LEVEL_CV (Level 4)
1309
+ }
1310
+
1311
+ return {
1312
+ "jti": claims.jti,
1313
+ "iss": claims.iss,
1314
+ "sub": claims.sub,
1315
+ "iat": claims.iat,
1316
+ "exp": claims.exp,
1317
+ "aud": list(claims.aud),
1318
+ "trust_level": trust_level_map.get(claims.trust_level, str(claims.trust_level)),
1319
+ "domain": claims.domain,
1320
+ "agent_name": claims.agent_name,
1321
+ }