aporthq-sdk-python 0.1.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.
- aporthq_sdk_python/__init__.py +42 -0
- aporthq_sdk_python/decision_types.py +83 -0
- aporthq_sdk_python/errors.py +31 -0
- aporthq_sdk_python/exceptions.py +32 -0
- aporthq_sdk_python/shared_types.py +59 -0
- aporthq_sdk_python/thin_client.py +306 -0
- aporthq_sdk_python-0.1.0.dist-info/METADATA +288 -0
- aporthq_sdk_python-0.1.0.dist-info/RECORD +11 -0
- aporthq_sdk_python-0.1.0.dist-info/WHEEL +5 -0
- aporthq_sdk_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- aporthq_sdk_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Passport SDK for Python
|
|
3
|
+
|
|
4
|
+
A production-grade thin Python SDK for The Passport for AI Agents, providing
|
|
5
|
+
easy integration with agent authentication and policy verification via API calls.
|
|
6
|
+
All policy logic, counters, and enforcement happen on the server side.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .thin_client import APortClient, APortClientOptions, PolicyVerifier
|
|
10
|
+
from .decision_types import (
|
|
11
|
+
Decision,
|
|
12
|
+
DecisionReason,
|
|
13
|
+
VerificationContext,
|
|
14
|
+
PolicyVerificationRequest,
|
|
15
|
+
PolicyVerificationResponse,
|
|
16
|
+
Jwks,
|
|
17
|
+
)
|
|
18
|
+
from .errors import AportError
|
|
19
|
+
|
|
20
|
+
# Backward compatibility - re-export from shared_types
|
|
21
|
+
from .shared_types import PassportData, AgentPassport
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Core SDK
|
|
26
|
+
"APortClient",
|
|
27
|
+
"APortClientOptions",
|
|
28
|
+
"PolicyVerifier",
|
|
29
|
+
"AportError",
|
|
30
|
+
|
|
31
|
+
# Decision types
|
|
32
|
+
"Decision",
|
|
33
|
+
"DecisionReason",
|
|
34
|
+
"VerificationContext",
|
|
35
|
+
"PolicyVerificationRequest",
|
|
36
|
+
"PolicyVerificationResponse",
|
|
37
|
+
"Jwks",
|
|
38
|
+
|
|
39
|
+
# Backward compatibility
|
|
40
|
+
"PassportData",
|
|
41
|
+
"AgentPassport",
|
|
42
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared types for SDK-Server communication
|
|
3
|
+
These types are used by both the SDK and the API endpoints
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Canonical request/response shapes for production-grade API
|
|
11
|
+
@dataclass
|
|
12
|
+
class PolicyVerificationRequest:
|
|
13
|
+
"""Canonical request shape for policy verification."""
|
|
14
|
+
|
|
15
|
+
agent_id: str # instance or template id
|
|
16
|
+
idempotency_key: Optional[str] = None # also sent as header; see below
|
|
17
|
+
context: Dict[str, Any] = None # policy-specific fields
|
|
18
|
+
|
|
19
|
+
def __post_init__(self):
|
|
20
|
+
if self.context is None:
|
|
21
|
+
self.context = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class PolicyVerificationResponse:
|
|
26
|
+
"""Canonical response shape for policy verification."""
|
|
27
|
+
|
|
28
|
+
decision_id: str
|
|
29
|
+
allow: bool
|
|
30
|
+
reasons: Optional[List[Dict[str, str]]] = None
|
|
31
|
+
assurance_level: Optional[str] = None # "L0" | "L1" | "L2" | "L3" | "L4"
|
|
32
|
+
expires_in: Optional[int] = None # for decision token mode
|
|
33
|
+
passport_digest: Optional[str] = None
|
|
34
|
+
signature: Optional[str] = None # HMAC/JWT
|
|
35
|
+
created_at: Optional[str] = None
|
|
36
|
+
_meta: Optional[Dict[str, Any]] = None # Server-Timing, etc.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Legacy types for backward compatibility
|
|
40
|
+
@dataclass
|
|
41
|
+
class DecisionReason:
|
|
42
|
+
"""Reason for a policy decision."""
|
|
43
|
+
|
|
44
|
+
code: str
|
|
45
|
+
message: str
|
|
46
|
+
severity: str # "info" | "warning" | "error"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Decision(PolicyVerificationResponse):
|
|
51
|
+
"""Policy decision result (legacy compatibility)."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class VerificationContext:
|
|
57
|
+
"""Context for policy verification (legacy compatibility)."""
|
|
58
|
+
|
|
59
|
+
agent_id: str
|
|
60
|
+
policy_id: str
|
|
61
|
+
context: Optional[Dict[str, Any]] = None
|
|
62
|
+
idempotency_key: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# JWKS support for local token validation
|
|
66
|
+
@dataclass
|
|
67
|
+
class JwksKey:
|
|
68
|
+
"""JSON Web Key."""
|
|
69
|
+
|
|
70
|
+
kty: str
|
|
71
|
+
use: str
|
|
72
|
+
kid: str
|
|
73
|
+
x5t: str
|
|
74
|
+
n: str
|
|
75
|
+
e: str
|
|
76
|
+
x5c: List[str]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class Jwks:
|
|
81
|
+
"""JSON Web Key Set."""
|
|
82
|
+
|
|
83
|
+
keys: List[JwksKey]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom error types for the APort Python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Dict, Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AportError(Exception):
|
|
9
|
+
"""Custom error for APort API failures."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
status: int,
|
|
14
|
+
reasons: Optional[List[Dict[str, str]]] = None,
|
|
15
|
+
decision_id: Optional[str] = None,
|
|
16
|
+
server_timing: Optional[str] = None,
|
|
17
|
+
raw_response: Optional[str] = None,
|
|
18
|
+
):
|
|
19
|
+
message = (
|
|
20
|
+
f"API request failed: {status} {', '.join([r['message'] for r in reasons])}"
|
|
21
|
+
if reasons
|
|
22
|
+
else f"API request failed: {status}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.name = "AportError"
|
|
27
|
+
self.status = status
|
|
28
|
+
self.reasons = reasons
|
|
29
|
+
self.decision_id = decision_id
|
|
30
|
+
self.server_timing = server_timing
|
|
31
|
+
self.raw_response = raw_response
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Custom exceptions for the Agent Passport SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgentPassportError(Exception):
|
|
5
|
+
"""Base exception for Agent Passport SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: str,
|
|
10
|
+
code: str,
|
|
11
|
+
status_code: int,
|
|
12
|
+
agent_id: str = None
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the Agent Passport error.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
message: Error message
|
|
19
|
+
code: Error code
|
|
20
|
+
status_code: HTTP status code
|
|
21
|
+
agent_id: Agent ID that caused the error
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.message = message
|
|
25
|
+
self.code = code
|
|
26
|
+
self.status_code = status_code
|
|
27
|
+
self.agent_id = agent_id
|
|
28
|
+
self.name = "AgentPassportError"
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
"""Return string representation of the error."""
|
|
32
|
+
return f"{self.name}: {self.message} (code: {self.code}, status: {self.status_code})"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Shared type definitions that match the TypeScript PassportData interface."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ModelInfo:
|
|
9
|
+
"""Model information for the agent."""
|
|
10
|
+
|
|
11
|
+
model_refs: Optional[List[Dict[str, Any]]] = None
|
|
12
|
+
tools: Optional[List[Dict[str, Any]]] = None
|
|
13
|
+
provenance: Optional[Dict[str, Any]] = None
|
|
14
|
+
data_access: Optional[Dict[str, Any]] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class PassportData:
|
|
19
|
+
"""Complete agent passport data structure."""
|
|
20
|
+
|
|
21
|
+
# Core Identity
|
|
22
|
+
agent_id: str
|
|
23
|
+
slug: str
|
|
24
|
+
name: str
|
|
25
|
+
owner: str
|
|
26
|
+
controller_type: str # "org" | "person"
|
|
27
|
+
claimed: bool
|
|
28
|
+
|
|
29
|
+
# Agent Details
|
|
30
|
+
role: str
|
|
31
|
+
description: str
|
|
32
|
+
permissions: List[str]
|
|
33
|
+
limits: Dict[str, Any]
|
|
34
|
+
regions: List[str]
|
|
35
|
+
|
|
36
|
+
# Status & Verification
|
|
37
|
+
status: str # "draft" | "active" | "suspended" | "revoked"
|
|
38
|
+
verification_status: str # "unverified" | "verified"
|
|
39
|
+
|
|
40
|
+
# Contact & Links
|
|
41
|
+
contact: str
|
|
42
|
+
|
|
43
|
+
# System Metadata
|
|
44
|
+
source: str # "admin" | "form" | "crawler"
|
|
45
|
+
created_at: str
|
|
46
|
+
updated_at: str
|
|
47
|
+
version: str
|
|
48
|
+
|
|
49
|
+
# Optional fields
|
|
50
|
+
verification_method: Optional[str] = None
|
|
51
|
+
links: Optional[Dict[str, str]] = None
|
|
52
|
+
framework: Optional[List[str]] = None
|
|
53
|
+
categories: Optional[List[str]] = None
|
|
54
|
+
logo_url: Optional[str] = None
|
|
55
|
+
model_info: Optional[ModelInfo] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Re-export for backward compatibility
|
|
59
|
+
AgentPassport = PassportData
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Production-grade thin Python SDK Client - API calls only
|
|
3
|
+
No policy logic, no Cloudflare imports, no counters
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
from urllib.parse import urljoin
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
from aiohttp import ClientTimeout, ClientError
|
|
14
|
+
|
|
15
|
+
from .decision_types import (
|
|
16
|
+
PolicyVerificationRequest,
|
|
17
|
+
PolicyVerificationResponse,
|
|
18
|
+
Jwks,
|
|
19
|
+
JwksKey,
|
|
20
|
+
)
|
|
21
|
+
from .errors import AportError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class APortClientOptions:
|
|
25
|
+
"""Configuration options for APortClient."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
base_url: Optional[str] = None,
|
|
30
|
+
api_key: Optional[str] = None,
|
|
31
|
+
timeout_ms: int = 800,
|
|
32
|
+
):
|
|
33
|
+
self.base_url = base_url or "https://api.aport.io"
|
|
34
|
+
self.api_key = api_key
|
|
35
|
+
self.timeout_ms = timeout_ms
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class APortClient:
|
|
39
|
+
"""Production-grade thin SDK Client for APort API."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, options: APortClientOptions):
|
|
42
|
+
self.opts = options
|
|
43
|
+
self.jwks_cache: Optional[Jwks] = None
|
|
44
|
+
self.jwks_cache_expiry: Optional[float] = None
|
|
45
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
46
|
+
|
|
47
|
+
async def __aenter__(self):
|
|
48
|
+
"""Async context manager entry."""
|
|
49
|
+
await self._ensure_session()
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
53
|
+
"""Async context manager exit."""
|
|
54
|
+
await self.close()
|
|
55
|
+
|
|
56
|
+
async def _ensure_session(self):
|
|
57
|
+
"""Ensure HTTP session is created."""
|
|
58
|
+
if self._session is None or self._session.closed:
|
|
59
|
+
timeout = ClientTimeout(total=self.opts.timeout_ms / 1000)
|
|
60
|
+
self._session = aiohttp.ClientSession(
|
|
61
|
+
timeout=timeout,
|
|
62
|
+
headers={
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"Accept": "application/json",
|
|
65
|
+
"User-Agent": "aport-sdk-python/0.1.0",
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async def close(self):
|
|
70
|
+
"""Close the HTTP session."""
|
|
71
|
+
if self._session and not self._session.closed:
|
|
72
|
+
await self._session.close()
|
|
73
|
+
|
|
74
|
+
def _get_headers(self, idempotency_key: Optional[str] = None) -> Dict[str, str]:
|
|
75
|
+
"""Get request headers."""
|
|
76
|
+
headers = {}
|
|
77
|
+
|
|
78
|
+
if self.opts.api_key:
|
|
79
|
+
headers["Authorization"] = f"Bearer {self.opts.api_key}"
|
|
80
|
+
|
|
81
|
+
if idempotency_key:
|
|
82
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
83
|
+
|
|
84
|
+
return headers
|
|
85
|
+
|
|
86
|
+
def _normalize_url(self, path: str) -> str:
|
|
87
|
+
"""Normalize URL by removing trailing slashes and ensuring proper path."""
|
|
88
|
+
base_url = self.opts.base_url.rstrip("/")
|
|
89
|
+
clean_path = path if path.startswith("/") else f"/{path}"
|
|
90
|
+
return f"{base_url}{clean_path}"
|
|
91
|
+
|
|
92
|
+
async def _make_request(
|
|
93
|
+
self,
|
|
94
|
+
method: str,
|
|
95
|
+
path: str,
|
|
96
|
+
data: Optional[Dict[str, Any]] = None,
|
|
97
|
+
idempotency_key: Optional[str] = None,
|
|
98
|
+
) -> Dict[str, Any]:
|
|
99
|
+
"""Make HTTP request with proper error handling."""
|
|
100
|
+
await self._ensure_session()
|
|
101
|
+
|
|
102
|
+
url = self._normalize_url(path)
|
|
103
|
+
headers = self._get_headers(idempotency_key)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
async with self._session.request(
|
|
107
|
+
method=method,
|
|
108
|
+
url=url,
|
|
109
|
+
headers=headers,
|
|
110
|
+
json=data,
|
|
111
|
+
) as response:
|
|
112
|
+
server_timing = response.headers.get("server-timing")
|
|
113
|
+
text = await response.text()
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
json_data = json.loads(text) if text else {}
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
json_data = {}
|
|
119
|
+
|
|
120
|
+
if not response.ok:
|
|
121
|
+
raise AportError(
|
|
122
|
+
status=response.status,
|
|
123
|
+
reasons=json_data.get("reasons"),
|
|
124
|
+
decision_id=json_data.get("decision_id"),
|
|
125
|
+
server_timing=server_timing,
|
|
126
|
+
raw_response=text,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if server_timing:
|
|
130
|
+
json_data["_meta"] = {"serverTiming": server_timing}
|
|
131
|
+
|
|
132
|
+
return json_data
|
|
133
|
+
|
|
134
|
+
except ClientError as e:
|
|
135
|
+
raise AportError(
|
|
136
|
+
status=0,
|
|
137
|
+
reasons=[{"code": "NETWORK_ERROR", "message": str(e)}],
|
|
138
|
+
)
|
|
139
|
+
except asyncio.TimeoutError:
|
|
140
|
+
raise AportError(
|
|
141
|
+
status=408,
|
|
142
|
+
reasons=[{"code": "TIMEOUT", "message": "Request timeout"}],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
async def verify_policy(
|
|
146
|
+
self,
|
|
147
|
+
agent_id: str,
|
|
148
|
+
policy_id: str,
|
|
149
|
+
context: Dict[str, Any] = None,
|
|
150
|
+
idempotency_key: Optional[str] = None,
|
|
151
|
+
) -> PolicyVerificationResponse:
|
|
152
|
+
"""Verify a policy against an agent."""
|
|
153
|
+
if context is None:
|
|
154
|
+
context = {}
|
|
155
|
+
|
|
156
|
+
request = PolicyVerificationRequest(
|
|
157
|
+
agent_id=agent_id,
|
|
158
|
+
context=context,
|
|
159
|
+
idempotency_key=idempotency_key,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
response_data = await self._make_request(
|
|
163
|
+
"POST",
|
|
164
|
+
f"/api/verify/policy/{policy_id}",
|
|
165
|
+
data=request.__dict__,
|
|
166
|
+
idempotency_key=idempotency_key,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return PolicyVerificationResponse(**response_data)
|
|
170
|
+
|
|
171
|
+
async def get_decision_token(
|
|
172
|
+
self,
|
|
173
|
+
agent_id: str,
|
|
174
|
+
policy_id: str,
|
|
175
|
+
context: Dict[str, Any] = None,
|
|
176
|
+
) -> str:
|
|
177
|
+
"""Get a decision token for near-zero latency validation."""
|
|
178
|
+
if context is None:
|
|
179
|
+
context = {}
|
|
180
|
+
|
|
181
|
+
request = PolicyVerificationRequest(
|
|
182
|
+
agent_id=agent_id,
|
|
183
|
+
context=context,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
response_data = await self._make_request(
|
|
187
|
+
"POST",
|
|
188
|
+
f"/api/verify/token/{policy_id}",
|
|
189
|
+
data=request.__dict__,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return response_data["token"]
|
|
193
|
+
|
|
194
|
+
async def validate_decision_token_local(
|
|
195
|
+
self, token: str
|
|
196
|
+
) -> PolicyVerificationResponse:
|
|
197
|
+
"""Validate a decision token locally using JWKS."""
|
|
198
|
+
try:
|
|
199
|
+
jwks = await self.get_jwks()
|
|
200
|
+
# For now, we'll still use the server endpoint
|
|
201
|
+
# TODO: Implement local JWT validation with JWKS
|
|
202
|
+
return await self.validate_decision_token(token)
|
|
203
|
+
except Exception:
|
|
204
|
+
raise AportError(
|
|
205
|
+
401,
|
|
206
|
+
[{"code": "INVALID_TOKEN", "message": "Token validation failed"}],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def validate_decision_token(
|
|
210
|
+
self, token: str
|
|
211
|
+
) -> PolicyVerificationResponse:
|
|
212
|
+
"""Validate a decision token via server (for debugging)."""
|
|
213
|
+
response_data = await self._make_request(
|
|
214
|
+
"POST",
|
|
215
|
+
"/api/verify/token/validate",
|
|
216
|
+
data={"token": token},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return PolicyVerificationResponse(**response_data["decision"])
|
|
220
|
+
|
|
221
|
+
async def get_passport_view(self, agent_id: str) -> Dict[str, Any]:
|
|
222
|
+
"""Get passport verification view (for debugging/about pages)."""
|
|
223
|
+
return await self._make_request("GET", f"/api/passports/{agent_id}/verify_view")
|
|
224
|
+
|
|
225
|
+
async def get_jwks(self) -> Jwks:
|
|
226
|
+
"""Get JWKS for local token validation."""
|
|
227
|
+
# Check cache first
|
|
228
|
+
if (
|
|
229
|
+
self.jwks_cache
|
|
230
|
+
and self.jwks_cache_expiry
|
|
231
|
+
and time.time() < self.jwks_cache_expiry
|
|
232
|
+
):
|
|
233
|
+
return self.jwks_cache
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
response_data = await self._make_request("GET", "/jwks.json")
|
|
237
|
+
self.jwks_cache = Jwks(**response_data)
|
|
238
|
+
self.jwks_cache_expiry = time.time() + (5 * 60) # Cache for 5 minutes
|
|
239
|
+
return self.jwks_cache
|
|
240
|
+
except Exception:
|
|
241
|
+
raise AportError(
|
|
242
|
+
500,
|
|
243
|
+
[{"code": "JWKS_FETCH_FAILED", "message": "Failed to fetch JWKS"}],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class PolicyVerifier:
|
|
248
|
+
"""Convenience class for policy-specific verification methods."""
|
|
249
|
+
|
|
250
|
+
def __init__(self, client: APortClient):
|
|
251
|
+
self.client = client
|
|
252
|
+
|
|
253
|
+
async def verify_refund(
|
|
254
|
+
self,
|
|
255
|
+
agent_id: str,
|
|
256
|
+
context: Dict[str, Any],
|
|
257
|
+
idempotency_key: Optional[str] = None,
|
|
258
|
+
) -> PolicyVerificationResponse:
|
|
259
|
+
"""Verify the finance.payment.refund.v1 policy."""
|
|
260
|
+
return await self.client.verify_policy(
|
|
261
|
+
agent_id, "finance.payment.refund.v1", context, idempotency_key
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
async def verify_release(
|
|
265
|
+
self,
|
|
266
|
+
agent_id: str,
|
|
267
|
+
context: Dict[str, Any],
|
|
268
|
+
idempotency_key: Optional[str] = None,
|
|
269
|
+
) -> PolicyVerificationResponse:
|
|
270
|
+
"""Verify the code.release.publish.v1 policy."""
|
|
271
|
+
return await self.client.verify_policy(
|
|
272
|
+
agent_id, "code.release.publish.v1", context, idempotency_key
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def verify_data_export(
|
|
276
|
+
self,
|
|
277
|
+
agent_id: str,
|
|
278
|
+
context: Dict[str, Any],
|
|
279
|
+
idempotency_key: Optional[str] = None,
|
|
280
|
+
) -> PolicyVerificationResponse:
|
|
281
|
+
"""Verify the data.export.create.v1 policy."""
|
|
282
|
+
return await self.client.verify_policy(
|
|
283
|
+
agent_id, "data.export.create.v1", context, idempotency_key
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def verify_messaging(
|
|
287
|
+
self,
|
|
288
|
+
agent_id: str,
|
|
289
|
+
context: Dict[str, Any],
|
|
290
|
+
idempotency_key: Optional[str] = None,
|
|
291
|
+
) -> PolicyVerificationResponse:
|
|
292
|
+
"""Verify the messaging.message.send.v1 policy."""
|
|
293
|
+
return await self.client.verify_policy(
|
|
294
|
+
agent_id, "messaging.message.send.v1", context, idempotency_key
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
async def verify_repository(
|
|
298
|
+
self,
|
|
299
|
+
agent_id: str,
|
|
300
|
+
context: Dict[str, Any],
|
|
301
|
+
idempotency_key: Optional[str] = None,
|
|
302
|
+
) -> PolicyVerificationResponse:
|
|
303
|
+
"""Verify the code.repository.merge.v1 policy."""
|
|
304
|
+
return await self.client.verify_policy(
|
|
305
|
+
agent_id, "code.repository.merge.v1", context, idempotency_key
|
|
306
|
+
)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aporthq-sdk-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for The Passport for AI Agents
|
|
5
|
+
Author-email: APort Team <team@aport.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://aport.io
|
|
8
|
+
Project-URL: Documentation, https://aport.io/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/aporthq/agent-passport
|
|
10
|
+
Project-URL: Issues, https://github.com/aporthq/agent-passport/issues
|
|
11
|
+
Keywords: agent-passport,ai,authentication,verification,aport,mcp,middleware
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
27
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# Agent Passport Python SDK
|
|
37
|
+
|
|
38
|
+
A production-grade thin Python SDK for The Passport for AI Agents, providing easy integration with agent authentication and policy verification via API calls. All policy logic, counters, and enforcement happen on the server side.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- ✅ **Thin Client Architecture** - No policy logic, no Cloudflare imports, no counters
|
|
43
|
+
- ✅ **Production Ready** - Timeouts, retries, proper error handling, Server-Timing support
|
|
44
|
+
- ✅ **Type Safe** - Full type hints with comprehensive type definitions
|
|
45
|
+
- ✅ **Idempotency Support** - Both header and body idempotency key support
|
|
46
|
+
- ✅ **Local Token Validation** - JWKS support for local decision token validation
|
|
47
|
+
- ✅ **Multiple Environments** - Production, sandbox, and self-hosted enterprise support
|
|
48
|
+
- ✅ **Async/Await** - Modern async Python with aiohttp
|
|
49
|
+
- ✅ **Context Manager** - Proper resource management with async context managers
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install aporthq-sdk-python
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Requirements:** Python 3.8 or higher
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
from aporthq_sdk_python import APortClient, APortClientOptions, PolicyVerifier, AportError
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
# Initialize client for production
|
|
67
|
+
client = APortClient(APortClientOptions(
|
|
68
|
+
base_url="https://api.aport.io", # Production API
|
|
69
|
+
api_key="your-api-key", # Optional
|
|
70
|
+
timeout_ms=800 # Optional: Request timeout (default: 800ms)
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
# Or for sandbox/testing
|
|
74
|
+
sandbox_client = APortClient(APortClientOptions(
|
|
75
|
+
base_url="https://sandbox-api.aport.io", # Sandbox API
|
|
76
|
+
api_key="your-sandbox-key"
|
|
77
|
+
))
|
|
78
|
+
|
|
79
|
+
# Or for self-hosted enterprise
|
|
80
|
+
enterprise_client = APortClient(APortClientOptions(
|
|
81
|
+
base_url="https://your-company.aport.io", # Your self-hosted instance
|
|
82
|
+
api_key="your-enterprise-key"
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
# Create a policy verifier for convenience
|
|
86
|
+
verifier = PolicyVerifier(client)
|
|
87
|
+
|
|
88
|
+
# Verify a refund policy with proper error handling
|
|
89
|
+
try:
|
|
90
|
+
decision = await verifier.verify_refund(
|
|
91
|
+
"your-agent-id",
|
|
92
|
+
{
|
|
93
|
+
"amount": 1000,
|
|
94
|
+
"currency": "USD",
|
|
95
|
+
"order_id": "order_123",
|
|
96
|
+
"reason": "defective"
|
|
97
|
+
},
|
|
98
|
+
"unique-key-123" # idempotency key
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if decision.allow:
|
|
102
|
+
print("✅ Refund approved!")
|
|
103
|
+
print(f"Decision ID: {decision.decision_id}")
|
|
104
|
+
print(f"Assurance Level: {decision.assurance_level}")
|
|
105
|
+
else:
|
|
106
|
+
print("❌ Refund denied!")
|
|
107
|
+
for reason in decision.reasons or []:
|
|
108
|
+
print(f" - [{reason.get('severity', 'info')}] {reason['code']}: {reason['message']}")
|
|
109
|
+
except AportError as error:
|
|
110
|
+
print(f"API Error {error.status}: {error}")
|
|
111
|
+
print(f"Reasons: {error.reasons}")
|
|
112
|
+
print(f"Decision ID: {error.decision_id}")
|
|
113
|
+
except Exception as error:
|
|
114
|
+
print(f"Policy verification failed: {error}")
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
asyncio.run(main())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Environments
|
|
121
|
+
|
|
122
|
+
The SDK supports different environments through the `base_url` parameter:
|
|
123
|
+
|
|
124
|
+
- **Production**: `https://api.aport.io` - The main APort API
|
|
125
|
+
- **Sandbox**: `https://sandbox-api.aport.io` - Testing environment with mock data
|
|
126
|
+
- **Self-hosted**: `https://your-domain.com` - Your own APort instance
|
|
127
|
+
|
|
128
|
+
You can also host your own APort service for complete control over policy verification and data privacy.
|
|
129
|
+
|
|
130
|
+
## API Reference
|
|
131
|
+
|
|
132
|
+
### `APortClient`
|
|
133
|
+
|
|
134
|
+
The core client for interacting with the APort API endpoints.
|
|
135
|
+
|
|
136
|
+
#### `__init__(options: APortClientOptions)`
|
|
137
|
+
Initializes the APort client.
|
|
138
|
+
- `options.base_url` (str): The base URL of your APort API (e.g., `https://api.aport.io`).
|
|
139
|
+
- `options.api_key` (str, optional): Your API Key for authenticated requests.
|
|
140
|
+
- `options.timeout_ms` (int, optional): Request timeout in milliseconds (default: 800ms).
|
|
141
|
+
|
|
142
|
+
#### `async verify_policy(agent_id: str, policy_id: str, context: Dict[str, Any] = None, idempotency_key: str = None) -> PolicyVerificationResponse`
|
|
143
|
+
Verifies a policy against an agent by calling the `/api/verify/policy/:pack_id` endpoint.
|
|
144
|
+
- `agent_id` (str): The ID of the agent.
|
|
145
|
+
- `policy_id` (str): The ID of the policy pack (e.g., `finance.payment.refund.v1`, `code.release.publish.v1`).
|
|
146
|
+
- `context` (Dict[str, Any], optional): The policy-specific context data.
|
|
147
|
+
- `idempotency_key` (str, optional): An optional idempotency key for the request.
|
|
148
|
+
|
|
149
|
+
#### `async get_decision_token(agent_id: str, policy_id: str, context: Dict[str, Any] = None) -> str`
|
|
150
|
+
Retrieves a short-lived decision token for near-zero latency local validation. Calls `/api/verify/token/:pack_id`.
|
|
151
|
+
|
|
152
|
+
#### `async validate_decision_token(token: str) -> PolicyVerificationResponse`
|
|
153
|
+
Validates a decision token via server (for debugging). Calls `/api/verify/token/validate`.
|
|
154
|
+
|
|
155
|
+
#### `async validate_decision_token_local(token: str) -> PolicyVerificationResponse`
|
|
156
|
+
Validates a decision token locally using JWKS (recommended for production). Falls back to server validation if JWKS unavailable.
|
|
157
|
+
|
|
158
|
+
#### `async get_passport_view(agent_id: str) -> Dict[str, Any]`
|
|
159
|
+
Retrieves a small, cacheable view of an agent's passport (limits, assurance, status) for display purposes (e.g., about pages, debugging). Calls `/api/passports/:id/verify_view`.
|
|
160
|
+
|
|
161
|
+
#### `async get_jwks() -> Jwks`
|
|
162
|
+
Retrieves the JSON Web Key Set for local token validation. Cached for 5 minutes.
|
|
163
|
+
|
|
164
|
+
### `PolicyVerifier`
|
|
165
|
+
|
|
166
|
+
A convenience class that wraps `APortClient` to provide policy-specific verification methods.
|
|
167
|
+
|
|
168
|
+
#### `__init__(client: APortClient)`
|
|
169
|
+
Initializes the PolicyVerifier with an `APortClient` instance.
|
|
170
|
+
|
|
171
|
+
#### `async verify_refund(agent_id: str, context: Dict[str, Any], idempotency_key: str = None) -> PolicyVerificationResponse`
|
|
172
|
+
Verifies the `finance.payment.refund.v1` policy.
|
|
173
|
+
|
|
174
|
+
#### `async verify_repository(agent_id: str, context: Dict[str, Any], idempotency_key: str = None) -> PolicyVerificationResponse`
|
|
175
|
+
Verifies the `code.repository.merge.v1` policy.
|
|
176
|
+
|
|
177
|
+
#### Additional Policy Methods
|
|
178
|
+
The `PolicyVerifier` also includes convenience methods for other policies:
|
|
179
|
+
- `verify_release()` - Verifies the `code.release.publish.v1` policy
|
|
180
|
+
- `verify_data_export()` - Verifies the `data.export.create.v1` policy
|
|
181
|
+
- `verify_messaging()` - Verifies the `messaging.message.send.v1` policy
|
|
182
|
+
|
|
183
|
+
These methods follow the same pattern as `verify_refund()` and `verify_repository()`.
|
|
184
|
+
|
|
185
|
+
## Error Handling
|
|
186
|
+
|
|
187
|
+
The SDK raises `AportError` for API request failures with detailed error information.
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from aporthq_sdk_python import AportError
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
await client.verify_policy("invalid-agent", "finance.payment.refund.v1", {})
|
|
194
|
+
except AportError as error:
|
|
195
|
+
print(f"Status: {error.status}")
|
|
196
|
+
print(f"Message: {error}")
|
|
197
|
+
print(f"Reasons: {error.reasons}")
|
|
198
|
+
print(f"Decision ID: {error.decision_id}")
|
|
199
|
+
print(f"Server Timing: {error.server_timing}")
|
|
200
|
+
except Exception as error:
|
|
201
|
+
print(f"Unexpected error: {error}")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Error Types
|
|
205
|
+
|
|
206
|
+
- **`AportError`**: API request failures with status codes, reasons, and decision IDs
|
|
207
|
+
- **Timeout Errors**: 408 status with `TIMEOUT` reason code
|
|
208
|
+
- **Network Errors**: 0 status with `NETWORK_ERROR` reason code
|
|
209
|
+
|
|
210
|
+
## Production Features
|
|
211
|
+
|
|
212
|
+
### Idempotency Support
|
|
213
|
+
The SDK supports idempotency keys in both the request body and the `Idempotency-Key` header (header takes precedence).
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
decision = await client.verify_policy(
|
|
217
|
+
"agent-123",
|
|
218
|
+
"finance.payment.refund.v1",
|
|
219
|
+
{"amount": 100, "currency": "USD"},
|
|
220
|
+
"unique-idempotency-key" # Sent in both header and body
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Server-Timing Support
|
|
225
|
+
The SDK automatically captures and exposes Server-Timing headers for performance monitoring.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
decision = await client.verify_policy("agent-123", "finance.payment.refund.v1", {})
|
|
229
|
+
print("Server timing:", decision._meta.get("serverTiming"))
|
|
230
|
+
# Example: "cache;dur=5,db;dur=12"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Local Token Validation
|
|
234
|
+
For high-performance scenarios, use local token validation with JWKS:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
# Get JWKS (cached for 5 minutes)
|
|
238
|
+
jwks = await client.get_jwks()
|
|
239
|
+
|
|
240
|
+
# Validate token locally (no server round-trip)
|
|
241
|
+
decision = await client.validate_decision_token_local(token)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Async Context Manager
|
|
245
|
+
Use the client as an async context manager for proper resource management:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
async with APortClient(options) as client:
|
|
249
|
+
decision = await client.verify_policy("agent-123", "finance.payment.refund.v1", {})
|
|
250
|
+
# Session is automatically closed
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Timeout and Retry Configuration
|
|
254
|
+
Configure timeouts and retry behavior:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
client = APortClient(APortClientOptions(
|
|
258
|
+
base_url="https://api.aport.io",
|
|
259
|
+
api_key="your-key",
|
|
260
|
+
timeout_ms=500 # 500ms timeout
|
|
261
|
+
))
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Type Hints
|
|
265
|
+
|
|
266
|
+
The SDK includes full type hints for all classes, methods, and types.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from aporthq_sdk_python import APortClient, APortClientOptions, PolicyVerificationResponse
|
|
270
|
+
|
|
271
|
+
options: APortClientOptions = APortClientOptions(
|
|
272
|
+
base_url='https://api.aport.io',
|
|
273
|
+
api_key='my-secret-key',
|
|
274
|
+
timeout_ms=800
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
client: APortClient = APortClient(options)
|
|
278
|
+
|
|
279
|
+
decision: PolicyVerificationResponse = await client.verify_policy(
|
|
280
|
+
"agent_123",
|
|
281
|
+
"finance.payment.refund.v1",
|
|
282
|
+
{"amount": 500, "currency": "EUR"}
|
|
283
|
+
)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aporthq_sdk_python/__init__.py,sha256=eOGLH6BOCzqfPMFl1Usue7-Xe_y0xy4IhSowDhHR5sg,1021
|
|
2
|
+
aporthq_sdk_python/decision_types.py,sha256=1KF9gwU4OB5s-aSvTNLQgmhL0gCbU361zPk3OHVUk1g,2011
|
|
3
|
+
aporthq_sdk_python/errors.py,sha256=InkX-s50kMvBrs0nxm9Fzw503gK3KyVaKicQkI1U8XM,879
|
|
4
|
+
aporthq_sdk_python/exceptions.py,sha256=nHh4eL1JVjM2k0GamRuWcumRxe85gdwJb0OwieonuZE,927
|
|
5
|
+
aporthq_sdk_python/shared_types.py,sha256=B1Lwy3cqbWeUtdT0TPci6N0Ovex3odvccecJdEYCLyk,1487
|
|
6
|
+
aporthq_sdk_python/thin_client.py,sha256=879DEHy5D8I3GB3wATdzvb4k1PwVhSzFpIE210kWrPY,9988
|
|
7
|
+
aporthq_sdk_python-0.1.0.dist-info/licenses/LICENSE,sha256=RoMdcdH_Zmdi36zfCG1MNgucuWfqb2lPxMCDZFF642s,1071
|
|
8
|
+
aporthq_sdk_python-0.1.0.dist-info/METADATA,sha256=NtW9nDDmZdKpRW9Nw6PIEChKkhfL-vIcFb2bP-Vc0CY,10638
|
|
9
|
+
aporthq_sdk_python-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
aporthq_sdk_python-0.1.0.dist-info/top_level.txt,sha256=WS8Dk9Gm8fcmcVN0zKETcK-ctKMpALjHtOoNsZMCbog,19
|
|
11
|
+
aporthq_sdk_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 LiftRails Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aporthq_sdk_python
|