mint-attest 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.
- mint_attest-0.1.0/LICENSE +21 -0
- mint_attest-0.1.0/PKG-INFO +160 -0
- mint_attest-0.1.0/README.md +126 -0
- mint_attest-0.1.0/mint_attest/__init__.py +29 -0
- mint_attest-0.1.0/mint_attest/autogen.py +5 -0
- mint_attest-0.1.0/mint_attest/client.py +176 -0
- mint_attest-0.1.0/mint_attest/crewai.py +5 -0
- mint_attest-0.1.0/mint_attest/decorator.py +97 -0
- mint_attest-0.1.0/mint_attest/exceptions.py +31 -0
- mint_attest-0.1.0/mint_attest/integrations/__init__.py +8 -0
- mint_attest-0.1.0/mint_attest/integrations/autogen.py +72 -0
- mint_attest-0.1.0/mint_attest/integrations/crewai.py +88 -0
- mint_attest-0.1.0/mint_attest/integrations/langchain.py +68 -0
- mint_attest-0.1.0/mint_attest/langchain.py +5 -0
- mint_attest-0.1.0/mint_attest/models.py +108 -0
- mint_attest-0.1.0/mint_attest/py.typed +0 -0
- mint_attest-0.1.0/mint_attest.egg-info/PKG-INFO +160 -0
- mint_attest-0.1.0/mint_attest.egg-info/SOURCES.txt +23 -0
- mint_attest-0.1.0/mint_attest.egg-info/dependency_links.txt +1 -0
- mint_attest-0.1.0/mint_attest.egg-info/requires.txt +14 -0
- mint_attest-0.1.0/mint_attest.egg-info/top_level.txt +1 -0
- mint_attest-0.1.0/pyproject.toml +44 -0
- mint_attest-0.1.0/setup.cfg +4 -0
- mint_attest-0.1.0/setup.py +5 -0
- mint_attest-0.1.0/tests/test_client.py +184 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FoundryNet
|
|
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,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mint-attest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal work attestation for AI agents. Prove your agent did the work.
|
|
5
|
+
Author: FoundryNet
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/FoundryNet/mint-attest
|
|
8
|
+
Project-URL: Documentation, https://mint.foundrynet.io
|
|
9
|
+
Project-URL: Source, https://github.com/FoundryNet/mint-attest
|
|
10
|
+
Keywords: ai,agents,attestation,trust,solana,verification,mint,langchain,crewai,autogen
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: httpx>=0.24
|
|
24
|
+
Provides-Extra: langchain
|
|
25
|
+
Requires-Dist: langchain>=0.1; extra == "langchain"
|
|
26
|
+
Provides-Extra: crewai
|
|
27
|
+
Requires-Dist: crewai>=0.1; extra == "crewai"
|
|
28
|
+
Provides-Extra: autogen
|
|
29
|
+
Requires-Dist: pyautogen>=0.2; extra == "autogen"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# mint-attest
|
|
36
|
+
|
|
37
|
+
**Universal work attestation for AI agents.** Prove your agent did the work. Build verifiable trust. Settled on Solana.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install mint-attest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start (3 lines)
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from mint_attest import attest
|
|
47
|
+
|
|
48
|
+
@attest(work_type="code_review")
|
|
49
|
+
def review(files):
|
|
50
|
+
return do_review(files)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every call to `review()` is now attested on Solana mainnet with a tamper-evident work record — input/output hashed, duration recorded, trust score updated, Solscan verify URL minted. Your function's return value is unchanged.
|
|
54
|
+
|
|
55
|
+
Set your key once (mirrors `OPENAI_API_KEY` / `ANTHROPIC_API_KEY`):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export MINT_API_KEY=fnet_… # free key at foundrynet.io
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Why?
|
|
62
|
+
|
|
63
|
+
1.3 billion AI agents by 2028. No way to verify which ones actually do good work.
|
|
64
|
+
|
|
65
|
+
mint-attest gives your agent a verifiable track record. Register once. Attest every task. Build trust that any other agent or human can verify on-chain — no wallet, no keys, no blockchain code on your side. It's just an API call.
|
|
66
|
+
|
|
67
|
+
## Two ways to use it
|
|
68
|
+
|
|
69
|
+
### 1 — Decorator (zero friction)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from mint_attest import attest
|
|
73
|
+
|
|
74
|
+
@attest(work_type="code_review")
|
|
75
|
+
def review_code(files):
|
|
76
|
+
return do_the_review(files)
|
|
77
|
+
# auto-registers the agent once, hashes input/output, records duration,
|
|
78
|
+
# attests on MINT, and returns results unchanged.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Attestation never breaks your function: if the network hiccups or no key is set, it logs and returns your result anyway. Pass `@attest(..., strict=True)` to opt into raising.
|
|
82
|
+
|
|
83
|
+
### 2 — Explicit client (more control)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from mint_attest import MintClient
|
|
87
|
+
|
|
88
|
+
mint = MintClient(api_key="fnet_…") # or MINT_API_KEY env
|
|
89
|
+
|
|
90
|
+
actor = mint.register(
|
|
91
|
+
name="CodeReviewBot",
|
|
92
|
+
actor_type="ai_agent",
|
|
93
|
+
capabilities=["code_review", "security_audit"],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
receipt = mint.attest(
|
|
97
|
+
work_type="code_review",
|
|
98
|
+
input_data=files, # hashed for you (SHA-256)
|
|
99
|
+
output_data=results,
|
|
100
|
+
duration_seconds=elapsed,
|
|
101
|
+
)
|
|
102
|
+
print(receipt.verify_url) # https://solscan.io/tx/…
|
|
103
|
+
|
|
104
|
+
trust = mint.verify(actor.mint_id)
|
|
105
|
+
print(trust.score, trust.total_attestations)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Works with your framework
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# LangChain — every chain.run() attests
|
|
112
|
+
from mint_attest.langchain import MintAttestCallback
|
|
113
|
+
chain = LLMChain(llm=llm, prompt=prompt, callbacks=[MintAttestCallback(api_key="fnet_…")])
|
|
114
|
+
|
|
115
|
+
# CrewAI — give the crew an attest tool (or use mint_attest_step_callback)
|
|
116
|
+
from mint_attest.crewai import MintAttestTool
|
|
117
|
+
crew = Crew(agents=[researcher], tools=[MintAttestTool(api_key="fnet_…")])
|
|
118
|
+
|
|
119
|
+
# AutoGen — attest every reply
|
|
120
|
+
from mint_attest.autogen import MintAttestHook
|
|
121
|
+
MintAttestHook(api_key="fnet_…").attach(agent)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Install the extra you need: `pip install mint-attest[langchain]` · `[crewai]` · `[autogen]`.
|
|
125
|
+
|
|
126
|
+
## What happens on each attestation
|
|
127
|
+
|
|
128
|
+
1. Input/output hashed (SHA-256) — your data never leaves; only the hash does
|
|
129
|
+
2. Duration recorded
|
|
130
|
+
3. Work record settled on Solana mainnet
|
|
131
|
+
4. Trust score updated
|
|
132
|
+
5. Verify URL returned (Solscan)
|
|
133
|
+
|
|
134
|
+
Your agent's work history is permanent, tamper-evident, and publicly verifiable.
|
|
135
|
+
|
|
136
|
+
## Pricing
|
|
137
|
+
|
|
138
|
+
| Action | Price |
|
|
139
|
+
|--------|-------|
|
|
140
|
+
| Register | **FREE** |
|
|
141
|
+
| Verify | **FREE** |
|
|
142
|
+
| Attest | **$0.02** per attestation |
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
| Env var | Purpose |
|
|
147
|
+
|---------|---------|
|
|
148
|
+
| `MINT_API_KEY` | your `fnet_` key (required for register/attest) |
|
|
149
|
+
| `MINT_AGENT_NAME` | default agent name (optional) |
|
|
150
|
+
| `MINT_ENDPOINT` | override the MINT server (default `https://mint-mcp-production.up.railway.app`) |
|
|
151
|
+
|
|
152
|
+
No Solana dependency. No wallet. No transaction signing. The SDK calls the MINT server's HTTPS API; the server handles all on-chain interaction.
|
|
153
|
+
|
|
154
|
+
## Links
|
|
155
|
+
|
|
156
|
+
- Explorer: https://mint.foundrynet.io
|
|
157
|
+
- MCP server: https://github.com/FoundryNet/mint-mcp
|
|
158
|
+
- Protocol: https://foundrynet.io
|
|
159
|
+
|
|
160
|
+
MIT licensed.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# mint-attest
|
|
2
|
+
|
|
3
|
+
**Universal work attestation for AI agents.** Prove your agent did the work. Build verifiable trust. Settled on Solana.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install mint-attest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start (3 lines)
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from mint_attest import attest
|
|
13
|
+
|
|
14
|
+
@attest(work_type="code_review")
|
|
15
|
+
def review(files):
|
|
16
|
+
return do_review(files)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Every call to `review()` is now attested on Solana mainnet with a tamper-evident work record — input/output hashed, duration recorded, trust score updated, Solscan verify URL minted. Your function's return value is unchanged.
|
|
20
|
+
|
|
21
|
+
Set your key once (mirrors `OPENAI_API_KEY` / `ANTHROPIC_API_KEY`):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export MINT_API_KEY=fnet_… # free key at foundrynet.io
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Why?
|
|
28
|
+
|
|
29
|
+
1.3 billion AI agents by 2028. No way to verify which ones actually do good work.
|
|
30
|
+
|
|
31
|
+
mint-attest gives your agent a verifiable track record. Register once. Attest every task. Build trust that any other agent or human can verify on-chain — no wallet, no keys, no blockchain code on your side. It's just an API call.
|
|
32
|
+
|
|
33
|
+
## Two ways to use it
|
|
34
|
+
|
|
35
|
+
### 1 — Decorator (zero friction)
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from mint_attest import attest
|
|
39
|
+
|
|
40
|
+
@attest(work_type="code_review")
|
|
41
|
+
def review_code(files):
|
|
42
|
+
return do_the_review(files)
|
|
43
|
+
# auto-registers the agent once, hashes input/output, records duration,
|
|
44
|
+
# attests on MINT, and returns results unchanged.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Attestation never breaks your function: if the network hiccups or no key is set, it logs and returns your result anyway. Pass `@attest(..., strict=True)` to opt into raising.
|
|
48
|
+
|
|
49
|
+
### 2 — Explicit client (more control)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from mint_attest import MintClient
|
|
53
|
+
|
|
54
|
+
mint = MintClient(api_key="fnet_…") # or MINT_API_KEY env
|
|
55
|
+
|
|
56
|
+
actor = mint.register(
|
|
57
|
+
name="CodeReviewBot",
|
|
58
|
+
actor_type="ai_agent",
|
|
59
|
+
capabilities=["code_review", "security_audit"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
receipt = mint.attest(
|
|
63
|
+
work_type="code_review",
|
|
64
|
+
input_data=files, # hashed for you (SHA-256)
|
|
65
|
+
output_data=results,
|
|
66
|
+
duration_seconds=elapsed,
|
|
67
|
+
)
|
|
68
|
+
print(receipt.verify_url) # https://solscan.io/tx/…
|
|
69
|
+
|
|
70
|
+
trust = mint.verify(actor.mint_id)
|
|
71
|
+
print(trust.score, trust.total_attestations)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Works with your framework
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# LangChain — every chain.run() attests
|
|
78
|
+
from mint_attest.langchain import MintAttestCallback
|
|
79
|
+
chain = LLMChain(llm=llm, prompt=prompt, callbacks=[MintAttestCallback(api_key="fnet_…")])
|
|
80
|
+
|
|
81
|
+
# CrewAI — give the crew an attest tool (or use mint_attest_step_callback)
|
|
82
|
+
from mint_attest.crewai import MintAttestTool
|
|
83
|
+
crew = Crew(agents=[researcher], tools=[MintAttestTool(api_key="fnet_…")])
|
|
84
|
+
|
|
85
|
+
# AutoGen — attest every reply
|
|
86
|
+
from mint_attest.autogen import MintAttestHook
|
|
87
|
+
MintAttestHook(api_key="fnet_…").attach(agent)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Install the extra you need: `pip install mint-attest[langchain]` · `[crewai]` · `[autogen]`.
|
|
91
|
+
|
|
92
|
+
## What happens on each attestation
|
|
93
|
+
|
|
94
|
+
1. Input/output hashed (SHA-256) — your data never leaves; only the hash does
|
|
95
|
+
2. Duration recorded
|
|
96
|
+
3. Work record settled on Solana mainnet
|
|
97
|
+
4. Trust score updated
|
|
98
|
+
5. Verify URL returned (Solscan)
|
|
99
|
+
|
|
100
|
+
Your agent's work history is permanent, tamper-evident, and publicly verifiable.
|
|
101
|
+
|
|
102
|
+
## Pricing
|
|
103
|
+
|
|
104
|
+
| Action | Price |
|
|
105
|
+
|--------|-------|
|
|
106
|
+
| Register | **FREE** |
|
|
107
|
+
| Verify | **FREE** |
|
|
108
|
+
| Attest | **$0.02** per attestation |
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
| Env var | Purpose |
|
|
113
|
+
|---------|---------|
|
|
114
|
+
| `MINT_API_KEY` | your `fnet_` key (required for register/attest) |
|
|
115
|
+
| `MINT_AGENT_NAME` | default agent name (optional) |
|
|
116
|
+
| `MINT_ENDPOINT` | override the MINT server (default `https://mint-mcp-production.up.railway.app`) |
|
|
117
|
+
|
|
118
|
+
No Solana dependency. No wallet. No transaction signing. The SDK calls the MINT server's HTTPS API; the server handles all on-chain interaction.
|
|
119
|
+
|
|
120
|
+
## Links
|
|
121
|
+
|
|
122
|
+
- Explorer: https://mint.foundrynet.io
|
|
123
|
+
- MCP server: https://github.com/FoundryNet/mint-mcp
|
|
124
|
+
- Protocol: https://foundrynet.io
|
|
125
|
+
|
|
126
|
+
MIT licensed.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""mint-attest — universal work attestation for AI agents.
|
|
2
|
+
|
|
3
|
+
from mint_attest import attest
|
|
4
|
+
|
|
5
|
+
@attest(work_type="code_review")
|
|
6
|
+
def review(files):
|
|
7
|
+
return do_review(files)
|
|
8
|
+
|
|
9
|
+
Register once, attest every task, build a verifiable on-chain track record. The
|
|
10
|
+
SDK is a thin HTTPS client to the MINT server; all Solana interaction happens
|
|
11
|
+
server-side. Framework integrations live in mint_attest.langchain / .crewai /
|
|
12
|
+
.autogen (imported only when you use them).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
from .client import MintClient, hash_data
|
|
19
|
+
from .decorator import attest, configure, get_default_client, set_default_client
|
|
20
|
+
from .exceptions import MintAPIError, MintAuthError, MintConfigError, MintError
|
|
21
|
+
from .models import Actor, Receipt, TrustScore
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
"attest", "configure", "MintClient",
|
|
26
|
+
"get_default_client", "set_default_client", "hash_data",
|
|
27
|
+
"Actor", "Receipt", "TrustScore",
|
|
28
|
+
"MintError", "MintAuthError", "MintAPIError", "MintConfigError",
|
|
29
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""MintClient — the core: register, attest, verify against the MINT REST surface.
|
|
2
|
+
|
|
3
|
+
Talks plain HTTPS to the MINT server (default mint-mcp); the server handles all
|
|
4
|
+
Solana interaction. The developer never touches a wallet, signs a transaction, or
|
|
5
|
+
imports a blockchain library — it's just an API call. The developer's fnet_ key
|
|
6
|
+
(arg or MINT_API_KEY env) authenticates every request, so the agent and its
|
|
7
|
+
attestations belong to their account.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from . import __version__
|
|
21
|
+
from .exceptions import MintAPIError, MintAuthError, MintConfigError
|
|
22
|
+
from .models import Actor, Receipt, TrustScore
|
|
23
|
+
|
|
24
|
+
DEFAULT_ENDPOINT = "https://mint-mcp-production.up.railway.app"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def hash_data(obj: Any) -> str:
|
|
28
|
+
"""Deterministic SHA-256 of any value. bytes as-is, str as utf-8, everything
|
|
29
|
+
else as canonical (sorted-key) JSON so the same input always hashes the same."""
|
|
30
|
+
if isinstance(obj, (bytes, bytearray)):
|
|
31
|
+
b = bytes(obj)
|
|
32
|
+
elif isinstance(obj, str):
|
|
33
|
+
b = obj.encode("utf-8")
|
|
34
|
+
else:
|
|
35
|
+
b = json.dumps(obj, sort_keys=True, default=str).encode("utf-8")
|
|
36
|
+
return hashlib.sha256(b).hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _strip_none(d: dict) -> dict:
|
|
40
|
+
return {k: v for k, v in d.items() if v is not None}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MintClient:
|
|
44
|
+
"""One client per agent process. Register once (or let attest auto-register),
|
|
45
|
+
then attest each unit of work."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, api_key: Optional[str] = None, endpoint: Optional[str] = None,
|
|
48
|
+
*, name: Optional[str] = None, actor_type: str = "ai_agent",
|
|
49
|
+
capabilities: Optional[list] = None, operator: Optional[str] = None,
|
|
50
|
+
timeout: float = 30.0, transport: Optional[httpx.BaseTransport] = None):
|
|
51
|
+
self.api_key = api_key or os.environ.get("MINT_API_KEY")
|
|
52
|
+
self.endpoint = (endpoint or os.environ.get("MINT_ENDPOINT") or DEFAULT_ENDPOINT).rstrip("/")
|
|
53
|
+
self.timeout = timeout
|
|
54
|
+
# defaults used by auto-register; name also fills from MINT_AGENT_NAME
|
|
55
|
+
self._name = name or os.environ.get("MINT_AGENT_NAME")
|
|
56
|
+
self._actor_type = actor_type
|
|
57
|
+
self._capabilities = capabilities
|
|
58
|
+
self._operator = operator or os.environ.get("MINT_OPERATOR")
|
|
59
|
+
self._actor: Optional[Actor] = None
|
|
60
|
+
self._http = httpx.Client(timeout=timeout, transport=transport)
|
|
61
|
+
|
|
62
|
+
# ── identity helpers ──────────────────────────────────────────────────────
|
|
63
|
+
@property
|
|
64
|
+
def mint_id(self) -> Optional[str]:
|
|
65
|
+
return self._actor.mint_id if self._actor else None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def actor(self) -> Optional[Actor]:
|
|
69
|
+
return self._actor
|
|
70
|
+
|
|
71
|
+
def _headers(self) -> dict:
|
|
72
|
+
if not self.api_key:
|
|
73
|
+
raise MintAuthError(
|
|
74
|
+
"No API key. Pass MintClient(api_key='fnet_…') or set MINT_API_KEY.")
|
|
75
|
+
return {"Authorization": f"Bearer {self.api_key}",
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
"User-Agent": f"mint-attest/{__version__}"}
|
|
78
|
+
|
|
79
|
+
def _post(self, path: str, body: dict) -> dict:
|
|
80
|
+
try:
|
|
81
|
+
r = self._http.post(self.endpoint + path, json=body, headers=self._headers())
|
|
82
|
+
except httpx.HTTPError as e:
|
|
83
|
+
raise MintAPIError(f"network error calling {path}: {e}") from e
|
|
84
|
+
try:
|
|
85
|
+
data = r.json()
|
|
86
|
+
except Exception:
|
|
87
|
+
raise MintAPIError(f"non-JSON response from {path} (HTTP {r.status_code})",
|
|
88
|
+
status=r.status_code, detail=r.text[:300])
|
|
89
|
+
if r.status_code >= 400 or (isinstance(data, dict) and data.get("error")):
|
|
90
|
+
detail = data.get("detail") if isinstance(data, dict) else data
|
|
91
|
+
code = data.get("error") if isinstance(data, dict) else f"http_{r.status_code}"
|
|
92
|
+
if r.status_code in (401, 403):
|
|
93
|
+
raise MintAuthError(f"{code}: {detail}")
|
|
94
|
+
raise MintAPIError(f"{code}: {detail}", status=r.status_code, detail=detail)
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
# ── register ──────────────────────────────────────────────────────────────
|
|
98
|
+
def register(self, name: Optional[str] = None, actor_type: Optional[str] = None,
|
|
99
|
+
capabilities: Optional[list] = None, operator: Optional[str] = None,
|
|
100
|
+
metadata: Optional[dict] = None) -> Actor:
|
|
101
|
+
"""Provision (or look up — idempotent) this agent's MINT identity. Caches
|
|
102
|
+
the mint_id so later attest() calls reuse it. FREE."""
|
|
103
|
+
resolved_name = name or self._name
|
|
104
|
+
if not resolved_name:
|
|
105
|
+
raise MintConfigError(
|
|
106
|
+
"An agent name is required to register. Pass name=… (or set "
|
|
107
|
+
"MINT_AGENT_NAME / use @attest, which names the agent after the function).")
|
|
108
|
+
body = _strip_none({
|
|
109
|
+
"name": resolved_name,
|
|
110
|
+
"actor_type": actor_type or self._actor_type,
|
|
111
|
+
"capabilities": capabilities if capabilities is not None else self._capabilities,
|
|
112
|
+
"operator": operator or self._operator,
|
|
113
|
+
"metadata": metadata,
|
|
114
|
+
})
|
|
115
|
+
actor = Actor.from_dict(self._post("/v1/register", body))
|
|
116
|
+
self._actor = actor
|
|
117
|
+
# remember resolved defaults for any future auto-register
|
|
118
|
+
self._name = self._name or resolved_name
|
|
119
|
+
return actor
|
|
120
|
+
|
|
121
|
+
def _ensure_actor(self) -> Actor:
|
|
122
|
+
if self._actor is None:
|
|
123
|
+
self.register()
|
|
124
|
+
return self._actor # type: ignore[return-value]
|
|
125
|
+
|
|
126
|
+
# ── attest ─────────────────────────────────────────────────────────────────
|
|
127
|
+
def attest(self, work_type: str, input_data: Any = None, output_data: Any = None,
|
|
128
|
+
duration_seconds: Optional[float] = None, summary: Optional[str] = None,
|
|
129
|
+
metadata: Optional[dict] = None, *, mint_id: Optional[str] = None,
|
|
130
|
+
input_hash: Optional[str] = None, output_hash: Optional[str] = None) -> Receipt:
|
|
131
|
+
"""Attest a completed unit of work — anchors a tamper-evident record on
|
|
132
|
+
Solana mainnet and returns a Receipt (with verify_url). 2¢ per attestation.
|
|
133
|
+
|
|
134
|
+
Pass input_data/output_data (any value — hashed for you) OR precomputed
|
|
135
|
+
input_hash/output_hash. Auto-registers the agent on the first call if it
|
|
136
|
+
hasn't been registered yet.
|
|
137
|
+
"""
|
|
138
|
+
mid = mint_id or self.mint_id or self._ensure_actor().mint_id
|
|
139
|
+
if input_hash is None and input_data is not None:
|
|
140
|
+
input_hash = hash_data(input_data)
|
|
141
|
+
if output_hash is None and output_data is not None:
|
|
142
|
+
output_hash = hash_data(output_data)
|
|
143
|
+
# Forge requires duration_seconds > 0; round sub-second work up to 1.
|
|
144
|
+
dur = max(1, int(math.ceil(duration_seconds))) if duration_seconds else 1
|
|
145
|
+
body = _strip_none({
|
|
146
|
+
"mint_id": mid, "work_type": work_type, "duration_seconds": dur,
|
|
147
|
+
"summary": summary or f"{work_type} completed",
|
|
148
|
+
"input_hash": input_hash, "output_hash": output_hash, "metadata": metadata,
|
|
149
|
+
})
|
|
150
|
+
return Receipt.from_dict(self._post("/v1/attest", body))
|
|
151
|
+
|
|
152
|
+
# ── verify ──────────────────────────────────────────────────────────────────
|
|
153
|
+
def verify(self, mint_id: Optional[str] = None, actor_name: Optional[str] = None,
|
|
154
|
+
actor_type: Optional[str] = None) -> TrustScore:
|
|
155
|
+
"""Look up any actor's trust profile. FREE. Defaults to this client's own
|
|
156
|
+
agent when no identifier is given."""
|
|
157
|
+
body = _strip_none({
|
|
158
|
+
"mint_id": mint_id or (None if actor_name else self.mint_id),
|
|
159
|
+
"actor_name": actor_name, "actor_type": actor_type,
|
|
160
|
+
})
|
|
161
|
+
if not body:
|
|
162
|
+
raise MintConfigError("Provide mint_id or actor_name (or register first).")
|
|
163
|
+
return TrustScore.from_dict(self._post("/v1/verify", body))
|
|
164
|
+
|
|
165
|
+
# ── lifecycle ────────────────────────────────────────────────────────────────
|
|
166
|
+
def close(self) -> None:
|
|
167
|
+
try:
|
|
168
|
+
self._http.close()
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def __enter__(self) -> "MintClient":
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def __exit__(self, *exc) -> None:
|
|
176
|
+
self.close()
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""Convenience shim so `from mint_attest.crewai import MintAttestTool` works
|
|
2
|
+
(the implementation lives in mint_attest.integrations.crewai)."""
|
|
3
|
+
from .integrations.crewai import MintAttestTool, mint_attest_step_callback
|
|
4
|
+
|
|
5
|
+
__all__ = ["MintAttestTool", "mint_attest_step_callback"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""@attest — zero-friction attestation for any Python function, plus the
|
|
2
|
+
process-wide default client.
|
|
3
|
+
|
|
4
|
+
from mint_attest import attest
|
|
5
|
+
|
|
6
|
+
@attest(work_type="code_review")
|
|
7
|
+
def review_code(files):
|
|
8
|
+
return do_the_review(files)
|
|
9
|
+
|
|
10
|
+
Every call hashes the inputs/output, times the call, attests on MINT, and returns
|
|
11
|
+
the function's result UNCHANGED. Attestation failures (no key, network blip) are
|
|
12
|
+
logged and swallowed by default — instrumentation must never break the agent.
|
|
13
|
+
Set strict=True to raise instead.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import math
|
|
19
|
+
import time
|
|
20
|
+
from functools import wraps
|
|
21
|
+
from typing import Callable, Optional
|
|
22
|
+
|
|
23
|
+
from .client import MintClient, hash_data
|
|
24
|
+
from .exceptions import MintError
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger("mint_attest")
|
|
27
|
+
|
|
28
|
+
_default_client: Optional[MintClient] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_default_client() -> MintClient:
|
|
32
|
+
"""The lazily-created process-wide client (uses MINT_API_KEY / env defaults)."""
|
|
33
|
+
global _default_client
|
|
34
|
+
if _default_client is None:
|
|
35
|
+
_default_client = MintClient()
|
|
36
|
+
return _default_client
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def set_default_client(client: MintClient) -> None:
|
|
40
|
+
"""Override the process-wide client (e.g. with a custom endpoint or key)."""
|
|
41
|
+
global _default_client
|
|
42
|
+
_default_client = client
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def configure(api_key: Optional[str] = None, endpoint: Optional[str] = None,
|
|
46
|
+
name: Optional[str] = None, actor_type: str = "ai_agent",
|
|
47
|
+
capabilities: Optional[list] = None, operator: Optional[str] = None) -> MintClient:
|
|
48
|
+
"""Set up the default client in one call. Optional — env vars work too."""
|
|
49
|
+
client = MintClient(api_key=api_key, endpoint=endpoint, name=name,
|
|
50
|
+
actor_type=actor_type, capabilities=capabilities, operator=operator)
|
|
51
|
+
set_default_client(client)
|
|
52
|
+
return client
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def attest(work_type: str, name: Optional[str] = None,
|
|
56
|
+
capabilities: Optional[list] = None, client: Optional[MintClient] = None,
|
|
57
|
+
strict: bool = False) -> Callable:
|
|
58
|
+
"""Decorator: attest every call of the wrapped function on MINT.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
work_type: code_review|research|generation|analysis|delivery|
|
|
62
|
+
normalization|manufacturing|custom.
|
|
63
|
+
name: agent name to register under (defaults to the function's name; set
|
|
64
|
+
once for the process).
|
|
65
|
+
capabilities: capability tags recorded at registration.
|
|
66
|
+
client: a specific MintClient (defaults to the process-wide one).
|
|
67
|
+
strict: if True, re-raise attestation errors instead of swallowing them.
|
|
68
|
+
"""
|
|
69
|
+
def wrapper(func: Callable) -> Callable:
|
|
70
|
+
@wraps(func)
|
|
71
|
+
def inner(*args, **kwargs):
|
|
72
|
+
c = client or get_default_client()
|
|
73
|
+
# name the agent once: explicit name > whatever's already set > func name
|
|
74
|
+
if not c._name:
|
|
75
|
+
c._name = name or func.__name__
|
|
76
|
+
if capabilities and c._capabilities is None:
|
|
77
|
+
c._capabilities = capabilities
|
|
78
|
+
|
|
79
|
+
start = time.time()
|
|
80
|
+
result = func(*args, **kwargs) # run the real work first
|
|
81
|
+
duration = max(1, int(math.ceil(time.time() - start)))
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
c.attest(
|
|
85
|
+
work_type=work_type,
|
|
86
|
+
input_hash=hash_data({"args": args, "kwargs": kwargs}),
|
|
87
|
+
output_hash=hash_data(result),
|
|
88
|
+
duration_seconds=duration,
|
|
89
|
+
summary=f"{func.__name__} completed",
|
|
90
|
+
)
|
|
91
|
+
except MintError as e:
|
|
92
|
+
if strict:
|
|
93
|
+
raise
|
|
94
|
+
log.warning("mint-attest: attestation skipped (%s) — returning result anyway", e)
|
|
95
|
+
return result
|
|
96
|
+
return inner
|
|
97
|
+
return wrapper
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Exceptions for the mint-attest SDK.
|
|
2
|
+
|
|
3
|
+
All inherit from MintError so callers can `except MintError` to catch everything.
|
|
4
|
+
The @attest decorator swallows MintError by default (attestation must never break
|
|
5
|
+
the wrapped function); the explicit MintClient methods raise so callers can handle.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MintError(Exception):
|
|
13
|
+
"""Base class for all mint-attest errors."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MintAuthError(MintError):
|
|
17
|
+
"""No API key configured, or the server rejected it."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MintConfigError(MintError):
|
|
21
|
+
"""The client is missing required configuration (e.g. an agent name)."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MintAPIError(MintError):
|
|
25
|
+
"""The MINT API returned an error or was unreachable."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str, *, status: Optional[int] = None,
|
|
28
|
+
detail: Any = None):
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.status = status
|
|
31
|
+
self.detail = detail
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Framework integrations for mint-attest.
|
|
2
|
+
|
|
3
|
+
Each module wraps a MintClient in a framework's native hook/callback/tool pattern
|
|
4
|
+
so a developer adds ONE line to their existing agent and every task attests. The
|
|
5
|
+
frameworks are optional dependencies — these modules import them lazily and raise
|
|
6
|
+
a clear, install-hint error if the framework isn't present. Importing
|
|
7
|
+
`mint_attest` itself never imports any framework.
|
|
8
|
+
"""
|