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.
- volidator_python-0.1.0/PKG-INFO +102 -0
- volidator_python-0.1.0/README.md +82 -0
- volidator_python-0.1.0/pyproject.toml +32 -0
- volidator_python-0.1.0/setup.cfg +4 -0
- volidator_python-0.1.0/tests/test_client.py +158 -0
- volidator_python-0.1.0/tests/test_cryptography.py +52 -0
- volidator_python-0.1.0/volidator/__init__.py +5 -0
- volidator_python-0.1.0/volidator/agent.py +137 -0
- volidator_python-0.1.0/volidator/client.py +1129 -0
- volidator_python-0.1.0/volidator/compliance.py +70 -0
- volidator_python-0.1.0/volidator/plugins/__init__.py +1 -0
- volidator_python-0.1.0/volidator/plugins/langchain.py +115 -0
- volidator_python-0.1.0/volidator/plugins/llamaindex.py +96 -0
- volidator_python-0.1.0/volidator_python.egg-info/PKG-INFO +102 -0
- volidator_python-0.1.0/volidator_python.egg-info/SOURCES.txt +16 -0
- volidator_python-0.1.0/volidator_python.egg-info/dependency_links.txt +1 -0
- volidator_python-0.1.0/volidator_python.egg-info/requires.txt +12 -0
- volidator_python-0.1.0/volidator_python.egg-info/top_level.txt +1 -0
|
@@ -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,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,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)
|