agentguard-python-sdk 0.1.3__py3-none-any.whl → 0.2.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.
- agentguard/client.py +74 -66
- agentguard/types.py +4 -4
- {agentguard_python_sdk-0.1.3.dist-info → agentguard_python_sdk-0.2.0.dist-info}/METADATA +1 -1
- {agentguard_python_sdk-0.1.3.dist-info → agentguard_python_sdk-0.2.0.dist-info}/RECORD +6 -6
- {agentguard_python_sdk-0.1.3.dist-info → agentguard_python_sdk-0.2.0.dist-info}/WHEEL +0 -0
- {agentguard_python_sdk-0.1.3.dist-info → agentguard_python_sdk-0.2.0.dist-info}/top_level.txt +0 -0
agentguard/client.py
CHANGED
|
@@ -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
|
|
|
@@ -105,48 +105,63 @@ class AgentGuardClient:
|
|
|
105
105
|
correlation_id: Optional[str] = None
|
|
106
106
|
) -> AgentGuardReceipt:
|
|
107
107
|
"""
|
|
108
|
-
|
|
108
|
+
End-to-end flow for accessing resources (Probe -> Pay -> Fetch).
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
amount_algo: The amount in ALGO as a string (e.g. '0.05').
|
|
114
|
-
microalgos: Alternatively, the exact amount in microAlgos (int).
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
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.
|
|
118
113
|
"""
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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")
|
|
131
150
|
|
|
132
|
-
#
|
|
133
|
-
# We generate these early so they can be persisted across retries if needed.
|
|
151
|
+
# Step B — Payment (Using Backend Relay)
|
|
134
152
|
idem_key = idempotency_key or str(uuid.uuid4())
|
|
135
153
|
act_nonce = nonce or str(uuid.uuid4())
|
|
136
154
|
act_correlation_id = correlation_id or str(idem_key)
|
|
137
155
|
|
|
138
|
-
# 3. Generate Consent Proof (SHA256 of metadata including microalgos)
|
|
139
|
-
# If timestamp is provided (from a previous retry), we reuse it.
|
|
140
156
|
consent_hash, act_timestamp = generate_consent_proof(
|
|
141
157
|
principal_id=self.wallet_address,
|
|
142
158
|
resource_url=resource_url,
|
|
143
159
|
purpose=purpose,
|
|
144
160
|
nonce=act_nonce,
|
|
145
161
|
microalgos=actual_microalgos,
|
|
146
|
-
timestamp=timestamp
|
|
162
|
+
timestamp=timestamp
|
|
147
163
|
)
|
|
148
164
|
|
|
149
|
-
# 4. Payload Construction
|
|
150
165
|
payload = {
|
|
151
166
|
"resource_url": resource_url,
|
|
152
167
|
"microalgos": actual_microalgos,
|
|
@@ -164,7 +179,6 @@ class AgentGuardClient:
|
|
|
164
179
|
"X-Correlation-ID": act_correlation_id
|
|
165
180
|
}
|
|
166
181
|
|
|
167
|
-
# 5. Cryptographic Signing (Priority 5)
|
|
168
182
|
if self.private_key:
|
|
169
183
|
signature = auth.sign_request(payload, self.private_key)
|
|
170
184
|
headers.update({
|
|
@@ -173,63 +187,57 @@ class AgentGuardClient:
|
|
|
173
187
|
"X-AgentGuard-Key-Id": self.wallet_address,
|
|
174
188
|
"X-AgentGuard-Nonce": act_nonce
|
|
175
189
|
})
|
|
176
|
-
# Priority 6: Signing verified via Span attributes if needed
|
|
177
190
|
|
|
178
|
-
# 6. Request with Retries
|
|
179
191
|
span = start_span("sdk.pay_request", act_correlation_id)
|
|
180
|
-
span.set_attribute("wallet", self.wallet_address)
|
|
181
|
-
span.set_attribute("microalgos", actual_microalgos)
|
|
182
|
-
span.set_attribute("idempotency_key", idem_key)
|
|
183
|
-
|
|
184
192
|
max_attempts = self.config.max_retries
|
|
185
193
|
base_delay = self.config.retry_base_delay
|
|
186
194
|
|
|
195
|
+
receipt = None
|
|
187
196
|
for attempt in range(max_attempts):
|
|
188
|
-
attempt_span = start_span(f"sdk.request_attempt_{attempt + 1}", act_correlation_id, parent_id=span.span_id)
|
|
189
197
|
try:
|
|
190
198
|
response = await self.async_client.post(f"{self.base_url}/pay", json=payload, headers=headers)
|
|
191
199
|
|
|
192
200
|
if response.status_code == 200:
|
|
193
201
|
res_data = response.json()
|
|
194
|
-
|
|
195
|
-
span.set_attribute("tx_id", res_data["tx_id"])
|
|
196
|
-
span.end()
|
|
197
|
-
return AgentGuardReceipt(
|
|
202
|
+
receipt = AgentGuardReceipt(
|
|
198
203
|
tx_id=res_data["tx_id"],
|
|
199
204
|
consent_hash=res_data["consent_hash"],
|
|
200
205
|
audit_url=res_data["audit_url"],
|
|
201
|
-
data=res_data.get("data", {})
|
|
202
206
|
)
|
|
207
|
+
break
|
|
203
208
|
else:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
attempt_span.end(error=f"http_{response.status_code}: {error_data.get('message')}")
|
|
210
|
-
|
|
211
|
-
# Transient errors -> retry
|
|
212
|
-
if response.status_code in [502, 503, 504, 429]:
|
|
213
|
-
if attempt < max_attempts - 1:
|
|
214
|
-
delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
|
|
215
|
-
await asyncio.sleep(delay)
|
|
216
|
-
continue
|
|
217
|
-
|
|
218
|
-
span.end(error=f"backend_rejection_{response.status_code}")
|
|
219
|
-
raise map_backend_error(response.status_code, error_data)
|
|
220
|
-
|
|
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())
|
|
221
213
|
except httpx.RequestError as e:
|
|
222
|
-
attempt_span.end(error=str(e))
|
|
223
214
|
if attempt < max_attempts - 1:
|
|
224
|
-
|
|
225
|
-
await asyncio.sleep(delay)
|
|
215
|
+
await asyncio.sleep(base_delay * (2 ** attempt))
|
|
226
216
|
continue
|
|
227
|
-
|
|
228
|
-
|
|
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}")
|
|
229
238
|
except Exception as e:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
raise
|
|
239
|
+
span.end(error=str(e))
|
|
240
|
+
raise TransportError(f"Failed to fetch data with proof: {e}")
|
|
233
241
|
|
|
234
242
|
async def verify(self, tx_id: str, verify_onchain: bool = False, local_proof: Optional[Dict[str, Any]] = None) -> AuditProof:
|
|
235
243
|
"""
|
agentguard/types.py
CHANGED
|
@@ -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
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
agentguard/__init__.py,sha256=gCRF3pf7qpWf6_VVwgl2X1A4apOSCx7zqd8g2r3fe44,130
|
|
2
2
|
agentguard/auth.py,sha256=PBb7l-x5MGTv_iwilK6LTi5Yhr76qcZL30p1-J80lDg,1720
|
|
3
|
-
agentguard/client.py,sha256=
|
|
3
|
+
agentguard/client.py,sha256=iQW4rseOh2LaRf4SIN4TsBEFepcy4gC7DyLNe4LAmbw,18443
|
|
4
4
|
agentguard/config.py,sha256=96xRt8FCVoRS1jVzNt4i4hzzP1NorT2NrAWPE_5XbCU,1032
|
|
5
5
|
agentguard/consent.py,sha256=iTVCf4FaFlsJQF0R63HtbgYTtDUPzRCVPULz-cmX2nA,2706
|
|
6
6
|
agentguard/errors.py,sha256=rEFQI4mrbccXRyjVYm_pZwHWr4aolcg8e5KMCIzrOdE,4181
|
|
7
7
|
agentguard/observability.py,sha256=2xSySYDtPhtiNUKXH0zQ0N4WG7j3ssQqPtOBDK6V2EQ,1706
|
|
8
|
-
agentguard/types.py,sha256=
|
|
9
|
-
agentguard_python_sdk-0.
|
|
10
|
-
agentguard_python_sdk-0.
|
|
11
|
-
agentguard_python_sdk-0.
|
|
12
|
-
agentguard_python_sdk-0.
|
|
8
|
+
agentguard/types.py,sha256=HpxtMCzV6x-TIsv99gurLwd53DqtC-ul_IMKPuzps2Y,599
|
|
9
|
+
agentguard_python_sdk-0.2.0.dist-info/METADATA,sha256=T6mwNf3cIpwXBtJVWOgRBeT1IImBMn5CI14uJHA71_I,2544
|
|
10
|
+
agentguard_python_sdk-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
agentguard_python_sdk-0.2.0.dist-info/top_level.txt,sha256=rEW5sAnjxXRs63ja2psmsMRMJJfi_3EAxf7crl1nHyw,11
|
|
12
|
+
agentguard_python_sdk-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
{agentguard_python_sdk-0.1.3.dist-info → agentguard_python_sdk-0.2.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|