agentguard-python-sdk 0.1.2__tar.gz → 0.2.0__tar.gz
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.
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/PKG-INFO +2 -1
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard/client.py +82 -69
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard/types.py +4 -4
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/PKG-INFO +2 -1
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/requires.txt +1 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/pyproject.toml +2 -1
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/README.md +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard/__init__.py +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard/auth.py +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard/config.py +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard/consent.py +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard/errors.py +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard/observability.py +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/SOURCES.txt +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/dependency_links.txt +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/top_level.txt +0 -0
- {agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentguard-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A production-grade middleware for AI agents to perform on-chain payments and verifiable consent.
|
|
5
5
|
Author: AgentGuard Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.9
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: httpx>=0.27.0
|
|
10
10
|
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
11
12
|
Requires-Dist: pynacl>=1.5.0
|
|
12
13
|
Requires-Dist: py-algorand-sdk>=2.0.0
|
|
13
14
|
|
|
@@ -15,7 +15,7 @@ from typing import Optional, Dict, Any
|
|
|
15
15
|
from .consent import generate_consent_proof
|
|
16
16
|
from . import auth
|
|
17
17
|
from .types import AgentGuardReceipt, AuditProof
|
|
18
|
-
from .errors import map_backend_error, TransportError, AgentGuardError, VerificationError
|
|
18
|
+
from .errors import map_backend_error, TransportError, AgentGuardError, VerificationError, PaymentError
|
|
19
19
|
from .config import AgentGuardConfig
|
|
20
20
|
from .observability import start_span
|
|
21
21
|
|
|
@@ -55,11 +55,12 @@ def parse_algo_amount_to_microalgos(amount: str) -> int:
|
|
|
55
55
|
class AgentGuardClient:
|
|
56
56
|
def __init__(
|
|
57
57
|
self,
|
|
58
|
-
wallet_address: str,
|
|
58
|
+
wallet_address: Optional[str] = None,
|
|
59
59
|
config: Optional[AgentGuardConfig] = None,
|
|
60
60
|
private_key: str = None,
|
|
61
61
|
backend_url: Optional[str] = None,
|
|
62
|
-
indexer_url: Optional[str] = None
|
|
62
|
+
indexer_url: Optional[str] = None,
|
|
63
|
+
principal_id: Optional[str] = None
|
|
63
64
|
):
|
|
64
65
|
"""
|
|
65
66
|
Initialize the AgentGuard client with a structured config object or direct overrides.
|
|
@@ -72,7 +73,11 @@ class AgentGuardClient:
|
|
|
72
73
|
if indexer_url:
|
|
73
74
|
self.config.indexer_url = indexer_url
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
# Support principal_id as an alias for wallet_address
|
|
77
|
+
self.wallet_address = wallet_address or principal_id
|
|
78
|
+
if not self.wallet_address:
|
|
79
|
+
raise ValueError("Either wallet_address or principal_id must be provided.")
|
|
80
|
+
|
|
76
81
|
self.base_url = self.config.backend_url.rstrip("/")
|
|
77
82
|
self.private_key = private_key
|
|
78
83
|
|
|
@@ -100,48 +105,63 @@ class AgentGuardClient:
|
|
|
100
105
|
correlation_id: Optional[str] = None
|
|
101
106
|
) -> AgentGuardReceipt:
|
|
102
107
|
"""
|
|
103
|
-
|
|
108
|
+
End-to-end flow for accessing resources (Probe -> Pay -> Fetch).
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
amount_algo: The amount in ALGO as a string (e.g. '0.05').
|
|
109
|
-
microalgos: Alternatively, the exact amount in microAlgos (int).
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
AgentGuardReceipt: A structured receipt with transaction details.
|
|
110
|
+
1. SDK sends GET resource_url with no payment.
|
|
111
|
+
2. If 200 -> return data directly, no payment.
|
|
112
|
+
3. If 402 -> extract price from headers, pay via backend, and retry with proof.
|
|
113
113
|
"""
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
# Step A — Probe Request
|
|
115
|
+
async with httpx.AsyncClient(timeout=self.timeout) as probe_client:
|
|
116
|
+
try:
|
|
117
|
+
probe_res = await probe_client.get(resource_url)
|
|
118
|
+
|
|
119
|
+
if probe_res.status_code == 200:
|
|
120
|
+
logger.info(f"Resource {resource_url} is free. Accessing directly.")
|
|
121
|
+
return AgentGuardReceipt(
|
|
122
|
+
tx_id=None,
|
|
123
|
+
consent_hash=None,
|
|
124
|
+
audit_url=None,
|
|
125
|
+
data=probe_res.json()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if probe_res.status_code != 402:
|
|
129
|
+
raise PaymentError(f"Resource server returned unexpected status: {probe_res.status_code}", "ERR_UNEXPECTED_STATUS")
|
|
130
|
+
|
|
131
|
+
# Extract 402 headers
|
|
132
|
+
header_amount = probe_res.headers.get("X-402-Payment-Amount")
|
|
133
|
+
header_receiver = probe_res.headers.get("X-402-Receiver-Address")
|
|
134
|
+
|
|
135
|
+
if header_amount:
|
|
136
|
+
actual_microalgos = int(header_amount)
|
|
137
|
+
logger.info(f"Resource requires payment: {actual_microalgos} microAlgos (per header)")
|
|
138
|
+
elif microalgos is not None:
|
|
139
|
+
actual_microalgos = microalgos
|
|
140
|
+
elif amount_algo is not None:
|
|
141
|
+
actual_microalgos = parse_algo_amount_to_microalgos(amount_algo)
|
|
142
|
+
else:
|
|
143
|
+
raise ValueError("Resource requires payment but provided no price header, and no fallback amount was provided.")
|
|
144
|
+
|
|
145
|
+
except httpx.RequestError as e:
|
|
146
|
+
raise TransportError(f"Failed to probe resource: {e}")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
if isinstance(e, AgentGuardError): raise
|
|
149
|
+
raise AgentGuardError(f"Unexpected error during probe: {str(e)}", "ERR_PROBE_FAILED")
|
|
126
150
|
|
|
127
|
-
#
|
|
128
|
-
# We generate these early so they can be persisted across retries if needed.
|
|
151
|
+
# Step B — Payment (Using Backend Relay)
|
|
129
152
|
idem_key = idempotency_key or str(uuid.uuid4())
|
|
130
153
|
act_nonce = nonce or str(uuid.uuid4())
|
|
131
154
|
act_correlation_id = correlation_id or str(idem_key)
|
|
132
155
|
|
|
133
|
-
# 3. Generate Consent Proof (SHA256 of metadata including microalgos)
|
|
134
|
-
# If timestamp is provided (from a previous retry), we reuse it.
|
|
135
156
|
consent_hash, act_timestamp = generate_consent_proof(
|
|
136
157
|
principal_id=self.wallet_address,
|
|
137
158
|
resource_url=resource_url,
|
|
138
159
|
purpose=purpose,
|
|
139
160
|
nonce=act_nonce,
|
|
140
161
|
microalgos=actual_microalgos,
|
|
141
|
-
timestamp=timestamp
|
|
162
|
+
timestamp=timestamp
|
|
142
163
|
)
|
|
143
164
|
|
|
144
|
-
# 4. Payload Construction
|
|
145
165
|
payload = {
|
|
146
166
|
"resource_url": resource_url,
|
|
147
167
|
"microalgos": actual_microalgos,
|
|
@@ -159,7 +179,6 @@ class AgentGuardClient:
|
|
|
159
179
|
"X-Correlation-ID": act_correlation_id
|
|
160
180
|
}
|
|
161
181
|
|
|
162
|
-
# 5. Cryptographic Signing (Priority 5)
|
|
163
182
|
if self.private_key:
|
|
164
183
|
signature = auth.sign_request(payload, self.private_key)
|
|
165
184
|
headers.update({
|
|
@@ -168,63 +187,57 @@ class AgentGuardClient:
|
|
|
168
187
|
"X-AgentGuard-Key-Id": self.wallet_address,
|
|
169
188
|
"X-AgentGuard-Nonce": act_nonce
|
|
170
189
|
})
|
|
171
|
-
# Priority 6: Signing verified via Span attributes if needed
|
|
172
190
|
|
|
173
|
-
# 6. Request with Retries
|
|
174
191
|
span = start_span("sdk.pay_request", act_correlation_id)
|
|
175
|
-
span.set_attribute("wallet", self.wallet_address)
|
|
176
|
-
span.set_attribute("microalgos", actual_microalgos)
|
|
177
|
-
span.set_attribute("idempotency_key", idem_key)
|
|
178
|
-
|
|
179
192
|
max_attempts = self.config.max_retries
|
|
180
193
|
base_delay = self.config.retry_base_delay
|
|
181
194
|
|
|
195
|
+
receipt = None
|
|
182
196
|
for attempt in range(max_attempts):
|
|
183
|
-
attempt_span = start_span(f"sdk.request_attempt_{attempt + 1}", act_correlation_id, parent_id=span.span_id)
|
|
184
197
|
try:
|
|
185
198
|
response = await self.async_client.post(f"{self.base_url}/pay", json=payload, headers=headers)
|
|
186
199
|
|
|
187
200
|
if response.status_code == 200:
|
|
188
201
|
res_data = response.json()
|
|
189
|
-
|
|
190
|
-
span.set_attribute("tx_id", res_data["tx_id"])
|
|
191
|
-
span.end()
|
|
192
|
-
return AgentGuardReceipt(
|
|
202
|
+
receipt = AgentGuardReceipt(
|
|
193
203
|
tx_id=res_data["tx_id"],
|
|
194
204
|
consent_hash=res_data["consent_hash"],
|
|
195
205
|
audit_url=res_data["audit_url"],
|
|
196
|
-
data=res_data.get("data", {})
|
|
197
206
|
)
|
|
207
|
+
break
|
|
198
208
|
else:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
attempt_span.end(error=f"http_{response.status_code}: {error_data.get('message')}")
|
|
205
|
-
|
|
206
|
-
# Transient errors -> retry
|
|
207
|
-
if response.status_code in [502, 503, 504, 429]:
|
|
208
|
-
if attempt < max_attempts - 1:
|
|
209
|
-
delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
|
|
210
|
-
await asyncio.sleep(delay)
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
span.end(error=f"backend_rejection_{response.status_code}")
|
|
214
|
-
raise map_backend_error(response.status_code, error_data)
|
|
215
|
-
|
|
209
|
+
if response.status_code in [502, 503, 504, 429] and attempt < max_attempts - 1:
|
|
210
|
+
await asyncio.sleep(base_delay * (2 ** attempt))
|
|
211
|
+
continue
|
|
212
|
+
raise map_backend_error(response.status_code, response.json())
|
|
216
213
|
except httpx.RequestError as e:
|
|
217
|
-
attempt_span.end(error=str(e))
|
|
218
214
|
if attempt < max_attempts - 1:
|
|
219
|
-
|
|
220
|
-
await asyncio.sleep(delay)
|
|
215
|
+
await asyncio.sleep(base_delay * (2 ** attempt))
|
|
221
216
|
continue
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
raise TransportError(f"Backend unreachable: {e}")
|
|
218
|
+
|
|
219
|
+
if not receipt:
|
|
220
|
+
raise PaymentError("Payment failed or timed out.")
|
|
221
|
+
|
|
222
|
+
# Step C — Retry with Proof
|
|
223
|
+
async with httpx.AsyncClient(timeout=self.timeout) as fetch_client:
|
|
224
|
+
logger.info(f"Payment successful (TX: {receipt.tx_id}). Fetching data...")
|
|
225
|
+
proof_headers = {
|
|
226
|
+
"X-AgentGuard-Proof": receipt.tx_id,
|
|
227
|
+
"X-AgentGuard-Principal": self.wallet_address
|
|
228
|
+
}
|
|
229
|
+
try:
|
|
230
|
+
final_res = await fetch_client.get(resource_url, headers=proof_headers)
|
|
231
|
+
if final_res.status_code == 200:
|
|
232
|
+
receipt.data = final_res.json()
|
|
233
|
+
span.end()
|
|
234
|
+
return receipt
|
|
235
|
+
else:
|
|
236
|
+
span.end(error=f"Resource rejected proof: {final_res.status_code}")
|
|
237
|
+
raise PaymentError(f"Proof rejected by resource server: {final_res.status_code}")
|
|
224
238
|
except Exception as e:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
raise
|
|
239
|
+
span.end(error=str(e))
|
|
240
|
+
raise TransportError(f"Failed to fetch data with proof: {e}")
|
|
228
241
|
|
|
229
242
|
async def verify(self, tx_id: str, verify_onchain: bool = False, local_proof: Optional[Dict[str, Any]] = None) -> AuditProof:
|
|
230
243
|
"""
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Dict, Any
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
3
|
|
|
4
4
|
@dataclass
|
|
5
5
|
class AgentGuardReceipt:
|
|
6
6
|
"""
|
|
7
7
|
Structured response for a successful AgentGuard payment.
|
|
8
8
|
"""
|
|
9
|
-
tx_id: str
|
|
10
|
-
consent_hash: str
|
|
11
|
-
audit_url: str
|
|
9
|
+
tx_id: Optional[str]
|
|
10
|
+
consent_hash: Optional[str]
|
|
11
|
+
audit_url: Optional[str]
|
|
12
12
|
data: Dict[str, Any] = field(default_factory=dict)
|
|
13
13
|
|
|
14
14
|
@dataclass
|
{agentguard_python_sdk-0.1.2 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentguard-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A production-grade middleware for AI agents to perform on-chain payments and verifiable consent.
|
|
5
5
|
Author: AgentGuard Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.9
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: httpx>=0.27.0
|
|
10
10
|
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
11
12
|
Requires-Dist: pynacl>=1.5.0
|
|
12
13
|
Requires-Dist: py-algorand-sdk>=2.0.0
|
|
13
14
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentguard-python-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "A production-grade middleware for AI agents to perform on-chain payments and verifiable consent."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -15,6 +15,7 @@ authors = [
|
|
|
15
15
|
dependencies = [
|
|
16
16
|
"httpx>=0.27.0",
|
|
17
17
|
"pydantic>=2.0.0",
|
|
18
|
+
"pydantic-settings>=2.0.0",
|
|
18
19
|
"pynacl>=1.5.0",
|
|
19
20
|
"py-algorand-sdk>=2.0.0"
|
|
20
21
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|