agentguard-python-sdk 0.1.0__py3-none-any.whl → 0.1.2__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/__init__.py +4 -3
- agentguard/auth.py +54 -0
- agentguard/client.py +330 -49
- agentguard/config.py +34 -0
- agentguard/consent.py +42 -21
- agentguard/errors.py +83 -0
- agentguard/observability.py +52 -0
- agentguard/types.py +25 -25
- {agentguard_python_sdk-0.1.0.dist-info → agentguard_python_sdk-0.1.2.dist-info}/METADATA +6 -4
- agentguard_python_sdk-0.1.2.dist-info/RECORD +12 -0
- agentguard_python_sdk-0.1.0.dist-info/RECORD +0 -8
- {agentguard_python_sdk-0.1.0.dist-info → agentguard_python_sdk-0.1.2.dist-info}/WHEEL +0 -0
- {agentguard_python_sdk-0.1.0.dist-info → agentguard_python_sdk-0.1.2.dist-info}/top_level.txt +0 -0
agentguard/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
from .client import AgentGuardClient
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from .client import AgentGuardClient
|
|
2
|
+
from .config import AgentGuardConfig
|
|
3
|
+
|
|
4
|
+
__all__ = ["AgentGuardClient", "AgentGuardConfig"]
|
agentguard/auth.py
ADDED
|
@@ -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")
|
agentguard/client.py
CHANGED
|
@@ -1,91 +1,305 @@
|
|
|
1
1
|
import httpx
|
|
2
2
|
import uuid
|
|
3
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
|
|
4
14
|
from typing import Optional, Dict, Any
|
|
5
15
|
from .consent import generate_consent_proof
|
|
16
|
+
from . import auth
|
|
6
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
|
|
7
21
|
|
|
8
22
|
logger = logging.getLogger("agentguard-sdk")
|
|
9
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
|
+
|
|
10
55
|
class AgentGuardClient:
|
|
11
|
-
def __init__(
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
wallet_address: str,
|
|
59
|
+
config: Optional[AgentGuardConfig] = None,
|
|
60
|
+
private_key: str = None,
|
|
61
|
+
backend_url: Optional[str] = None,
|
|
62
|
+
indexer_url: Optional[str] = None
|
|
63
|
+
):
|
|
12
64
|
"""
|
|
13
|
-
Initialize the AgentGuard
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
wallet_address: The user's wallet address (Principal ID).
|
|
17
|
-
base_url: The URL of the AgentGuard Backend.
|
|
65
|
+
Initialize the AgentGuard client with a structured config object or direct overrides.
|
|
18
66
|
"""
|
|
67
|
+
self.config = config or AgentGuardConfig()
|
|
68
|
+
|
|
69
|
+
# Apply direct overrides if provided
|
|
70
|
+
if backend_url:
|
|
71
|
+
self.config.backend_url = backend_url
|
|
72
|
+
if indexer_url:
|
|
73
|
+
self.config.indexer_url = indexer_url
|
|
74
|
+
|
|
19
75
|
self.wallet_address = wallet_address
|
|
20
|
-
self.base_url =
|
|
21
|
-
self.
|
|
76
|
+
self.base_url = self.config.backend_url.rstrip("/")
|
|
77
|
+
self.private_key = private_key
|
|
78
|
+
|
|
79
|
+
# Priority 6: Segmented Timeouts injected from Config
|
|
80
|
+
self.timeout = httpx.Timeout(
|
|
81
|
+
connect=self.config.timeout_connect,
|
|
82
|
+
read=self.config.timeout_read,
|
|
83
|
+
write=self.config.timeout_write,
|
|
84
|
+
pool=self.config.timeout_pool
|
|
85
|
+
)
|
|
86
|
+
self.async_client = httpx.AsyncClient(timeout=self.timeout)
|
|
87
|
+
|
|
88
|
+
# Configure logging based on config
|
|
89
|
+
logging.getLogger("agentguard-sdk").setLevel(self.config.logging_level)
|
|
22
90
|
|
|
23
91
|
async def pay_and_fetch(
|
|
24
92
|
self,
|
|
25
93
|
resource_url: str,
|
|
26
|
-
|
|
27
|
-
|
|
94
|
+
purpose: str,
|
|
95
|
+
amount_algo: Optional[str] = None,
|
|
96
|
+
microalgos: Optional[int] = None,
|
|
97
|
+
idempotency_key: Optional[str] = None,
|
|
98
|
+
nonce: Optional[str] = None,
|
|
99
|
+
timestamp: Optional[int] = None,
|
|
100
|
+
correlation_id: Optional[str] = None
|
|
28
101
|
) -> AgentGuardReceipt:
|
|
29
102
|
"""
|
|
30
103
|
Asynchronously perform an on-chain payment and return a typed receipt.
|
|
31
104
|
|
|
32
105
|
Args:
|
|
33
106
|
resource_url: The URL/ID of the resource being accessed.
|
|
34
|
-
amount_algo: The amount to pay in ALGO.
|
|
35
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).
|
|
36
110
|
|
|
37
111
|
Returns:
|
|
38
112
|
AgentGuardReceipt: A structured receipt with transaction details.
|
|
39
113
|
"""
|
|
40
|
-
# 1.
|
|
41
|
-
|
|
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.")
|
|
42
117
|
|
|
43
|
-
|
|
44
|
-
|
|
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.")
|
|
126
|
+
|
|
127
|
+
# 2. Idempotency & Replay Protection (PHASE C)
|
|
128
|
+
# We generate these early so they can be persisted across retries if needed.
|
|
129
|
+
idem_key = idempotency_key or str(uuid.uuid4())
|
|
130
|
+
act_nonce = nonce or str(uuid.uuid4())
|
|
131
|
+
act_correlation_id = correlation_id or str(idem_key)
|
|
132
|
+
|
|
133
|
+
# 3. Generate Consent Proof (SHA256 of metadata including microalgos)
|
|
134
|
+
# If timestamp is provided (from a previous retry), we reuse it.
|
|
135
|
+
consent_hash, act_timestamp = generate_consent_proof(
|
|
45
136
|
principal_id=self.wallet_address,
|
|
46
137
|
resource_url=resource_url,
|
|
47
138
|
purpose=purpose,
|
|
48
|
-
nonce=
|
|
139
|
+
nonce=act_nonce,
|
|
140
|
+
microalgos=actual_microalgos,
|
|
141
|
+
timestamp=timestamp # Pass through for retry consistency
|
|
49
142
|
)
|
|
50
143
|
|
|
51
|
-
#
|
|
144
|
+
# 4. Payload Construction
|
|
52
145
|
payload = {
|
|
53
146
|
"resource_url": resource_url,
|
|
54
|
-
"
|
|
147
|
+
"microalgos": actual_microalgos,
|
|
55
148
|
"purpose": purpose,
|
|
56
149
|
"principal_id": self.wallet_address,
|
|
57
150
|
"consent_hash": consent_hash,
|
|
58
|
-
"nonce":
|
|
59
|
-
"timestamp":
|
|
151
|
+
"nonce": act_nonce,
|
|
152
|
+
"timestamp": act_timestamp,
|
|
153
|
+
"idempotency_key": idem_key,
|
|
154
|
+
"correlation_id": act_correlation_id
|
|
60
155
|
}
|
|
61
156
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
157
|
+
headers = {
|
|
158
|
+
"X-Idempotency-Key": idem_key,
|
|
159
|
+
"X-Correlation-ID": act_correlation_id
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# 5. Cryptographic Signing (Priority 5)
|
|
163
|
+
if self.private_key:
|
|
164
|
+
signature = auth.sign_request(payload, self.private_key)
|
|
165
|
+
headers.update({
|
|
166
|
+
"X-AgentGuard-Signature": signature,
|
|
167
|
+
"X-AgentGuard-Timestamp": str(act_timestamp),
|
|
168
|
+
"X-AgentGuard-Key-Id": self.wallet_address,
|
|
169
|
+
"X-AgentGuard-Nonce": act_nonce
|
|
170
|
+
})
|
|
171
|
+
# Priority 6: Signing verified via Span attributes if needed
|
|
172
|
+
|
|
173
|
+
# 6. Request with Retries
|
|
174
|
+
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
|
+
max_attempts = self.config.max_retries
|
|
180
|
+
base_delay = self.config.retry_base_delay
|
|
181
|
+
|
|
182
|
+
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
|
+
try:
|
|
185
|
+
response = await self.async_client.post(f"{self.base_url}/pay", json=payload, headers=headers)
|
|
186
|
+
|
|
187
|
+
if response.status_code == 200:
|
|
188
|
+
res_data = response.json()
|
|
189
|
+
attempt_span.end()
|
|
190
|
+
span.set_attribute("tx_id", res_data["tx_id"])
|
|
191
|
+
span.end()
|
|
192
|
+
return AgentGuardReceipt(
|
|
193
|
+
tx_id=res_data["tx_id"],
|
|
194
|
+
consent_hash=res_data["consent_hash"],
|
|
195
|
+
audit_url=res_data["audit_url"],
|
|
196
|
+
data=res_data.get("data", {})
|
|
197
|
+
)
|
|
198
|
+
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)
|
|
79
215
|
|
|
80
|
-
|
|
216
|
+
except httpx.RequestError as e:
|
|
217
|
+
attempt_span.end(error=str(e))
|
|
218
|
+
if attempt < max_attempts - 1:
|
|
219
|
+
delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
|
|
220
|
+
await asyncio.sleep(delay)
|
|
221
|
+
continue
|
|
222
|
+
span.end(error="transport_failure")
|
|
223
|
+
raise TransportError(f"Could not connect to AgentGuard Backend: {e}")
|
|
224
|
+
except Exception as e:
|
|
225
|
+
attempt_span.end(error=str(e))
|
|
226
|
+
span.end(error="unexpected_sdk_error")
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
async def verify(self, tx_id: str, verify_onchain: bool = False, local_proof: Optional[Dict[str, Any]] = None) -> AuditProof:
|
|
81
230
|
"""
|
|
82
231
|
Asynchronously verify a transaction against the blockchain audit trail.
|
|
232
|
+
|
|
233
|
+
If verify_onchain is True, the SDK performs an independent check against the
|
|
234
|
+
Algorand indexer and re-computes the proof hash locally.
|
|
235
|
+
|
|
236
|
+
If local_proof is provided, the SDK uses it to recompute the hash even if the
|
|
237
|
+
backend has no record of the transaction (Split-Brain Recovery).
|
|
83
238
|
"""
|
|
84
239
|
try:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
240
|
+
# 1. Fetch Backend's Assertion (Metadata + Proof)
|
|
241
|
+
res_data = {}
|
|
242
|
+
backend_failed = False
|
|
243
|
+
try:
|
|
244
|
+
response = await self.async_client.get(f"{self.base_url}/verify/{tx_id}")
|
|
245
|
+
response.raise_for_status()
|
|
246
|
+
res_data = response.json()
|
|
247
|
+
except Exception as e:
|
|
248
|
+
if not verify_onchain:
|
|
249
|
+
# In legacy mode, we must have the backend response
|
|
250
|
+
raise
|
|
251
|
+
logger.warning(f"Backend lookup for {tx_id} failed: {e}. Attempting independent recovery.")
|
|
252
|
+
backend_failed = True
|
|
253
|
+
|
|
254
|
+
# 2. Independent Logic
|
|
255
|
+
if verify_onchain:
|
|
256
|
+
logger.info(f"PERFORMING INDEPENDENT ON-CHAIN VERIFICATION FOR {tx_id}")
|
|
257
|
+
|
|
258
|
+
# A. Recompute Hash locally
|
|
259
|
+
# Content source: Backend provided proof OR user provided local_proof
|
|
260
|
+
canonical_json = res_data.get("canonical_json")
|
|
261
|
+
|
|
262
|
+
if not canonical_json and local_proof:
|
|
263
|
+
# [Priority 8 BONUS] Reconstruct canonical JSON from raw local proof
|
|
264
|
+
from .auth import canonicalize_payload
|
|
265
|
+
canonical_bytes = canonicalize_payload(local_proof)
|
|
266
|
+
canonical_json = canonical_bytes.decode("utf-8")
|
|
267
|
+
|
|
268
|
+
if not canonical_json:
|
|
269
|
+
reason = "Backend missing proof" if not backend_failed else "Backend unreachable"
|
|
270
|
+
raise VerificationError(f"{reason} and no local_proof provided for recovery.")
|
|
271
|
+
|
|
272
|
+
local_hash = hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()
|
|
273
|
+
|
|
274
|
+
# B. Interrogate Blockchain directly
|
|
275
|
+
on_chain_hash = await self._fetch_on_chain_proof(tx_id)
|
|
276
|
+
|
|
277
|
+
# C. Compare
|
|
278
|
+
if local_hash != on_chain_hash:
|
|
279
|
+
# [SECURITY BOUNDARY] The Backend is lying.
|
|
280
|
+
logger.error(f"TRUSTLESS VERIFICATION FAILED: Local Hash {local_hash} != On-chain Hash {on_chain_hash}")
|
|
281
|
+
return AuditProof(
|
|
282
|
+
verified=False,
|
|
283
|
+
tx_id=tx_id,
|
|
284
|
+
principal_id=res_data.get("principal_id", "N/A"),
|
|
285
|
+
consent_hash=local_hash,
|
|
286
|
+
on_chain_hash=on_chain_hash,
|
|
287
|
+
explorer_url=self.config.backend_url, # Placeholder
|
|
288
|
+
message="CRITICAL FAILURE: Local proof does not match blockchain evidence."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
logger.info("TRUSTLESS VERIFICATION SUCCESS: Blockchain evidence matches backend assertion.")
|
|
292
|
+
return AuditProof(
|
|
293
|
+
verified=True,
|
|
294
|
+
tx_id=tx_id,
|
|
295
|
+
principal_id=res_data.get("principal_id", "N/A"),
|
|
296
|
+
consent_hash=local_hash,
|
|
297
|
+
on_chain_hash=on_chain_hash,
|
|
298
|
+
explorer_url=res_data.get("explorer_url", ""),
|
|
299
|
+
message="Trustless independent verification successful."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Legacy/Backend-trusting behavior
|
|
89
303
|
return AuditProof(
|
|
90
304
|
verified=res_data["verified"],
|
|
91
305
|
tx_id=res_data["tx_id"],
|
|
@@ -95,25 +309,90 @@ class AgentGuardClient:
|
|
|
95
309
|
explorer_url=res_data["explorer_url"],
|
|
96
310
|
message=res_data["message"]
|
|
97
311
|
)
|
|
312
|
+
except httpx.HTTPStatusError as e:
|
|
313
|
+
try:
|
|
314
|
+
error_data = e.response.json()
|
|
315
|
+
except Exception:
|
|
316
|
+
error_data = {"message": e.response.text, "code": "ERR_UNKNOWN"}
|
|
317
|
+
raise map_backend_error(e.response.status_code, error_data, original_exception=e) from e
|
|
318
|
+
except httpx.RequestError as e:
|
|
319
|
+
raise TransportError(f"Verification transport failed: {e}", original_exception=e) from e
|
|
98
320
|
except Exception as e:
|
|
99
|
-
|
|
100
|
-
raise
|
|
321
|
+
if isinstance(e, AgentGuardError): raise
|
|
322
|
+
raise VerificationError(f"Independent verification failed: {str(e)}", original_exception=e)
|
|
101
323
|
|
|
102
|
-
async def
|
|
324
|
+
async def _fetch_on_chain_proof(self, tx_id: str) -> str:
|
|
325
|
+
"""
|
|
326
|
+
Directly queries the Algorand Indexer to extract the proof hash.
|
|
327
|
+
Supports atomic group detection.
|
|
328
|
+
"""
|
|
329
|
+
indexer_url = self.config.indexer_url.rstrip("/")
|
|
330
|
+
try:
|
|
331
|
+
# 1. Fetch TX
|
|
332
|
+
resp = await self.async_client.get(f"{indexer_url}/transactions/{tx_id}")
|
|
333
|
+
resp.raise_for_status()
|
|
334
|
+
tx_data = resp.json().get("transaction", {})
|
|
335
|
+
|
|
336
|
+
# 2. Extract proof or hunt in group
|
|
337
|
+
relevant_txn = tx_data
|
|
338
|
+
group_id = tx_data.get("group")
|
|
339
|
+
|
|
340
|
+
if group_id and not tx_data.get("application-transaction"):
|
|
341
|
+
# If this was one part of a group (like the payment part), find the App call
|
|
342
|
+
import urllib.parse
|
|
343
|
+
safe_group = urllib.parse.quote(group_id)
|
|
344
|
+
g_resp = await self.async_client.get(f"{indexer_url}/transactions?group-id={safe_group}")
|
|
345
|
+
if g_resp.status_code == 200:
|
|
346
|
+
group_txns = g_resp.json().get("transactions", [])
|
|
347
|
+
for g_tx in group_txns:
|
|
348
|
+
if g_tx.get("application-transaction"):
|
|
349
|
+
relevant_txn = g_tx
|
|
350
|
+
break
|
|
351
|
+
|
|
352
|
+
app_txn = relevant_txn.get("application-transaction", {})
|
|
353
|
+
args = app_txn.get("application-args", [])
|
|
354
|
+
|
|
355
|
+
if not args:
|
|
356
|
+
raise VerificationError("Blockchain record found but no AppArgs proof exists.")
|
|
357
|
+
|
|
358
|
+
# Decode proof (Arg 0 is Sha256 hex as base64-encoded bytes on wire)
|
|
359
|
+
on_chain_hash_hex = base64.b64decode(args[0]).decode("utf-8")
|
|
360
|
+
return on_chain_hash_hex
|
|
361
|
+
|
|
362
|
+
except httpx.HTTPStatusError as e:
|
|
363
|
+
if e.response.status_code == 404:
|
|
364
|
+
raise VerificationError(f"Transaction {tx_id} not found on blockchain indexer.")
|
|
365
|
+
raise TransportError(f"Indexer connection failed: {e.response.status_code}")
|
|
366
|
+
except Exception as e:
|
|
367
|
+
raise VerificationError(f"Failed to extract blockchain proof: {str(e)}")
|
|
368
|
+
|
|
369
|
+
async def register_budget(self, limit_algo: Optional[str] = None, limit_microalgos: Optional[int] = None) -> Dict[str, Any]:
|
|
103
370
|
"""
|
|
104
371
|
Convenience method to register/update the user's budget.
|
|
105
372
|
"""
|
|
373
|
+
if limit_algo is not None:
|
|
374
|
+
actual_limit = parse_algo_amount_to_microalgos(limit_algo)
|
|
375
|
+
elif limit_microalgos is not None:
|
|
376
|
+
actual_limit = limit_microalgos
|
|
377
|
+
else:
|
|
378
|
+
raise ValueError("Either limit_algo or limit_microalgos must be provided.")
|
|
379
|
+
|
|
106
380
|
payload = {
|
|
107
381
|
"wallet_address": self.wallet_address,
|
|
108
|
-
"
|
|
382
|
+
"budget_microalgos": actual_limit
|
|
109
383
|
}
|
|
110
384
|
try:
|
|
111
385
|
response = await self.async_client.post(f"{self.base_url}/user/register", json=payload)
|
|
112
386
|
response.raise_for_status()
|
|
113
387
|
return response.json()
|
|
114
|
-
except
|
|
115
|
-
|
|
116
|
-
|
|
388
|
+
except httpx.HTTPStatusError as e:
|
|
389
|
+
try:
|
|
390
|
+
error_data = e.response.json()
|
|
391
|
+
except Exception:
|
|
392
|
+
error_data = {"message": e.response.text, "code": "ERR_UNKNOWN"}
|
|
393
|
+
raise map_backend_error(e.response.status_code, error_data, original_exception=e) from e
|
|
394
|
+
except httpx.RequestError as e:
|
|
395
|
+
raise TransportError(f"Budget registration transport failed: {e}", original_exception=e) from e
|
|
117
396
|
|
|
118
397
|
async def close(self):
|
|
119
398
|
"""Close the underlying HTTP client."""
|
|
@@ -124,3 +403,5 @@ class AgentGuardClient:
|
|
|
124
403
|
|
|
125
404
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
126
405
|
await self.close()
|
|
406
|
+
|
|
407
|
+
# Removed _log_event because it's replaced by Span system
|
agentguard/config.py
ADDED
|
@@ -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
|
agentguard/consent.py
CHANGED
|
@@ -2,50 +2,69 @@ import time
|
|
|
2
2
|
import json
|
|
3
3
|
import hashlib
|
|
4
4
|
import logging
|
|
5
|
+
import unicodedata
|
|
5
6
|
from typing import Dict, Any
|
|
6
7
|
|
|
7
8
|
# Configure logging
|
|
8
9
|
logger = logging.getLogger("agentguard-sdk")
|
|
9
10
|
|
|
10
|
-
def generate_consent(
|
|
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]:
|
|
11
20
|
"""
|
|
12
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.
|
|
13
27
|
"""
|
|
14
|
-
timestamp
|
|
28
|
+
if timestamp is None:
|
|
29
|
+
timestamp = int(time.time())
|
|
15
30
|
|
|
31
|
+
# Normalize strings to NFC at the creation boundary
|
|
16
32
|
consent_obj = {
|
|
17
|
-
"principal_id": principal_id,
|
|
18
|
-
"data_fiduciary": data_fiduciary,
|
|
19
|
-
"purpose": purpose,
|
|
20
|
-
"item": item,
|
|
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,
|
|
21
38
|
"timestamp": timestamp,
|
|
22
|
-
"nonce": nonce
|
|
39
|
+
"nonce": nonce,
|
|
40
|
+
"hash_version": 2 # Current Refactor Version
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
logger.info(f"Generated DPDP consent for principal={principal_id} at {timestamp}")
|
|
43
|
+
logger.info(f"Generated DPDP consent (v2) for principal={principal_id} at {timestamp}")
|
|
26
44
|
return consent_obj
|
|
27
45
|
|
|
28
46
|
def hash_consent(consent: Dict[str, Any]) -> str:
|
|
29
47
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
Canonical hashing implementation.
|
|
49
|
+
MUST REMAINT SYNCED with Backend and MCP Server.
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
Rules:
|
|
52
|
+
1. sort_keys=True
|
|
53
|
+
2. separators=(',', ':')
|
|
54
|
+
3. ensure_ascii=False
|
|
55
|
+
4. Encoding: utf-8
|
|
38
56
|
"""
|
|
39
|
-
# Convert consent dict to JSON string with
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
#
|
|
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)
|
|
43
62
|
consent_hash = hashlib.sha256(consent_json.encode("utf-8")).hexdigest()
|
|
44
63
|
|
|
45
64
|
logger.info(f"Consent hash generated: {consent_hash}")
|
|
46
65
|
return consent_hash
|
|
47
66
|
|
|
48
|
-
def generate_consent_proof(principal_id: str, resource_url: str, purpose: str, nonce: str) -> tuple[str, int]:
|
|
67
|
+
def generate_consent_proof(principal_id: str, resource_url: str, purpose: str, nonce: str, microalgos: int, timestamp: int = None) -> tuple[str, int]:
|
|
49
68
|
"""
|
|
50
69
|
High-level helper to generate a consent hash and return the timestamp used.
|
|
51
70
|
"""
|
|
@@ -53,6 +72,8 @@ def generate_consent_proof(principal_id: str, resource_url: str, purpose: str, n
|
|
|
53
72
|
principal_id=principal_id,
|
|
54
73
|
purpose=purpose,
|
|
55
74
|
item=resource_url,
|
|
56
|
-
nonce=nonce
|
|
75
|
+
nonce=nonce,
|
|
76
|
+
microalgos=microalgos,
|
|
77
|
+
timestamp=timestamp
|
|
57
78
|
)
|
|
58
79
|
return hash_consent(consent), consent["timestamp"]
|
agentguard/errors.py
ADDED
|
@@ -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
|
agentguard/types.py
CHANGED
|
@@ -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,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentguard-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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: pynacl>=1.5.0
|
|
12
|
+
Requires-Dist: py-algorand-sdk>=2.0.0
|
|
11
13
|
|
|
12
14
|
# AgentGuard SDK
|
|
13
15
|
|
|
@@ -18,7 +20,7 @@ By isolating private keys in a hardened **MCP Server (Vault)** and using the **A
|
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
20
22
|
```bash
|
|
21
|
-
pip install agentguard
|
|
23
|
+
pip install agentguard-python-sdk
|
|
22
24
|
```
|
|
23
25
|
|
|
24
26
|
## Quick Start (3-Line Usage)
|
|
@@ -34,7 +36,7 @@ async def main():
|
|
|
34
36
|
# 2. Perform a secure payment (includes automatic DPDP consent hashing)
|
|
35
37
|
receipt = await client.pay_and_fetch(
|
|
36
38
|
resource_url="https://api.stock.com/reliance",
|
|
37
|
-
amount_algo=0.05,
|
|
39
|
+
amount_algo="0.05",
|
|
38
40
|
purpose="Financial Analysis"
|
|
39
41
|
)
|
|
40
42
|
print(f"Payment Successful! TX ID: {receipt.tx_id}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
agentguard/__init__.py,sha256=gCRF3pf7qpWf6_VVwgl2X1A4apOSCx7zqd8g2r3fe44,130
|
|
2
|
+
agentguard/auth.py,sha256=PBb7l-x5MGTv_iwilK6LTi5Yhr76qcZL30p1-J80lDg,1720
|
|
3
|
+
agentguard/client.py,sha256=B8ttu1_AcUFODeW27Q6jDHDyh1Yn1AdUWfBDs3Xg50w,17798
|
|
4
|
+
agentguard/config.py,sha256=96xRt8FCVoRS1jVzNt4i4hzzP1NorT2NrAWPE_5XbCU,1032
|
|
5
|
+
agentguard/consent.py,sha256=iTVCf4FaFlsJQF0R63HtbgYTtDUPzRCVPULz-cmX2nA,2706
|
|
6
|
+
agentguard/errors.py,sha256=rEFQI4mrbccXRyjVYm_pZwHWr4aolcg8e5KMCIzrOdE,4181
|
|
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,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
agentguard/__init__.py,sha256=P8UrlV5qCw8DCFwGmAunbItyBncuDH7UK7HzsqYMxBk,69
|
|
2
|
-
agentguard/client.py,sha256=8rvSOzfrtjKNy5PibBbikX9Sm-_-OnW4OXxrFWUGRUM,4550
|
|
3
|
-
agentguard/consent.py,sha256=RDefJwimc9MM14fbRrM9740yMHate7IIyJElkDoPPHE,1846
|
|
4
|
-
agentguard/types.py,sha256=lJCLHl-DYYV6wDETIhj7NbE548YviUXgMp6svH5nM60,534
|
|
5
|
-
agentguard_python_sdk-0.1.0.dist-info/METADATA,sha256=I67rw1MY1cM2EjF3eO48ij2lu6GVmmi8y2TEkXov2j4,2410
|
|
6
|
-
agentguard_python_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
-
agentguard_python_sdk-0.1.0.dist-info/top_level.txt,sha256=rEW5sAnjxXRs63ja2psmsMRMJJfi_3EAxf7crl1nHyw,11
|
|
8
|
-
agentguard_python_sdk-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
{agentguard_python_sdk-0.1.0.dist-info → agentguard_python_sdk-0.1.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|