vulnify
Runtime authorization for AI agents. Before the agent exports or sends customer records, your app asks Vulnify. The decision is ALLOW, REVIEW, or BLOCK. The score is an integer from 0 to 100 and comes back with reasons.
Vulnify sees action metadata — agent, action, resource, destination, and record count — not the records. In monitor mode the event is stored and not enforced: obey decision. evaluated_decision is what enforcement would have returned, and monitored is True. If Vulnify cannot be reached, the default is fail-closed.
Standard library only. Python 3.9+.
Production API: https://api.vulnify.io
Documentation: https://docs.vulnify.io
Install
pip install vulnify
Production scenario
An agent is about to export customer records to an external destination. Call check() first.
ALLOWruns the export.REVIEWstops and tells the caller a human must approve. The export does not run.BLOCKdoes not run the export.- If Vulnify cannot be reached,
check()returnsBLOCKwithdegraded=True. The export does not run.
Set VULNIFY_API_KEY. With no base_url, this calls https://api.vulnify.io. For a local API, set VULNIFY_BASE_URL=http://localhost:3000 or pass base_url="http://localhost:3000". An explicit base_url wins over VULNIFY_BASE_URL.
import os
import sys
from vulnify import Vulnify
def export_customer_records() -> None:
"""Replace the body with the real export. It runs only after ALLOW."""
print("exporting customer records to the external destination")
def main() -> None:
api_key = os.environ.get("VULNIFY_API_KEY")
if not api_key:
raise SystemExit("Set VULNIFY_API_KEY")
# Local API: Vulnify(api_key=api_key, base_url="http://localhost:3000")
vulnify = Vulnify(api_key=api_key)
decision = vulnify.check(
agent="SalesBot",
action="EXPORT_DATA",
resource="Customer Database",
destination="EXTERNAL_EMAIL",
records_affected=12000,
)
if decision.decision == "ALLOW":
export_customer_records()
return
reasons = "; ".join(decision.reasons) or "no reason given"
if decision.decision == "REVIEW":
score = "unknown" if decision.risk_score is None else str(decision.risk_score)
event_id = decision.id or "none"
raise SystemExit(
f"A human must approve this export before it runs (event {event_id}, score {score}). {reasons}"
)
if decision.degraded:
raise SystemExit(f"Vulnify could not be reached. The export was not run. {reasons}")
raise SystemExit(f"Export blocked. The export was not run. {reasons}")
if __name__ == "__main__":
try:
main()
except Exception as err:
print(err, file=sys.stderr)
raise SystemExit(1)
A REVIEW is approved on the Vulnify server (Slack, an MFA step-up, or a separate approver). This process does not approve it and does not poll. A separate MCP or HTTP gateway injects secrets only after ALLOW.
guard(fn, **action) runs fn only for ALLOW and raises VulnifyBlockedError for REVIEW and BLOCK. Pass wait={"timeout": 300} only when you mean to poll until someone approves. Approval is review status APPROVED while decision remains REVIEW. protect(**action) is the same check as a decorator. Retries reuse the same Idempotency-Key, so a retry does not create a second event. get_event and wait_for_review use those same retries; if the API is still unavailable they raise VulnifyError instead of applying fail_mode.
Decisions
ALLOW— run the action.risk_scoreis 0–100.reasonsexplains the score.REVIEW— do not run the action yet.review["status"]starts asPENDING. Tell the caller a human must approve. When the review is resolved,decisionstaysREVIEW. The go signal isreview["status"] == "APPROVED".DENIEDandEXPIREDmean the action must not run. A laterget_eventdoes not turn an approval intodecision="ALLOW".BLOCK— do not run the action.
Follow decision in monitor mode as well. An invalid API key, an unknown agent or resource, a rejected payload, or a body the API refuses as too large (413) raises VulnifyError. The same is true of any other HTTP 4xx except 408 and 429. fail_mode="open" does not swallow those errors and does not retry them.
get_event reads GET /v1/events/{id}. That response currently omits quotaExceeded and sandbox. The SDK does not invent them: quota_exceeded and sandbox stay at the default False, which is not a value the GET returned. check() includes both fields.
Optional content is scanned for sensitive data and is not stored. Matches return on dlp_findings.
Fail-closed
fail_mode defaults to "closed". A timeout, a network error, 408, 429, or a 5xx becomes decision="BLOCK", degraded=True, and a reason beginning with Vulnify unavailable. The scenario above does not call export_customer_records(). Set fail_mode="open" only when an outage should let the action through. A 413 or any other non-retryable 4xx never takes that path.
Audit events are hash-chained. SIEM export is JSON or CEF. Evidence in the product maps to LGPD, ISO/IEC 42001, NIST AI RMF, and the EU AI Act. That mapping is not a certification.
vulnify.adapters guards CrewAI and LangGraph tools without importing either framework. Samples are in examples/.
Development
pip install -e ".[dev]"
pytest
License
MIT. Copyright 2026 Vulnify.
Metadata
Release files for vulnify 0.2.0
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.2.0.tar.gz | 12.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vulnify-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.4 kB
Release files / vulnify-0.2.0.tar.gz
| Download URL | vulnify-0.2.0.tar.gz |
|---|---|
| Size | 12.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ce5a29c9448ec0831e7c4a2b5e9b33b15ed51e3d9b36c2055ed0fa6eabf24881
|
|
BLAKE2b-256 checksum How to use checksums |
ac8c39100c76159b126aa87429ea0ec352c47448c50eefbd8bdf63f0c6a31442
|
| 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 2, 2026.
Transparency logRelease files / vulnify-0.2.0-py3-none-any.whl
| Download URL | vulnify-0.2.0-py3-none-any.whl |
|---|---|
| Size | 11.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1b1804e4918e41e941af766e4474b100c847459d495ad9b5fe3d779305cdfa8f
|
|
BLAKE2b-256 checksum How to use checksums |
3549ed2b268d8a9792972e8f653a2599d936f509ab0add1584ada0f94b5c9a25
|
| 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 2, 2026.
Transparency log