vouch-protocol 2.1.0__tar.gz → 2.2.1__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 (293) hide show
  1. {vouch_protocol-2.1.0/vouch_protocol.egg-info → vouch_protocol-2.2.1}/PKG-INFO +4 -4
  2. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/README.md +2 -2
  3. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/pyproject.toml +6 -2
  4. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_hybrid.py +97 -1
  5. vouch_protocol-2.2.1/tests/test_mcp_server_shield.py +139 -0
  6. vouch_protocol-2.2.1/tests/test_shield_rules.py +356 -0
  7. vouch_protocol-2.2.1/tests/test_shield_vectors.py +90 -0
  8. vouch_protocol-2.2.1/tests/test_threshold_loader.py +132 -0
  9. vouch_protocol-2.2.1/tests/test_vouch_mcp.py +468 -0
  10. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/__init__.py +778 -778
  11. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/data_integrity_hybrid.py +32 -4
  12. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/mcp/server.py +194 -32
  13. vouch_protocol-2.2.1/vouch/mcp/__init__.py +269 -0
  14. vouch_protocol-2.2.1/vouch/mcp/_guard.py +420 -0
  15. vouch_protocol-2.2.1/vouch/shield/__init__.py +55 -0
  16. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/shield/demo.py +19 -16
  17. vouch_protocol-2.2.1/vouch/shield/rules.py +413 -0
  18. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/shield/shield.py +134 -32
  19. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/shield/trust_registry.py +42 -28
  20. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/threshold.py +34 -9
  21. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1/vouch_protocol.egg-info}/PKG-INFO +4 -4
  22. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/SOURCES.txt +8 -1
  23. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/requires.txt +1 -1
  24. vouch_protocol-2.1.0/vouch/shield/__init__.py +0 -34
  25. vouch_protocol-2.1.0/vouch/shield/permissions.py +0 -224
  26. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/LICENSE +0 -0
  27. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/MANIFEST.in +0 -0
  28. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/setup.cfg +0 -0
  29. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_accountability.py +0 -0
  30. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_agent_enroll.py +0 -0
  31. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_attribution.py +0 -0
  32. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_audio_bridge.py +0 -0
  33. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_audit_trail.py +0 -0
  34. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_auditor.py +0 -0
  35. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_authority_state.py +0 -0
  36. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_authority_state_vectors.py +0 -0
  37. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_autosign.py +0 -0
  38. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_behavioral_attestation.py +0 -0
  39. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_bridge.py +0 -0
  40. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_budget.py +0 -0
  41. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_c2pa_formats.py +0 -0
  42. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_cache.py +0 -0
  43. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_canary.py +0 -0
  44. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_caveats.py +0 -0
  45. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_cli_init.py +0 -0
  46. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_cli_root.py +0 -0
  47. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_conformance.py +0 -0
  48. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_credential_gate.py +0 -0
  49. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_data_integrity.py +0 -0
  50. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_delegation_attenuation.py +0 -0
  51. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_deliberation.py +0 -0
  52. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_disconnected_edge.py +0 -0
  53. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_dtn_pads.py +0 -0
  54. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_dx_sugar.py +0 -0
  55. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_examples_robotics.py +0 -0
  56. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_fastapi_credential_gate.py +0 -0
  57. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_fleet.py +0 -0
  58. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_git_workflow.py +0 -0
  59. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_grade.py +0 -0
  60. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_halos_evidence.py +0 -0
  61. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_hardware_seam.py +0 -0
  62. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_hasura_integration.py +0 -0
  63. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_heartbeat.py +0 -0
  64. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_hybrid_interop.py +0 -0
  65. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_intent_recheck.py +0 -0
  66. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_jcs.py +0 -0
  67. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_jcs_interop.py +0 -0
  68. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_liveness_conformance.py +0 -0
  69. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_media_badge.py +0 -0
  70. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_media_native.py +0 -0
  71. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_merkle.py +0 -0
  72. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_mint_contributor_credential.py +0 -0
  73. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_multikey.py +0 -0
  74. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_nonce.py +0 -0
  75. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_orbital_accumulator.py +0 -0
  76. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_proof_of_integration.py +0 -0
  77. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_provenance.py +0 -0
  78. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_quorum.py +0 -0
  79. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reasoning.py +0 -0
  80. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_receipts.py +0 -0
  81. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_recovery.py +0 -0
  82. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation.py +0 -0
  83. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_aggregate.py +0 -0
  84. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_disputes.py +0 -0
  85. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_ledger.py +0 -0
  86. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_policy.py +0 -0
  87. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_portability.py +0 -0
  88. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_revocation.py +0 -0
  89. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_access.py +0 -0
  90. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_conformance.py +0 -0
  91. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_consent.py +0 -0
  92. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_custody.py +0 -0
  93. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_embodiment.py +0 -0
  94. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_fusion.py +0 -0
  95. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_handover.py +0 -0
  96. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_handshake_blackbox_passport.py +0 -0
  97. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_identity.py +0 -0
  98. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_lease_quorum.py +0 -0
  99. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_lifecycle.py +0 -0
  100. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_liveness_revocation.py +0 -0
  101. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_odd.py +0 -0
  102. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_perception.py +0 -0
  103. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_pq.py +0 -0
  104. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_provenance_capability.py +0 -0
  105. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_root_identity.py +0 -0
  106. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_safety_record.py +0 -0
  107. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_swarm.py +0 -0
  108. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_teleop.py +0 -0
  109. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_wear.py +0 -0
  110. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_root_of_trust.py +0 -0
  111. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_root_of_trust_vectors.py +0 -0
  112. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_ros2_action_gate.py +0 -0
  113. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_runtime_vectors.py +0 -0
  114. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_scan.py +0 -0
  115. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_secure_key_custody.py +0 -0
  116. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_security_hardening.py +0 -0
  117. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_shield_guard.py +0 -0
  118. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_signer_vc.py +0 -0
  119. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_status_list.py +0 -0
  120. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_threshold.py +0 -0
  121. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_tool_registry.py +0 -0
  122. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transparency.py +0 -0
  123. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transport.py +0 -0
  124. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transport_http_rendezvous.py +0 -0
  125. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transport_rendezvous.py +0 -0
  126. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transport_udna_sdk.py +0 -0
  127. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_trifecta.py +0 -0
  128. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_trust_check.py +0 -0
  129. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_trust_entropy.py +0 -0
  130. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_validator_server.py +0 -0
  131. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_vc.py +0 -0
  132. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_verifier_vc.py +0 -0
  133. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/accountability.py +0 -0
  134. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/agent.py +0 -0
  135. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/async_verifier.py +0 -0
  136. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/attenuation.py +0 -0
  137. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/attribution.py +0 -0
  138. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/attribution_cli.py +0 -0
  139. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/audio.py +0 -0
  140. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/audit_trail.py +0 -0
  141. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/auditor.py +0 -0
  142. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/authority_state.py +0 -0
  143. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/autosign.py +0 -0
  144. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/behavioral_attestation.py +0 -0
  145. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/bridge/__init__.py +0 -0
  146. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/bridge/audio_routes.py +0 -0
  147. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/bridge/config.py +0 -0
  148. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/bridge/server.py +0 -0
  149. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/budget.py +0 -0
  150. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/cache.py +0 -0
  151. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/canary.py +0 -0
  152. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/caveats.py +0 -0
  153. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/cli.py +0 -0
  154. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/config.py +0 -0
  155. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/conformance.py +0 -0
  156. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/credential.py +0 -0
  157. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/data_integrity.py +0 -0
  158. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/deliberation.py +0 -0
  159. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/did_web.py +0 -0
  160. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/fleet.py +0 -0
  161. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/gate.py +0 -0
  162. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/grade.py +0 -0
  163. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/heartbeat.py +0 -0
  164. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/_common.py +0 -0
  165. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/a2a.py +0 -0
  166. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/adk.py +0 -0
  167. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/amnesia.py +0 -0
  168. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/autogen/__init__.py +0 -0
  169. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/autogpt/__init__.py +0 -0
  170. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/autogpt/commands.py +0 -0
  171. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/claude-code/settings.hooks.json +0 -0
  172. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/crewai/__init__.py +0 -0
  173. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/crewai/tool.py +0 -0
  174. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/fastapi.py +0 -0
  175. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/google.py +0 -0
  176. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/goose.py +0 -0
  177. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/hasura/__init__.py +0 -0
  178. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/hasura/webhook.py +0 -0
  179. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/langchain/__init__.py +0 -0
  180. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/langchain/tool.py +0 -0
  181. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/langgraph.py +0 -0
  182. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/mcp/__init__.py +0 -0
  183. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/mlflow.py +0 -0
  184. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/n8n.py +0 -0
  185. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/openai.py +0 -0
  186. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/safetensors.py +0 -0
  187. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/streamlit/__init__.py +0 -0
  188. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/streamlit/seal.py +0 -0
  189. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/vertex_ai/__init__.py +0 -0
  190. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/intent_recheck.py +0 -0
  191. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/jcs.py +0 -0
  192. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/keys.py +0 -0
  193. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/keystore.py +0 -0
  194. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/kms.py +0 -0
  195. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/liveness_conformance.py +0 -0
  196. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/mcp_guard.py +0 -0
  197. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/media/__init__.py +0 -0
  198. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/media/badge.py +0 -0
  199. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/media/c2pa.py +0 -0
  200. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/media/native.py +0 -0
  201. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/merkle.py +0 -0
  202. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/metrics.py +0 -0
  203. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/multikey.py +0 -0
  204. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/nonce.py +0 -0
  205. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/onboard.py +0 -0
  206. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/proof_of_integration.py +0 -0
  207. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/provenance.py +0 -0
  208. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/quorum.py +0 -0
  209. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/ratelimit.py +0 -0
  210. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reasoning.py +0 -0
  211. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/receipts.py +0 -0
  212. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/recovery.py +0 -0
  213. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/registry.py +0 -0
  214. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation.py +0 -0
  215. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_aggregate.py +0 -0
  216. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_disputes.py +0 -0
  217. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_ledger.py +0 -0
  218. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_policy.py +0 -0
  219. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_portability.py +0 -0
  220. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/revocation.py +0 -0
  221. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/__init__.py +0 -0
  222. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/_signing.py +0 -0
  223. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/_verify.py +0 -0
  224. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/access.py +0 -0
  225. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/accumulator.py +0 -0
  226. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/blackbox.py +0 -0
  227. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/bundle.py +0 -0
  228. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/capability.py +0 -0
  229. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/conformance.py +0 -0
  230. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/consent.py +0 -0
  231. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/custody.py +0 -0
  232. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/dtn_revocation.py +0 -0
  233. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/edge_trust.py +0 -0
  234. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/embodiment.py +0 -0
  235. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/freshness.py +0 -0
  236. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/fusion.py +0 -0
  237. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/geoscope.py +0 -0
  238. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/halos.py +0 -0
  239. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/handover.py +0 -0
  240. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/handshake.py +0 -0
  241. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/hardware.py +0 -0
  242. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/identity.py +0 -0
  243. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/lease.py +0 -0
  244. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/lifecycle.py +0 -0
  245. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/liveness.py +0 -0
  246. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/localization.py +0 -0
  247. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/odd.py +0 -0
  248. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/orbital.py +0 -0
  249. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/passport.py +0 -0
  250. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/perception.py +0 -0
  251. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/perception_consensus.py +0 -0
  252. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/physical_quorum.py +0 -0
  253. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/pq.py +0 -0
  254. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/presence.py +0 -0
  255. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/provenance.py +0 -0
  256. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/quorum_trust.py +0 -0
  257. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/revocation.py +0 -0
  258. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/root_identity.py +0 -0
  259. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/safety_record.py +0 -0
  260. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/swarm.py +0 -0
  261. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/teleop.py +0 -0
  262. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/wear.py +0 -0
  263. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/root_of_trust.py +0 -0
  264. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/scan/__init__.py +0 -0
  265. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/scan/detector.py +0 -0
  266. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/scan/patterns.py +0 -0
  267. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/seal.py +0 -0
  268. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/shield/flight_recorder.py +0 -0
  269. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/signer.py +0 -0
  270. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/ssrf.py +0 -0
  271. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/status_list.py +0 -0
  272. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/status_list_fetcher.py +0 -0
  273. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/tool_registry.py +0 -0
  274. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transparency.py +0 -0
  275. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/__init__.py +0 -0
  276. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/base.py +0 -0
  277. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/did_key.py +0 -0
  278. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/envelope.py +0 -0
  279. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/http_rendezvous.py +0 -0
  280. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/http_transport.py +0 -0
  281. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/manager.py +0 -0
  282. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/rendezvous.py +0 -0
  283. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/udna.py +0 -0
  284. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/trifecta.py +0 -0
  285. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/trust_check.py +0 -0
  286. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/trust_entropy.py +0 -0
  287. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/validator_server.py +0 -0
  288. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/vc.py +0 -0
  289. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/verification.py +0 -0
  290. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/verifier.py +0 -0
  291. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/dependency_links.txt +0 -0
  292. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/entry_points.txt +0 -0
  293. {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vouch-protocol
3
- Version: 2.1.0
3
+ Version: 2.2.1
4
4
  Summary: The Identity & Reputation Standard for AI Agents
5
5
  Author-email: Vouch Protocol Contributors <hello@vouch-protocol.com>
6
6
  License: Apache License 2.0
@@ -224,7 +224,7 @@ Requires-Dist: python-dateutil
224
224
  Requires-Dist: cryptography
225
225
  Requires-Dist: pydantic>=2.0.0
226
226
  Requires-Dist: httpx>=0.24.0
227
- Requires-Dist: pqcrypto>=0.4.0
227
+ Requires-Dist: pqcrypto<2.0.0,>=0.4.0
228
228
  Provides-Extra: langchain
229
229
  Requires-Dist: langchain>=0.1.0; extra == "langchain"
230
230
  Provides-Extra: crewai
@@ -387,7 +387,7 @@ Vouch is not one tool, it is a set of them. Here is the whole map.
387
387
  - **Continuous trust** heartbeats and session vouchers, so trust is a live signal that has to be renewed, not a badge that is issued once and trusted forever.
388
388
  - **Authority freshness** a signed authority state with a counter that only goes up, so a state change (a fraud signal, a suspended mandate) collapses the acceptable freshness window to now for a high-consequence action, even when time-decay trust still passes. See [docs/authority-freshness.md](docs/authority-freshness.md).
389
389
 
390
- ### Framework integrations (new in v1.6.2)
390
+ ### Framework integrations
391
391
  Standalone packages that drop Vouch into the agent framework you already use. Each one issues a verifiable credential for a tool call, with optional delegation back to a human principal.
392
392
  - **`vouch-langchain`** a LangChain tool that signs each tool call before it leaves the agent.
393
393
  - **`vouch-langgraph`** signs LangGraph tool calls and graph nodes, so a signed trail runs across the whole graph.
@@ -397,7 +397,7 @@ Standalone packages that drop Vouch into the agent framework you already use. Ea
397
397
  - **`vouch-mlflow`** signs an MLflow model artifact at registration time, bound to a content digest so any change to the weights breaks the signature.
398
398
  - **`vouch-safetensors`** embeds a credential in a `.safetensors` header, complementary to OpenSSF Model Signing, so a model carries who produced it.
399
399
 
400
- The standalone **`vouch-mcp`** package above ships alongside these in v1.6.2.
400
+ The standalone **`vouch-mcp`** package above ships alongside these.
401
401
 
402
402
  ### SDKs, in the language you use
403
403
  Python, TypeScript, and Go are the full reference implementations. A Rust core with idiomatic Swift, JVM (Java and Kotlin), .NET, and C wrappers shares one codebase, so every language produces byte-identical output, verified against shared test vectors. A WebAssembly build is included for the browser and the edge. See the table further down for status per language.
@@ -97,7 +97,7 @@ Vouch is not one tool, it is a set of them. Here is the whole map.
97
97
  - **Continuous trust** heartbeats and session vouchers, so trust is a live signal that has to be renewed, not a badge that is issued once and trusted forever.
98
98
  - **Authority freshness** a signed authority state with a counter that only goes up, so a state change (a fraud signal, a suspended mandate) collapses the acceptable freshness window to now for a high-consequence action, even when time-decay trust still passes. See [docs/authority-freshness.md](docs/authority-freshness.md).
99
99
 
100
- ### Framework integrations (new in v1.6.2)
100
+ ### Framework integrations
101
101
  Standalone packages that drop Vouch into the agent framework you already use. Each one issues a verifiable credential for a tool call, with optional delegation back to a human principal.
102
102
  - **`vouch-langchain`** a LangChain tool that signs each tool call before it leaves the agent.
103
103
  - **`vouch-langgraph`** signs LangGraph tool calls and graph nodes, so a signed trail runs across the whole graph.
@@ -107,7 +107,7 @@ Standalone packages that drop Vouch into the agent framework you already use. Ea
107
107
  - **`vouch-mlflow`** signs an MLflow model artifact at registration time, bound to a content digest so any change to the weights breaks the signature.
108
108
  - **`vouch-safetensors`** embeds a credential in a `.safetensors` header, complementary to OpenSSF Model Signing, so a model carries who produced it.
109
109
 
110
- The standalone **`vouch-mcp`** package above ships alongside these in v1.6.2.
110
+ The standalone **`vouch-mcp`** package above ships alongside these.
111
111
 
112
112
  ### SDKs, in the language you use
113
113
  Python, TypeScript, and Go are the full reference implementations. A Rust core with idiomatic Swift, JVM (Java and Kotlin), .NET, and C wrappers shares one codebase, so every language produces byte-identical output, verified against shared test vectors. A WebAssembly build is included for the browser and the edge. See the table further down for status per language.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vouch-protocol"
7
- version = "2.1.0"
7
+ version = "2.2.1"
8
8
  description = "The Identity & Reputation Standard for AI Agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -37,7 +37,11 @@ dependencies = [
37
37
  # credential. Bundled by default: pqcrypto ships
38
38
  # prebuilt wheels for all mainstream platforms (~80 KB, no compiler needed)
39
39
  # and the PQ story is core to Vouch, so we do not gate it behind an extra.
40
- "pqcrypto>=0.4.0",
40
+ # 0.4.x and 1.0.x are both supported: they differ in the ML-DSA-44 entry
41
+ # point names and in how verify() reports a verdict, and
42
+ # vouch/data_integrity_hybrid.py handles both. The major version is capped
43
+ # so a future release cannot change those contracts underneath a verifier.
44
+ "pqcrypto>=0.4.0,<2.0.0",
41
45
  ]
