proofbundle 3.2.3__tar.gz → 3.3.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.
Files changed (206) hide show
  1. {proofbundle-3.2.3/src/proofbundle.egg-info → proofbundle-3.3.0}/PKG-INFO +7 -5
  2. {proofbundle-3.2.3 → proofbundle-3.3.0}/README.md +6 -4
  3. {proofbundle-3.2.3 → proofbundle-3.3.0}/pyproject.toml +1 -1
  4. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/__init__.py +1 -1
  5. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/automation_verdict.py +6 -0
  6. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/cli.py +66 -4
  7. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/decision.py +75 -4
  8. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/outcome.py +40 -5
  9. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policy.py +27 -1
  10. proofbundle-3.3.0/src/proofbundle/relation.py +319 -0
  11. {proofbundle-3.2.3 → proofbundle-3.3.0/src/proofbundle.egg-info}/PKG-INFO +7 -5
  12. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/SOURCES.txt +3 -0
  13. proofbundle-3.3.0/tests/test_relation_profile.py +736 -0
  14. proofbundle-3.3.0/tests/test_relation_property.py +110 -0
  15. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_schema_parity.py +61 -0
  16. {proofbundle-3.2.3 → proofbundle-3.3.0}/LICENSE +0 -0
  17. {proofbundle-3.2.3 → proofbundle-3.3.0}/setup.cfg +0 -0
  18. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/_inspect_registry.py +0 -0
  19. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/_integration.py +0 -0
  20. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/_strict_json.py +0 -0
  21. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/__init__.py +0 -0
  22. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/_provenance.py +0 -0
  23. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/eee.py +0 -0
  24. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/inspect_ai.py +0 -0
  25. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/lm_eval.py +0 -0
  26. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/promptfoo.py +0 -0
  27. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/adapters/samples.py +0 -0
  28. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors.py +0 -0
  29. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_chia.py +0 -0
  30. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_chia_add.py +0 -0
  31. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_markovian.py +0 -0
  32. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_ots.py +0 -0
  33. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/anchors_rfc3161.py +0 -0
  34. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/assurance.py +0 -0
  35. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/beacon.py +0 -0
  36. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/budget.py +0 -0
  37. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/bundle.py +0 -0
  38. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/canonical.py +0 -0
  39. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/checkpoint.py +0 -0
  40. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/demo.py +0 -0
  41. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/dsse.py +0 -0
  42. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/eee_eval_schema.json +0 -0
  43. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/emit.py +0 -0
  44. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/errors.py +0 -0
  45. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/evalcard.py +0 -0
  46. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/evalclaim.py +0 -0
  47. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/evidence_pack.py +0 -0
  48. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/experimental/__init__.py +0 -0
  49. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/experimental/enclave.py +0 -0
  50. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/hashalg.py +0 -0
  51. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/hf_evals.py +0 -0
  52. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/inspect_hook.py +0 -0
  53. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/intoto.py +0 -0
  54. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/kbjwt.py +0 -0
  55. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/merkle.py +0 -0
  56. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/persample.py +0 -0
  57. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/decision-receipt-template-v1.json +0 -0
  58. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/research-preview-v1.json +0 -0
  59. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/strict-eval-authenticated-root-template-v1.json +0 -0
  60. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/strict-eval-template-v1.json +0 -0
  61. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policies/strict-prereg-template-v1.json +0 -0
  62. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/policy_profiles.py +0 -0
  63. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/pqsig.py +0 -0
  64. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/prereg.py +0 -0
  65. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/public_transparency.py +0 -0
  66. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/py.typed +0 -0
  67. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/pytest_plugin.py +0 -0
  68. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/renewal.py +0 -0
  69. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/run_ledger.py +0 -0
  70. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/sdjwt.py +0 -0
  71. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/sdjwt_issue.py +0 -0
  72. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/sdjwt_vc.py +0 -0
  73. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/signature.py +0 -0
  74. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/statuslist.py +0 -0
  75. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/subject_binding.py +0 -0
  76. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/tlogproof.py +0 -0
  77. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/trust_pack.py +0 -0
  78. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle/verification_summary.py +0 -0
  79. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/dependency_links.txt +0 -0
  80. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/entry_points.txt +0 -0
  81. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/requires.txt +0 -0
  82. {proofbundle-3.2.3 → proofbundle-3.3.0}/src/proofbundle.egg-info/top_level.txt +0 -0
  83. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_adapters.py +0 -0
  84. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_adversarial.py +0 -0
  85. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchor_hardening_321.py +0 -0
  86. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchor_longevity_property.py +0 -0
  87. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchor_target_trustedtime.py +0 -0
  88. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_chia.py +0 -0
  89. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_chia_add.py +0 -0
  90. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_chia_claims.py +0 -0
  91. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_generic.py +0 -0
  92. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_markovian.py +0 -0
  93. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_ots.py +0 -0
  94. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_anchors_rfc3161.py +0 -0
  95. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_assurance.py +0 -0
  96. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_automation_verdict.py +0 -0
  97. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_beacon.py +0 -0
  98. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_branch_base_check.py +0 -0
  99. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_budget.py +0 -0
  100. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_bundle.py +0 -0
  101. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_bundle_robustness.py +0 -0
  102. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_canonical.py +0 -0
  103. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_checkpoint.py +0 -0
  104. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_checkpoint_external_vectors.py +0 -0
  105. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_checkpoint_quorum_property.py +0 -0
  106. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_claims_hygiene.py +0 -0
  107. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cli.py +0 -0
  108. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cli_eval.py +0 -0
  109. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cli_require_anchor.py +0 -0
  110. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_conformance.py +0 -0
  111. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_content_root_property.py +0 -0
  112. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cosignature.py +0 -0
  113. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_cosignature_mldsa.py +0 -0
  114. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_anchors.py +0 -0
  115. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_cli.py +0 -0
  116. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_evidence.py +0 -0
  117. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_fuzz.py +0 -0
  118. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_hardening.py +0 -0
  119. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_policy.py +0 -0
  120. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_schema.py +0 -0
  121. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_validator_api.py +0 -0
  122. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_decision_verify.py +0 -0
  123. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_demo.py +0 -0
  124. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_docs_truth.py +0 -0
  125. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_dsse_adversarial.py +0 -0
  126. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_dup_key_reject.py +0 -0
  127. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_ed25519_semantics.py +0 -0
  128. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_eee.py +0 -0
  129. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_emit.py +0 -0
  130. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_enclave_assurance.py +0 -0
  131. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_eval_claim_schema.py +0 -0
  132. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_eval_evidence_class.py +0 -0
  133. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_evalcard.py +0 -0
  134. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_evalclaim.py +0 -0
  135. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_evidence_pack.py +0 -0
  136. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_examples.py +0 -0
  137. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_experimental_enclave.py +0 -0
  138. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_fork_pr_secret_isolation.py +0 -0
  139. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_fuzz_parsers.py +0 -0
  140. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_hashalg.py +0 -0
  141. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_hf_entry_verify_eee_digest.py +0 -0
  142. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_hf_evals.py +0 -0
  143. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_inspect_hook.py +0 -0
  144. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto.py +0 -0
  145. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_claims_hygiene.py +0 -0
  146. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_content_root_migration.py +0 -0
  147. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_dsse.py +0 -0
  148. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_eval_result.py +0 -0
  149. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_examples.py +0 -0
  150. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_spec_diff.py +0 -0
  151. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_intoto_svr.py +0 -0
  152. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_kbjwt.py +0 -0
  153. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_lens_review_fixes_3_1_3.py +0 -0
  154. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_merkle.py +0 -0
  155. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_merkle_consistency_property.py +0 -0
  156. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_merkle_property.py +0 -0
  157. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_mldsa_acvp_vectors.py +0 -0
  158. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_official_vectors.py +0 -0
  159. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_ok_semantics.py +0 -0
  160. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_ots_external_vectors.py +0 -0
  161. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_outcome_receiver_corroboration.py +0 -0
  162. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_outcome_verify.py +0 -0
  163. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_oversized_int_guard.py +0 -0
  164. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_persample.py +0 -0
  165. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_pinned_key_validation.py +0 -0
  166. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_policy_explain_lint.py +0 -0
  167. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_policy_lifecycle_purpose.py +0 -0
  168. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_policy_profiles.py +0 -0
  169. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_policy_templates.py +0 -0
  170. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_pqsig.py +0 -0
  171. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_predicate_type_enforcement.py +0 -0
  172. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_prereg.py +0 -0
  173. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_promptfoo.py +0 -0
  174. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_provenance.py +0 -0
  175. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_public_transparency.py +0 -0
  176. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_pytest_plugin.py +0 -0
  177. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_rekor_interop.py +0 -0
  178. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_release_integrity_gate.py +0 -0
  179. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_renewal.py +0 -0
  180. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_renewal_external_token_glue.py +0 -0
  181. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_renewal_policy.py +0 -0
  182. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_renewal_signed.py +0 -0
  183. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_rfc6962_external_vectors.py +0 -0
  184. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_root_authenticity.py +0 -0
  185. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_run_ledger.py +0 -0
  186. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_rust_parity_gate.py +0 -0
  187. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_schema.py +0 -0
  188. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_adversarial.py +0 -0
  189. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_duplicate_cnf.py +0 -0
  190. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_issue.py +0 -0
  191. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_reference.py +0 -0
  192. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_resource_budget.py +0 -0
  193. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_vc.py +0 -0
  194. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwt_verify_binding.py +0 -0
  195. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_sdjwtvc_external_vectors.py +0 -0
  196. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_signature.py +0 -0
  197. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_statuslist.py +0 -0
  198. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_subject_binding.py +0 -0
  199. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_tlogproof.py +0 -0
  200. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_tree_context_authenticity.py +0 -0
  201. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_trust_pack.py +0 -0
  202. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_trust_pack_crypto_agility.py +0 -0
  203. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_trust_pack_outcome_receivers_role.py +0 -0
  204. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_trust_policy.py +0 -0
  205. {proofbundle-3.2.3 → proofbundle-3.3.0}/tests/test_verification_summary.py +0 -0
  206. {proofbundle-3.2.3 → proofbundle-3.3.0}/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.3
