capiscio-sdk 2.3.0__py3-none-any.whl → 2.4.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 CHANGED
@@ -14,7 +14,7 @@ Example:
14
14
  >>> result = validate_agent_card(card_dict) # Uses Go core
15
15
  """
16
16
 
17
- __version__ = "0.3.1"
17
+ __version__ = "2.3.1"
18
18
 
19
19
  # Core exports
20
20
  from .executor import CapiscioSecurityExecutor, secure, secure_agent
@@ -1,6 +1,6 @@
1
1
  """gRPC client wrapper for capiscio-core."""
2
2
 
3
- from typing import Optional
3
+ from typing import Generator, Optional
4
4
 
5
5
  import grpc
6
6
 
@@ -9,6 +9,7 @@ from capiscio_sdk._rpc.process import ProcessManager, get_process_manager
9
9
  # Import generated stubs
10
10
  from capiscio_sdk._rpc.gen.capiscio.v1 import badge_pb2, badge_pb2_grpc
11
11
  from capiscio_sdk._rpc.gen.capiscio.v1 import did_pb2, did_pb2_grpc
12
+ from capiscio_sdk._rpc.gen.capiscio.v1 import mcp_pb2, mcp_pb2_grpc
12
13
  from capiscio_sdk._rpc.gen.capiscio.v1 import trust_pb2, trust_pb2_grpc
13
14
  from capiscio_sdk._rpc.gen.capiscio.v1 import revocation_pb2, revocation_pb2_grpc
14
15
  from capiscio_sdk._rpc.gen.capiscio.v1 import scoring_pb2, scoring_pb2_grpc
@@ -59,6 +60,7 @@ class CapiscioRPCClient:
59
60
  # Service stubs (initialized on connect)
60
61
  self._badge_stub: Optional[badge_pb2_grpc.BadgeServiceStub] = None
61
62
  self._did_stub: Optional[did_pb2_grpc.DIDServiceStub] = None
63
+ self._mcp_stub: Optional[mcp_pb2_grpc.MCPServiceStub] = None
62
64
  self._trust_stub: Optional[trust_pb2_grpc.TrustStoreServiceStub] = None
63
65
  self._revocation_stub: Optional[revocation_pb2_grpc.RevocationServiceStub] = None
64
66
  self._scoring_stub: Optional[scoring_pb2_grpc.ScoringServiceStub] = None
@@ -68,6 +70,7 @@ class CapiscioRPCClient:
68
70
  # Service wrappers
69
71
  self._badge: Optional["BadgeClient"] = None
70
72
  self._did: Optional["DIDClient"] = None
73
+ self._mcp: Optional["MCPClient"] = None
71
74
  self._trust: Optional["TrustStoreClient"] = None
72
75
  self._revocation: Optional["RevocationClient"] = None
73
76
  self._scoring: Optional["ScoringClient"] = None
@@ -103,6 +106,7 @@ class CapiscioRPCClient:
103
106
  # Initialize stubs
104
107
  self._badge_stub = badge_pb2_grpc.BadgeServiceStub(self._channel)
105
108
  self._did_stub = did_pb2_grpc.DIDServiceStub(self._channel)
109
+ self._mcp_stub = mcp_pb2_grpc.MCPServiceStub(self._channel)
106
110
  self._trust_stub = trust_pb2_grpc.TrustStoreServiceStub(self._channel)
107
111
  self._revocation_stub = revocation_pb2_grpc.RevocationServiceStub(self._channel)
108
112
  self._scoring_stub = scoring_pb2_grpc.ScoringServiceStub(self._channel)
@@ -112,6 +116,7 @@ class CapiscioRPCClient:
112
116
  # Initialize service wrappers
113
117
  self._badge = BadgeClient(self._badge_stub)
114
118
  self._did = DIDClient(self._did_stub)
119
+ self._mcp = MCPClient(self._mcp_stub)
115
120
  self._trust = TrustStoreClient(self._trust_stub)
116
121
  self._revocation = RevocationClient(self._revocation_stub)
117
122
  self._scoring = ScoringClient(self._scoring_stub)
@@ -129,6 +134,7 @@ class CapiscioRPCClient:
129
134
  # Clear stubs
130
135
  self._badge_stub = None
131
136
  self._did_stub = None
137
+ self._mcp_stub = None
132
138
  self._trust_stub = None
133
139
  self._revocation_stub = None
134
140
  self._scoring_stub = None
@@ -159,6 +165,13 @@ class CapiscioRPCClient:
159
165
  assert self._did is not None
160
166
  return self._did
161
167
 
168
+ @property
169
+ def mcp(self) -> "MCPClient":
170
+ """Access the MCPService (RFC-006 / RFC-007)."""
171
+ self._ensure_connected()
172
+ assert self._mcp is not None
173
+ return self._mcp
174
+
162
175
  @property
163
176
  def trust(self) -> "TrustStoreClient":
164
177
  """Access the TrustStoreService."""
@@ -501,7 +514,7 @@ class BadgeClient:
501
514
  renew_before_seconds: int = 60,
502
515
  check_interval_seconds: int = 30,
503
516
  trust_level: int = 1,
504
- ):
517
+ ) -> Generator[dict, None, None]:
505
518
  """Start a badge keeper daemon (RFC-002 §7.3).
506
519
 
507
520
  The keeper automatically renews badges before they expire, ensuring
@@ -521,8 +534,8 @@ class BadgeClient:
521
534
  trust_level: Trust level for CA mode (1-4, default: 1)
522
535
 
523
536
  Yields:
