proofbundle 3.2.2__tar.gz → 3.2.3__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.
Files changed (209) hide show
  1. {proofbundle-3.2.2/src/proofbundle.egg-info → proofbundle-3.2.3}/PKG-INFO +9 -4
  2. {proofbundle-3.2.2 → proofbundle-3.2.3}/README.md +8 -3
  3. {proofbundle-3.2.2 → proofbundle-3.2.3}/pyproject.toml +1 -1
  4. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/__init__.py +24 -1
  5. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/_strict_json.py +43 -3
  6. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/_provenance.py +30 -2
  7. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_markovian.py +6 -0
  8. proofbundle-3.2.3/src/proofbundle/assurance.py +201 -0
  9. proofbundle-3.2.3/src/proofbundle/automation_verdict.py +101 -0
  10. proofbundle-3.2.3/src/proofbundle/budget.py +103 -0
  11. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/bundle.py +13 -1
  12. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/cli.py +83 -7
  13. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/decision.py +53 -3
  14. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/dsse.py +17 -0
  15. proofbundle-3.2.3/src/proofbundle/evalcard.py +62 -0
  16. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/evalclaim.py +49 -2
  17. proofbundle-3.2.3/src/proofbundle/outcome.py +689 -0
  18. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policy.py +48 -1
  19. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/renewal.py +133 -2
  20. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/run_ledger.py +18 -1
  21. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/sdjwt.py +43 -13
  22. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/sdjwt_vc.py +3 -2
  23. proofbundle-3.2.3/src/proofbundle/signature.py +69 -0
  24. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/trust_pack.py +53 -6
  25. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/verification_summary.py +16 -1
  26. {proofbundle-3.2.2 → proofbundle-3.2.3/src/proofbundle.egg-info}/PKG-INFO +9 -4
  27. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/SOURCES.txt +13 -0
  28. proofbundle-3.2.3/tests/test_assurance.py +193 -0
  29. proofbundle-3.2.3/tests/test_automation_verdict.py +216 -0
  30. proofbundle-3.2.3/tests/test_budget.py +240 -0
  31. proofbundle-3.2.3/tests/test_bundle.py +132 -0
  32. proofbundle-3.2.3/tests/test_enclave_assurance.py +247 -0
  33. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_eval_claim_schema.py +10 -0
  34. proofbundle-3.2.3/tests/test_evalcard.py +136 -0
  35. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_evalclaim.py +19 -0
  36. proofbundle-3.2.3/tests/test_outcome_receiver_corroboration.py +436 -0
  37. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_outcome_verify.py +81 -0
  38. proofbundle-3.2.3/tests/test_provenance.py +177 -0
  39. proofbundle-3.2.3/tests/test_renewal_external_token_glue.py +269 -0
  40. proofbundle-3.2.3/tests/test_rust_parity_gate.py +391 -0
  41. proofbundle-3.2.3/tests/test_sdjwtvc_external_vectors.py +266 -0
  42. proofbundle-3.2.3/tests/test_signature.py +106 -0
  43. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_trust_pack.py +22 -0
  44. proofbundle-3.2.3/tests/test_trust_pack_outcome_receivers_role.py +75 -0
  45. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_trust_policy.py +80 -3
  46. proofbundle-3.2.2/src/proofbundle/outcome.py +0 -380
  47. proofbundle-3.2.2/src/proofbundle/signature.py +0 -31
  48. proofbundle-3.2.2/tests/test_bundle.py +0 -67
  49. proofbundle-3.2.2/tests/test_provenance.py +0 -77
  50. proofbundle-3.2.2/tests/test_sdjwtvc_external_vectors.py +0 -162
  51. proofbundle-3.2.2/tests/test_signature.py +0 -36
  52. {proofbundle-3.2.2 → proofbundle-3.2.3}/LICENSE +0 -0
  53. {proofbundle-3.2.2 → proofbundle-3.2.3}/setup.cfg +0 -0
  54. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/_inspect_registry.py +0 -0
  55. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/_integration.py +0 -0
  56. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/__init__.py +0 -0
  57. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/eee.py +0 -0
  58. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/inspect_ai.py +0 -0
  59. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/lm_eval.py +0 -0
  60. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/promptfoo.py +0 -0
  61. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/adapters/samples.py +0 -0
  62. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors.py +0 -0
  63. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_chia.py +0 -0
  64. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_chia_add.py +0 -0
  65. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_ots.py +0 -0
  66. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/anchors_rfc3161.py +0 -0
  67. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/beacon.py +0 -0
  68. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/canonical.py +0 -0
  69. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/checkpoint.py +0 -0
  70. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/demo.py +0 -0
  71. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/eee_eval_schema.json +0 -0
  72. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/emit.py +0 -0
  73. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/errors.py +0 -0
  74. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/evidence_pack.py +0 -0
  75. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/experimental/__init__.py +0 -0
  76. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/experimental/enclave.py +0 -0
  77. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/hashalg.py +0 -0
  78. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/hf_evals.py +0 -0
  79. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/inspect_hook.py +0 -0
  80. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/intoto.py +0 -0
  81. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/kbjwt.py +0 -0
  82. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/merkle.py +0 -0
  83. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/persample.py +0 -0
  84. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/decision-receipt-template-v1.json +0 -0
  85. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/research-preview-v1.json +0 -0
  86. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +0 -0
  87. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/strict-eval-template-v1.json +0 -0
  88. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policies/strict-prereg-template-v1.json +0 -0
  89. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/policy_profiles.py +0 -0
  90. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/pqsig.py +0 -0
  91. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/prereg.py +0 -0
  92. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/public_transparency.py +0 -0
  93. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/py.typed +0 -0
  94. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/pytest_plugin.py +0 -0
  95. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/sdjwt_issue.py +0 -0
  96. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/statuslist.py +0 -0
  97. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/subject_binding.py +0 -0
  98. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle/tlogproof.py +0 -0
  99. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  100. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/entry_points.txt +0 -0
  101. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/requires.txt +0 -0
  102. {proofbundle-3.2.2 → proofbundle-3.2.3}/src/proofbundle.egg-info/top_level.txt +0 -0
  103. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_adapters.py +0 -0
  104. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_adversarial.py +0 -0
  105. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchor_hardening_321.py +0 -0
  106. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchor_longevity_property.py +0 -0
  107. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchor_target_trustedtime.py +0 -0
  108. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_chia.py +0 -0
  109. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_chia_add.py +0 -0
  110. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_chia_claims.py +0 -0
  111. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_generic.py +0 -0
  112. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_markovian.py +0 -0
  113. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_ots.py +0 -0
  114. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_anchors_rfc3161.py +0 -0
  115. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_beacon.py +0 -0
  116. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_branch_base_check.py +0 -0
  117. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_bundle_robustness.py +0 -0
  118. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_canonical.py +0 -0
  119. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_checkpoint.py +0 -0
  120. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_checkpoint_external_vectors.py +0 -0
  121. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_checkpoint_quorum_property.py +0 -0
  122. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_claims_hygiene.py +0 -0
  123. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cli.py +0 -0
  124. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cli_eval.py +0 -0
  125. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cli_require_anchor.py +0 -0
  126. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_conformance.py +0 -0
  127. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_content_root_property.py +0 -0
  128. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cosignature.py +0 -0
  129. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_cosignature_mldsa.py +0 -0
  130. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_anchors.py +0 -0
  131. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_cli.py +0 -0
  132. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_evidence.py +0 -0
  133. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_fuzz.py +0 -0
  134. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_hardening.py +0 -0
  135. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_policy.py +0 -0
  136. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_schema.py +0 -0
  137. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_validator_api.py +0 -0
  138. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_decision_verify.py +0 -0
  139. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_demo.py +0 -0
  140. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_docs_truth.py +0 -0
  141. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_dsse_adversarial.py +0 -0
  142. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_dup_key_reject.py +0 -0
  143. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_ed25519_semantics.py +0 -0
  144. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_eee.py +0 -0
  145. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_emit.py +0 -0
  146. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_eval_evidence_class.py +0 -0
  147. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_evidence_pack.py +0 -0
  148. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_examples.py +0 -0
  149. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_experimental_enclave.py +0 -0
  150. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_fork_pr_secret_isolation.py +0 -0
  151. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_fuzz_parsers.py +0 -0
  152. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_hashalg.py +0 -0
  153. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_hf_entry_verify_eee_digest.py +0 -0
  154. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_hf_evals.py +0 -0
  155. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_inspect_hook.py +0 -0
  156. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto.py +0 -0
  157. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_claims_hygiene.py +0 -0
  158. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_content_root_migration.py +0 -0
  159. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_dsse.py +0 -0
  160. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_eval_result.py +0 -0
  161. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_examples.py +0 -0
  162. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_spec_diff.py +0 -0
  163. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_intoto_svr.py +0 -0
  164. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_kbjwt.py +0 -0
  165. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_lens_review_fixes_3_1_3.py +0 -0
  166. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_merkle.py +0 -0
  167. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_merkle_consistency_property.py +0 -0
  168. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_merkle_property.py +0 -0
  169. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_mldsa_acvp_vectors.py +0 -0
  170. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_official_vectors.py +0 -0
  171. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_ok_semantics.py +0 -0
  172. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_ots_external_vectors.py +0 -0
  173. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_oversized_int_guard.py +0 -0
  174. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_persample.py +0 -0
  175. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_pinned_key_validation.py +0 -0
  176. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_policy_explain_lint.py +0 -0
  177. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_policy_lifecycle_purpose.py +0 -0
  178. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_policy_profiles.py +0 -0
  179. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_policy_templates.py +0 -0
  180. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_pqsig.py +0 -0
  181. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_predicate_type_enforcement.py +0 -0
  182. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_prereg.py +0 -0
  183. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_promptfoo.py +0 -0
  184. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_public_transparency.py +0 -0
  185. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_pytest_plugin.py +0 -0
  186. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_rekor_interop.py +0 -0
  187. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_release_integrity_gate.py +0 -0
  188. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_renewal.py +0 -0
  189. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_renewal_policy.py +0 -0
  190. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_renewal_signed.py +0 -0
  191. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_rfc6962_external_vectors.py +0 -0
  192. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_root_authenticity.py +0 -0
  193. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_run_ledger.py +0 -0
  194. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_schema.py +0 -0
  195. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_schema_parity.py +0 -0
  196. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_adversarial.py +0 -0
  197. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_duplicate_cnf.py +0 -0
  198. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_issue.py +0 -0
  199. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_reference.py +0 -0
  200. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_resource_budget.py +0 -0
  201. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_vc.py +0 -0
  202. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_sdjwt_verify_binding.py +0 -0
  203. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_statuslist.py +0 -0
  204. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_subject_binding.py +0 -0
  205. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_tlogproof.py +0 -0
  206. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_tree_context_authenticity.py +0 -0
  207. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_trust_pack_crypto_agility.py +0 -0
  208. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_verification_summary.py +0 -0
  209. {proofbundle-3.2.2 → proofbundle-3.2.3}/tests/test_verify_matrix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proofbundle