3
+ Version: 3.3.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
@@ -72,7 +72,7 @@ Merkle, one file, no server, no network.
72
72
  [![PyPI](https://img.shields.io/pypi/v/proofbundle.svg)](https://pypi.org/project/proofbundle/)
73
73
  [![Python](https://img.shields.io/pypi/pyversions/proofbundle.svg)](https://pypi.org/project/proofbundle/)
74
74
  [![License: MIT](https://img.shields.io/badge/license-MIT-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/LICENSE)
75
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21230467.svg)](https://doi.org/10.5281/zenodo.21230467)
75
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21110642.svg)](https://doi.org/10.5281/zenodo.21110642)
76
76
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
77
77
  [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
78
78
  [![PyPI attestations](https://img.shields.io/badge/PyPI-PEP_740_attested-D6248A.svg)](https://pypi.org/project/proofbundle/#files)
@@ -211,9 +211,11 @@ a preprint.
211
211
  ## Cite this work
212
212
 
213
213
  If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
214
- [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff); the archival record and
215
- concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
216
- current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
214
+ [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff). The archival software record
215
+ is on Zenodo under concept DOI [10.5281/zenodo.21110642](https://doi.org/10.5281/zenodo.21110642); the
216
+ Technical Note (design write-up) under concept DOI
217
+ [10.5281/zenodo.21230466](https://doi.org/10.5281/zenodo.21230466), also linked from
218
+ [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
217
219
 
218
220
  ## Post-quantum posture (honest, two layers)
219
221
 
@@ -18,7 +18,7 @@ Merkle, one file, no server, no network.
18
18
  [![PyPI](https://img.shields.io/pypi/v/proofbundle.svg)](https://pypi.org/project/proofbundle/)
19
19
  [![Python](https://img.shields.io/pypi/pyversions/proofbundle.svg)](https://pypi.org/project/proofbundle/)
20
20
  [![License: MIT](https://img.shields.io/badge/license-MIT-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/LICENSE)
21
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21230467.svg)](https://doi.org/10.5281/zenodo.21230467)
21
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21110642.svg)](https://doi.org/10.5281/zenodo.21110642)
22
22
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
23
23
  [![Mutation tested](https://img.shields.io/badge/tests-mutation_gated-D6248A.svg)](https://github.com/b7n0de/proofbundle/blob/main/scripts/mutation_check.py)
24
24
  [![PyPI attestations](https://img.shields.io/badge/PyPI-PEP_740_attested-D6248A.svg)](https://pypi.org/project/proofbundle/#files)
@@ -157,9 +157,11 @@ a preprint.
157
157
  ## Cite this work
158
158
 
159
159
  If proofbundle helped your evaluation pipeline, please cite it. Machine-readable metadata is in
160
- [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff); the archival record and
161
- concept DOI are on Zenodo ([10.5281/zenodo.21230467](https://doi.org/10.5281/zenodo.21230467)), and the
162
- current design write-up is the Technical Note linked from [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
160
+ [`CITATION.cff`](https://github.com/b7n0de/proofbundle/blob/main/CITATION.cff). The archival software record
161
+ is on Zenodo under concept DOI [10.5281/zenodo.21110642](https://doi.org/10.5281/zenodo.21110642); the
162
+ Technical Note (design write-up) under concept DOI
163
+ [10.5281/zenodo.21230466](https://doi.org/10.5281/zenodo.21230466), also linked from
164
+ [b7n0de.com/proofbundle](https://b7n0de.com/proofbundle).
163
165
 
164
166
  ## Post-quantum posture (honest, two layers)
165
167
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "proofbundle"
7
- version = "3.2.3"
7
+ version = "3.3.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
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.3"
16
+ __version__ = "3.3.0"
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
@@ -33,6 +33,12 @@ AUTOMATION_BLOCKER_REASONS = {
33
33
  "POLICY_FAILED": "The supplied trust policy / authorization gate was not satisfied",
34
34
  "REFERENCES_NOT_RESOLVED": "One or more referenced/bound artifacts did not resolve (see the "
35
35
  "predicate's own *_ok / *_bound / *_intact fields for which)",
36
+ # relation/v0.1 (EXPERIMENTAL): raised by the DECISION verify path when the trust
37
+ # policy's relations section is violated (the outcome-path policy gate is a documented
38
+ # follow-up — verify_outcome_receipt has no policy parameter today) (a named relation did not resolve, or an attached,
39
+ # verified successor supersedes the receipt under rejectSuperseded). LIVE, not dormant.
40
+ "LINEAGE_REQUIREMENT_FAILED": "The trust policy's lineage requirement was not met (relations "
41
+ "section: unresolved required relation or superseded receipt)",
36
42
  }
37
43
 
38
44
 
@@ -1207,6 +1207,39 @@ def _cmd_decision_emit(args: argparse.Namespace) -> int:
1207
1207
  return 0
1208
1208
 
1209
1209
 
1210
+ def _load_related(paths, pub: bytes) -> tuple[dict, list[str]]:
1211
+ """relation/v0.1 (--with-related): load DSSE envelopes of RELATED receipts, verify each one
1212
+ STANDALONE under the SAME relying-party key, and key it by its content root. Same-key is the
1213
+ honest v0.1 CLI contract (it matches the strictest relationSigner policy value); per-target
1214
+ keys are a documented follow-up. A file that fails to load is a usage error (exit 2 at the
1215
+ caller) — an envelope that loads but does NOT verify is kept with verified=False, so an
1216
+ attached-but-wrong target FAILS lineage instead of vanishing."""
1217
+ from . import anchors as _anchors_mod # noqa: PLC0415
1218
+ from . import dsse as _dsse # noqa: PLC0415
1219
+ related: dict = {}
1220
+ errs: list[str] = []
1221
+ for path in paths or []:
1222
+ try:
1223
+ with open(path, encoding="utf-8") as handle:
1224
+ env = loads_strict(handle.read()) # WP-C1: duplicate keys rejected
1225
+ body = _dsse.load_payload(env)
1226
+ root_hex = _anchors_mod.statement_content_root(body).hex()
1227
+ # L3-audit fix: inside the try so a malformed-envelope error names the offending file too.
1228
+ verified = bool(_dsse.verify_envelope(env, pub, payload_type="application/vnd.in-toto+json"))
1229
+ except (ProofBundleError, OSError, ValueError) as exc:
1230
+ errs.append(f"cannot read --with-related {path}: {exc}")
1231
+ continue
1232
+ rels = None
1233
+ try:
1234
+ stmt = loads_strict(body.decode("utf-8"))
1235
+ if isinstance(stmt, dict) and isinstance(stmt.get("predicate"), dict):
1236
+ rels = stmt["predicate"].get("relationships")
1237
+ except (ProofBundleError, ValueError):
1238
+ rels = None
1239
+ related[root_hex] = {"verified": verified, "relationships": rels}
1240
+ return related, errs
1241
+
1242
+
1210
1243
  def _cmd_decision_verify(args: argparse.Namespace) -> int:
1211
1244
  import base64 # noqa: PLC0415
1212
1245
  from .decision import verify_decision_receipt # noqa: PLC0415
@@ -1246,10 +1279,16 @@ def _cmd_decision_verify(args: argparse.Namespace) -> int:
1246
1279
  merged = dict(pol_trust)
1247
1280
  merged.update(rp_trust or {})
1248
1281
  rp_trust = merged
1282
+ related, rel_errs = _load_related(getattr(args, "with_related", None), pub)
1283
+ if rel_errs:
1284
+ for e in rel_errs:
1285
+ print(f"ERROR: {e}", file=sys.stderr)
1286
+ return 2
1249
1287
  result = verify_decision_receipt(env, pub, strict=args.strict, expected_audience=args.aud,
1250
1288
  expected_nonce=args.nonce, policy=policy, anchors=anchors,
1251
1289
  rp_trust=rp_trust,
1252
- require_derived_subject=args.require_derived_subject)
1290
+ require_derived_subject=args.require_derived_subject,
1291
+ related=related or None)
1253
1292
  except (ProofBundleError, OSError, ValueError) as exc:
1254
1293
  print(f"ERROR: {exc}", file=sys.stderr)
1255
1294
  return 2
@@ -1265,7 +1304,7 @@ def _cmd_decision_verify(args: argparse.Namespace) -> int:
1265
1304
  # Findings 01/03 (crypto-review X2): the uniform automation verdict + EvidenceLevel ladder are
1266
1305
  # part of the library result; a pipeline doing `... --json | jq .automation.safeForAutomation`
1267
1306
  # must not get null (indistinguishable from a real "not evaluated"). Emit them here too.
1268
- "automation", "evidence_levels", "warnings", "errors",
1307
+ "automation", "evidence_levels", "lineage", "warnings", "errors",
1269
1308
  ) if k in result}
1270
1309
  print(json.dumps(report, indent=2, default=str))
1271
1310
  else:
@@ -1315,6 +1354,11 @@ def _cmd_decision_verify(args: argparse.Namespace) -> int:
1315
1354
  # signal — fail-closed, never a silent exit 0 (fix-review Finding 3). None = no anchor supplied.
1316
1355
  if result["anchors_ok"] is False:
1317
1356
  return 1
1357
+ # relation/v0.1: a REQUESTED lineage check (edges present or --with-related supplied) that FAILs
1358
+ # (cycle, depth, malformed ancestor, attached-but-unverified target) is a tamper/structure signal —
1359
+ # fail-closed exit 2, never a silent exit 0. DECLARED_UNRESOLVED stays exit 0 (declared-only is honest).
1360
+ if isinstance(result.get("lineage"), dict) and result["lineage"].get("lineage") == "FAIL":
1361
+ return 2
1318
1362
  if result["policy_ok"] is False:
1319
1363
  return 3
1320
1364
  return 0
@@ -1422,11 +1466,16 @@ def _cmd_outcome_verify(args: argparse.Namespace) -> int:
1422
1466
  with open(args.envelope, encoding="utf-8") as handle:
1423
1467
  env = loads_strict(handle.read()) # WP-C1: duplicate keys rejected
1424
1468
  pub = base64.b64decode(args.pub)
1469
+ related, rel_errs = _load_related(getattr(args, "with_related", None), pub)
1470
+ if rel_errs:
1471
+ for e in rel_errs:
1472
+ print(f"ERROR: {e}", file=sys.stderr)
1473
+ return 2
1425
1474
  result = verify_outcome_receipt(
1426
1475
  env, pub, strict=args.strict,
1427
1476
  expected_decision_ref=args.expected_decision_ref, decision_maker_id=args.decision_maker_id,
1428
1477
  expected_audience=args.aud, expected_nonce=args.nonce,
1429
- require_derived_subject=args.require_derived_subject)
1478
+ require_derived_subject=args.require_derived_subject, related=related or None)
1430
1479
  except (ProofBundleError, OSError, ValueError) as exc:
1431
1480
  print(f"ERROR: {exc}", file=sys.stderr)
1432
1481
  return 2
@@ -1439,7 +1488,7 @@ def _cmd_outcome_verify(args: argparse.Namespace) -> int:
1439
1488
  # is not silently blind to them (a jq filter on an absent key is indistinguishable from a real
1440
1489
  # None). executor_role_trusted/receiver_* are None unless the caller supplied a trust_pack.
1441
1490
  "automation", "evidence_levels", "executor_role_trusted", "receiver_bound", "receiver_role_trusted",
1442
- "warnings", "errors",
1491
+ "lineage", "warnings", "errors",
1443
1492
  ) if k in result}
1444
1493
  print(json.dumps(report, indent=2, default=str))
1445
1494
  else:
@@ -1481,6 +1530,10 @@ def _cmd_outcome_verify(args: argparse.Namespace) -> int:
1481
1530
  # fail-closed binding as --aud/--nonce — an EXTERNAL_ATTESTED subject then must not exit 0.
1482
1531
  if result["subject_derived_ok"] is False:
1483
1532
  return 2
1533
+ # relation/v0.1 (adversarial-review finding, 2026-07-16): mirror of the decision-verify gate —
1534
+ # a REQUESTED lineage check (--with-related / edges present) that FAILs must never exit 0.
1535
+ if isinstance(result.get("lineage"), dict) and result["lineage"].get("lineage") == "FAIL":
1536
+ return 2
1484
1537
  return 0
1485
1538
 
1486
1539
 
@@ -1983,6 +2036,12 @@ def build_parser() -> argparse.ArgumentParser:
1983
2036
  d_verify.add_argument("--bitcoin-header", action="append", default=None, metavar="HEIGHT:MERKLEROOT_HEX",
1984
2037
  help="WP-A1: a relying-party-supplied Bitcoin block header (internal byte order) "
1985
2038
  "that confirms an OpenTimestamps statement anchor — frozen is never trusted")
2039
+ d_verify.add_argument("--with-related", dest="with_related", action="append", default=None, metavar="PATH",
2040
+ help="relation/v0.1 (EXPERIMENTAL): attach a RELATED receipt's DSSE envelope for "
2041
+ "offline lineage resolution (repeatable). Each target is verified standalone "
2042
+ "under the SAME --pub (same-key contract); an attached target that does not "
2043
+ "verify FAILS lineage — relationship declared by issuer, not a statement of "
2044
+ "correctness")
1986
2045
  d_verify.add_argument("--require-derived-subject", dest="require_derived_subject", action="store_true",
1987
2046
  help="Finding 05: fail closed (exit 2) unless the Statement subject is a DERIVED "
1988
2047
  "commitment to the predicate (subject_binding.classify_subject) — rejects a "
@@ -2027,6 +2086,9 @@ def build_parser() -> argparse.ArgumentParser:
2027
2086
  o_verify.add_argument("--strict", action="store_true", help="enforce strict-v0.1 required fields")
2028
2087
  o_verify.add_argument("--aud", default=None, help="expected audience (checks validity.audience against replay)")
2029
2088
  o_verify.add_argument("--nonce", default=None, help="expected nonce (checks validity.nonce against replay)")
2089
+ o_verify.add_argument("--with-related", dest="with_related", action="append", default=None, metavar="PATH",
2090
+ help="relation/v0.1 (EXPERIMENTAL): attach a RELATED receipt's DSSE envelope for "
2091
+ "offline lineage resolution (repeatable; same-key contract as decision verify)")
2030
2092
  o_verify.add_argument("--require-derived-subject", dest="require_derived_subject", action="store_true",
2031
2093
  help="Finding 05: fail closed (exit 2) unless the Statement subject is a DERIVED "
2032
2094
  "commitment to the predicate (subject_binding.classify_subject) — rejects a "
@@ -48,7 +48,7 @@ _REQUIRED_STRICT = _REQUIRED_ALWAYS + ("notChecked", "decisionChangeConditions",
48
48
  # prereg anchor) are referenced indirectly via `evidenceRefs`. An `anchors` field inside the predicate is
49
49
  # therefore a fail-closed unknown-field error.
50
50
  _OPTIONAL = ("recordedAt", "delegationRefs", "actionOutcome", "traceContext", "validity",
51
- "notChecked", "decisionChangeConditions", "privacy")
51
+ "notChecked", "decisionChangeConditions", "privacy", "relationships")
52
52
  _ALLOWED_TOP = set(_REQUIRED_ALWAYS) | set(_OPTIONAL)
53
53
 
54
54
  # Time-bearing paths that, if present, MUST be RFC3339-Z.
@@ -254,6 +254,16 @@ def validate_decision_predicate(predicate: Any, *, strict: bool = False) -> list
254
254
  elif strict and isinstance(priv, dict) and not isinstance(priv.get("rawInputsIncluded"), bool):
255
255
  errors.append("privacy.rawInputsIncluded (boolean) is required in strict mode")
256
256
 
257
+ # relationships (optional, relation/v0.1 EXPERIMENTAL): typed, SIGNED lineage edges to earlier
258
+ # receipts — inside the predicate so the existing DSSE signature covers them (unlike detached
259
+ # `anchors`). Edge closure/enums/digest shape are enforced by relation.validate_relationships
260
+ # (fail-closed, incl. per-edge additionalProperties:false), so the block is NOT walked by the
261
+ # generic nested-closure below.
262
+ if "relationships" in predicate:
263
+ from .relation import validate_relationships
264
+ errors.extend(f"relationships: {e}" if not e.startswith("relationships") else e
265
+ for e in validate_relationships(predicate.get("relationships")))
266
+
257
267
  # Nested schema closure (Finding 04): additionalProperties:false at the TOP level does not, by itself,
258
268
  # close these nested objects — an undeclared key inside decision/policyBoundary/proposedAction/
259
269
  # decisionMaker/evidenceRefs[] (and their sub-objects) previously rode along silently.
@@ -395,6 +405,9 @@ def _empty_result() -> dict:
395
405
  # (assurance.classify_digest_evidence) computed at the end of verify — never gate anything on
396
406
  # these two here, the old ok/*_proven/evidence_bound fields above are UNCHANGED for compat.
397
407
  "automation": None, "evidence_levels": None,
408
+ # relation/v0.1 (EXPERIMENTAL, additive): lineage verdict over the predicate's OPTIONAL
409
+ # relationships edges — None until computed over AUTHENTICATED bytes; never gates `ok`.
410
+ "lineage": None, "lineage_requirement_failed": None, "lineage_ok": None,
398
411
  "warnings": [], "errors": [],
399
412
  }
400
413
 
@@ -403,7 +416,8 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
403
416
  expected_audience: str | None = None, expected_nonce: str | None = None,
404
417
  policy: dict | None = None, anchors: list | None = None,
405
418
  rp_trust: dict | None = None, require_derived_subject: bool = False,
406
- evidence_resolver: Callable[[dict], bool] | None = None) -> dict:
419
+ evidence_resolver: Callable[[dict], bool] | None = None,
420
+ related: dict | None = None) -> dict:
407
421
  """Verify a DSSE-signed Decision Receipt. Crypto first, then structure over the EXACT signed bytes (never
408
422
  re-serialized). Returns the snake_case structured result; each check independent, non-applicable = None.
409
423
 
@@ -535,6 +549,33 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
535
549
  "actionOutcome.outcomeRef": _outcome_level,
536
550
  "evidenceRefs": _evref_levels,
537
551
  }
552
+
553
+ # relation/v0.1 (EXPERIMENTAL, additive): evaluate the OPTIONAL relationships edges against
554
+ # caller-attached targets (`related`, offline — the CLI's --with-related). Computed ONLY over
555
+ # authenticated bytes (this block), NEVER feeds `ok`/crypto (lattice monotonicity); a lineage
556
+ # FAIL surfaces via errors[] and the policy layer, not by flipping the crypto verdict.
557
+ if "relationships" in predicate or related:
558
+ from . import anchors as _anchors_for_rel # noqa: PLC0415
559
+ from .relation import successor_warning, verify_relationship_edges # noqa: PLC0415
560
+ try:
561
+ _subject_hex = _anchors_for_rel.statement_content_root(body).hex()
562
+ except Exception:
563
+ _subject_hex = None
564
+ r["lineage"] = verify_relationship_edges(
565
+ predicate.get("relationships"), related, subject_hex=_subject_hex)
566
+ # Advisory by default; the policy's reject_superseded turns it into a blocker below.
567
+ _sw = successor_warning(predicate.get("relationships"), related, subject_hex=_subject_hex)
568
+ r["lineage"]["supersededByAttached"] = _sw
569
+ if _sw:
570
+ r["warnings"].append(f"lineage: {_sw}")
571
+ if r["lineage"]["lineage"] == "FAIL":
572
+ r["errors"].extend(r["lineage"]["errors"] or ["relation: lineage verification FAILED"])
573
+ # No-Fake (6-Linsen-Audit L2, 2026-07-16): a REQUESTED lineage check that FAILs must be
574
+ # visible in the aggregate `ok` and `automation` — like every other error category in this
575
+ # result — not only at the CLI exit code. lineage_ok is False ONLY on FAIL (DECLARED_UNRESOLVED
576
+ # and NOT_EVALUATED are honest non-fails, stay None). This tightens the verdict downward; it
577
+ # NEVER lifts it (crypto_ok is untouched, lattice monotonicity preserved).
578
+ r["lineage_ok"] = False if r["lineage"]["lineage"] == "FAIL" else None
538
579
  # 3.1.2 fail-closed fix (audit 2026-07-13, sibling of the eval-path F4 hardening and the decision
539
580
  # template/expiry gates): a relying party who supplies expected_audience/expected_nonce is ASKING for
540
581
  # RFC-9901-§7.3-style replay/audience binding. If the receipt carries NO validity object (or a
@@ -662,6 +703,27 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
662
703
  "decision_receipt.trusted_decision_makers) — POLICY: OK then proves integrity by an "
663
704
  "UNKNOWN signer. Pin the expected decision-maker key(s).")
664
705
 
706
+ # relation/v0.1 policy gate (relations section, LIVE): a violation fails policy_ok (exit-3 class)
707
+ # and raises the dedicated automation blocker LINEAGE_REQUIREMENT_FAILED below — it NEVER touches
708
+ # the crypto verdict (lattice monotonicity). require_relation_resolution is conditional on
709
+ # presence: a named relation that appears as an edge MUST be VERIFIED (attached + standalone-
710
+ # verified); an absent relation is no violation.
711
+ if policy is not None and isinstance(policy.get("relations"), dict) and r["crypto_ok"]:
712
+ _rel_pol = policy["relations"]
713
+ _lin = r.get("lineage") or {}
714
+ _viol = []
715
+ _req = _rel_pol.get("require_relation_resolution") or []
716
+ for _e in (_lin.get("edges") or []):
717
+ if _e.get("relation") in _req and _e.get("resolution") != "VERIFIED":
718
+ _viol.append(f"relation {_e.get('relation')!r} must resolve (target attached and "
719
+ f"verified), got {_e.get('resolution')}")
720
+ if _rel_pol.get("reject_superseded") and _lin.get("supersededByAttached"):
721
+ _viol.append(f"reject_superseded: {_lin['supersededByAttached']}")
722
+ if _viol:
723
+ r["policy_ok"] = False
724
+ r["lineage_requirement_failed"] = True
725
+ r["errors"].extend("LINEAGE_REQUIREMENT_FAILED: " + v for v in _viol)
726
+
665
727
  # Aggregate verdict: authenticated AND well-structured AND no applicable trust check FAILED.
666
728
  # None means not-applicable (passes); only an explicit False fails the aggregate.
667
729
  r["ok"] = bool(
@@ -669,13 +731,22 @@ def verify_decision_receipt(envelope: dict, public_key: bytes, *, strict: bool =
669
731
  and r["policy_ok"] is not False and r["signer_trusted"] is not False
670
732
  and r["audience_ok"] is not False and r["nonce_ok"] is not False
671
733
  and r["evidence_bound"] is not False and r["anchors_ok"] is not False
672
- and r["subject_derived_ok"] is not False)
734
+ and r["subject_derived_ok"] is not False and r["lineage_ok"] is not False)
673
735
 
674
736
  # Finding 01 (additive): the STRICTER automation-safety verdict — policy_ok must be True, never merely
675
737
  # "not False" — never changes `ok` above.
676
738
  from .automation_verdict import automation_summary # noqa: PLC0415
677
739
  r["automation"] = automation_summary(r, required_checks={
678
740
  "crypto": "crypto_ok", "structure": "structure_ok", "policy": "policy_ok",
679
- "references": ["evidence_bound", "audience_ok", "nonce_ok", "anchors_ok", "subject_derived_ok"],
741
+ "references": ["evidence_bound", "audience_ok", "nonce_ok", "anchors_ok", "subject_derived_ok",
742
+ "lineage_ok"],
680
743
  })
744
+ # relation/v0.1: the dedicated blocker name (LIVE, not dormant) — POLICY_FAILED already fires via
745
+ # policy_ok=False above; this names the REASON so a consumer can distinguish a lineage gate from
746
+ # any other policy failure. Only ever ADDS a blocker (never turns safeForAutomation true).
747
+ if r.get("lineage_requirement_failed") and isinstance(r.get("automation"), dict):
748
+ blockers = r["automation"].setdefault("automationBlockers", [])
749
+ if "LINEAGE_REQUIREMENT_FAILED" not in blockers:
750
+ blockers.append("LINEAGE_REQUIREMENT_FAILED")
751
+ r["automation"]["safeForAutomation"] = False
681
752
  return r
@@ -45,7 +45,10 @@ _OPTIONAL = ("actualActionDigest", "responseDigest", "effectDigest", "recordedAt
45
45
  "traceContext", "limitations", "validity",
46
46
  # Finding 16 (self-fixable part, additive): receiverRefs / sequence — see the module-level
47
47
  # note above `_OUTCOME_RECEIVER_ROLE` for what each closes and its honest, documented limit.
48
- "receiverRefs", "sequence")
48
+ "receiverRefs", "sequence",
49
+ # relation/v0.1 (EXPERIMENTAL, 3.3.0): typed signed lineage edges — validated fail-closed
50
+ # by relation.validate_relationships (incl. per-edge closure), not by the generic walker.
51
+ "relationships")
49
52
  _ALLOWED_TOP = set(_REQUIRED_ALWAYS) | set(_OPTIONAL)
50
53
 
51
54
  _DIGEST_FIELDS = ("requestedActionDigest", "actualActionDigest", "responseDigest", "effectDigest")
@@ -186,6 +189,14 @@ def validate_outcome_predicate(predicate: Any, *, strict: bool = False) -> list[
186
189
  if "validity" in predicate and not isinstance(val, dict):
187
190
  errors.append("validity must be an object")
188
191
 
192
+ # relationships (optional, relation/v0.1 EXPERIMENTAL): typed, SIGNED lineage edges — inside the
193
+ # predicate so the DSSE signature covers them; closure/enums/digests enforced fail-closed by the
194
+ # relation module itself.
195
+ if "relationships" in predicate:
196
+ from .relation import validate_relationships
197
+ errors.extend(f"relationships: {e}" if not e.startswith("relationships") else e
198
+ for e in validate_relationships(predicate.get("relationships")))
199
+
189
200
  # Nested schema closure (Finding 04): additionalProperties:false at the TOP level does not, by itself,
190
201
  # close traceContext/validity — an undeclared key inside either previously rode along silently.
191
202
  errors.extend(nested_closure_violations(predicate, _NESTED_ALLOWED))
@@ -422,7 +433,7 @@ def _empty_result() -> dict:
422
433
  # a non-empty receiverRefs are supplied. Neither is wired into the aggregate `ok` (receiverRefs is
423
434
  # OPTIONAL supplementary evidence, not core to the outcome's own validity — see verify docstring).
424
435
  "receiver_bound": None, "receiver_role_trusted": None,
425
- "warnings": [], "errors": [],
436
+ "lineage": None, "lineage_ok": None, "warnings": [], "errors": [],
426
437
  }
427
438
 
428
439
 
@@ -431,7 +442,8 @@ def verify_outcome_receipt(envelope: dict, public_key: bytes, *, strict: bool =
431
442
  expected_audience: str | None = None, expected_nonce: str | None = None,
432
443
  require_derived_subject: bool = False, trust_pack: dict | None = None,
433
444
  evidence_resolver: Callable[[dict], bool] | None = None,
434
- receiver_attestation_resolver: Callable[[dict], bool] | None = None) -> dict:
445
+ receiver_attestation_resolver: Callable[[dict], bool] | None = None,
446
+ related: dict | None = None) -> dict:
435
447
  """Verify a DSSE-signed Outcome Receipt. Crypto first, then structure over the EXACT signed bytes.
436
448
 
437
449
  Outcome-specific fail-closed checks (each applies only after crypto passes; non-applicable = None):
@@ -547,6 +559,28 @@ def verify_outcome_receipt(envelope: dict, public_key: bytes, *, strict: bool =
547
559
  "role separation violated — executor.id equals the decisionMaker id; whoever decides "
548
560
  "must not witness their own execution (fail-closed)")
549
561
 
562
+ # relation/v0.1 (EXPERIMENTAL, additive): evaluate the OPTIONAL relationships edges against
563
+ # caller-attached targets (offline --with-related). Only over AUTHENTICATED bytes; NEVER feeds
564
+ # the crypto verdict (lattice monotonicity) — a lineage FAIL surfaces via errors[] + policy.
565
+ if "relationships" in predicate or related:
566
+ from . import anchors as _anchors_for_rel # noqa: PLC0415
567
+ from .relation import successor_warning, verify_relationship_edges # noqa: PLC0415
568
+ try:
569
+ _subject_hex = _anchors_for_rel.statement_content_root(body).hex()
570
+ except Exception:
571
+ _subject_hex = None
572
+ r["lineage"] = verify_relationship_edges(
573
+ predicate.get("relationships"), related, subject_hex=_subject_hex)
574
+ _sw = successor_warning(predicate.get("relationships"), related, subject_hex=_subject_hex)
575
+ r["lineage"]["supersededByAttached"] = _sw
576
+ if _sw:
577
+ r["warnings"].append(f"lineage: {_sw}")
578
+ if r["lineage"]["lineage"] == "FAIL":
579
+ r["errors"].extend(r["lineage"]["errors"] or ["relation: lineage verification FAILED"])
580
+ # No-Fake (6-Linsen-Audit L2): mirror of the decision path — a REQUESTED lineage FAIL is
581
+ # visible in `ok`/`automation`, not only at the CLI exit code. FAIL->False, else None.
582
+ r["lineage_ok"] = False if r["lineage"]["lineage"] == "FAIL" else None
583
+
550
584
  # execution proof (honesty limit, warning not hard-fail).
551
585
  r["execution_proven"] = outcome_execution_proven(predicate)
552
586
  if r["execution_proven"] is False:
@@ -675,7 +709,8 @@ def verify_outcome_receipt(envelope: dict, public_key: bytes, *, strict: bool =
675
709
  # Finding 01 (additive, backward compatible): None (no trust_pack supplied, the pre-existing
676
710
  # default for every caller) passes exactly like every other optional check above; only an explicit
677
711
  # False (a trust_pack WAS supplied and the executor is not a trusted role member) fails ok.
678
- and r["executor_role_trusted"] is not False)
712
+ and r["executor_role_trusted"] is not False
713
+ and r["lineage_ok"] is not False)
679
714
 
680
715
  # Finding 01 (additive): the STRICTER automation-safety verdict — never changes `ok` above. Outcome has
681
716
  # no separate trust-policy layer (yet); executor_role_trusted (the outcomeExecutors role gate) is the
@@ -684,6 +719,6 @@ def verify_outcome_receipt(envelope: dict, public_key: bytes, *, strict: bool =
684
719
  r["automation"] = automation_summary(r, required_checks={
685
720
  "crypto": "crypto_ok", "structure": "structure_ok", "policy": "executor_role_trusted",
686
721
  "references": ["decision_bound", "role_separation_ok", "audience_ok", "nonce_ok",
687
- "subject_derived_ok"],
722
+ "subject_derived_ok", "lineage_ok"],
688
723
  })
689
724
  return r
@@ -105,7 +105,7 @@ def _pinned_key_forgeable(b64: str) -> bool:
105
105
 
106
106
 
107
107
  _TOP_KEYS = {"schema", "policy_id", "allowed_schema_versions", "allowed_issuers", "signature",
108
- "merkle", "sd_jwt", "status", "assurance", "decision_receipt", "anchors",
108
+ "merkle", "sd_jwt", "status", "assurance", "decision_receipt", "anchors", "relations",
109
109
  "deploymentReady", "requiresIdentityOverlay", # AP-2 §6.2 template metadata
110
110
  "valid_until", # AP-2 §6.4 lifecycle expiry
111
111
  "valid_from", # A-P0-2 §6 lifecycle not-before
@@ -135,6 +135,11 @@ def _parse_iso_utc(s):
135
135
  _ANCHORS_KEYS = {"require_anchor", "require_anchor_target", "allow_pending",
136
136
  "trusted_tsa_roots", "bitcoin_block_headers", "trusted_tsa_policy_oids"}
137
137
  _ANCHOR_TARGETS = ("receipt", "preRegistration", "statement")
138
+ # relation/v0.1 (EXPERIMENTAL, 3.3.0): the lineage policy section. relation_signer is a documented
139
+ # FOLLOW-UP (the CLI --with-related contract is same-key today; a pinned-set needs per-target key
140
+ # plumbing) — an unknown key here is fail-closed, so a future field cannot silently no-op.
141
+ _RELATIONS_KEYS = {"require_relation_resolution", "reject_superseded"}
142
+ _RELATION_NAMES = ("supersedes", "revises", "corrects", "retracts", "renews", "derivedFrom", "amends")
138
143
  _DECISION_KEYS = {"trusted_decision_makers", "allowed_decision_types", "allowed_verdicts",
139
144
  "required_evidence_relations", "accepted_predicate_types", "require_policy_digest",
140
145
  "require_external_anchor", "allow_pending", "require_audience", "require_nonce",
@@ -410,6 +415,19 @@ def load_policy(source: Union[str, dict]) -> dict:
410
415
  for k, v in hdrs.items()):
411
416
  raise PolicyError("anchors.bitcoin_block_headers must map a decimal height string to a "
412
417
  "64-char hex merkle root")
418
+ if "relations" in policy:
419
+ # relation/v0.1 lineage requirements (v0.2-gated like decision_receipt/anchors).
420
+ if policy.get("schema") != POLICY_SCHEMA_V0_2:
421
+ raise PolicyError("relations section requires schema proofbundle/trust-policy/v0.2")
422
+ rel = _require_dict(policy["relations"], "relations")
423
+ _reject_unknown(rel, _RELATIONS_KEYS, "relations")
424
+ if "require_relation_resolution" in rel:
425
+ rr = rel["require_relation_resolution"]
426
+ if (not isinstance(rr, list) or not rr
427
+ or not all(isinstance(x, str) and x in _RELATION_NAMES for x in rr)):
428
+ raise PolicyError("relations.require_relation_resolution must be a non-empty list of "
429
+ f"relation names out of {list(_RELATION_NAMES)}")
430
+ _require_bool(rel, "reject_superseded", "relations")
413
431
  if "decision_receipt" in policy:
414
432
  dr = _require_dict(policy["decision_receipt"], "decision_receipt")
415
433
  _reject_unknown(dr, _DECISION_KEYS, "decision_receipt")
@@ -977,6 +995,14 @@ def explain_policy(policy: dict) -> list:
977
995
  if anc.get("allow_pending"):
978
996
  detail += " (pending accepted)"
979
997
  lines.append(f"external time anchor required ({detail})")
998
+ # relation/v0.1: the relations section is enforced by the decision verify path (a
999
+ # violation fails policy_ok, exit 3; the outcome-path policy gate is a documented follow-up) — listed here for explain⟺enforce parity, same rule as anchors.
1000
+ rel = policy.get("relations") or {}
1001
+ if rel.get("require_relation_resolution"):
1002
+ lines.append("lineage relations must resolve (target attached + verified): "
1003
+ + ", ".join(rel["require_relation_resolution"]))
1004
+ if rel.get("reject_superseded"):
1005
+ lines.append("superseded receipt rejected (an attached, verified successor blocks automation)")
980
1006
  dr = policy.get("decision_receipt") or {}
981
1007
  if dr:
982
1008
  active = [k for k in dr if dr.get(k)]