Vulnify Python SDK
Ask Vulnify before your AI agent acts. Get ALLOW, REVIEW, or BLOCK, with a risk score and the reasons.
Docs · Dashboard · MCP · Changelog
Vulnify is runtime authorization for AI agents. Before an agent reads, writes, deletes, exports, or emails data, your code sends Vulnify the action metadata (agent, action, resource, destination, record count). Vulnify answers with a decision, a 0 to 100 risk score, and the reasons, and writes the decision to your audit log. Vulnify sees the metadata, not the records.
- Zero dependencies. The core package uses only the standard library. Python 3.9+.
- Fail-closed by default. If Vulnify cannot be reached, the action is blocked unless you opt into
fail_mode="open". - Human review built in.
REVIEWholds the action until a person approves or denies it. - Framework adapters for LangChain, LangGraph, CrewAI, OpenAI Agents, and MCP, plus webhook verification and a policies-as-code CLI.
Quickstart
1. Install
pip install vulnify
2. Create an API key in the dashboard (a TEST key is fine to start) and export it:
export VULNIFY_API_KEY="..."
3. Ask before the action runs
import os
from vulnify import Vulnify
vulnify = Vulnify(api_key=os.environ["VULNIFY_API_KEY"])
decision = vulnify.check(
agent="SalesBot",
action="EXPORT_DATA",
resource="Customer Database",
destination="EXTERNAL_EMAIL",
records_affected=12000,
)
outcome = decision.final_decision or decision.decision # "ALLOW", "REVIEW", or "BLOCK"
if outcome == "ALLOW":
export_customer_records()
else:
print(outcome, decision.risk_score, decision.reasons)
Or let guard() do the branching. The function runs only on ALLOW. Anything else raises VulnifyBlockedError:
from vulnify import VulnifyBlockedError
try:
vulnify.guard(
export_customer_records,
agent="SalesBot",
action="EXPORT_DATA",
resource="Customer Database",
destination="EXTERNAL_EMAIL",
records_affected=12000,
)
except VulnifyBlockedError as err:
print(err.result.decision, err.result.reasons)
@vulnify.protect(agent=..., action=..., resource=...) is the same check as a decorator.
How it works
- Your agent is about to act. Your code calls
check()(orguard(), or an adapter) with the action metadata. - Vulnify evaluates your policies and the risk engine and returns a decision:
ALLOW: run the action.REVIEW: do not run it yet. A person approves or denies it. Poll withwait_for_review(event_id), or passwait={"timeout": 300}toguard().BLOCK: do not run it.
- Every decision lands in the audit log with its score and reasons.
Each decision has a detail view in the dashboard that explains it:
Which field is authoritative
| Field | Meaning |
|---|---|
final_decision |
The authoritative result. REVIEW while a review is pending, ALLOW after approval, BLOCK after denial or expiry. Can be None on replays of decisions made before the field shipped; then use decision. |
decision |
The outcome stored on the event. It does not change when a review is resolved. |
evaluated_decision |
What full enforcement would decide. Differs from decision in monitor mode. |
monitored |
True when the agent runs in monitor mode (recorded, not enforced). |
risk_score / risk_level |
Integer 0 to 100, and its level. |
reasons |
Why Vulnify decided this. |
policy |
The policy that matched, if any. |
dlp_findings / lgpd_categories |
Sensitive-data findings from optional content. The content is scanned and not stored. |
review |
Review state; review["status"] is PENDING, APPROVED, DENIED, or EXPIRED. |
degraded |
True when Vulnify was unreachable and fail_mode decided. |
quota_exceeded / sandbox |
Plan quota flag, and whether this was a TEST-key sandbox event. |
Monitor mode
Roll out without blocking anything: in monitor mode the action is allowed and recorded, and the dashboard shows what enforcement would have done.
Audit log
Every decision, including human reviews, is appended to a per-organization SHA-256 hash chain that you can verify from the dashboard. Evidence and audit exports are a JSON or CSV download that includes a SHA-256 checksum. Settings has "SIEM export: JSON or CEF lines". See Audit log.
Configuration
Vulnify(
api_key="...", # required
base_url=None, # defaults to VULNIFY_BASE_URL, then https://api.vulnify.io
timeout=3.0, # seconds per HTTP request
fail_mode="closed", # "closed" blocks when Vulnify is unreachable; "open" allows
retries=2, # retries after the first attempt, for transient failures only
)
| Option | Default | Description |
|---|---|---|
api_key |
(required) | Your Vulnify API key. The client does not read it from the environment; pass os.environ["VULNIFY_API_KEY"]. |
base_url |
VULNIFY_BASE_URL, else https://api.vulnify.io |
API host. An explicit base_url wins over the environment variable. Local API: http://localhost:3000. |
timeout |
3.0 |
Timeout in seconds for each HTTP request. |
fail_mode |
"closed" |
What check() returns after retries when Vulnify is unreachable (network error, timeout, 408, 429, 5xx): "closed" returns BLOCK with degraded=True, "open" returns ALLOW with degraded=True. |
retries |
2 |
Extra attempts for network errors, 408, 429, and 5xx, with a short backoff. check() reuses the same Idempotency-Key, so a retry never creates a second event. |
Any other HTTP 4xx (bad key, unknown agent or resource, invalid or oversized payload) raises VulnifyError immediately. It is not retried, and fail_mode="open" never turns it into an ALLOW.
| Environment variable | Used by | Description |
|---|---|---|
VULNIFY_BASE_URL |
client, CLI | API host when base_url / --base-url is not given. |
VULNIFY_API_KEY |
CLI | API key; takes precedence over the stored CLI credentials. In your own code, pass it to Vulnify(api_key=...). |
check() arguments
| Argument | Description |
|---|---|
action |
READ_DATA, WRITE_DATA, DELETE_DATA, EXPORT_DATA, or SEND_EMAIL. Required. |
agent / agent_id |
Agent name as registered in Vulnify, or its id. One is required unless the API key is bound to an agent. |
resource / resource_id |
Resource name as registered in Vulnify, or its id. One is required. |
destination |
INTERNAL, EXTERNAL_EMAIL, or EXTERNAL_API. |
records_affected |
Number of records the action touches. |
content |
Optional text scanned for sensitive data. Scanned in memory and not stored. |
idempotency_key |
Optional. Defaults to a fresh UUID per call. |
Other methods: get_event(event_id) reads one decision, and wait_for_review(event_id, timeout=300.0, poll=2.0) returns ALLOW, BLOCK, or TIMEOUT (or APPROVED, DENIED, EXPIRED for older decisions without final_decision).
Framework integrations
vulnify.adapters wraps your tools so they run only on ALLOW. Adapters are duck-typed and import a framework only when they need one of its types, so the core install stays dependency-free. Each adapter takes a describe callback that turns the tool's arguments into check() keywords:
def describe_export(args):
return {
"agent": "SalesBot",
"action": "EXPORT_DATA",
"resource": "Customer Database",
"records_affected": args.get("rows"),
}
| Framework | Install | Adapter |
|---|---|---|
| LangChain | pip install "vulnify[langchain]" |
guard_langchain_tool(vulnify, tool, describe) |
| LangGraph | pip install langgraph |
langgraph_tool_guard(vulnify, {"tool_name": describe}), async: alanggraph_tool_guard |
| OpenAI Agents | pip install "vulnify[openai-agents]" |
guard_openai_agents_tool(vulnify, tool, describe) |
| MCP server | pip install "vulnify[mcp]" |
guard_mcp_handler(vulnify, describe, handler) |
| MCP client | pip install "vulnify[mcp]" |
guard_mcp_client(vulnify, session, {"tool_name": describe}) |
| CrewAI | pip install crewai |
guard_crewai_tool(vulnify, tool, describe), hook: crewai_before_tool_call |
The framework extras require Python 3.10+. A blocked tool answers the agent with Vulnify's reasons instead of running; pass on_blocked="raise" to the tool wrappers (LangChain, OpenAI Agents, CrewAI) to raise VulnifyBlockedError instead. MCP adapters return an MCP error result. All adapters accept wait={"timeout": 300} to wait for a human review. Runnable samples are in examples/.
LangChain
from langchain_core.tools import tool
from vulnify.adapters import guard_langchain_tool
@tool
def export_customers(rows: int) -> str:
"""Export customer records."""
return f"exported {rows}"
export_customers = guard_langchain_tool(vulnify, export_customers, describe_export)
export_customers.invoke({"rows": 3})
LangGraph
from langgraph.prebuilt import ToolNode
from vulnify.adapters import langgraph_tool_guard
node = ToolNode(tools, wrap_tool_call=langgraph_tool_guard(vulnify, {"export_customers": describe_export}))
A call that is not allowed returns an error ToolMessage with the reasons, so the model sees why. Tools not in the map run unchanged.
OpenAI Agents
from agents import function_tool
from vulnify.adapters import guard_openai_agents_tool
@function_tool
def export_customers(rows: int) -> str:
"""Export customer records."""
return f"exported {rows}"
export_customers = guard_openai_agents_tool(vulnify, export_customers, describe_export)
MCP
from vulnify.adapters import guard_mcp_client, guard_mcp_handler
# Inside your MCP server: register the wrapped function.
async def export_customers(rows: int) -> str:
"""Export customer records."""
return f"exported {rows}"
server.tool()(guard_mcp_handler(vulnify, describe_export, export_customers))
# In an MCP client: check listed tools before the request is sent.
session = guard_mcp_client(vulnify, session, {"export_customers": describe_export})
Webhooks
Vulnify can POST decision, anomaly, and test deliveries to an endpoint you register. Each delivery is signed with HMAC-SHA256 in the X-Vulnify-Signature header (t=<unix seconds>,v1=<hex>), keyed with your whsec_ endpoint secret. Verify and parse it with one call, passing the raw request body:
from flask import Flask, request
from vulnify import WebhookVerificationError, construct_webhook_from_request
app = Flask(__name__)
@app.post("/webhooks/vulnify")
def vulnify_webhook():
try:
event = construct_webhook_from_request(SECRET, request.headers, request.get_data())
except WebhookVerificationError:
return "", 400
# event.id is the delivery id (dedupe retries on it); event.event_id is the event.
if event.type == "BLOCK":
print("blocked", event.event_id, event.data.final_decision)
return "", 204
| Function | What it does |
|---|---|
construct_webhook_from_request(secret, headers, raw_body) |
Reads the signature header (case-insensitive), verifies, and parses. |
construct_webhook(secret, signature_header, raw_body) |
Verifies, then parses. |
verify_webhook_signature(secret, signature_header, raw_body) |
Verifies only. Raises WebhookVerificationError. Default tolerance is 300 seconds. |
parse_webhook(raw_body) |
Parses only, into DecisionWebhook, AnomalyWebhook, or TestWebhook. |
Read the raw body with request.get_data() (Flask), await request.body() (FastAPI), or request.body (Django). Do not re-serialize parsed JSON. Samples: Flask, FastAPI, Django. Delivery format: Webhook deliveries.
CLI
Policies as code and one-off decisions from the terminal. Same commands as the npm CLI.
pip install "vulnify[cli]"
vulnify init # writes vulnify/policies/example.yaml and a test file
vulnify login --api-key "$VULNIFY_API_KEY"
vulnify check --agent SalesBot --action EXPORT_DATA --resource "Customer Database" --records 12000
vulnify policies validate # offline schema check with file:line errors
vulnify policies pull --out vulnify/policies
vulnify policies apply --dry-run
vulnify test --local
Every command accepts --json. There is no telemetry. Credentials are stored in ~/.config/vulnify/credentials.json (mode 0600); VULNIFY_API_KEY and VULNIFY_BASE_URL take precedence.
| Exit code | Meaning |
|---|---|
0 |
Success, or ALLOW from check |
1 |
Validation error, test failure, or another failure |
2 |
REVIEW from check |
3 |
BLOCK from check |
4 |
This server has no policies-as-code API yet |
5 |
Authentication error |
Policy file format: Policy files. CLI reference: CLI.
Vulnify MCP server
The Vulnify MCP server gives AI assistants and IDEs tools to check an action, read a decision, and test policies. Cursor, Claude, Grok, or your own code connect to it. It does not run your agent's tools, and it does not sit between the agent and another MCP server.
The recommended path is Sign in with Vulnify (OAuth 2.1). The hosted server is https://mcp.vulnify.io/mcp. Add that URL in the client and leave the API-key and OAuth client fields empty. It speaks Streamable HTTP and keeps no session. After you allow access, the server offers five tools: decide_action, get_decision, list_policies, check_action, and test_policies. It has no approve tool and no deny tool. A person resolves a REVIEW in Vulnify.
Use an API key when you need plan_policy_changes or check_mcp_tool_call, when the client cannot open a browser, or when the key has an IP allowlist. The local process is @vulnify/mcp 0.1.2. It needs Node.js 20 or newer, reads VULNIFY_API_KEY, and does not open the OAuth consent page:
npx -y @vulnify/mcp
On the hosted server, send the same key on every request as Authorization: Bearer <api key> or X-Vulnify-Key: <api key>. An API-key session lists all seven tools.
This is different from two other MCP features:
- The MCP gateway (
POST /v1/gateway/mcpon the API) decides an agent's MCP tool call and, when the decision allows it, forwards the call to a registered upstream MCP server. See MCP gateway. - The MCP adapters in this SDK (
guard_mcp_handler,guard_mcp_client) guard tools inside your own MCP server or client. See MCP above.
Plans and pricing
The SDK is MIT-licensed. Usage limits, retention, and features depend on your Vulnify plan. See vulnify.io/pricing.
Support
- Docs: docs.vulnify.io
- Bugs and feature requests: GitHub issues
- Product support and billing: hello@vulnify.io (Support)
Security
Report vulnerabilities to security@vulnify.io. See the disclosure policy.
Development
pip install -e ".[dev,cli]"
pytest
License
MIT. Copyright 2026 Vulnify.
Metadata
Release files for vulnify 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vulnify-0.4.2.tar.gz | 40.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vulnify-0.4.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 75.3 kB
Release files / vulnify-0.4.2.tar.gz
| Download URL | vulnify-0.4.2.tar.gz |
|---|---|
| Size | 40.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
585bb207c1ee39df865aec62152036e015381528045cd66e61bd5e48767232d9
|
|
BLAKE2b-256 checksum How to use checksums |
1dcbb3a3924c120005422e242807acae21911aed3920ab45ab4931fde019597f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency logRelease files / vulnify-0.4.2-py3-none-any.whl
| Download URL | vulnify-0.4.2-py3-none-any.whl |
|---|---|
| Size | 35.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4fee6bf1947c5303c4879c7d0343dea720102a6705d7535c1cd2b95889aac98b
|
|
BLAKE2b-256 checksum How to use checksums |
01f1c35389a69348825fffd14eac0cba21439fce53d32771eafb67beb47453f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency log