proofbundle 1.3.0__tar.gz → 1.4.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.
- {proofbundle-1.3.0/src/proofbundle.egg-info → proofbundle-1.4.0}/PKG-INFO +33 -19
- {proofbundle-1.3.0 → proofbundle-1.4.0}/README.md +29 -14
- {proofbundle-1.3.0 → proofbundle-1.4.0}/pyproject.toml +6 -7
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/__init__.py +6 -1
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/adapters/__init__.py +3 -1
- proofbundle-1.4.0/src/proofbundle/adapters/promptfoo.py +131 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/cli.py +31 -0
- proofbundle-1.4.0/src/proofbundle/hf_evals.py +185 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/sdjwt.py +5 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0/src/proofbundle.egg-info}/PKG-INFO +33 -19
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/SOURCES.txt +4 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/requires.txt +0 -4
- proofbundle-1.4.0/tests/test_hf_evals.py +152 -0
- proofbundle-1.4.0/tests/test_promptfoo.py +177 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/LICENSE +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/setup.cfg +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/_inspect_registry.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/_integration.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/adapters/eee.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/adapters/lm_eval.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/bundle.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/checkpoint.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/dsse.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/eee_eval_schema.json +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/emit.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/errors.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/evalclaim.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/inspect_hook.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/intoto.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/kbjwt.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/merkle.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/py.typed +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/pytest_plugin.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/sdjwt_issue.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/signature.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/statuslist.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle/tlogproof.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/src/proofbundle.egg-info/top_level.txt +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_adapters.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_adversarial.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_bundle.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_bundle_robustness.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_checkpoint.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_cli.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_cli_eval.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_cosignature.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_cosignature_mldsa.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_eee.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_emit.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_eval_claim_schema.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_evalclaim.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_examples.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_inspect_hook.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_intoto.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_intoto_dsse.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_kbjwt.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_merkle.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_merkle_property.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_pytest_plugin.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_rekor_interop.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_rfc6962_external_vectors.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_schema.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_sdjwt_issue.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_sdjwt_reference.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_signature.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_statuslist.py +0 -0
- {proofbundle-1.3.0 → proofbundle-1.4.0}/tests/test_tlogproof.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proofbundle
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT.
|
|
5
5
|
Author: Konrad Gruszka
|
|
6
6
|
License: MIT
|
|
@@ -14,14 +14,13 @@ Classifier: Development Status :: 3 - Alpha
|
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
22
|
Classifier: Topic :: Security :: Cryptography
|
|
24
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.10
|
|
25
24
|
Description-Content-Type: text/markdown
|
|
26
25
|
License-File: LICENSE
|
|
27
26
|
Requires-Dist: cryptography>=42
|
|
@@ -34,7 +33,7 @@ Requires-Dist: cryptography>=48; extra == "pq"
|
|
|
34
33
|
Provides-Extra: pytest
|
|
35
34
|
Requires-Dist: pytest>=7; extra == "pytest"
|
|
36
35
|
Provides-Extra: inspect
|
|
37
|
-
Requires-Dist: inspect_ai<0.4,>=0.3.112;
|
|
36
|
+
Requires-Dist: inspect_ai<0.4,>=0.3.112; extra == "inspect"
|
|
38
37
|
Provides-Extra: dev
|
|
39
38
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
40
39
|
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
@@ -44,7 +43,7 @@ Requires-Dist: build>=1; extra == "dev"
|
|
|
44
43
|
Requires-Dist: hypothesis>=6; extra == "dev"
|
|
45
44
|
Requires-Dist: rfc8785>=0.1.4; extra == "dev"
|
|
46
45
|
Requires-Dist: sd-jwt>=0.10; extra == "dev"
|
|
47
|
-
Requires-Dist: inspect_ai<0.4,>=0.3.112;
|
|
46
|
+
Requires-Dist: inspect_ai<0.4,>=0.3.112; extra == "dev"
|
|
48
47
|
Dynamic: license-file
|
|
49
48
|
|
|
50
49
|
<div align="center">
|
|
@@ -82,7 +81,7 @@ file, no server, no network.**
|
|
|
82
81
|
|
|
83
82
|
**At a glance:** `proofbundle emit` signs and anchors a payload; `proofbundle
|
|
84
83
|
verify` checks one self-contained `bundle.json` with three offline cryptographic
|
|
85
|
-
checks → `OK` or `FAILED`. No network, no daemon, no own crypto.
|
|
84
|
+
checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 209 tests + a CI mutation gate.
|
|
86
85
|
|
|
87
86
|
## Contents
|
|
88
87
|
|
|
@@ -159,12 +158,19 @@ A bundle is a single JSON document. `proofbundle` checks, offline:
|
|
|
159
158
|
Certificate Transparency)
|
|
160
159
|
3. **sd-jwt** (optional) — an embedded SD-JWT selective-disclosure credential is
|
|
161
160
|
well formed, and if an issuer key is given, correctly issuer-signed
|
|
162
|
-
4. **sd-jwt-key-binding** (optional, v1.2) —
|
|
163
|
-
|
|
164
|
-
`iat`/`aud`/`nonce`/`sd_hash` are
|
|
165
|
-
disclosure set, and the signature verifies under the
|
|
166
|
-
holder key. A present-but-broken KB-JWT fails the bundle
|
|
167
|
-
|
|
161
|
+
4. **sd-jwt-key-binding** (optional, v1.2; hardened v1.3) — proof-of-possession, **fail-closed**.
|
|
162
|
+
The check runs only once the issuer signature itself verified (an unauthenticated SD-JWT can
|
|
163
|
+
never report a valid holder binding). Then: `typ` is `kb+jwt`, `iat`/`aud`/`nonce`/`sd_hash` are
|
|
164
|
+
present, `sd_hash` binds the exact presented disclosure set, and the signature verifies under the
|
|
165
|
+
issuer-bound `cnf.jwk` holder key. A present-but-broken KB-JWT fails the bundle; and if the issuer
|
|
166
|
+
**bound** a `cnf` holder key but the presentation carries **no** KB-JWT, that fails too — stripping
|
|
167
|
+
the binding is a bearer downgrade, not a valid receipt. `verify --aud/--nonce` enforce RFC 9901
|
|
168
|
+
§7.3 audience/replay binding when the relying party supplies them.
|
|
169
|
+
|
|
170
|
+
Beyond the single-file bundle, the library also verifies, all offline: a **witnessed C2SP checkpoint**
|
|
171
|
+
(`verify_witnessed_checkpoint`, Ed25519 + post-quantum ML-DSA-44 cosignatures — the quorum counts distinct
|
|
172
|
+
witness **keys**, not names, so one key under many names can never stuff a threshold), a **C2SP
|
|
173
|
+
`.tlog-proof`** (`proofbundle verify-proof`), and a **Token Status List** revocation snapshot.
|
|
168
174
|
|
|
169
175
|
The verifier treats the payload as opaque bytes. It proves that these exact
|
|
170
176
|
bytes were signed and anchored, not what they mean. That is on purpose: it keeps
|
|
@@ -203,7 +209,7 @@ flowchart LR
|
|
|
203
209
|
pip install proofbundle
|
|
204
210
|
```
|
|
205
211
|
|
|
206
|
-
Requires Python 3.
|
|
212
|
+
Requires Python 3.10+ and [`cryptography`](https://cryptography.io). Signature
|
|
207
213
|
math is delegated to `cryptography`; this project never rolls its own crypto.
|
|
208
214
|
The Merkle and SD-JWT logic is pure standard library.
|
|
209
215
|
|
|
@@ -480,7 +486,7 @@ threshold was met without revealing the model or the data. See [INTEROP.md](INTE
|
|
|
480
486
|
[lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) `results_*.json` (the
|
|
481
487
|
genuine `acc,none` filter-suffix format). **`proofbundle.adapters.from_eee_dataset`** (v0.9) reads an
|
|
482
488
|
Every Eval Ever v0.2.2 aggregate JSON and builds a signed receipt — validated against the vendored EEE
|
|
483
|
-
schema, with **no runtime import** of `every_eval_ever` (it needs Python 3.12; proofbundle stays 3.
|
|
489
|
+
schema, with **no runtime import** of `every_eval_ever` (it needs Python 3.12; proofbundle stays 3.10+).
|
|
484
490
|
- **in-toto test-result export, DSSE-signed** (v0.9) — `proofbundle.intoto.export_intoto_dsse(claim,
|
|
485
491
|
signer)` emits the receipt as a DSSE-signed in-toto Statement v1 with the **generic
|
|
486
492
|
`test-result/v0.1` predicate** (result PASSED/FAILED, `configuration` ResourceDescriptors), so a generic
|
|
@@ -540,15 +546,23 @@ ISO/IEC DIS 24970), including the anti-patterns no one should claim.
|
|
|
540
546
|
(RFC 9901 §4.3, fail-closed; closes #1) with `cnf.jwk` issuance and holder presentation, **C2SP
|
|
541
547
|
tlog-cosignature** verification (Ed25519 cosignature/v1, witness quorum, split-view resistance), and
|
|
542
548
|
`verify --verbose` with the recomputed Merkle root (closes #2).
|
|
543
|
-
- **v1.3
|
|
549
|
+
- **v1.3** — the portable proof: **C2SP tlog-proof** emit/verify (+
|
|
544
550
|
`verify-proof` CLI), **ML-DSA-44** witness cosignatures (post-quantum, `[pq]` extra), **Token
|
|
545
551
|
Status List** snapshot verification (offline revocation), SD-JWT VC markers (`dc+sd-jwt`,
|
|
546
552
|
`vct`, `status`), COMPLIANCE.md (EU AI Act Art. 12 / NIST AI RMF mapping), and the mutation
|
|
547
553
|
suite as a CI gate.
|
|
548
|
-
- **
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
554
|
+
- **v1.4 (current release)** — distribution: a **promptfoo** adapter (results.json v3 →
|
|
555
|
+
pass_rate receipt), the **Hugging Face Community Evals bridge** (`pb1.` receipt tokens for the
|
|
556
|
+
`.eval_results` `verifyToken` field + a strict YAML entry emitter — proofbundle-verifiable,
|
|
557
|
+
explicitly NOT the HF-internal badge token), `hf-token` CLI, and the **Python 3.10+ floor**
|
|
558
|
+
(3.9 is EOL since 2025-10).
|
|
559
|
+
- **v2.0 (next major, research differentiation)** — **per-sample Merkle receipts**: the payload carries a
|
|
560
|
+
Merkle root over *individual* sample results, so a verifier can force a random-sample re-check instead of
|
|
561
|
+
trusting only the reported aggregate. This is the THREAT_MODEL's one named structural gap ("forced random
|
|
562
|
+
sub-sampling"), and closing it is the road from *authorship* to *sampled correctness*.
|
|
563
|
+
- **Deferred** (explicitly not yet built, stated honestly) — full SD-JWT VC conformance + `vct`
|
|
564
|
+
type-metadata resolution (pre-IESG), an official in-toto eval predicate (proposal path via
|
|
565
|
+
OpenSSF/CoSAI), and a full in-toto client.
|
|
552
566
|
|
|
553
567
|
## Contributing
|
|
554
568
|
|
|
@@ -33,7 +33,7 @@ file, no server, no network.**
|
|
|
33
33
|
|
|
34
34
|
**At a glance:** `proofbundle emit` signs and anchors a payload; `proofbundle
|
|
35
35
|
verify` checks one self-contained `bundle.json` with three offline cryptographic
|
|
36
|
-
checks → `OK` or `FAILED`. No network, no daemon, no own crypto.
|
|
36
|
+
checks → `OK` or `FAILED`. No network, no daemon, no own crypto. 209 tests + a CI mutation gate.
|
|
37
37
|
|
|
38
38
|
## Contents
|
|
39
39
|
|
|
@@ -110,12 +110,19 @@ A bundle is a single JSON document. `proofbundle` checks, offline:
|
|
|
110
110
|
Certificate Transparency)
|
|
111
111
|
3. **sd-jwt** (optional) — an embedded SD-JWT selective-disclosure credential is
|
|
112
112
|
well formed, and if an issuer key is given, correctly issuer-signed
|
|
113
|
-
4. **sd-jwt-key-binding** (optional, v1.2) —
|
|
114
|
-
|
|
115
|
-
`iat`/`aud`/`nonce`/`sd_hash` are
|
|
116
|
-
disclosure set, and the signature verifies under the
|
|
117
|
-
holder key. A present-but-broken KB-JWT fails the bundle
|
|
118
|
-
|
|
113
|
+
4. **sd-jwt-key-binding** (optional, v1.2; hardened v1.3) — proof-of-possession, **fail-closed**.
|
|
114
|
+
The check runs only once the issuer signature itself verified (an unauthenticated SD-JWT can
|
|
115
|
+
never report a valid holder binding). Then: `typ` is `kb+jwt`, `iat`/`aud`/`nonce`/`sd_hash` are
|
|
116
|
+
present, `sd_hash` binds the exact presented disclosure set, and the signature verifies under the
|
|
117
|
+
issuer-bound `cnf.jwk` holder key. A present-but-broken KB-JWT fails the bundle; and if the issuer
|
|
118
|
+
**bound** a `cnf` holder key but the presentation carries **no** KB-JWT, that fails too — stripping
|
|
119
|
+
the binding is a bearer downgrade, not a valid receipt. `verify --aud/--nonce` enforce RFC 9901
|
|
120
|
+
§7.3 audience/replay binding when the relying party supplies them.
|
|
121
|
+
|
|
122
|
+
Beyond the single-file bundle, the library also verifies, all offline: a **witnessed C2SP checkpoint**
|
|
123
|
+
(`verify_witnessed_checkpoint`, Ed25519 + post-quantum ML-DSA-44 cosignatures — the quorum counts distinct
|
|
124
|
+
witness **keys**, not names, so one key under many names can never stuff a threshold), a **C2SP
|
|
125
|
+
`.tlog-proof`** (`proofbundle verify-proof`), and a **Token Status List** revocation snapshot.
|
|
119
126
|
|
|
120
127
|
The verifier treats the payload as opaque bytes. It proves that these exact
|
|
121
128
|
bytes were signed and anchored, not what they mean. That is on purpose: it keeps
|
|
@@ -154,7 +161,7 @@ flowchart LR
|
|
|
154
161
|
pip install proofbundle
|
|
155
162
|
```
|
|
156
163
|
|
|
157
|
-
Requires Python 3.
|
|
164
|
+
Requires Python 3.10+ and [`cryptography`](https://cryptography.io). Signature
|
|
158
165
|
math is delegated to `cryptography`; this project never rolls its own crypto.
|
|
159
166
|
The Merkle and SD-JWT logic is pure standard library.
|
|
160
167
|
|
|
@@ -431,7 +438,7 @@ threshold was met without revealing the model or the data. See [INTEROP.md](INTE
|
|
|
431
438
|
[lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) `results_*.json` (the
|
|
432
439
|
genuine `acc,none` filter-suffix format). **`proofbundle.adapters.from_eee_dataset`** (v0.9) reads an
|
|
433
440
|
Every Eval Ever v0.2.2 aggregate JSON and builds a signed receipt — validated against the vendored EEE
|
|
434
|
-
schema, with **no runtime import** of `every_eval_ever` (it needs Python 3.12; proofbundle stays 3.
|
|
441
|
+
schema, with **no runtime import** of `every_eval_ever` (it needs Python 3.12; proofbundle stays 3.10+).
|
|
435
442
|
- **in-toto test-result export, DSSE-signed** (v0.9) — `proofbundle.intoto.export_intoto_dsse(claim,
|
|
436
443
|
signer)` emits the receipt as a DSSE-signed in-toto Statement v1 with the **generic
|
|
437
444
|
`test-result/v0.1` predicate** (result PASSED/FAILED, `configuration` ResourceDescriptors), so a generic
|
|
@@ -491,15 +498,23 @@ ISO/IEC DIS 24970), including the anti-patterns no one should claim.
|
|
|
491
498
|
(RFC 9901 §4.3, fail-closed; closes #1) with `cnf.jwk` issuance and holder presentation, **C2SP
|
|
492
499
|
tlog-cosignature** verification (Ed25519 cosignature/v1, witness quorum, split-view resistance), and
|
|
493
500
|
`verify --verbose` with the recomputed Merkle root (closes #2).
|
|
494
|
-
- **v1.3
|
|
501
|
+
- **v1.3** — the portable proof: **C2SP tlog-proof** emit/verify (+
|
|
495
502
|
`verify-proof` CLI), **ML-DSA-44** witness cosignatures (post-quantum, `[pq]` extra), **Token
|
|
496
503
|
Status List** snapshot verification (offline revocation), SD-JWT VC markers (`dc+sd-jwt`,
|
|
497
504
|
`vct`, `status`), COMPLIANCE.md (EU AI Act Art. 12 / NIST AI RMF mapping), and the mutation
|
|
498
505
|
suite as a CI gate.
|
|
499
|
-
- **
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
506
|
+
- **v1.4 (current release)** — distribution: a **promptfoo** adapter (results.json v3 →
|
|
507
|
+
pass_rate receipt), the **Hugging Face Community Evals bridge** (`pb1.` receipt tokens for the
|
|
508
|
+
`.eval_results` `verifyToken` field + a strict YAML entry emitter — proofbundle-verifiable,
|
|
509
|
+
explicitly NOT the HF-internal badge token), `hf-token` CLI, and the **Python 3.10+ floor**
|
|
510
|
+
(3.9 is EOL since 2025-10).
|
|
511
|
+
- **v2.0 (next major, research differentiation)** — **per-sample Merkle receipts**: the payload carries a
|
|
512
|
+
Merkle root over *individual* sample results, so a verifier can force a random-sample re-check instead of
|
|
513
|
+
trusting only the reported aggregate. This is the THREAT_MODEL's one named structural gap ("forced random
|
|
514
|
+
sub-sampling"), and closing it is the road from *authorship* to *sampled correctness*.
|
|
515
|
+
- **Deferred** (explicitly not yet built, stated honestly) — full SD-JWT VC conformance + `vct`
|
|
516
|
+
type-metadata resolution (pre-IESG), an official in-toto eval predicate (proposal path via
|
|
517
|
+
OpenSSF/CoSAI), and a full in-toto client.
|
|
503
518
|
|
|
504
519
|
## Contributing
|
|
505
520
|
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "proofbundle"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.0"
|
|
8
8
|
description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
12
|
authors = [{ name = "Konrad Gruszka" }]
|
|
13
13
|
keywords = [
|
|
@@ -26,7 +26,6 @@ classifiers = [
|
|
|
26
26
|
"Intended Audience :: Developers",
|
|
27
27
|
"License :: OSI Approved :: MIT License",
|
|
28
28
|
"Programming Language :: Python :: 3",
|
|
29
|
-
"Programming Language :: Python :: 3.9",
|
|
30
29
|
"Programming Language :: Python :: 3.10",
|
|
31
30
|
"Programming Language :: Python :: 3.11",
|
|
32
31
|
"Programming Language :: Python :: 3.12",
|
|
@@ -56,11 +55,11 @@ pq = ["cryptography>=48"]
|
|
|
56
55
|
pytest = ["pytest>=7"]
|
|
57
56
|
# The inspect_ai adapter uses the STABLE read_eval_log API (lazy import). Pinned with an UPPER bound:
|
|
58
57
|
# the .eval format + pydantic schema change between versions (inspect_ai issue 834), and the fixture
|
|
59
|
-
# test is bound to this range.
|
|
60
|
-
#
|
|
61
|
-
inspect = ['inspect_ai>=0.3.112,<0.4
|
|
58
|
+
# test is bound to this range. Since v1.4.0 the project floor is Python 3.10, so no per-extra version
|
|
59
|
+
# marker is needed here (inspect_ai's own >=3.10 requirement is always satisfiable on a supported base).
|
|
60
|
+
inspect = ['inspect_ai>=0.3.112,<0.4']
|
|
62
61
|
dev = ["pytest>=7", "ruff>=0.5", "jsonschema>=4", "mypy>=1.8", "build>=1", "hypothesis>=6",
|
|
63
|
-
"rfc8785>=0.1.4", "sd-jwt>=0.10", 'inspect_ai>=0.3.112,<0.4
|
|
62
|
+
"rfc8785>=0.1.4", "sd-jwt>=0.10", 'inspect_ai>=0.3.112,<0.4']
|
|
64
63
|
|
|
65
64
|
[project.urls]
|
|
66
65
|
Homepage = "https://b7n0de.com"
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
-
__version__ = "1.
|
|
16
|
+
__version__ = "1.4.0"
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"__version__",
|
|
@@ -30,6 +30,8 @@ __all__ = [
|
|
|
30
30
|
"verify_cosignature",
|
|
31
31
|
"verify_witnessed_checkpoint",
|
|
32
32
|
"verify_status_snapshot",
|
|
33
|
+
"receipt_token",
|
|
34
|
+
"verify_receipt_token",
|
|
33
35
|
"VerificationResult",
|
|
34
36
|
"Check",
|
|
35
37
|
"ProofBundleError",
|
|
@@ -47,6 +49,8 @@ _LAZY = {
|
|
|
47
49
|
"verify_cosignature": ".checkpoint",
|
|
48
50
|
"verify_witnessed_checkpoint": ".checkpoint",
|
|
49
51
|
"verify_status_snapshot": ".statuslist",
|
|
52
|
+
"receipt_token": ".hf_evals",
|
|
53
|
+
"verify_receipt_token": ".hf_evals",
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime stays lazy
|
|
@@ -55,6 +59,7 @@ if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime s
|
|
|
55
59
|
from .errors import Check, ProofBundleError, VerificationResult
|
|
56
60
|
from .checkpoint import verify_cosignature, verify_witnessed_checkpoint
|
|
57
61
|
from .kbjwt import verify_key_binding
|
|
62
|
+
from .hf_evals import receipt_token, verify_receipt_token
|
|
58
63
|
from .statuslist import verify_status_snapshot
|
|
59
64
|
from .tlogproof import verify_tlog_proof
|
|
60
65
|
from .merkle import verify_consistency, verify_inclusion
|
|
@@ -7,5 +7,7 @@ each fixture in tests/fixtures documents its source + version.
|
|
|
7
7
|
from .inspect_ai import from_inspect_ai_log
|
|
8
8
|
from .eee import from_eee_dataset
|
|
9
9
|
from .lm_eval import from_lm_eval_results
|
|
10
|
+
from .promptfoo import from_promptfoo_results
|
|
10
11
|
|
|
11
|
-
__all__ = ["from_lm_eval_results", "from_inspect_ai_log", "from_eee_dataset"
|
|
12
|
+
__all__ = ["from_lm_eval_results", "from_inspect_ai_log", "from_eee_dataset",
|
|
13
|
+
"from_promptfoo_results"]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Adapter for promptfoo `eval -o results.json` output (file-based, NO promptfoo import) — v1.4.
|
|
2
|
+
|
|
3
|
+
Parses the exported OutputFile JSON only. Format verified 2026-07-02 against promptfoo `main`
|
|
4
|
+
(`src/types/index.ts` OutputFile / EvaluateSummaryV3 / EvaluateStats, `src/util/output.ts`
|
|
5
|
+
writeOutput): top level carries `evalId`, `results`, `config`, `metadata`; the current summary is
|
|
6
|
+
**version 3** (`results.version == 3`) with whole-run aggregates in `results.stats` as
|
|
7
|
+
`successes` / `failures` / `errors`. The metric emitted is **pass_rate** = successes / (successes
|
|
8
|
+
+ failures + errors), rendered as a fixed-point decimal string (the claim schema takes decimal
|
|
9
|
+
strings, never floats).
|
|
10
|
+
|
|
11
|
+
Version honesty: legacy files with `results.version` 1/2 have a different shape (`table` instead
|
|
12
|
+
of `prompts`) and are REJECTED with a clear message rather than half-parsed; the "v4" storage
|
|
13
|
+
version never appears in output files (they still say `version: 3`). Redaction flags
|
|
14
|
+
(`PROMPTFOO_STRIP_*`) only affect prompt/response bodies — nothing this adapter reads.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
from decimal import Decimal
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from ..evalclaim import build_eval_claim
|
|
25
|
+
|
|
26
|
+
_SCALE = 6 # pass_rate decimal places — fixed-point, schema-conformant
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _pass_rate(successes: int, failures: int, errors: int) -> "tuple[str, int]":
|
|
30
|
+
total = successes + failures + errors
|
|
31
|
+
if total <= 0:
|
|
32
|
+
raise ValueError("promptfoo results contain no test outcomes (stats all zero)")
|
|
33
|
+
rate = (Decimal(successes) / Decimal(total)).quantize(Decimal(1).scaleb(-_SCALE))
|
|
34
|
+
return f"{rate:.{_SCALE}f}", total
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def from_promptfoo_results(path, *, comparator: str, threshold: str, timestamp: str,
|
|
38
|
+
model_salt: Optional[bytes] = None,
|
|
39
|
+
dataset_salt: Optional[bytes] = None):
|
|
40
|
+
"""Read a promptfoo results.json (summary v3) and build an eval claim over the run's
|
|
41
|
+
pass_rate. Returns (claim, salts).
|
|
42
|
+
|
|
43
|
+
- suite = `config.description` when present, else the `evalId` (the run's identity).
|
|
44
|
+
- model_id = the sorted, de-duplicated provider ids of the run (a promptfoo eval may span
|
|
45
|
+
several providers; the salted commitment pins the exact set).
|
|
46
|
+
- dataset_id = sha256 over the canonical JSON of `config.tests` AS RECORDED in results.json.
|
|
47
|
+
Inline tests bind the actual content (the suite IS the dataset); a `file://`/glob reference binds
|
|
48
|
+
the reference, not the resolved file content (an offline reader cannot resolve it) — provenance
|
|
49
|
+
records which case applies (`dataset_commitment_scope`) so the binding is never overstated.
|
|
50
|
+
- provenance: promptfooVersion, evalId, summary timestamp, per-outcome counts.
|
|
51
|
+
"""
|
|
52
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
53
|
+
if not isinstance(data, dict) or not isinstance(data.get("results"), dict):
|
|
54
|
+
raise ValueError("not a promptfoo output file (missing results object)")
|
|
55
|
+
summary = data["results"]
|
|
56
|
+
version = summary.get("version")
|
|
57
|
+
if version != 3:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"unsupported promptfoo summary version {version!r} — this adapter parses the "
|
|
60
|
+
"current version 3 (legacy v1/v2 files use a different 'table' shape; re-export "
|
|
61
|
+
"with a current promptfoo)")
|
|
62
|
+
stats = summary.get("stats")
|
|
63
|
+
if not isinstance(stats, dict):
|
|
64
|
+
raise ValueError("promptfoo results.stats missing")
|
|
65
|
+
counts = {}
|
|
66
|
+
for key in ("successes", "failures", "errors"):
|
|
67
|
+
val = stats.get(key, 0)
|
|
68
|
+
if isinstance(val, bool) or not isinstance(val, int) or val < 0:
|
|
69
|
+
raise ValueError(f"promptfoo stats.{key} must be a non-negative integer")
|
|
70
|
+
counts[key] = val
|
|
71
|
+
score, n = _pass_rate(counts["successes"], counts["failures"], counts["errors"])
|
|
72
|
+
|
|
73
|
+
config = data.get("config") or {}
|
|
74
|
+
eval_id = data.get("evalId")
|
|
75
|
+
suite = str(config.get("description") or eval_id or "promptfoo-run")
|
|
76
|
+
|
|
77
|
+
# Commit to the providers that ACTUALLY produced a result in this run — not config.providers, which can
|
|
78
|
+
# list providers that never ran (e.g. --filter-providers). Only fall back to config.providers if the
|
|
79
|
+
# summary recorded no per-result provider at all (release-review fix).
|
|
80
|
+
providers = set()
|
|
81
|
+
for res in summary.get("results", []):
|
|
82
|
+
prov = (res or {}).get("provider")
|
|
83
|
+
if isinstance(prov, dict) and prov.get("id"):
|
|
84
|
+
providers.add(str(prov["id"]))
|
|
85
|
+
elif isinstance(prov, str):
|
|
86
|
+
providers.add(prov)
|
|
87
|
+
if not providers:
|
|
88
|
+
for prov in config.get("providers") or []:
|
|
89
|
+
if isinstance(prov, str):
|
|
90
|
+
providers.add(prov)
|
|
91
|
+
elif isinstance(prov, dict) and prov.get("id"):
|
|
92
|
+
providers.add(str(prov["id"]))
|
|
93
|
+
model_id = "+".join(sorted(providers)) if providers else "unknown:promptfoo-provider"
|
|
94
|
+
|
|
95
|
+
tests = config.get("tests")
|
|
96
|
+
tests_canonical = json.dumps(tests, sort_keys=True, separators=(",", ":")) if tests else ""
|
|
97
|
+
dataset_id = ("promptfoo-tests-sha256:"
|
|
98
|
+
+ hashlib.sha256(tests_canonical.encode("utf-8")).hexdigest())
|
|
99
|
+
# HONESTY (release-review): the commitment is over `config.tests` AS RECORDED in results.json. When tests are
|
|
100
|
+
# INLINE, that is the content (the suite IS the dataset). When they are a `file://`/glob REFERENCE — whether a
|
|
101
|
+
# top-level string, a list containing a string, OR a `file://` nested inside an inline test's vars/assert —
|
|
102
|
+
# promptfoo records the unresolved reference, so the commitment binds the reference, NOT the file content
|
|
103
|
+
# (an offline reader cannot resolve external files). When there are NO tests at all the commitment is over the
|
|
104
|
+
# empty string and binds nothing. We label each case so a verifier is never misled that content was bound.
|
|
105
|
+
tests_by_reference = isinstance(tests, str) or (
|
|
106
|
+
isinstance(tests, list) and any(isinstance(t, str) for t in tests)) or ("file://" in tests_canonical)
|
|
107
|
+
if not tests:
|
|
108
|
+
commitment_scope = "config.tests_absent"
|
|
109
|
+
elif tests_by_reference:
|
|
110
|
+
commitment_scope = "config.tests_reference_only"
|
|
111
|
+
else:
|
|
112
|
+
commitment_scope = "config.tests_inline_content"
|
|
113
|
+
|
|
114
|
+
metadata = data.get("metadata") or {}
|
|
115
|
+
provenance = {"harness": "promptfoo",
|
|
116
|
+
"successes": str(counts["successes"]), "failures": str(counts["failures"]),
|
|
117
|
+
"errors": str(counts["errors"]),
|
|
118
|
+
"pass_rate_formula": "successes/(successes+failures+errors)",
|
|
119
|
+
"dataset_commitment_scope": commitment_scope}
|
|
120
|
+
if eval_id:
|
|
121
|
+
provenance["eval_id"] = str(eval_id)
|
|
122
|
+
if metadata.get("promptfooVersion"):
|
|
123
|
+
provenance["promptfoo_version"] = str(metadata["promptfooVersion"])
|
|
124
|
+
if summary.get("timestamp"):
|
|
125
|
+
provenance["run_timestamp"] = str(summary["timestamp"])
|
|
126
|
+
|
|
127
|
+
return build_eval_claim(
|
|
128
|
+
suite=suite, suite_version=f"promptfoo-summary-v{version}",
|
|
129
|
+
metric="pass_rate", comparator=comparator, threshold=threshold, score=score, n=n,
|
|
130
|
+
model_id=model_id, dataset_id=dataset_id, issuer="", timestamp=timestamp,
|
|
131
|
+
provenance=provenance, model_salt=model_salt, dataset_salt=dataset_salt)
|
|
@@ -155,6 +155,28 @@ def _cmd_verify_proof(args: argparse.Namespace) -> int:
|
|
|
155
155
|
return 0 if res["ok"] else 1
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def _cmd_hf_token(args: argparse.Namespace) -> int:
|
|
159
|
+
from .bundle import load_bundle # noqa: PLC0415
|
|
160
|
+
from .hf_evals import receipt_token, verify_receipt_token # noqa: PLC0415
|
|
161
|
+
try:
|
|
162
|
+
if args.verify:
|
|
163
|
+
token = args.bundle_or_token
|
|
164
|
+
if token.endswith(".txt") or "/" in token:
|
|
165
|
+
with open(token, encoding="utf-8") as handle:
|
|
166
|
+
token = handle.read().strip()
|
|
167
|
+
result, _bundle = verify_receipt_token(token)
|
|
168
|
+
for check in result.checks:
|
|
169
|
+
print(str(check))
|
|
170
|
+
print("=> OK" if result.ok else "=> FAILED")
|
|
171
|
+
return 0 if result.ok else 1
|
|
172
|
+
token = receipt_token(load_bundle(args.bundle_or_token))
|
|
173
|
+
print(token)
|
|
174
|
+
return 0
|
|
175
|
+
except (ProofBundleError, OSError, ValueError) as exc: # file/JSON/format errors → clean exit
|
|
176
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
177
|
+
return 2
|
|
178
|
+
|
|
179
|
+
|
|
158
180
|
def build_parser() -> argparse.ArgumentParser:
|
|
159
181
|
parser = argparse.ArgumentParser(
|
|
160
182
|
prog="proofbundle",
|
|
@@ -207,6 +229,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
207
229
|
verify_proof.add_argument("--json", action="store_true", help="machine readable output")
|
|
208
230
|
verify_proof.set_defaults(func=_cmd_verify_proof)
|
|
209
231
|
|
|
232
|
+
hf_token = sub.add_parser(
|
|
233
|
+
"hf-token",
|
|
234
|
+
help="pack a receipt into a pb1. token for HF eval_results, or verify one (v1.4)")
|
|
235
|
+
hf_token.add_argument("bundle_or_token",
|
|
236
|
+
help="bundle JSON path (emit) or pb1. token / token file (--verify)")
|
|
237
|
+
hf_token.add_argument("--verify", action="store_true",
|
|
238
|
+
help="verify a pb1. token instead of emitting one")
|
|
239
|
+
hf_token.set_defaults(func=_cmd_hf_token)
|
|
240
|
+
|
|
210
241
|
return parser
|
|
211
242
|
|
|
212
243
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Hugging Face Community Evals bridge — receipt tokens + `.eval_results/*.yaml` entries (v1.4).
|
|
2
|
+
|
|
3
|
+
HF Community Evals (Feb 2026, beta) lets anyone PR benchmark results into a model repo as
|
|
4
|
+
`.eval_results/*.yaml`; entries carry an optional, string-typed ``verifyToken`` documented as "a
|
|
5
|
+
signature that can be used to prove that evaluation is provably auditable and reproducible".
|
|
6
|
+
Schema verified 2026-07-02 against hub-docs (`eval_results.yaml` spec + docs page).
|
|
7
|
+
|
|
8
|
+
**Honesty first — what this module does and does not claim.** The Hub's own "verified" badge is
|
|
9
|
+
granted by HF **server-side** (currently: evaluation ran in HF Jobs with inspect-ai); the token
|
|
10
|
+
format HF validates is not publicly specified. proofbundle therefore does NOT fabricate an
|
|
11
|
+
HF-validated token. It defines its own self-describing, offline-verifiable token profile —
|
|
12
|
+
|
|
13
|
+
``pb1.`` + base64url(zlib(bundle JSON))
|
|
14
|
+
|
|
15
|
+
— which any third party (and, should they choose, HF) can verify with `verify_receipt_token`:
|
|
16
|
+
the full receipt travels inside the token, so verification is exactly `verify_bundle`, offline.
|
|
17
|
+
Putting it in the schema-valid `verifyToken` field makes the result *proofbundle-verifiable*;
|
|
18
|
+
it does not and must not be presented as HF-endorsed. The docs and `to_eval_results_entry` keep
|
|
19
|
+
that distinction explicit (the receipt link belongs in `source.url` / `notes` either way).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import base64
|
|
25
|
+
import json
|
|
26
|
+
import math
|
|
27
|
+
import zlib
|
|
28
|
+
from typing import Optional, Tuple
|
|
29
|
+
|
|
30
|
+
from .bundle import verify_bundle
|
|
31
|
+
from .errors import BundleFormatError, UnsupportedError, VerificationResult
|
|
32
|
+
|
|
33
|
+
__all__ = ["TOKEN_PREFIX", "receipt_token", "verify_receipt_token",
|
|
34
|
+
"to_eval_results_entry", "eval_results_yaml"]
|
|
35
|
+
|
|
36
|
+
TOKEN_PREFIX = "pb1."
|
|
37
|
+
_MAX_TOKEN_BYTES = 262_144 # 256 KiB decompressed cap — a receipt is a few KB; refuse zip bombs
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _b64url(data: bytes) -> str:
|
|
41
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _b64url_decode(s: str) -> bytes:
|
|
45
|
+
raw = s.encode("ascii")
|
|
46
|
+
return base64.urlsafe_b64decode(raw + b"=" * (-len(raw) % 4))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def receipt_token(bundle: dict) -> str:
|
|
50
|
+
"""Pack a receipt bundle into a compact, self-contained token: ``pb1.`` +
|
|
51
|
+
base64url(zlib(canonical bundle JSON)). The token IS the receipt — verifying it is verifying
|
|
52
|
+
the bundle, offline, no lookup."""
|
|
53
|
+
if not isinstance(bundle, dict) or "payload_b64" not in bundle:
|
|
54
|
+
raise BundleFormatError("receipt_token needs a bundle dict")
|
|
55
|
+
canonical = json.dumps(bundle, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
56
|
+
return TOKEN_PREFIX + _b64url(zlib.compress(canonical, 9))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def verify_receipt_token(token: str) -> Tuple[VerificationResult, Optional[dict]]:
|
|
60
|
+
"""Unpack and verify a ``pb1.`` receipt token. Returns (VerificationResult, bundle_dict).
|
|
61
|
+
Malformed tokens raise BundleFormatError — never a crash, never a silent pass."""
|
|
62
|
+
if not isinstance(token, str) or not token.startswith(TOKEN_PREFIX):
|
|
63
|
+
raise BundleFormatError(f"not a proofbundle receipt token (expected {TOKEN_PREFIX!r} prefix)")
|
|
64
|
+
try:
|
|
65
|
+
decomp = zlib.decompressobj()
|
|
66
|
+
raw = decomp.decompress(_b64url_decode(token[len(TOKEN_PREFIX):]), _MAX_TOKEN_BYTES)
|
|
67
|
+
if decomp.unconsumed_tail:
|
|
68
|
+
raise BundleFormatError("receipt token exceeds the decompression cap")
|
|
69
|
+
bundle = json.loads(raw)
|
|
70
|
+
except BundleFormatError:
|
|
71
|
+
raise
|
|
72
|
+
except (ValueError, TypeError, zlib.error) as exc:
|
|
73
|
+
raise BundleFormatError("receipt token is not valid base64url(zlib(JSON))") from exc
|
|
74
|
+
if not isinstance(bundle, dict):
|
|
75
|
+
raise BundleFormatError("receipt token does not contain a bundle object")
|
|
76
|
+
# Normalize an unsupported schema/alg to BundleFormatError so the documented contract holds — a malformed
|
|
77
|
+
# token never escapes as a different exception type (release-review fix).
|
|
78
|
+
try:
|
|
79
|
+
return verify_bundle(bundle), bundle
|
|
80
|
+
except UnsupportedError as exc:
|
|
81
|
+
raise BundleFormatError(f"receipt token bundle uses an unsupported schema/algorithm: {exc}") from exc
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def to_eval_results_entry(bundle: dict, *, dataset_id: str, task_id: str, value,
|
|
85
|
+
date: Optional[str] = None, source_url: Optional[str] = None,
|
|
86
|
+
source_name: Optional[str] = None, source_user: Optional[str] = None,
|
|
87
|
+
notes: Optional[str] = None, include_token: bool = True,
|
|
88
|
+
require_verified: bool = True) -> dict:
|
|
89
|
+
"""Build one HF `.eval_results/*.yaml` entry for a receipt.
|
|
90
|
+
|
|
91
|
+
``dataset_id``/``task_id`` name the Hub benchmark (per its `eval.yaml`); ``value`` is the
|
|
92
|
+
REQUIRED metric number — the caller chooses what to disclose (a receipt may withhold the
|
|
93
|
+
exact score via SD-JWT; publishing a value here IS a disclosure decision).
|
|
94
|
+
|
|
95
|
+
No-Fake guard: with ``require_verified=True`` (default) the bundle must verify NOW — an entry
|
|
96
|
+
is never generated from a broken receipt. ``include_token=True`` puts the ``pb1.`` token in
|
|
97
|
+
``verifyToken`` (schema-valid, proofbundle-verifiable; NOT the HF-internal badge token — HF's
|
|
98
|
+
"verified" badge is HF's server-side decision, and this module makes no claim about it).
|
|
99
|
+
"""
|
|
100
|
+
if require_verified:
|
|
101
|
+
result = verify_bundle(bundle)
|
|
102
|
+
if not result.ok:
|
|
103
|
+
raise BundleFormatError(
|
|
104
|
+
"refusing to build an eval_results entry from a bundle that does not verify: "
|
|
105
|
+
+ "; ".join(f"{c.name}: {c.detail}" for c in result.checks if not c.ok))
|
|
106
|
+
if not dataset_id or not task_id:
|
|
107
|
+
raise BundleFormatError("dataset_id and task_id are required (the Hub benchmark identity)")
|
|
108
|
+
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
|
|
109
|
+
raise BundleFormatError("value must be a number (or numeric string)")
|
|
110
|
+
# Reject non-numeric strings AND non-finite values (release-review fix): inf/-inf/nan (whether a float or a
|
|
111
|
+
# string like '1e400'/'nan' that float() accepts) would serialize to the non-standard tokens Infinity/NaN —
|
|
112
|
+
# neither valid JSON nor unambiguous YAML — so an eval_results.yaml value must be a FINITE number.
|
|
113
|
+
try:
|
|
114
|
+
numeric = float(value)
|
|
115
|
+
except (ValueError, TypeError, OverflowError) as exc: # OverflowError: an int beyond float range
|
|
116
|
+
raise BundleFormatError(f"value {value!r} is not a representable finite number") from exc
|
|
117
|
+
if not math.isfinite(numeric):
|
|
118
|
+
raise BundleFormatError(
|
|
119
|
+
"value must be a finite number — inf/-inf/nan cannot be represented in eval_results.yaml")
|
|
120
|
+
|
|
121
|
+
entry: dict = {"dataset": {"id": dataset_id, "task_id": task_id},
|
|
122
|
+
"value": numeric if isinstance(value, str) else value}
|
|
123
|
+
if include_token:
|
|
124
|
+
entry["verifyToken"] = receipt_token(bundle)
|
|
125
|
+
if date is not None:
|
|
126
|
+
entry["date"] = date
|
|
127
|
+
source = {}
|
|
128
|
+
if source_url:
|
|
129
|
+
source["url"] = source_url
|
|
130
|
+
if source_name:
|
|
131
|
+
source["name"] = source_name
|
|
132
|
+
if source_user:
|
|
133
|
+
source["user"] = source_user
|
|
134
|
+
if source:
|
|
135
|
+
if "url" not in source:
|
|
136
|
+
raise BundleFormatError("source.url is required when a source is given (HF schema)")
|
|
137
|
+
entry["source"] = source
|
|
138
|
+
if notes is not None:
|
|
139
|
+
entry["notes"] = notes
|
|
140
|
+
return entry
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _yaml_scalar(value) -> str:
|
|
144
|
+
"""Serialize one scalar for the entry YAML. Strings are always double-quoted (JSON-style
|
|
145
|
+
escaping is valid YAML), so dates stay strings and tokens survive any special characters."""
|
|
146
|
+
if isinstance(value, bool):
|
|
147
|
+
return "true" if value else "false"
|
|
148
|
+
if isinstance(value, (int, float)):
|
|
149
|
+
return json.dumps(value)
|
|
150
|
+
return json.dumps(str(value))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def eval_results_yaml(entries) -> str:
|
|
154
|
+
"""Render entries as a `.eval_results/*.yaml` document (block style, deterministic key
|
|
155
|
+
order per the HF spec example). Only the known, shallow schema is emitted — this is a
|
|
156
|
+
purpose-built serializer, not a general YAML writer."""
|
|
157
|
+
order = ("dataset", "value", "verifyToken", "date", "source", "notes")
|
|
158
|
+
dataset_order = ("id", "task_id", "revision")
|
|
159
|
+
source_order = ("url", "name", "user") # HF hub-docs spec fields (no 'org' — 'user' covers the HF org/user)
|
|
160
|
+
lines = []
|
|
161
|
+
for entry in entries:
|
|
162
|
+
unknown = set(entry) - set(order)
|
|
163
|
+
if unknown:
|
|
164
|
+
raise BundleFormatError(f"unknown eval_results entry field(s): {sorted(unknown)}")
|
|
165
|
+
first = True
|
|
166
|
+
for key in order:
|
|
167
|
+
if key not in entry:
|
|
168
|
+
continue
|
|
169
|
+
prefix = "- " if first else " "
|
|
170
|
+
first = False
|
|
171
|
+
val = entry[key]
|
|
172
|
+
if key in ("dataset", "source"):
|
|
173
|
+
sub_order = dataset_order if key == "dataset" else source_order
|
|
174
|
+
# fail-loud on unknown nested keys too (symmetric with the top-level check) — a dropped field
|
|
175
|
+
# would silently omit data from the published entry (release-review fix).
|
|
176
|
+
sub_unknown = set(val) - set(sub_order)
|
|
177
|
+
if sub_unknown:
|
|
178
|
+
raise BundleFormatError(f"unknown {key} field(s): {sorted(sub_unknown)}")
|
|
179
|
+
lines.append(f"{prefix}{key}:")
|
|
180
|
+
for sub in sub_order:
|
|
181
|
+
if sub in val:
|
|
182
|
+
lines.append(f" {sub}: {_yaml_scalar(val[sub])}")
|
|
183
|
+
else:
|
|
184
|
+
lines.append(f"{prefix}{key}: {_yaml_scalar(val)}")
|
|
185
|
+
return "\n".join(lines) + "\n"
|