524
- KeeperEvent dicts with: type, badge_jti, subject, trust_level,
525
- expires_at, error, error_code, timestamp, token
537
+ dict: KeeperEvent dicts with keys: type, badge_jti, subject, trust_level,
538
+ expires_at, error, error_code, timestamp, token
526
539
 
527
540
  Example:
528
541
  # CA mode
@@ -1264,6 +1277,395 @@ class SimpleGuardClient:
1264
1277
  }, None
1265
1278
 
1266
1279
 
1280
+ class MCPClient:
1281
+ """Client wrapper for MCPService (RFC-006 Tool Authority + RFC-007 Server Identity).
1282
+
1283
+ This client provides access to MCP security operations including:
1284
+ - Tool access evaluation (RFC-006 §6.2-6.4)
1285
+ - Server identity verification (RFC-007 §7.2)
1286
+ - Server identity parsing from HTTP/JSON-RPC transports
1287
+
1288
+ Example:
1289
+ from capiscio_sdk._rpc.client import CapiscioRPCClient
1290
+
1291
+ client = CapiscioRPCClient()
1292
+ client.connect()
1293
+
1294
+ # Evaluate tool access with a badge
1295
+ result = client.mcp.evaluate_tool_access(
1296
+ tool_name="write_file",
1297
+ params_hash="abc123",
1298
+ server_origin="https://files.example.com",
1299
+ badge_jws=badge_token,
1300
+ )
1301
+
1302
+ if result["decision"] == "allow":
1303
+ print(f"Tool access granted for {result['agent_did']}")
1304
+ else:
1305
+ print(f"Access denied: {result['deny_reason']}")
1306
+
1307
+ # Verify server identity
1308
+ server_result = client.mcp.verify_server_identity(
1309
+ server_did="did:web:example.com:mcp:files",
1310
+ server_badge=server_badge,
1311
+ transport_origin="https://files.example.com",
1312
+ )
1313
+ """
1314
+
1315
+ def __init__(self, stub: mcp_pb2_grpc.MCPServiceStub) -> None:
1316
+ self._stub = stub
1317
+
1318
+ def evaluate_tool_access(
1319
+ self,
1320
+ tool_name: str,
1321
+ params_hash: str = "",
1322
+ server_origin: str = "",
1323
+ *,
1324
+ badge_jws: Optional[str] = None,
1325
+ api_key: Optional[str] = None,
1326
+ policy_version: str = "",
1327
+ trusted_issuers: Optional[list[str]] = None,
1328
+ min_trust_level: int = 0,
1329
+ accept_level_zero: bool = False,
1330
+ allowed_tools: Optional[list[str]] = None,
1331
+ ) -> dict:
1332
+ """Evaluate tool access request (RFC-006 §6.2-6.4).
1333
+
1334
+ Evaluates whether a caller (identified by badge or API key) is
1335
+ authorized to invoke a specific tool. Returns both a decision
1336
+ and evidence record atomically.
1337
+
1338
+ Args:
1339
+ tool_name: Name of the tool being invoked
1340
+ params_hash: Hash of the tool parameters (for audit)
1341
+ server_origin: Origin of the MCP server handling the request
1342
+ badge_jws: Caller's badge JWT (for badged access)
1343
+ api_key: Caller's API key (for API key access)
1344
+ policy_version: Optional policy version to use
1345
+ trusted_issuers: List of trusted badge issuers
1346
+ min_trust_level: Minimum required trust level (0-4)
1347
+ accept_level_zero: Accept self-signed (level 0) badges
1348
+ allowed_tools: Explicit list of allowed tools (if set, tool_name must match)
1349
+
1350
+ Returns:
1351
+ Dict with:
1352
+ decision: "allow" or "deny"
1353
+ deny_reason: Reason if denied (e.g., "badge_missing", "trust_insufficient")
1354
+ deny_detail: Detailed error message if denied
1355
+ agent_did: DID of the authenticated agent (if authenticated)
1356
+ badge_jti: Badge JTI (if badge was used)
1357
+ auth_level: Authentication level ("anonymous", "api_key", "badge")
1358
+ trust_level: Agent's trust level (0-4)
1359
+ evidence_json: RFC-006 §7 evidence record as JSON
1360
+ evidence_id: Unique evidence record ID
1361
+ timestamp: Evaluation timestamp (ISO format)
1362
+
1363
+ Example:
1364
+ # Evaluate with badge
1365
+ result = client.mcp.evaluate_tool_access(
1366
+ tool_name="write_file",
1367
+ params_hash=hashlib.sha256(json.dumps(params).encode()).hexdigest(),
1368
+ server_origin="https://files.example.com",
1369
+ badge_jws=badge_token,
1370
+ min_trust_level=2, # Require OV or higher
1371
+ )
1372
+
1373
+ if result["decision"] == "allow":
1374
+ # Proceed with tool execution
1375
+ pass
1376
+ else:
1377
+ raise PermissionError(result["deny_detail"])
1378
+ """
1379
+ # Build config
1380
+ config = mcp_pb2.EvaluateConfig(
1381
+ trusted_issuers=trusted_issuers or [],
1382
+ min_trust_level=min_trust_level,
1383
+ accept_level_zero=accept_level_zero,
1384
+ allowed_tools=allowed_tools or [],
1385
+ )
1386
+
1387
+ # Build request with caller credential
1388
+ request = mcp_pb2.EvaluateToolAccessRequest(
1389
+ tool_name=tool_name,
1390
+ params_hash=params_hash,
1391
+ server_origin=server_origin,
1392
+ policy_version=policy_version,
1393
+ config=config,
1394
+ )
1395
+
1396
+ # Set credential (badge or api_key, mutually exclusive)
1397
+ if badge_jws:
1398
+ request.badge_jws = badge_jws
1399
+ elif api_key:
1400
+ request.api_key = api_key
1401
+
1402
+ response = self._stub.EvaluateToolAccess(request)
1403
+
1404
+ # Map enums to strings
1405
+ decision_map = {
1406
+ mcp_pb2.MCP_DECISION_UNSPECIFIED: "unspecified",
1407
+ mcp_pb2.MCP_DECISION_ALLOW: "allow",
1408
+ mcp_pb2.MCP_DECISION_DENY: "deny",
1409
+ }
1410
+
1411
+ deny_reason_map = {
1412
+ mcp_pb2.MCP_DENY_REASON_UNSPECIFIED: "",
1413
+ mcp_pb2.MCP_DENY_REASON_BADGE_MISSING: "badge_missing",
1414
+ mcp_pb2.MCP_DENY_REASON_BADGE_INVALID: "badge_invalid",
1415
+ mcp_pb2.MCP_DENY_REASON_BADGE_EXPIRED: "badge_expired",
1416
+ mcp_pb2.MCP_DENY_REASON_BADGE_REVOKED: "badge_revoked",
1417
+ mcp_pb2.MCP_DENY_REASON_TRUST_INSUFFICIENT: "trust_insufficient",
1418
+ mcp_pb2.MCP_DENY_REASON_TOOL_NOT_ALLOWED: "tool_not_allowed",
1419
+ mcp_pb2.MCP_DENY_REASON_ISSUER_UNTRUSTED: "issuer_untrusted",
1420
+ mcp_pb2.MCP_DENY_REASON_POLICY_DENIED: "policy_denied",
1421
+ }
1422
+
1423
+ auth_level_map = {
1424
+ mcp_pb2.MCP_AUTH_LEVEL_UNSPECIFIED: "unspecified",
1425
+ mcp_pb2.MCP_AUTH_LEVEL_ANONYMOUS: "anonymous",
1426
+ mcp_pb2.MCP_AUTH_LEVEL_API_KEY: "api_key",
1427
+ mcp_pb2.MCP_AUTH_LEVEL_BADGE: "badge",
1428
+ }
1429
+
1430
+ # Format timestamp
1431
+ timestamp_str = ""
1432
+ if response.timestamp:
1433
+ from datetime import datetime, timezone
1434
+ timestamp_str = datetime.fromtimestamp(
1435
+ response.timestamp.seconds + response.timestamp.nanos / 1e9,
1436
+ timezone.utc
1437
+ ).isoformat()
1438
+
1439
+ return {
1440
+ "decision": decision_map.get(response.decision, "unspecified"),
1441
+ "deny_reason": deny_reason_map.get(response.deny_reason, ""),
1442
+ "deny_detail": response.deny_detail,
1443
+ "agent_did": response.agent_did,
1444
+ "badge_jti": response.badge_jti,
1445
+ "auth_level": auth_level_map.get(response.auth_level, "unspecified"),
1446
+ "trust_level": response.trust_level,
1447
+ "evidence_json": response.evidence_json,
1448
+ "evidence_id": response.evidence_id,
1449
+ "timestamp": timestamp_str,
1450
+ }
1451
+
1452
+ def verify_server_identity(
1453
+ self,
1454
+ server_did: str,
1455
+ server_badge: str = "",
1456
+ transport_origin: str = "",
1457
+ endpoint_path: str = "",
1458
+ *,
1459
+ trusted_issuers: Optional[list[str]] = None,
1460
+ min_trust_level: int = 0,
1461
+ accept_level_zero: bool = False,
1462
+ offline_mode: bool = False,
1463
+ skip_origin_binding: bool = False,
1464
+ ) -> dict:
1465
+ """Verify MCP server identity (RFC-007 §7.2).
1466
+
1467
+ Verifies a server's disclosed identity (DID + badge) and checks
1468
+ that it matches the transport origin. Returns the verification
1469
+ state and any errors.
1470
+
1471
+ Args:
1472
+ server_did: Server's DID (did:web:... or did:key:...)
1473
+ server_badge: Server's badge JWT (optional for level 0)
1474
+ transport_origin: Origin from the transport (e.g., "https://files.example.com")
1475
+ endpoint_path: Endpoint path being accessed
1476
+ trusted_issuers: List of trusted badge issuers
1477
+ min_trust_level: Minimum required trust level (0-4)
1478
+ accept_level_zero: Accept self-signed (level 0) servers
1479
+ offline_mode: Skip online validation (use cache only)
1480
+ skip_origin_binding: Skip transport origin binding check (RFC-007 §5.3)
1481
+
1482
+ Returns:
1483
+ Dict with:
1484
+ state: Server state ("verified_principal", "declared_principal", "unverified_origin")
1485
+ trust_level: Server's trust level (0-4)
1486
+ server_did: Verified server DID
1487
+ badge_jti: Server badge JTI (if badge was provided)
1488
+ error_code: Error code if verification failed
1489
+ error_detail: Detailed error message
1490
+
1491
+ Example:
1492
+ # Verify server before trusting tool results
1493
+ result = client.mcp.verify_server_identity(
1494
+ server_did="did:web:files.example.com:mcp:files",
1495
+ server_badge=server_badge_token,
1496
+ transport_origin="https://files.example.com",
1497
+ min_trust_level=1, # Require at least DV
1498
+ )
1499
+
1500
+ if result["state"] == "verified_principal":
1501
+ print(f"Server verified at trust level {result['trust_level']}")
1502
+ else:
1503
+ print(f"Server verification failed: {result['error_detail']}")
1504
+ """
1505
+ # Build config
1506
+ config = mcp_pb2.MCPVerifyConfig(
1507
+ trusted_issuers=trusted_issuers or [],
1508
+ min_trust_level=min_trust_level,
1509
+ accept_level_zero=accept_level_zero,
1510
+ offline_mode=offline_mode,
1511
+ skip_origin_binding=skip_origin_binding,
1512
+ )
1513
+
1514
+ request = mcp_pb2.VerifyServerIdentityRequest(
1515
+ server_did=server_did,
1516
+ server_badge=server_badge,
1517
+ transport_origin=transport_origin,
1518
+ endpoint_path=endpoint_path,
1519
+ config=config,
1520
+ )
1521
+
1522
+ response = self._stub.VerifyServerIdentity(request)
1523
+
1524
+ # Map enums to strings
1525
+ state_map = {
1526
+ mcp_pb2.MCP_SERVER_STATE_UNSPECIFIED: "unspecified",
1527
+ mcp_pb2.MCP_SERVER_STATE_VERIFIED_PRINCIPAL: "verified_principal",
1528
+ mcp_pb2.MCP_SERVER_STATE_DECLARED_PRINCIPAL: "declared_principal",
1529
+ mcp_pb2.MCP_SERVER_STATE_UNVERIFIED_ORIGIN: "unverified_origin",
1530
+ }
1531
+
1532
+ error_code_map = {
1533
+ mcp_pb2.MCP_SERVER_ERROR_NONE: "",
1534
+ mcp_pb2.MCP_SERVER_ERROR_DID_INVALID: "did_invalid",
1535
+ mcp_pb2.MCP_SERVER_ERROR_BADGE_INVALID: "badge_invalid",
1536
+ mcp_pb2.MCP_SERVER_ERROR_BADGE_EXPIRED: "badge_expired",
1537
+ mcp_pb2.MCP_SERVER_ERROR_BADGE_REVOKED: "badge_revoked",
1538
+ mcp_pb2.MCP_SERVER_ERROR_TRUST_INSUFFICIENT: "trust_insufficient",
1539
+ mcp_pb2.MCP_SERVER_ERROR_ORIGIN_MISMATCH: "origin_mismatch",
1540
+ mcp_pb2.MCP_SERVER_ERROR_PATH_MISMATCH: "path_mismatch",
1541
+ mcp_pb2.MCP_SERVER_ERROR_ISSUER_UNTRUSTED: "issuer_untrusted",
1542
+ }
1543
+
1544
+ return {
1545
+ "state": state_map.get(response.state, "unspecified"),
1546
+ "trust_level": response.trust_level,
1547
+ "server_did": response.server_did,
1548
+ "badge_jti": response.badge_jti,
1549
+ "error_code": error_code_map.get(response.error_code, ""),
1550
+ "error_detail": response.error_detail,
1551
+ }
1552
+
1553
+ def parse_server_identity_http(
1554
+ self,
1555
+ capiscio_server_did: str = "",
1556
+ capiscio_server_badge: str = "",
1557
+ ) -> dict:
1558
+ """Parse server identity from HTTP headers (RFC-007 §5.2).
1559
+
1560
+ Extracts server identity from HTTP headers. Use this before
1561
+ verify_server_identity() to extract the DID and badge.
1562
+
1563
+ Args:
1564
+ capiscio_server_did: Value of Capiscio-Server-DID header
1565
+ capiscio_server_badge: Value of Capiscio-Server-Badge header
1566
+
1567
+ Returns:
1568
+ Dict with:
1569
+ server_did: Extracted server DID
1570
+ server_badge: Extracted server badge JWT
1571
+ identity_present: Whether identity was present
1572
+
1573
+ Example:
1574
+ # Extract from HTTP headers
1575
+ headers = response.headers
1576
+ identity = client.mcp.parse_server_identity_http(
1577
+ capiscio_server_did=headers.get("Capiscio-Server-DID", ""),
1578
+ capiscio_server_badge=headers.get("Capiscio-Server-Badge", ""),
1579
+ )
1580
+
1581
+ if identity["identity_present"]:
1582
+ # Verify the extracted identity
1583
+ result = client.mcp.verify_server_identity(
1584
+ server_did=identity["server_did"],
1585
+ server_badge=identity["server_badge"],
1586
+ transport_origin="https://files.example.com",
1587
+ )
1588
+ """
1589
+ http_headers = mcp_pb2.MCPHttpHeaders(
1590
+ capiscio_server_did=capiscio_server_did,
1591
+ capiscio_server_badge=capiscio_server_badge,
1592
+ )
1593
+
1594
+ request = mcp_pb2.ParseServerIdentityRequest(http_headers=http_headers)
1595
+ response = self._stub.ParseServerIdentity(request)
1596
+
1597
+ return {
1598
+ "server_did": response.server_did,
1599
+ "server_badge": response.server_badge,
1600
+ "identity_present": response.identity_present,
1601
+ }
1602
+
1603
+ def parse_server_identity_jsonrpc(self, meta_json: str) -> dict:
1604
+ """Parse server identity from JSON-RPC _meta (RFC-007 §5.3).
1605
+
1606
+ Extracts server identity from JSON-RPC _meta field. Use this
1607
+ for stdio transport or any JSON-RPC based MCP connection.
1608
+
1609
+ Args:
1610
+ meta_json: JSON string of the _meta object containing
1611
+ "serverDid" and "serverBadge" fields
1612
+
1613
+ Returns:
1614
+ Dict with:
1615
+ server_did: Extracted server DID
1616
+ server_badge: Extracted server badge JWT
1617
+ identity_present: Whether identity was present
1618
+
1619
+ Example:
1620
+ # Extract from JSON-RPC response
1621
+ meta = response.get("_meta", {})
1622
+ identity = client.mcp.parse_server_identity_jsonrpc(
1623
+ meta_json=json.dumps(meta)
1624
+ )
1625
+
1626
+ if identity["identity_present"]:
1627
+ # Verify the extracted identity
1628
+ result = client.mcp.verify_server_identity(
1629
+ server_did=identity["server_did"],
1630
+ server_badge=identity["server_badge"],
1631
+ transport_origin="", # N/A for stdio
1632
+ )
1633
+ """
1634
+ jsonrpc_meta = mcp_pb2.MCPJsonRpcMeta(meta_json=meta_json)
1635
+
1636
+ request = mcp_pb2.ParseServerIdentityRequest(jsonrpc_meta=jsonrpc_meta)
1637
+ response = self._stub.ParseServerIdentity(request)
1638
+
1639
+ return {
1640
+ "server_did": response.server_did,
1641
+ "server_badge": response.server_badge,
1642
+ "identity_present": response.identity_present,
1643
+ }
1644
+
1645
+ def health(self, client_version: str = "") -> dict:
1646
+ """Check MCP service health and version compatibility.
1647
+
1648
+ Args:
1649
+ client_version: Client's version for compatibility check
1650
+
1651
+ Returns:
1652
+ Dict with:
1653
+ healthy: Whether service is healthy
1654
+ core_version: capiscio-core version
1655
+ proto_version: Proto/gRPC version
1656
+ version_compatible: Whether versions are compatible
1657
+ """
1658
+ request = mcp_pb2.MCPHealthRequest(client_version=client_version)
1659
+ response = self._stub.Health(request)
1660
+
1661
+ return {
1662
+ "healthy": response.healthy,
1663
+ "core_version": response.core_version,
1664
+ "proto_version": response.proto_version,
1665
+ "version_compatible": response.version_compatible,
1666
+ }
1667
+
1668
+
1267
1669
  class RegistryClient:
