agentguard-python-sdk 0.1.1__tar.gz → 0.1.3__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 (21) hide show
  1. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/PKG-INFO +6 -3
  2. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/README.md +54 -54
  3. agentguard_python_sdk-0.1.3/agentguard/__init__.py +4 -0
  4. agentguard_python_sdk-0.1.3/agentguard/auth.py +54 -0
  5. agentguard_python_sdk-0.1.3/agentguard/client.py +412 -0
  6. agentguard_python_sdk-0.1.3/agentguard/config.py +34 -0
  7. agentguard_python_sdk-0.1.3/agentguard/consent.py +79 -0
  8. agentguard_python_sdk-0.1.3/agentguard/errors.py +83 -0
  9. agentguard_python_sdk-0.1.3/agentguard/observability.py +52 -0
  10. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/agentguard/types.py +25 -25
  11. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/agentguard_python_sdk.egg-info/PKG-INFO +6 -3
  12. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/agentguard_python_sdk.egg-info/SOURCES.txt +4 -0
  13. agentguard_python_sdk-0.1.3/agentguard_python_sdk.egg-info/requires.txt +5 -0
  14. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/pyproject.toml +25 -22
  15. agentguard_python_sdk-0.1.1/agentguard/__init__.py +0 -3
  16. agentguard_python_sdk-0.1.1/agentguard/client.py +0 -126
  17. agentguard_python_sdk-0.1.1/agentguard/consent.py +0 -58
  18. agentguard_python_sdk-0.1.1/agentguard_python_sdk.egg-info/requires.txt +0 -2
  19. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/agentguard_python_sdk.egg-info/dependency_links.txt +0 -0
  20. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/agentguard_python_sdk.egg-info/top_level.txt +0 -0
  21. {agentguard_python_sdk-0.1.1 → agentguard_python_sdk-0.1.3}/setup.cfg +0 -0
@@ -1,13 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentguard-python-sdk
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A production-grade middleware for AI agents to perform on-chain payments and verifiable consent.
5
5
  Author: AgentGuard Team
6
- License: MIT
6
+ License-Expression: MIT
7
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
12
+ Requires-Dist: pynacl>=1.5.0
13
+ Requires-Dist: py-algorand-sdk>=2.0.0
11
14
 
12
15
  # AgentGuard SDK
13
16
 
@@ -34,7 +37,7 @@ async def main():
34
37
  # 2. Perform a secure payment (includes automatic DPDP consent hashing)
35
38
  receipt = await client.pay_and_fetch(
36
39
  resource_url="https://api.stock.com/reliance",
37
- amount_algo=0.05,
40
+ amount_algo="0.05",
38
41
  purpose="Financial Analysis"
39
42
  )
40
43
  print(f"Payment Successful! TX ID: {receipt.tx_id}")
