aotrust-protocol 2.3.2__tar.gz → 2.3.4__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.
- {aotrust_protocol-2.3.2/aotrust_protocol.egg-info → aotrust_protocol-2.3.4}/PKG-INFO +1 -1
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/agent_notary/__init__.py +2 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/agent_notary/client.py +92 -15
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4/aotrust_protocol.egg-info}/PKG-INFO +1 -1
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/aotrust_protocol.egg-info/SOURCES.txt +2 -1
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/pyproject.toml +5 -1
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/tests/test_client.py +1 -1
- aotrust_protocol-2.3.4/tests/test_live_smoke.py +92 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/LICENSE +0 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/README.md +0 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/agent_notary/exceptions.py +0 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/agent_notary/models.py +0 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/agent_notary/tools.py +0 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/aotrust_protocol.egg-info/dependency_links.txt +0 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/aotrust_protocol.egg-info/requires.txt +0 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/aotrust_protocol.egg-info/top_level.txt +0 -0
- {aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/setup.cfg +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Asyncio-native NotaryClient for
|
|
1
|
+
"""Asyncio-native NotaryClient for AOTrust."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -23,9 +23,19 @@ DEFAULT_POLL_TIMEOUT = 120.0
|
|
|
23
23
|
MAX_RETRIES = 3
|
|
24
24
|
RETRY_DELAYS = (1.0, 2.0, 4.0)
|
|
25
25
|
|
|
26
|
+
VERIFY_WEB_BASE = "https://verify.aotrust.link"
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
class NotaryClient:
|
|
28
|
-
"""Async client for the
|
|
30
|
+
"""Async client for the AOTrust Notary API.
|
|
31
|
+
|
|
32
|
+
URL map matches the live API (openapi.yaml v2.3.4+):
|
|
33
|
+
- notarize() -> POST /notarize (anonymous x402 payment path)
|
|
34
|
+
- quote() -> POST /v1/notarize/quote (authenticated CI channel; api_key required)
|
|
35
|
+
- get_status() -> GET /v1/status/{job_id}
|
|
36
|
+
- shield_free()-> POST /v1/shield/free (free tier, rate limited)
|
|
37
|
+
- pubkey lazy-fetch: GET /v1/notary/pubkey (cached)
|
|
38
|
+
"""
|
|
29
39
|
|
|
30
40
|
def __init__(
|
|
31
41
|
self,
|
|
@@ -34,15 +44,44 @@ class NotaryClient:
|
|
|
34
44
|
notary_pubkey: Optional[str] = None,
|
|
35
45
|
):
|
|
36
46
|
self.api_key = api_key or os.environ.get("NOTARY_API_KEY", "")
|
|
37
|
-
self.base_url = (base_url or os.environ.get("NOTARY_API_URL", "https://api.aotrust.link
|
|
38
|
-
|
|
47
|
+
self.base_url = (base_url or os.environ.get("NOTARY_API_URL", "https://api.aotrust.link")).rstrip("/")
|
|
48
|
+
# No hardcoded fallback: if not provided, fetched lazily from the server (cached).
|
|
49
|
+
self._pubkey_provided = notary_pubkey or os.environ.get("NOTARY_PUBKEY", "") or ""
|
|
50
|
+
self._pubkey_fetched: Optional[str] = None
|
|
39
51
|
|
|
40
52
|
def _headers(self) -> dict:
|
|
41
|
-
|
|
53
|
+
from . import __version__
|
|
54
|
+
h = {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
"User-Agent": f"aotrust-protocol/{__version__}",
|
|
57
|
+
}
|
|
42
58
|
if self.api_key:
|
|
43
59
|
h["X-API-Key"] = self.api_key
|
|
44
60
|
return h
|
|
45
61
|
|
|
62
|
+
async def get_notary_pubkey(self) -> str:
|
|
63
|
+
"""Notary Ed25519 public key (hex, 64 chars).
|
|
64
|
+
|
|
65
|
+
Resolution order:
|
|
66
|
+
1. explicitly provided (constructor / NOTARY_PUBKEY env)
|
|
67
|
+
2. lazy fetch GET /v1/notary/pubkey — fetched once, cached
|
|
68
|
+
Raises NotaryError when neither is available.
|
|
69
|
+
"""
|
|
70
|
+
if self._pubkey_provided:
|
|
71
|
+
return self._pubkey_provided
|
|
72
|
+
if self._pubkey_fetched:
|
|
73
|
+
return self._pubkey_fetched
|
|
74
|
+
status, body = await self._request("GET", "/v1/notary/pubkey")
|
|
75
|
+
self._check_status(status, body)
|
|
76
|
+
pubkey = body.get("pubkey_hex", body.get("pubkey", body.get("notary_pubkey", "")))
|
|
77
|
+
if not pubkey:
|
|
78
|
+
raise NotaryError(
|
|
79
|
+
"notary_pubkey not provided and not available from server "
|
|
80
|
+
f"(GET /v1/notary/pubkey returned: {body})"
|
|
81
|
+
)
|
|
82
|
+
self._pubkey_fetched = pubkey
|
|
83
|
+
return pubkey
|
|
84
|
+
|
|
46
85
|
async def _request(
|
|
47
86
|
self,
|
|
48
87
|
method: str,
|
|
@@ -70,6 +109,9 @@ class NotaryClient:
|
|
|
70
109
|
def _check_status(self, status: int, body: dict) -> None:
|
|
71
110
|
if status == 401:
|
|
72
111
|
raise NotaryAuthError(body.get("error", "Unauthorized"))
|
|
112
|
+
if status == 402:
|
|
113
|
+
# x402 challenge: surface the full payment requirement to the caller
|
|
114
|
+
raise NotaryPaymentError(f"Payment required: {body}")
|
|
73
115
|
if status == 403:
|
|
74
116
|
detail = body.get("detail", body.get("error", "Forbidden"))
|
|
75
117
|
raise NotaryPaymentError(detail)
|
|
@@ -81,14 +123,26 @@ class NotaryClient:
|
|
|
81
123
|
raise NotaryServerError(body.get("error", f"Server error {status}"), status_code=status)
|
|
82
124
|
|
|
83
125
|
async def notarize(self, request: NotarizeRequest) -> NotarizeResult:
|
|
84
|
-
|
|
126
|
+
"""POST /notarize — anonymous x402 path.
|
|
127
|
+
|
|
128
|
+
Without payment the server answers 402 with the x402 challenge
|
|
129
|
+
(x402Version/accepts[0].resource=...); NotaryPaymentError carries it.
|
|
130
|
+
Payload built for the live x402 contract: work_hash at top level
|
|
131
|
+
(the CI-channel envelope with tx_hash/payload.work_hash is NOT used here).
|
|
132
|
+
"""
|
|
133
|
+
payload: dict = {"work_hash": request.work_hash or ""}
|
|
134
|
+
if request.agent_sig:
|
|
135
|
+
payload["agent_sig"] = request.agent_sig
|
|
136
|
+
if request.agent_pubkey:
|
|
137
|
+
payload["agent_pubkey"] = request.agent_pubkey
|
|
138
|
+
status, body = await self._request("POST", "/notarize", json_data=payload)
|
|
85
139
|
self._check_status(status, body)
|
|
86
140
|
if status not in (200, 202):
|
|
87
141
|
raise NotaryError(f"Unexpected status {status}: {body}")
|
|
88
142
|
return NotarizeResult(job_id=body.get("job_id", ""))
|
|
89
143
|
|
|
90
144
|
async def get_status(self, job_id: str) -> StatusResult:
|
|
91
|
-
status, body = await self._request("GET", f"/status/{job_id}")
|
|
145
|
+
status, body = await self._request("GET", f"/v1/status/{job_id}")
|
|
92
146
|
self._check_status(status, body)
|
|
93
147
|
result = body.get("result", {})
|
|
94
148
|
return StatusResult(
|
|
@@ -98,15 +152,28 @@ class NotaryClient:
|
|
|
98
152
|
)
|
|
99
153
|
|
|
100
154
|
async def get_pdr(self, job_id: str) -> PdrResult:
|
|
101
|
-
status
|
|
102
|
-
self.
|
|
103
|
-
|
|
155
|
+
"""Fetch PDR via status endpoint and build the public verify URL."""
|
|
156
|
+
status_result = await self.get_status(job_id)
|
|
157
|
+
pdr_b64 = status_result.pdr_b64 or ""
|
|
104
158
|
return PdrResult(
|
|
105
|
-
pdr_b64=
|
|
106
|
-
payment_hash=
|
|
107
|
-
verify_url=
|
|
159
|
+
pdr_b64=pdr_b64,
|
|
160
|
+
payment_hash=status_result.payment_hash,
|
|
161
|
+
verify_url=self._verify_url(pdr_b64),
|
|
108
162
|
)
|
|
109
163
|
|
|
164
|
+
async def verify_pdr(self, pdr_b64: str) -> dict:
|
|
165
|
+
"""GET /v1/pdr/verify/{pdr_b64} — server-side PDR verification."""
|
|
166
|
+
from urllib.parse import quote as _urlquote
|
|
167
|
+
status, body = await self._request("GET", f"/v1/pdr/verify/{_urlquote(pdr_b64, safe='')}")
|
|
168
|
+
self._check_status(status, body)
|
|
169
|
+
return body
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _verify_url(pdr_b64: str) -> str:
|
|
173
|
+
"""Public web verifier URL (the /v1/verify/{job_id} path does not exist)."""
|
|
174
|
+
b64url = pdr_b64.replace("+", "-").replace("/", "_").rstrip("=")
|
|
175
|
+
return f"{VERIFY_WEB_BASE}/?pdr={b64url}"
|
|
176
|
+
|
|
110
177
|
async def wait_for_pdr(
|
|
111
178
|
self,
|
|
112
179
|
job_id: str,
|
|
@@ -125,7 +192,17 @@ class NotaryClient:
|
|
|
125
192
|
raise NotaryServerError(f"Timed out waiting for PDR ({timeout}s)")
|
|
126
193
|
|
|
127
194
|
async def quote(self) -> dict:
|
|
128
|
-
|
|
195
|
+
"""POST /v1/notarize/quote — authenticated CI channel.
|
|
196
|
+
|
|
197
|
+
Requires api_key; the public x402 flow does not need a quote
|
|
198
|
+
(price is always $0.01, see the 402 challenge from notarize()).
|
|
199
|
+
"""
|
|
200
|
+
if not self.api_key:
|
|
201
|
+
raise NotaryAuthError(
|
|
202
|
+
"quote() requires an API key (authenticated CI channel, POST /v1/notarize/quote). "
|
|
203
|
+
"The public x402 flow needs no quote: price is flat $0.01 per PDR."
|
|
204
|
+
)
|
|
205
|
+
status, body = await self._request("POST", "/v1/notarize/quote")
|
|
129
206
|
self._check_status(status, body)
|
|
130
207
|
return body
|
|
131
208
|
|
|
@@ -148,6 +225,6 @@ class NotaryClient:
|
|
|
148
225
|
payload["agent_sig"] = agent_sig
|
|
149
226
|
if agent_pubkey:
|
|
150
227
|
payload["agent_pubkey"] = agent_pubkey
|
|
151
|
-
status, body = await self._request("POST", "/shield/free", json_data=payload)
|
|
228
|
+
status, body = await self._request("POST", "/v1/shield/free", json_data=payload)
|
|
152
229
|
self._check_status(status, body)
|
|
153
230
|
return body
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aotrust-protocol"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.4"
|
|
8
8
|
description = "Asyncio-native SDK for A&O Trust Layer — Agent Notary Service"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -40,4 +40,8 @@ include = ["agent_notary*"]
|
|
|
40
40
|
|
|
41
41
|
[tool.pytest.ini_options]
|
|
42
42
|
asyncio_mode = "auto"
|
|
43
|
+
markers = [
|
|
44
|
+
"live: live smoke tests against production (run explicitly with -m live)",
|
|
45
|
+
]
|
|
46
|
+
addopts = "-m 'not live'"
|
|
43
47
|
testpaths = ["tests"]
|
|
@@ -116,7 +116,7 @@ class TestNotaryClientErrors:
|
|
|
116
116
|
class TestNotaryClientConstruction:
|
|
117
117
|
def test_default_base_url(self):
|
|
118
118
|
client = NotaryClient(api_key="k")
|
|
119
|
-
assert "aotrust.link
|
|
119
|
+
assert client.base_url == "https://api.aotrust.link"
|
|
120
120
|
|
|
121
121
|
def test_custom_base_url(self):
|
|
122
122
|
client = NotaryClient(api_key="k", base_url="http://localhost:9999/v1")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
test_live_smoke.py — живой smoke-тест SDK против прода (новый протокол).
|
|
3
|
+
|
|
4
|
+
Запуск ТОЛЬКО явно: `pytest -m live`. По умолчанию скипается.
|
|
5
|
+
НЕ платит: 402-челлендж — граница теста (кошелёк/ключи не трогаются).
|
|
6
|
+
|
|
7
|
+
Путь (ровно тот, что прошёл node-агент):
|
|
8
|
+
1. Чистый клиент без ключей, дефолтный base_url (без /v1)
|
|
9
|
+
2. notarize() → 402-челлендж x402 (x402Version/accepts[0].resource — полный URL)
|
|
10
|
+
3. get_status() на свежем (несуществующем) job → 404 NotaryNotFoundError (эндпоинт жив)
|
|
11
|
+
4. get_notary_pubkey() → pubkey подтянут с сервера (lazy-fetch работает)
|
|
12
|
+
|
|
13
|
+
Асинхронные вызовы обёрнуты asyncio.run — тест не зависит от pytest_asyncio.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import asyncio
|
|
19
|
+
import uuid
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
PROJECT_ROOT = str(Path(__file__).resolve().parent.parent)
|
|
23
|
+
sys.path.insert(0, PROJECT_ROOT)
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
pytestmark = [pytest.mark.live]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _live_guard():
|
|
31
|
+
"""Пропускать, если live-тесты выключены (CI/offline)."""
|
|
32
|
+
if os.environ.get("ANS_LIVE_TESTS") == "0":
|
|
33
|
+
pytest.skip("ANS_LIVE_TESTS=0")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_live_notarize_402_challenge():
|
|
37
|
+
_live_guard()
|
|
38
|
+
from agent_notary import NotaryClient, NotarizeRequest
|
|
39
|
+
from agent_notary.exceptions import NotaryPaymentError
|
|
40
|
+
|
|
41
|
+
client = NotaryClient() # чистый: без api_key, без pubkey
|
|
42
|
+
assert client.base_url == "https://api.aotrust.link"
|
|
43
|
+
req = NotarizeRequest(tx_hash="0x" + "a" * 64, work_hash="b" * 64)
|
|
44
|
+
|
|
45
|
+
async def run():
|
|
46
|
+
try:
|
|
47
|
+
await client.notarize(req)
|
|
48
|
+
return None
|
|
49
|
+
except NotaryPaymentError as e:
|
|
50
|
+
return str(e)
|
|
51
|
+
|
|
52
|
+
msg = asyncio.run(run())
|
|
53
|
+
assert msg is not None, "notarize() без оплаты должен дать NotaryPaymentError (402)"
|
|
54
|
+
# 402-челлендж: тело содержит x402-структуру с полным resource URL
|
|
55
|
+
assert "402" in msg, f"expected 402 in error body: {msg[:200]}"
|
|
56
|
+
assert "accepts" in msg, f"expected x402 accepts[] in error body: {msg[:200]}"
|
|
57
|
+
assert "resource" in msg, f"expected resource in error body: {msg[:200]}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_live_get_status_404_on_unknown_job():
|
|
61
|
+
_live_guard()
|
|
62
|
+
from agent_notary import NotaryClient
|
|
63
|
+
from agent_notary.exceptions import NotaryNotFoundError
|
|
64
|
+
|
|
65
|
+
client = NotaryClient()
|
|
66
|
+
fresh_job = str(uuid.uuid4())
|
|
67
|
+
|
|
68
|
+
async def run():
|
|
69
|
+
try:
|
|
70
|
+
await client.get_status(fresh_job)
|
|
71
|
+
return None
|
|
72
|
+
except NotaryNotFoundError as e:
|
|
73
|
+
return str(e)
|
|
74
|
+
|
|
75
|
+
msg = asyncio.run(run())
|
|
76
|
+
assert msg is not None, "get_status() на несуществующем job должен дать 404"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_live_pubkey_lazy_fetch():
|
|
80
|
+
_live_guard()
|
|
81
|
+
from agent_notary import NotaryClient
|
|
82
|
+
|
|
83
|
+
client = NotaryClient()
|
|
84
|
+
|
|
85
|
+
async def run():
|
|
86
|
+
return await client.get_notary_pubkey()
|
|
87
|
+
|
|
88
|
+
pubkey = asyncio.run(run())
|
|
89
|
+
assert isinstance(pubkey, str) and len(pubkey) == 64, f"pubkey hex 64 chars, got: {pubkey!r}"
|
|
90
|
+
assert all(c in "0123456789abcdef" for c in pubkey.lower())
|
|
91
|
+
# Кеш: второй вызов не ходит в сеть (тот же атрибут)
|
|
92
|
+
assert client._pubkey_fetched == pubkey
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aotrust_protocol-2.3.2 → aotrust_protocol-2.3.4}/aotrust_protocol.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|