1268
1670
  """Client wrapper for RegistryService."""
1269
1671
 
capiscio_sdk/badge.py CHANGED
@@ -30,7 +30,7 @@ Example usage:
30
30
  from dataclasses import dataclass, field
31
31
  from datetime import datetime
32
32
  from enum import Enum
33
- from typing import List, Optional, Union
33
+ from typing import Generator, List, Optional, Union
34
34
 
35
35
  from capiscio_sdk._rpc.client import CapiscioRPCClient
36
36
 
@@ -64,16 +64,30 @@ class VerifyMode(Enum):
64
64
 
65
65
 
66
66
  class TrustLevel(Enum):
67
- """Trust level as defined in RFC-002."""
67
+ """Trust level as defined in RFC-002 §5.
68
+
69
+ Levels:
70
+ LEVEL_0 (SS): Self-Signed - did:key, iss == sub. Development only.
71
+ LEVEL_1 (REG): Registered - Account registration with CA.
72
+ LEVEL_2 (DV): Domain Validated - DNS/HTTP domain ownership proof.
73
+ LEVEL_3 (OV): Organization Validated - Legal entity verification.
74
+ LEVEL_4 (EV): Extended Validated - Manual review + security audit.
75
+ """
76
+
77
+ LEVEL_0 = "0"
78
+ """Self-Signed (SS) - did:key, iss == sub. Development only."""
68
79
 