@@ -1,54 +1,54 @@
1
- # AgentGuard SDK
2
-
3
- AgentGuard is a production-grade middleware that enables AI agents to perform on-chain payments and generate verifiable consent proofs in compliance with DPDP (Digital Personal Data Protection) standards.
4
-
5
- By isolating private keys in a hardened **MCP Server (Vault)** and using the **AgentGuard Backend (Dispatcher)** as a secure proxy, the SDK allows developers to add monetization and compliance to their agents with zero blockchain complexity.
6
-
7
- ## Installation
8
-
9
- ```bash
10
- pip install agentguard-python-sdk
11
- ```
12
-
13
- ## Quick Start (3-Line Usage)
14
-
15
- ```python
16
- import asyncio
17
- from agentguard import AgentGuardClient
18
-
19
- async def main():
20
- # 1. Initialize the client
21
- async with AgentGuardClient(wallet_address="YOUR_WALLET_ADDRESS") as client:
22
-
23
- # 2. Perform a secure payment (includes automatic DPDP consent hashing)
24
- receipt = await client.pay_and_fetch(
25
- resource_url="https://api.stock.com/reliance",
26
- amount_algo=0.05,
27
- purpose="Financial Analysis"
28
- )
29
- print(f"Payment Successful! TX ID: {receipt.tx_id}")
30
-
31
- # 3. Verify compliance audit trail
32
- proof = await client.verify(receipt.tx_id)
33
- print(f"On-Chain Verified: {proof.verified}")
34
-
35
- if __name__ == "__main__":
36
- asyncio.run(main())
37
- ```
38
-
39
- ## Architecture
40
-
41
- 1. **SDK**: Generates UUID nonces and SHA256 consent hashes.
42
- 2. **Backend**: Proxies requests to the internal vault and maintains audit records.
43
- 3. **MCP Server (Vault)**: A hardened, isolated environment that signs transactions using Algorand.
44
- 4. **Algorand Blockchain**: Provides the irrefutable proof-of-consent and payment settlement.
45
-
46
- ## Features
47
-
48
- - **Async First**: Built on `httpx` for high-performance agentic loops.
49
- - **Zero-Trust**: Private keys never leave the vault.
50
- - **DPDP Compliant**: Automatic cryptographic logging of purpose-bound consent.
51
- - **Simple Audit**: Direct links to blockchain explorers for every transaction.
52
-
53
- ---
54
- © 2026 AgentGuard Team. Built for the Algorand Ecosystem.
1
+ # AgentGuard SDK
2
+
3
+ AgentGuard is a production-grade middleware that enables AI agents to perform on-chain payments and generate verifiable consent proofs in compliance with DPDP (Digital Personal Data Protection) standards.
4
+
5
+ By isolating private keys in a hardened **MCP Server (Vault)** and using the **AgentGuard Backend (Dispatcher)** as a secure proxy, the SDK allows developers to add monetization and compliance to their agents with zero blockchain complexity.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install agentguard-python-sdk
11
+ ```
12
+
13
+ ## Quick Start (3-Line Usage)
14
+
15
+ ```python
16
+ import asyncio
17
+ from agentguard import AgentGuardClient
18
+
19
+ async def main():
20
+ # 1. Initialize the client
21
+ async with AgentGuardClient(wallet_address="YOUR_WALLET_ADDRESS") as client:
22
+
23
+ # 2. Perform a secure payment (includes automatic DPDP consent hashing)
24
+ receipt = await client.pay_and_fetch(
25
+ resource_url="https://api.stock.com/reliance",
26
+ amount_algo="0.05",
27
+ purpose="Financial Analysis"
28
+ )
29
+ print(f"Payment Successful! TX ID: {receipt.tx_id}")
30
+
31
+ # 3. Verify compliance audit trail
32
+ proof = await client.verify(receipt.tx_id)
33
+ print(f"On-Chain Verified: {proof.verified}")
34
+
35
+ if __name__ == "__main__":
36
+ asyncio.run(main())
37
+ ```
38
+
39
+ ## Architecture
40
+
41
+ 1. **SDK**: Generates UUID nonces and SHA256 consent hashes.
42
+ 2. **Backend**: Proxies requests to the internal vault and maintains audit records.
43
+ 3. **MCP Server (Vault)**: A hardened, isolated environment that signs transactions using Algorand.
44
+ 4. **Algorand Blockchain**: Provides the irrefutable proof-of-consent and payment settlement.
45
+
46
+ ## Features
47
+
48
+ - **Async First**: Built on `httpx` for high-performance agentic loops.
49
+ - **Zero-Trust**: Private keys never leave the vault.
50
+ - **DPDP Compliant**: Automatic cryptographic logging of purpose-bound consent.
51
+ - **Simple Audit**: Direct links to blockchain explorers for every transaction.
52
+
53
+ ---
54
+ © 2026 AgentGuard Team. Built for the Algorand Ecosystem.
@@ -0,0 +1,4 @@
1
+ from .client import AgentGuardClient
2
+ from .config import AgentGuardConfig
3
+
4
+ __all__ = ["AgentGuardClient", "AgentGuardConfig"]
@@ -0,0 +1,54 @@
1
+ import json
2
+ import unicodedata
3
+ import hashlib
4
+ import base64
5
+ import time
6
+ from typing import Dict, Any
7
+ import nacl.signing
8
+ import nacl.encoding
9
+
10
+ def canonicalize_payload(payload: Dict[str, Any]) -> bytes:
11
+ """
12
+ Deterministic canonicalization (Priority 5).
13
+ 1. NFC Normalize all strings recursively.
14
+ 2. Sort keys.
15
+ 3. No whitespace (separators=(',', ':')).
16
+ 4. UTF-8 encoding.
17
+ """
18
+ def normalize_rec(obj):
19
+ if isinstance(obj, str):
20
+ return unicodedata.normalize("NFC", obj)
21
+ if isinstance(obj, dict):
22
+ return {normalize_rec(k): normalize_rec(v) for k, v in obj.items()}
23
+ if isinstance(obj, list):
24
+ return [normalize_rec(i) for i in obj]
25
+ return obj
26
+
27
+ norm_payload = normalize_rec(payload)
28
+ canonical_str = json.dumps(
29
+ norm_payload,
30
+ sort_keys=True,
31
+ separators=(",", ":"),
32
+ ensure_ascii=False
33
+ )
34
+ return canonical_str.encode("utf-8")
35
+
36
+ def sign_request(payload: Dict[str, Any], private_key_b64: str) -> str:
37
+ """
38
+ Signs a canonical payload using ED25519.
39
+ Expects private_key_b64 (32-byte seed or 64-byte expanded key in b64).
40
+ """
41
+ # Decoding private key
42
+ # Most Algorand private keys are the 32-byte seed.
43
+ # NaCl signing keys are initialized from the 32-byte seed.
44
+ seed = base64.b64decode(private_key_b64)
45
+ if len(seed) > 32:
46
+ seed = seed[:32] # Algorand SK is often seed + pubkey
47
+
48
+ signing_key = nacl.signing.SigningKey(seed)
49
+
50
+ canonical_bytes = canonicalize_payload(payload)
51
+ signature = signing_key.sign(canonical_bytes)
52
+
53
+ # Return detached signature in base64
54
+ return base64.b64encode(signature.signature).decode("utf-8")
@@ -0,0 +1,412 @@
1
+ import httpx
2
+ import uuid
3
+ import logging
4
+ import re
5
+ import warnings
6
+ import asyncio
7
+ import random
8
+ import time
9
+ import json
10
+ import hashlib
11
+ import base64
12
+ import urllib.parse
13
+ from decimal import Decimal, InvalidOperation
14
+ from typing import Optional, Dict, Any
15
+ from .consent import generate_consent_proof
16
+ from . import auth
17
+ from .types import AgentGuardReceipt, AuditProof
18
+ from .errors import map_backend_error, TransportError, AgentGuardError, VerificationError
19
+ from .config import AgentGuardConfig
20
+ from .observability import start_span
21
+
22
+ logger = logging.getLogger("agentguard-sdk")
23
+
24
+ def parse_algo_amount_to_microalgos(amount: str) -> int:
25
+ """
26
+ Parses a string ALGO amount into integer microAlgos.
27
+
28
+ STRICTLY rejects:
29
+ - Non-string inputs
30
+ - Scientific notation (e.g. 1e-6)
31
+ - Negative values
32
+ - More than 6 decimal places
33
+ - Unnecessary leading zeros (e.g. 00.1)
34
+
35
+ ARCHITECTURAL BOUNDARY:
36
+ - Decimal is used ONLY here for the one-time string-to-int conversion.
37
+ - The rest of the system MUST use integer microalgos.
38
+ """
39
+ if not isinstance(amount, str):
40
+ raise ValueError(f"Amount must be a string (e.g. '0.25'), got {type(amount).__name__}")
41
+
42
+ # Strict Regex: (0 OR [1-9] followed by digits) followed optionally by a decimal and up to 6 digits
43
+ # Blocks: '00.1', '.5', '1e-1', '01'
44
+ if not re.match(r"^(0|[1-9]\d*)(\.\d{1,6})?$", amount):
45
+ raise ValueError(f"Invalid ALGO format: '{amount}'. Use '0.05' not '00.05' or '1e-2'.")
46
+
47
+ try:
48
+ # Atomic conversion at the trust boundary
49
+ d_amount = Decimal(amount)
50
+ microalgos = int(d_amount * Decimal("1000000"))
51
+ return microalgos
52
+ except (InvalidOperation, ValueError) as e:
53
+ raise ValueError(f"Conversion failed: {e}")
54
+
55
+ class AgentGuardClient:
56
+ def __init__(
57
+ self,
58
+ wallet_address: Optional[str] = None,
59
+ config: Optional[AgentGuardConfig] = None,
60
+ private_key: str = None,
61
+ backend_url: Optional[str] = None,
62
+ indexer_url: Optional[str] = None,
63
+ principal_id: Optional[str] = None
64
+ ):
65
+ """
66
+ Initialize the AgentGuard client with a structured config object or direct overrides.
67
+ """
68
+ self.config = config or AgentGuardConfig()
69
+
70
+ # Apply direct overrides if provided
71
+ if backend_url:
72
+ self.config.backend_url = backend_url
73
+ if indexer_url:
74
+ self.config.indexer_url = indexer_url
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
+
81
+ self.base_url = self.config.backend_url.rstrip("/")
82
+ self.private_key = private_key
83
+
84
+ # Priority 6: Segmented Timeouts injected from Config
85
+ self.timeout = httpx.Timeout(
86
+ connect=self.config.timeout_connect,
87
+ read=self.config.timeout_read,
88
+ write=self.config.timeout_write,
89
+ pool=self.config.timeout_pool
90
+ )
91
+ self.async_client = httpx.AsyncClient(timeout=self.timeout)
92
+
93
+ # Configure logging based on config
94
+ logging.getLogger("agentguard-sdk").setLevel(self.config.logging_level)
95
+
96
+ async def pay_and_fetch(
97
+ self,
98
+ resource_url: str,
99
+ purpose: str,
100
+ amount_algo: Optional[str] = None,
101
+ microalgos: Optional[int] = None,
102
+ idempotency_key: Optional[str] = None,
103
+ nonce: Optional[str] = None,
104
+ timestamp: Optional[int] = None,
105
+ correlation_id: Optional[str] = None
106
+ ) -> AgentGuardReceipt:
107
+ """
108
+ Asynchronously perform an on-chain payment and return a typed receipt.
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.
118
+ """
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.")
131
+
132
+ # 2. Idempotency & Replay Protection (PHASE C)
133
+ # We generate these early so they can be persisted across retries if needed.
134
+ idem_key = idempotency_key or str(uuid.uuid4())
135
+ act_nonce = nonce or str(uuid.uuid4())
136
+ act_correlation_id = correlation_id or str(idem_key)
137
+
138
+ # 3. Generate Consent Proof (SHA256 of metadata including microalgos)
139
+ # If timestamp is provided (from a previous retry), we reuse it.
140
+ consent_hash, act_timestamp = generate_consent_proof(
141
+ principal_id=self.wallet_address,
142
+ resource_url=resource_url,
143
+ purpose=purpose,
144
+ nonce=act_nonce,
145
+ microalgos=actual_microalgos,
146
+ timestamp=timestamp # Pass through for retry consistency
147
+ )
148
+
149
+ # 4. Payload Construction
150
+ payload = {
151
+ "resource_url": resource_url,
152
+ "microalgos": actual_microalgos,
153
+ "purpose": purpose,
154
+ "principal_id": self.wallet_address,
155
+ "consent_hash": consent_hash,
156
+ "nonce": act_nonce,
157
+ "timestamp": act_timestamp,
158
+ "idempotency_key": idem_key,
159
+ "correlation_id": act_correlation_id
160
+ }
161
+
162
+ headers = {
163
+ "X-Idempotency-Key": idem_key,
164
+ "X-Correlation-ID": act_correlation_id
165
+ }
166
+
167
+ # 5. Cryptographic Signing (Priority 5)
168
+ if self.private_key:
169
+ signature = auth.sign_request(payload, self.private_key)
170
+ headers.update({
171
+ "X-AgentGuard-Signature": signature,
172
+ "X-AgentGuard-Timestamp": str(act_timestamp),
173
+ "X-AgentGuard-Key-Id": self.wallet_address,
174
+ "X-AgentGuard-Nonce": act_nonce
175
+ })
176
+ # Priority 6: Signing verified via Span attributes if needed
177
+
178
+ # 6. Request with Retries
179
+ 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
+ max_attempts = self.config.max_retries
185
+ base_delay = self.config.retry_base_delay
186
+
187
+ 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
+ try:
190
+ response = await self.async_client.post(f"{self.base_url}/pay", json=payload, headers=headers)
191
+
192
+ if response.status_code == 200:
193
+ res_data = response.json()
194
+ attempt_span.end()
195
+ span.set_attribute("tx_id", res_data["tx_id"])
196
+ span.end()
197
+ return AgentGuardReceipt(
198
+ tx_id=res_data["tx_id"],
199
+ consent_hash=res_data["consent_hash"],
200
+ audit_url=res_data["audit_url"],
201
+ data=res_data.get("data", {})
202
+ )
203
+ 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
+
221
+ except httpx.RequestError as e:
222
+ attempt_span.end(error=str(e))
223
+ if attempt < max_attempts - 1:
224
+ delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
225
+ await asyncio.sleep(delay)
226
+ continue
227
+ span.end(error="transport_failure")
228
+ raise TransportError(f"Could not connect to AgentGuard Backend: {e}")
229
+ except Exception as e:
230
+ attempt_span.end(error=str(e))
231
+ span.end(error="unexpected_sdk_error")
232
+ raise
233
+
234
+ async def verify(self, tx_id: str, verify_onchain: bool = False, local_proof: Optional[Dict[str, Any]] = None) -> AuditProof:
235
+ """
236
+ Asynchronously verify a transaction against the blockchain audit trail.
237
+
238
+ If verify_onchain is True, the SDK performs an independent check against the
239
+ Algorand indexer and re-computes the proof hash locally.
240
+
241
+ If local_proof is provided, the SDK uses it to recompute the hash even if the
242
+ backend has no record of the transaction (Split-Brain Recovery).
243
+ """
244
+ try:
245
+ # 1. Fetch Backend's Assertion (Metadata + Proof)
246
+ res_data = {}
247
+ backend_failed = False
248
+ try:
249
+ response = await self.async_client.get(f"{self.base_url}/verify/{tx_id}")
250
+ response.raise_for_status()
251
+ res_data = response.json()
252
+ except Exception as e:
253
+ if not verify_onchain:
254
+ # In legacy mode, we must have the backend response
255
+ raise
256
+ logger.warning(f"Backend lookup for {tx_id} failed: {e}. Attempting independent recovery.")
257
+ backend_failed = True
258
+
259
+ # 2. Independent Logic
260
+ if verify_onchain:
261
+ logger.info(f"PERFORMING INDEPENDENT ON-CHAIN VERIFICATION FOR {tx_id}")
262
+
263
+ # A. Recompute Hash locally
264
+ # Content source: Backend provided proof OR user provided local_proof
265
+ canonical_json = res_data.get("canonical_json")
266
+
267
+ if not canonical_json and local_proof:
268
+ # [Priority 8 BONUS] Reconstruct canonical JSON from raw local proof
269
+ from .auth import canonicalize_payload
270
+ canonical_bytes = canonicalize_payload(local_proof)
271
+ canonical_json = canonical_bytes.decode("utf-8")
272
+
273
+ if not canonical_json:
274
+ reason = "Backend missing proof" if not backend_failed else "Backend unreachable"
275
+ raise VerificationError(f"{reason} and no local_proof provided for recovery.")
276
+
277
+ local_hash = hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()
278
+
279
+ # B. Interrogate Blockchain directly
280
+ on_chain_hash = await self._fetch_on_chain_proof(tx_id)
281
+
282
+ # C. Compare
283
+ if local_hash != on_chain_hash:
284
+ # [SECURITY BOUNDARY] The Backend is lying.
285
+ logger.error(f"TRUSTLESS VERIFICATION FAILED: Local Hash {local_hash} != On-chain Hash {on_chain_hash}")
286
+ return AuditProof(
287
+ verified=False,
288
+ tx_id=tx_id,
289
+ principal_id=res_data.get("principal_id", "N/A"),
290
+ consent_hash=local_hash,
291
+ on_chain_hash=on_chain_hash,
292
+ explorer_url=self.config.backend_url, # Placeholder
293
+ message="CRITICAL FAILURE: Local proof does not match blockchain evidence."
294
+ )
295
+
296
+ logger.info("TRUSTLESS VERIFICATION SUCCESS: Blockchain evidence matches backend assertion.")
297
+ return AuditProof(
298
+ verified=True,
299
+ tx_id=tx_id,
300
+ principal_id=res_data.get("principal_id", "N/A"),
301
+ consent_hash=local_hash,
302
+ on_chain_hash=on_chain_hash,
303
+ explorer_url=res_data.get("explorer_url", ""),
304
+ message="Trustless independent verification successful."
305
+ )
306
+
307
+ # Legacy/Backend-trusting behavior
308
+ return AuditProof(
309
+ verified=res_data["verified"],
310
+ tx_id=res_data["tx_id"],
311
+ principal_id=res_data["principal_id"],
312
+ consent_hash=res_data["consent_hash"],
313
+ on_chain_hash=res_data["on_chain_hash"],
314
+ explorer_url=res_data["explorer_url"],
315
+ message=res_data["message"]
316
+ )
317
+ except httpx.HTTPStatusError as e:
318
+ try:
319
+ error_data = e.response.json()
320
+ except Exception:
321
+ error_data = {"message": e.response.text, "code": "ERR_UNKNOWN"}
322
+ raise map_backend_error(e.response.status_code, error_data, original_exception=e) from e
323
+ except httpx.RequestError as e:
324
+ raise TransportError(f"Verification transport failed: {e}", original_exception=e) from e
325
+ except Exception as e:
326
+ if isinstance(e, AgentGuardError): raise
327
+ raise VerificationError(f"Independent verification failed: {str(e)}", original_exception=e)
328
+
329
+ async def _fetch_on_chain_proof(self, tx_id: str) -> str:
330
+ """
331
+ Directly queries the Algorand Indexer to extract the proof hash.
332
+ Supports atomic group detection.
333
+ """
334
+ indexer_url = self.config.indexer_url.rstrip("/")
335
+ try:
336
+ # 1. Fetch TX
337
+ resp = await self.async_client.get(f"{indexer_url}/transactions/{tx_id}")
338
+ resp.raise_for_status()
339
+ tx_data = resp.json().get("transaction", {})
340
+
341
+ # 2. Extract proof or hunt in group
342
+ relevant_txn = tx_data
343
+ group_id = tx_data.get("group")
344
+
345
+ if group_id and not tx_data.get("application-transaction"):
346
+ # If this was one part of a group (like the payment part), find the App call
347
+ import urllib.parse
348
+ safe_group = urllib.parse.quote(group_id)
349
+ g_resp = await self.async_client.get(f"{indexer_url}/transactions?group-id={safe_group}")
350
+ if g_resp.status_code == 200:
351
+ group_txns = g_resp.json().get("transactions", [])
352
+ for g_tx in group_txns:
353
+ if g_tx.get("application-transaction"):
354
+ relevant_txn = g_tx
355
+ break
356
+
357
+ app_txn = relevant_txn.get("application-transaction", {})
358
+ args = app_txn.get("application-args", [])
359
+
360
+ if not args:
361
+ raise VerificationError("Blockchain record found but no AppArgs proof exists.")
362
+
363
+ # Decode proof (Arg 0 is Sha256 hex as base64-encoded bytes on wire)
364
+ on_chain_hash_hex = base64.b64decode(args[0]).decode("utf-8")
365
+ return on_chain_hash_hex
366
+
367
+ except httpx.HTTPStatusError as e:
368
+ if e.response.status_code == 404:
369
+ raise VerificationError(f"Transaction {tx_id} not found on blockchain indexer.")
370
+ raise TransportError(f"Indexer connection failed: {e.response.status_code}")
371
+ except Exception as e:
372
+ raise VerificationError(f"Failed to extract blockchain proof: {str(e)}")
373
+
374
+ async def register_budget(self, limit_algo: Optional[str] = None, limit_microalgos: Optional[int] = None) -> Dict[str, Any]:
375
+ """
376
+ Convenience method to register/update the user's budget.
377
+ """
378
+ if limit_algo is not None:
379
+ actual_limit = parse_algo_amount_to_microalgos(limit_algo)
380
+ elif limit_microalgos is not None:
381
+ actual_limit = limit_microalgos
382
+ else:
383
+ raise ValueError("Either limit_algo or limit_microalgos must be provided.")
384
+
385
+ payload = {
386
+ "wallet_address": self.wallet_address,
387
+ "budget_microalgos": actual_limit
388
+ }
389
+ try:
390
+ response = await self.async_client.post(f"{self.base_url}/user/register", json=payload)
391
+ response.raise_for_status()
392
+ return response.json()
393
+ except httpx.HTTPStatusError as e:
394
+ try:
395
+ error_data = e.response.json()
396
+ except Exception:
397
+ error_data = {"message": e.response.text, "code": "ERR_UNKNOWN"}
398
+ raise map_backend_error(e.response.status_code, error_data, original_exception=e) from e
399
+ except httpx.RequestError as e:
400
+ raise TransportError(f"Budget registration transport failed: {e}", original_exception=e) from e
401
+
402
+ async def close(self):
403
+ """Close the underlying HTTP client."""
404
+ await self.async_client.aclose()
405
+
406
+ async def __aenter__(self):
407
+ return self
408
+
409
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
410
+ await self.close()
411
+
412
+ # Removed _log_event because it's replaced by Span system
@@ -0,0 +1,34 @@
1
+ from pydantic_settings import BaseSettings
2
+ from pydantic import Field
3
+ from typing import Optional
4
+
5
+ class AgentGuardConfig(BaseSettings):
6
+ """
7
+ Typed configuration for the AgentGuard SDK.
8
+ Supports environment variable overrides with AGENTGUARD_ prefix.
9
+ """
10
+ environment: str = Field("testnet", pattern="^(local|development|testnet|production)$")
11
+ backend_url: str = Field("https://agentguard-backend.onrender.com")
12
+
13
+ # Transport Policy
14
+ timeout_connect: float = 5.0
15
+ timeout_read: float = 30.0
16
+ timeout_write: float = 10.0
17
+ timeout_pool: float = 5.0
18
+
19
+ # Retry Policy
20
+ max_retries: int = 3
21
+ retry_base_delay: float = 1.0
22
+
23
+ # Observability
24
+ logging_level: str = "INFO"
25
+ tracing_enabled: bool = True
26
+
27
+ # [Priority 8] Trustless Verification
28
+ indexer_url: str = Field("https://testnet-idx.algonode.cloud/v2")
29
+ verify_timeout: float = 30.0
30
+ verify_max_retries: int = 3
31
+
32
+ class Config:
33
+ env_prefix = "AGENTGUARD_"
34
+ case_sensitive = False
@@ -0,0 +1,79 @@
1
+ import time
2
+ import json
3
+ import hashlib
4
+ import logging
5
+ import unicodedata
6
+ from typing import Dict, Any
7
+
8
+ # Configure logging
9
+ logger = logging.getLogger("agentguard-sdk")
10
+
11
+ def generate_consent(
12
+ principal_id: str,
13
+ purpose: str,
14
+ item: str,
15
+ nonce: str,
16
+ microalgos: int,
17
+ data_fiduciary: str = "AlgoBharat Agentic Node",
18
+ timestamp: int = None
19
+ ) -> Dict[str, Any]:
20
+ """
21
+ Creates a user consent object dictionary containing DPDP compliant fields.
22
+ Includes microalgos (int) and hash_version for future-proofing.
23
+
24
+ Representation Rule:
25
+ - Internal: microalgos (int) is the single source of truth.
26
+ - Strings: All metadata is normalized to NFC.
27
+ """
28
+ if timestamp is None:
29
+ timestamp = int(time.time())
30
+
31
+ # Normalize strings to NFC at the creation boundary
32
+ consent_obj = {
33
+ "principal_id": unicodedata.normalize("NFC", principal_id),
34
+ "data_fiduciary": unicodedata.normalize("NFC", data_fiduciary),
35
+ "purpose": unicodedata.normalize("NFC", purpose),
36
+ "item": unicodedata.normalize("NFC", item),
37
+ "microalgos": microalgos,
38
+ "timestamp": timestamp,
39
+ "nonce": nonce,
40
+ "hash_version": 2 # Current Refactor Version
41
+ }
42
+
43
+ logger.info(f"Generated DPDP consent (v2) for principal={principal_id} at {timestamp}")
44
+ return consent_obj
45
+
46
+ def hash_consent(consent: Dict[str, Any]) -> str:
47
+ """
48
+ Canonical hashing implementation.
49
+ MUST REMAINT SYNCED with Backend and MCP Server.
50
+
51
+ Rules:
52
+ 1. sort_keys=True
53
+ 2. separators=(',', ':')
54
+ 3. ensure_ascii=False
55
+ 4. Encoding: utf-8
56
+ """
57
+ # Convert consent dict to JSON string with strict canonical form:
58
+ # - Sorted keys
59
+ # - No spaces (separators=(",", ":"))
60
+ # Use separators=(",", ":") and ensure_ascii=False for identical bytes across boundaries
61
+ consent_json = json.dumps(consent, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
62
+ consent_hash = hashlib.sha256(consent_json.encode("utf-8")).hexdigest()
63
+
64
+ logger.info(f"Consent hash generated: {consent_hash}")
65
+ return consent_hash
66
+
67
+ def generate_consent_proof(principal_id: str, resource_url: str, purpose: str, nonce: str, microalgos: int, timestamp: int = None) -> tuple[str, int]:
68
+ """
69
+ High-level helper to generate a consent hash and return the timestamp used.
70
+ """
71
+ consent = generate_consent(
72
+ principal_id=principal_id,
73
+ purpose=purpose,
74
+ item=resource_url,
75
+ nonce=nonce,
76
+ microalgos=microalgos,
77
+ timestamp=timestamp
78
+ )
79
+ return hash_consent(consent), consent["timestamp"]
@@ -0,0 +1,83 @@
1
+ class AgentGuardError(Exception):
2
+ """Base exception for all AgentGuard SDK errors."""
3
+ def __init__(self, message: str, code: str, retryable: bool = False, original_exception=None):
4
+ super().__init__(message)
5
+ self.message = message
6
+ self.code = code
7
+ self.retryable = retryable
8
+ self.original_exception = original_exception
9
+
10
+ class ValidationError(AgentGuardError):
11
+ def __init__(self, message: str, original_exception=None):
12
+ super().__init__(message, "ERR_VALIDATION_FAILURE", False, original_exception)
13
+
14
+ class TransportError(AgentGuardError):
15
+ def __init__(self, message: str, original_exception=None):
16
+ super().__init__(message, "ERR_TRANSPORT_FAILURE", True, original_exception)
17
+
18
+ # Specific errors based on Backend contract
19
+ class AuthenticationError(AgentGuardError):
20
+ def __init__(self, message: str, code: str = "ERR_AUTH_FAILURE", retryable: bool = False, original_exception=None):
21
+ super().__init__(message, code, retryable, original_exception)
22
+
23
+ class InvalidSignatureError(AuthenticationError):
24
+ def __init__(self, message: str, original_exception=None):
25
+ super().__init__(message, "ERR_INVALID_SIGNATURE", False, original_exception)
26
+
27
+ class ExpiredSignatureError(AuthenticationError):
28
+ def __init__(self, message: str, original_exception=None):
29
+ super().__init__(message, "ERR_EXPIRED_SIGNATURE", False, original_exception)
30
+
31
+ class PaymentError(AgentGuardError):
32
+ def __init__(self, message: str, code: str = "ERR_PAYMENT_FAILURE", retryable: bool = False, original_exception=None):
33
+ super().__init__(message, code, retryable, original_exception)
34
+
35
+ class ReplayAttackError(PaymentError):
36
+ def __init__(self, message: str, original_exception=None):
37
+ super().__init__(message, "ERR_REPLAY_ATTACK", False, original_exception)
38
+
39
+ class DuplicatePaymentError(PaymentError):
40
+ def __init__(self, message: str, original_exception=None):
41
+ super().__init__(message, "ERR_DUPLICATE_PAYMENT", False, original_exception)
42
+
43
+ class BudgetExceededError(PaymentError):
44
+ def __init__(self, message: str, original_exception=None):
45
+ super().__init__(message, "ERR_BUDGET_EXCEEDED", False, original_exception)
46
+
47
+ class VerificationError(AgentGuardError):
48
+ def __init__(self, message: str, original_exception=None):
49
+ super().__init__(message, "ERR_VERIFICATION_FAILURE", False, original_exception)
50
+
51
+ class UnknownExecutionStateError(PaymentError):
52
+ def __init__(self, message: str, original_exception=None):
53
+ super().__init__(message, "ERR_UNKNOWN_EXECUTION_STATE", False, original_exception)
54
+
55
+ def map_backend_error(status_code: int, error_data: dict, original_exception=None) -> AgentGuardError:
56
+ """Maps the standardized backend JSONError payload to SDK exceptions."""
57
+ if not isinstance(error_data, dict):
58
+ return TransportError(f"Unexpected response format from backend: {error_data}", original_exception)
59
+
60
+ code = error_data.get("code", "ERR_UNKNOWN")
61
+ message = error_data.get("message", "Unknown error occurred")
62
+ retryable = error_data.get("retryable", False)
63
+
64
+ if code == "ERR_VALIDATION_FAILURE":
65
+ return ValidationError(message, original_exception)
66
+ elif code == "ERR_REPLAY_ATTACK":
67
+ return ReplayAttackError(message, original_exception)
68
+ elif code == "ERR_DUPLICATE_PAYMENT":
69
+ return DuplicatePaymentError(message, original_exception)
70
+ elif code == "ERR_BUDGET_EXCEEDED":
71
+ return BudgetExceededError(message, original_exception)
72
+ elif code == "ERR_UNKNOWN_EXECUTION_STATE":
73
+ return UnknownExecutionStateError(message, original_exception)
74
+ elif code == "ERR_AUTH_FAILURE":
75
+ return AuthenticationError(message, code, retryable, original_exception)
76
+ elif code == "ERR_INVALID_SIGNATURE":
77
+ return InvalidSignatureError(message, original_exception)
78
+ elif code == "ERR_EXPIRED_SIGNATURE":
79
+ return ExpiredSignatureError(message, original_exception)
80
+ elif "ERR_PAYMENT" in code:
81
+ return PaymentError(message, code, retryable, original_exception)
82
+ else:
83
+ return AgentGuardError(message, code, retryable, original_exception)
@@ -0,0 +1,52 @@
1
+ import time
2
+ import json
3
+ import uuid
4
+ import logging
5
+ from datetime import datetime
6
+ from typing import Optional, Dict, Any
7
+
8
+ # [PRIORITY 9] SDK OBSERVABILITY CORE
9
+
10
+ class Span:
11
+ def __init__(self, name: str, trace_id: str, parent_id: Optional[str] = None):
12
+ self.name = name
13
+ self.trace_id = trace_id
14
+ self.span_id = str(uuid.uuid4())[:8]
15
+ self.parent_id = parent_id
16
+ self.start_time = time.time()
17
+ self.metadata = {}
18
+
19
+ def set_attribute(self, key: str, value: Any):
20
+ self.metadata[key] = value
21
+
22
+ def end(self, error: Optional[str] = None):
23
+ duration = (time.time() - self.start_time) * 1000
24
+ log_data = {
25
+ "timestamp": datetime.utcnow().isoformat() + "Z",
26
+ "level": "INFO" if not error else "ERROR",
27
+ "service": "agentguard-sdk",
28
+ "event": "span_end",
29
+ "span_name": self.name,
30
+ "span_id": self.span_id,
31
+ "parent_id": self.parent_id,
32
+ "trace_id": self.trace_id,
33
+ "duration_ms": duration,
34
+ "error": error,
35
+ "metadata": self.metadata
36
+ }
37
+ logging.getLogger("agentguard-sdk").info(json.dumps(log_data))
38
+
39
+ def start_span(name: str, trace_id: str, parent_id: Optional[str] = None) -> Span:
40
+ span = Span(name, trace_id, parent_id)
41
+ log_data = {
42
+ "timestamp": datetime.utcnow().isoformat() + "Z",
43
+ "level": "INFO",
44
+ "service": "agentguard-sdk",
45
+ "event": "span_start",
46
+ "span_name": name,
47
+ "span_id": span.span_id,
48
+ "parent_id": parent_id,
49
+ "trace_id": trace_id
50
+ }
51
+ logging.getLogger("agentguard-sdk").info(json.dumps(log_data))
52
+ return span
@@ -1,25 +1,25 @@
1
- from dataclasses import dataclass, field
2
- from typing import Dict, Any
3
-
4
- @dataclass
5
- class AgentGuardReceipt:
6
- """
7
- Structured response for a successful AgentGuard payment.
8
- """
9
- tx_id: str
10
- consent_hash: str
11
- audit_url: str
12
- data: Dict[str, Any] = field(default_factory=dict)
13
-
14
- @dataclass
15
- class AuditProof:
16
- """
17
- Structured response for a DPDP compliance audit.
18
- """
19
- verified: bool
20
- tx_id: str
21
- principal_id: str
22
- consent_hash: str
23
- on_chain_hash: str
24
- explorer_url: str
25
- message: str
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, Any
3
+
4
+ @dataclass
5
+ class AgentGuardReceipt:
6
+ """
7
+ Structured response for a successful AgentGuard payment.
8
+ """
9
+ tx_id: str
10
+ consent_hash: str
11
+ audit_url: str
12
+ data: Dict[str, Any] = field(default_factory=dict)
13
+
14
+ @dataclass
15
+ class AuditProof:
16
+ """
17
+ Structured response for a DPDP compliance audit.
18
+ """
19
+ verified: bool
20
+ tx_id: str
21
+ principal_id: str
22
+ consent_hash: str
23
+ on_chain_hash: str
24
+ explorer_url: str
25
+ message: str
@@ -1,13 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentguard-python-sdk
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A production-grade middleware for AI agents to perform on-chain payments and verifiable consent.
5
5
  Author: AgentGuard Team
6
- License: MIT
6
+ License-Expression: MIT
7
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
12
+ Requires-Dist: pynacl>=1.5.0
13
+ Requires-Dist: py-algorand-sdk>=2.0.0
11
14
 
12
15
  # AgentGuard SDK
13
16
 
@@ -34,7 +37,7 @@ async def main():
34
37
  # 2. Perform a secure payment (includes automatic DPDP consent hashing)
35
38
  receipt = await client.pay_and_fetch(
36
39
  resource_url="https://api.stock.com/reliance",
37
- amount_algo=0.05,
40
+ amount_algo="0.05",
38
41
  purpose="Financial Analysis"
39
42
  )
40
43
  print(f"Payment Successful! TX ID: {receipt.tx_id}")
@@ -1,8 +1,12 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  agentguard/__init__.py
4
+ agentguard/auth.py
4
5
  agentguard/client.py
6
+ agentguard/config.py
5
7
  agentguard/consent.py
8
+ agentguard/errors.py
9
+ agentguard/observability.py
6
10
  agentguard/types.py
7
11
  agentguard_python_sdk.egg-info/PKG-INFO
8
12
  agentguard_python_sdk.egg-info/SOURCES.txt
@@ -0,0 +1,5 @@
1
+ httpx>=0.27.0
2
+ pydantic>=2.0.0
3
+ pydantic-settings>=2.0.0
4
+ pynacl>=1.5.0
5
+ py-algorand-sdk>=2.0.0
@@ -1,22 +1,25 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "agentguard-python-sdk"
7
- version = "0.1.1"
8
- description = "A production-grade middleware for AI agents to perform on-chain payments and verifiable consent."
9
- readme = "README.md"
10
- requires-python = ">=3.9"
11
- license = {text = "MIT"}
12
- authors = [
13
- {name = "AgentGuard Team"}
14
- ]
15
- dependencies = [
16
- "httpx>=0.27.0",
17
- "pydantic>=2.0.0"
18
- ]
19
-
20
- [tool.setuptools.packages.find]
21
- where = ["."]
22
- include = ["agentguard*"]
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agentguard-python-sdk"
7
+ version = "0.1.3"
8
+ description = "A production-grade middleware for AI agents to perform on-chain payments and verifiable consent."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ {name = "AgentGuard Team"}
14
+ ]
15
+ dependencies = [
16
+ "httpx>=0.27.0",
17
+ "pydantic>=2.0.0",
18
+ "pydantic-settings>=2.0.0",
19
+ "pynacl>=1.5.0",
20
+ "py-algorand-sdk>=2.0.0"
21
+ ]
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["."]
25
+ include = ["agentguard*"]
@@ -1,3 +0,0 @@
1
- from .client import AgentGuardClient
2
-
3
- __all__ = ["AgentGuardClient"]
@@ -1,126 +0,0 @@
1
- import httpx
2
- import uuid
3
- import logging
4
- from typing import Optional, Dict, Any
5
- from .consent import generate_consent_proof
6
- from .types import AgentGuardReceipt, AuditProof
7
-
8
- logger = logging.getLogger("agentguard-sdk")
9
-
10
- class AgentGuardClient:
11
- def __init__(self, wallet_address: str, base_url: str = "https://agentguard-backend.onrender.com"):
12
- """
13
- Initialize the AgentGuard production client.
14
-
15
- Args:
16
- wallet_address: The user's wallet address (Principal ID).
17
- base_url: The URL of the AgentGuard Backend.
18
- """
19
- self.wallet_address = wallet_address
20
- self.base_url = base_url.rstrip("/")
21
- self.async_client = httpx.AsyncClient(timeout=30.0)
22
-
23
- async def pay_and_fetch(
24
- self,
25
- resource_url: str,
26
- amount_algo: float,
27
- purpose: str
28
- ) -> AgentGuardReceipt:
29
- """
30
- Asynchronously perform an on-chain payment and return a typed receipt.
31
-
32
- Args:
33
- resource_url: The URL/ID of the resource being accessed.
34
- amount_algo: The amount to pay in ALGO.
35
- purpose: The purpose of the data processing.
36
-
37
- Returns:
38
- AgentGuardReceipt: A structured receipt with transaction details.
39
- """
40
- # 1. Generate Nonce for replay protection
41
- nonce = str(uuid.uuid4())
42
-
43
- # 2. Generate Consent Proof (SHA256 of metadata)
44
- consent_hash, timestamp = generate_consent_proof(
45
- principal_id=self.wallet_address,
46
- resource_url=resource_url,
47
- purpose=purpose,
48
- nonce=nonce
49
- )
50
-
51
- # 3. Request Payment via Backend Proxy
52
- payload = {
53
- "resource_url": resource_url,
54
- "amount_algo": amount_algo,
55
- "purpose": purpose,
56
- "principal_id": self.wallet_address,
57
- "consent_hash": consent_hash,
58
- "nonce": nonce,
59
- "timestamp": timestamp
60
- }
61
-
62
- try:
63
- response = await self.async_client.post(f"{self.base_url}/pay", json=payload)
64
- response.raise_for_status()
65
- res_data = response.json()
66
-
67
- return AgentGuardReceipt(
68
- tx_id=res_data["tx_id"],
69
- consent_hash=res_data["consent_hash"],
70
- audit_url=res_data["audit_url"],
71
- data=res_data.get("data", {})
72
- )
73
- except httpx.HTTPStatusError as e:
74
- logger.error(f"Payment failed: {e.response.text}")
75
- raise Exception(f"Payment failed: {e.response.text}")
76
- except Exception as e:
77
- logger.error(f"Connection failed: {e}")
78
- raise Exception(f"Could not connect to AgentGuard Backend: {e}")
79
-
80
- async def verify(self, tx_id: str) -> AuditProof:
81
- """
82
- Asynchronously verify a transaction against the blockchain audit trail.
83
- """
84
- try:
85
- response = await self.async_client.get(f"{self.base_url}/verify/{tx_id}")
86
- response.raise_for_status()
87
- res_data = response.json()
88
-
89
- return AuditProof(
90
- verified=res_data["verified"],
91
- tx_id=res_data["tx_id"],
92
- principal_id=res_data["principal_id"],
93
- consent_hash=res_data["consent_hash"],
94
- on_chain_hash=res_data["on_chain_hash"],
95
- explorer_url=res_data["explorer_url"],
96
- message=res_data["message"]
97
- )
98
- except Exception as e:
99
- logger.error(f"Verification failed: {e}")
100
- raise Exception(f"Verification failed: {e}")
101
-
102
- async def register_budget(self, limit_algo: float) -> Dict[str, Any]:
103
- """
104
- Convenience method to register/update the user's budget.
105
- """
106
- payload = {
107
- "wallet_address": self.wallet_address,
108
- "budget_algo": limit_algo
109
- }
110
- try:
111
- response = await self.async_client.post(f"{self.base_url}/user/register", json=payload)
112
- response.raise_for_status()
113
- return response.json()
114
- except Exception as e:
115
- logger.error(f"Budget registration failed: {e}")
116
- raise Exception(f"Budget registration failed: {e}")
117
-
118
- async def close(self):
119
- """Close the underlying HTTP client."""
120
- await self.async_client.aclose()
121
-
122
- async def __aenter__(self):
123
- return self
124
-
125
- async def __aexit__(self, exc_type, exc_val, exc_tb):
126
- await self.close()
@@ -1,58 +0,0 @@
1
- import time
2
- import json
3
- import hashlib
4
- import logging
5
- from typing import Dict, Any
6
-
7
- # Configure logging
8
- logger = logging.getLogger("agentguard-sdk")
9
-
10
- def generate_consent(principal_id: str, purpose: str, item: str, nonce: str, data_fiduciary: str = "AgentGuard Platform") -> Dict[str, Any]:
11
- """
12
- Creates a user consent object dictionary containing DPDP compliant fields.
13
- """
14
- timestamp = int(time.time())
15
-
16
- consent_obj = {
17
- "principal_id": principal_id,
18
- "data_fiduciary": data_fiduciary,
19
- "purpose": purpose,
20
- "item": item,
21
- "timestamp": timestamp,
22
- "nonce": nonce
23
- }
24
-
25
- logger.info(f"Generated DPDP consent for principal={principal_id} at {timestamp}")
26
- return consent_obj
27
-
28
- def hash_consent(consent: Dict[str, Any]) -> str:
29
- """
30
- Converts a consent dictionary into a sorted JSON string
31
- and generates a SHA256 hash of it for on-chain logging.
32
-
33
- Args:
34
- consent: The consent dictionary.
35
-
36
- Returns:
37
- str: SHA256 hash string.
38
- """
39
- # Convert consent dict to JSON string with sorted keys to ensure determinism
40
- consent_json = json.dumps(consent, sort_keys=True)
41
-
42
- # Generate SHA256 hash
43
- consent_hash = hashlib.sha256(consent_json.encode("utf-8")).hexdigest()
44
-
45
- logger.info(f"Consent hash generated: {consent_hash}")
46
- return consent_hash
47
-
48
- def generate_consent_proof(principal_id: str, resource_url: str, purpose: str, nonce: str) -> tuple[str, int]:
49
- """
50
- High-level helper to generate a consent hash and return the timestamp used.
51
- """
52
- consent = generate_consent(
53
- principal_id=principal_id,
54
- purpose=purpose,
55
- item=resource_url,
56
- nonce=nonce
57
- )
58
- return hash_consent(consent), consent["timestamp"]
@@ -1,2 +0,0 @@
1
- httpx>=0.27.0
2
- pydantic>=2.0.0