3
- Version: 3.2.2
3
+ Version: 3.2.3
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
@@ -205,7 +205,7 @@ a preprint.
205
205
  | **K-Veritas** ([arXiv 2605.08586](https://arxiv.org/abs/2605.08586)) — nonrepudiable experimental results | the academic case for tamper-evident, execution-bound experiment reports | preprint | proofbundle is a released, offline, eval-shaped receipt for exactly this problem, not the only take on it |
206
206
  | **Attestable Audits** ([arXiv 2506.23706](https://arxiv.org/abs/2506.23706)) — TEE-verified safety audits | that the computation actually ran, inside a trusted enclave | preprint (research prototype) | a receipt proves authorship + integrity, **not** that the computation was correct — that needs a TEE or independent reproduction |
207
207
  | **BenchJack** ([arXiv 2605.12673](https://arxiv.org/abs/2605.12673)) — auditing agent benchmarks | whether the benchmark itself is gameable (reward-hacking) | preprint | a receipt over a gameable benchmark is honestly still just a receipt; it says nothing about whether the eval was well designed |
208
- | **Evaluation Cards** ([arXiv 2606.09809](https://arxiv.org/abs/2606.09809)) — reporting / interpretation layer | a structured, human-facing account of what a result means | preprint | a receipt can bind a card's integrity, not its quality |
208
+ | **Evaluation Cards** ([arXiv 2606.09809](https://arxiv.org/abs/2606.09809)) — reporting / interpretation layer | a structured, human-facing account of what a result means | preprint | a receipt can bind a card's integrity (`evaluation_card_sha256`, EVAL_CLAIM.md), not its quality |
209
209
  | in-toto / Sigstore, SCITT / Rekor v2, OpenSSF Model Signing (stable standards / production) | artifact-provenance, public transparency, model-artifact signing | — | see [INTEROP.md](https://github.com/b7n0de/proofbundle/blob/main/INTEROP.md) for the honest tool-by-tool comparison |
210
210
 
211
211
  ## Cite this work
@@ -341,12 +341,17 @@ is anchored to external RFC 6962 vectors and a real Rekor proof, not just its ow
341
341
  by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
342
342
  [SECURITY.md](https://github.com/b7n0de/proofbundle/blob/main/SECURITY.md).
343
343
 
344
- **Roadmap (stated honestly, all forward-looking):**
344
+ **Roadmap (stated honestly, not yet built):**
345
345
 
346
346
  - A post-quantum *payload* signature (crypto-agility for the receipt itself) is on the roadmap; today the
347
347
  post-quantum coverage is witness-side ML-DSA-44 cosignatures only.
348
348
  - A CLI flag to select the content-root algorithm is still deferred (`jcs-sha256-v1` is the signed default).
349
- - **Preview:** a TEE-attestation bridge — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
349
+
350
+ **Already shipped, preview/experimental maturity (not on the roadmap above — built, tested, install
351
+ extra `[experimental]`, but API/wire-format may still change without deprecation):**
352
+
353
+ - A TEE-attestation bridge (RATS/EAT, RFC 9334 + RFC 9711) that makes `assurance_level =
354
+ enclave_attested` independently verifiable — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
350
355
 
351
356
  ## Contributing
352
357
 
@@ -151,7 +151,7 @@ a preprint.
151
151
  | **K-Veritas** ([arXiv 2605.08586](https://arxiv.org/abs/2605.08586)) — nonrepudiable experimental results | the academic case for tamper-evident, execution-bound experiment reports | preprint | proofbundle is a released, offline, eval-shaped receipt for exactly this problem, not the only take on it |
152
152
  | **Attestable Audits** ([arXiv 2506.23706](https://arxiv.org/abs/2506.23706)) — TEE-verified safety audits | that the computation actually ran, inside a trusted enclave | preprint (research prototype) | a receipt proves authorship + integrity, **not** that the computation was correct — that needs a TEE or independent reproduction |
153
153
  | **BenchJack** ([arXiv 2605.12673](https://arxiv.org/abs/2605.12673)) — auditing agent benchmarks | whether the benchmark itself is gameable (reward-hacking) | preprint | a receipt over a gameable benchmark is honestly still just a receipt; it says nothing about whether the eval was well designed |
154
- | **Evaluation Cards** ([arXiv 2606.09809](https://arxiv.org/abs/2606.09809)) — reporting / interpretation layer | a structured, human-facing account of what a result means | preprint | a receipt can bind a card's integrity, not its quality |
154
+ | **Evaluation Cards** ([arXiv 2606.09809](https://arxiv.org/abs/2606.09809)) — reporting / interpretation layer | a structured, human-facing account of what a result means | preprint | a receipt can bind a card's integrity (`evaluation_card_sha256`, EVAL_CLAIM.md), not its quality |
155
155
  | in-toto / Sigstore, SCITT / Rekor v2, OpenSSF Model Signing (stable standards / production) | artifact-provenance, public transparency, model-artifact signing | — | see [INTEROP.md](https://github.com/b7n0de/proofbundle/blob/main/INTEROP.md) for the honest tool-by-tool comparison |
156
156
 
157
157
  ## Cite this work
@@ -287,12 +287,17 @@ is anchored to external RFC 6962 vectors and a real Rekor proof, not just its ow
287
287
  by itself — it is the small, offline, standards-native receipt layer between them. Security policy:
288
288
  [SECURITY.md](https://github.com/b7n0de/proofbundle/blob/main/SECURITY.md).
289
289
 
290
- **Roadmap (stated honestly, all forward-looking):**
290
+ **Roadmap (stated honestly, not yet built):**
291
291
 
292
292
  - A post-quantum *payload* signature (crypto-agility for the receipt itself) is on the roadmap; today the
293
293
  post-quantum coverage is witness-side ML-DSA-44 cosignatures only.
294
294
  - A CLI flag to select the content-root algorithm is still deferred (`jcs-sha256-v1` is the signed default).
295
- - **Preview:** a TEE-attestation bridge — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
295
+
296
+ **Already shipped, preview/experimental maturity (not on the roadmap above — built, tested, install
297
+ extra `[experimental]`, but API/wire-format may still change without deprecation):**
298
+
299
+ - A TEE-attestation bridge (RATS/EAT, RFC 9334 + RFC 9711) that makes `assurance_level =
300
+ enclave_attested` independently verifiable — see [docs/EXPERIMENTAL_ENCLAVE.md](https://github.com/b7n0de/proofbundle/blob/main/docs/EXPERIMENTAL_ENCLAVE.md).
296
301
 
297
302
  ## Contributing
298
303
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofbundle"
7
- version = "3.2.2"
7
+ version = "3.2.3"
8
8
  description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- __version__ = "3.2.2"
16
+ __version__ = "3.2.3"
17
17
 
18
18
  # The `proofbundle/v0.1` normative spec revision this build implements — kept in sync with the
19
19
  # `Revision:` line at the top of SPEC.md by tests/test_docs_truth.py (WP-B1, closes #28). Bump
@@ -44,6 +44,8 @@ __all__ = [
44
44
  "audit_challenge",
45
45
  "prereg_hash",
46
46
  "verify_prereg",
47
+ "evaluation_card_hash",
48
+ "verify_evaluation_card",
47
49
  "beacon_audit_challenge",
48
50
  "canonicalize_statement",
49
51
  "statement_content_root",
@@ -64,6 +66,15 @@ __all__ = [
64
66
  "verify_mldsa",
65
67
  "verify_slhdsa",
66
68
  "verify_hybrid",
69
+ "EvidenceLevel",
70
+ "classify_digest_evidence",
71
+ "classify_receiver_corroboration",
72
+ "evidence_ladder_summary",
73
+ "evidence_ladder_best",
74
+ "VerificationBudget",
75
+ "DEFAULT_BUDGET",
76
+ "BudgetExceeded",
77
+ "automation_summary",
67
78
  "VerificationResult",
68
79
  "Check",
69
80
  "ProofBundleError",
@@ -89,6 +100,8 @@ _LAZY = {
89
100
  "audit_challenge": ".persample",
90
101
  "prereg_hash": ".prereg",
91
102
  "verify_prereg": ".prereg",
103
+ "evaluation_card_hash": ".evalcard",
104
+ "verify_evaluation_card": ".evalcard",
92
105
  "beacon_audit_challenge": ".beacon",
93
106
  "canonicalize_statement": ".canonical",
94
107
  "statement_content_root": ".canonical",
@@ -109,6 +122,11 @@ _LAZY = {
109
122
  "verify_mldsa": ".pqsig",
110
123
  "verify_slhdsa": ".pqsig",
111
124
  "verify_hybrid": ".pqsig",
125
+ "EvidenceLevel": ".assurance", "classify_digest_evidence": ".assurance",
126
+ "classify_receiver_corroboration": ".assurance", "evidence_ladder_summary": ".assurance",
127
+ "evidence_ladder_best": ".assurance",
128
+ "VerificationBudget": ".budget", "DEFAULT_BUDGET": ".budget", "BudgetExceeded": ".budget",
129
+ "automation_summary": ".automation_verdict",
112
130
  }
113
131
 
114
132
  if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime stays lazy
@@ -129,7 +147,12 @@ if TYPE_CHECKING: # static analysers + IDEs see the real names/types; runtime s
129
147
  evaluate_renewal_policy, last_ats, renew_hashtree, renew_timestamp,
130
148
  verify_sequence)
131
149
  from .pqsig import verify_hybrid, verify_mldsa, verify_slhdsa
150
+ from .assurance import (EvidenceLevel, classify_digest_evidence, classify_receiver_corroboration,
151
+ evidence_ladder_best, evidence_ladder_summary)
152
+ from .budget import DEFAULT_BUDGET, BudgetExceeded, VerificationBudget
153
+ from .automation_verdict import automation_summary
132
154
  from .prereg import prereg_hash, verify_prereg
155
+ from .evalcard import evaluation_card_hash, verify_evaluation_card
133
156
  from .statuslist import verify_status_snapshot
134
157
  from .tlogproof import verify_tlog_proof
135
158
  from .merkle import verify_consistency, verify_inclusion
@@ -49,7 +49,30 @@ def _reject_duplicate_keys(pairs: list) -> dict:
49
49
  return obj
50
50
 
51
51
 
52
- def loads_strict(text: Union[str, bytes]) -> Any:
52
+ def _enforce_node_budget(obj: Any, json_nodes: int) -> None:
53
+ """Bounded iterative walk (crypto-review 2026-07-15): refuse a PARSED structure whose combined
54
+ dict-key + list-item count exceeds ``json_nodes`` — a wide-but-small-bytes document that slips under the
55
+ raw ``input_bytes`` cap. Aborts the moment the ceiling is passed, so it never walks the whole of an
56
+ over-budget document; the raw cap already bounds this walk's own worst case. Raises
57
+ :class:`proofbundle.budget.BudgetExceeded` (a ``ProofBundleError`` subclass), fail-closed."""
58
+ from .budget import BudgetExceeded # noqa: PLC0415 - local import avoids an import cycle
59
+ count = 0
60
+ stack = [obj]
61
+ while stack:
62
+ cur = stack.pop()
63
+ if isinstance(cur, dict):
64
+ count += len(cur)
65
+ if count > json_nodes:
66
+ raise BudgetExceeded("json_nodes", count, json_nodes)
67
+ stack.extend(cur.values())
68
+ elif isinstance(cur, list):
69
+ count += len(cur)
70
+ if count > json_nodes:
71
+ raise BudgetExceeded("json_nodes", count, json_nodes)
72
+ stack.extend(cur)
73
+
74
+
75
+ def loads_strict(text: Union[str, bytes], *, budget: Any = None) -> Any:
53
76
  """``json.loads`` that rejects duplicate object keys at any nesting depth.
54
77
 
55
78
  Raises :class:`BundleFormatError` for a duplicate key (fail-closed, clear message), maps
@@ -58,9 +81,24 @@ def loads_strict(text: Union[str, bytes]) -> Any:
58
81
  with more than ``sys.get_int_max_str_digits()`` digits (CWE-674 / CVE-2020-10735) to it too —
59
82
  never a raw traceback — mirroring :func:`proofbundle.bundle.load_bundle`. Ordinary JSON syntax
60
83
  errors keep raising ``ValueError`` (``json.JSONDecodeError``) so existing ``except (ValueError,
61
- ...)`` handling at the call sites stays correct."""
84
+ ...)`` handling at the call sites stays correct.
85
+
86
+ DoS backstop (Finding 15b, crypto-review 2026-07-15): this is the ONE parse chokepoint every verify
87
+ path funnels through, so the resource caps live here rather than at each of ~10 call sites. The raw
88
+ ``text`` is refused BEFORE parsing when it exceeds ``budget.input_bytes`` (``json.loads`` cost scales
89
+ with input size, so an unbounded parse of a 50 MB envelope is a real pre-loop DoS the downstream
90
+ signature/list caps cannot reach), and the PARSED structure is refused when its combined dict-key +
91
+ list-item count exceeds ``budget.json_nodes``. Both raise :class:`proofbundle.budget.BudgetExceeded`
92
+ (a ``ProofBundleError`` subclass, so existing ``except (ProofBundleError, ...)`` sites treat it as
93
+ fail-closed malformed/over-limit input). ``budget`` defaults to ``DEFAULT_BUDGET``; pass a tighter one
94
+ to test the guard."""
95
+ from .budget import DEFAULT_BUDGET # noqa: PLC0415 - local import avoids an import cycle
96
+ b = budget if budget is not None else DEFAULT_BUDGET
97
+ if len(text) > b.input_bytes:
98
+ from .budget import BudgetExceeded # noqa: PLC0415
99
+ raise BudgetExceeded("input_bytes", len(text), b.input_bytes)
62
100
  try:
63
- return json.loads(text, object_pairs_hook=_reject_duplicate_keys)
101
+ obj = json.loads(text, object_pairs_hook=_reject_duplicate_keys)
64
102
  except RecursionError as exc:
65
103
  raise BundleFormatError("JSON nesting is too deep") from exc
66
104
  except ValueError as exc:
@@ -70,3 +108,5 @@ def loads_strict(text: Union[str, bytes]) -> Any:
70
108
  if "integer string conversion" in str(exc):
71
109
  raise BundleFormatError("JSON integer literal is implausibly long (fail-closed)") from exc
72
110
  raise
111
+ _enforce_node_budget(obj, b.json_nodes)
112
+ return obj
@@ -14,6 +14,17 @@ Design notes (verified against framework source, 2026-07):
14
14
  - The hash is over the config's JSON, prefixed with a domain tag, hex sha256. It is provenance
15
15
  metadata (traceability), NOT a security commitment — it is not salted and reveals structure;
16
16
  it exists so two receipts from the same config are linkable and a changed config is visible.
17
+
18
+ **Benchmark-hacking VISIBILITY (additive).** `run_attempts`/`aborted_runs`/`methodology_sha256`/
19
+ `benchjack_audit_report_sha256` make retry/best-of-many patterns and the methodology behind a
20
+ result *visible* in the signed claim. They are honest metadata, NEVER a cryptographic guarantee
21
+ against a gamed benchmark — `eval_evidence_class` already separates `methodology` from
22
+ `score_evidence` (a receipt never judges whether the suite is well designed), and THREAT_MODEL.md
23
+ states the same boundary for benchmark-hacking explicitly (BenchJack, arXiv:2605.12673): crypto
24
+ cannot prove benchmark truth, only that these numbers are what was signed. The two digest fields
25
+ are plain sha256 references an auditor re-hashes by hand — same mechanism, same epistemic
26
+ strength as `prereg_sha256`/`evaluation_card_sha256` (a match proves only "this is the document
27
+ the issuer pointed at", never that the document is honest or complete).
17
28
  """
18
29
 
19
30
  from __future__ import annotations
@@ -48,11 +59,19 @@ def config_hash(config) -> Optional[str]:
48
59
 
49
60
 
50
61
  def add_provenance(provenance: dict, *, run_id=None, config=None, log_timestamp=None,
51
- config_hash_value: Optional[str] = None) -> dict:
62
+ config_hash_value: Optional[str] = None,
63
+ run_attempts: Optional[int] = None, aborted_runs: Optional[int] = None,
64
+ methodology_sha256: Optional[str] = None,
65
+ benchjack_audit_report_sha256: Optional[str] = None) -> dict:
52
66
  """Merge the standard traceability fields into a provenance dict, skipping absent ones.
53
67
 
54
68
  ``config_hash_value`` lets a caller pass a precomputed hash (e.g. over already-canonical
55
- material) instead of a config object; otherwise ``config`` is hashed here."""
69
+ material) instead of a config object; otherwise ``config`` is hashed here.
70
+
71
+ ``run_attempts``/``aborted_runs`` (non-negative integers) and ``methodology_sha256``/
72
+ ``benchjack_audit_report_sha256`` (plain sha256 hex references) are additive VISIBILITY-only
73
+ fields for benchmark-hacking transparency — see the module docstring; they carry no
74
+ verification semantics here (no gate calls this a "guarantee")."""
56
75
  if run_id:
57
76
  provenance["run_id"] = str(run_id)
58
77
  if log_timestamp is not None:
@@ -60,4 +79,13 @@ def add_provenance(provenance: dict, *, run_id=None, config=None, log_timestamp=
60
79
  ch = config_hash_value if config_hash_value is not None else config_hash(config)
61
80
  if ch:
62
81
  provenance["config_hash"] = ch
82
+ for name, value in (("run_attempts", run_attempts), ("aborted_runs", aborted_runs)):
83
+ if value is not None:
84
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
85
+ raise ValueError(f"{name} must be a non-negative integer, got {value!r}")
86
+ provenance[name] = value
87
+ if methodology_sha256 is not None:
88
+ provenance["methodology_sha256"] = str(methodology_sha256)
89
+ if benchjack_audit_report_sha256 is not None:
90
+ provenance["benchjack_audit_report_sha256"] = str(benchjack_audit_report_sha256)
63
91
  return provenance
@@ -57,6 +57,12 @@ def verify_markovian(proof: bytes, canonical_root: bytes, *, frozen: dict,
57
57
  # which wallet/merkle_root was committed; BundleFormatError keeps the never-raise contract)
58
58
  try:
59
59
  from ._strict_json import loads_strict # noqa: PLC0415
60
+ from .budget import DEFAULT_BUDGET, BudgetExceeded # noqa: PLC0415
61
+ # DoS (crypto-review 2026-07-15): cap the raw proof BEFORE .decode(), mirroring anchors_chia's
62
+ # len(proof) > _MAX_PROOF_BYTES guard (loads_strict caps the decoded str, but the decode itself is
63
+ # otherwise unbounded). BudgetExceeded is caught below and reported as a clean _fail, never raised.
64
+ if len(proof) > DEFAULT_BUDGET.input_bytes:
65
+ raise BudgetExceeded("input_bytes", len(proof), DEFAULT_BUDGET.input_bytes)
60
66
  env = loads_strict(proof.decode("utf-8"))
61
67
  if not isinstance(env, dict):
62
68
  raise ValueError("envelope is not a JSON object")
@@ -0,0 +1,201 @@
1
+ """EvidenceLevel — a uniform, orderable strength ladder over the digest-presence-only 'proven'/'bound'
2
+ verify-time checks (2026-07 verify-layer hardening, Finding 03, additive/non-breaking).
3
+
4
+ WURZEL: ``action_outcome_proven`` (decision.py), ``outcome_execution_proven`` (outcome.py), and the
5
+ ``evidence_bound`` shape check in decision.py all stop at "does a syntactically valid sha256 digest
6
+ OBJECT exist at this field" (``_is_digest``) — a 64-hex string of AN ATTACKER'S CHOOSING satisfies it as
7
+ readily as the digest of the real referenced artifact; none of them checks the digest against actual
8
+ resolved bytes. ``decision.resolve_evidence_ref`` already exists to go further (it checks a digest against
9
+ ACTUAL bytes an offline caller supplies), but no ``verify_*`` path ever calls it — the deeper evidence
10
+ primitive is built, never wired.
11
+
12
+ This module makes the STRENGTH of a 'proven'/'bound' claim explicit and orderable, WITHOUT changing the
13
+ existing boolean ``*_proven``/``evidence_bound`` fields (they stay, unchanged, for backward compatibility)
14
+ — each ``verify_*`` function gains ADDITIVE, more precise field(s) that classify a claim onto this ladder.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import enum
19
+ import re
20
+ from typing import Any, Callable, Optional
21
+
22
+ __all__ = [
23
+ "EvidenceLevel", "EVIDENCE_LEVEL_NAMES", "classify_digest_evidence",
24
+ "classify_receiver_corroboration",
25
+ "evidence_ladder_summary", "evidence_ladder_best", "EFFECT_OBSERVED_NOT_IMPLEMENTED",
26
+ ]
27
+
28
+ _SHA256_HEX = re.compile(r"^[0-9a-f]{64}$")
29
+
30
+
31
+ class EvidenceLevel(enum.IntEnum):
32
+ """Ordered ladder from a bare claim to full effect observation (No-Overclaim, Finding 03). Higher is
33
+ strictly stronger; compare/sort with the plain ``int``/``IntEnum`` ordering."""
34
+
35
+ CLAIMED = 0 # a value is asserted but not even shaped as a digest
36
+ REFERENCE_WELL_FORMED = 1 # a syntactically valid sha256 digest OBJECT is present (the old
37
+ # *_proven==True / evidence_bound==True bar — attacker-choosable content)
38
+ CONTENT_RESOLVED = 2 # the digest was checked against ACTUALLY RESOLVED bytes
39
+ # (mirrors decision.resolve_evidence_ref's content_root_ok)
40
+ RECEIPT_CRYPTO_VERIFIED = 3 # the resolved content is ITSELF a cryptographically verified receipt
41
+ POLICY_AUTHORIZED = 4 # a trust policy additionally authorizes the claim (signer/role pinned)
42
+ INDEPENDENTLY_ATTESTED = 5 # a THIRD PARTY (not the original claimant) attests the same content
43
+ EFFECT_OBSERVED = 6 # the real-world EFFECT itself was observed, not merely a receipt about it
44
+
45
+
46
+ EVIDENCE_LEVEL_NAMES: tuple[str, ...] = tuple(level.name for level in EvidenceLevel)
47
+
48
+ # EFFECT_OBSERVED is structurally UNREACHABLE from this module alone (Finding 16 — real-world effect
49
+ # observation, e.g. a monitored side channel confirming the outcome actually happened in the world, not
50
+ # merely that a receipt about it was signed/resolved). No verify_* path in this repo can compute it today.
51
+ # Making that explicit here — rather than silently never emitting it — is itself the honest No-Fake point;
52
+ # a caller grepping for "EFFECT_OBSERVED" finds this marker, not silence.
53
+ #
54
+ # Finding 16 UPDATE (receiver/observer corroboration, self-fixable part): the SELF-FIXABLE portion of
55
+ # Finding 16 IS now built — outcome.py's optional `receiverRefs` + `classify_receiver_corroboration` below
56
+ # make INDEPENDENTLY_ATTESTED (level 5, "a THIRD PARTY attests the same content") reachable when a
57
+ # receiver/observer's own signed acknowledgement is resolved and verified as coming from a party distinct
58
+ # from the executor. EFFECT_OBSERVED (level 6) stays UNREACHABLE even then — a signed receiver receipt is
59
+ # still a RECEIPT ABOUT the effect, never a live-monitored observation of the real-world effect itself; that
60
+ # is Finding 16's honestly-documented INHERENT limit (proofbundle cannot itself make a third-party system
61
+ # sign anything — real-world side-channel monitoring is ecosystem adoption outside this repo).
62
+ EFFECT_OBSERVED_NOT_IMPLEMENTED = (
63
+ "EvidenceLevel.EFFECT_OBSERVED is not reachable by any verify_* path in this repo (Finding 16's "
64
+ "self-fixable receiver-corroboration part now reaches INDEPENDENTLY_ATTESTED; EFFECT_OBSERVED itself "
65
+ "still needs a real-world effect-observation channel, which is an inherent, not-yet-built limit outside "
66
+ "proofbundle's own control) — TODO, tracked, not silently absent."
67
+ )
68
+
69
+
70
+ def _is_digest(obj: Any) -> bool:
71
+ return isinstance(obj, dict) and isinstance(obj.get("sha256"), str) and bool(_SHA256_HEX.match(obj["sha256"]))
72
+
73
+
74
+ def classify_digest_evidence(digest_obj: Any, *, applicable: bool = True,
75
+ evidence_resolver: Optional[Callable[[Any], bool]] = None) -> dict:
76
+ """Classify ONE digest-bound field (e.g. an ``effectDigest``, a ``decisionRef``, one
77
+ ``evidenceRefs[]`` entry) onto the :class:`EvidenceLevel` ladder. Never raises; a malformed input
78
+ classifies as ``CLAIMED``, it never crashes the caller.
79
+
80
+ ``applicable=False`` (e.g. ``status != 'executed'``) -> ``level=None`` (not applicable, mirrors the
81
+ existing ``*_proven=None`` convention: a non-applicable claim is not a WEAK claim, it is not a claim
82
+ at all).
83
+
84
+ ``evidence_resolver``, when supplied, is called with ``digest_obj`` and must return True iff the
85
+ digest was checked against the ACTUAL resolved bytes (mirrors ``resolve_evidence_ref``'s
86
+ ``content_root_ok``); on True the level reaches ``CONTENT_RESOLVED``, never higher —
87
+ ``RECEIPT_CRYPTO_VERIFIED``/``POLICY_AUTHORIZED``/``INDEPENDENTLY_ATTESTED`` are each a STRONGER claim
88
+ this classifier does not itself verify (conflating "checked against real bytes" with "the real bytes'
89
+ OWN signature was checked" would be exactly the kind of unearned strength bump No-Overclaim forbids).
90
+ A raising/exception-throwing ``evidence_resolver`` is treated as False (fail-closed: an exception is
91
+ not evidence, never silently promoted).
92
+ """
93
+ if not applicable:
94
+ return {"level": None, "level_name": None, "detail": "not applicable"}
95
+ if not _is_digest(digest_obj):
96
+ return {"level": EvidenceLevel.CLAIMED, "level_name": EvidenceLevel.CLAIMED.name,
97
+ "detail": "no well-formed sha256 digest object present"}
98
+ level = EvidenceLevel.REFERENCE_WELL_FORMED
99
+ detail = "a well-formed sha256 digest object is present (attacker-choosable content, not content-checked)"
100
+ if evidence_resolver is not None:
101
+ try:
102
+ resolved = bool(evidence_resolver(digest_obj))
103
+ except Exception: # noqa: BLE001 - fail-closed: a raising resolver proves nothing
104
+ resolved = False
105
+ if resolved:
106
+ level = EvidenceLevel.CONTENT_RESOLVED
107
+ detail = "digest checked against actually-resolved content bytes"
108
+ return {"level": level, "level_name": level.name, "detail": detail}
109
+
110
+
111
+ def classify_receiver_corroboration(digest_obj: Any, *, applicable: bool = True,
112
+ evidence_resolver: Optional[Callable[[Any], bool]] = None,
113
+ independent_attestation_resolver: Optional[Callable[[Any], bool]] = None,
114
+ executor_key_id: Optional[str] = None,
115
+ receiver_key_id: Optional[str] = None,
116
+ ) -> dict:
117
+ """Classify a receiver/observer corroboration ref (Finding 16, additive) ONE STEP BEYOND
118
+ :func:`classify_digest_evidence` — reaches ``EvidenceLevel.INDEPENDENTLY_ATTESTED`` when
119
+ ``independent_attestation_resolver`` confirms the referenced content is ITSELF a validly-signed
120
+ statement from a party DISTINCT from the original claimant (e.g. a receiver's or observer's own
121
+ DSSE-signed acknowledgement of an Action Outcome) — never merely a resolved digest, which is
122
+ :func:`classify_digest_evidence`'s own documented ceiling (its docstring: "RECEIPT_CRYPTO_VERIFIED /
123
+ POLICY_AUTHORIZED / INDEPENDENTLY_ATTESTED are each a STRONGER claim this classifier does not itself
124
+ verify").
125
+
126
+ The three-tier informal ladder a caller might reach for here — SELF_ASSERTED / DIGEST_REFERENCED /
127
+ RECEIVER_CORROBORATED — maps onto this module's EXISTING orderable :class:`EvidenceLevel` rather than
128
+ a new competing enum (CLAIMED/REFERENCE_WELL_FORMED ≈ SELF_ASSERTED/DIGEST_REFERENCED,
129
+ INDEPENDENTLY_ATTESTED ≈ RECEIVER_CORROBORATED — "a THIRD PARTY attests the same content" is exactly
130
+ what a receiver/observer corroboration IS).
131
+
132
+ Never raises: a raising ``independent_attestation_resolver`` is fail-closed (treated as False, the base
133
+ ``classify_digest_evidence`` level is kept — never silently promoted, mirrors the existing
134
+ ``evidence_resolver`` contract). The resolver is only ever consulted once the digest has ALREADY reached
135
+ at least ``CONTENT_RESOLVED`` — an attacker-choosable digest that was never resolved cannot be promoted
136
+ straight to INDEPENDENTLY_ATTESTED by a permissive attestation resolver alone.
137
+
138
+ STRUCTURAL independence (crypto-review, 2026-07-15): "INDEPENDENTLY_ATTESTED" means the corroborating
139
+ statement is from a party DISTINCT from the executor/claimant. proofbundle asserts this only when it can
140
+ PROVE it: a receiver reaches INDEPENDENTLY_ATTESTED ONLY IF BOTH ``executor_key_id`` AND
141
+ ``receiver_key_id`` are present AND they differ. An ABSENT ``executor_key_id`` blocks promotion just as
142
+ an absent/equal receiver key id does — the executor authors and signs its own outcome predicate and
143
+ ``executor.keyId`` is schema-optional, so a one-sided check (fire only when executor_key_id is supplied)
144
+ would be trivially evaded by simply omitting one's own keyId. Without knowing BOTH parties' key ids
145
+ proofbundle cannot show they differ, so it does not claim independence (fail-closed to the base level).
146
+
147
+ INHERENT limit (honestly not closed here): two DISTINCT key ids can still belong to the SAME real-world
148
+ principal (an executor using a second key it also controls). proofbundle cannot bind a key id to a
149
+ real-world identity on its own — that is exactly what the ``outcomeReceivers`` Trust Pack role provides
150
+ (``outcome.receiver_trusted_by_role``: a curated list of trusted, genuinely-independent receiver keys).
151
+ So key-id distinctness here is the STRUCTURAL floor; principal-level independence needs that out-of-band
152
+ trust binding."""
153
+ base = classify_digest_evidence(digest_obj, applicable=applicable, evidence_resolver=evidence_resolver)
154
+ if base["level"] is None or base["level"] < EvidenceLevel.CONTENT_RESOLVED or independent_attestation_resolver is None:
155
+ return base
156
+ # Provable distinctness: to ASSERT independence, BOTH key ids must be present, be STRINGS, AND differ.
157
+ # The isinstance(str) guards close a type-confusion evasion (crypto-review 2026-07-15): a non-str
158
+ # receiver_key_id (e.g. ["kid-exec"]) is `!= "kid-exec"` in Python, so a bare `==` distinctness check
159
+ # would read a wrapped copy of the executor's OWN id as "distinct". An absent/non-str/equal key id is
160
+ # self-corroboration that cannot be shown independent -> fail-closed, no promotion.
161
+ if not isinstance(executor_key_id, str) or not isinstance(receiver_key_id, str) or receiver_key_id == executor_key_id:
162
+ return {**base, "detail": base["detail"] + " (independence not provable: executor and receiver key "
163
+ "ids must both be present and differ; an absent/equal key id is self-corroboration — "
164
+ "principal-level independence for two distinct keys needs the outcomeReceivers trust role)"}
165
+ try:
166
+ attested = bool(independent_attestation_resolver(digest_obj))
167
+ except Exception: # noqa: BLE001 - fail-closed: a raising resolver proves nothing
168
+ attested = False
169
+ if not attested:
170
+ return base
171
+ return {"level": EvidenceLevel.INDEPENDENTLY_ATTESTED,
172
+ "level_name": EvidenceLevel.INDEPENDENTLY_ATTESTED.name,
173
+ "detail": "the referenced content is itself a validly-signed statement from a party distinct "
174
+ "from the original claimant (receiver/observer corroboration)"}
175
+
176
+
177
+ def evidence_ladder_summary(*fields: dict) -> dict:
178
+ """Roll several :func:`classify_digest_evidence` results into ONE summary using AND semantics: a chain
179
+ of evidence is only as strong as its WEAKEST applicable link (e.g. ``decision.py``'s
180
+ ``evidenceRefs[]`` — ``evidence_bound`` is only meaningful when EVERY ref is bound). Non-applicable
181
+ (``level=None``) fields are ignored, never silently counted as CLAIMED. When no field is applicable,
182
+ returns ``level=None`` (mirrors the existing ``evidence_bound=None`` "nothing to bind" convention —
183
+ never a vacuous strong verdict over an empty set)."""
184
+ applicable = [f for f in fields if f.get("level") is not None]
185
+ if not applicable:
186
+ return {"level": None, "level_name": None, "fields": list(fields)}
187
+ weakest = min(applicable, key=lambda f: f["level"])
188
+ return {"level": weakest["level"], "level_name": weakest["level_name"], "fields": list(fields)}
189
+
190
+
191
+ def evidence_ladder_best(*fields: dict) -> dict:
192
+ """Roll several :func:`classify_digest_evidence` results into ONE summary using OR semantics: only ONE
193
+ of several alternative digest fields needs to hold for the claim to be satisfied (e.g.
194
+ ``outcome.py``'s ``effectDigest`` OR ``actualActionDigest`` — the existing boolean
195
+ ``outcome_execution_proven`` is exactly this OR). Picks the STRONGEST applicable field. When no field
196
+ is applicable, returns ``level=None``."""
197
+ applicable = [f for f in fields if f.get("level") is not None]
198
+ if not applicable:
199
+ return {"level": None, "level_name": None, "fields": list(fields)}
200
+ strongest = max(applicable, key=lambda f: f["level"])
201
+ return {"level": strongest["level"], "level_name": strongest["level_name"], "fields": list(fields)}
@@ -0,0 +1,101 @@
1
+ """automation_summary — a uniform, additive automation-safety verdict layered on top of any proofbundle
2
+ ``verify_*`` result dict (2026-07 verify-layer hardening, Finding 01).
3
+
4
+ WURZEL: ``bundle.py::root_authenticity_summary`` already computes a ``safeForAutomation`` /
5
+ ``automationBlockers`` verdict for the core evidence bundle — it exists ONLY there. The other five
6
+ receipt-chain predicates (``decision.py``, ``outcome.py``, ``trust_pack.py``, ``verification_summary.py``,
7
+ ``run_ledger.py``) each compute their OWN aggregate ``ok`` using an ``is not False`` pattern over their
8
+ optional/not-applicable checks (documented, intentional: ``None`` = "not requested, passes"). That is the
9
+ RIGHT default for ``ok`` (a caller who never asked for a policy check should not be told the receipt is
10
+ somehow invalid) — but it is the WRONG bar for an AUTOMATION decision: a caller who filters on ``ok`` alone
11
+ can walk away believing a receipt was policy-authorized when ``policy_ok`` was actually ``None``
12
+ (never evaluated), because ``None is not False`` is ``True``.
13
+
14
+ ``automation_summary`` does NOT change any existing ``ok`` field (additive, no breaking default flip — see
15
+ CHANGELOG "Unreleased" for the ONE deliberately-deferred breaking piece, the ``bundle.py`` CLI exit-code
16
+ default). It computes a SEPARATE, stricter verdict: ``safeForAutomation`` is true only when policy IS
17
+ ``True`` (never merely "not False"), mirroring ``bundle.py``'s own ``policy_ok is True`` bar (P0-B, audit
18
+ 2026-07-13). Each of the five ``verify_*`` functions stashes this at ``result["automation"]`` — the old
19
+ ``result["ok"]`` field is untouched.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from typing import Any, Mapping, Optional, Sequence
24
+
25
+ __all__ = ["automation_summary", "AUTOMATION_BLOCKER_REASONS"]
26
+
27
+ # The human-legible reason for each automationBlockers enum value (mirrors bundle.py's
28
+ # AUTOMATION_BLOCKER_REASONS — kept here, next to the blocker logic, so the two can never drift apart).
29
+ AUTOMATION_BLOCKER_REASONS = {
30
+ "CRYPTO_NOT_OK": "The cryptographic (DSSE / threshold-signature) verdict is not true",
31
+ "STRUCTURE_NOT_OK": "The predicate structure did not fully validate",
32
+ "POLICY_NOT_EVALUATED": "No trust policy / authorization gate was evaluated for this predicate type",
33
+ "POLICY_FAILED": "The supplied trust policy / authorization gate was not satisfied",
34
+ "REFERENCES_NOT_RESOLVED": "One or more referenced/bound artifacts did not resolve (see the "
35
+ "predicate's own *_ok / *_bound / *_intact fields for which)",
36
+ }
37
+
38
+
39
+ def _tri(result: Mapping[str, Any], key: Optional[str]) -> Optional[bool]:
40
+ if key is None:
41
+ return None
42
+ value = result.get(key)
43
+ return None if value is None else bool(value)
44
+
45
+
46
+ def automation_summary(result: Mapping[str, Any], *, required_checks: Mapping[str, Any]) -> dict:
47
+ """Build a uniform automation-safety verdict from a ``verify_*`` result dict.
48
+
49
+ ``required_checks`` maps the four canonical automation dimensions to the ACTUAL field name(s) in
50
+ ``result`` that decide them for THIS predicate type (field names differ per ``verify_*`` function):
51
+
52
+ ``"crypto"`` -- str, the crypto/signature verdict field (e.g. ``"crypto_ok"``,
53
+ ``"root_threshold_met"``).
54
+ ``"structure"`` -- str, the structural verdict field (e.g. ``"structure_ok"``).
55
+ ``"policy"`` -- str or ``None``. When a str, the field is treated the SAME way
56
+ ``bundle.py::root_authenticity_summary`` treats ``policy_ok``: safe requires the
57
+ field to be ``True`` EXACTLY (``is True``), never merely ``is not False`` --
58
+ ``None`` (not evaluated) yields ``POLICY_NOT_EVALUATED``, never a silent pass.
59
+ When ``None``, this predicate type carries no policy/authorization layer at all
60
+ -- the policy dimension is reported ``None`` (not applicable) and never blocks
61
+ ``safeForAutomation``.
62
+ ``"references"`` -- a sequence of field names whose values, when EXPLICITLY ``False``, mean a
63
+ referenced/bound artifact did not resolve (e.g. ``decision_bound``,
64
+ ``evidence_bound``, ``chain_intact``). ``None`` entries (not applicable / not
65
+ requested) never block.
66
+
67
+ Returns ``{"cryptoValid", "structureValid", "policyAuthorized", "referencesResolved",
68
+ "safeForAutomation", "automationBlockers"}``. This function is PURE (no side effects on ``result``);
69
+ the caller is responsible for stashing the return value at ``result["automation"]``.
70
+ """
71
+ crypto_key = required_checks.get("crypto")
72
+ structure_key = required_checks.get("structure")
73
+ policy_key = required_checks.get("policy")
74
+ reference_keys: Sequence[str] = required_checks.get("references") or ()
75
+
76
+ crypto_ok = _tri(result, crypto_key)
77
+ structure_ok = _tri(result, structure_key)
78
+ policy_val = result.get(policy_key) if policy_key is not None else None
79
+ unresolved = [name for name in reference_keys if result.get(name) is False]
80
+
81
+ blockers: list[str] = []
82
+ if crypto_ok is not True:
83
+ blockers.append("CRYPTO_NOT_OK")
84
+ if structure_ok is not True:
85
+ blockers.append("STRUCTURE_NOT_OK")
86
+ if policy_key is not None:
87
+ if policy_val is None:
88
+ blockers.append("POLICY_NOT_EVALUATED")
89
+ elif policy_val is not True:
90
+ blockers.append("POLICY_FAILED")
91
+ if unresolved:
92
+ blockers.append("REFERENCES_NOT_RESOLVED")
93
+
94
+ return {
95
+ "cryptoValid": crypto_ok,
96
+ "structureValid": structure_ok,
97
+ "policyAuthorized": None if policy_key is None else (policy_val is True),
98
+ "referencesResolved": None if not reference_keys else not unresolved,
99
+ "safeForAutomation": not blockers,
100
+ "automationBlockers": blockers,
101
+ }