capiscio-sdk 0.2.0__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- capiscio_sdk/__init__.py +69 -1
- capiscio_sdk/_rpc/__init__.py +7 -0
- capiscio_sdk/_rpc/client.py +1321 -0
- capiscio_sdk/_rpc/gen/__init__.py +1 -0
- capiscio_sdk/_rpc/process.py +232 -0
- capiscio_sdk/badge.py +737 -0
- capiscio_sdk/badge_keeper.py +304 -0
- capiscio_sdk/config.py +1 -1
- capiscio_sdk/dv.py +296 -0
- capiscio_sdk/errors.py +11 -1
- capiscio_sdk/executor.py +17 -0
- capiscio_sdk/integrations/fastapi.py +74 -0
- capiscio_sdk/scoring/__init__.py +73 -3
- capiscio_sdk/simple_guard.py +346 -0
- capiscio_sdk/types.py +1 -1
- capiscio_sdk/validators/__init__.py +59 -2
- capiscio_sdk/validators/_core.py +376 -0
- capiscio_sdk-2.3.0.dist-info/METADATA +532 -0
- capiscio_sdk-2.3.0.dist-info/RECORD +36 -0
- {capiscio_sdk-0.2.0.dist-info → capiscio_sdk-2.3.0.dist-info}/WHEEL +1 -1
- capiscio_sdk-0.2.0.dist-info/METADATA +0 -221
- capiscio_sdk-0.2.0.dist-info/RECORD +0 -26
- {capiscio_sdk-0.2.0.dist-info → capiscio_sdk-2.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,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
|
+
}
|