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 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
+ [![PyPI version](https://img.shields.io/pypi/v/verdicter)](https://pypi.org/project/verdicter/)
37
+ [![Python versions](https://img.shields.io/pypi/pyversions/verdicter)](https://pypi.org/project/verdicter/)
38
+ [![license](https://img.shields.io/pypi/l/verdicter)](./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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any