69
80
  LEVEL_1 = "1"
70
- """Domain Validated (DV) - Basic verification."""
81
+ """Registered (REG) - Account registration with CA."""
71
82
 
72
83
  LEVEL_2 = "2"
73
- """Organization Validated (OV) - Business verification."""
84
+ """Domain Validated (DV) - DNS/HTTP domain ownership proof."""
74
85
 
75
86
  LEVEL_3 = "3"
76
- """Extended Validation (EV) - Rigorous vetting."""
87
+ """Organization Validated (OV) - Legal entity verification."""
88
+
89
+ LEVEL_4 = "4"
90
+ """Extended Validated (EV) - Manual review + security audit."""
77
91
 
78
92
  @classmethod
79
93
  def from_string(cls, value: str) -> "TrustLevel":
@@ -81,7 +95,7 @@ class TrustLevel(Enum):
81
95
  for level in cls:
82
96
  if level.value == value:
83
97
  return level
84
- raise ValueError(f"Unknown trust level: {value}")
98
+ raise ValueError(f"Unknown trust level: {value}. Valid levels: 0 (SS), 1 (REG), 2 (DV), 3 (OV), 4 (EV)")
85
99
 
86
100
 
87
101
  @dataclass
@@ -90,15 +104,24 @@ class BadgeClaims:
90
104
 
