volidator-python 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: volidator-python
3
+ Version: 0.1.0
4
+ Summary: Zero-knowledge, server-blind audit logging for Python and agentic workflows.
5
+ License: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: cryptography>=3.4.0
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest>=7.0.0; extra == "test"
14
+ Requires-Dist: pytest-asyncio>=0.18.0; extra == "test"
15
+ Requires-Dist: httpx>=0.20.0; extra == "test"
16
+ Provides-Extra: langchain
17
+ Requires-Dist: langchain-core>=0.1.0; extra == "langchain"
18
+ Provides-Extra: llamaindex
19
+ Requires-Dist: llama-index-core>=0.10.0; extra == "llamaindex"
20
+
21
+ # Volidator Python SDK
22
+
23
+ **Zero-knowledge, server-blind audit logging for Python and autonomous AI agentic workflows.**
24
+
25
+ Volidator encrypts every log entry locally — on your server — before sending it. The Volidator backend stores only ciphertext and blind indexes. Your plaintext data never leaves your infrastructure unencrypted.
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install volidator-python
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Environment Setup
38
+
39
+ Generate a secure 32-byte encryption key (64 hex characters) with the `vol-dek-` prefix:
40
+ ```bash
41
+ python -c "import os; print('vol-dek-' + os.urandom(32).hex())"
42
+ ```
43
+
44
+ Add secrets to your environment:
45
+ ```bash
46
+ VOLIDATOR_API_KEY="val_live_xxxxxxxx..."
47
+ VOLIDATOR_ENCRYPTION_KEY="vol-dek-xxxxxxxx..."
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Quick Start
53
+
54
+ ```python
55
+ import os
56
+ from volidator import VolidatorClient
57
+
58
+ volidator = VolidatorClient(
59
+ api_key=os.environ["VOLIDATOR_API_KEY"],
60
+ encryption_key=os.environ["VOLIDATOR_ENCRYPTION_KEY"]
61
+ )
62
+
63
+ # Log a system event
64
+ volidator.log({
65
+ "actor": "usr_12345",
66
+ "action": "user.login.success",
67
+ "target": "workspace_abc789",
68
+ "metadata": {
69
+ "device": "Workstation",
70
+ "auth_provider": "Okta"
71
+ }
72
+ })
73
+ ```
74
+
75
+ ---
76
+
77
+ ## AI Agent Auditing (`VolidatorAgent`)
78
+
79
+ For auditing LLM chains, agent reasoning loops, and multi-agent handoffs, use the `volidator.agent` namespace:
80
+
81
+ ```python
82
+ # Log a decision made autonomously by the model
83
+ volidator.agent.decision({
84
+ "actor": "writer-agent-v1",
85
+ "traceId": "run-xyz-456",
86
+ "decision": "publish_article",
87
+ "rationale": "grammar check and safety alignments passed verification checks",
88
+ "confidenceScore": 0.98,
89
+ "modelId": "gpt-4o"
90
+ })
91
+
92
+ # Log a tool execution call
93
+ volidator.agent.tool_call({
94
+ "actor": "search-agent-v1",
95
+ "traceId": "run-xyz-456",
96
+ "toolName": "web_search",
97
+ "toolInput": {"query": "AI regulation"},
98
+ "toolOutput": {"results": [...]},
99
+ "success": True,
100
+ "latencyMs": 245
101
+ })
102
+ ```
@@ -0,0 +1,82 @@
1
+ # Volidator Python SDK
2
+
3
+ **Zero-knowledge, server-blind audit logging for Python and autonomous AI agentic workflows.**
4
+
5
+ Volidator encrypts every log entry locally — on your server — before sending it. The Volidator backend stores only ciphertext and blind indexes. Your plaintext data never leaves your infrastructure unencrypted.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install volidator-python
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Environment Setup
18
+
19
+ Generate a secure 32-byte encryption key (64 hex characters) with the `vol-dek-` prefix:
20
+ ```bash
21
+ python -c "import os; print('vol-dek-' + os.urandom(32).hex())"
22
+ ```
23
+
24
+ Add secrets to your environment:
25
+ ```bash
26
+ VOLIDATOR_API_KEY="val_live_xxxxxxxx..."
27
+ VOLIDATOR_ENCRYPTION_KEY="vol-dek-xxxxxxxx..."
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ import os
36
+ from volidator import VolidatorClient
37
+
38
+ volidator = VolidatorClient(
39
+ api_key=os.environ["VOLIDATOR_API_KEY"],
40
+ encryption_key=os.environ["VOLIDATOR_ENCRYPTION_KEY"]
41
+ )
42
+
43
+ # Log a system event
44
+ volidator.log({
45
+ "actor": "usr_12345",
46
+ "action": "user.login.success",
47
+ "target": "workspace_abc789",
48
+ "metadata": {
49
+ "device": "Workstation",
50
+ "auth_provider": "Okta"
51
+ }
52
+ })
53
+ ```
54
+
55
+ ---
56
+
57
+ ## AI Agent Auditing (`VolidatorAgent`)
58
+
59
+ For auditing LLM chains, agent reasoning loops, and multi-agent handoffs, use the `volidator.agent` namespace:
60
+
61
+ ```python
62
+ # Log a decision made autonomously by the model
63
+ volidator.agent.decision({
64
+ "actor": "writer-agent-v1",
65
+ "traceId": "run-xyz-456",
66
+ "decision": "publish_article",
67
+ "rationale": "grammar check and safety alignments passed verification checks",
68
+ "confidenceScore": 0.98,
69
+ "modelId": "gpt-4o"
70
+ })
71
+
72
+ # Log a tool execution call
73
+ volidator.agent.tool_call({
74
+ "actor": "search-agent-v1",
75
+ "traceId": "run-xyz-456",
76
+ "toolName": "web_search",
77
+ "toolInput": {"query": "AI regulation"},
78
+ "toolOutput": {"results": [...]},
79
+ "success": True,
80
+ "latencyMs": 245
81
+ })
82
+ ```
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "volidator-python"
7
+ version = "0.1.0"
8
+ description = "Zero-knowledge, server-blind audit logging for Python and agentic workflows."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: OS Independent",
16
+ ]
17
+ dependencies = [
18
+ "cryptography>=3.4.0",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ test = [
23
+ "pytest>=7.0.0",
24
+ "pytest-asyncio>=0.18.0",
25
+ "httpx>=0.20.0",
26
+ ]
27
+ langchain = [
28
+ "langchain-core>=0.1.0",
29
+ ]
30
+ llamaindex = [
31
+ "llama-index-core>=0.10.0",
32
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,158 @@
1
+ import asyncio
2
+ import sys
3
+ import time
4
+ from unittest.mock import MagicMock, patch
5
+ import pytest
6
+ from volidator.client import VolidatorClient, agent_context_store, logical_clock_store
7
+ from volidator.plugins.langchain import VolidatorLangChainHandler
8
+ from volidator.plugins.llamaindex import VolidatorLlamaIndexHandler
9
+
10
+
11
+ def test_prepare_log_entry():
12
+ client = VolidatorClient(
13
+ api_key="val_live_123",
14
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
15
+ redact_keys=["actor", "metadata.ssn"],
16
+ reference_keys=["target"]
17
+ )
18
+
19
+ payload = {
20
+ "actor": "sensitive_user_pii",
21
+ "action": "user.login",
22
+ "target": {"id": "workspace_999", "pii": "sensitive_workspace_name"},
23
+ "metadata": {
24
+ "ssn": "123-456-789",
25
+ "public_info": "hello"
26
+ }
27
+ }
28
+
29
+ entry = client.prepare_log_entry(payload)
30
+
31
+ # 1. Blind indexes should be computed from the plain raw PII values
32
+ active_hashed_key = client._get_hashed_key("v1")
33
+ expected_actor_blind = client.generate_blind_index("sensitive_user_pii", active_hashed_key)
34
+ expected_target_blind = client.generate_blind_index("sensitive_workspace_name", active_hashed_key)
35
+
36
+ assert entry["actorBlindIndex"] == expected_actor_blind
37
+ assert entry["targetBlindIndex"] == expected_target_blind
38
+
39
+ # 2. Decrypted payload should have redacted actor and JIT reference target
40
+ decrypted = client.decrypt_payload(entry["encryptedPayload"])
41
+ assert decrypted["actor"] == "[REDACTED:actor]"
42
+ assert decrypted["target"] == "[REF:workspace_999]"
43
+ assert decrypted["metadata"]["ssn"] == "[REDACTED:ssn]"
44
+ assert decrypted["metadata"]["public_info"] == "hello"
45
+
46
+
47
+ def test_contextvar_token_reset():
48
+ client = VolidatorClient(
49
+ api_key="val_live_123",
50
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
51
+ )
52
+
53
+ # Start clean
54
+ assert agent_context_store.get() is None
55
+
56
+ with client.run_in_agent_context({"traceId": "t-1", "toolName": "search"}):
57
+ assert agent_context_store.get() == {"traceId": "t-1", "toolName": "search"}
58
+
59
+ # Must reset back to None
60
+ assert agent_context_store.get() is None
61
+
62
+
63
+ def test_trace_from_langchain_reset():
64
+ client = VolidatorClient(
65
+ api_key="val_live_123",
66
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
67
+ )
68
+
69
+ assert agent_context_store.get() is None
70
+
71
+ langchain_config = {
72
+ "run_id": "lang-run-uuid-123",
73
+ "configurable": {"thread_id": "graph-thread-456"}
74
+ }
75
+
76
+ with client.trace_from_langchain(langchain_config):
77
+ # run_id has priority
78
+ assert agent_context_store.get() == {"traceId": "lang-run-uuid-123"}
79
+
80
+ assert agent_context_store.get() is None
81
+
82
+
83
+ def test_logical_clock_context():
84
+ client = VolidatorClient(
85
+ api_key="val_live_123",
86
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
87
+ )
88
+
89
+ assert logical_clock_store.get() is None
90
+
91
+ with client.run_in_clock_context(10):
92
+ assert logical_clock_store.get() == {"clock": 10}
93
+ clock1 = client.get_and_increment_clock()
94
+ assert clock1 == 11
95
+ assert logical_clock_store.get() == {"clock": 11}
96
+
97
+ assert logical_clock_store.get() is None
98
+
99
+
100
+ def test_batcher_size_and_flush():
101
+ client = VolidatorClient(
102
+ api_key="val_live_123",
103
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
104
+ )
105
+
106
+ mock_log_batch = MagicMock(return_value={"accepted": 3, "rejected": 0})
107
+ client.log_batch = mock_log_batch
108
+
109
+ batcher = client.batcher({"autoFlushCount": 3})
110
+
111
+ batcher.push({"action": "act-1"})
112
+ batcher.push({"action": "act-2"})
113
+ assert batcher.size() == 2
114
+
115
+ # Pushing 3rd should trigger flush automatically in a background thread
116
+ batcher.push({"action": "act-3"})
117
+
118
+ # Wait briefly for background thread to run flush
119
+ time.sleep(0.1)
120
+
121
+ assert batcher.size() == 0
122
+ mock_log_batch.assert_called_once()
123
+
124
+
125
+ def test_missing_plugin_dependencies():
126
+ client = VolidatorClient(
127
+ api_key="val_live_123",
128
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
129
+ )
130
+
131
+ # Test LangChain ImportError
132
+ with patch("volidator.plugins.langchain.HAS_LANGCHAIN", False):
133
+ with pytest.raises(RuntimeError) as exc_info:
134
+ VolidatorLangChainHandler(client, actor="usr_alice")
135
+ assert "langchain-core is not installed" in str(exc_info.value)
136
+
137
+ # Test LlamaIndex ImportError
138
+ with patch("volidator.plugins.llamaindex.HAS_LLAMAINDEX", False):
139
+ with pytest.raises(RuntimeError) as exc_info:
140
+ VolidatorLlamaIndexHandler(client, actor="usr_alice")
141
+ assert "llama-index-core is not installed" in str(exc_info.value)
142
+
143
+
144
+ @pytest.mark.asyncio
145
+ async def test_urllib_async_fallback_no_httpx():
146
+ client = VolidatorClient(
147
+ api_key="val_live_123",
148
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
149
+ )
150
+
151
+ # Mocking HAS_HTTPX as False
152
+ with patch("volidator.client.HAS_HTTPX", False):
153
+ with patch.object(client, "_fetch_with_retry", return_value=b"ok") as mock_fetch:
154
+ payload = {"actor": "usr_123", "action": "test"}
155
+ success = await client.log_async(payload)
156
+
157
+ assert success is True
158
+ mock_fetch.assert_called_once()
@@ -0,0 +1,52 @@
1
+ import datetime
2
+ from volidator.client import VolidatorClient, canonicalize
3
+
4
+
5
+ def test_blind_indexing_parity():
6
+ # Test case matching Node SDK output
7
+ client = VolidatorClient(
8
+ api_key="val_live_123",
9
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
10
+ )
11
+
12
+ # In JS: key hash is SHA256 of "vol-dek-0123456789abcdef..."
13
+ # HMAC-SHA256 of "usr_12345" using that hashed key
14
+ active_hashed_key = client._get_hashed_key("v1")
15
+ blind_index = client.generate_blind_index("usr_12345", active_hashed_key)
16
+
17
+ # Expected output matches the exact SHA256/HMAC layout
18
+ assert len(blind_index) == 64
19
+ assert isinstance(blind_index, str)
20
+
21
+
22
+ def test_encryption_decryption_roundtrip():
23
+ client = VolidatorClient(
24
+ api_key="val_live_123",
25
+ encryption_key="vol-dek-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
26
+ )
27
+
28
+ payload = {"actor": "usr_123", "action": "test.action", "target": "workspace_abc"}
29
+ encrypted_str = client.encrypt_payload(payload)
30
+
31
+ assert encrypted_str.startswith("v1:")
32
+
33
+ decrypted = client.decrypt_payload(encrypted_str)
34
+ assert decrypted == payload
35
+
36
+
37
+ def test_canonicalize():
38
+ # Sort keys
39
+ assert canonicalize({"b": 2, "a": 1}) == '{"a":1,"b":2}'
40
+
41
+ # Remove undefined (None) values in dicts
42
+ assert canonicalize({"x": None, "y": 1}) == '{"x":null,"y":1}'
43
+
44
+ # Replace None with null in arrays
45
+ assert canonicalize([None, 1]) == '[null,1]'
46
+
47
+ # Handle primitive lists
48
+ assert canonicalize(["foo", "bar"]) == '["foo","bar"]'
49
+
50
+ # Handle dates
51
+ dt = datetime.datetime(2026, 7, 11, 5, 39, 10, tzinfo=datetime.timezone.utc)
52
+ assert canonicalize(dt) == '"2026-07-11T05:39:10.000Z"'
@@ -0,0 +1,5 @@
1
+ from .client import VolidatorClient
2
+ from .compliance import VolidatorCompliance
3
+ from .agent import VolidatorAgent
4
+
5
+ __all__ = ["VolidatorClient", "VolidatorCompliance", "VolidatorAgent"]
@@ -0,0 +1,137 @@
1
+ from typing import Any
2
+
3
+
4
+ class VolidatorAgent:
5
+ def __init__(self, client):
6
+ self.client = client
7
+
8
+ def _log_agent(
9
+ self,
10
+ action: str,
11
+ eu_ai_act: str,
12
+ nist_ai_rmf: str,
13
+ soc2_control: str,
14
+ iso_control: str,
15
+ payload: dict[str, Any]
16
+ ) -> bool:
17
+ core_fields = {
18
+ "actor",
19
+ "actorId",
20
+ "target",
21
+ "targetId",
22
+ "tenant",
23
+ "tenantId",
24
+ "metadata",
25
+ "context",
26
+ "telemetry",
27
+ "req",
28
+ "traceId",
29
+ "spanId",
30
+ "parentSpanId",
31
+ }
32
+
33
+ # Enriched metadata includes any additional keyword args passed at root level
34
+ agent_data = {k: v for k, v in payload.items() if k not in core_fields}
35
+ metadata = payload.get("metadata") or {}
36
+
37
+ enriched_metadata = {
38
+ **metadata,
39
+ **agent_data,
40
+ "eu_ai_act": eu_ai_act,
41
+ "nist_ai_rmf": nist_ai_rmf,
42
+ "soc2_control": soc2_control,
43
+ "iso27001": iso_control,
44
+ }
45
+
46
+ # Build base payload structure
47
+ base_payload = {k: v for k, v in payload.items() if k in core_fields}
48
+ base_payload["action"] = action
49
+ base_payload["metadata"] = enriched_metadata
50
+
51
+ # AI logs have a larger 5MB metadata allowance
52
+ return self.client.log(base_payload, max_meta_override=5242880)
53
+
54
+ async def _log_agent_async(
55
+ self,
56
+ action: str,
57
+ eu_ai_act: str,
58
+ nist_ai_rmf: str,
59
+ soc2_control: str,
60
+ iso_control: str,
61
+ payload: dict[str, Any]
62
+ ) -> bool:
63
+ core_fields = {
64
+ "actor",
65
+ "actorId",
66
+ "target",
67
+ "targetId",
68
+ "tenant",
69
+ "tenantId",
70
+ "metadata",
71
+ "context",
72
+ "telemetry",
73
+ "req",
74
+ "traceId",
75
+ "spanId",
76
+ "parentSpanId",
77
+ }
78
+
79
+ agent_data = {k: v for k, v in payload.items() if k not in core_fields}
80
+ metadata = payload.get("metadata") or {}
81
+
82
+ enriched_metadata = {
83
+ **metadata,
84
+ **agent_data,
85
+ "eu_ai_act": eu_ai_act,
86
+ "nist_ai_rmf": nist_ai_rmf,
87
+ "soc2_control": soc2_control,
88
+ "iso27001": iso_control,
89
+ }
90
+
91
+ base_payload = {k: v for k, v in payload.items() if k in core_fields}
92
+ base_payload["action"] = action
93
+ base_payload["metadata"] = enriched_metadata
94
+
95
+ return await self.client.log_async(base_payload, max_meta_override=5242880)
96
+
97
+ # 1. tool_call / tool_call_async
98
+ def tool_call(self, payload: dict[str, Any]) -> bool:
99
+ return self._log_agent("agent.tool_call", "Article 12", "MANAGE 2.2", "CC6.6", "A.12.4.1", payload)
100
+
101
+ async def tool_call_async(self, payload: dict[str, Any]) -> bool:
102
+ return await self._log_agent_async("agent.tool_call", "Article 12", "MANAGE 2.2", "CC6.6", "A.12.4.1", payload)
103
+
104
+ # 2. decision / decision_async
105
+ def decision(self, payload: dict[str, Any]) -> bool:
106
+ return self._log_agent("agent.decision", "Article 12 & 13", "GOVERN 1.7", "CC6.2", "A.18.1.3", payload)
107
+
108
+ async def decision_async(self, payload: dict[str, Any]) -> bool:
109
+ return await self._log_agent_async("agent.decision", "Article 12 & 13", "GOVERN 1.7", "CC6.2", "A.18.1.3", payload)
110
+
111
+ # 3. escalation / escalation_async
112
+ def escalation(self, payload: dict[str, Any]) -> bool:
113
+ return self._log_agent("agent.escalation", "Article 14", "GOVERN 5.1", "CC6.3", "A.6.1.2", payload)
114
+
115
+ async def escalation_async(self, payload: dict[str, Any]) -> bool:
116
+ return await self._log_agent_async("agent.escalation", "Article 14", "GOVERN 5.1", "CC6.3", "A.6.1.2", payload)
117
+
118
+ # 4. anomaly / anomaly_async
119
+ def anomaly(self, payload: dict[str, Any]) -> bool:
120
+ return self._log_agent("agent.anomaly", "Article 9", "MANAGE 2.4", "CC7.2", "A.16.1.2", payload)
121
+
122
+ async def anomaly_async(self, payload: dict[str, Any]) -> bool:
123
+ return await self._log_agent_async("agent.anomaly", "Article 9", "MANAGE 2.4", "CC7.2", "A.16.1.2", payload)
124
+
125
+ # 5. refusal / refusal_async
126
+ def refusal(self, payload: dict[str, Any]) -> bool:
127
+ return self._log_agent("agent.refusal", "Article 5", "GOVERN 1.1", "CC6.8", "A.18.1.3", payload)
128
+
129
+ async def refusal_async(self, payload: dict[str, Any]) -> bool:
130
+ return await self._log_agent_async("agent.refusal", "Article 5", "GOVERN 1.1", "CC6.8", "A.18.1.3", payload)
131
+
132
+ # 6. handoff / handoff_async
133
+ def handoff(self, payload: dict[str, Any]) -> bool:
134
+ return self._log_agent("agent.handoff", "Article 12", "MAP 1.6", "CC6.6", "A.12.4.1", payload)
135
+
136
+ async def handoff_async(self, payload: dict[str, Any]) -> bool:
137
+ return await self._log_agent_async("agent.handoff", "Article 12", "MAP 1.6", "CC6.6", "A.12.4.1", payload)