verdicter 1.0.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.
- verdicter/__init__.py +22 -0
- verdicter/adapters/__init__.py +0 -0
- verdicter/adapters/langchain.py +82 -0
- verdicter/client.py +242 -0
- verdicter/errors.py +21 -0
- verdicter/types.py +67 -0
- verdicter-1.0.0.dist-info/METADATA +161 -0
- verdicter-1.0.0.dist-info/RECORD +9 -0
- verdicter-1.0.0.dist-info/WHEEL +4 -0
verdicter/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .client import Verdicter
|
|
2
|
+
from .errors import VerdicterError
|
|
3
|
+
from .types import (
|
|
4
|
+
Decision,
|
|
5
|
+
EvaluateRequest,
|
|
6
|
+
EvaluateResponse,
|
|
7
|
+
TraceStep,
|
|
8
|
+
VerdicterConfig,
|
|
9
|
+
WrapOptions,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.0"
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Verdicter",
|
|
15
|
+
"VerdicterError",
|
|
16
|
+
"Decision",
|
|
17
|
+
"EvaluateRequest",
|
|
18
|
+
"EvaluateResponse",
|
|
19
|
+
"TraceStep",
|
|
20
|
+
"VerdicterConfig",
|
|
21
|
+
"WrapOptions",
|
|
22
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain adapter for Verdicter.
|
|
3
|
+
|
|
4
|
+
Wraps LangChain tools so every invocation passes through Verdicter's
|
|
5
|
+
policy engine before execution.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
from verdicter.adapters.langchain import wrap_tools
|
|
10
|
+
|
|
11
|
+
safe_tools = wrap_tools(tools, client, agent_id="support_bot")
|
|
12
|
+
agent = create_react_agent(llm=llm, tools=safe_tools)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from ..errors import VerdicterError
|
|
20
|
+
from ..types import WrapOptions
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ..client import Verdicter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def wrap_tools(tools: list[Any], client: "Verdicter", *, agent_id: str, **kwargs: Any) -> list[Any]:
|
|
27
|
+
"""Wrap a list of LangChain tools with Verdicter policy enforcement."""
|
|
28
|
+
options = WrapOptions(agent_id=agent_id, **kwargs)
|
|
29
|
+
return [wrap_tool(t, client, options) for t in tools]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def wrap_tool(tool: Any, client: "Verdicter", options: WrapOptions) -> Any:
|
|
33
|
+
"""Wrap a single LangChain tool with Verdicter policy enforcement."""
|
|
34
|
+
try:
|
|
35
|
+
from langchain_core.tools import BaseTool
|
|
36
|
+
except ImportError:
|
|
37
|
+
raise ImportError(
|
|
38
|
+
"langchain-core is required for the LangChain adapter. "
|
|
39
|
+
"Install it with: pip install verdicter[langchain]"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
original_invoke = tool._run if hasattr(tool, "_run") else tool.invoke
|
|
43
|
+
tool_name = tool.name
|
|
44
|
+
|
|
45
|
+
async def guarded_ainvoke(input: Any, **kw: Any) -> Any:
|
|
46
|
+
payload = input if isinstance(input, dict) else {"input": input}
|
|
47
|
+
res = await client.evaluate(
|
|
48
|
+
agent_id=options.agent_id,
|
|
49
|
+
tool=tool_name,
|
|
50
|
+
payload=payload,
|
|
51
|
+
session_id=options.session_id,
|
|
52
|
+
context=options.context,
|
|
53
|
+
credential_name=options.credential_name,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if res.decision == "DENY":
|
|
57
|
+
if options.on_deny:
|
|
58
|
+
return options.on_deny(res)
|
|
59
|
+
raise VerdicterError(
|
|
60
|
+
f'Tool "{tool_name}" denied: {res.reason or "policy match"}', "UNKNOWN"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if res.decision == "ESCALATE":
|
|
64
|
+
if options.on_escalate:
|
|
65
|
+
return options.on_escalate(res)
|
|
66
|
+
raise VerdicterError(
|
|
67
|
+
f'Tool "{tool_name}" requires human approval: {res.reason or "escalation policy"}',
|
|
68
|
+
"UNKNOWN",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
effective = (
|
|
72
|
+
res.modified_payload
|
|
73
|
+
if res.decision == "MODIFY" and res.modified_payload
|
|
74
|
+
else payload
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if hasattr(tool, "ainvoke"):
|
|
78
|
+
return await tool.ainvoke(effective, **kw)
|
|
79
|
+
return original_invoke(effective, **kw)
|
|
80
|
+
|
|
81
|
+
tool.ainvoke = guarded_ainvoke # type: ignore[method-assign]
|
|
82
|
+
return tool
|
verdicter/client.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any, Callable, TypeVar
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .errors import VerdicterError
|
|
10
|
+
from .types import (
|
|
11
|
+
EvaluateRequest,
|
|
12
|
+
EvaluateResponse,
|
|
13
|
+
TraceStep,
|
|
14
|
+
VerdicterConfig,
|
|
15
|
+
WrapOptions,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
SDK_VERSION = "1.0.0"
|
|
19
|
+
RETRYABLE_STATUSES = {429, 502, 503, 504}
|
|
20
|
+
|
|
21
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Verdicter:
|
|
25
|
+
"""
|
|
26
|
+
Runtime security client for AI agents.
|
|
27
|
+
|
|
28
|
+
Example::
|
|
29
|
+
|
|
30
|
+
from verdicter import Verdicter
|
|
31
|
+
|
|
32
|
+
client = Verdicter(api_key="verdicter_live_...")
|
|
33
|
+
|
|
34
|
+
result = await client.evaluate(
|
|
35
|
+
agent_id="support_bot",
|
|
36
|
+
tool="send_email",
|
|
37
|
+
payload={"to": user_email, "subject": subject, "body": body},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if result.decision == "ALLOW":
|
|
41
|
+
await send_email(payload)
|
|
42
|
+
elif result.decision == "DENY":
|
|
43
|
+
raise PermissionError(result.reason)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: str,
|
|
49
|
+
*,
|
|
50
|
+
base_url: str = "https://api.verdicter.dev",
|
|
51
|
+
timeout: float = 5.0,
|
|
52
|
+
max_retries: int = 2,
|
|
53
|
+
retry_backoff: float = 0.2,
|
|
54
|
+
fail_open: bool = False,
|
|
55
|
+
) -> None:
|
|
56
|
+
if not api_key:
|
|
57
|
+
raise VerdicterError("api_key is required", "UNAUTHORIZED")
|
|
58
|
+
|
|
59
|
+
self._api_key = api_key
|
|
60
|
+
self._base_url = base_url.rstrip("/")
|
|
61
|
+
self._timeout = timeout
|
|
62
|
+
self._max_retries = max_retries
|
|
63
|
+
self._retry_backoff = retry_backoff
|
|
64
|
+
self._fail_open = fail_open
|
|
65
|
+
|
|
66
|
+
self._headers = {
|
|
67
|
+
"Authorization": f"Bearer {api_key}",
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
"X-Verdicter-SDK": f"python/{SDK_VERSION}",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async def evaluate(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
agent_id: str,
|
|
76
|
+
tool: str,
|
|
77
|
+
payload: dict[str, Any],
|
|
78
|
+
session_id: str | None = None,
|
|
79
|
+
context: dict[str, Any] | None = None,
|
|
80
|
+
credential_name: str | None = None,
|
|
81
|
+
) -> EvaluateResponse:
|
|
82
|
+
"""Evaluate an agent tool call against your policies."""
|
|
83
|
+
if not agent_id:
|
|
84
|
+
raise VerdicterError("agent_id is required", "VALIDATION_ERROR")
|
|
85
|
+
if not tool:
|
|
86
|
+
raise VerdicterError("tool is required", "VALIDATION_ERROR")
|
|
87
|
+
if not isinstance(payload, dict):
|
|
88
|
+
raise VerdicterError("payload must be a dict", "VALIDATION_ERROR")
|
|
89
|
+
|
|
90
|
+
body: dict[str, Any] = {
|
|
91
|
+
"agent_id": agent_id,
|
|
92
|
+
"tool": tool,
|
|
93
|
+
"payload": payload,
|
|
94
|
+
"context": context or {},
|
|
95
|
+
}
|
|
96
|
+
if session_id:
|
|
97
|
+
body["session_id"] = session_id
|
|
98
|
+
if credential_name:
|
|
99
|
+
body["credential_name"] = credential_name
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
return await self._post("/v1/evaluate", body)
|
|
103
|
+
except VerdicterError as e:
|
|
104
|
+
if self._fail_open and e.code in ("NETWORK_ERROR", "TIMEOUT"):
|
|
105
|
+
return EvaluateResponse(
|
|
106
|
+
call_id="failopen",
|
|
107
|
+
decision="ALLOW",
|
|
108
|
+
reason="Verdicter unreachable — fail-open policy applied",
|
|
109
|
+
risk_score=0,
|
|
110
|
+
trace=[],
|
|
111
|
+
latency_ms=0,
|
|
112
|
+
timestamp="",
|
|
113
|
+
)
|
|
114
|
+
raise
|
|
115
|
+
|
|
116
|
+
def wrap(self, tool: str, fn: F, options: WrapOptions) -> F:
|
|
117
|
+
"""
|
|
118
|
+
Wrap a coroutine function so every call is evaluated first.
|
|
119
|
+
|
|
120
|
+
Example::
|
|
121
|
+
|
|
122
|
+
safe_send = client.wrap("send_email", send_email, WrapOptions(agent_id="bot"))
|
|
123
|
+
await safe_send(to=email, subject=subject, body=body)
|
|
124
|
+
"""
|
|
125
|
+
async def wrapped(*args: Any, **kwargs: Any) -> Any:
|
|
126
|
+
payload = kwargs if kwargs else (args[0] if args and isinstance(args[0], dict) else {})
|
|
127
|
+
res = await self.evaluate(
|
|
128
|
+
agent_id=options.agent_id,
|
|
129
|
+
tool=tool,
|
|
130
|
+
payload=payload,
|
|
131
|
+
session_id=options.session_id,
|
|
132
|
+
context=options.context,
|
|
133
|
+
credential_name=options.credential_name,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if res.decision == "DENY":
|
|
137
|
+
if options.on_deny:
|
|
138
|
+
return await _maybe_await(options.on_deny(res))
|
|
139
|
+
raise VerdicterError(
|
|
140
|
+
f'Action "{tool}" denied: {res.reason or "policy match"}', "UNKNOWN"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if res.decision == "ESCALATE":
|
|
144
|
+
if options.on_escalate:
|
|
145
|
+
return await _maybe_await(options.on_escalate(res))
|
|
146
|
+
raise VerdicterError(
|
|
147
|
+
f'Action "{tool}" requires human approval: {res.reason or "escalation policy"}',
|
|
148
|
+
"UNKNOWN",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
effective_payload = (
|
|
152
|
+
res.modified_payload
|
|
153
|
+
if res.decision == "MODIFY" and res.modified_payload
|
|
154
|
+
else payload
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if isinstance(effective_payload, dict) and kwargs:
|
|
158
|
+
return await _maybe_await(fn(**effective_payload))
|
|
159
|
+
return await _maybe_await(fn(effective_payload))
|
|
160
|
+
|
|
161
|
+
wrapped.__name__ = getattr(fn, "__name__", tool) # type: ignore[attr-defined]
|
|
162
|
+
return wrapped # type: ignore[return-value]
|
|
163
|
+
|
|
164
|
+
# ── Internal ──────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async def _post(self, path: str, body: dict[str, Any], attempt: int = 0) -> EvaluateResponse:
|
|
167
|
+
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
|
168
|
+
try:
|
|
169
|
+
resp = await client.post(
|
|
170
|
+
f"{self._base_url}{path}",
|
|
171
|
+
json=body,
|
|
172
|
+
headers=self._headers,
|
|
173
|
+
)
|
|
174
|
+
except httpx.TimeoutException:
|
|
175
|
+
if attempt < self._max_retries:
|
|
176
|
+
await asyncio.sleep(self._retry_backoff * (2 ** attempt))
|
|
177
|
+
return await self._post(path, body, attempt + 1)
|
|
178
|
+
raise VerdicterError(
|
|
179
|
+
f"Request timed out after {self._timeout}s", "TIMEOUT"
|
|
180
|
+
)
|
|
181
|
+
except httpx.RequestError as e:
|
|
182
|
+
if attempt < self._max_retries:
|
|
183
|
+
await asyncio.sleep(self._retry_backoff * (2 ** attempt))
|
|
184
|
+
return await self._post(path, body, attempt + 1)
|
|
185
|
+
raise VerdicterError(f"Network error: {e}", "NETWORK_ERROR")
|
|
186
|
+
|
|
187
|
+
if resp.status_code in (401, 403):
|
|
188
|
+
raise VerdicterError("Invalid or revoked API key", "UNAUTHORIZED", resp.status_code)
|
|
189
|
+
if resp.status_code == 404:
|
|
190
|
+
data = _safe_json(resp)
|
|
191
|
+
raise VerdicterError(str(data.get("error", "Agent not found")), "AGENT_NOT_FOUND", 404)
|
|
192
|
+
if resp.status_code == 422:
|
|
193
|
+
data = _safe_json(resp)
|
|
194
|
+
raise VerdicterError(str(data.get("error", "Validation failed")), "VALIDATION_ERROR", 422)
|
|
195
|
+
if resp.status_code == 429:
|
|
196
|
+
raise VerdicterError("Rate limit exceeded", "RATE_LIMITED", 429)
|
|
197
|
+
if resp.status_code in RETRYABLE_STATUSES and attempt < self._max_retries:
|
|
198
|
+
await asyncio.sleep(self._retry_backoff * (2 ** attempt))
|
|
199
|
+
return await self._post(path, body, attempt + 1)
|
|
200
|
+
if not resp.is_success:
|
|
201
|
+
data = _safe_json(resp)
|
|
202
|
+
raise VerdicterError(
|
|
203
|
+
str(data.get("error", f"HTTP {resp.status_code}")), "UNKNOWN", resp.status_code
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return _parse_response(resp.json())
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _safe_json(resp: httpx.Response) -> dict[str, Any]:
|
|
210
|
+
try:
|
|
211
|
+
return resp.json()
|
|
212
|
+
except Exception:
|
|
213
|
+
return {}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _parse_response(data: dict[str, Any]) -> EvaluateResponse:
|
|
217
|
+
trace = [
|
|
218
|
+
TraceStep(
|
|
219
|
+
step=s["step"],
|
|
220
|
+
result=s["result"],
|
|
221
|
+
detail=s.get("detail"),
|
|
222
|
+
policy_id=s.get("policyId"),
|
|
223
|
+
)
|
|
224
|
+
for s in data.get("trace", [])
|
|
225
|
+
]
|
|
226
|
+
return EvaluateResponse(
|
|
227
|
+
call_id=data["callId"],
|
|
228
|
+
decision=data["decision"],
|
|
229
|
+
reason=data.get("reason"),
|
|
230
|
+
policy_id=data.get("policyId"),
|
|
231
|
+
modified_payload=data.get("modifiedPayload"),
|
|
232
|
+
risk_score=data["riskScore"],
|
|
233
|
+
trace=trace,
|
|
234
|
+
latency_ms=data["latencyMs"],
|
|
235
|
+
timestamp=data["timestamp"],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def _maybe_await(value: Any) -> Any:
|
|
240
|
+
if inspect.isawaitable(value):
|
|
241
|
+
return await value
|
|
242
|
+
return value
|
verdicter/errors.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .types import ErrorCode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VerdicterError(Exception):
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
message: str,
|
|
11
|
+
code: ErrorCode = "UNKNOWN",
|
|
12
|
+
status_code: Optional[int] = None,
|
|
13
|
+
call_id: Optional[str] = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.code = code
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.call_id = call_id
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return f"VerdicterError(code={self.code!r}, message={str(self)!r})"
|
verdicter/types.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Coroutine, Literal, Optional
|
|
5
|
+
|
|
6
|
+
Decision = Literal["ALLOW", "DENY", "ESCALATE", "MODIFY"]
|
|
7
|
+
|
|
8
|
+
ErrorCode = Literal[
|
|
9
|
+
"UNAUTHORIZED",
|
|
10
|
+
"AGENT_NOT_FOUND",
|
|
11
|
+
"VALIDATION_ERROR",
|
|
12
|
+
"RATE_LIMITED",
|
|
13
|
+
"TIMEOUT",
|
|
14
|
+
"NETWORK_ERROR",
|
|
15
|
+
"UNKNOWN",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TraceStep:
|
|
21
|
+
step: str
|
|
22
|
+
result: Literal["pass", "fail", "skip"]
|
|
23
|
+
detail: Optional[str] = None
|
|
24
|
+
policy_id: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class EvaluateResponse:
|
|
29
|
+
call_id: str
|
|
30
|
+
decision: Decision
|
|
31
|
+
risk_score: int
|
|
32
|
+
trace: list[TraceStep]
|
|
33
|
+
latency_ms: int
|
|
34
|
+
timestamp: str
|
|
35
|
+
reason: Optional[str] = None
|
|
36
|
+
policy_id: Optional[str] = None
|
|
37
|
+
modified_payload: Optional[dict[str, Any]] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class EvaluateRequest:
|
|
42
|
+
agent_id: str
|
|
43
|
+
tool: str
|
|
44
|
+
payload: dict[str, Any]
|
|
45
|
+
session_id: Optional[str] = None
|
|
46
|
+
context: Optional[dict[str, Any]] = None
|
|
47
|
+
credential_name: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class VerdicterConfig:
|
|
52
|
+
api_key: str
|
|
53
|
+
base_url: str = "https://api.verdicter.dev"
|
|
54
|
+
timeout: float = 5.0
|
|
55
|
+
max_retries: int = 2
|
|
56
|
+
retry_backoff: float = 0.2
|
|
57
|
+
fail_open: bool = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class WrapOptions:
|
|
62
|
+
agent_id: str
|
|
63
|
+
session_id: Optional[str] = None
|
|
64
|
+
context: Optional[dict[str, Any]] = None
|
|
65
|
+
credential_name: Optional[str] = None
|
|
66
|
+
on_deny: Optional[Callable[[EvaluateResponse], Any]] = None
|
|
67
|
+
on_escalate: Optional[Callable[[EvaluateResponse], Any]] = None
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verdicter
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Runtime security for AI agents — evaluate tool calls against policies in real time
|
|
5
|
+
Project-URL: Homepage, https://verdicter.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.verdicter.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/QuackaDuck/arbiter-app
|
|
8
|
+
Author: Verdicter
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,ai,policy,runtime,sdk,security,verdicter
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.24.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
27
|
+
Requires-Dist: respx; extra == 'dev'
|
|
28
|
+
Provides-Extra: langchain
|
|
29
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# verdicter
|
|
33
|
+
|
|
34
|
+
**Runtime security for AI agents.** Evaluate every tool call against your policies — get ALLOW, DENY, MODIFY, or ESCALATE in under 50ms.
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/verdicter/)
|
|
37
|
+
[](https://pypi.org/project/verdicter/)
|
|
38
|
+
[](./LICENSE)
|
|
39
|
+
|
|
40
|
+
→ **[Get your free API key at verdicter.dev](https://verdicter.dev)**
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install verdicter
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
With LangChain support:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "verdicter[langchain]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from verdicter import Verdicter
|
|
63
|
+
|
|
64
|
+
client = Verdicter(api_key="verdicter_live_...")
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
result = await client.evaluate(
|
|
68
|
+
agent_id="support_bot", # registered in your Verdicter dashboard
|
|
69
|
+
tool="send_email",
|
|
70
|
+
payload={"to": user_email, "subject": subject, "body": body},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if result.decision == "ALLOW":
|
|
74
|
+
await send_email(payload)
|
|
75
|
+
elif result.decision == "MODIFY":
|
|
76
|
+
await send_email(result.modified_payload) # Verdicter rewrote the payload
|
|
77
|
+
elif result.decision == "DENY":
|
|
78
|
+
raise PermissionError(f"Blocked: {result.reason}")
|
|
79
|
+
elif result.decision == "ESCALATE":
|
|
80
|
+
await request_human_approval(payload)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Wrap a function
|
|
86
|
+
|
|
87
|
+
Zero per-call boilerplate — evaluation happens automatically on every invocation:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from verdicter import Verdicter, WrapOptions
|
|
91
|
+
|
|
92
|
+
client = Verdicter(api_key="verdicter_live_...")
|
|
93
|
+
|
|
94
|
+
safe_send_email = client.wrap(
|
|
95
|
+
"send_email",
|
|
96
|
+
send_email,
|
|
97
|
+
WrapOptions(agent_id="support_bot"),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Policy enforcement is automatic
|
|
101
|
+
await safe_send_email(to=user_email, subject=subject, body=body)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## LangChain adapter
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from verdicter import Verdicter
|
|
110
|
+
from verdicter.adapters.langchain import wrap_tools
|
|
111
|
+
from langchain_core.tools import tool
|
|
112
|
+
|
|
113
|
+
client = Verdicter(api_key="verdicter_live_...")
|
|
114
|
+
|
|
115
|
+
@tool
|
|
116
|
+
def send_email(to: str, subject: str, body: str) -> str:
|
|
117
|
+
"""Send an email."""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
# Wrap your tools — every invocation goes through Verdicter
|
|
121
|
+
safe_tools = wrap_tools([send_email], client, agent_id="support_bot")
|
|
122
|
+
|
|
123
|
+
agent = create_react_agent(llm=llm, tools=safe_tools)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
client = Verdicter(
|
|
132
|
+
api_key="verdicter_live_...",
|
|
133
|
+
timeout=5.0, # seconds, default 5.0
|
|
134
|
+
max_retries=2, # default 2
|
|
135
|
+
fail_open=False, # if True, ALLOW on network errors (default: False = fail closed)
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Decisions
|
|
142
|
+
|
|
143
|
+
| Decision | Meaning |
|
|
144
|
+
|----------|---------|
|
|
145
|
+
| `ALLOW` | Policy passed — run the tool |
|
|
146
|
+
| `DENY` | Policy blocked it — don't run |
|
|
147
|
+
| `MODIFY` | Policy rewrote the payload — use `result.modified_payload` |
|
|
148
|
+
| `ESCALATE` | Needs human review — route to your approval flow |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Links
|
|
153
|
+
|
|
154
|
+
- [Sign up free → verdicter.dev](https://verdicter.dev)
|
|
155
|
+
- [Dashboard → app.verdicter.dev](https://app.verdicter.dev)
|
|
156
|
+
- [Documentation → docs.verdicter.dev](https://docs.verdicter.dev)
|
|
157
|
+
- [npm package → npmjs.com/package/verdicter](https://www.npmjs.com/package/verdicter)
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
MIT License © [Verdicter](https://verdicter.dev)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
verdicter/__init__.py,sha256=3ae6Z7fQML9UwI_vqPyLAK5vPqtkQkAUORdbFWd3doo,396
|
|
2
|
+
verdicter/client.py,sha256=DeGGlaccLsjbPCqtL6YBcjR69jciD_bDMzw3UiIrNts,8451
|
|
3
|
+
verdicter/errors.py,sha256=sSgjgzCa5ffvrlT6AW-Vvokpm88pzoeQTSgLlENFhXY,553
|
|
4
|
+
verdicter/types.py,sha256=tJYhmb-pcfaqaA0UulQn6aBC4O8AdF1xFBnkbnkC9zQ,1540
|
|
5
|
+
verdicter/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
verdicter/adapters/langchain.py,sha256=60Wp-LM4HDe9gsjlKBB9bb9p0_0-heYizCo4JyQ-GmA,2671
|
|
7
|
+
verdicter-1.0.0.dist-info/METADATA,sha256=1Y3VMT0dzf7ShBkY650Nm7Qp7Mbe4-rTGKzBg0Byxmw,4456
|
|
8
|
+
verdicter-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
verdicter-1.0.0.dist-info/RECORD,,
|