91
105
  Attributes:
92
106
  jti: Unique badge identifier (UUID).
93
- issuer: Badge issuer URL (CA).
107
+ issuer: Badge issuer URL (CA) or did:key for self-signed.
94
108
  subject: Agent DID (did:web format).
95
109
  audience: Optional list of intended audience URLs.
96
110
  issued_at: When the badge was issued.
97
111
  expires_at: When the badge expires.
98
- trust_level: Trust level (1=DV, 2=OV, 3=EV).
112
+ trust_level: Trust level per RFC-002 §5:
113
+ - 0 (SS): Self-Signed - Development only
114
+ - 1 (REG): Registered - Account registration
115
+ - 2 (DV): Domain Validated - DNS/HTTP proof
116
+ - 3 (OV): Organization Validated - Legal entity
117
+ - 4 (EV): Extended Validated - Security audit
99
118
  domain: Agent's verified domain.
100
119
  agent_name: Human-readable agent name.
101
120
  agent_id: Extracted agent ID from subject DID.
121
+ ial: Identity Assurance Level (RFC-002 §7.2.1):
122
+ - "0": Account-attested (no key proof)
123
+ - "1": Proof of Possession (key holder verified, has cnf claim)
124
+ raw_claims: Original JWT claims dict for advanced access.
102
125
  """
103
126
 
104
127
  jti: str
@@ -110,6 +133,8 @@ class BadgeClaims:
110
133
  domain: str
111
134
  agent_name: str = ""
112
135
  audience: List[str] = field(default_factory=list)
136
+ ial: str = "0" # RFC-002 §7.2.1: Default IAL-0 (account-attested)
137
+ raw_claims: Optional[dict] = field(default=None, repr=False) # For advanced access
113
138
 
114
139
  @property
115
140
  def agent_id(self) -> str:
@@ -133,6 +158,11 @@ class BadgeClaims:
133
158
  @classmethod
134
159
  def from_dict(cls, data: dict) -> "BadgeClaims":
135
160
  """Create BadgeClaims from a dictionary."""
161
+ # Handle audience - can be string or list
162
+ aud = data.get("aud", [])
163
+ if isinstance(aud, str):
164
+ aud = [aud] if aud else []
165
+
136
166
  return cls(
137
167
  jti=data.get("jti", ""),
138
168
  issuer=data.get("iss", ""),
@@ -142,12 +172,18 @@ class BadgeClaims:
142
172
  trust_level=TrustLevel.from_string(data.get("trust_level", "1")),
143
173
  domain=data.get("domain", ""),
144
174
  agent_name=data.get("agent_name", ""),
145
- audience=data.get("aud", []),
175
+ audience=aud,
176
+ ial=data.get("ial", "0"), # RFC-002 §7.2.1
177
+ raw_claims=data, # Preserve for advanced access (cnf, key, etc.)
146
178
  )
147
179
 
148
180
  def to_dict(self) -> dict:
149
- """Convert to dictionary."""
150
- return {
181
+ """Convert to dictionary.
182
+
183
+ Preserves the cnf (confirmation) claim for IAL-1 badges to support
184
+ round-trip serialization.
185
+ """
186
+ result = {
151
187
  "jti": self.jti,
152
188
  "iss": self.issuer,
153
189
  "sub": self.subject,
@@ -157,7 +193,36 @@ class BadgeClaims:
157
193
  "domain": self.domain,
158
194
  "agent_name": self.agent_name,
159
195
  "aud": self.audience,
196
+ "ial": self.ial,
160
197
  }
198
+ # Preserve cnf claim for IAL-1 / key-bound badges if it was present
199
+ if self.raw_claims is not None and "cnf" in self.raw_claims:
200
+ result["cnf"] = self.raw_claims["cnf"]
201
+ return result
202
+
203
+ @property
204
+ def has_key_binding(self) -> bool:
205
+ """Check if this badge has IAL-1 key binding (ial='1' and cnf claim).
206
+
207
+ Per RFC-002 §7.2.1, IAL-1 badges MUST include a 'cnf' (confirmation) claim
208
+ that cryptographically binds the badge to the agent's private key.
209
+ """
210
+ return (
211
+ self.ial == "1"
212
+ and self.raw_claims is not None
213
+ and "cnf" in self.raw_claims
214
+ )
215
+
216
+ @property
217
+ def confirmation_key(self) -> Optional[dict]:
218
+ """Get the confirmation key (cnf claim) if present.
219
+
220
+ Returns the JWK thumbprint or key from the cnf claim for IAL-1 badges.
221
+ Returns None for IAL-0 badges or if cnf is not present.
222
+ """
223
+ if self.raw_claims is None:
224
+ return None
225
+ return self.raw_claims.get("cnf")
161
226
 
162
227
 
163
228
  @dataclass
@@ -454,7 +519,12 @@ async def request_badge(
454
519
  ca_url: Certificate Authority URL (default: CapiscIO registry).
455
520
  api_key: API key for authentication with the CA.
456
521
  domain: Agent's domain (required for verification).
457
- trust_level: Requested trust level (1=DV, 2=OV, 3=EV).
522
+ trust_level: Requested trust level per RFC-002 §5:
523
+ - 1 (REG): Registered - Account registration
524
+ - 2 (DV): Domain Validated - DNS/HTTP proof
525
+ - 3 (OV): Organization Validated - Legal entity
526
+ - 4 (EV): Extended Validated - Security audit
527
+ Note: LEVEL_0 (Self-Signed) is not available via CA request.
458
528
  audience: Optional audience restrictions for the badge.
459
529
  timeout: Request timeout in seconds (not used with gRPC).
460
530
 
@@ -652,7 +722,7 @@ def start_badge_keeper(
652
722
  renew_before_seconds: int = 60,
653
723
  check_interval_seconds: int = 30,
654
724
  trust_level: Union[TrustLevel, str, int] = TrustLevel.LEVEL_1,
655
- ):
725
+ ) -> Generator[dict, None, None]:
656
726
  """Start a badge keeper daemon (RFC-002 §7.3).
