aotrust-protocol 2.3.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A&O Trust Layer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: aotrust-protocol
3
+ Version: 2.3.0
4
+ Summary: Asyncio-native SDK for A&O Trust Layer — Agent Notary Service
5
+ Author: A&O Trust Layer
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Topic :: Security :: Cryptography
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: aiohttp>=3.9
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: pynacl>=1.5
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
24
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
25
+ Requires-Dist: aioresponses>=0.7; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # aotrust-protocol SDK
29
+
30
+ Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install aotrust-protocol
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ ```python
41
+ import asyncio
42
+ from agent_notary import NotaryClient, NotarizeRequest
43
+
44
+ async def main():
45
+ client = NotaryClient(
46
+ api_key="your-api-key",
47
+ base_url="https://api.aotrust.link/v1"
48
+ )
49
+
50
+ # Submit notarization
51
+ req = NotarizeRequest(
52
+ tx_hash="EzrfDW5b...",
53
+ work_hash="599d6999...",
54
+ agent_sig="base64_sig_A...",
55
+ agent_pubkey="aff91a18...",
56
+ )
57
+ result = await client.notarize(req)
58
+ print(f"Job: {result.job_id}")
59
+
60
+ # Poll for PDR
61
+ status = await client.wait_for_pdr(result.job_id, timeout=60)
62
+ if status.is_anchored:
63
+ pdr = await client.get_pdr(result.job_id)
64
+ assert pdr.verify(client.notary_pubkey)
65
+ print("PDR Valid: YES")
66
+
67
+ asyncio.run(main())
68
+ ```
69
+
70
+ ## Configuration
71
+
72
+ | Env Var | Constructor Arg | Description |
73
+ |---------|-----------------|-------------|
74
+ | `NOTARY_API_KEY` | `api_key` | API key for auth |
75
+ | `NOTARY_API_URL` | `base_url` | API base URL |
76
+ | `NOTARY_PUBKEY` | `notary_pubkey` | Notary Ed25519 public key (hex) |
77
+
78
+ ## Endpoints
79
+
80
+ | Method | Path | Description |
81
+ |--------|------|-------------|
82
+ | POST | `/v1/notarize` | Submit notarization |
83
+ | GET | `/v1/status/{job_id}` | Poll job status |
84
+ | GET | `/v1/pdr/{job_id}` | Get PDR |
85
+ | POST | `/v1/notarize/quote` | Get price quote |
86
+ | GET | `/.well-known/agent.json` | MCP Server Card |
87
+ | GET | `/openapi.json` | OpenAPI 3.0 spec |
88
+
89
+ ## PDR Verification
90
+
91
+ ```python
92
+ pdr = await client.get_pdr(job_id)
93
+ is_valid = pdr.verify(notary_pubkey_hex="9c7d64bb...")
94
+ ```
95
+
96
+ Uses NEP-413 raw-buffer verification. No SHA256 pre-hash.
97
+
98
+ ## Error Handling
99
+
100
+ ```python
101
+ from agent_notary.exceptions import NotaryAuthError, NotaryPaymentError, NotaryNotFoundError
102
+
103
+ try:
104
+ result = await client.notarize(req)
105
+ except NotaryAuthError:
106
+ print("Invalid API key")
107
+ except NotaryPaymentError as e:
108
+ print(f"Payment/sig failed: {e}")
109
+ ```
110
+
111
+ ## Development
112
+
113
+ ```bash
114
+ pip install -e ".[dev]"
115
+ pytest tests/ -v
116
+ ```
@@ -0,0 +1,89 @@
1
+ # aotrust-protocol SDK
2
+
3
+ Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install aotrust-protocol
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ import asyncio
15
+ from agent_notary import NotaryClient, NotarizeRequest
16
+
17
+ async def main():
18
+ client = NotaryClient(
19
+ api_key="your-api-key",
20
+ base_url="https://api.aotrust.link/v1"
21
+ )
22
+
23
+ # Submit notarization
24
+ req = NotarizeRequest(
25
+ tx_hash="EzrfDW5b...",
26
+ work_hash="599d6999...",
27
+ agent_sig="base64_sig_A...",
28
+ agent_pubkey="aff91a18...",
29
+ )
30
+ result = await client.notarize(req)
31
+ print(f"Job: {result.job_id}")
32
+
33
+ # Poll for PDR
34
+ status = await client.wait_for_pdr(result.job_id, timeout=60)
35
+ if status.is_anchored:
36
+ pdr = await client.get_pdr(result.job_id)
37
+ assert pdr.verify(client.notary_pubkey)
38
+ print("PDR Valid: YES")
39
+
40
+ asyncio.run(main())
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ | Env Var | Constructor Arg | Description |
46
+ |---------|-----------------|-------------|
47
+ | `NOTARY_API_KEY` | `api_key` | API key for auth |
48
+ | `NOTARY_API_URL` | `base_url` | API base URL |
49
+ | `NOTARY_PUBKEY` | `notary_pubkey` | Notary Ed25519 public key (hex) |
50
+
51
+ ## Endpoints
52
+
53
+ | Method | Path | Description |
54
+ |--------|------|-------------|
55
+ | POST | `/v1/notarize` | Submit notarization |
56
+ | GET | `/v1/status/{job_id}` | Poll job status |
57
+ | GET | `/v1/pdr/{job_id}` | Get PDR |
58
+ | POST | `/v1/notarize/quote` | Get price quote |
59
+ | GET | `/.well-known/agent.json` | MCP Server Card |
60
+ | GET | `/openapi.json` | OpenAPI 3.0 spec |
61
+
62
+ ## PDR Verification
63
+
64
+ ```python
65
+ pdr = await client.get_pdr(job_id)
66
+ is_valid = pdr.verify(notary_pubkey_hex="9c7d64bb...")
67
+ ```
68
+
69
+ Uses NEP-413 raw-buffer verification. No SHA256 pre-hash.
70
+
71
+ ## Error Handling
72
+
73
+ ```python
74
+ from agent_notary.exceptions import NotaryAuthError, NotaryPaymentError, NotaryNotFoundError
75
+
76
+ try:
77
+ result = await client.notarize(req)
78
+ except NotaryAuthError:
79
+ print("Invalid API key")
80
+ except NotaryPaymentError as e:
81
+ print(f"Payment/sig failed: {e}")
82
+ ```
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ pip install -e ".[dev]"
88
+ pytest tests/ -v
89
+ ```
@@ -0,0 +1,26 @@
1
+ """A&O Trust Layer — Agent Notary SDK (asyncio-native)."""
2
+
3
+ from .client import NotaryClient
4
+ from .models import NotarizeRequest, NotarizeResult, PdrResult, StatusResult
5
+ from .exceptions import (
6
+ NotaryError,
7
+ NotaryAuthError,
8
+ NotaryNotFoundError,
9
+ NotaryPaymentError,
10
+ NotaryServerError,
11
+ )
12
+ from .tools import AOTrustTool
13
+
14
+ __all__ = [
15
+ "NotaryClient",
16
+ "NotarizeRequest",
17
+ "NotarizeResult",
18
+ "PdrResult",
19
+ "StatusResult",
20
+ "NotaryError",
21
+ "NotaryAuthError",
22
+ "NotaryNotFoundError",
23
+ "NotaryPaymentError",
24
+ "NotaryServerError",
25
+ "AOTrustTool",
26
+ ]
@@ -0,0 +1,153 @@
1
+ """Asyncio-native NotaryClient for A&O Trust Layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from typing import Optional
8
+
9
+ import aiohttp
10
+
11
+ from .models import NotarizeRequest, NotarizeResult, StatusResult, PdrResult
12
+ from .exceptions import (
13
+ NotaryError,
14
+ NotaryAuthError,
15
+ NotaryNotFoundError,
16
+ NotaryPaymentError,
17
+ NotaryServerError,
18
+ )
19
+
20
+ DEFAULT_TIMEOUT = 30
21
+ DEFAULT_POLL_INTERVAL = 2.0
22
+ DEFAULT_POLL_TIMEOUT = 120.0
23
+ MAX_RETRIES = 3
24
+ RETRY_DELAYS = (1.0, 2.0, 4.0)
25
+
26
+
27
+ class NotaryClient:
28
+ """Async client for the A&O Trust Layer Notary API."""
29
+
30
+ def __init__(
31
+ self,
32
+ api_key: Optional[str] = None,
33
+ base_url: Optional[str] = None,
34
+ notary_pubkey: Optional[str] = None,
35
+ ):
36
+ 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/v1")).rstrip("/")
38
+ self.notary_pubkey = notary_pubkey or os.environ.get("NOTARY_PUBKEY", "9c7d64bbabe90ead8f18982497a47acbd95677cef0d9285caec95377e4ccf6b0")
39
+
40
+ def _headers(self) -> dict:
41
+ h = {"Content-Type": "application/json"}
42
+ if self.api_key:
43
+ h["X-API-Key"] = self.api_key
44
+ return h
45
+
46
+ async def _request(
47
+ self,
48
+ method: str,
49
+ path: str,
50
+ json_data: Optional[dict] = None,
51
+ timeout: float = DEFAULT_TIMEOUT,
52
+ ) -> tuple[int, dict]:
53
+ url = f"{self.base_url}{path}"
54
+ last_exc = None
55
+ for attempt in range(MAX_RETRIES):
56
+ try:
57
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session:
58
+ async with session.request(method, url, headers=self._headers(), json=json_data) as resp:
59
+ body = await resp.json(content_type=None) if resp.content_type and "json" in resp.content_type else {}
60
+ if resp.status in (502, 503) and attempt < MAX_RETRIES - 1:
61
+ await asyncio.sleep(RETRY_DELAYS[attempt])
62
+ continue
63
+ return resp.status, body
64
+ except aiohttp.ClientError as exc:
65
+ last_exc = exc
66
+ if attempt < MAX_RETRIES - 1:
67
+ await asyncio.sleep(RETRY_DELAYS[attempt])
68
+ raise NotaryServerError(f"Request failed after {MAX_RETRIES} retries: {last_exc}")
69
+
70
+ def _check_status(self, status: int, body: dict) -> None:
71
+ if status == 401:
72
+ raise NotaryAuthError(body.get("error", "Unauthorized"))
73
+ if status == 403:
74
+ detail = body.get("detail", body.get("error", "Forbidden"))
75
+ raise NotaryPaymentError(detail)
76
+ if status == 404:
77
+ raise NotaryNotFoundError(body.get("error", "Not found"))
78
+ if status == 429:
79
+ raise NotaryError(body.get("error", body.get("message", "Rate limit reached")))
80
+ if status >= 500:
81
+ raise NotaryServerError(body.get("error", f"Server error {status}"), status_code=status)
82
+
83
+ async def notarize(self, request: NotarizeRequest) -> NotarizeResult:
84
+ status, body = await self._request("POST", "/notarize", json_data=request.to_api_payload())
85
+ self._check_status(status, body)
86
+ if status not in (200, 202):
87
+ raise NotaryError(f"Unexpected status {status}: {body}")
88
+ return NotarizeResult(job_id=body.get("job_id", ""))
89
+
90
+ async def get_status(self, job_id: str) -> StatusResult:
91
+ status, body = await self._request("GET", f"/status/{job_id}")
92
+ self._check_status(status, body)
93
+ result = body.get("result", {})
94
+ return StatusResult(
95
+ status=body.get("status", "unknown"),
96
+ pdr_b64=result.get("pdr_b64"),
97
+ payment_hash=result.get("near_tx_hash") or result.get("payment_hash"),
98
+ )
99
+
100
+ async def get_pdr(self, job_id: str) -> PdrResult:
101
+ status, body = await self._request("GET", f"/pdr/{job_id}")
102
+ self._check_status(status, body)
103
+ verify_url = f"{self.base_url}/verify/{job_id}" if "verify_url" not in body else body.get("verify_url")
104
+ return PdrResult(
105
+ pdr_b64=body.get("pdr", body.get("pdr_b64", "")),
106
+ payment_hash=body.get("near_tx_hash") or body.get("payment_hash"),
107
+ verify_url=verify_url,
108
+ )
109
+
110
+ async def wait_for_pdr(
111
+ self,
112
+ job_id: str,
113
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
114
+ timeout: float = DEFAULT_POLL_TIMEOUT,
115
+ ) -> StatusResult:
116
+ elapsed = 0.0
117
+ while elapsed < timeout:
118
+ result = await self.get_status(job_id)
119
+ if result.is_anchored:
120
+ return result
121
+ if result.status.lower() in ("rejected", "error", "failed"):
122
+ raise NotaryPaymentError(f"Notarization {result.status}")
123
+ await asyncio.sleep(poll_interval)
124
+ elapsed += poll_interval
125
+ raise NotaryServerError(f"Timed out waiting for PDR ({timeout}s)")
126
+
127
+ async def quote(self) -> dict:
128
+ status, body = await self._request("POST", "/notarize/quote")
129
+ self._check_status(status, body)
130
+ return body
131
+
132
+ async def shield_free(self, work_hash: str, agent_sig: str = None, agent_pubkey: str = None) -> dict:
133
+ """Create a free UNPAID PDR (no wallet required).
134
+
135
+ Args:
136
+ work_hash: SHA-256 hex string (64 chars)
137
+ agent_sig: Optional base64 Ed25519 signature (for bilateral v0x04)
138
+ agent_pubkey: Optional hex Ed25519 public key
139
+
140
+ Returns:
141
+ dict with: status, job_id, pdr_b64, shield_id, payment_anchor_type, tier
142
+
143
+ Raises:
144
+ NotaryError: on API error or rate limit (429)
145
+ """
146
+ payload: dict = {"work_hash": work_hash}
147
+ if agent_sig:
148
+ payload["agent_sig"] = agent_sig
149
+ if agent_pubkey:
150
+ payload["agent_pubkey"] = agent_pubkey
151
+ status, body = await self._request("POST", "/shield/free", json_data=payload)
152
+ self._check_status(status, body)
153
+ return body
@@ -0,0 +1,25 @@
1
+ """Custom exceptions for NotaryClient."""
2
+
3
+
4
+ class NotaryError(Exception):
5
+ """Base exception for all NotaryClient errors."""
6
+
7
+
8
+ class NotaryAuthError(NotaryError):
9
+ """401 Unauthorized — invalid or missing API key."""
10
+
11
+
12
+ class NotaryPaymentError(NotaryError):
13
+ """403 Forbidden — payment verification or agent signature failed."""
14
+
15
+
16
+ class NotaryNotFoundError(NotaryError):
17
+ """404 Not Found — job_id or PDR not found."""
18
+
19
+
20
+ class NotaryServerError(NotaryError):
21
+ """5xx server error with retry context."""
22
+
23
+ def __init__(self, message: str, status_code: int = 0):
24
+ super().__init__(message)
25
+ self.status_code = status_code
@@ -0,0 +1,148 @@
1
+ """Pydantic models for NotaryClient request/response schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import struct
7
+ from enum import IntEnum
8
+ from typing import Optional
9
+ from pydantic import BaseModel, Field, validator
10
+
11
+ NEP413_TAG = 2147484061
12
+ EXTERNAL_PDR_SIZE = 239
13
+ EXTERNAL_PAYLOAD_SIZE = 175
14
+
15
+
16
+ class PaymentAnchorType(IntEnum):
17
+ UNPAID = 0x00
18
+ NEAR_DIRECT = 0x01
19
+ AGENT_MARKET_SUBMIT = 0x02
20
+ AGENT_MARKET_RELEASE= 0x03
21
+ NEAR_ESCROW_LOCK = 0x04
22
+ X402_BASE = 0x05
23
+ X402_POLYGON = 0x06
24
+ X402_NEAR = 0x07
25
+ CHAIN_SIG_PAYMENT = 0x08
26
+ NEAR_ESCROW_SETTLED = 0x09
27
+ UNKNOWN = 0xFF
28
+
29
+
30
+ class NotarizeRequest(BaseModel):
31
+ tx_hash: str = Field(..., min_length=32, max_length=128, description="tx hash (NEAR Base58 or EVM hex)")
32
+ work_hash: Optional[str] = Field(None, description="SHA-256 hex digest (64 chars)")
33
+ agent_sig: Optional[str] = Field(None, description="Base64 Ed25519 signature (sig_A)")
34
+ agent_pubkey: Optional[str] = Field(None, description="Hex Ed25519 public key")
35
+
36
+ def to_api_payload(self) -> dict:
37
+ payload: dict = {"tx_hash": self.tx_hash}
38
+ if self.work_hash:
39
+ payload["payload"] = {"work_hash": self.work_hash}
40
+ if self.agent_sig:
41
+ payload["agent_sig"] = self.agent_sig
42
+ if self.agent_pubkey:
43
+ payload["agent_pubkey"] = self.agent_pubkey
44
+ return payload
45
+
46
+
47
+ class NotarizeResult(BaseModel):
48
+ job_id: str
49
+
50
+
51
+ class StatusResult(BaseModel):
52
+ status: str
53
+ pdr_b64: Optional[str] = None
54
+ payment_hash: Optional[str] = None
55
+
56
+ @property
57
+ def is_anchored(self) -> bool:
58
+ return self.status.lower() == "anchored"
59
+
60
+
61
+ class PdrResult(BaseModel):
62
+ pdr_b64: str
63
+ payment_hash: Optional[str] = None
64
+ payment_anchor_type: Optional[int] = None
65
+ format: str = "nep413-dualsig-v1"
66
+ verify_url: Optional[str] = None
67
+
68
+ @property
69
+ def pdr_bytes(self) -> bytes:
70
+ return base64.b64decode(self.pdr_b64)
71
+
72
+ def _resolve_payment_anchor_type(self) -> None:
73
+ if self.payment_anchor_type is not None:
74
+ return
75
+ raw = self.pdr_bytes
76
+ version = raw[0]
77
+ if version == 0x02:
78
+ self.payment_anchor_type = PaymentAnchorType.NEAR_DIRECT
79
+ elif version in (0x03, 0x04):
80
+ pat_byte = raw[2]
81
+ try:
82
+ self.payment_anchor_type = PaymentAnchorType(pat_byte)
83
+ except ValueError:
84
+ self.payment_anchor_type = PaymentAnchorType.UNKNOWN
85
+ else:
86
+ self.payment_anchor_type = PaymentAnchorType.UNKNOWN
87
+
88
+ @property
89
+ def payload_bytes(self) -> bytes:
90
+ raw = self.pdr_bytes
91
+ version = raw[0]
92
+ if version == 0x02:
93
+ expected = EXTERNAL_PDR_SIZE + 1 # v2.2 was 238 (one byte less than v2.3's 239)
94
+ payload_size = EXTERNAL_PAYLOAD_SIZE + 1 # v2.2 was 174 (one byte less than v2.3's 175)
95
+ elif version in (0x03, 0x04):
96
+ expected = EXTERNAL_PDR_SIZE
97
+ payload_size = EXTERNAL_PAYLOAD_SIZE
98
+ else:
99
+ raise ValueError(f"Unknown PDR version: {version:#04x}")
100
+ if len(raw) != expected:
101
+ raise ValueError(f"Invalid PDR size: {len(raw)}, expected {expected}")
102
+ return raw[:payload_size]
103
+
104
+ @property
105
+ def signature_bytes(self) -> bytes:
106
+ raw = self.pdr_bytes
107
+ version = raw[0]
108
+ if version == 0x02:
109
+ payload_size = EXTERNAL_PAYLOAD_SIZE + 1 # v2.2 was 174
110
+ elif version in (0x03, 0x04):
111
+ payload_size = EXTERNAL_PAYLOAD_SIZE
112
+ else:
113
+ payload_size = EXTERNAL_PAYLOAD_SIZE
114
+ return raw[payload_size:]
115
+
116
+ def verify(self, notary_pubkey_hex: str) -> bool:
117
+ from nacl.signing import VerifyKey
118
+ from nacl.exceptions import BadSignatureError
119
+
120
+ raw = self.pdr_bytes
121
+ version = raw[0]
122
+ if version == 0x02:
123
+ expected = EXTERNAL_PDR_SIZE + 1 # v2.2 was 238
124
+ payload_size = EXTERNAL_PAYLOAD_SIZE + 1 # v2.2 was 174
125
+ elif version in (0x03, 0x04):
126
+ expected = EXTERNAL_PDR_SIZE
127
+ payload_size = EXTERNAL_PAYLOAD_SIZE
128
+ else:
129
+ return False
130
+ if len(raw) != expected:
131
+ return False
132
+
133
+ self._resolve_payment_anchor_type()
134
+
135
+ payload = raw[:payload_size]
136
+ signature = raw[payload_size:]
137
+ verify_key = VerifyKey(bytes.fromhex(notary_pubkey_hex))
138
+ nep413_buffer = struct.pack("<II", NEP413_TAG, len(payload)) + payload
139
+ try:
140
+ verify_key.verify(nep413_buffer, signature)
141
+ except BadSignatureError:
142
+ return False
143
+
144
+ if self.payment_anchor_type not in (None, PaymentAnchorType.UNPAID):
145
+ if self.payment_hash is not None and len(self.payment_hash) < 4:
146
+ return False
147
+
148
+ return True
@@ -0,0 +1,68 @@
1
+ """AOTrust Tool for LangChain / CrewAI integration.
2
+
3
+ Usage:
4
+ from agent_notary.tools import AOTrustTool
5
+ tool = AOTrustTool()
6
+ result = tool.run("my report content")
7
+ """
8
+
9
+ import asyncio
10
+ import hashlib
11
+
12
+
13
+ class AOTrustTool:
14
+ """Tool for AI agents to create cryptographic proof of existence for outputs."""
15
+
16
+ name: str = "aotrust_notarize"
17
+ description: str = (
18
+ "Notarize a digital artifact, report, or code to prove its existence "
19
+ "at the current time. Returns a verification URL and Shield ID. "
20
+ "Free tier: 5 requests per day per IP."
21
+ )
22
+
23
+ def __init__(self, api_url: str = "https://api.aotrust.link"):
24
+ self.api_url = api_url
25
+
26
+ def run(self, work_text_or_hash: str) -> str:
27
+ """Synchronous run — compute hash if needed, notarize, return result string."""
28
+ work_hash = self._resolve_hash(work_text_or_hash)
29
+ try:
30
+ result = asyncio.run(self._notarize(work_hash))
31
+ shield_id = result.get("shield_id", "—")
32
+ verify_url = self._verify_url(result.get("pdr_b64", ""))
33
+ return f"✅ Notarized. Shield ID: {shield_id}\nVerify: {verify_url}"
34
+ except Exception as e:
35
+ return f"❌ Notarization failed: {e}"
36
+
37
+ async def _notarize(self, work_hash: str) -> dict:
38
+ from agent_notary.client import NotaryClient
39
+ client = NotaryClient(base_url=f"{self.api_url}/v1")
40
+ return await client.shield_free(work_hash)
41
+
42
+ async def arun(self, work_text_or_hash: str) -> str:
43
+ """Async run for LangChain async chains."""
44
+ work_hash = self._resolve_hash(work_text_or_hash)
45
+ try:
46
+ result = await self._notarize(work_hash)
47
+ shield_id = result.get("shield_id", "—")
48
+ verify_url = self._verify_url(result.get("pdr_b64", ""))
49
+ return f"✅ Notarized. Shield ID: {shield_id}\nVerify: {verify_url}"
50
+ except Exception as e:
51
+ return f"❌ Notarization failed: {e}"
52
+
53
+ def _arun(self, work_text_or_hash: str) -> str:
54
+ """LangChain BaseTool compatibility."""
55
+ return self.arun(work_text_or_hash)
56
+
57
+ @staticmethod
58
+ def _resolve_hash(work_text_or_hash: str) -> str:
59
+ """If input is 64 hex chars, use as-is. Otherwise compute SHA-256."""
60
+ if len(work_text_or_hash) == 64 and all(c in "0123456789abcdef" for c in work_text_or_hash.lower()):
61
+ return work_text_or_hash
62
+ return hashlib.sha256(work_text_or_hash.encode()).hexdigest()
63
+
64
+ @staticmethod
65
+ def _verify_url(pdr_b64: str) -> str:
66
+ """Build a verify.aotrust.link URL with base64url-encoded PDR."""
67
+ b64url = pdr_b64.replace("+", "-").replace("/", "_").rstrip("=")
68
+ return f"https://verify.aotrust.link/?pdr={b64url}"
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: aotrust-protocol
3
+ Version: 2.3.0
4
+ Summary: Asyncio-native SDK for A&O Trust Layer — Agent Notary Service
5
+ Author: A&O Trust Layer
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Topic :: Security :: Cryptography
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: aiohttp>=3.9
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: pynacl>=1.5
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
24
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
25
+ Requires-Dist: aioresponses>=0.7; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # aotrust-protocol SDK
29
+
30
+ Asyncio-native Python SDK for the AOTrust Notary API — cryptographic proof-of-existence for AI agent outputs.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install aotrust-protocol
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ ```python
41
+ import asyncio
42
+ from agent_notary import NotaryClient, NotarizeRequest
43
+
44
+ async def main():
45
+ client = NotaryClient(
46
+ api_key="your-api-key",
47
+ base_url="https://api.aotrust.link/v1"
48
+ )
49
+
50
+ # Submit notarization
51
+ req = NotarizeRequest(
52
+ tx_hash="EzrfDW5b...",
53
+ work_hash="599d6999...",
54
+ agent_sig="base64_sig_A...",
55
+ agent_pubkey="aff91a18...",
56
+ )
57
+ result = await client.notarize(req)
58
+ print(f"Job: {result.job_id}")
59
+
60
+ # Poll for PDR
61
+ status = await client.wait_for_pdr(result.job_id, timeout=60)
62
+ if status.is_anchored:
63
+ pdr = await client.get_pdr(result.job_id)
64
+ assert pdr.verify(client.notary_pubkey)
65
+ print("PDR Valid: YES")
66
+
67
+ asyncio.run(main())
68
+ ```
69
+
70
+ ## Configuration
71
+
72
+ | Env Var | Constructor Arg | Description |
73
+ |---------|-----------------|-------------|
74
+ | `NOTARY_API_KEY` | `api_key` | API key for auth |
75
+ | `NOTARY_API_URL` | `base_url` | API base URL |
76
+ | `NOTARY_PUBKEY` | `notary_pubkey` | Notary Ed25519 public key (hex) |
77
+
78
+ ## Endpoints
79
+
80
+ | Method | Path | Description |
81
+ |--------|------|-------------|
82
+ | POST | `/v1/notarize` | Submit notarization |
83
+ | GET | `/v1/status/{job_id}` | Poll job status |
84
+ | GET | `/v1/pdr/{job_id}` | Get PDR |
85
+ | POST | `/v1/notarize/quote` | Get price quote |
86
+ | GET | `/.well-known/agent.json` | MCP Server Card |
87
+ | GET | `/openapi.json` | OpenAPI 3.0 spec |
88
+
89
+ ## PDR Verification
90
+
91
+ ```python
92
+ pdr = await client.get_pdr(job_id)
93
+ is_valid = pdr.verify(notary_pubkey_hex="9c7d64bb...")
94
+ ```
95
+
96
+ Uses NEP-413 raw-buffer verification. No SHA256 pre-hash.
97
+
98
+ ## Error Handling
99
+
100
+ ```python
101
+ from agent_notary.exceptions import NotaryAuthError, NotaryPaymentError, NotaryNotFoundError
102
+
103
+ try:
104
+ result = await client.notarize(req)
105
+ except NotaryAuthError:
106
+ print("Invalid API key")
107
+ except NotaryPaymentError as e:
108
+ print(f"Payment/sig failed: {e}")
109
+ ```
110
+
111
+ ## Development
112
+
113
+ ```bash
114
+ pip install -e ".[dev]"
115
+ pytest tests/ -v
116
+ ```
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ agent_notary/__init__.py
5
+ agent_notary/client.py
6
+ agent_notary/exceptions.py
7
+ agent_notary/models.py
8
+ agent_notary/tools.py
9
+ aotrust_protocol.egg-info/PKG-INFO
10
+ aotrust_protocol.egg-info/SOURCES.txt
11
+ aotrust_protocol.egg-info/dependency_links.txt
12
+ aotrust_protocol.egg-info/requires.txt
13
+ aotrust_protocol.egg-info/top_level.txt
14
+ tests/test_client.py
@@ -0,0 +1,9 @@
1
+ aiohttp>=3.9
2
+ pydantic>=2.0
3
+ pynacl>=1.5
4
+
5
+ [dev]
6
+ pytest>=7.0
7
+ pytest-asyncio>=0.23
8
+ pytest-cov>=4.0
9
+ aioresponses>=0.7
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aotrust-protocol"
7
+ version = "2.3.0"
8
+ description = "Asyncio-native SDK for A&O Trust Layer — Agent Notary Service"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "A&O Trust Layer"}]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Framework :: AsyncIO",
21
+ "Topic :: Security :: Cryptography",
22
+ ]
23
+
24
+ dependencies = [
25
+ "aiohttp>=3.9",
26
+ "pydantic>=2.0",
27
+ "pynacl>=1.5",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=7.0",
33
+ "pytest-asyncio>=0.23",
34
+ "pytest-cov>=4.0",
35
+ "aioresponses>=0.7",
36
+ ]
37
+
38
+ [tool.setuptools.packages.find]
39
+ include = ["agent_notary*"]
40
+
41
+ [tool.pytest.ini_options]
42
+ asyncio_mode = "auto"
43
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,139 @@
1
+ """Tests for NotaryClient SDK."""
2
+
3
+ import asyncio
4
+ import json
5
+ import base64
6
+ import struct
7
+ import pytest
8
+ from unittest.mock import AsyncMock, patch, MagicMock
9
+
10
+ from agent_notary import NotaryClient, NotarizeRequest, PdrResult
11
+ from agent_notary.exceptions import (
12
+ NotaryAuthError,
13
+ NotaryNotFoundError,
14
+ NotaryPaymentError,
15
+ NotaryServerError,
16
+ )
17
+ from agent_notary.models import NEP413_TAG, EXTERNAL_PDR_SIZE, EXTERNAL_PAYLOAD_SIZE
18
+
19
+
20
+ # ── Fixtures ──────────────────────────────────────────────────
21
+
22
+ MOCK_API_KEY = "test-api-key-12345"
23
+ MOCK_JOB_ID = "fe207267-6568-402d-8ee9-b332dff8b2f9"
24
+ MOCK_NOTARY_PUBKEY = "9c7d64bbabe90ead8f18982497a47acbd95677cef0d9285caec95377e4ccf6b0"
25
+
26
+
27
+ def _make_client(**kwargs):
28
+ return NotaryClient(api_key=MOCK_API_KEY, base_url="http://localhost:8741/v1", **kwargs)
29
+
30
+
31
+ def _make_real_pdr_b64():
32
+ from nacl.signing import SigningKey
33
+ seed = bytes(range(32))
34
+ sk = SigningKey(seed)
35
+ payload = b'\x03\x01\x00' + b'\x00' * (EXTERNAL_PAYLOAD_SIZE - 3)
36
+ nep413_buffer = struct.pack("<II", NEP413_TAG, len(payload)) + payload
37
+ sig = sk.sign(nep413_buffer).signature
38
+ return base64.b64encode(payload + sig).decode()
39
+
40
+
41
+ # ── NotarizeRequest ──────────────────────────────────────────
42
+
43
+ class TestNotarizeRequest:
44
+ def test_minimal_payload(self):
45
+ req = NotarizeRequest(tx_hash="EzrfDW5bA3KqkHeMq4J5hXKBq8MD7XDjhGg8RzP5HFvY")
46
+ payload = req.to_api_payload()
47
+ assert payload["tx_hash"] == "EzrfDW5bA3KqkqHeMq4J5hXKBq8MD7XDjhGg8RzP5HFvY"[:44] or "tx_hash" in payload
48
+ assert "agent_sig" not in payload
49
+
50
+ def test_full_chain_payload(self):
51
+ req = NotarizeRequest(
52
+ tx_hash="EzrfDW5bA3KqkHeMq4J5hXKBq8MD7XDjhGg8RzP5HFvY",
53
+ work_hash="599d6999abc123" + "0" * 50,
54
+ agent_sig="dGVzdHNpZw==",
55
+ agent_pubkey="aff91a18" + "0" * 56,
56
+ )
57
+ payload = req.to_api_payload()
58
+ assert "payload" in payload
59
+ assert payload["payload"]["work_hash"] == req.work_hash
60
+ assert payload["agent_sig"] == req.agent_sig
61
+
62
+
63
+ # ── PdrResult verification ───────────────────────────────────
64
+
65
+ class TestPdrResult:
66
+ def test_verify_valid_pdr(self):
67
+ from nacl.signing import SigningKey
68
+ seed = bytes(range(32))
69
+ sk = SigningKey(seed)
70
+ pubkey_hex = bytes(sk.verify_key).hex()
71
+ payload = b'\x03\x01\x00' + b'\x01' * (EXTERNAL_PAYLOAD_SIZE - 3)
72
+ nep413_buffer = struct.pack("<II", NEP413_TAG, len(payload)) + payload
73
+ sig = sk.sign(nep413_buffer).signature
74
+ pdr_bytes = payload + sig
75
+ pdr_b64 = base64.b64encode(pdr_bytes).decode()
76
+ result = PdrResult(pdr_b64=pdr_b64)
77
+ assert result.verify(pubkey_hex) is True
78
+
79
+ def test_verify_invalid_pdr(self):
80
+ pdr_b64 = base64.b64encode(b'\x03\x01\x00' + b'\x00' * (EXTERNAL_PDR_SIZE - 3)).decode()
81
+ result = PdrResult(pdr_b64=pdr_b64)
82
+ assert result.verify(MOCK_NOTARY_PUBKEY) is False
83
+
84
+ def test_verify_wrong_size(self):
85
+ pdr_b64 = base64.b64encode(b'\x00' * 100).decode()
86
+ result = PdrResult(pdr_b64=pdr_b64)
87
+ assert result.verify(MOCK_NOTARY_PUBKEY) is False
88
+
89
+
90
+ # ── NotaryClient error handling ──────────────────────────────
91
+
92
+ class TestNotaryClientErrors:
93
+ def test_auth_error(self):
94
+ client = _make_client()
95
+ with pytest.raises(NotaryAuthError):
96
+ client._check_status(401, {"error": "Unauthorized"})
97
+
98
+ def test_payment_error(self):
99
+ client = _make_client()
100
+ with pytest.raises(NotaryPaymentError):
101
+ client._check_status(403, {"error": "Payment verification failed"})
102
+
103
+ def test_not_found_error(self):
104
+ client = _make_client()
105
+ with pytest.raises(NotaryNotFoundError):
106
+ client._check_status(404, {"error": "Not found"})
107
+
108
+ def test_server_error(self):
109
+ client = _make_client()
110
+ with pytest.raises(NotaryServerError):
111
+ client._check_status(502, {"error": "Bad gateway"})
112
+
113
+
114
+ # ── NotaryClient construction ────────────────────────────────
115
+
116
+ class TestNotaryClientConstruction:
117
+ def test_default_base_url(self):
118
+ client = NotaryClient(api_key="k")
119
+ assert "aotrust.link/v1" in client.base_url
120
+
121
+ def test_custom_base_url(self):
122
+ client = NotaryClient(api_key="k", base_url="http://localhost:9999/v1")
123
+ assert client.base_url == "http://localhost:9999/v1"
124
+
125
+ def test_env_var_fallback(self):
126
+ with patch.dict(os.environ, {"NOTARY_API_KEY": "env-key"}, clear=False):
127
+ client = NotaryClient()
128
+ assert client.api_key == "env-key"
129
+
130
+ def test_headers_include_api_key(self):
131
+ client = NotaryClient(api_key="my-secret")
132
+ assert client._headers()["X-API-Key"] == "my-secret"
133
+
134
+ def test_headers_without_api_key(self):
135
+ client = NotaryClient(api_key="")
136
+ assert "X-API-Key" not in client._headers()
137
+
138
+
139
+ import os