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.
@@ -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,7 @@
1
+ langchain-core>=1.0
2
+ aotrust-protocol>=2.3.6
3
+ aiohttp>=3.9
4
+
5
+ [dev]
6
+ pytest
7
+ pytest-asyncio
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()