42
46
 
43
47
  [project.optional-dependencies]
@@ -502,7 +502,9 @@ def test_composite_proof_value_is_over_the_legacy_digest():
502
502
  digest = data_integrity.legacy_proof_digest(cred, unsigned)
503
503
  assert len(digest) == 32
504
504
  ed_pub.verify(combined[: data_integrity_hybrid.ED25519_SIGNATURE_SIZE], digest)
505
- assert pqcrypto.verify(ml_pub, digest, combined[data_integrity_hybrid.ED25519_SIGNATURE_SIZE :])
505
+ assert data_integrity_hybrid._mldsa44_verify(
506
+ pqcrypto, ml_pub, digest, combined[data_integrity_hybrid.ED25519_SIGNATURE_SIZE :]
507
+ )
506
508
 
507
509
 
508
510
  # ---------------------------------------------------------------------------
@@ -599,3 +601,97 @@ def test_verify_dual_rejects_a_set_missing_the_classical_proof():
599
601
  )
600
602
  is False
601
603
  )
604
+
605
+
606
+ # ---------------------------------------------------------------------------
607
+ # pqcrypto API contracts: 0.4.x and 1.0.0 differ in how they report a verdict
608
+ # ---------------------------------------------------------------------------
609
+
610
+
611
+ class _BoolVerify:
612
+ """The pqcrypto 0.4.x contract: verify() returns a bool."""
613
+
614
+ def __init__(self, verdict):
615
+ self.verdict = verdict
616
+
617
+ def verify(self, public_key, message, signature):
618
+ return self.verdict
619
+
620
+
621
+ class _RaisingVerify:
622
+ """The pqcrypto 1.0.0 contract: verify() returns None on success and
623
+ raises on failure."""
624
+
625
+ def __init__(self, error=None):
626
+ self.error = error
627
+
628
+ def verify(self, public_key, message, signature):
629
+ if self.error is not None:
630
+ raise self.error
631
+ return None
632
+
633
+
634
+ def test_verify_reads_the_bool_returning_contract():
635
+ assert data_integrity_hybrid._mldsa44_verify(_BoolVerify(True), b"pub", b"msg", b"sig") is True
636
+ assert (
637
+ data_integrity_hybrid._mldsa44_verify(_BoolVerify(False), b"pub", b"msg", b"sig") is False
638
+ )
639
+
640
+
641
+ def test_verify_reads_the_raising_contract():
642
+ """A None return means success under pqcrypto 1.0.0. Reading it as a bool
643
+ would reject every valid post-quantum proof."""
644
+ assert data_integrity_hybrid._mldsa44_verify(_RaisingVerify(), b"pub", b"msg", b"sig") is True
645
+
646
+
647
+ def test_verify_rejects_when_the_library_raises():
648
+ for error in (
649
+ ValueError("signature verification failed"),
650
+ RuntimeError("truncated input"),
651
+ TypeError("wrong key type"),
652
+ ):
653
+ assert (
654
+ data_integrity_hybrid._mldsa44_verify(_RaisingVerify(error), b"pub", b"msg", b"sig")
655
+ is False
656
+ )
657
+
658
+
659
+ def test_keygen_accepts_either_entry_point_name():
660
+ """pqcrypto 1.0.0 renamed generate_keypair() to keygen()."""
661
+
662
+ class _Keygen:
663
+ def keygen(self):
664
+ return b"pub-new", b"sec-new"
665
+
666
+ class _GenerateKeypair:
667
+ def generate_keypair(self):
668
+ return b"pub-old", b"sec-old"
669
+
670
+ class _Neither:
671
+ pass
672
+
673
+ assert data_integrity_hybrid._mldsa44_keygen(_Keygen()) == (b"pub-new", b"sec-new")
674
+ assert data_integrity_hybrid._mldsa44_keygen(_GenerateKeypair()) == (b"pub-old", b"sec-old")
675
+ with pytest.raises(RuntimeError):
676
+ data_integrity_hybrid._mldsa44_keygen(_Neither())
677
+
678
+
679
+ def test_a_tampered_signature_is_rejected_under_the_installed_library():
680
+ """End-to-end against whichever pqcrypto is actually installed: a real
681
+ signature verifies and a flipped byte does not."""
682
+ ml_pub, ml_sec = data_integrity_hybrid.generate_mldsa44_keypair()
683
+ message = b"vouch protocol ml-dsa-44 contract check"
684
+ signature = pqcrypto.sign(ml_sec, message)
685
+
686
+ assert data_integrity_hybrid._mldsa44_verify(pqcrypto, ml_pub, message, signature) is True
687
+
688
+ tampered = bytearray(signature)
689
+ tampered[0] ^= 0x01
690
+ assert (
691
+ data_integrity_hybrid._mldsa44_verify(pqcrypto, ml_pub, message, bytes(tampered)) is False
692
+ )
693
+ assert (
694
+ data_integrity_hybrid._mldsa44_verify(pqcrypto, ml_pub, b"other message", signature)
695
+ is False
696
+ )
697
+ assert data_integrity_hybrid._mldsa44_verify(pqcrypto, ml_pub, message, b"short") is False
@@ -0,0 +1,139 @@
1
+ """vouch-mcp consults Shield before it signs.
2
+
3
+ A credential is a statement that this agent authorised an action. If policy says
4
+ it may not, there is nothing to attest, so no credential is issued. That makes
5
+ the signer the first of two refusal points, the second being the tool server
6
+ that receives the credential.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import json
13
+
14
+ import pytest
15
+
16
+ pytest.importorskip("mcp", reason="MCP SDK not installed")
17
+
18
+ from vouch import multikey # noqa: E402
19
+ from vouch.integrations.mcp import server as notary # noqa: E402
20
+ from vouch.autosign import reset_default_signer # noqa: E402
21
+ from vouch.keys import generate_identity # noqa: E402
22
+
23
+ TARGET = "files"
24
+
25
+
26
+ @pytest.fixture
27
+ def notary_env(tmp_path, monkeypatch):
28
+ keypair = generate_identity()
29
+ raw = base64.urlsafe_b64decode(json.loads(keypair.public_key_jwk)["x"] + "==")
30
+ did = "did:key:" + multikey.encode_ed25519_public(raw)
31
+
32
+ rules = tmp_path / "rules.json"
33
+ rules.write_text(
34
+ json.dumps(
35
+ {
36
+ "version": 2,
37
+ "rules": [
38
+ {
39
+ "did": did,
40
+ "allow": [
41
+ {
42
+ "id": "read-reports",
43
+ "action": "read_file",
44
+ "target": TARGET,
45
+ "resource": "reports/**",
46
+ }
47
+ ],
48
+ }
49
+ ],
50
+ "deny_default": True,
51
+ }
52
+ )
53
+ )
54
+
55
+ monkeypatch.setenv("VOUCH_DID", did)
56
+ monkeypatch.setenv("VOUCH_PRIVATE_KEY", keypair.private_key_jwk)
57
+ monkeypatch.setattr(notary, "_RULES_PATH", str(rules))
58
+ notary._shield_cache.clear()
59
+ # resolve_signer() memoises the process-wide default signer, so a stale one
60
+ # from an earlier test would sign under the wrong DID.
61
+ reset_default_signer()
62
+ yield {"did": did}
63
+ notary._shield_cache.clear()
64
+ reset_default_signer()
65
+
66
+
67
+ def test_sign_issues_a_credential_for_a_permitted_intent(notary_env):
68
+ out = json.loads(notary.sign(action="read_file", target=TARGET, resource="reports/q3.txt"))
69
+
70
+ assert out.get("error") is None
71
+ assert out["credentialSubject"]["intent"]["resource"] == "reports/q3.txt"
72
+ assert out["proof"]["cryptosuite"] == "eddsa-jcs-2022"
73
+
74
+
75
+ def test_sign_refuses_a_denied_action_with_a_structured_refusal(notary_env):
76
+ """Nothing to present: the notary will not stamp it."""
77
+ out = json.loads(notary.sign(action="delete_file", target=TARGET, resource="reports/q3.txt"))
78
+
79
+ assert out["error"] == "refused"
80
+ assert out["reason"] == "no matching rule"
81
+ assert out["action"] == "delete_file"
82
+ assert "proof" not in out
83
+
84
+
85
+ def test_sign_refuses_a_resource_outside_scope(notary_env):
86
+ out = json.loads(notary.sign(action="read_file", target=TARGET, resource="secrets/keys.txt"))
87
+
88
+ assert out["error"] == "refused"
89
+ assert out["reason"] == "resource outside scope"
90
+
91
+
92
+ def test_sign_refuses_traversal(notary_env):
93
+ out = json.loads(
94
+ notary.sign(action="read_file", target=TARGET, resource="reports/../secrets/keys.txt")
95
+ )
96
+ assert out["error"] == "refused"
97
+ assert out["reason"] == "resource outside scope"
98
+
99
+
100
+ def test_sign_refuses_everything_without_a_rules_file(monkeypatch, tmp_path):
101
+ keypair = generate_identity()
102
+ raw = base64.urlsafe_b64decode(json.loads(keypair.public_key_jwk)["x"] + "==")
103
+ did = "did:key:" + multikey.encode_ed25519_public(raw)
104
+ monkeypatch.setenv("VOUCH_DID", did)
105
+ monkeypatch.setenv("VOUCH_PRIVATE_KEY", keypair.private_key_jwk)
106
+ monkeypatch.setattr(notary, "_RULES_PATH", None)
107
+ notary._shield_cache.clear()
108
+ reset_default_signer()
109
+
110
+ out = json.loads(notary.sign(action="read_file", target=TARGET, resource="reports/q3.txt"))
111
+ assert out["error"] == "refused"
112
+ assert out["reason"] == "unknown did"
113
+ notary._shield_cache.clear()
114
+ reset_default_signer()
115
+
116
+
117
+ def test_check_action_reports_the_decision_and_its_reason(notary_env):
118
+ did = notary_env["did"]
119
+
120
+ allowed = notary.check_action(
121
+ action="read_file", target=TARGET, resource="reports/q3.txt", did=did
122
+ )
123
+ assert allowed.startswith("ALLOW")
124
+ assert "read-reports" in allowed
125
+
126
+ outside = notary.check_action(
127
+ action="read_file", target=TARGET, resource="secrets/keys.txt", did=did
128
+ )
129
+ assert outside == "DENY: resource outside scope"
130
+
131
+ wrong_action = notary.check_action(
132
+ action="delete_file", target=TARGET, resource="reports/q3.txt", did=did
133
+ )
134
+ assert wrong_action == "DENY: no matching rule"
135
+
136
+ stranger = notary.check_action(
137
+ action="read_file", target=TARGET, resource="reports/q3.txt", did="did:key:z6MkStranger"
138
+ )
139
+ assert stranger == "DENY: unknown did"
@@ -0,0 +1,356 @@
1
+ """Shield v2: action / target / resource rules with globs on resource.
2
+
3
+ The property under test throughout is that nothing is permitted unless a rule
4
+ says so, and that a rule says only what it appears to say.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+
11
+ import pytest
12
+
13
+ from vouch.shield import Shield, ShieldConfig
14
+ from vouch.shield.rules import (
15
+ Decision,
16
+ load_rules,
17
+ load_rules_file,
18
+ normalize_pattern,
19
+ normalize_resource,
20
+ resource_matches,
21
+ )
22
+
23
+ DID = "did:web:agent.example.com"
24
+ OTHER = "did:web:someone-else.example.com"
25
+
26
+
27
+ def doc(*allow, did=DID):
28
+ return {"version": 2, "rules": [{"did": did, "allow": list(allow)}], "deny_default": True}
29
+
30
+
31
+ def rule(action="read_file", target="filesystem", resource="reports/**", **kw):
32
+ return {"action": action, "target": target, "resource": resource, **kw}
33
+
34
+
35
+ @pytest.fixture
36
+ def rules():
37
+ return load_rules(doc(rule(id="reports-read")))
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # The three fields
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ def test_allows_when_all_three_fields_match(rules):
46
+ decision = rules.check(DID, "read_file", "filesystem", "reports/q3.txt")
47
+ assert decision.allow
48
+ assert decision.reason == "allowed"
49
+ assert decision.rule_id == "reports-read"
50
+
51
+
52
+ def test_denies_on_action_mismatch(rules):
53
+ decision = rules.check(DID, "write_file", "filesystem", "reports/q3.txt")
54
+ assert not decision.allow
55
+ assert decision.reason == "no matching rule"
56
+
57
+
58
+ def test_denies_on_target_mismatch(rules):
59
+ decision = rules.check(DID, "read_file", "network", "reports/q3.txt")
60
+ assert not decision.allow
61
+ assert decision.reason == "no matching rule"
62
+
63
+
64
+ def test_denies_on_resource_mismatch_with_a_distinct_reason(rules):
65
+ """'you may not do this' and 'not there' are different things to say."""
66
+ decision = rules.check(DID, "read_file", "filesystem", "secrets/keys.txt")
67
+ assert not decision.allow
68
+ assert decision.reason == "resource outside scope"
69
+
70
+
71
+ def test_unknown_did_is_denied(rules):
72
+ decision = rules.check(OTHER, "read_file", "filesystem", "reports/q3.txt")
73
+ assert not decision.allow
74
+ assert decision.reason == "unknown did"
75
+
76
+
77
+ def test_action_and_target_are_case_sensitive(rules):
78
+ assert not rules.check(DID, "READ_FILE", "filesystem", "reports/q3.txt").allow
79
+ assert not rules.check(DID, "read_file", "Filesystem", "reports/q3.txt").allow
80
+
81
+
82
+ def test_decision_is_falsy_when_denied(rules):
83
+ assert not rules.check(DID, "read_file", "filesystem", "secrets/x")
84
+ assert rules.check(DID, "read_file", "filesystem", "reports/x")
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Glob depth semantics
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ @pytest.mark.parametrize(
93
+ "pattern,resource,expected",
94
+ [
95
+ # '**' is any depth, including the bare prefix.
96
+ ("reports/**", "reports", True),
97
+ ("reports/**", "reports/q3.txt", True),
98
+ ("reports/**", "reports/2026/q3.txt", True),
99
+ ("reports/**", "reports/a/b/c/d", True),
100
+ ("reports/**", "secrets/keys.txt", False),
101
+ # A prefix must match a whole segment, not a substring.
102
+ ("reports/**", "reportsX/a", False),
103
+ # '*' is exactly one segment.
104
+ ("reports/*", "reports/q3.txt", True),
105
+ ("reports/*", "reports/2026/q3.txt", False),
106
+ ("reports/*", "reports", False),
107
+ # '**' in the middle.
108
+ ("a/**/b", "a/b", True),
109
+ ("a/**/b", "a/x/b", True),
110
+ ("a/**/b", "a/x/y/b", True),
111
+ ("a/**/b", "a/x/c", False),
112
+ # Everything.
113
+ ("**", "anything/at/all", True),
114
+ ("**", "single", True),
115
+ # A resource with no '/' is one segment, which is what makes a
116
+ # non-path resource such as a table name work.
117
+ ("public.*", "public.customers", True),
118
+ ("public.*", "internal.salaries", False),
119
+ # '.' is literal, not a regex wildcard.
120
+ ("a.b", "axb", False),
121
+ ("a.b", "a.b", True),
122
+ ],
123
+ )
124
+ def test_glob_depth_semantics(pattern, resource, expected):
125
+ assert resource_matches(normalize_pattern(pattern), normalize_resource(resource)) is expected
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Normalisation and traversal
130
+ # ---------------------------------------------------------------------------
131
+
132
+
133
+ @pytest.mark.parametrize(
134
+ "raw,expected",
135
+ [
136
+ ("reports/q3.txt", "reports/q3.txt"),
137
+ ("/reports/q3.txt", "reports/q3.txt"),
138
+ ("reports//q3.txt", "reports/q3.txt"),
139
+ ("reports/./q3.txt", "reports/q3.txt"),
140
+ ("reports/", "reports"),
141
+ ("a/b/../c", "a/c"),
142
+ # Escapes the root: refused rather than clamped.
143
+ ("../x", None),
144
+ ("..", None),
145
+ ("reports/../..", None),
146
+ ("", None),
147
+ ("a\x00b", None),
148
+ ("a\x1fb", None),
149
+ ],
150
+ )
151
+ def test_normalisation(raw, expected):
152
+ assert normalize_resource(raw) == expected
153
+
154
+
155
+ def test_traversal_cannot_reach_outside_the_glob():
156
+ """The case the whole normalisation step exists for."""
157
+ rules = load_rules(doc(rule(resource="reports/**")))
158
+ decision = rules.check(DID, "read_file", "filesystem", "reports/../etc/passwd")
159
+ assert not decision.allow
160
+ assert decision.reason == "resource outside scope"
161
+
162
+
163
+ def test_leading_slash_and_double_slash_do_not_change_the_verdict():
164
+ rules = load_rules(doc(rule(resource="reports/**")))
165
+ for variant in ("reports/q3.txt", "/reports/q3.txt", "reports//q3.txt", "reports/./q3.txt"):
166
+ assert rules.check(DID, "read_file", "filesystem", variant).allow, variant
167
+
168
+
169
+ def test_control_characters_are_refused_with_their_own_reason():
170
+ rules = load_rules(doc(rule()))
171
+ decision = rules.check(DID, "read_file", "filesystem", "reports/\x00etc")
172
+ assert not decision.allow
173
+ assert decision.reason == "invalid resource"
174
+
175
+
176
+ def test_a_rule_pattern_is_normalised_too():
177
+ """'/reports/**' and 'reports/**' mean the same thing."""
178
+ rules = load_rules(doc(rule(resource="/reports/**")))
179
+ assert rules.check(DID, "read_file", "filesystem", "reports/q3.txt").allow
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Fail closed
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ def test_empty_rules_denies_everything():
188
+ rules = load_rules({"version": 2, "rules": [], "deny_default": True})
189
+ assert rules.ok
190
+ assert not rules.check(DID, "read_file", "filesystem", "reports/q3.txt").allow
191
+
192
+
193
+ def test_no_rules_key_denies_everything():
194
+ rules = load_rules({"version": 2, "deny_default": True})
195
+ assert not rules.check(DID, "read_file", "filesystem", "reports/q3.txt").allow
196
+
197
+
198
+ @pytest.mark.parametrize(
199
+ "document",
200
+ [
201
+ {"rules": []}, # no version
202
+ {"version": 1, "rules": []}, # v1
203
+ {"version": 2, "rules": {}}, # rules not a list
204
+ {"version": 2, "rules": [{"did": DID}]}, # no allow
205
+ {"version": 2, "rules": [{"did": "", "allow": []}]},
206
+ {"version": 2, "rules": [{"allow": []}]}, # no did
207
+ {"version": 2, "rules": [{"did": DID, "allow": [{"action": "a", "target": "b"}]}]},
208
+ {"version": 2, "rules": [{"did": DID, "allow": [{}]}]},
209
+ {"version": 2, "rules": [], "deny_default": False},
210
+ "not a mapping",
211
+ None,
212
+ ],
213
+ )
214
+ def test_malformed_rules_deny_everything(document, caplog):
215
+ rules = load_rules(document)
216
+ assert not rules.ok
217
+ decision = rules.check(DID, "read_file", "filesystem", "reports/q3.txt")
218
+ assert not decision.allow
219
+ assert decision.reason == "malformed rules"
220
+
221
+
222
+ def test_malformed_rules_are_logged(caplog):
223
+ with caplog.at_level("ERROR"):
224
+ load_rules({"version": 1, "rules": []})
225
+ assert any("denying all calls" in r.getMessage() for r in caplog.records)
226
+
227
+
228
+ def test_a_typo_in_a_rule_key_is_rejected_not_ignored():
229
+ """'resourse' must not become an allow-anything rule."""
230
+ document = {
231
+ "version": 2,
232
+ "rules": [{"did": DID, "allow": [{"action": "a", "target": "b", "resourse": "x"}]}],
233
+ "deny_default": True,
234
+ }
235
+ rules = load_rules(document)
236
+ assert not rules.ok
237
+ assert not rules.check(DID, "a", "b", "anything").allow
238
+
239
+
240
+ def test_unknown_block_key_is_rejected():
241
+ document = {
242
+ "version": 2,
243
+ "rules": [{"did": DID, "allow": [], "deny": [rule()]}],
244
+ "deny_default": True,
245
+ }
246
+ assert not load_rules(document).ok
247
+
248
+
249
+ @pytest.mark.parametrize("bad", ["a/**b", "a/../b", "a/./b", "", "**b"])
250
+ def test_malformed_resource_patterns_are_rejected(bad):
251
+ assert not load_rules(doc(rule(resource=bad))).ok
252
+
253
+
254
+ def test_missing_rules_file_denies_everything(tmp_path):
255
+ rules = load_rules_file(tmp_path / "nope.yaml")
256
+ assert not rules.ok
257
+ assert rules.check(DID, "read_file", "filesystem", "x").reason == "malformed rules"
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # File loading
262
+ # ---------------------------------------------------------------------------
263
+
264
+
265
+ def test_loads_json_rules(tmp_path):
266
+ path = tmp_path / "rules.json"
267
+ path.write_text(json.dumps(doc(rule())))
268
+ assert load_rules_file(path).check(DID, "read_file", "filesystem", "reports/x").allow
269
+
270
+
271
+ def test_loads_yaml_rules(tmp_path):
272
+ pytest.importorskip("yaml")
273
+ path = tmp_path / "rules.yaml"
274
+ path.write_text(
275
+ "version: 2\n"
276
+ "rules:\n"
277
+ f" - did: {DID}\n"
278
+ " allow:\n"
279
+ ' - {action: read_file, target: filesystem, resource: "reports/**"}\n'
280
+ "deny_default: true\n"
281
+ )
282
+ rules = load_rules_file(path)
283
+ assert rules.ok
284
+ assert rules.check(DID, "read_file", "filesystem", "reports/q3.txt").allow
285
+ assert not rules.check(DID, "read_file", "filesystem", "secrets/x").allow
286
+
287
+
288
+ def test_invalid_json_file_denies_everything(tmp_path):
289
+ path = tmp_path / "rules.json"
290
+ path.write_text("{not json")
291
+ assert not load_rules_file(path).ok
292
+
293
+
294
+ def test_rule_ids_are_assigned_when_absent():
295
+ rules = load_rules(doc(rule(), rule(action="list_dir")))
296
+ decision = rules.check(DID, "list_dir", "filesystem", "reports/x")
297
+ assert decision.allow
298
+ assert decision.rule_id == f"{DID}#1"
299
+
300
+
301
+ def test_several_rules_for_one_did_are_all_considered():
302
+ rules = load_rules(doc(rule(), rule(action="list_dir", resource="logs/**")))
303
+ assert rules.check(DID, "read_file", "filesystem", "reports/x").allow
304
+ assert rules.check(DID, "list_dir", "filesystem", "logs/a/b").allow
305
+ assert not rules.check(DID, "list_dir", "filesystem", "reports/x").allow
306
+
307
+
308
+ # ---------------------------------------------------------------------------
309
+ # The Shield facade
310
+ # ---------------------------------------------------------------------------
311
+
312
+
313
+ def test_shield_with_no_rules_path_denies_everything():
314
+ shield = Shield(ShieldConfig(require_signature=False))
315
+ assert not shield.check(DID, "read_file", "filesystem", "reports/q3.txt").allow
316
+
317
+
318
+ def test_shield_check_from_a_file(tmp_path):
319
+ path = tmp_path / "rules.json"
320
+ path.write_text(json.dumps(doc(rule())))
321
+ shield = Shield(ShieldConfig(rules_path=str(path), require_signature=False))
322
+
323
+ assert shield.check(DID, "read_file", "filesystem", "reports/q3.txt").allow
324
+ assert not shield.check(DID, "read_file", "filesystem", "secrets/k").allow
325
+
326
+
327
+ def test_shield_allow_adds_an_in_memory_rule():
328
+ shield = Shield(ShieldConfig(require_signature=False))
329
+ shield.allow(DID, action="read_file", target="filesystem", resource="reports/**")
330
+
331
+ assert shield.check(DID, "read_file", "filesystem", "reports/q3.txt").allow
332
+ assert not shield.check(DID, "read_file", "filesystem", "etc/passwd").allow
333
+ assert len(shield.rules_for(DID)) == 1
334
+
335
+
336
+ def test_shield_intercept_defaults_resource_to_the_arguments():
337
+ """Without an explicit resource, a decision binds to these exact arguments."""
338
+ from vouch import jcs
339
+
340
+ shield = Shield(ShieldConfig(require_signature=False, default_target="tool"))
341
+ shield.trust_did(DID)
342
+ args = {"path": "reports/q3.txt"}
343
+ shield.allow(DID, action="read_file", target="tool", resource=jcs.canonicalize_str(args))
344
+
345
+ assert shield.intercept(tool="read_file", args=args, did=DID).allowed
346
+ # Different arguments, so a different resource, so no match.
347
+ assert not shield.intercept(tool="read_file", args={"path": "reports/q4.txt"}, did=DID).allowed
348
+
349
+
350
+ def test_decision_is_immutable():
351
+ """A decision is a verdict, not a mutable flag someone can flip later."""
352
+ import dataclasses
353
+
354
+ decision = Decision(True, "allowed", "r1")
355
+ with pytest.raises(dataclasses.FrozenInstanceError):
356
+ decision.allow = False # type: ignore[misc]