agentsync-sdk 1.0.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.
agentsync/__init__.py ADDED
@@ -0,0 +1,88 @@
1
+ """
2
+ AgentSync Python SDK.
3
+
4
+ Idiomatic Python client for the AgentSync x402 micropayment gateway
5
+ on the Radix ledger. Supports ROLA authentication, Mode A (credit
6
+ buffer), Mode B (single-pay), and Mode C (RAP/1 bundled non-custodial
7
+ on-chain settlement), plus catalog discovery and withdrawal attestation.
8
+ """
9
+
10
+ from .attestation import SignatureEnvelope, verify_platform_attestation
11
+ from .client import AgentSyncClient, CatalogModule, set_default_registry_component
12
+ from .errors import AuthenticationError, GatewayError, InsufficientCreditError
13
+ from .rap import generate_composite_manifest, generate_rap_manifest
14
+ from .types import (
15
+ AgentSyncClientOptions,
16
+ AgentSyncFetchOptions,
17
+ AuthChallengeResponse,
18
+ AuthVerifyResponse,
19
+ CatalogEntry,
20
+ CatalogResponse,
21
+ CompositeManifestConfig,
22
+ CompositePurchase,
23
+ InsufficientCreditPayload,
24
+ PremiumDataResponse,
25
+ RadixAccountAddress,
26
+ RapManifestConfig,
27
+ RapPublicKey,
28
+ RapSignatureValue,
29
+ RapHash,
30
+ RapRootManifest,
31
+ RapNotary,
32
+ RapDraftInputs,
33
+ RapEconomicParams,
34
+ RapSignatureFile,
35
+ RapPayload,
36
+ RolaSigner,
37
+ SessionDebitEntry,
38
+ SessionSummary,
39
+ SettleResponse,
40
+ WithdrawalPayload,
41
+ WithdrawResponse,
42
+ UsdcDecimal,
43
+ )
44
+
45
+ __all__ = [
46
+ # Client
47
+ "AgentSyncClient",
48
+ "CatalogModule",
49
+ "set_default_registry_component",
50
+ # Standalone functions
51
+ "generate_composite_manifest",
52
+ "generate_rap_manifest",
53
+ "verify_platform_attestation",
54
+ # Errors
55
+ "AuthenticationError",
56
+ "GatewayError",
57
+ "InsufficientCreditError",
58
+ # Types
59
+ "AgentSyncClientOptions",
60
+ "AgentSyncFetchOptions",
61
+ "AuthChallengeResponse",
62
+ "AuthVerifyResponse",
63
+ "CatalogEntry",
64
+ "CatalogResponse",
65
+ "CompositeManifestConfig",
66
+ "CompositePurchase",
67
+ "InsufficientCreditPayload",
68
+ "PremiumDataResponse",
69
+ "RadixAccountAddress",
70
+ "RapManifestConfig",
71
+ "RapPublicKey",
72
+ "RapSignatureValue",
73
+ "RapHash",
74
+ "RapRootManifest",
75
+ "RapNotary",
76
+ "RapDraftInputs",
77
+ "RapEconomicParams",
78
+ "RapSignatureFile",
79
+ "RapPayload",
80
+ "RolaSigner",
81
+ "SessionDebitEntry",
82
+ "SessionSummary",
83
+ "SettleResponse",
84
+ "SignatureEnvelope",
85
+ "WithdrawalPayload",
86
+ "WithdrawResponse",
87
+ "UsdcDecimal",
88
+ ]
@@ -0,0 +1,89 @@
1
+ """
2
+ Platform attestation verification for AgentSync.
3
+
4
+ Verifies the Ed25519 signature over the SHA-256 pre-hash of an attestation
5
+ payload, mirroring the platform signing path in ``worker/src/attestation.ts``.
6
+
7
+ Signature scheme: SHA-256 pre-hash → Ed25519 verify (matches Scrypto's
8
+ ``verify_ed25519`` convention where messages are hashed before verifying).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import hmac
15
+ from dataclasses import dataclass
16
+
17
+ from nacl.exceptions import BadSignatureError
18
+ from nacl.signing import VerifyKey
19
+
20
+ SUPPORTED_ALGORITHM = "ed25519+sha256"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class SignatureEnvelope:
25
+ """Attestation envelope produced by the platform."""
26
+
27
+ payload: str
28
+ signature: str
29
+ public_key: str
30
+ algorithm: str
31
+
32
+
33
+ def verify_platform_attestation(
34
+ envelope: SignatureEnvelope,
35
+ trusted_public_key: str,
36
+ ) -> bool:
37
+ """Verify a platform attestation envelope against the trusted platform key.
38
+
39
+ The function is pure and synchronous: the trusted public key is injected by
40
+ the caller, never fetched from the network. To prevent tautological spoofing
41
+ (an attacker signing a tampered payload with their own key), the envelope's
42
+ ``public_key`` must match ``trusted_public_key`` before any signature check.
43
+
44
+ Args:
45
+ envelope: Attestation envelope (payload, signature, public key,
46
+ algorithm) produced by the platform.
47
+ trusted_public_key: Hex-encoded 32-byte Ed25519 public key the caller
48
+ has independently verified as the platform's key.
49
+
50
+ Returns:
51
+ ``True`` if the algorithm is supported, the embedded key matches the
52
+ trusted key, and the signature verifies over ``SHA-256(payload)``.
53
+ """
54
+ if envelope.algorithm != SUPPORTED_ALGORITHM:
55
+ return False
56
+
57
+ if not _constant_time_hex_equals(envelope.public_key, trusted_public_key):
58
+ return False
59
+
60
+ public_key = _hex_to_bytes(trusted_public_key)
61
+ signature = _hex_to_bytes(envelope.signature)
62
+ if public_key is None or signature is None:
63
+ return False
64
+ if len(public_key) != 32 or len(signature) != 64:
65
+ return False
66
+
67
+ # SHA-256 pre-hash — matches the platform's Ed25519(SHA-256(payload)) scheme.
68
+ message_hash = hashlib.sha256(envelope.payload.encode("utf-8")).digest()
69
+
70
+ try:
71
+ VerifyKey(public_key).verify(message_hash, signature)
72
+ return True
73
+ except (BadSignatureError, ValueError):
74
+ return False
75
+
76
+
77
+ def _hex_to_bytes(value: str) -> bytes | None:
78
+ """Decode a hex string to bytes, returning ``None`` on malformed input."""
79
+ try:
80
+ return bytes.fromhex(value)
81
+ except ValueError:
82
+ return None
83
+
84
+
85
+ def _constant_time_hex_equals(a: str, b: str) -> bool:
86
+ """Compare two hex strings in constant time."""
87
+ if len(a) != len(b):
88
+ return False
89
+ return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))
agentsync/client.py ADDED
@@ -0,0 +1,565 @@
1
+ """
2
+ AgentSyncClient — primary entry point for the ``agentsync`` Python SDK.
3
+
4
+ Configuration:
5
+ - base_url: Gateway API URL (e.g. https://agentsync-api.rennv-agentsync.workers.dev)
6
+ - agent_address: The agent's Radix component/account address
7
+ - signer: Optional ROLA signing callback
8
+
9
+ Public surface:
10
+ - client.catalog.list() → list[CatalogEntry]
11
+ - client.catalog.get_llms_txt() → str
12
+ - client.catalog.by_service_id() → list[CatalogEntry]
13
+ - client.catalog.by_provider_address() → list[CatalogEntry]
14
+ - client.fetch(path, opts?) → httpx.Response (Mode A/B x402)
15
+ - client.generate_rap_manifest(agent, amount, config) → RTM string (Mode C)
16
+ - client.settle_session() → dict with manifest + attestation
17
+ - client.request_withdrawal(amt) → dict with manifest + attestation
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json as _json
23
+ import os
24
+ from datetime import datetime, timezone
25
+ from typing import Any, Optional
26
+
27
+ import httpx
28
+
29
+ from .errors import AuthenticationError, GatewayError, InsufficientCreditError
30
+ from .rap import generate_composite_manifest as _generate_composite_manifest_fn
31
+ from .rap import generate_rap_manifest as _generate_rap_manifest_fn
32
+ from .types import (
33
+ AgentSyncClientOptions,
34
+ AgentSyncFetchOptions,
35
+ AuthChallengeResponse,
36
+ AuthVerifyResponse,
37
+ CatalogEntry,
38
+ CatalogResponse,
39
+ CompositeManifestConfig,
40
+ CompositePurchase,
41
+ InsufficientCreditPayload,
42
+ RapManifestConfig,
43
+ SettleResponse,
44
+ WithdrawResponse,
45
+ )
46
+
47
+ # ── Internal helpers ───────────────────────────────────────────────────
48
+
49
+
50
+ # Live Stokenet AgentRegistry component — override via the
51
+ # REGISTRY_COMPONENT env var or ``set_default_registry_component``.
52
+ _LIVE_STOKENET_REGISTRY_COMPONENT = (
53
+ "component_tdx_2_1cz75s5p3xjnk38puy34hdapqgyddg9v5tmg7juzzpwvw8rgww9ac0c"
54
+ )
55
+
56
+ _default_registry_component = os.environ.get(
57
+ "REGISTRY_COMPONENT", _LIVE_STOKENET_REGISTRY_COMPONENT
58
+ )
59
+
60
+
61
+ def set_default_registry_component(address: str) -> None:
62
+ """Override the default registry component address used in generated
63
+ Radix transaction manifests."""
64
+ global _default_registry_component
65
+ _default_registry_component = address
66
+
67
+
68
+ # ── Manifest formatters ────────────────────────────────────────────────
69
+
70
+
71
+ def _format_settle_manifest(
72
+ agent_address: str,
73
+ payload: str,
74
+ signature: str,
75
+ ) -> str:
76
+ parts = payload.split(":")
77
+ amount = parts[1] if len(parts) >= 2 else "0.000000"
78
+ nonce = parts[2] if len(parts) >= 3 else "0"
79
+
80
+ return (
81
+ "CALL_METHOD\n"
82
+ f' Address("{_default_registry_component}")\n'
83
+ ' "settle_agent_session"\n'
84
+ f' Decimal("{amount}")\n'
85
+ f' Address("{agent_address}")\n'
86
+ f' {nonce}u64\n'
87
+ f' Bytes("{signature}");'
88
+ )
89
+
90
+
91
+ def _format_withdraw_manifest(
92
+ agent_address: str,
93
+ payload: str,
94
+ signature: str,
95
+ ) -> str:
96
+ parts = payload.split(":")
97
+ amount = parts[1] if len(parts) >= 2 else "0.000000"
98
+ nonce = parts[2] if len(parts) >= 3 else "0"
99
+
100
+ return (
101
+ "CALL_METHOD\n"
102
+ f' Address("{_default_registry_component}")\n'
103
+ ' "request_withdrawal"\n'
104
+ f' Decimal("{amount}")\n'
105
+ f' Address("{agent_address}")\n'
106
+ f' {nonce}u64\n'
107
+ f' Bytes("{signature}");'
108
+ )
109
+
110
+
111
+ # ── Catalog module ─────────────────────────────────────────────────────
112
+
113
+
114
+ class CatalogModule:
115
+ """Discovery module — browse available data products and fetch
116
+ AI-agent documentation from the gateway."""
117
+
118
+ def __init__(self, client: "AgentSyncClient") -> None:
119
+ self._client = client
120
+
121
+ async def list(self) -> list[CatalogEntry]:
122
+ """Fetch the full x402 product catalog (filtered ≥ 0.75 XRD floor).
123
+
124
+ No authentication required — public endpoint.
125
+ """
126
+ resp = await self._client._raw_fetch(
127
+ f"{self._client._base_url}/v1/catalog",
128
+ headers={"accept": "application/json"},
129
+ )
130
+
131
+ if not resp.is_success:
132
+ try:
133
+ body = resp.json()
134
+ except Exception:
135
+ body = {}
136
+ raise GatewayError(
137
+ f"Catalog fetch failed: {resp.status_code}",
138
+ resp.status_code,
139
+ body,
140
+ )
141
+
142
+ body = resp.json()
143
+ services = body["data"]["services"]
144
+ return [
145
+ CatalogEntry(
146
+ service_id=s["service_id"],
147
+ provider_address=s["provider_address"],
148
+ provider_name=s["provider_name"],
149
+ name=s["name"],
150
+ description=s["description"],
151
+ base_tariff=s["base_tariff"],
152
+ currency=s.get("currency", "USDC"),
153
+ endpoint_paths=s.get("endpoint_paths", []),
154
+ swagger_url=s.get("swagger_url", ""),
155
+ )
156
+ for s in services
157
+ ]
158
+
159
+ async def get_llms_txt(self) -> str:
160
+ """Fetch the ``llms.txt`` markdown documentation.
161
+
162
+ No authentication required — public endpoint.
163
+ """
164
+ resp = await self._client._raw_fetch(
165
+ f"{self._client._base_url}/.well-known/llms.txt",
166
+ headers={"accept": "text/markdown"},
167
+ )
168
+
169
+ if not resp.is_success:
170
+ raise GatewayError(
171
+ f"llms.txt fetch failed: {resp.status_code}",
172
+ resp.status_code,
173
+ None,
174
+ )
175
+
176
+ return resp.text
177
+
178
+ @staticmethod
179
+ def by_service_id(
180
+ entries: list[CatalogEntry], service_id: str
181
+ ) -> list[CatalogEntry]:
182
+ """Filter catalog entries by ``service_id``."""
183
+ return [e for e in entries if e.service_id == service_id]
184
+
185
+ @staticmethod
186
+ def by_provider_address(
187
+ entries: list[CatalogEntry], provider_address: str
188
+ ) -> list[CatalogEntry]:
189
+ """Filter catalog entries by ``provider_address``."""
190
+ return [e for e in entries if e.provider_address == provider_address]
191
+
192
+
193
+ # ── Client ─────────────────────────────────────────────────────────────
194
+
195
+
196
+ class AgentSyncClient:
197
+ """Primary entry point for the AgentSync Python SDK.
198
+
199
+ Handles ROLA authentication, Mode A/B x402 fetch, Mode C RAP/1
200
+ bundle settlement, session settlement, and withdrawal attestation.
201
+ """
202
+
203
+ def __init__(self, options: AgentSyncClientOptions) -> None:
204
+ self._base_url = options.base_url.rstrip("/")
205
+ self._agent_address = options.agent_address
206
+ self._signer = options.signer
207
+ self._options = options
208
+ self._bearer_token: Optional[str] = None
209
+ self._token_expires_at: Optional[datetime] = None
210
+ self._http = httpx.AsyncClient()
211
+ self.catalog = CatalogModule(self)
212
+
213
+ # ── Package-private helpers for CatalogModule ──────────────────────
214
+
215
+ @property
216
+ def _raw_fetch(self):
217
+ """Raw HTTP fetch for unauthenticated catalog endpoints."""
218
+ return self._http.get
219
+
220
+ # ── ROLA Authentication ───────────────────────────────────────────
221
+
222
+ async def authenticate(self) -> None:
223
+ """Perform the full ROLA challenge-response flow.
224
+
225
+ Must be called before any authenticated endpoint unless auto-auth
226
+ was triggered by a prior call.
227
+ """
228
+ if self._signer is None:
229
+ raise AuthenticationError(
230
+ "No signer provided — cannot authenticate. "
231
+ "Pass a `signer` callback in AgentSyncClientOptions.",
232
+ 401,
233
+ )
234
+
235
+ # 1. Request challenge
236
+ chal_resp = await self._http.post(
237
+ f"{self._base_url}/v1/auth/challenge",
238
+ headers={"content-type": "application/json"},
239
+ )
240
+
241
+ if not chal_resp.is_success:
242
+ raise AuthenticationError(
243
+ f"Challenge request failed: {chal_resp.status_code}",
244
+ chal_resp.status_code,
245
+ )
246
+
247
+ challenge = chal_resp.json()
248
+
249
+ # 2. Sign challenge via user-provided callback
250
+ signed = await self._signer(challenge["data"]["challenge"])
251
+
252
+ # 3. Verify and get token
253
+ verify_resp = await self._http.post(
254
+ f"{self._base_url}/v1/auth/verify",
255
+ headers={"content-type": "application/json"},
256
+ json={
257
+ "challenge": challenge["data"]["challenge"],
258
+ "signature": signed["signature"],
259
+ "identity_address": self._agent_address,
260
+ "public_key": signed["public_key"],
261
+ },
262
+ )
263
+
264
+ if not verify_resp.is_success:
265
+ raise AuthenticationError(
266
+ f"Verification failed: {verify_resp.status_code}",
267
+ verify_resp.status_code,
268
+ )
269
+
270
+ verified = verify_resp.json()
271
+ self._bearer_token = verified["data"]["auth_token"]
272
+ self._token_expires_at = datetime.fromisoformat(
273
+ verified["data"]["expires_at"]
274
+ )
275
+
276
+ async def _ensure_auth(self) -> str:
277
+ """Return the current bearer token, authenticating if necessary."""
278
+ if (
279
+ self._bearer_token is not None
280
+ and self._token_expires_at is not None
281
+ and self._token_expires_at > datetime.now(timezone.utc)
282
+ ):
283
+ return self._bearer_token
284
+ await self.authenticate()
285
+ return self._bearer_token # type: ignore[return-value]
286
+
287
+ # ── Generic authenticated request ─────────────────────────────────
288
+
289
+ async def _request(self, path: str, init: dict[str, Any] | None = None) -> Any:
290
+ """Low-level authenticated request with standard error handling."""
291
+ if init is None:
292
+ init = {}
293
+ token = await self._ensure_auth()
294
+
295
+ headers = {**init.get("headers", {})}
296
+ headers.setdefault("content-type", "application/json")
297
+ headers["authorization"] = f"Bearer {token}"
298
+
299
+ method = init.get("method", "GET")
300
+ body = init.get("body") or init.get("json")
301
+
302
+ if isinstance(body, dict):
303
+ resp = await self._http.request(
304
+ method,
305
+ f"{self._base_url}{path}",
306
+ headers=headers,
307
+ json=body,
308
+ )
309
+ elif isinstance(body, (str, bytes)):
310
+ resp = await self._http.request(
311
+ method,
312
+ f"{self._base_url}{path}",
313
+ headers=headers,
314
+ content=body,
315
+ )
316
+ else:
317
+ resp = await self._http.request(
318
+ method,
319
+ f"{self._base_url}{path}",
320
+ headers=headers,
321
+ )
322
+
323
+ if not resp.is_success:
324
+ try:
325
+ resp_body = resp.json()
326
+ except Exception:
327
+ resp_body = {}
328
+
329
+ if resp.status_code == 401:
330
+ self._bearer_token = None
331
+ self._token_expires_at = None
332
+ msg = (
333
+ resp_body.get("error", {}).get("message")
334
+ if isinstance(resp_body, dict)
335
+ else "Authentication failed"
336
+ )
337
+ raise AuthenticationError(msg, resp.status_code)
338
+
339
+ if resp.status_code == 402:
340
+ details = (
341
+ resp_body.get("error", {}).get("details")
342
+ if isinstance(resp_body, dict)
343
+ else None
344
+ )
345
+ raise InsufficientCreditError(
346
+ details.get("required", "0") if details else "0",
347
+ details.get("available", "0") if details else "0",
348
+ resp_body,
349
+ )
350
+
351
+ raise GatewayError(
352
+ (
353
+ resp_body.get("error", {}).get("message")
354
+ if isinstance(resp_body, dict)
355
+ else f"HTTP {resp.status_code}"
356
+ ),
357
+ resp.status_code,
358
+ resp_body,
359
+ )
360
+
361
+ return resp.json()
362
+
363
+ # ── Augmented fetch (x402) ─────────────────────────────────────────
364
+
365
+ async def fetch(
366
+ self,
367
+ path: str,
368
+ opts: AgentSyncFetchOptions | None = None,
369
+ ) -> httpx.Response:
370
+ """Augmented fetch for premium data endpoints.
371
+
372
+ **Mode A (Credit Buffer)**: Default. Automatically injects ROLA
373
+ auth headers. Throws ``InsufficientCreditError`` on 402.
374
+
375
+ **Mode B (Single-Pay)**: Pass ``rap_payload`` (and optional
376
+ ``requirements``) in opts to send the body
377
+ ``{ payment: { rap_payload, requirements } }`` — the structure
378
+ expected by ``handleModeBSponsored``.
379
+ """
380
+ if opts is None:
381
+ opts = AgentSyncFetchOptions()
382
+
383
+ token = await self._ensure_auth()
384
+
385
+ headers: dict[str, str] = {**(opts.headers or {})}
386
+ headers["authorization"] = f"Bearer {token}"
387
+
388
+ method = opts.method or "GET"
389
+ body = opts.body
390
+
391
+ if opts.rap_payload is not None:
392
+ method = "POST"
393
+ headers.setdefault("content-type", "application/json")
394
+ body = _json.dumps(
395
+ {
396
+ "payment": {
397
+ "rap_payload": opts.rap_payload.to_dict(),
398
+ "requirements": opts.requirements or {},
399
+ }
400
+ }
401
+ )
402
+
403
+ if body is not None:
404
+ resp = await self._http.request(
405
+ method,
406
+ f"{self._base_url}{path}",
407
+ headers=headers,
408
+ content=body if isinstance(body, bytes) else body,
409
+ )
410
+ else:
411
+ resp = await self._http.request(
412
+ method,
413
+ f"{self._base_url}{path}",
414
+ headers=headers,
415
+ )
416
+
417
+ if not resp.is_success:
418
+ if resp.status_code == 401:
419
+ self._bearer_token = None
420
+ self._token_expires_at = None
421
+ try:
422
+ body_json = resp.json()
423
+ except Exception:
424
+ body_json = {}
425
+ msg = (
426
+ body_json.get("error", {}).get("message")
427
+ if isinstance(body_json, dict)
428
+ else "Authentication failed"
429
+ )
430
+ raise AuthenticationError(msg, resp.status_code)
431
+
432
+ if resp.status_code == 402:
433
+ try:
434
+ body_json = resp.json()
435
+ except Exception:
436
+ body_json = {}
437
+ details = (
438
+ body_json.get("error", {}).get("details")
439
+ if isinstance(body_json, dict)
440
+ else None
441
+ )
442
+ raise InsufficientCreditError(
443
+ details.get("required", "0") if details else "0",
444
+ details.get("available", "0") if details else "0",
445
+ body_json,
446
+ )
447
+
448
+ return resp
449
+
450
+ # ── Mode C: Path A (Non-Custodial) Manifest ────────────────────────
451
+
452
+ def generate_rap_manifest(
453
+ self,
454
+ agent: str,
455
+ amount: str,
456
+ config: RapManifestConfig,
457
+ ) -> str:
458
+ """Generate the Radix transaction manifest (RTM) for Mode C
459
+ bundled settlement — **Path A (non-custodial)**.
460
+
461
+ Produces a manifest that withdraws USDC from the agent's on-chain
462
+ Radix account and routes it through ``process_api_payment`` on
463
+ the registry. Platform gas is sponsored via ``lock_fee``.
464
+
465
+ **Synchronous** — no Ed25519 attestation needed. The agent's
466
+ Radix account signature on the transaction proves authorization.
467
+
468
+ Args:
469
+ agent: The agent's Radix account address (the withdrawing account).
470
+ amount: USDC amount to settle as a 6-decimal string.
471
+ config: Economic parameters (platform account, USDC resource,
472
+ registry component, XRD fee loan amount).
473
+
474
+ Returns:
475
+ Raw Radix Transaction Manifest (RTM) string.
476
+ """
477
+ return _generate_rap_manifest_fn(agent, amount, config)
478
+
479
+ # ── Composite Manifest (Mode B Multi-Endpoint) ─────────────────────
480
+
481
+ def generate_composite_manifest(
482
+ self,
483
+ config: CompositeManifestConfig,
484
+ purchases: list[CompositePurchase],
485
+ ) -> str:
486
+ """Generate a composite Radix transaction manifest (RTM) that
487
+ atomically settles multiple API endpoint purchases in a single
488
+ on-chain transaction.
489
+
490
+ Extends Mode B (Single-Pay) to bundle N endpoint purchases into
491
+ one atomic manifest: a single aggregate ``withdraw`` followed by
492
+ per-endpoint ``TAKE_FROM_WORKTOP`` + ``CALL_METHOD
493
+ process_api_payment`` pairs.
494
+
495
+ **Synchronous** — no network call. The agent submits the resulting
496
+ RTM to the Radix network via their wallet.
497
+
498
+ Args:
499
+ config: Economic parameters (agent, platform account, USDC
500
+ resource, registry component, XRD fee loan amount).
501
+ purchases: List of endpoint purchases (endpoint_id + amount).
502
+
503
+ Returns:
504
+ Raw Radix Transaction Manifest (RTM) string.
505
+ """
506
+ return _generate_composite_manifest_fn(config, purchases)
507
+
508
+ # ── Settlement ────────────────────────────────────────────────────
509
+
510
+ async def settle_session(self) -> dict[str, Any]:
511
+ """Initiate session settlement.
512
+
513
+ Calls ``POST /v1/settle`` and returns payload, signature,
514
+ session summary, and a ready-to-submit RTM string.
515
+ """
516
+ data = await self._request("/v1/settle", {"method": "POST"})
517
+
518
+ manifest = _format_settle_manifest(
519
+ self._agent_address,
520
+ data["data"]["payload"],
521
+ data["data"]["signature"],
522
+ )
523
+
524
+ return {
525
+ "payload": data["data"]["payload"],
526
+ "signature": data["data"]["signature"],
527
+ "session_summary": data["data"]["session_summary"],
528
+ "manifest": manifest,
529
+ }
530
+
531
+ # ── Withdrawal ────────────────────────────────────────────────────
532
+
533
+ async def request_withdrawal(
534
+ self, amount_usdc: str
535
+ ) -> dict[str, Any]:
536
+ """Request an attested USDC withdrawal from the registry.
537
+
538
+ Calls ``POST /v1/withdraw`` and returns payload, signature,
539
+ withdrawal details, and a ready-to-submit RTM string.
540
+
541
+ Args:
542
+ amount_usdc: USDC amount as a 6-decimal string
543
+ (e.g. ``"10.000000"``).
544
+ """
545
+ data = await self._request(
546
+ "/v1/withdraw",
547
+ {"method": "POST", "json": {"amount_usdc": amount_usdc}},
548
+ )
549
+
550
+ manifest = _format_withdraw_manifest(
551
+ self._agent_address,
552
+ data["data"]["payload"],
553
+ data["data"]["signature"],
554
+ )
555
+
556
+ return {
557
+ "payload": data["data"]["payload"],
558
+ "signature": data["data"]["signature"],
559
+ "withdrawal": data["data"]["withdrawal"],
560
+ "manifest": manifest,
561
+ }
562
+
563
+ async def close(self) -> None:
564
+ """Release the underlying HTTP client resources."""
565
+ await self._http.aclose()
agentsync/errors.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ Error types for the AgentSync SDK.
3
+
4
+ Mirrors the TypeScript error hierarchy in `sdk-typescript/src/errors.ts`.
5
+ All exception classes carry structured fields so calling code can
6
+ programmatically inspect failures without string parsing.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+
14
+ class InsufficientCreditError(Exception):
15
+ """Thrown when the gateway returns ``402 Payment Required`` — the
16
+ agent's credit buffer is too low for the requested API call."""
17
+
18
+ def __init__(
19
+ self,
20
+ required: str,
21
+ available: str,
22
+ response_body: Any = None,
23
+ ) -> None:
24
+ self.required = required
25
+ self.available = available
26
+ self.response_body = response_body
27
+ super().__init__(
28
+ f"Insufficient credit: required {required} USDC, "
29
+ f"available {available} USDC. "
30
+ "Top up your credit buffer via the on-chain registry's "
31
+ "top_up_usdc_vault method."
32
+ )
33
+
34
+
35
+ class AuthenticationError(Exception):
36
+ """Thrown when authentication fails (bad credentials, expired token,
37
+ etc.)."""
38
+
39
+ def __init__(self, message: str, status: int) -> None:
40
+ self.status = status
41
+ super().__init__(message)
42
+
43
+
44
+ class GatewayError(Exception):
45
+ """Thrown when the gateway returns an unexpected error response."""
46
+
47
+ def __init__(self, message: str, status: int, body: Any = None) -> None:
48
+ self.status = status
49
+ self.body = body
50
+ super().__init__(message)
agentsync/rap.py ADDED
@@ -0,0 +1,165 @@
1
+ """
2
+ RAP/1 Module — Mode C Path A Settlement (Python SDK).
3
+
4
+ Builds the Path A non-custodial Radix transaction manifest (RTM) for
5
+ on-chain bundled settlement of API consumption sessions.
6
+
7
+ Path A (non-custodial): USDC is withdrawn directly from the agent's
8
+ on-chain Radix account at settlement time via ``process_api_payment``.
9
+ Platform gas is sponsored via ``lock_fee``. No Ed25519 attestation
10
+ signing is needed in the SDK — the agent's Radix account signature
11
+ on the transaction provides proof of authorization.
12
+
13
+ Lifecycle:
14
+ 1. ``generate_rap_manifest`` → build Path A RTM string (synchronous)
15
+ 2. Developer submits RTM to Radix network
16
+ """
17
+
18
+ from typing import Sequence
19
+
20
+ from .types import (
21
+ CompositeManifestConfig,
22
+ CompositePurchase,
23
+ RapManifestConfig,
24
+ )
25
+
26
+
27
+ def generate_rap_manifest(
28
+ agent: str,
29
+ amount: str,
30
+ config: RapManifestConfig,
31
+ ) -> str:
32
+ """Generate the Radix transaction manifest (RTM) for Mode C bundled
33
+ settlement — **Path A (non-custodial)**.
34
+
35
+ The manifest follows the non-custodial Path A flow:
36
+
37
+ 1. ``lock_fee`` — platform sponsors XRD network fee
38
+ 2. ``withdraw`` — pull USDC from agent's on-chain Radix account
39
+ 3. ``TAKE_ALL_FROM_WORKTOP`` — take the withdrawn USDC into a bucket
40
+ 4. ``CALL_METHOD process_api_payment`` — split payment via registry
41
+ (99.5% provider, 0.25% gas vault, 0.25% developer treasury)
42
+
43
+ **Synchronous** — no Ed25519 attestation required. The agent's Radix
44
+ account signature on the transaction proves authorization to withdraw.
45
+
46
+ Args:
47
+ agent: The agent's Radix account address (the withdrawing account).
48
+ amount: USDC amount to settle as a 6-decimal string.
49
+ config: Economic parameters (platform account, USDC resource,
50
+ registry component, XRD fee loan amount).
51
+
52
+ Returns:
53
+ Raw Radix Transaction Manifest (RTM) string.
54
+ """
55
+ return "\n".join(
56
+ [
57
+ "CALL_METHOD",
58
+ f' Address("{config.platform_account}")',
59
+ ' "lock_fee"',
60
+ f' Decimal("{config.fee_loan_amount}");',
61
+ "CALL_METHOD",
62
+ f' Address("{agent}")',
63
+ ' "withdraw"',
64
+ f' Address("{config.usdc_resource}")',
65
+ f' Decimal("{amount}");',
66
+ "TAKE_ALL_FROM_WORKTOP",
67
+ f' Address("{config.usdc_resource}")',
68
+ ' Bucket("payment");',
69
+ "CALL_METHOD",
70
+ f' Address("{config.registry_component}")',
71
+ ' "process_api_payment"',
72
+ ' Bucket("payment");',
73
+ "CALL_METHOD",
74
+ f' Address("{config.merchant_account}")',
75
+ ' "deposit_batch"',
76
+ ' Expression("ENTIRE_WORKTOP");',
77
+ ]
78
+ )
79
+
80
+
81
+ # ── Composite Manifest Builder ──────────────────────────────────────────
82
+
83
+
84
+ def _sum_usdc_amounts(amounts: Sequence[str]) -> str:
85
+ """Sum USDC decimal strings via integer micro-unit arithmetic."""
86
+ total = 0
87
+ for amt in amounts:
88
+ total += round(float(amt) * 1_000_000)
89
+ return f"{total / 1_000_000:.6f}"
90
+
91
+
92
+ def generate_composite_manifest(
93
+ config: CompositeManifestConfig,
94
+ purchases: Sequence[CompositePurchase],
95
+ ) -> str:
96
+ """Generate a composite Radix transaction manifest (RTM) that atomically
97
+ settles multiple API endpoint purchases in a single on-chain transaction.
98
+
99
+ The manifest follows the non-custodial Path A flow extended for N endpoints:
100
+
101
+ 1. ``lock_fee`` — platform sponsors XRD network fee
102
+ 2. ``withdraw`` — single aggregate USDC withdrawal from agent's account
103
+ 3. For each purchase:
104
+ a. ``TAKE_FROM_WORKTOP`` — partition exact USDC into ``payment_{i}`` bucket
105
+ b. ``CALL_METHOD process_api_payment`` — settle that endpoint via registry
106
+
107
+ All ``process_api_payment`` calls execute within the same atomic
108
+ transaction, so the entire batch either fully succeeds or fully reverts.
109
+
110
+ Args:
111
+ config: Economic parameters (agent, platform account, USDC resource,
112
+ registry component, XRD fee loan amount).
113
+ purchases: Ordered sequence of endpoint purchases (endpoint_id + amount).
114
+
115
+ Returns:
116
+ Raw Radix Transaction Manifest (RTM) string.
117
+ """
118
+ if not purchases:
119
+ raise ValueError(
120
+ "generate_composite_manifest: purchases must not be empty"
121
+ )
122
+
123
+ total = _sum_usdc_amounts([p.amount for p in purchases])
124
+
125
+ lines: list[str] = [
126
+ # 1. Platform sponsors network fee
127
+ "CALL_METHOD",
128
+ f' Address("{config.platform_account}")',
129
+ ' "lock_fee"',
130
+ f' Decimal("{config.fee_loan_amount}");',
131
+ # 2. Single aggregate withdraw from agent account
132
+ "CALL_METHOD",
133
+ f' Address("{config.agent}")',
134
+ ' "withdraw"',
135
+ f' Address("{config.usdc_resource}")',
136
+ f' Decimal("{total}");',
137
+ ]
138
+
139
+ # 3. Per-endpoint: partition worktop → process_api_payment
140
+ for i, p in enumerate(purchases):
141
+ bucket = f"payment_{i}"
142
+ lines.extend(
143
+ [
144
+ "TAKE_FROM_WORKTOP",
145
+ f' Address("{config.usdc_resource}")',
146
+ f' Decimal("{p.amount}")',
147
+ f' Bucket("{bucket}");',
148
+ "CALL_METHOD",
149
+ f' Address("{config.registry_component}")',
150
+ ' "process_api_payment"',
151
+ f' Bucket("{bucket}");',
152
+ ]
153
+ )
154
+
155
+ # 4. Deposit any remaining worktop balance to the merchant account
156
+ lines.extend(
157
+ [
158
+ "CALL_METHOD",
159
+ f' Address("{config.merchant_account}")',
160
+ ' "deposit_batch"',
161
+ ' Expression("ENTIRE_WORKTOP");',
162
+ ]
163
+ )
164
+
165
+ return "\n".join(lines)
agentsync/types.py ADDED
@@ -0,0 +1,328 @@
1
+ """
2
+ AgentSync Python SDK — type definitions.
3
+
4
+ Mirrors the gateway API response shapes from the TypeScript SDK.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Coroutine, Optional, Protocol, runtime_checkable
9
+
10
+
11
+ # ── ROLA Auth ──────────────────────────────────────────────────────────
12
+
13
+ @dataclass(frozen=True)
14
+ class AuthChallengeResponse:
15
+ ok: bool
16
+ data: dict[str, str] # challenge, nonce, expires_at
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class AuthVerifyResponse:
21
+ ok: bool
22
+ data: dict[str, str] # auth_token, expires_at
23
+
24
+
25
+ # ── Catalog ────────────────────────────────────────────────────────────
26
+
27
+ # Radix account component address (e.g. `account_tdx_2_...`)
28
+ RadixAccountAddress = str
29
+
30
+ # USDC decimal string (e.g. `"0.75"`, `"2.500"`)
31
+ UsdcDecimal = str
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class CatalogEntry:
36
+ service_id: str
37
+ provider_address: RadixAccountAddress
38
+ provider_name: str
39
+ name: str
40
+ description: str
41
+ base_tariff: UsdcDecimal
42
+ currency: str = "USDC"
43
+ endpoint_paths: list[str] = field(default_factory=list)
44
+ swagger_url: str = ""
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class CatalogResponse:
49
+ ok: bool
50
+ data: dict[str, Any] # services, total, platform_floor_usdc, currency
51
+
52
+
53
+ # ── Premium Data Proxy ─────────────────────────────────────────────────
54
+
55
+ @dataclass(frozen=True)
56
+ class PremiumDataResponse:
57
+ ok: bool
58
+ data: dict[str, Any] # proxied, mode, remaining_balance?, txid?
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class InsufficientCreditPayload:
63
+ ok: bool
64
+ error: dict[str, Any] # code, message, details (required, available)
65
+
66
+
67
+ # ── RAP/1: Mode C (Path A / Non-Custodial) ─────────────────────────────
68
+
69
+ @dataclass(frozen=True)
70
+ class RapManifestConfig:
71
+ """Economic configuration for Path A (non-custodial) manifest generation."""
72
+ platform_account: str
73
+ usdc_resource: str
74
+ registry_component: str
75
+ fee_loan_amount: str
76
+ merchant_account: str
77
+
78
+
79
+ # ── Composite Manifests (Mode B Multi-Endpoint) ──────────────────────────
80
+
81
+ @dataclass(frozen=True)
82
+ class CompositePurchase:
83
+ """A single purchase within a composite (multi-endpoint) manifest."""
84
+ endpoint_id: str
85
+ amount: str
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class CompositeManifestConfig:
90
+ """Configuration for generating a composite (multi-endpoint) manifest."""
91
+ agent: str
92
+ platform_account: str
93
+ usdc_resource: str
94
+ registry_component: str
95
+ fee_loan_amount: str
96
+ merchant_account: str
97
+
98
+
99
+ # ── Settlement ─────────────────────────────────────────────────────────
100
+
101
+ @dataclass(frozen=True)
102
+ class SessionDebitEntry:
103
+ timestamp: str
104
+ cost_usdc: str
105
+ provider_address: str
106
+ service_id: str
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class SessionSummary:
111
+ total_debits: int
112
+ total_spent: str
113
+ by_provider: dict[str, str]
114
+ by_service: dict[str, str]
115
+ entries: list[SessionDebitEntry]
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class SettleResponse:
120
+ ok: bool
121
+ data: dict[str, Any] # payload, signature, session_summary
122
+
123
+
124
+ # ── Withdrawal ─────────────────────────────────────────────────────────
125
+
126
+ @dataclass(frozen=True)
127
+ class WithdrawalPayload:
128
+ agent: str
129
+ amount: str
130
+ nonce: int
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class WithdrawResponse:
135
+ ok: bool
136
+ data: dict[str, Any] # payload, signature, withdrawal
137
+
138
+
139
+ # ── Auth Signer ────────────────────────────────────────────────────────
140
+
141
+ @runtime_checkable
142
+ class RolaSigner(Protocol):
143
+ """Callback protocol for ROLA challenge signing."""
144
+ def __call__(self, challenge: str) -> Coroutine[Any, Any, dict[str, str]]: ...
145
+
146
+
147
+ # ── Client Options ─────────────────────────────────────────────────────
148
+
149
+ @dataclass(frozen=True)
150
+ class AgentSyncClientOptions:
151
+ """Options passed to AgentSyncClient.
152
+
153
+ base_url: Gateway API base URL (e.g. ``https://agentsync-api.rennv-agentsync.workers.dev``).
154
+ """
155
+ base_url: str
156
+ agent_address: RadixAccountAddress
157
+ signer: Optional[RolaSigner] = None
158
+
159
+
160
+ # ── Mode B (x402 Sponsored) rap_payload wire shapes ─────────────────────
161
+
162
+ @dataclass(frozen=True)
163
+ class RapPublicKey:
164
+ """Wire shape of a public key (serde camelCase)."""
165
+ curve: str
166
+ hex: str
167
+
168
+ def to_dict(self) -> dict[str, Any]:
169
+ return {"curve": self.curve, "hex": self.hex}
170
+
171
+
172
+ @dataclass(frozen=True)
173
+ class RapSignatureValue:
174
+ """Wire shape of a signature value (serde camelCase)."""
175
+ curve: str
176
+ hex: str
177
+
178
+ def to_dict(self) -> dict[str, Any]:
179
+ return {"curve": self.curve, "hex": self.hex}
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class RapHash:
184
+ """Wire shape of a hash (serde camelCase)."""
185
+ id: str
186
+ hex: str
187
+
188
+ def to_dict(self) -> dict[str, Any]:
189
+ return {"id": self.id, "hex": self.hex}
190
+
191
+
192
+ @dataclass(frozen=True)
193
+ class RapRootManifest:
194
+ """Wire shape of the draft inputs root manifest (serde camelCase)."""
195
+ kind: str
196
+ rtm: str
197
+ source_name: Optional[str] = None
198
+
199
+ def to_dict(self) -> dict[str, Any]:
200
+ result: dict[str, Any] = {"kind": self.kind, "rtm": self.rtm}
201
+ if self.source_name is not None:
202
+ result["sourceName"] = self.source_name
203
+ return result
204
+
205
+
206
+ @dataclass(frozen=True)
207
+ class RapNotary:
208
+ """Wire shape of the draft inputs notary (serde camelCase)."""
209
+ type: str
210
+ version: int
211
+ public_key: RapPublicKey
212
+ notary_is_signatory: Optional[bool] = None
213
+
214
+ def to_dict(self) -> dict[str, Any]:
215
+ result: dict[str, Any] = {
216
+ "type": self.type,
217
+ "version": self.version,
218
+ "publicKey": self.public_key.to_dict(),
219
+ }
220
+ if self.notary_is_signatory is not None:
221
+ result["notaryIsSignatory"] = self.notary_is_signatory
222
+ return result
223
+
224
+
225
+ @dataclass(frozen=True)
226
+ class RapDraftInputs:
227
+ """Wire shape of ``DraftInputsState`` (serde camelCase)."""
228
+ state: str
229
+ network: str
230
+ root_manifest: RapRootManifest
231
+ notary: RapNotary
232
+ agent_public_key: str
233
+ requested_tariff: str
234
+ subintents: Optional[Any] = None
235
+
236
+ def to_dict(self) -> dict[str, Any]:
237
+ result: dict[str, Any] = {
238
+ "state": self.state,
239
+ "network": self.network,
240
+ "rootManifest": self.root_manifest.to_dict(),
241
+ "notary": self.notary.to_dict(),
242
+ "agentPublicKey": self.agent_public_key,
243
+ "requestedTariff": self.requested_tariff,
244
+ }
245
+ if self.subintents is not None:
246
+ result["subintents"] = self.subintents
247
+ return result
248
+
249
+
250
+ @dataclass(frozen=True)
251
+ class RapEconomicParams:
252
+ """Wire shape of ``LedgerEconomicParams`` (serde snake_case)."""
253
+ fee_loan_amount: str
254
+ registry_component: str
255
+ lock_fee_payer_account: str
256
+ usdc_resource: str
257
+ xrd_gas_vault: str
258
+ merchant_account: str
259
+ start_epoch_inclusive: Optional[int] = None
260
+ end_epoch_exclusive: Optional[int] = None
261
+ intent_discriminator: Optional[int] = None
262
+
263
+ def to_dict(self) -> dict[str, Any]:
264
+ result: dict[str, Any] = {
265
+ "fee_loan_amount": self.fee_loan_amount,
266
+ "registry_component": self.registry_component,
267
+ "lock_fee_payer_account": self.lock_fee_payer_account,
268
+ "usdc_resource": self.usdc_resource,
269
+ "xrd_gas_vault": self.xrd_gas_vault,
270
+ "merchant_account": self.merchant_account,
271
+ }
272
+ if self.start_epoch_inclusive is not None:
273
+ result["start_epoch_inclusive"] = self.start_epoch_inclusive
274
+ if self.end_epoch_exclusive is not None:
275
+ result["end_epoch_exclusive"] = self.end_epoch_exclusive
276
+ if self.intent_discriminator is not None:
277
+ result["intent_discriminator"] = self.intent_discriminator
278
+ return result
279
+
280
+
281
+ @dataclass(frozen=True)
282
+ class RapSignatureFile:
283
+ """Wire shape of ``LegacySignatureFile`` (serde camelCase)."""
284
+ type: str
285
+ version: int
286
+ scope: dict[str, Any]
287
+ signature: RapSignatureValue
288
+ hash: RapHash
289
+ public_key: RapPublicKey
290
+
291
+ def to_dict(self) -> dict[str, Any]:
292
+ return {
293
+ "type": self.type,
294
+ "version": self.version,
295
+ "scope": self.scope,
296
+ "signature": self.signature.to_dict(),
297
+ "hash": self.hash.to_dict(),
298
+ "publicKey": self.public_key.to_dict(),
299
+ }
300
+
301
+
302
+ @dataclass(frozen=True)
303
+ class RapPayload:
304
+ """The ``rap_payload`` object expected by ``handleModeBSponsored``."""
305
+ draft_inputs: RapDraftInputs
306
+ economic_params: RapEconomicParams
307
+ signatures: RapSignatureFile
308
+
309
+ def to_dict(self) -> dict[str, Any]:
310
+ return {
311
+ "draft_inputs": self.draft_inputs.to_dict(),
312
+ "economic_params": self.economic_params.to_dict(),
313
+ "signatures": self.signatures.to_dict(),
314
+ }
315
+
316
+
317
+ # ── Fetch Extensions ───────────────────────────────────────────────────
318
+
319
+ @dataclass(frozen=True)
320
+ class AgentSyncFetchOptions:
321
+ """Extended request options for AgentSyncClient.fetch."""
322
+ method: str = "GET"
323
+ headers: Optional[dict[str, str]] = None
324
+ body: Optional[Any] = None
325
+ # Mode B (Single-Pay): the RAP payload object. The SDK serializes this as
326
+ # `{ payment: { rap_payload, requirements } }` in the request body.
327
+ rap_payload: Optional[RapPayload] = None
328
+ requirements: Optional[dict[str, Any]] = None
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentsync-sdk
3
+ Version: 1.0.0
4
+ Summary: Python SDK for the AgentSync x402 micropayment gateway on Radix
5
+ License: MIT
6
+ Classifier: Development Status :: 5 - Production/Stable
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: httpx>=0.27.0
17
+ Requires-Dist: PyNaCl>=1.5.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == "dev"
20
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
21
+ Requires-Dist: pytest-httpx>=0.30.0; extra == "dev"
22
+
23
+ # AgentSync Python SDK
24
+
25
+ Python SDK for the AgentSync x402 micropayment gateway on Radix.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install agentsync-sdk
31
+ ```
32
+
33
+ > Note: the distribution name is `agentsync-sdk`; the import package remains
34
+ > `agentsync` (`from agentsync import ...`). For local development use
35
+ > `pip install -e .` in the repo.
36
+
37
+ ## Development
38
+
39
+ ```bash
40
+ pip install -e ".[dev]"
41
+ pytest
42
+ ```
43
+
44
+ ## Platform Attestation Verification
45
+
46
+ Verify a platform-signed attestation envelope against the trusted platform
47
+ Ed25519 public key:
48
+
49
+ ```python
50
+ from agentsync import SignatureEnvelope, verify_platform_attestation
51
+
52
+ trusted_public_key = "9afcc1cf66b256d7d9567a906fba4317305d92c3e7fd7f27386dffe182332576"
53
+ envelope = SignatureEnvelope(
54
+ payload="account_tdx_2_...:5.500000:0",
55
+ signature="<128-char hex>",
56
+ public_key=trusted_public_key,
57
+ algorithm="ed25519+sha256",
58
+ )
59
+
60
+ valid = verify_platform_attestation(envelope, trusted_public_key) # bool
61
+ ```
62
+
63
+ `verify_platform_attestation()` is pure and synchronous: the trusted key is
64
+ injected by the caller and never fetched from the network. It asserts that the
65
+ envelope's `public_key` matches the trusted key before verifying the
66
+ `Ed25519(SHA-256(payload))` signature.
67
+
68
+ The TypeScript counterpart lives at [`../sdk-typescript`](../sdk-typescript).
@@ -0,0 +1,10 @@
1
+ agentsync/__init__.py,sha256=0zQIqBWkq3Lm06SwVL5BtqlITPU4VkQgxuZFWSMmW0E,2368
2
+ agentsync/attestation.py,sha256=CQu4gM_8eVH5bptBfLBE5zPRuoNXz7GxK9HfJtcU8Zk,2899
3
+ agentsync/client.py,sha256=2tlEFvEOMJnaqGvW8nrMX4VhFcGp7YrTORsWyoKcAOg,20724
4
+ agentsync/errors.py,sha256=UkTaAywvscWMDsEruX_bpCQQ3omRC5Q17TrAQNVJiRY,1550
5
+ agentsync/rap.py,sha256=A15PsV7RiUYKgtpAaVoCRlUMtimvKTDiT4vVK74nTYI,6035
6
+ agentsync/types.py,sha256=f3JfvkYrm69WlFmlAJkQpP7Bz1UhNTjb1z1xOGSBrGI,10582
7
+ agentsync_sdk-1.0.0.dist-info/METADATA,sha256=xfIS2e3_zBD_QvEXPj-F66u88IqZR1bvTrgWXEkQC-Y,2117
8
+ agentsync_sdk-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ agentsync_sdk-1.0.0.dist-info/top_level.txt,sha256=8R5D9yLyyVwJIvwInvOWXj2hiEny0OnFg03eJFLvO14,10
10
+ agentsync_sdk-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ agentsync