langchain-aotrust 0.1.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.
- langchain_aotrust-0.1.0/LICENSE +21 -0
- langchain_aotrust-0.1.0/PKG-INFO +103 -0
- langchain_aotrust-0.1.0/README.md +86 -0
- langchain_aotrust-0.1.0/langchain_aotrust/__init__.py +33 -0
- langchain_aotrust-0.1.0/langchain_aotrust/_client.py +61 -0
- langchain_aotrust-0.1.0/langchain_aotrust/tools.py +115 -0
- langchain_aotrust-0.1.0/langchain_aotrust.egg-info/PKG-INFO +103 -0
- langchain_aotrust-0.1.0/langchain_aotrust.egg-info/SOURCES.txt +13 -0
- langchain_aotrust-0.1.0/langchain_aotrust.egg-info/dependency_links.txt +1 -0
- langchain_aotrust-0.1.0/langchain_aotrust.egg-info/requires.txt +7 -0
- langchain_aotrust-0.1.0/langchain_aotrust.egg-info/top_level.txt +1 -0
- langchain_aotrust-0.1.0/pyproject.toml +26 -0
- langchain_aotrust-0.1.0/setup.cfg +4 -0
- langchain_aotrust-0.1.0/tests/test_live.py +45 -0
- langchain_aotrust-0.1.0/tests/test_tools.py +160 -0
|
@@ -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,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-aotrust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools for AOTrust notarization — cryptographic proof-of-existence for AI agent outputs (x402, NEAR-anchored PDRs)
|
|
5
|
+
Author: A&O Trust Layer
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: langchain-core>=1.0
|
|
11
|
+
Requires-Dist: aotrust-protocol>=2.3.6
|
|
12
|
+
Requires-Dist: aiohttp>=3.9
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# langchain-aotrust
|
|
19
|
+
|
|
20
|
+
<!-- mcp-name: link.aotrust/notary -->
|
|
21
|
+
|
|
22
|
+
LangChain tools for **AOTrust** notarization — cryptographic
|
|
23
|
+
proof-of-existence for AI agent outputs. Free tier (no wallet required),
|
|
24
|
+
PDRs signed with Ed25519, Merkle-included and anchored on NEAR.
|
|
25
|
+
|
|
26
|
+
Works with any agent built on `langchain-core` (LangChain, LangGraph,
|
|
27
|
+
CrewAI via the standard BaseTool interface, pydantic-ai adapters).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install langchain-aotrust
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from langchain_aotrust import (
|
|
39
|
+
aotrust_notarize_free,
|
|
40
|
+
aotrust_verify,
|
|
41
|
+
aotrust_status,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# 1. Notarize an artifact (free tier — no API key, no wallet)
|
|
45
|
+
print(aotrust_notarize_free.invoke({"artifact_text": "analysis v3: portfolio risk moderate"}))
|
|
46
|
+
# Notarized (tier=free). shield_id=9f2c... job_id=7a1b... work_hash=...
|
|
47
|
+
# Verify later at https://verify.aotrust.link or with aotrust_verify(job_id).
|
|
48
|
+
|
|
49
|
+
# 2. Check pipeline status
|
|
50
|
+
print(aotrust_status.invoke({"job_id": "<job_id>"}))
|
|
51
|
+
# Status for job_id=7a1b...: status=anchored, anchored=yes, payment_hash=...
|
|
52
|
+
|
|
53
|
+
# 3. Full cryptographic verification
|
|
54
|
+
print(aotrust_verify.invoke({"job_id": "<job_id>"}))
|
|
55
|
+
# Verification for job_id=7a1b...: valid=True, payment_anchor_type=0, ...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Wiring into a LangChain agent:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from langchain_aotrust import aotrust_notarize_free, aotrust_verify
|
|
62
|
+
from langchain.agents import create_tool_calling_agent # any agent works
|
|
63
|
+
|
|
64
|
+
tools = [aotrust_notarize_free, aotrust_verify, aotrust_status]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Async agents (LangGraph) can await the plain functions directly:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from langchain_aotrust import aotrust_notarize_free_async
|
|
71
|
+
result = await aotrust_notarize_free_async(artifact_text="...")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Configuration (optional — defaults to the public endpoint):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from langchain_aotrust import configure
|
|
78
|
+
configure(base_url="https://api.aotrust.link") # NO /v1 suffix — the SDK appends paths
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## What the proofs mean
|
|
82
|
+
|
|
83
|
+
| Horizon | Guarantee |
|
|
84
|
+
|---|---|
|
|
85
|
+
| **Instant** | SHA-256 work_hash bound into a signed PDR (Ed25519, notary key) — tamper-evident from notarization time |
|
|
86
|
+
| **Python** | PDR bytes verifiable offline by anyone with the notary public key |
|
|
87
|
+
| **Forever** | Daily Merkle root anchored on NEAR blockchain — proof survives even if the service disappears |
|
|
88
|
+
|
|
89
|
+
## Free tier
|
|
90
|
+
|
|
91
|
+
- No API key, no wallet, no payment.
|
|
92
|
+
- Rate limited per IP (5 notarizations / 24h) — paid x402 tier ($0.01) in a
|
|
93
|
+
future release for higher throughput.
|
|
94
|
+
|
|
95
|
+
## Links
|
|
96
|
+
|
|
97
|
+
- Docs: https://docs.aotrust.link
|
|
98
|
+
- Live verifier: https://verify.aotrust.link
|
|
99
|
+
- SDK: https://pypi.org/project/aotrust-protocol/
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# langchain-aotrust
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: link.aotrust/notary -->
|
|
4
|
+
|
|
5
|
+
LangChain tools for **AOTrust** notarization — cryptographic
|
|
6
|
+
proof-of-existence for AI agent outputs. Free tier (no wallet required),
|
|
7
|
+
PDRs signed with Ed25519, Merkle-included and anchored on NEAR.
|
|
8
|
+
|
|
9
|
+
Works with any agent built on `langchain-core` (LangChain, LangGraph,
|
|
10
|
+
CrewAI via the standard BaseTool interface, pydantic-ai adapters).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install langchain-aotrust
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from langchain_aotrust import (
|
|
22
|
+
aotrust_notarize_free,
|
|
23
|
+
aotrust_verify,
|
|
24
|
+
aotrust_status,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# 1. Notarize an artifact (free tier — no API key, no wallet)
|
|
28
|
+
print(aotrust_notarize_free.invoke({"artifact_text": "analysis v3: portfolio risk moderate"}))
|
|
29
|
+
# Notarized (tier=free). shield_id=9f2c... job_id=7a1b... work_hash=...
|
|
30
|
+
# Verify later at https://verify.aotrust.link or with aotrust_verify(job_id).
|
|
31
|
+
|
|
32
|
+
# 2. Check pipeline status
|
|
33
|
+
print(aotrust_status.invoke({"job_id": "<job_id>"}))
|
|
34
|
+
# Status for job_id=7a1b...: status=anchored, anchored=yes, payment_hash=...
|
|
35
|
+
|
|
36
|
+
# 3. Full cryptographic verification
|
|
37
|
+
print(aotrust_verify.invoke({"job_id": "<job_id>"}))
|
|
38
|
+
# Verification for job_id=7a1b...: valid=True, payment_anchor_type=0, ...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Wiring into a LangChain agent:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from langchain_aotrust import aotrust_notarize_free, aotrust_verify
|
|
45
|
+
from langchain.agents import create_tool_calling_agent # any agent works
|
|
46
|
+
|
|
47
|
+
tools = [aotrust_notarize_free, aotrust_verify, aotrust_status]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Async agents (LangGraph) can await the plain functions directly:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from langchain_aotrust import aotrust_notarize_free_async
|
|
54
|
+
result = await aotrust_notarize_free_async(artifact_text="...")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Configuration (optional — defaults to the public endpoint):
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from langchain_aotrust import configure
|
|
61
|
+
configure(base_url="https://api.aotrust.link") # NO /v1 suffix — the SDK appends paths
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## What the proofs mean
|
|
65
|
+
|
|
66
|
+
| Horizon | Guarantee |
|
|
67
|
+
|---|---|
|
|
68
|
+
| **Instant** | SHA-256 work_hash bound into a signed PDR (Ed25519, notary key) — tamper-evident from notarization time |
|
|
69
|
+
| **Python** | PDR bytes verifiable offline by anyone with the notary public key |
|
|
70
|
+
| **Forever** | Daily Merkle root anchored on NEAR blockchain — proof survives even if the service disappears |
|
|
71
|
+
|
|
72
|
+
## Free tier
|
|
73
|
+
|
|
74
|
+
- No API key, no wallet, no payment.
|
|
75
|
+
- Rate limited per IP (5 notarizations / 24h) — paid x402 tier ($0.01) in a
|
|
76
|
+
future release for higher throughput.
|
|
77
|
+
|
|
78
|
+
## Links
|
|
79
|
+
|
|
80
|
+
- Docs: https://docs.aotrust.link
|
|
81
|
+
- Live verifier: https://verify.aotrust.link
|
|
82
|
+
- SDK: https://pypi.org/project/aotrust-protocol/
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""langchain-aotrust — LangChain tools for AOTrust notarization.
|
|
2
|
+
|
|
3
|
+
Cryptographic proof-of-existence for AI agent outputs: free-tier notarization
|
|
4
|
+
(SHA-256 work_hash -> PDR, NEAR-anchored) and public verification, exposed
|
|
5
|
+
as LangChain BaseTool instances compatible with any agent built on
|
|
6
|
+
langchain-core (LangChain, LangGraph, CrewAI, pydantic-ai via the standard
|
|
7
|
+
BaseTool interface).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
from ._client import get_client, configure, reset_client
|
|
13
|
+
from .tools import (
|
|
14
|
+
aotrust_notarize_free,
|
|
15
|
+
aotrust_verify,
|
|
16
|
+
aotrust_status,
|
|
17
|
+
aotrust_notarize_free_async,
|
|
18
|
+
aotrust_verify_async,
|
|
19
|
+
aotrust_status_async,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"__version__",
|
|
24
|
+
"get_client",
|
|
25
|
+
"configure",
|
|
26
|
+
"reset_client",
|
|
27
|
+
"aotrust_notarize_free",
|
|
28
|
+
"aotrust_verify",
|
|
29
|
+
"aotrust_status",
|
|
30
|
+
"aotrust_notarize_free_async",
|
|
31
|
+
"aotrust_verify_async",
|
|
32
|
+
"aotrust_status_async",
|
|
33
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Client singleton + sync/async bridge for langchain-aotrust.
|
|
2
|
+
|
|
3
|
+
URL contract: the AOTrust NotaryClient assembles full endpoint paths itself
|
|
4
|
+
(``/v1/shield/free``, ``/v1/status/{job_id}``, ...) — the ``base_url`` must
|
|
5
|
+
NEVER include a trailing ``/v1`` segment. Passing ``https://api.aotrust.link/v1``
|
|
6
|
+
would produce double-prefixed ``/v1/v1/...`` requests (the bug recorded in
|
|
7
|
+
examples/langchain_tool.py of aotrust-protocol).
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
import concurrent.futures
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from agent_notary import NotaryClient
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://api.aotrust.link"
|
|
16
|
+
|
|
17
|
+
_client: Optional[NotaryClient] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_client() -> NotaryClient:
|
|
21
|
+
"""Lazy singleton of the async NotaryClient (aotrust-protocol)."""
|
|
22
|
+
global _client
|
|
23
|
+
if _client is None:
|
|
24
|
+
# base_url default https://api.aotrust.link — WITHOUT a /v1 suffix
|
|
25
|
+
_client = NotaryClient(base_url="https://api.aotrust.link")
|
|
26
|
+
return _client
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def configure(base_url: Optional[str] = None, api_key: Optional[str] = None) -> None:
|
|
30
|
+
"""Replace the singleton (call before first use, or use reset_client()).
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
base_url: API origin WITHOUT the /v1 path segment (e.g.
|
|
34
|
+
https://api.aotrust.link or a testnet mirror).
|
|
35
|
+
api_key: optional key for authenticated endpoints (quote channel).
|
|
36
|
+
"""
|
|
37
|
+
global _client
|
|
38
|
+
_client = NotaryClient(api_key=api_key, base_url=base_url)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def reset_client() -> None:
|
|
42
|
+
"""Drop the singleton (next get_client() re-creates it). Mainly for tests."""
|
|
43
|
+
global _client
|
|
44
|
+
_client = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_async(coro):
|
|
48
|
+
"""Run a coroutine from sync code, safe inside a running event loop.
|
|
49
|
+
|
|
50
|
+
LangChain agents invoke tools synchronously by default, but agents can
|
|
51
|
+
also be driven from within an async application (LangGraph, Jupyter
|
|
52
|
+
kernels). Plain ``asyncio.run()`` raises ``RuntimeError`` when a loop is
|
|
53
|
+
already running, so this bridge detects a running loop and executes the
|
|
54
|
+
coroutine on a dedicated worker thread with its own fresh loop.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
asyncio.get_running_loop()
|
|
58
|
+
except RuntimeError:
|
|
59
|
+
return asyncio.run(coro)
|
|
60
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
61
|
+
return pool.submit(asyncio.run, coro).result()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""LangChain tools wrapping the AOTrust notary API.
|
|
2
|
+
|
|
3
|
+
Every tool docstring doubles as the LLM-facing tool description — keep it
|
|
4
|
+
actionable for agents: what it does, what it returns, when to use it.
|
|
5
|
+
|
|
6
|
+
All sync tools bridge into the SDK's asyncio-native client via
|
|
7
|
+
``langchain_aotrust._client.run_async`` (safe inside a running event loop —
|
|
8
|
+
a dedicated worker-thread loop is used there; see _client.run_async).
|
|
9
|
+
"""
|
|
10
|
+
import hashlib
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from langchain_core.tools import tool
|
|
14
|
+
|
|
15
|
+
from ._client import get_client, run_async
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@tool
|
|
19
|
+
def aotrust_notarize_free(artifact_text: str) -> str:
|
|
20
|
+
"""Notarize an AI artifact on AOTrust (free tier, no wallet required).
|
|
21
|
+
|
|
22
|
+
Computes the SHA-256 work_hash of the artifact text and registers a
|
|
23
|
+
free UNPAID PDR (proof-of-existence record, NEAR-anchored, Ed25519
|
|
24
|
+
signed). Use this whenever an agent's output must be provably
|
|
25
|
+
timestamped and tamper-evident. Returns a human-readable confirmation
|
|
26
|
+
with the shield_id and job_id to verify later at
|
|
27
|
+
https://verify.aotrust.link. Rate limited per IP (free tier)."""
|
|
28
|
+
import hashlib
|
|
29
|
+
|
|
30
|
+
work_hash = hashlib.sha256(artifact_text.encode("utf-8")).hexdigest()
|
|
31
|
+
res = run_async(get_client().shield_free(work_hash))
|
|
32
|
+
shield_id = res.get("shield_id", "n/a")
|
|
33
|
+
job_id = res.get("job_id", "n/a")
|
|
34
|
+
tier = res.get("tier", "free")
|
|
35
|
+
return (
|
|
36
|
+
f"Notarized (tier={tier}). shield_id={shield_id} job_id={job_id} "
|
|
37
|
+
f"work_hash={work_hash} "
|
|
38
|
+
f"Verify later at https://verify.aotrust.link or with aotrust_verify(job_id)."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@tool
|
|
43
|
+
def aotrust_verify(job_id: str) -> str:
|
|
44
|
+
"""Verify a previously notarized artifact on AOTrust by job_id.
|
|
45
|
+
|
|
46
|
+
Fetches the PDR for the given job_id and runs full cryptographic
|
|
47
|
+
verification (Ed25519 signature, Merkle inclusion, NEAR anchoring).
|
|
48
|
+
Returns validity (true/false), the payment_anchor_type and whether the
|
|
49
|
+
record is anchored on-chain. Use after aotrust_notarize_free or when a
|
|
50
|
+
user supplies a job_id to prove a record exists."""
|
|
51
|
+
pdr = run_async(get_client().get_pdr(job_id))
|
|
52
|
+
v = run_async(get_client().verify_pdr(pdr.pdr_b64))
|
|
53
|
+
valid = v.get("valid")
|
|
54
|
+
if valid is None:
|
|
55
|
+
valid = v.get("tx_verified_on_chain") is not None and v.get("sig_valid", False)
|
|
56
|
+
pat = v.get("payment_anchor_type") or pdr.payment_anchor_type
|
|
57
|
+
return (
|
|
58
|
+
f"Verification for job_id={job_id}: valid={valid}, "
|
|
59
|
+
f"payment_anchor_type={pat}, anchored status fetched, "
|
|
60
|
+
f"verify_url={pdr.verify_url or 'https://verify.aotrust.link'}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@tool
|
|
65
|
+
def aotrust_status(job_id: str) -> str:
|
|
66
|
+
"""Check the notarization status of a job_id on AOTrust.
|
|
67
|
+
|
|
68
|
+
Returns the current pipeline status (queued/anchored/etc.), whether the
|
|
69
|
+
record is already anchored on NEAR, and the anchoring timestamp when
|
|
70
|
+
available. Cheaper than aotrust_verify — use it to poll progress."""
|
|
71
|
+
st = run_async(get_client().get_status(job_id))
|
|
72
|
+
anchored = "yes" if st.is_anchored else "no"
|
|
73
|
+
ts = getattr(st, "payment_hash", None) or ""
|
|
74
|
+
return f"Status for job_id={job_id}: status={st.status}, anchored={anchored}, payment_hash={ts}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── plain async variants (NOT @tool) — for LangGraph async agents ──────────
|
|
78
|
+
|
|
79
|
+
async def aotrust_notarize_free_async(artifact_text: str) -> str:
|
|
80
|
+
"""Async counterpart of aotrust_notarize_free (await directly in async agents)."""
|
|
81
|
+
import hashlib
|
|
82
|
+
|
|
83
|
+
work_hash = hashlib.sha256(artifact_text.encode("utf-8")).hexdigest()
|
|
84
|
+
res = await get_client().shield_free(work_hash)
|
|
85
|
+
shield_id = res.get("shield_id", "n/a")
|
|
86
|
+
job_id = res.get("job_id", "n/a")
|
|
87
|
+
tier = res.get("tier", "free")
|
|
88
|
+
return (
|
|
89
|
+
f"Notarized (tier={tier}). shield_id={shield_id} job_id={job_id} "
|
|
90
|
+
f"work_hash={work_hash} "
|
|
91
|
+
f"Verify later at https://verify.aotrust.link or with aotrust_verify(job_id)."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def aotrust_verify_async(job_id: str) -> str:
|
|
96
|
+
"""Async counterpart of aotrust_verify (await directly in async agents)."""
|
|
97
|
+
pdr = await get_client().get_pdr(job_id)
|
|
98
|
+
v = await get_client().verify_pdr(pdr.pdr_b64)
|
|
99
|
+
valid = v.get("valid")
|
|
100
|
+
if valid is None:
|
|
101
|
+
valid = v.get("tx_verified_on_chain") is not None and v.get("sig_valid", False)
|
|
102
|
+
pat = v.get("payment_anchor_type") or pdr.payment_anchor_type
|
|
103
|
+
return (
|
|
104
|
+
f"Verification for job_id={job_id}: valid={valid}, "
|
|
105
|
+
f"payment_anchor_type={pat}, anchored status fetched, "
|
|
106
|
+
f"verify_url={pdr.verify_url or 'https://verify.aotrust.link'}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def aotrust_status_async(job_id: str) -> str:
|
|
111
|
+
"""Async counterpart of aotrust_status (await directly in async agents)."""
|
|
112
|
+
st = await get_client().get_status(job_id)
|
|
113
|
+
anchored = "yes" if st.is_anchored else "no"
|
|
114
|
+
ts = getattr(st, "payment_hash", None) or ""
|
|
115
|
+
return f"Status for job_id={job_id}: status={st.status}, anchored={anchored}, payment_hash={ts}"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-aotrust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools for AOTrust notarization — cryptographic proof-of-existence for AI agent outputs (x402, NEAR-anchored PDRs)
|
|
5
|
+
Author: A&O Trust Layer
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: langchain-core>=1.0
|
|
11
|
+
Requires-Dist: aotrust-protocol>=2.3.6
|
|
12
|
+
Requires-Dist: aiohttp>=3.9
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# langchain-aotrust
|
|
19
|
+
|
|
20
|
+
<!-- mcp-name: link.aotrust/notary -->
|
|
21
|
+
|
|
22
|
+
LangChain tools for **AOTrust** notarization — cryptographic
|
|
23
|
+
proof-of-existence for AI agent outputs. Free tier (no wallet required),
|
|
24
|
+
PDRs signed with Ed25519, Merkle-included and anchored on NEAR.
|
|
25
|
+
|
|
26
|
+
Works with any agent built on `langchain-core` (LangChain, LangGraph,
|
|
27
|
+
CrewAI via the standard BaseTool interface, pydantic-ai adapters).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install langchain-aotrust
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from langchain_aotrust import (
|
|
39
|
+
aotrust_notarize_free,
|
|
40
|
+
aotrust_verify,
|
|
41
|
+
aotrust_status,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# 1. Notarize an artifact (free tier — no API key, no wallet)
|
|
45
|
+
print(aotrust_notarize_free.invoke({"artifact_text": "analysis v3: portfolio risk moderate"}))
|
|
46
|
+
# Notarized (tier=free). shield_id=9f2c... job_id=7a1b... work_hash=...
|
|
47
|
+
# Verify later at https://verify.aotrust.link or with aotrust_verify(job_id).
|
|
48
|
+
|
|
49
|
+
# 2. Check pipeline status
|
|
50
|
+
print(aotrust_status.invoke({"job_id": "<job_id>"}))
|
|
51
|
+
# Status for job_id=7a1b...: status=anchored, anchored=yes, payment_hash=...
|
|
52
|
+
|
|
53
|
+
# 3. Full cryptographic verification
|
|
54
|
+
print(aotrust_verify.invoke({"job_id": "<job_id>"}))
|
|
55
|
+
# Verification for job_id=7a1b...: valid=True, payment_anchor_type=0, ...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Wiring into a LangChain agent:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from langchain_aotrust import aotrust_notarize_free, aotrust_verify
|
|
62
|
+
from langchain.agents import create_tool_calling_agent # any agent works
|
|
63
|
+
|
|
64
|
+
tools = [aotrust_notarize_free, aotrust_verify, aotrust_status]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Async agents (LangGraph) can await the plain functions directly:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from langchain_aotrust import aotrust_notarize_free_async
|
|
71
|
+
result = await aotrust_notarize_free_async(artifact_text="...")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Configuration (optional — defaults to the public endpoint):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from langchain_aotrust import configure
|
|
78
|
+
configure(base_url="https://api.aotrust.link") # NO /v1 suffix — the SDK appends paths
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## What the proofs mean
|
|
82
|
+
|
|
83
|
+
| Horizon | Guarantee |
|
|
84
|
+
|---|---|
|
|
85
|
+
| **Instant** | SHA-256 work_hash bound into a signed PDR (Ed25519, notary key) — tamper-evident from notarization time |
|
|
86
|
+
| **Python** | PDR bytes verifiable offline by anyone with the notary public key |
|
|
87
|
+
| **Forever** | Daily Merkle root anchored on NEAR blockchain — proof survives even if the service disappears |
|
|
88
|
+
|
|
89
|
+
## Free tier
|
|
90
|
+
|
|
91
|
+
- No API key, no wallet, no payment.
|
|
92
|
+
- Rate limited per IP (5 notarizations / 24h) — paid x402 tier ($0.01) in a
|
|
93
|
+
future release for higher throughput.
|
|
94
|
+
|
|
95
|
+
## Links
|
|
96
|
+
|
|
97
|
+
- Docs: https://docs.aotrust.link
|
|
98
|
+
- Live verifier: https://verify.aotrust.link
|
|
99
|
+
- SDK: https://pypi.org/project/aotrust-protocol/
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
langchain_aotrust/__init__.py
|
|
5
|
+
langchain_aotrust/_client.py
|
|
6
|
+
langchain_aotrust/tools.py
|
|
7
|
+
langchain_aotrust.egg-info/PKG-INFO
|
|
8
|
+
langchain_aotrust.egg-info/SOURCES.txt
|
|
9
|
+
langchain_aotrust.egg-info/dependency_links.txt
|
|
10
|
+
langchain_aotrust.egg-info/requires.txt
|
|
11
|
+
langchain_aotrust.egg-info/top_level.txt
|
|
12
|
+
tests/test_live.py
|
|
13
|
+
tests/test_tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
langchain_aotrust
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-aotrust"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain tools for AOTrust notarization — cryptographic proof-of-existence for AI agent outputs (x402, NEAR-anchored PDRs)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "A&O Trust Layer"}]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"langchain-core>=1.0",
|
|
15
|
+
"aotrust-protocol>=2.3.6",
|
|
16
|
+
"aiohttp>=3.9",
|
|
17
|
+
]
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = ["pytest", "pytest-asyncio"]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
addopts = "-m 'not live'"
|
|
23
|
+
markers = ["live: touches live API"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
include = ["langchain_aotrust*"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""test_live.py — live smoke against https://api.aotrust.link.
|
|
2
|
+
|
|
3
|
+
Excluded from the default run (pyproject addopts -m 'not live').
|
|
4
|
+
Run explicitly: pytest -m live
|
|
5
|
+
"""
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from langchain_aotrust import (
|
|
12
|
+
aotrust_notarize_free,
|
|
13
|
+
aotrust_status,
|
|
14
|
+
)
|
|
15
|
+
import langchain_aotrust._client as client_mod
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.live
|
|
19
|
+
def test_live_notarize_free_roundtrip():
|
|
20
|
+
fake = client_mod._client
|
|
21
|
+
try:
|
|
22
|
+
client_mod._client = None # force the real singleton
|
|
23
|
+
artifact = f"langchain-aotrust live smoke {os.urandom(4).hex()}"
|
|
24
|
+
out = aotrust_notarize_free.invoke({"artifact_text": artifact})
|
|
25
|
+
assert "shield_id=" in out
|
|
26
|
+
assert "job_id=" in out
|
|
27
|
+
# extract job_id and check status
|
|
28
|
+
job_id = out.split("job_id=")[1].split(" ")[0].strip()
|
|
29
|
+
status_out = aotrust_status.invoke({"job_id": job_id})
|
|
30
|
+
assert "status=" in status_out
|
|
31
|
+
finally:
|
|
32
|
+
client_mod._client = fake
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.live
|
|
36
|
+
def test_live_free_tier_shape():
|
|
37
|
+
import asyncio
|
|
38
|
+
from langchain_aotrust import get_client
|
|
39
|
+
|
|
40
|
+
work_hash = hashlib.sha256(b"langchain-aotrust live shape probe").hexdigest()
|
|
41
|
+
res = asyncio.run(get_client().shield_free(work_hash))
|
|
42
|
+
assert isinstance(res, dict)
|
|
43
|
+
assert "shield_id" in res
|
|
44
|
+
assert "job_id" in res
|
|
45
|
+
assert res.get("tier") in ("free", None)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""test_tools.py — mock-based tests, NO network."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import asyncio
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from langchain_aotrust import (
|
|
8
|
+
aotrust_notarize_free,
|
|
9
|
+
aotrust_verify,
|
|
10
|
+
aotrust_status,
|
|
11
|
+
aotrust_notarize_free_async,
|
|
12
|
+
aotrust_verify_async,
|
|
13
|
+
aotrust_status_async,
|
|
14
|
+
)
|
|
15
|
+
import langchain_aotrust._client as client_mod
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _FakeClient:
|
|
19
|
+
"""Stands in for aotrust_protocol.NotaryClient — records base_url, no network."""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.base_url = "https://api.aotrust.link"
|
|
23
|
+
self.calls = []
|
|
24
|
+
|
|
25
|
+
async def shield_free(self, work_hash):
|
|
26
|
+
self.calls.append(("shield_free", work_hash))
|
|
27
|
+
return {
|
|
28
|
+
"status": "queued",
|
|
29
|
+
"job_id": "job-123",
|
|
30
|
+
"pdr_b64": "QUJD",
|
|
31
|
+
"shield_id": "9f2c1a",
|
|
32
|
+
"payment_anchor_type": 0,
|
|
33
|
+
"tier": "free",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async def get_status(self, job_id):
|
|
37
|
+
self.calls.append(("get_status", job_id))
|
|
38
|
+
return asyncio.stub_status if False else _status_result()
|
|
39
|
+
|
|
40
|
+
async def get_pdr(self, job_id):
|
|
41
|
+
self.calls.append(("get_pdr", job_id))
|
|
42
|
+
return _pdr_result()
|
|
43
|
+
|
|
44
|
+
async def verify_pdr(self, pdr_b64):
|
|
45
|
+
self.calls.append(("verify_pdr", pdr_b64))
|
|
46
|
+
return {"valid": True, "payment_anchor_type": 0, "sig_valid": True}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _status_result():
|
|
50
|
+
from agent_notary import StatusResult
|
|
51
|
+
return StatusResult(status="anchored", pdr_b64="QUJD", payment_hash="deadbeef")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _pdr_result():
|
|
55
|
+
from agent_notary import PdrResult
|
|
56
|
+
return PdrResult(
|
|
57
|
+
pdr_b64="QUJD",
|
|
58
|
+
payment_hash="deadbeef",
|
|
59
|
+
payment_anchor_type=0,
|
|
60
|
+
verify_url="https://verify.aotrust.link/?pdr=QUJD",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestNotarizeFree(unittest.TestCase):
|
|
65
|
+
def setUp(self):
|
|
66
|
+
self.fake = _FakeClient()
|
|
67
|
+
client_mod._client = self.fake
|
|
68
|
+
|
|
69
|
+
def tearDown(self):
|
|
70
|
+
client_mod._client = None
|
|
71
|
+
|
|
72
|
+
def test_returns_string_with_shield_id(self):
|
|
73
|
+
out = aotrust_notarize_free.invoke({"artifact_text": "hello world"})
|
|
74
|
+
self.assertIsInstance(out, str)
|
|
75
|
+
self.assertIn("shield_id=9f2c1a", out)
|
|
76
|
+
self.assertIn("job_id=job-123", out)
|
|
77
|
+
self.assertIn("verify.aotrust.link", out)
|
|
78
|
+
|
|
79
|
+
def test_sha256_stable(self):
|
|
80
|
+
expected = hashlib.sha256(b"hello world").hexdigest()
|
|
81
|
+
aotrust_notarize_free.invoke({"artifact_text": "hello world"})
|
|
82
|
+
self.assertEqual(self.fake.calls[0][1], expected)
|
|
83
|
+
# determinism: same input -> same hash
|
|
84
|
+
aotrust_notarize_free.invoke({"artifact_text": "hello world"})
|
|
85
|
+
self.assertEqual(self.fake.calls[0][1], self.fake.calls[1][1])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestVerify(unittest.TestCase):
|
|
89
|
+
def setUp(self):
|
|
90
|
+
self.fake = _FakeClient()
|
|
91
|
+
client_mod._client = self.fake
|
|
92
|
+
|
|
93
|
+
def tearDown(self):
|
|
94
|
+
client_mod._client = self.fake
|
|
95
|
+
|
|
96
|
+
def test_parses_valid(self):
|
|
97
|
+
out = aotrust_verify.invoke({"job_id": "job-123"})
|
|
98
|
+
self.assertIn("valid=True", out)
|
|
99
|
+
self.assertIn("payment_anchor_type=0", out)
|
|
100
|
+
# get_pdr + verify_pdr both called
|
|
101
|
+
names = [c[0] for c in self.fake.calls]
|
|
102
|
+
self.assertIn("get_pdr", names)
|
|
103
|
+
self.assertIn("verify_pdr", names)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestStatus(unittest.TestCase):
|
|
107
|
+
def setUp(self):
|
|
108
|
+
self.fake = _FakeClient()
|
|
109
|
+
client_mod._client = self.fake
|
|
110
|
+
|
|
111
|
+
def tearDown(self):
|
|
112
|
+
client_mod._client = self.fake
|
|
113
|
+
|
|
114
|
+
def test_status_anchored(self):
|
|
115
|
+
out = aotrust_status.invoke({"job_id": "job-123"})
|
|
116
|
+
self.assertIn("status=anchored", out)
|
|
117
|
+
self.assertIn("anchored=yes", out)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestAsyncVariants(unittest.TestCase):
|
|
121
|
+
def setUp(self):
|
|
122
|
+
self.fake = _FakeClient()
|
|
123
|
+
client_mod._client = self.fake
|
|
124
|
+
|
|
125
|
+
def tearDown(self):
|
|
126
|
+
client_mod._client = self.fake
|
|
127
|
+
|
|
128
|
+
def test_async_notarize(self):
|
|
129
|
+
out = asyncio.run(aotrust_notarize_free_async("hello world"))
|
|
130
|
+
self.assertIn("shield_id=9f2c1a", out)
|
|
131
|
+
|
|
132
|
+
def test_async_verify(self):
|
|
133
|
+
out = asyncio.run(aotrust_verify_async("job-123"))
|
|
134
|
+
self.assertIn("valid=True", out)
|
|
135
|
+
|
|
136
|
+
def test_async_status(self):
|
|
137
|
+
out = asyncio.run(aotrust_status_async("job-123"))
|
|
138
|
+
self.assertIn("status=anchored", out)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestBaseUrlNoV1(unittest.TestCase):
|
|
142
|
+
"""Двойной /v1 — ловится здесь: singleton обязан строиться БЕЗ /v1 суффикса."""
|
|
143
|
+
|
|
144
|
+
def test_default_client_base_url_has_no_v1(self):
|
|
145
|
+
from langchain_aotrust import get_client
|
|
146
|
+
c = get_client()
|
|
147
|
+
self.assertFalse(c.base_url.rstrip("/").endswith("/v1"),
|
|
148
|
+
f"base_url must NOT contain /v1 (got {c.base_url})")
|
|
149
|
+
self.assertTrue(c.base_url.startswith("https://api.aotrust.link"))
|
|
150
|
+
|
|
151
|
+
def test_configure_respects_custom_url(self):
|
|
152
|
+
from langchain_aotrust import configure, get_client, reset_client
|
|
153
|
+
configure(base_url="https://test.example.org")
|
|
154
|
+
c = get_client()
|
|
155
|
+
self.assertEqual(c.base_url, "https://test.example.org")
|
|
156
|
+
reset_client()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
unittest.main()
|