volidator-python 0.1.0__py3-none-any.whl
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.
- volidator/__init__.py +5 -0
- volidator/agent.py +137 -0
- volidator/client.py +1129 -0
- volidator/compliance.py +70 -0
- volidator/plugins/__init__.py +1 -0
- volidator/plugins/langchain.py +115 -0
- volidator/plugins/llamaindex.py +96 -0
- volidator_python-0.1.0.dist-info/METADATA +102 -0
- volidator_python-0.1.0.dist-info/RECORD +11 -0
- volidator_python-0.1.0.dist-info/WHEEL +5 -0
- volidator_python-0.1.0.dist-info/top_level.txt +1 -0
volidator/compliance.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class VolidatorCompliance:
|
|
5
|
+
def __init__(self, client):
|
|
6
|
+
self.client = client
|
|
7
|
+
|
|
8
|
+
def _log_with_control(
|
|
9
|
+
self,
|
|
10
|
+
action: str,
|
|
11
|
+
soc2_control: str,
|
|
12
|
+
iso_control: str,
|
|
13
|
+
payload: dict[str, Any],
|
|
14
|
+
max_meta_override: Optional[int] = None
|
|
15
|
+
) -> bool:
|
|
16
|
+
metadata = payload.get("metadata") or {}
|
|
17
|
+
enriched_metadata = {
|
|
18
|
+
**metadata,
|
|
19
|
+
"soc2_control": soc2_control,
|
|
20
|
+
"iso27001": iso_control,
|
|
21
|
+
}
|
|
22
|
+
full_payload = {**payload, "action": action, "metadata": enriched_metadata}
|
|
23
|
+
return self.client.log(full_payload, max_meta_override)
|
|
24
|
+
|
|
25
|
+
async def _log_with_control_async(
|
|
26
|
+
self,
|
|
27
|
+
action: str,
|
|
28
|
+
soc2_control: str,
|
|
29
|
+
iso_control: str,
|
|
30
|
+
payload: dict[str, Any],
|
|
31
|
+
max_meta_override: Optional[int] = None
|
|
32
|
+
) -> bool:
|
|
33
|
+
metadata = payload.get("metadata") or {}
|
|
34
|
+
enriched_metadata = {
|
|
35
|
+
**metadata,
|
|
36
|
+
"soc2_control": soc2_control,
|
|
37
|
+
"iso27001": iso_control,
|
|
38
|
+
}
|
|
39
|
+
full_payload = {**payload, "action": action, "metadata": enriched_metadata}
|
|
40
|
+
return await self.client.log_async(full_payload, max_meta_override)
|
|
41
|
+
|
|
42
|
+
def access_revoked(self, payload: dict[str, Any]) -> bool:
|
|
43
|
+
return self._log_with_control("access.revoked", "CC6.1", "A.9.2.6", payload)
|
|
44
|
+
|
|
45
|
+
async def access_revoked_async(self, payload: dict[str, Any]) -> bool:
|
|
46
|
+
return await self._log_with_control_async("access.revoked", "CC6.1", "A.9.2.6", payload)
|
|
47
|
+
|
|
48
|
+
def access_granted(self, payload: dict[str, Any]) -> bool:
|
|
49
|
+
return self._log_with_control("access.granted", "CC6.1", "A.9.2.1", payload)
|
|
50
|
+
|
|
51
|
+
async def access_granted_async(self, payload: dict[str, Any]) -> bool:
|
|
52
|
+
return await self._log_with_control_async("access.granted", "CC6.1", "A.9.2.1", payload)
|
|
53
|
+
|
|
54
|
+
def data_exported(self, payload: dict[str, Any]) -> bool:
|
|
55
|
+
return self._log_with_control("data.exported", "CC6.6", "A.12.4.1", payload)
|
|
56
|
+
|
|
57
|
+
async def data_exported_async(self, payload: dict[str, Any]) -> bool:
|
|
58
|
+
return await self._log_with_control_async("data.exported", "CC6.6", "A.12.4.1", payload)
|
|
59
|
+
|
|
60
|
+
def system_config_changed(self, payload: dict[str, Any]) -> bool:
|
|
61
|
+
return self._log_with_control("system.config_changed", "CC6.2", "A.12.1.2", payload)
|
|
62
|
+
|
|
63
|
+
async def system_config_changed_async(self, payload: dict[str, Any]) -> bool:
|
|
64
|
+
return await self._log_with_control_async("system.config_changed", "CC6.2", "A.12.1.2", payload)
|
|
65
|
+
|
|
66
|
+
def mfa_enabled(self, payload: dict[str, Any]) -> bool:
|
|
67
|
+
return self._log_with_control("mfa.enabled", "CC6.3", "A.9.4.2", payload)
|
|
68
|
+
|
|
69
|
+
async def mfa_enabled_async(self, payload: dict[str, Any]) -> bool:
|
|
70
|
+
return await self._log_with_control_async("mfa.enabled", "CC6.3", "A.9.4.2", payload)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Volidator framework integration plugins
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
from ..client import agent_context_store
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from langchain_core.callbacks import BaseCallbackHandler # type: ignore
|
|
7
|
+
HAS_LANGCHAIN = True
|
|
8
|
+
except ImportError:
|
|
9
|
+
class BaseCallbackHandler:
|
|
10
|
+
pass
|
|
11
|
+
HAS_LANGCHAIN = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VolidatorLangChainHandler(BaseCallbackHandler):
|
|
15
|
+
def __init__(self, client, actor: str, tenant: Optional[str] = None):
|
|
16
|
+
if not HAS_LANGCHAIN:
|
|
17
|
+
raise RuntimeError(
|
|
18
|
+
"langchain-core is not installed. Please install it with 'pip install langchain-core' to use this handler."
|
|
19
|
+
)
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.client = client
|
|
22
|
+
self.actor = actor
|
|
23
|
+
self.tenant = tenant
|
|
24
|
+
self.run_map = {}
|
|
25
|
+
self._tokens = {}
|
|
26
|
+
|
|
27
|
+
def on_tool_start(
|
|
28
|
+
self,
|
|
29
|
+
serialized: dict[str, Any],
|
|
30
|
+
input_str: str,
|
|
31
|
+
*,
|
|
32
|
+
run_id: Any,
|
|
33
|
+
parent_run_id: Optional[Any] = None,
|
|
34
|
+
**kwargs: Any
|
|
35
|
+
) -> None:
|
|
36
|
+
tool_name = serialized.get("name") or "unnamed_tool"
|
|
37
|
+
rationale = f"Executing tool {tool_name} with input: {input_str[:100]}"
|
|
38
|
+
|
|
39
|
+
# Bind agent context in contextvars
|
|
40
|
+
token = agent_context_store.set({
|
|
41
|
+
"traceId": str(run_id),
|
|
42
|
+
"toolName": tool_name,
|
|
43
|
+
"rationale": rationale,
|
|
44
|
+
})
|
|
45
|
+
self._tokens[str(run_id)] = token
|
|
46
|
+
|
|
47
|
+
self.run_map[str(run_id)] = {
|
|
48
|
+
"toolName": tool_name,
|
|
49
|
+
"toolInput": {"input": input_str},
|
|
50
|
+
"startTime": time.time(),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def on_tool_end(
|
|
54
|
+
self,
|
|
55
|
+
output: str,
|
|
56
|
+
*,
|
|
57
|
+
run_id: Any,
|
|
58
|
+
parent_run_id: Optional[Any] = None,
|
|
59
|
+
**kwargs: Any
|
|
60
|
+
) -> None:
|
|
61
|
+
# Prevent token leaking
|
|
62
|
+
token = self._tokens.pop(str(run_id), None)
|
|
63
|
+
if token:
|
|
64
|
+
agent_context_store.reset(token)
|
|
65
|
+
|
|
66
|
+
run = self.run_map.pop(str(run_id), None)
|
|
67
|
+
if not run:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
latency_ms = int((time.time() - run["startTime"]) * 1000)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
self.client.agent.tool_call({
|
|
74
|
+
"actor": self.actor,
|
|
75
|
+
"tenant": self.tenant,
|
|
76
|
+
"toolName": run["toolName"],
|
|
77
|
+
"toolInput": run["toolInput"],
|
|
78
|
+
"toolOutput": {"output": str(output)},
|
|
79
|
+
"latencyMs": latency_ms,
|
|
80
|
+
"success": True,
|
|
81
|
+
})
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def on_tool_error(
|
|
86
|
+
self,
|
|
87
|
+
error: BaseException,
|
|
88
|
+
*,
|
|
89
|
+
run_id: Any,
|
|
90
|
+
parent_run_id: Optional[Any] = None,
|
|
91
|
+
**kwargs: Any
|
|
92
|
+
) -> None:
|
|
93
|
+
# Prevent token leaking
|
|
94
|
+
token = self._tokens.pop(str(run_id), None)
|
|
95
|
+
if token:
|
|
96
|
+
agent_context_store.reset(token)
|
|
97
|
+
|
|
98
|
+
run = self.run_map.pop(str(run_id), None)
|
|
99
|
+
if not run:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
latency_ms = int((time.time() - run["startTime"]) * 1000)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
self.client.agent.tool_call({
|
|
106
|
+
"actor": self.actor,
|
|
107
|
+
"tenant": self.tenant,
|
|
108
|
+
"toolName": run["toolName"],
|
|
109
|
+
"toolInput": run["toolInput"],
|
|
110
|
+
"toolOutput": {"error": str(error)},
|
|
111
|
+
"latencyMs": latency_ms,
|
|
112
|
+
"success": False,
|
|
113
|
+
})
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
from ..client import agent_context_store
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from llama_index.core.callbacks.base_handler import BaseCallbackHandler # type: ignore
|
|
7
|
+
from llama_index.core.callbacks.schema import CBEventType # type: ignore
|
|
8
|
+
HAS_LLAMAINDEX = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
class BaseCallbackHandler:
|
|
11
|
+
pass
|
|
12
|
+
class CBEventType:
|
|
13
|
+
TOOL = "tool"
|
|
14
|
+
HAS_LLAMAINDEX = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VolidatorLlamaIndexHandler(BaseCallbackHandler):
|
|
18
|
+
def __init__(self, client, actor: str, tenant: Optional[str] = None):
|
|
19
|
+
if not HAS_LLAMAINDEX:
|
|
20
|
+
raise RuntimeError(
|
|
21
|
+
"llama-index-core is not installed. Please install it with 'pip install llama-index-core' to use this handler."
|
|
22
|
+
)
|
|
23
|
+
# Call llama-index BaseCallbackHandler constructor if it exists
|
|
24
|
+
if hasattr(BaseCallbackHandler, "__init__"):
|
|
25
|
+
super().__init__(event_starts_to_ignore=[], event_ends_to_ignore=[])
|
|
26
|
+
self.client = client
|
|
27
|
+
self.actor = actor
|
|
28
|
+
self.tenant = tenant
|
|
29
|
+
self.run_map = {}
|
|
30
|
+
self._tokens = {}
|
|
31
|
+
|
|
32
|
+
def on_event_start(
|
|
33
|
+
self,
|
|
34
|
+
event_type: CBEventType,
|
|
35
|
+
payload: Optional[dict[str, Any]] = None,
|
|
36
|
+
event_id: str = "",
|
|
37
|
+
**kwargs: Any
|
|
38
|
+
) -> str:
|
|
39
|
+
if event_type == CBEventType.TOOL:
|
|
40
|
+
tool_name = "unnamed_tool"
|
|
41
|
+
tool_input = {}
|
|
42
|
+
if payload and "tool" in payload:
|
|
43
|
+
tool_name = payload["tool"].name
|
|
44
|
+
tool_input = payload.get("input", {})
|
|
45
|
+
|
|
46
|
+
rationale = f"Executing LlamaIndex tool {tool_name}"
|
|
47
|
+
|
|
48
|
+
token = agent_context_store.set({
|
|
49
|
+
"traceId": event_id,
|
|
50
|
+
"toolName": tool_name,
|
|
51
|
+
"rationale": rationale,
|
|
52
|
+
})
|
|
53
|
+
self._tokens[event_id] = token
|
|
54
|
+
|
|
55
|
+
self.run_map[event_id] = {
|
|
56
|
+
"toolName": tool_name,
|
|
57
|
+
"toolInput": tool_input,
|
|
58
|
+
"startTime": time.time(),
|
|
59
|
+
}
|
|
60
|
+
return event_id
|
|
61
|
+
|
|
62
|
+
def on_event_end(
|
|
63
|
+
self,
|
|
64
|
+
event_type: CBEventType,
|
|
65
|
+
payload: Optional[dict[str, Any]] = None,
|
|
66
|
+
event_id: str = "",
|
|
67
|
+
**kwargs: Any
|
|
68
|
+
) -> None:
|
|
69
|
+
token = self._tokens.pop(event_id, None)
|
|
70
|
+
if token:
|
|
71
|
+
agent_context_store.reset(token)
|
|
72
|
+
|
|
73
|
+
if event_type == CBEventType.TOOL:
|
|
74
|
+
run = self.run_map.pop(event_id, None)
|
|
75
|
+
if not run:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
latency_ms = int((time.time() - run["startTime"]) * 1000)
|
|
79
|
+
|
|
80
|
+
tool_output = {}
|
|
81
|
+
success = True
|
|
82
|
+
if payload and "response" in payload:
|
|
83
|
+
tool_output = {"output": str(payload["response"])}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
self.client.agent.tool_call({
|
|
87
|
+
"actor": self.actor,
|
|
88
|
+
"tenant": self.tenant,
|
|
89
|
+
"toolName": run["toolName"],
|
|
90
|
+
"toolInput": run["toolInput"],
|
|
91
|
+
"toolOutput": tool_output,
|
|
92
|
+
"latencyMs": latency_ms,
|
|
93
|
+
"success": success,
|
|
94
|
+
})
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
@@ -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,11 @@
|
|
|
1
|
+
volidator/__init__.py,sha256=-r28T-iGsOZO_GhUrcFCVLo961UKb1Mug-_N8LdLAeQ,186
|
|
2
|
+
volidator/agent.py,sha256=cHnP4cKIn_CtpXeyMeoshYUhrDprgusgPFZdhmoaE6Q,4982
|
|
3
|
+
volidator/client.py,sha256=0NalVvOg5yJKraytUQsNgt7X5VPsipQe4HOS5CpaxYI,44745
|
|
4
|
+
volidator/compliance.py,sha256=_Ci4C4QT4kOopLW4K6fWMjyUUSgxho-RxDHIwOFRcyU,2877
|
|
5
|
+
volidator/plugins/__init__.py,sha256=HvRs6AxjXeGeLI8exhFMRWH_Wdpn0dh49LT8VHWZ8f8,42
|
|
6
|
+
volidator/plugins/langchain.py,sha256=b1HTK51PXHH6SqS4skpQa2EbThHr7b3VhVTwsTMF_HQ,3329
|
|
7
|
+
volidator/plugins/llamaindex.py,sha256=4YrmM9yLfXx-FWSOJHnIQnxdr99qCc9Ce33unvz8_-Y,3169
|
|
8
|
+
volidator_python-0.1.0.dist-info/METADATA,sha256=1RkJviwt0SVRuOIAsoFdsYS0CDrBwXm9jY609fiYI7o,2781
|
|
9
|
+
volidator_python-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
10
|
+
volidator_python-0.1.0.dist-info/top_level.txt,sha256=7taXS7XldS3KWgMj_tly0_mG0o4wUN-rpEhfluSs3U4,10
|
|
11
|
+
volidator_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
volidator
|