assay-verify 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.
- assay_verify-0.1.0/PKG-INFO +59 -0
- assay_verify-0.1.0/README.md +42 -0
- assay_verify-0.1.0/assay_verify/__init__.py +17 -0
- assay_verify-0.1.0/assay_verify/_core.py +211 -0
- assay_verify-0.1.0/assay_verify/mcp.py +83 -0
- assay_verify-0.1.0/assay_verify.egg-info/PKG-INFO +59 -0
- assay_verify-0.1.0/assay_verify.egg-info/SOURCES.txt +11 -0
- assay_verify-0.1.0/assay_verify.egg-info/dependency_links.txt +1 -0
- assay_verify-0.1.0/assay_verify.egg-info/top_level.txt +1 -0
- assay_verify-0.1.0/pyproject.toml +27 -0
- assay_verify-0.1.0/setup.cfg +4 -0
- assay_verify-0.1.0/tests/test_core.py +49 -0
- assay_verify-0.1.0/tests/test_mcp.py +39 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assay-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Verification SDK for AI agent outputs - attest-at-generation, verify-on-consume. Pure stdlib, zero dependencies.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/chunxiaoxx/nautilus-compass
|
|
7
|
+
Project-URL: Protocol, https://github.com/chunxiaoxx/nautilus-compass/blob/main/docs/protocol/ASSAY_PROTOCOL_V0.md
|
|
8
|
+
Keywords: ai,agents,verification,provenance,ed25519,receipts,benchmark,trust
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Topic :: Security :: Cryptography
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# assay-verify
|
|
19
|
+
|
|
20
|
+
**AI 产出物的验证 SDK** — Let's Encrypt 模式,不是穆迪模式。验证不是一项服务,
|
|
21
|
+
是默认。Assay Protocol v0 的参考实现(CC-BY,任何人可实现)。
|
|
22
|
+
|
|
23
|
+
## 三行代码
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from assay_verify import verify, attest, keygen
|
|
27
|
+
|
|
28
|
+
keygen("my.key") # 一次性:生成密钥对
|
|
29
|
+
sig = attest("claims.json", "my.key") # 生产端:产出即打包(自动签名)
|
|
30
|
+
result = verify("claims.json", sig, pub_hex) # 消费端:一行验真
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
自定义声明(基准数字、agent 产出):
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
payload = {"agent": "my-bot", "benchmark": "swe-x", "score": 0.83,
|
|
37
|
+
"sha256": hashlib.sha256(artifacts).hexdigest()}
|
|
38
|
+
sig = attest("run.log", "my.key", payload=payload)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 三态语义(诚实验证器必须能说"算不了")
|
|
42
|
+
|
|
43
|
+
- `VALID` — 签名有效且哈希吻合
|
|
44
|
+
- `INVALID` — 验签失败(篡改或错钥)
|
|
45
|
+
- `MALFORMED` — 材料缺失/格式错
|
|
46
|
+
|
|
47
|
+
## 特性
|
|
48
|
+
|
|
49
|
+
- **零第三方依赖**,纯标准库(python ≥3.8)
|
|
50
|
+
- ed25519(RFC 8032)纯 python 实现,与 nautilus-compass 主实现**交叉互验**通过
|
|
51
|
+
- 向后兼容:直接验证 compass 已发布的签名成绩单(docs/wall/)
|
|
52
|
+
|
|
53
|
+
## 安装(即将上 PyPI)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install assay-verify
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
或直接拷贝 `assay_verify/` 目录(单包,无依赖,欢迎 vendor)。
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# assay-verify
|
|
2
|
+
|
|
3
|
+
**AI 产出物的验证 SDK** — Let's Encrypt 模式,不是穆迪模式。验证不是一项服务,
|
|
4
|
+
是默认。Assay Protocol v0 的参考实现(CC-BY,任何人可实现)。
|
|
5
|
+
|
|
6
|
+
## 三行代码
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from assay_verify import verify, attest, keygen
|
|
10
|
+
|
|
11
|
+
keygen("my.key") # 一次性:生成密钥对
|
|
12
|
+
sig = attest("claims.json", "my.key") # 生产端:产出即打包(自动签名)
|
|
13
|
+
result = verify("claims.json", sig, pub_hex) # 消费端:一行验真
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
自定义声明(基准数字、agent 产出):
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
payload = {"agent": "my-bot", "benchmark": "swe-x", "score": 0.83,
|
|
20
|
+
"sha256": hashlib.sha256(artifacts).hexdigest()}
|
|
21
|
+
sig = attest("run.log", "my.key", payload=payload)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 三态语义(诚实验证器必须能说"算不了")
|
|
25
|
+
|
|
26
|
+
- `VALID` — 签名有效且哈希吻合
|
|
27
|
+
- `INVALID` — 验签失败(篡改或错钥)
|
|
28
|
+
- `MALFORMED` — 材料缺失/格式错
|
|
29
|
+
|
|
30
|
+
## 特性
|
|
31
|
+
|
|
32
|
+
- **零第三方依赖**,纯标准库(python ≥3.8)
|
|
33
|
+
- ed25519(RFC 8032)纯 python 实现,与 nautilus-compass 主实现**交叉互验**通过
|
|
34
|
+
- 向后兼容:直接验证 compass 已发布的签名成绩单(docs/wall/)
|
|
35
|
+
|
|
36
|
+
## 安装(即将上 PyPI)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install assay-verify
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
或直接拷贝 `assay_verify/` 目录(单包,无依赖,欢迎 vendor)。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""assay-verify · AI 产出物的验证 SDK(Let's Encrypt 模式,Assay Protocol v0)。
|
|
3
|
+
|
|
4
|
+
三行代码::
|
|
5
|
+
|
|
6
|
+
from assay_verify import verify, attest, keygen
|
|
7
|
+
|
|
8
|
+
keygen("my.key") # 一次性
|
|
9
|
+
sig = attest("claims.json", "my.key") # 生产端:产出即打包
|
|
10
|
+
result = verify("claims.json", sig, pub_hex) # 消费端:一行验真(三态)
|
|
11
|
+
|
|
12
|
+
零第三方依赖,纯标准库。协议:Assay Protocol v0(CC-BY)。
|
|
13
|
+
"""
|
|
14
|
+
from ._core import verify, attest, keygen, AssayResult # noqa: F401
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = ["verify", "attest", "keygen", "AssayResult"]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""assay-verify 核心:纯 python ed25519(RFC 8032)+ 证据签名 + 三态验真。
|
|
3
|
+
|
|
4
|
+
ed25519 参考实现与 nautilus-compass tools/verifypack、scripts/verify_receipt.py
|
|
5
|
+
同源交叉验证(两套实现互验 VALID,2026-09-19)。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import secrets
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# ── ed25519(RFC 8032,签名+验证参考实现)────────────────────────────
|
|
16
|
+
_p = 2**255 - 19
|
|
17
|
+
_q = 2**252 + 27742317777372353535851937790883648493
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _inv(x: int) -> int:
|
|
21
|
+
return pow(x, _p - 2, _p)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_d = -121665 * _inv(121666) % _p
|
|
25
|
+
_I = pow(2, (_p - 1) // 4, _p)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _xrecover(y: int) -> int:
|
|
29
|
+
xx = (y * y - 1) * _inv(_d * y * y + 1)
|
|
30
|
+
x = pow(xx, (_p + 3) // 8, _p)
|
|
31
|
+
if (x * x - xx) % _p != 0:
|
|
32
|
+
x = x * _I % _p
|
|
33
|
+
if x % 2 != 0:
|
|
34
|
+
x = _p - x
|
|
35
|
+
return x
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_By = 4 * _inv(5) % _p
|
|
39
|
+
_Bx = _xrecover(_By)
|
|
40
|
+
_B = (_Bx % _p, _By % _p, 1, _Bx * _By % _p)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _edwards_add(P, Q):
|
|
44
|
+
x1, y1, z1, t1 = P
|
|
45
|
+
x2, y2, z2, t2 = Q
|
|
46
|
+
a = (y1 - x1) * (y2 - x2) % _p
|
|
47
|
+
b = (y1 + x1) * (y2 + x2) % _p
|
|
48
|
+
c = t1 * 2 * _d * t2 % _p
|
|
49
|
+
dd = z1 * 2 * z2 % _p
|
|
50
|
+
e, f, g, h = b - a, dd - c, dd + c, b + a
|
|
51
|
+
return (e * f % _p, g * h % _p, f * g % _p, e * h % _p)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _scalarmult(P, e):
|
|
55
|
+
if e == 0:
|
|
56
|
+
return (0, 1, 1, 0)
|
|
57
|
+
Q = _scalarmult(P, e // 2)
|
|
58
|
+
Q = _edwards_add(Q, Q)
|
|
59
|
+
if e & 1:
|
|
60
|
+
Q = _edwards_add(Q, P)
|
|
61
|
+
return Q
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _point_decompress(s: bytes):
|
|
65
|
+
if len(s) != 32:
|
|
66
|
+
raise ValueError("bad point")
|
|
67
|
+
y = int.from_bytes(s, "little")
|
|
68
|
+
sign = y >> 255
|
|
69
|
+
y &= (1 << 255) - 1
|
|
70
|
+
x = _xrecover(y)
|
|
71
|
+
if x & 1 != sign:
|
|
72
|
+
x = _p - x
|
|
73
|
+
P = (x, y, 1, x * y % _p)
|
|
74
|
+
if not (0 <= x < _p and 0 <= y < _p):
|
|
75
|
+
raise ValueError("out of range")
|
|
76
|
+
return P
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sha512(m: bytes) -> bytes:
|
|
80
|
+
return hashlib.sha512(m).digest()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _sha512_modq(m: bytes) -> int:
|
|
84
|
+
return int.from_bytes(_sha512(m), "little") % _q
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _encode_point(P) -> bytes:
|
|
88
|
+
"""射影坐标 → 32B 压缩编码(必须先仿射化:x=X/Z,y=Y/Z,Z≠1 时直接取 Y 是错的)。"""
|
|
89
|
+
x, y, z, _ = P
|
|
90
|
+
zinv = _inv(z)
|
|
91
|
+
x = x * zinv % _p
|
|
92
|
+
y = y * zinv % _p
|
|
93
|
+
return (y | ((x & 1) << 255)).to_bytes(32, "little")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _ed25519_verify(public: bytes, msg: bytes, signature: bytes) -> bool:
|
|
97
|
+
if len(public) != 32 or len(signature) != 64:
|
|
98
|
+
return False
|
|
99
|
+
try:
|
|
100
|
+
A = _point_decompress(public)
|
|
101
|
+
R = _point_decompress(signature[:32])
|
|
102
|
+
except ValueError:
|
|
103
|
+
return False
|
|
104
|
+
S = int.from_bytes(signature[32:], "little")
|
|
105
|
+
h = _sha512_modq(signature[:32] + public + msg)
|
|
106
|
+
sB = _scalarmult(_B, S)
|
|
107
|
+
hA = _scalarmult(A, h)
|
|
108
|
+
RhA = _edwards_add(R, hA)
|
|
109
|
+
return (sB[1] * RhA[2] - RhA[1] * sB[2]) % _p == 0 \
|
|
110
|
+
and (sB[0] * RhA[2] - RhA[0] * sB[2]) % _p == 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _ed25519_sign(msg: bytes, seed: bytes) -> bytes:
|
|
114
|
+
h = _sha512(seed)
|
|
115
|
+
a = int.from_bytes(h[:32], "little")
|
|
116
|
+
a &= (1 << 254) - 8
|
|
117
|
+
a |= 1 << 254
|
|
118
|
+
pub = _encode_point(_scalarmult(_B, a))
|
|
119
|
+
prefix = h[32:]
|
|
120
|
+
r = _sha512_modq(prefix + msg)
|
|
121
|
+
Rs = _encode_point(_scalarmult(_B, r))
|
|
122
|
+
s = (r + _sha512_modq(Rs + pub + msg) * a) % _q
|
|
123
|
+
return Rs + s.to_bytes(32, "little")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _ed25519_keypair() -> tuple[bytes, bytes]:
|
|
127
|
+
seed = secrets.token_bytes(32)
|
|
128
|
+
return seed, _pub_from_seed(seed)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _pub_from_seed(seed: bytes) -> bytes:
|
|
132
|
+
h = _sha512(seed)
|
|
133
|
+
a = int.from_bytes(h[:32], "little")
|
|
134
|
+
a &= (1 << 254) - 8
|
|
135
|
+
a |= 1 << 254
|
|
136
|
+
return _encode_point(_scalarmult(_B, a))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ── 协议层(Assay Protocol v0)────────────────────────────────────────
|
|
140
|
+
def canonical_json(obj) -> str:
|
|
141
|
+
return json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def default_payload(target: Path) -> dict:
|
|
145
|
+
"""墙面成绩单格式:{"scorecard": 文件名, "sha256": 全文哈希}。"""
|
|
146
|
+
return {"scorecard": target.name,
|
|
147
|
+
"sha256": hashlib.sha256(target.read_bytes()).hexdigest()}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class AssayResult:
|
|
152
|
+
ok: bool
|
|
153
|
+
target: str
|
|
154
|
+
sha256: str
|
|
155
|
+
detail: str = ""
|
|
156
|
+
|
|
157
|
+
def __bool__(self):
|
|
158
|
+
return self.ok
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def verify(target, sig, pubkey, payload=None) -> AssayResult:
|
|
162
|
+
"""消费端一行:验 (目标文件, .sig, 公钥hex) 三元组。三态:
|
|
163
|
+
VALID / INVALID(验签失败) / MALFORMED(材料缺失或格式错)。"""
|
|
164
|
+
target, sig = Path(target), Path(sig)
|
|
165
|
+
if not target.is_file():
|
|
166
|
+
return AssayResult(False, str(target), "", "target missing (MALFORMED)")
|
|
167
|
+
if not sig.is_file():
|
|
168
|
+
return AssayResult(False, str(target), "", "signature file missing (MALFORMED)")
|
|
169
|
+
try:
|
|
170
|
+
sig_bytes = bytes.fromhex(sig.read_text(encoding="utf-8").strip())
|
|
171
|
+
pub = bytes.fromhex(str(pubkey).strip())
|
|
172
|
+
except ValueError as e:
|
|
173
|
+
return AssayResult(False, str(target), "", f"bad hex: {e} (MALFORMED)")
|
|
174
|
+
if len(sig_bytes) != 64 or len(pub) != 32:
|
|
175
|
+
return AssayResult(False, str(target), "", "bad length (MALFORMED)")
|
|
176
|
+
try:
|
|
177
|
+
pay = payload if payload is not None else default_payload(target)
|
|
178
|
+
msg = canonical_json(pay).encode("utf-8")
|
|
179
|
+
except (TypeError, ValueError) as e:
|
|
180
|
+
return AssayResult(False, str(target), "", f"payload error: {e} (MALFORMED)")
|
|
181
|
+
ok = _ed25519_verify(pub, msg, sig_bytes)
|
|
182
|
+
digest = pay.get("sha256") if isinstance(pay, dict) else None
|
|
183
|
+
if not digest:
|
|
184
|
+
digest = hashlib.sha256(target.read_bytes()).hexdigest()
|
|
185
|
+
return AssayResult(ok, str(target), str(digest),
|
|
186
|
+
"VALID" if ok else "INVALID (signature mismatch)")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def keygen(key_path, name="assay") -> Path:
|
|
190
|
+
"""生成密钥对:写 <key_path>(私钥 hex)与同名 .pub。返回私钥路径。"""
|
|
191
|
+
seed, pub = _ed25519_keypair()
|
|
192
|
+
kp = Path(key_path)
|
|
193
|
+
kp.parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
kp.write_text(seed.hex() + "\n", encoding="utf-8", newline="\n")
|
|
195
|
+
pp = kp.with_suffix(".pub")
|
|
196
|
+
pp.write_text(pub.hex() + "\n", encoding="utf-8", newline="\n")
|
|
197
|
+
return kp
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def attest(target, key_path, payload=None, out=None) -> Path:
|
|
201
|
+
"""生产端一行:对目标文件签名,产 <target>.sig(返回 sig 路径)。
|
|
202
|
+
payload 缺省=墙面成绩单格式;自定义声明传 dict。"""
|
|
203
|
+
target = Path(target)
|
|
204
|
+
seed = bytes.fromhex(Path(key_path).read_text(encoding="utf-8").strip())
|
|
205
|
+
if len(seed) != 32:
|
|
206
|
+
raise ValueError("key file must contain 32-byte hex seed")
|
|
207
|
+
pay = payload if payload is not None else default_payload(target)
|
|
208
|
+
sig = _ed25519_sign(canonical_json(pay).encode("utf-8"), seed)
|
|
209
|
+
out = Path(out) if out else target.with_suffix(target.suffix + ".sig")
|
|
210
|
+
out.write_text(sig.hex(), encoding="utf-8", newline="\n")
|
|
211
|
+
return out
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""assay-verify 自带 MCP 服务器(stdio JSON-RPC)——验证能力经 MCP 分发。
|
|
3
|
+
|
|
4
|
+
运行:python -m assay_verify.mcp
|
|
5
|
+
任何 MCP 客户端(Claude Code/Desktop/Cursor/Cline…)配置::
|
|
6
|
+
|
|
7
|
+
{"mcpServers": {"assay": {"command": "python", "args": ["-m", "assay_verify.mcp"]}}}
|
|
8
|
+
|
|
9
|
+
即获得工具 `assay_verify`——对任意 (target, sig, pubkey) 一键三态验真。
|
|
10
|
+
零第三方依赖。
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from ._core import AssayResult, verify
|
|
18
|
+
|
|
19
|
+
PROTOCOL = "2024-11-05"
|
|
20
|
+
TOOLS = [{
|
|
21
|
+
"name": "assay_verify",
|
|
22
|
+
"description": "Verify a signed AI artifact (Assay Protocol v0). Returns "
|
|
23
|
+
"VALID / INVALID / MALFORMED with sha256 anchor.",
|
|
24
|
+
"inputSchema": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"target": {"type": "string", "description": "path to the target file"},
|
|
28
|
+
"sig": {"type": "string", "description": "path to the .sig file"},
|
|
29
|
+
"pubkey": {"type": "string", "description": "verifier public key (hex)"},
|
|
30
|
+
"payload_json": {"type": "string",
|
|
31
|
+
"description": "optional canonical payload JSON "
|
|
32
|
+
"(default: wall scorecard format)"},
|
|
33
|
+
},
|
|
34
|
+
"required": ["target", "sig", "pubkey"],
|
|
35
|
+
},
|
|
36
|
+
}]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _result(r: AssayResult) -> dict:
|
|
40
|
+
return {"content": [{"type": "text",
|
|
41
|
+
"text": json.dumps(
|
|
42
|
+
{"ok": r.ok, "detail": r.detail, "target": r.target,
|
|
43
|
+
"sha256": r.sha256}, ensure_ascii=False)}]}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _handle(method: str, params: dict) -> dict:
|
|
47
|
+
if method == "initialize":
|
|
48
|
+
return {"protocolVersion": PROTOCOL, "capabilities": {"tools": {}},
|
|
49
|
+
"serverInfo": {"name": "assay-verify", "version": "0.1.0"}}
|
|
50
|
+
if method == "tools/list":
|
|
51
|
+
return {"tools": TOOLS}
|
|
52
|
+
if method == "tools/call":
|
|
53
|
+
args = params.get("arguments", {})
|
|
54
|
+
payload = None
|
|
55
|
+
if args.get("payload_json"):
|
|
56
|
+
payload = json.loads(args["payload_json"])
|
|
57
|
+
return _result(verify(args["target"], args["sig"], args["pubkey"],
|
|
58
|
+
payload=payload))
|
|
59
|
+
raise ValueError(f"unknown method: {method}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def serve(stdin=sys.stdin.buffer, stdout=sys.stdout.buffer) -> None:
|
|
63
|
+
"""极简 MCP stdio 循环:Content-Length 分帧 JSON-RPC。"""
|
|
64
|
+
while True:
|
|
65
|
+
line = stdin.readline()
|
|
66
|
+
if not line:
|
|
67
|
+
break
|
|
68
|
+
text = line.decode("utf-8", "replace").strip()
|
|
69
|
+
if not text.startswith("{"):
|
|
70
|
+
continue
|
|
71
|
+
try:
|
|
72
|
+
msg = json.loads(text)
|
|
73
|
+
out = {"jsonrpc": "2.0", "id": msg.get("id"),
|
|
74
|
+
"result": _handle(msg.get("method", ""), msg.get("params", {}) or {})}
|
|
75
|
+
except Exception as e: # noqa: BLE001 — 错误也走 JSON-RPC
|
|
76
|
+
out = {"jsonrpc": "2.0", "id": msg.get("id") if isinstance(msg, dict) else None,
|
|
77
|
+
"error": {"code": -32603, "message": str(e)}}
|
|
78
|
+
stdout.write((json.dumps(out, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
79
|
+
stdout.flush()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
serve()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assay-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Verification SDK for AI agent outputs - attest-at-generation, verify-on-consume. Pure stdlib, zero dependencies.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/chunxiaoxx/nautilus-compass
|
|
7
|
+
Project-URL: Protocol, https://github.com/chunxiaoxx/nautilus-compass/blob/main/docs/protocol/ASSAY_PROTOCOL_V0.md
|
|
8
|
+
Keywords: ai,agents,verification,provenance,ed25519,receipts,benchmark,trust
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Topic :: Security :: Cryptography
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# assay-verify
|
|
19
|
+
|
|
20
|
+
**AI 产出物的验证 SDK** — Let's Encrypt 模式,不是穆迪模式。验证不是一项服务,
|
|
21
|
+
是默认。Assay Protocol v0 的参考实现(CC-BY,任何人可实现)。
|
|
22
|
+
|
|
23
|
+
## 三行代码
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from assay_verify import verify, attest, keygen
|
|
27
|
+
|
|
28
|
+
keygen("my.key") # 一次性:生成密钥对
|
|
29
|
+
sig = attest("claims.json", "my.key") # 生产端:产出即打包(自动签名)
|
|
30
|
+
result = verify("claims.json", sig, pub_hex) # 消费端:一行验真
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
自定义声明(基准数字、agent 产出):
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
payload = {"agent": "my-bot", "benchmark": "swe-x", "score": 0.83,
|
|
37
|
+
"sha256": hashlib.sha256(artifacts).hexdigest()}
|
|
38
|
+
sig = attest("run.log", "my.key", payload=payload)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 三态语义(诚实验证器必须能说"算不了")
|
|
42
|
+
|
|
43
|
+
- `VALID` — 签名有效且哈希吻合
|
|
44
|
+
- `INVALID` — 验签失败(篡改或错钥)
|
|
45
|
+
- `MALFORMED` — 材料缺失/格式错
|
|
46
|
+
|
|
47
|
+
## 特性
|
|
48
|
+
|
|
49
|
+
- **零第三方依赖**,纯标准库(python ≥3.8)
|
|
50
|
+
- ed25519(RFC 8032)纯 python 实现,与 nautilus-compass 主实现**交叉互验**通过
|
|
51
|
+
- 向后兼容:直接验证 compass 已发布的签名成绩单(docs/wall/)
|
|
52
|
+
|
|
53
|
+
## 安装(即将上 PyPI)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install assay-verify
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
或直接拷贝 `assay_verify/` 目录(单包,无依赖,欢迎 vendor)。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
assay_verify/__init__.py
|
|
4
|
+
assay_verify/_core.py
|
|
5
|
+
assay_verify/mcp.py
|
|
6
|
+
assay_verify.egg-info/PKG-INFO
|
|
7
|
+
assay_verify.egg-info/SOURCES.txt
|
|
8
|
+
assay_verify.egg-info/dependency_links.txt
|
|
9
|
+
assay_verify.egg-info/top_level.txt
|
|
10
|
+
tests/test_core.py
|
|
11
|
+
tests/test_mcp.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
assay_verify
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "assay-verify"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Verification SDK for AI agent outputs - attest-at-generation, verify-on-consume. Pure stdlib, zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
keywords = ["ai", "agents", "verification", "provenance", "ed25519", "receipts", "benchmark", "trust"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
19
|
+
"Topic :: Security :: Cryptography",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/chunxiaoxx/nautilus-compass"
|
|
24
|
+
Protocol = "https://github.com/chunxiaoxx/nautilus-compass/blob/main/docs/protocol/ASSAY_PROTOCOL_V0.md"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
packages = ["assay_verify"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""assay-verify SDK 测试:往返 + 跨实现兼容(验 compass 已发真成绩单)+ 三态。"""
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
7
|
+
from assay_verify import verify, attest, keygen # noqa: E402
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[3]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_roundtrip_and_tamper(tmp_path):
|
|
13
|
+
t = tmp_path / "claims.md"
|
|
14
|
+
t.write_text("# claims\naccuracy 0.91\n", encoding="utf-8")
|
|
15
|
+
kp = keygen(tmp_path / "k")
|
|
16
|
+
pub = (tmp_path / "k.pub").read_text(encoding="utf-8").strip()
|
|
17
|
+
sig = attest(t, kp)
|
|
18
|
+
r = verify(t, sig, pub)
|
|
19
|
+
assert r.ok and r.detail == "VALID"
|
|
20
|
+
t.write_text("# claims tampered\n", encoding="utf-8")
|
|
21
|
+
r2 = verify(t, sig, pub)
|
|
22
|
+
assert not r2.ok and "INVALID" in r2.detail
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_malformed_missing(tmp_path):
|
|
26
|
+
r = verify(tmp_path / "nope.md", tmp_path / "nope.sig", "00" * 32)
|
|
27
|
+
assert not r.ok and "MALFORMED" in r.detail
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_custom_payload_roundtrip(tmp_path):
|
|
31
|
+
t = tmp_path / "run.log"
|
|
32
|
+
t.write_text("bench output ...\n", encoding="utf-8")
|
|
33
|
+
kp = keygen(tmp_path / "k2")
|
|
34
|
+
pub = (tmp_path / "k2.pub").read_text(encoding="utf-8").strip()
|
|
35
|
+
pay = {"agent": "demo-bot", "benchmark": "swe-x", "score": 0.83,
|
|
36
|
+
"sha256": "deadbeef"}
|
|
37
|
+
sig = attest(t, kp, payload=pay)
|
|
38
|
+
assert verify(t, sig, pub, payload=pay).ok
|
|
39
|
+
assert not verify(t, sig, pub).ok # payload 不匹配→INVALID
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_backward_compat_real_scorecards():
|
|
43
|
+
"""跨实现兼容证明:必须能验 compass 主仓已发布的两张真成绩单。"""
|
|
44
|
+
pub = (ROOT / ".verifypack" / "compass.pub").read_text(encoding="utf-8").strip()
|
|
45
|
+
for name in ("EXAM5_SCORECARD.md", "EXAM5_SCORECARD_RETAKE1.md"):
|
|
46
|
+
t = ROOT / "docs" / "wall" / name
|
|
47
|
+
s = ROOT / "docs" / "wall" / (name.replace(".md", ".sig"))
|
|
48
|
+
r = verify(t, s, pub)
|
|
49
|
+
assert r.ok, f"{name}: {r.detail}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""MCP stdio 服务器测试:initialize → tools/list → tools/call(真成绩单)往返。"""
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
9
|
+
from assay_verify import mcp # noqa: E402
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[3]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _rpc(server_in, server_out, method, params=None, id_=1):
|
|
15
|
+
server_in.write((json.dumps({"jsonrpc": "2.0", "id": id_,
|
|
16
|
+
"method": method, "params": params or {}}) + "\n").encode("utf-8"))
|
|
17
|
+
server_in.seek(0)
|
|
18
|
+
mcp.serve(server_in, server_out)
|
|
19
|
+
server_out.seek(0)
|
|
20
|
+
return json.loads(server_out.readline().decode("utf-8"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_mcp_roundtrip_real_scorecard():
|
|
24
|
+
stdin, stdout = io.BytesIO(), io.BytesIO()
|
|
25
|
+
r1 = _rpc(stdin, stdout, "initialize")
|
|
26
|
+
assert r1["result"]["serverInfo"]["name"] == "assay-verify"
|
|
27
|
+
|
|
28
|
+
stdin, stdout = io.BytesIO(), io.BytesIO()
|
|
29
|
+
r2 = _rpc(stdin, stdout, "tools/list")
|
|
30
|
+
assert r2["result"]["tools"][0]["name"] == "assay_verify"
|
|
31
|
+
|
|
32
|
+
pub = (ROOT / ".verifypack" / "compass.pub").read_text(encoding="utf-8").strip()
|
|
33
|
+
t = ROOT / "docs" / "wall" / "EXAM5_SCORECARD.md"
|
|
34
|
+
s = ROOT / "docs" / "wall" / "EXAM5_SCORECARD.sig"
|
|
35
|
+
stdin, stdout = io.BytesIO(), io.BytesIO()
|
|
36
|
+
r3 = _rpc(stdin, stdout, "tools/call", {"arguments": {
|
|
37
|
+
"target": str(t), "sig": str(s), "pubkey": pub}})
|
|
38
|
+
text = r3["result"]["content"][0]["text"]
|
|
39
|
+
assert json.loads(text)["detail"] == "VALID"
|