agentguard-python-sdk 0.1.2__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 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
 
@@ -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
- self.wallet_address = wallet_address
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
- Asynchronously perform an on-chain payment and return a typed receipt.
108
+ End-to-end flow for accessing resources (Probe -> Pay -> Fetch).
104
109
 
105
- Args:
106
- resource_url: The URL/ID of the resource being accessed.
107
- purpose: The purpose of the data processing.
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
- # 1. Determine and Validate Amount
115
- if amount_algo is not None and microalgos is not None:
116
- raise ValueError("Provide either amount_algo or microalgos, not both.")
117
-
118
- if amount_algo is not None:
119
- actual_microalgos = parse_algo_amount_to_microalgos(amount_algo)
120
- elif microalgos is not None:
121
- if not isinstance(microalgos, int) or microalgos < 0:
122
- raise ValueError(f"microalgos must be a non-negative integer, got {microalgos}")
123
- actual_microalgos = microalgos
124
- else:
125
- 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")
126
150
 
127
- # 2. Idempotency & Replay Protection (PHASE C)
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 # Pass through for retry consistency
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
- attempt_span.end()
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
- try:
200
- error_data = response.json()
201
- except:
202
- error_data = {"message": response.text}
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
- delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
220
- await asyncio.sleep(delay)
215
+ await asyncio.sleep(base_delay * (2 ** attempt))
221
216
  continue
222
- span.end(error="transport_failure")
223
- 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}")
224
238
  except Exception as e:
225
- attempt_span.end(error=str(e))
226
- span.end(error="unexpected_sdk_error")
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
  """
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentguard-python-sdk
3
- Version: 0.1.2
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
 
@@ -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=B8ttu1_AcUFODeW27Q6jDHDyh1Yn1AdUWfBDs3Xg50w,17798
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=U1anjHwYfjFhbzWP-xkaLfPqCkpzJ4DtjOt5kP9bsLA,559
9
- agentguard_python_sdk-0.1.2.dist-info/METADATA,sha256=BO-xdHnBpkK7GoVCQnEzSObJQnHGyAsxmym8h591SVY,2503
10
- agentguard_python_sdk-0.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
- agentguard_python_sdk-0.1.2.dist-info/top_level.txt,sha256=rEW5sAnjxXRs63ja2psmsMRMJJfi_3EAxf7crl1nHyw,11
12
- agentguard_python_sdk-0.1.2.dist-info/RECORD,,
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,,