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.
- {vouch_protocol-2.1.0/vouch_protocol.egg-info → vouch_protocol-2.2.1}/PKG-INFO +4 -4
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/README.md +2 -2
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/pyproject.toml +6 -2
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_hybrid.py +97 -1
- vouch_protocol-2.2.1/tests/test_mcp_server_shield.py +139 -0
- vouch_protocol-2.2.1/tests/test_shield_rules.py +356 -0
- vouch_protocol-2.2.1/tests/test_shield_vectors.py +90 -0
- vouch_protocol-2.2.1/tests/test_threshold_loader.py +132 -0
- vouch_protocol-2.2.1/tests/test_vouch_mcp.py +468 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/__init__.py +778 -778
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/data_integrity_hybrid.py +32 -4
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/mcp/server.py +194 -32
- vouch_protocol-2.2.1/vouch/mcp/__init__.py +269 -0
- vouch_protocol-2.2.1/vouch/mcp/_guard.py +420 -0
- vouch_protocol-2.2.1/vouch/shield/__init__.py +55 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/shield/demo.py +19 -16
- vouch_protocol-2.2.1/vouch/shield/rules.py +413 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/shield/shield.py +134 -32
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/shield/trust_registry.py +42 -28
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/threshold.py +34 -9
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1/vouch_protocol.egg-info}/PKG-INFO +4 -4
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/SOURCES.txt +8 -1
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/requires.txt +1 -1
- vouch_protocol-2.1.0/vouch/shield/__init__.py +0 -34
- vouch_protocol-2.1.0/vouch/shield/permissions.py +0 -224
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/LICENSE +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/MANIFEST.in +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/setup.cfg +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_accountability.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_agent_enroll.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_attribution.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_audio_bridge.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_audit_trail.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_auditor.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_authority_state.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_authority_state_vectors.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_autosign.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_behavioral_attestation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_bridge.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_budget.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_c2pa_formats.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_cache.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_canary.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_caveats.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_cli_init.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_cli_root.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_conformance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_credential_gate.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_data_integrity.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_delegation_attenuation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_deliberation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_disconnected_edge.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_dtn_pads.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_dx_sugar.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_examples_robotics.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_fastapi_credential_gate.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_fleet.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_git_workflow.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_grade.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_halos_evidence.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_hardware_seam.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_hasura_integration.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_heartbeat.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_hybrid_interop.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_intent_recheck.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_jcs.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_jcs_interop.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_liveness_conformance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_media_badge.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_media_native.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_merkle.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_mint_contributor_credential.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_multikey.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_nonce.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_orbital_accumulator.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_proof_of_integration.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_provenance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_quorum.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reasoning.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_receipts.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_recovery.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_aggregate.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_disputes.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_ledger.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_policy.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_reputation_portability.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_revocation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_access.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_conformance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_consent.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_custody.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_embodiment.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_fusion.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_handover.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_handshake_blackbox_passport.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_identity.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_lease_quorum.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_lifecycle.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_liveness_revocation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_odd.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_perception.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_pq.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_provenance_capability.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_root_identity.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_safety_record.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_swarm.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_teleop.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_robot_wear.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_root_of_trust.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_root_of_trust_vectors.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_ros2_action_gate.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_runtime_vectors.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_scan.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_secure_key_custody.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_security_hardening.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_shield_guard.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_signer_vc.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_status_list.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_threshold.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_tool_registry.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transparency.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transport.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transport_http_rendezvous.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transport_rendezvous.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_transport_udna_sdk.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_trifecta.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_trust_check.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_trust_entropy.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_validator_server.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_vc.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/tests/test_verifier_vc.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/accountability.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/agent.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/async_verifier.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/attenuation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/attribution.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/attribution_cli.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/audio.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/audit_trail.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/auditor.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/authority_state.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/autosign.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/behavioral_attestation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/bridge/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/bridge/audio_routes.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/bridge/config.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/bridge/server.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/budget.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/cache.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/canary.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/caveats.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/cli.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/config.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/conformance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/credential.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/data_integrity.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/deliberation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/did_web.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/fleet.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/gate.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/grade.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/heartbeat.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/_common.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/a2a.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/adk.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/amnesia.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/autogen/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/autogpt/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/autogpt/commands.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/claude-code/settings.hooks.json +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/crewai/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/crewai/tool.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/fastapi.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/google.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/goose.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/hasura/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/hasura/webhook.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/langchain/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/langchain/tool.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/langgraph.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/mcp/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/mlflow.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/n8n.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/openai.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/safetensors.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/streamlit/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/streamlit/seal.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/integrations/vertex_ai/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/intent_recheck.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/jcs.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/keys.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/keystore.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/kms.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/liveness_conformance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/mcp_guard.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/media/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/media/badge.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/media/c2pa.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/media/native.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/merkle.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/metrics.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/multikey.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/nonce.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/onboard.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/proof_of_integration.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/provenance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/quorum.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/ratelimit.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reasoning.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/receipts.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/recovery.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/registry.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_aggregate.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_disputes.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_ledger.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_policy.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/reputation_portability.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/revocation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/_signing.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/_verify.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/access.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/accumulator.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/blackbox.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/bundle.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/capability.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/conformance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/consent.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/custody.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/dtn_revocation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/edge_trust.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/embodiment.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/freshness.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/fusion.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/geoscope.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/halos.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/handover.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/handshake.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/hardware.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/identity.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/lease.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/lifecycle.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/liveness.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/localization.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/odd.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/orbital.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/passport.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/perception.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/perception_consensus.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/physical_quorum.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/pq.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/presence.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/provenance.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/quorum_trust.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/revocation.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/root_identity.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/safety_record.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/swarm.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/teleop.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/robotics/wear.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/root_of_trust.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/scan/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/scan/detector.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/scan/patterns.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/seal.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/shield/flight_recorder.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/signer.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/ssrf.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/status_list.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/status_list_fetcher.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/tool_registry.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transparency.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/__init__.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/base.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/did_key.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/envelope.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/http_rendezvous.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/http_transport.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/manager.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/rendezvous.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/transport/udna.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/trifecta.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/trust_check.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/trust_entropy.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/validator_server.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/vc.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/verification.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch/verifier.py +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/dependency_links.txt +0 -0
- {vouch_protocol-2.1.0 → vouch_protocol-2.2.1}/vouch_protocol.egg-info/entry_points.txt +0 -0
- {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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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]
|