657
727
 
658
728
  The keeper automatically renews badges before they expire, ensuring
@@ -672,8 +742,8 @@ def start_badge_keeper(
672
742
  trust_level: Trust level for CA mode (1-4, default: 1)
673
743
 
674
744
  Yields:
675
- KeeperEvent dicts with: type, badge_jti, subject, trust_level,
676
- expires_at, error, error_code, timestamp, token
745
+ dict: KeeperEvent dicts with keys: type, badge_jti, subject, trust_level,
746
+ expires_at, error, error_code, timestamp, token
677
747
 
678
748
  Example:
679
749
  # CA mode - production
@@ -1,5 +1,5 @@
1
1
  """FastAPI integration for Capiscio SimpleGuard."""
2
- from typing import Callable, Awaitable, Any, Dict
2
+ from typing import Callable, Awaitable, Any, Dict, List, Optional
3
3
  try:
4
4
  from starlette.middleware.base import BaseHTTPMiddleware
5
5
  from starlette.requests import Request
@@ -15,22 +15,34 @@ import time
15
15
  class CapiscioMiddleware(BaseHTTPMiddleware):
16
16
  """
17
17
  Middleware to enforce A2A identity verification on incoming requests.
18
+
19
+ Args:
20
+ app: The ASGI application.
21
+ guard: SimpleGuard instance for verification.
22
+ exclude_paths: List of paths to skip verification (e.g., ["/health", "/.well-known/agent-card.json"]).
18
23
  """
19
- def __init__(self, app: ASGIApp, guard: SimpleGuard) -> None:
24
+ def __init__(
25
+ self,
26
+ app: ASGIApp,
27
+ guard: SimpleGuard,
28
+ exclude_paths: Optional[List[str]] = None
29
+ ) -> None:
20
30
  super().__init__(app)
21
31
  self.guard = guard
32
+ self.exclude_paths = exclude_paths or []
22
33
 
23
34
  async def dispatch(
24
35
  self,
25
36
  request: Request,
26
37
  call_next: Callable[[Request], Awaitable[Response]]
27
38
  ) -> Response:
28
- # Allow health checks or public endpoints if needed
29
- # For now, we assume everything under /agent/ needs protection
30
- # But let's just check for the header.
31
-
39
+ # Allow CORS preflight
32
40
  if request.method == "OPTIONS":
33
41
  return await call_next(request)
42
+
43
+ # Skip verification for excluded paths
44
+ if request.url.path in self.exclude_paths:
45
+ return await call_next(request)
34
46
 
35
47
  # RFC-002 §9.1: X-Capiscio-Badge header
36
48
  auth_header = request.headers.get("X-Capiscio-Badge")
@@ -1,7 +1,8 @@
1
1
  """Message validation logic."""
2
2
  from typing import TYPE_CHECKING, Any, Dict, List
3
3
  from ..types import ValidationResult, ValidationIssue, ValidationSeverity
4
- from ..scoring import TrustScorer, AvailabilityScorer
4
+ # Import legacy scorer directly to avoid deprecation warning (we only use score_not_tested)
5
+ from ..scoring.availability import AvailabilityScorer as _LegacyAvailabilityScorer
5
6
 
6
7
  if TYPE_CHECKING:
7
8
  from ..scoring.types import ComplianceScore
@@ -19,8 +20,8 @@ class MessageValidator:
19
20
  def __init__(self) -> None:
20
21
  """Initialize message validator."""
21
22
  self._url_validator = URLSecurityValidator()
22
- self._trust_scorer = TrustScorer()
23
- self._availability_scorer = AvailabilityScorer()
23
+ # Use legacy scorer directly (not the deprecated wrapper) for score_not_tested()
24
+ self._availability_scorer = _LegacyAvailabilityScorer()
24
25
 
25
26
  def validate(self, message: Dict[str, Any], skip_signature_verification: bool = True) -> ValidationResult:
26
27
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capiscio-sdk
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Runtime security middleware for A2A agents
5
5
  Project-URL: Homepage, https://capisc.io
6
6
  Project-URL: Documentation, https://docs.capisc.io/sdk-python
@@ -1,5 +1,5 @@
1
- capiscio_sdk/__init__.py,sha256=obPQ1c-sPLj8DOMUKtJwbHhy_kO9nVjOd8wYx5kesUo,2683
2
- capiscio_sdk/badge.py,sha256=6dGyv12oweTVRf5L7mZqNDW5LYAnruOGmJAvepCzTVI,24219
1
+ capiscio_sdk/__init__.py,sha256=s4wpl1PgPbYiF75ArKjBNSlkmXPqnzm99YzkWMcx3UY,2683
2
+ capiscio_sdk/badge.py,sha256=nUdsJNtUfSD-wWpIpeyzoM7JsBbkoLpxsQRKQAmtpkE,27446
3
3
  capiscio_sdk/badge_keeper.py,sha256=JrxksWiGM7zfKi8BxCqVbVfg4RKEQELH0rUx3Jf-nfg,10900
4
4
  capiscio_sdk/config.py,sha256=-1KNubnRE06SENRXcnGvrj-y1EfKz2uAMdz7s0n6ark,3972
5
5
  capiscio_sdk/dv.py,sha256=wCHjkWTpeKR0INcrusYG-ub3-rnI-e3JiO0nWgaKoF4,8078
@@ -9,13 +9,13 @@ capiscio_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  capiscio_sdk/simple_guard.py,sha256=ZOmJQCN0MVud07pqAzFJYf0tYBkwMv2t2zXUKC6IDLg,13717
10
10
  capiscio_sdk/types.py,sha256=A_0SLhrbX7apkH5yYGTkG-_284xRjCK0N3lEseVRJRs,7978
11
11
  capiscio_sdk/_rpc/__init__.py,sha256=nMnCp5CIisoEA2SX3tYgLprAOyMxVxFfKIWj3glkneA,275
12
- capiscio_sdk/_rpc/client.py,sha256=_CYmNhOQp1suZt9-5I3mm53Y_cEjDeWkwuQMOlgAhAA,47104
12
+ capiscio_sdk/_rpc/client.py,sha256=8XNDx4TeWzkZcv1oSlyhSuBerUkJuVjt23WsADkwWzo,63639
13
13
  capiscio_sdk/_rpc/process.py,sha256=5jyPUDT3Ig1x2UXhAPWPuKqQScAeMkzvs8USshqYrCM,7445
14
14
  capiscio_sdk/_rpc/gen/__init__.py,sha256=JgqmySPbQjyx56NVnyIe_BvNdjwWy-or_OcZPomydYE,29
15
15
  capiscio_sdk/infrastructure/__init__.py,sha256=5yK7kbk1tcHqlnFIN0lR5N_4ZWWquNivLfHkXOAtso0,152
16
16
  capiscio_sdk/infrastructure/cache.py,sha256=FjIRRzNbsPxFZrxdeek51IwFSmR682Wpj1bvvmgqwiU,1841
17
17
  capiscio_sdk/infrastructure/rate_limiter.py,sha256=7Q_GfcooWKjioDeB46PpPb00laDmkLpUYGM0Ui8nXqI,3642
18
- capiscio_sdk/integrations/fastapi.py,sha256=zww7Pg8OMG7Ts03Omq1-1h4ZRR08819FpsTkHW-M8Ak,2949
18
+ capiscio_sdk/integrations/fastapi.py,sha256=EQ28RmG6Gu_DTamMxH0jFZmWW4d6rzvSk4CBkmx070s,3308
19
19
  capiscio_sdk/scoring/__init__.py,sha256=ldC3WyM7jbcGjsEWQK_anB7VjG8y9mZMbNaBUwPgRrY,3621
20
20
  capiscio_sdk/scoring/availability.py,sha256=CzXA1ED48U1Cc06sh0Mtl_kxZP6af-9cceBumTXQhO8,9130
21
21
  capiscio_sdk/scoring/compliance.py,sha256=JZyYuT18A_eiDNdOz-doTIYwW6YhVPvfRj_siNAkkTY,9780
@@ -25,12 +25,12 @@ capiscio_sdk/validators/__init__.py,sha256=q1UjIFUkztKgfC-5N8mGb-zfET3vsEbKbxtN7
25
25
  capiscio_sdk/validators/_core.py,sha256=S76QX_KTdqDbuQPy9WxV2Z0jP4o1H65rABWBwp_6R74,13301
26
26
  capiscio_sdk/validators/agent_card.py,sha256=d8VfGXvtHiuU0DuC1W64Z_JPVqOVh-nRMVShOAznJbk,18506
27
27
  capiscio_sdk/validators/certificate.py,sha256=7RguWk9ahtrxe3TuRqiv5t5vBGta_cUp4qjcx7GwAl0,14373
28
- capiscio_sdk/validators/message.py,sha256=TGVCAwFHcen1v9KLK5UtxVXq61yOqOnd4yudxSxFMmw,15472
28
+ capiscio_sdk/validators/message.py,sha256=vQ8LJGmKL_P_V3oicr15j9dLgwLDjNtUkhbpKngt1ho,15646
29
29
  capiscio_sdk/validators/protocol.py,sha256=bkaJJXseulTJ4Sdiio8gE8Q_Pyqj0BRsJe6BGHSQSnA,5377
30
30
  capiscio_sdk/validators/semver.py,sha256=mlF3GO5ZPA-w6FzSxhjcr56sgCdS0YVVAd1dUr1bxWs,6385
31
31
  capiscio_sdk/validators/signature.py,sha256=lI8XzaKfG_dXSOQXZ40Lda0ntga9EqqC4zAId2kOt6g,8072
32
32
  capiscio_sdk/validators/url_security.py,sha256=SdpOrB48hrfgAMuLvpWH2P0LLCJtg6QBohGDIye8f1E,9802
33
- capiscio_sdk-2.3.0.dist-info/METADATA,sha256=esm8I3-_3y1S0VFgtzreTQtURLwTIJdCDpLiGtZx0cs,17302
34
- capiscio_sdk-2.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
35
- capiscio_sdk-2.3.0.dist-info/licenses/LICENSE,sha256=AMM_E-ILcCpX0JALqX3BL9yfgSx654BtkhX-CBFYp1Q,10758
36
- capiscio_sdk-2.3.0.dist-info/RECORD,,
33
+ capiscio_sdk-2.4.0.dist-info/METADATA,sha256=G2WODrHlhYHQqNdGhGbQxGC4M36kIpeDcQ5VFOdwy_g,17302
34
+ capiscio_sdk-2.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
35
+ capiscio_sdk-2.4.0.dist-info/licenses/LICENSE,sha256=AMM_E-ILcCpX0JALqX3BL9yfgSx654BtkhX-CBFYp1Q,10758
36
+ capiscio_sdk-2.4.0.dist-info/RECORD,,