Skip to main content

Vulnify: decide every AI agent action before it runs

Vulnify Python SDK

Ask Vulnify before your AI agent acts. Get ALLOW, REVIEW, or BLOCK, with a risk score and the reasons.

PyPI version Python versions Downloads License: MIT CI

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. REVIEW holds 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

An AI agent asks Vulnify before acting. ALLOW: it runs. REVIEW: it waits for a person. BLOCK: it never starts.

  1. Your agent is about to act. Your code calls check() (or guard(), or an adapter) with the action metadata.
  2. 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 with wait_for_review(event_id), or pass wait={"timeout": 300} to guard().
    • BLOCK: do not run it.
  3. Every decision lands in the audit log with its score and reasons.

Each decision has a detail view in the dashboard that explains it:

Vulnify dashboard: a SalesBot export of 12,000 customer records to an external email, blocked with risk score 100

Vulnify dashboard: a SupportBot read of one internal record, allowed with risk score 30

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.

Vulnify dashboard in monitor mode: an export that was allowed and recorded, and would have been blocked with enforcement on

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.

Vulnify audit log listing decisions by agent, action, resource, destination, risk, and decision

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/mcp on 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

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)

Source distribution for vulnify 0.4.2
File Size Uploaded
vulnify-0.4.2.tar.gz 40.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vulnify 0.4.2
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.4.2 This release

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page