agentguard-python-sdk 0.1.3__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.
Files changed (17) hide show
  1. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/PKG-INFO +1 -1
  2. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard/client.py +74 -66
  3. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard/types.py +4 -4
  4. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/PKG-INFO +1 -1
  5. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/pyproject.toml +1 -1
  6. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/README.md +0 -0
  7. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard/__init__.py +0 -0
  8. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard/auth.py +0 -0
  9. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard/config.py +0 -0
  10. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard/consent.py +0 -0
  11. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard/errors.py +0 -0
  12. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard/observability.py +0 -0
  13. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/SOURCES.txt +0 -0
  14. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/dependency_links.txt +0 -0
  15. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/requires.txt +0 -0
  16. {agentguard_python_sdk-0.1.3 → agentguard_python_sdk-0.2.0}/agentguard_python_sdk.egg-info/top_level.txt +0 -0
  17. {agentguard_python_sdk-0.1.3 → 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.1.3
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
@@ -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
- Asynchronously perform an on-chain payment and return a typed receipt.
108
+ End-to-end flow for accessing resources (Probe -> Pay -> Fetch).
109
109
 
110
- Args:
111
- resource_url: The URL/ID of the resource being accessed.
112
- purpose: The purpose of the data processing.
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
- # 1. Determine and Validate Amount
120
- if amount_algo is not None and microalgos is not None:
121
- raise ValueError("Provide either amount_algo or microalgos, not both.")
122
-
123
- if amount_algo is not None:
124
- actual_microalgos = parse_algo_amount_to_microalgos(amount_algo)
125
- elif microalgos is not None:
126
- if not isinstance(microalgos, int) or microalgos < 0:
127
- raise ValueError(f"microalgos must be a non-negative integer, got {microalgos}")
128
- actual_microalgos = microalgos
129
- else:
130
- raise ValueError("Either amount_algo or microalgos must be provided.")
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
- # 2. Idempotency & Replay Protection (PHASE C)
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 # Pass through for retry consistency
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
- attempt_span.end()
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
- try:
205
- error_data = response.json()
206
- except:
207
- error_data = {"message": response.text}
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
- delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
225
- await asyncio.sleep(delay)
215
+ await asyncio.sleep(base_delay * (2 ** attempt))
226
216
  continue
227
- span.end(error="transport_failure")
228
- raise TransportError(f"Could not connect to AgentGuard Backend: {e}")
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
- attempt_span.end(error=str(e))
231
- span.end(error="unexpected_sdk_error")
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
  """
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentguard-python-sdk
3
- Version: 0.1.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentguard-python-sdk"
7
- version = "0.1.3"
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"