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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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