Official Vericto SDK for Python: check SQL before it runs (snowflake-connector-python, sqlite3, SQLAlchemy, any DB-API driver, LangChain, LlamaIndex, dbt and Airflow).
Check every SQL statement with Vericto before it runs: thin wrappers for
snowflake-connector-python,sqlite3, SQLAlchemy and any DB-API driver, and integrations for LangChain, LlamaIndex, dbt and Airflow.
Vericto is a deterministic SQL firewall: it parses each statement into an AST and
evaluates it against your workspace's rules (destructive statements, missing
WHERE/LIMIT, risky DDL, data exposure, sensitive columns, agent allowlists).
This package puts that check in front of your driver's execute(): blocked
statements raise before they reach the database, flagged ones run and are
reported, and statements that read masked columns run as the masked rewrite
Vericto returns.
It is a thin client of the Vericto Runtime API (POST /api/v1/proxy/query).
Evaluation happens in your Vericto workspace; nothing is evaluated locally in this
release (see Evaluators).
- Python 3.10+, typed (
py.typed), one dependency (urllib3). - Drivers are optional extras; importing
verictonever imports a driver.
pip install vericto # core + sqlite3 + generic DB-API wrapper
pip install "vericto[snowflake]" # + snowflake-connector-python
pip install "vericto[sqlalchemy]" # + SQLAlchemy
pip install "vericto[langchain]" # LangChain SQL agents and chains
pip install "vericto[llamaindex]" # LlamaIndex text-to-SQL
pip install "vericto[airflow]" # Airflow 3 operator and cluster policy
pip install "vericto[dbt]" # vericto-dbt: CI checks for dbt projects (no extra dependency)Create an API key with the proxy:evaluate scope in the Vericto dashboard
(API keys) and register the database you are protecting. Then:
| Variable | Meaning | Default |
|---|---|---|
VERICTO_API_KEY |
API key (vtro_...), scope proxy:evaluate |
required |
VERICTO_DATABASE_ID |
The registered database (UUID): its rules, sensitive-column tags and agent allowlists apply | required |
VERICTO_API_URL |
API base URL | https://api.vericto.com |
VERICTO_FAIL_MODE |
closed or open (see Fail mode) |
closed |
VERICTO_TIMEOUT |
Seconds per attempt (see Timeouts and retries) | 5 |
VERICTO_CA_BUNDLE |
Extra CA bundle (PEM) to trust | system store |
A keyword argument beats its variable: Vericto(api_key=..., database_id=..., api_url=..., timeout=..., max_retries=..., fail_mode=...). The CA bundle is an
argument of the evaluator, not of Vericto:
Vericto(RuntimeApiEvaluator(ca_bundle="corp-ca.pem")). An empty or
whitespace-only variable counts as unset (its default applies); an invalid value
raises VerictoConfigError when the guard is built. The key is sent only in the
X-API-Key header, only over HTTPS (plain HTTP is accepted for localhost,
127.0.0.1 and ::1 alone), and never appears in repr(), logs or exception
messages.
import vericto.snowflake
from vericto import VerictoBlocked
conn = vericto.snowflake.connect( # same arguments as snowflake.connector.connect
account="myorg-myaccount",
user="APP",
authenticator="SNOWFLAKE_JWT",
private_key_file="rsa_key.p8",
warehouse="APP_WH",
)
cur = conn.cursor()
cur.execute("SELECT id, email FROM customers WHERE id = %s", (42,))
try:
cur.execute("DELETE FROM customers") # never reaches Snowflake
except VerictoBlocked as exc:
print(exc.verdict.rule_code, exc.suggested_fix)Already have a connection? vericto.snowflake.wrap(conn). Checked entry points:
cursor.execute, executemany, execute_async, connection.execute_string and
execute_stream. Scripts are split with the connector's own splitter and every
statement is evaluated before the first one runs: a block anywhere means
nothing executes.
import time
import vericto.sqlite
conn = vericto.sqlite.connect("app.db") # a real sqlite3.Connection subclass
conn.executescript("CREATE TABLE sessions (id INTEGER PRIMARY KEY, expires_at REAL);") # checked whole first
conn.execute("DELETE FROM sessions WHERE expires_at < ?", (time.time(),))execute, executemany and executescript are checked on the connection and on
its cursors. executescript is split with sqlite3.complete_statement (SQLite's
own tokenizer, so triggers, strings and comments are handled) and evaluated whole
before it runs. See SQLite status.
from sqlalchemy import create_engine, text
import vericto.sqlalchemy
engine = create_engine("sqlite://") # or snowflake://, postgresql+psycopg://, mysql+pymysql://, oracle://, mssql://
remove = vericto.sqlalchemy.instrument(engine) # AsyncEngine works too (see "Sync and async")
with engine.connect() as conn:
print(conn.execute(text("SELECT 1")).scalar_one())
remove()A before_cursor_execute listener checks the compiled SQL of every statement
(ORM, Core and text()). Dialects map from engine.dialect.name: snowflake,
postgresql → postgres, mysql/mariadb → mysql, oracle, mssql,
sqlite. VerictoBlocked propagates unwrapped.
import psycopg
import vericto
from vericto import Vericto
guard = Vericto() # one per process; thread-safe
conn = vericto.wrap(psycopg.connect("dbname=app"), dialect="postgres", guard=guard)
verdict = guard.enforce("SELECT email FROM customers LIMIT 10", "postgres")
run_this = verdict.sql_to_execute # the masked rewrite, if anyMore drivers and every agent framework: Frameworks.
Vericto is the guard: one instance per process (thread-safe, it pools its HTTP
connections). The wrappers call it for you; call it yourself around anything else.
| Method | Does | Raises |
|---|---|---|
enforce(sql, dialect, *, paramstyle=None, context=None, end_user=None) -> Verdict |
Evaluates and applies the decision: logs flags, applies the fail mode. Run verdict.sql_to_execute. |
VerictoBlocked; VerictoUnavailable (fail closed); VerictoConfigError |
prepare(sql, dialect, *, paramstyle=None, context=None, end_user=None) -> str |
enforce(), returning the SQL text to execute (the masked rewrite when there is one). |
as enforce |
enforce_all(statements, dialect, *, context=None, end_user=None) -> list[Verdict] |
enforce() for a batch, every statement before any of them runs. |
as enforce |
check(sql, dialect, *, context=None, end_user=None) -> Verdict |
Asks only: returns the verdict whatever it is, BLOCKED included; under fail_mode="open", an UNEVALUATED verdict with the reason when Vericto cannot answer. |
VerictoUnavailable (fail closed); VerictoConfigError |
require_dialect(dialect) -> str |
Fails fast if the server does not evaluate the dialect; returns the normalized id (postgresql → postgres). |
VerictoUnsupportedDialect |
clear_cache() |
Drops cached verdicts (see Caching). |
paramstyle is the driver's DB-API paramstyle, given only when the statement runs
with parameters (see Parameters and placeholders).
Dialects: postgres, mysql, oracle, mssql, snowflake, sqlite
(KNOWN_DIALECTS; driver names such as postgresql, mariadb, sqlserver and
sqlite3 are accepted). context and end_user are the declared context of the
evaluation: see Business policy context.
from vericto import Decision, Vericto, VerictoBlocked
guard = Vericto(on_verdict=lambda v: print("audit:", v.decision.value, v.rule_code, v.event_id))
verdict = guard.check("DELETE FROM orders", "postgres") # asks only
assert verdict.decision is Decision.BLOCKED and verdict.blocked
print(verdict.rule_code, verdict.severity, verdict.suggested_fix)
sql = guard.prepare("SELECT id, email FROM customers WHERE id = 7", "postgres")
print(sql) # the masked rewrite when email is a masked column
try:
guard.enforce("DELETE FROM orders", "postgres")
except VerictoBlocked as exc:
print(exc) # Query blocked by VERICTO-001 (severity critical). Suggested fix: ...A Verdict (frozen dataclass) carries:
| Field | Meaning |
|---|---|
decision |
Decision.ALLOWED, FLAGGED, MONITORED, PARSE_ERROR, BLOCKED, or UNEVALUATED (local: no verdict under fail_mode="open"). An unknown decision from the server is read as BLOCKED. |
sql, dialect |
The statement as submitted and its dialect. |
rule_code, severity, message |
The deciding rule (VERICTO-001), its severity, and the server's reason when it adds one. |
suggested_fix |
A safer statement Vericto proposes, when it has one. |
rewritten_query |
When set, the statement to run instead of sql (masked columns). sql_to_execute is that or sql; masked says which. |
findings |
Finding(rule_code, severity, rule_id, ast_node_path, suggested_fix) for each rule that matched. |
sensitive_columns, access_denied |
The tagged columns the statement reads, and what the API key's allowlist denied. |
parse_error |
The parser's message, on PARSE_ERROR. |
event_id |
The audit-trail event, to look the evaluation up in Vericto. |
blocked, degraded, cached |
decision is BLOCKED; ran without a verdict (fail-open); came from the cache. |
raw |
The response body, for fields this version does not model. |
| Verdict | What the wrapper does |
|---|---|
ALLOWED |
Runs the statement. |
FLAGGED |
Runs it, logs a warning on the vericto logger, calls on_verdict (and warns with VerictoFlaggedWarning if warn_on_flag=True). |
MONITORED |
Runs it, logs at INFO, calls on_verdict. |
PARSE_ERROR (allowed) |
The workspace's parse_error_action let it through: runs it and logs a warning. When the workspace blocks unparseable SQL, the server answers BLOCKED. |
BLOCKED |
Raises VerictoBlocked (with verdict, findings, suggested_fix) before the driver is called. |
mask (rewritten_query) |
Runs rewritten_query instead of your SQL: the sensitive columns come back masked. Never the original. |
| no verdict | The fail mode decides. |
on_verdict (a callable) receives every verdict, blocked and unevaluated ones
included, for metrics or your own audit log. Log lines carry the rule code,
severity and Vericto event_id, never the SQL text (it can hold personal
data in literals). Every evaluation is recorded in the Vericto audit trail.
When Vericto cannot answer (network error, timeout, HTTP 5xx, a response that is
not JSON or has no decision, rate limit after retries, or the monthly quota
QUOTA_EXCEEDED), check(), enforce() and every wrapper follow the same
contract, shared with vericto-node 0.4.0:
fail_mode="closed"(default): raiseVerictoUnavailable; the statement does not run.fail_mode="open": never raise for it.check()returns anUNEVALUATEDverdict whosemessageis the reason (and logs a warning).enforce()returns one too: the statement runs unevaluated,VerictoFailOpenWarningis emitted, a warning is logged, and the verdict goes toon_verdict. The LangChainVerictoCheckSQLToolanswersUNEVALUATEDwith the reason.
Configuration errors are never failed open: a missing or revoked key (401), a
key without the scope or for another database (403), a malformed request (400) or
an unsupported dialect raise VerictoConfigError in both modes, because failing
open on them would switch the firewall off silently.
Changed in 0.5.0: check() used to raise VerictoUnavailable under
fail_mode="open" too (only the LangChain check tool answered UNEVALUATED). Code
that caught it there now gets the verdict: test
verdict.decision is Decision.UNEVALUATED instead.
from vericto import Vericto, VerictoUnavailable
# Nothing listens on port 9: a network error. Fail closed, the default:
guard = Vericto(api_url="http://127.0.0.1:9", max_retries=0)
try:
guard.enforce("SELECT 1", "postgres")
except VerictoUnavailable as exc:
print("not executed:", exc)
try:
guard.check("SELECT 1", "postgres") # check() follows the fail mode too
raise AssertionError("unreachable: closed raises")
except VerictoUnavailable:
print("check: no verdict")
# Fail open: the statement may run unevaluated, with a VerictoFailOpenWarning,
# and check() returns an UNEVALUATED verdict with the reason instead of raising.
open_guard = Vericto(api_url="http://127.0.0.1:9", max_retries=0, fail_mode="open")
verdict = open_guard.enforce("SELECT 1", "postgres")
assert verdict.degraded and open_guard.prepare("SELECT 1", "postgres") == "SELECT 1"
asked = open_guard.check("DROP TABLE orders", "postgres")
print(asked.decision.value, asked.message) # UNEVALUATED Vericto did not return a verdict ...
assert asked.decision.value == "UNEVALUATED" and asked.messagecheck, enforce, prepare and enforce_all take two optional keyword
arguments with the declared context of the evaluation, which business policies
can read:
| Argument | Sent as | Limits (the server's) |
|---|---|---|
context={"ticket": ...} |
context.ticket |
the change ticket the statement belongs to; 1 to 128 characters, no control characters |
context={"tenant": ...} |
context.tenant |
the tenant it runs for; 1 to 128 characters, no control characters |
end_user={...} |
end_user |
the end user your service acts for: up to 16 attributes; keys are identifiers (a letter or _, then letters, digits or _, at most 63 characters), values 1 to 256 characters, no control characters |
Note: takes effect when business policies are enabled in your workspace. Otherwise the server ignores both fields. The values are asserted by your service: they carry the trust of the API key that sends them, never more.
The limits are checked before anything is sent: an invalid value raises
VerictoConfigError (in both fail modes; the message names the field, never the
value). No other context key is accepted. The verdict cache keys on them too.
vericto.sqlalchemy.instrument and the LangChain and LlamaIndex
VerictoSQLDatabase (and VerictoCheckSQLTool) take context= and end_user= as
a value or as a callable read for every statement, for example a
contextvars.ContextVar's get. Types: vericto.PolicyContext, vericto.EndUser.
from vericto import Vericto, VerictoConfigError
guard = Vericto()
verdict = guard.enforce(
"UPDATE orders SET status = 'refunded' WHERE id = 7",
"postgres",
context={"ticket": "CHG-1042", "tenant": "acme"},
end_user={"id": "u_829", "role": "support"},
)
print(verdict.decision.value) # ALLOWED
try: # checked before it is sent: no request is made
guard.check("SELECT 1", "postgres", end_user={"user-id": "u_829"})
raise AssertionError("unreachable: an invalid key is refused")
except VerictoConfigError as exc:
print(exc) # end_user: keys are identifiers (letters, digits, _), at most 63 characters.timeout (VERICTO_TIMEOUT, default 5 s) bounds each attempt: connecting,
sending and reading the whole response. Connection errors, timeouts, HTTP 5xx,
a 200 response without a decision and 429 RATE_LIMIT_EXCEEDED are retried
max_retries times (default 2) with exponential backoff and jitter
(backoff=0.2 s base on RuntimeApiEvaluator, capped at 2 s, honouring
Retry-After up to that cap). QUOTA_EXCEEDED and configuration errors are not
retried. The worst case before VerictoUnavailable is therefore about
(max_retries + 1) × timeout plus the backoff: about 15.6 s with the defaults.
Lower both for latency-sensitive paths, for example Vericto(timeout=1, max_retries=1).
The first check also asks GET /api/v1/meta/config which dialects the server
evaluates (cached for 10 minutes, dialects_ttl); if that call fails, nothing is
raised for it and the evaluation request decides.
The API is synchronous and blocking: there is no async variant in this
release. Every check is one HTTPS round trip on the calling thread, and the
guard is thread-safe, so share one per process.
- In
asynciocode, run the check in a worker thread so the event loop keeps going:sql = await asyncio.to_thread(guard.prepare, sql, "postgres"). vericto.sqlalchemy.instrument()accepts anAsyncEngine, but the check runs inside SQLAlchemy's execution, on the event loop: it blocks the loop for one round trip per statement.- The framework guides say, for each framework, where the check runs (LangChain, CrewAI and Django run sync tools and ORM calls in worker threads).
Values are never sent to Vericto. The SQL text goes as your application wrote it, placeholders included, and the driver binds the values afterwards.
One adjustment: format/pyformat drivers (psycopg, PyMySQL, and
snowflake-connector-python by default) write parameters as %s / %(name)s,
which is not SQL and would come back as PARSE_ERROR. When such a statement is
executed with parameters, Vericto evaluates it with the dialect's own
placeholders ($1 for PostgreSQL, ? elsewhere) and %% read as %. A masked
rewrite is mapped back to your markers before it runs; if that cannot be done
exactly, PlaceholderMappingError is raised and nothing runs. qmark, numeric
and named drivers, and statements executed without parameters, are sent
byte-for-byte.
Off by default (cache_ttl=0). With Vericto(cache_ttl=30), identical
(sql, dialect) pairs reuse a verdict for 30 seconds (LRU, cache_size=1024),
and the whole cache is dropped when a response reports a new ruleset_version.
Why off: a cached hit is not evaluated, so it is not in the Vericto audit trail and a rule or sensitive-column change takes effect only when the entry expires (the Runtime API does not report the ruleset version today, so the TTL is the only bound). Enable it for hot, repetitive statements where latency matters more than a per-execution audit record, and keep the TTL short.
SQLite has no server, so there is no Vericto proxy for it: this wrapper is the
enforcement point. The Vericto server evaluates the sqlite dialect
(vericto-engine 4.1.0+; GET /api/v1/meta/config lists postgres, mysql,
oracle, mssql, snowflake, sqlite), including SQLite-specific rules such
as ATTACH DATABASE (VERICTO-098), PRAGMA foreign_keys = OFF (VERICTO-099),
load_extension() (VERICTO-100) and VACUUM INTO (VERICTO-101).
vericto.sqlite.connect() and vericto.sqlalchemy.instrument() on a SQLite
engine still ask the server first and raise VerictoUnsupportedDialect before
any SQL runs if it does not list sqlite (for example an older self-hosted
server). There is deliberately no fallback to another dialect: evaluating SQLite
as PostgreSQL would give wrong answers. See Live tests for an
end-to-end check against a real server.
The wrapper protects the SQL that goes through it. Code that opens its own
connection, uses cursor.wrapped, or another process with the same credentials
is not checked. Pair the wrapper with database-side least privilege (and, for
Snowflake, a network policy) when the guarantee matters, and use the Vericto TCP
proxy where one exists (PostgreSQL, MySQL).
All inherit from VerictoError.
| Exception | When |
|---|---|
VerictoBlocked |
Vericto blocked the statement. .verdict, .findings, .suggested_fix, .sql. |
VerictoUnavailable |
No verdict and fail_mode="closed". |
VerictoQuotaExceeded |
Subclass of VerictoUnavailable: monthly quota used (.resets_at). |
VerictoConfigError |
Key, scope, database id, URL or request problem, or an invalid context / end_user. Raised in both fail modes. |
VerictoUnsupportedDialect |
Subclass of VerictoConfigError: the server does not evaluate that dialect. |
PlaceholderMappingError |
A masked rewrite could not be mapped back to %s-style placeholders. |
Vericto takes an evaluator: anything with evaluate(sql, dialect) -> Verdict
and supported_dialects(). When the caller gives context or end_user, the
guard passes them as keyword arguments of the same names (an evaluator without
them raises TypeError then, rather than dropping them). This release ships
RuntimeApiEvaluator. An embedded
evaluator (the Vericto engine in-process with synced rules, for latency and
offline use) can be added later behind the same interface without changing the
wrappers or your code beyond the constructor.
Official integrations are modules this package ships; patterns are frameworks with no vericto module, where the guide shows the few lines that call the guard. Each guide has a runnable example (checked in CI against a local Runtime API stand-in, with scripted models) and the versions it was tested with.
| Framework | Kind | Guide | Tested with |
|---|---|---|---|
LangChain (SQLDatabase, create_sql_agent, create_agent, SQL tools) |
Official: vericto.langchain (vericto[langchain]) |
docs/frameworks/langchain.md | langchain-core 1.7.0, langchain-community 0.4.2, langchain-classic 1.0.8, langchain 1.4.7 |
LlamaIndex (NLSQLTableQueryEngine, SQLTableRetrieverQueryEngine, NLSQLRetriever) |
Official: vericto.llamaindex (vericto[llamaindex]) |
docs/frameworks/llamaindex.md | llama-index-core 0.14.25 |
SQLAlchemy (ORM, Core, text(); AsyncEngine) |
Official: vericto.sqlalchemy (vericto[sqlalchemy]) |
docs/frameworks/sqlalchemy.md | SQLAlchemy 2.1.6 |
Any DB-API driver; sqlite3; snowflake-connector-python |
Official: vericto.wrap, vericto.sqlite, vericto.snowflake |
docs/frameworks/dbapi.md | sqlite3, psycopg 3.3.6 |
| Django | Pattern: connection.execute_wrapper |
docs/frameworks/django.md | Django 6.1.2 |
| CrewAI | Pattern: a guarded @tool |
docs/frameworks/crewai.md | crewai 1.15.30 |
| OpenAI Agents SDK | Pattern: a guarded function_tool |
docs/frameworks/openai-agents.md | openai-agents 0.24.0 |
| Pydantic AI | Pattern: a guarded tool raising ModelRetry |
docs/frameworks/pydantic-ai.md | pydantic-ai-slim 2.60.0 |
| Vanna 2.x | Pattern: a guarded SqlRunner |
docs/frameworks/vanna.md | vanna 2.0.2 |
| dbt | Official: vericto-dbt check (vericto[dbt]) |
dbt | dbt manifest v12 fixtures (no dbt runtime) |
| Airflow 3 | Official: vericto.airflow (vericto[airflow]) |
Airflow | Airflow 3.3.2 (common-sql 2.1.1), official constraints |
| LangChain.js | @vericto/sdk/langchain (Node) |
vericto-node | see vericto-node |
All on Python 3.13 with vericto 0.5.0. The pattern in one line: call
guard.prepare(sql, dialect) right before your code executes sql, execute what
it returns, hand a VerictoBlocked back to the model as text (rule and
suggested_fix), and let VerictoUnavailable / VerictoConfigError propagate so
an outage fails closed. Overview: docs/frameworks.
Each is a thin layer over the guard above; the core API is unchanged.
Runtime paths (agents, Airflow tasks) use the Runtime API with a
proxy:evaluate key, as the wrappers do, and spend the runtime query quota.
vericto-dbt checks SQL that is not being executed, so it uses the CI-check
API (POST /api/v1/ci/check-key, the endpoint of vericto check in
vericto-cli) with a ci_dryrun:execute key, and spends the monthly CI-check
allowance instead.
Full guide: docs/frameworks/langchain.md.
from langchain_community.agent_toolkits import SQLDatabaseToolkit, create_sql_agent
from vericto.langchain import VerictoCheckSQLTool, VerictoSQLDatabase
db = VerictoSQLDatabase.from_uri("postgresql+psycopg://app@db/app") # every SQLDatabase argument works
agent = create_sql_agent(llm, toolkit=SQLDatabaseToolkit(db=db, llm=llm), extra_tools=[VerictoCheckSQLTool(db=db)])VerictoSQLDatabase is a langchain_community.utilities.SQLDatabase (also
VerictoSQLDatabase(engine, guard=..., dialect=..., on_block=..., context=..., end_user=...)). Every query
it runs, and the sample rows get_table_info puts in the prompt, is checked
after SQLAlchemy compiles it; schema reflection is not sent.
- BLOCKED,
on_block="return"(default): nothing runs;run/run_no_throw(soQuerySQLDatabaseTool) return a message the model can act on:Error: Vericto blocked this query (VERICTO-010, severity critical). It was NOT executed.+ the reason +Suggested fix: ....on_block="raise"raisesVerictoBlockedinstead (run(fetch="cursor")always raises). - Mask: the masked rewrite runs, sample rows included. FLAGGED /
MONITORED: run, logged, passed to
on_verdict. - Vericto unavailable: under
fail_mode="closed"(default) raised, never handed to the model;fail_mode="open"runs the query unevaluated. A configuration error is raised in both modes. - A blocked sample-rows query is left out of the table info instead of failing.
VerictoCheckSQLTool(vericto_check_sql) only asks: it returns ALLOWED, FLAGGED, the masked query that would run, or the block with its fix; underfail_mode="open",UNEVALUATEDwith the reason when Vericto cannot answer. Each call is one Runtime API evaluation.context=andend_user=(a value, or a callable read per query) go with every evaluation; see Business policy context.- An engine already instrumented with
vericto.sqlalchemy.instrumentis not checked twice (new helper:vericto.sqlalchemy.is_instrumented).
Full guide: docs/frameworks/llamaindex.md.
from llama_index.core.query_engine import NLSQLTableQueryEngine
from vericto.llamaindex import VerictoSQLDatabase
sql_database = VerictoSQLDatabase(engine, include_tables=["orders"], on_block="return")
query_engine = NLSQLTableQueryEngine(sql_database=sql_database, tables=["orders"], llm=llm)A llama_index.core.SQLDatabase: run_sql and insert_into_table are checked.
With on_block="return" (default) a blocked run_sql returns the same
explanation as the SQL result (metadata vericto_blocked), so the synthesized
answer says why; "raise" raises VerictoBlocked (NLSQLTableQueryEngine
turns any retrieval error into Error: ... text itself; NLSQLRetriever(..., handle_sql_errors=False) lets it through). Masks, flags and unavailability as
for LangChain.
dbt compile # or: dbt build --empty
VERICTO_API_KEY=vtro_... vericto-dbt check # table, exit 1 on BLOCKED
vericto-dbt check --fail-on flagged --format sarif --output vericto.sarif
python -m vericto dbt check --format json # same commandReads target/manifest.json (and target/compiled, or target/run with
--sql-source run) and checks, for the root project, the compiled SQL of every
model and snapshot, their pre-hook/post-hook statements and the
on-run-start/on-run-end hooks. Seeds have no SQL; tests and analyses are
opt-in (--resource-type), packages too (--include-packages); --select
takes globs on name, unique id or path. Hooks that still contain Jinja (dbt
renders them only at run time) are listed as skipped. dbt is never run or
imported: only its artifacts are read.
- Dialect from the manifest's adapter:
snowflake,postgres,redshift(aspostgres, said in the output),mysql/mariadb,sqlserver(asmssql),oracle. Any other adapter stops with exit code 2 before anything is sent;--dialectoverrides. - Exit codes, as vericto-cli:
0pass,1a finding at or above--fail-on(blockeddefault,flagged,any),2usage (no manifest, nothing compiled, unsupported adapter),3key/scope/monthly allowance,4Vericto unreachable. - Privacy: if the workspace is in
sanitizedtelemetry mode, literals are replaced by placeholders before sending (vericto-cli's sanitizer, ported); if that setting cannot be read nothing is sent unless--allow-unsanitized. --database-id/VERICTO_DATABASE_IDapplies that database's sensitive-column tags. Runs carry GitHub/GitLab provenance and appear in the dashboard's CI reports.- SARIF points at the model file (line 1: compiled lines do not map to the Jinja source; the message gives the compiled line). GitHub Actions example: examples/dbt_github_actions.yml.
The command is vericto-dbt, not vericto dbt: vericto is vericto-cli's
binary, and a Python script of the same name would shadow it on PATH.
Airflow 3 (vericto[airflow] requires apache-airflow>=3.0). Airflow 2 is
end-of-life upstream
since 2026-04-22 and is no longer tested. vericto 0.2.x remains installable from
PyPI for Python 3.9 and Airflow 2 users (pip install "vericto[airflow]<0.3").
from vericto.airflow import VerictoSQLExecuteQueryOperator
VerictoSQLExecuteQueryOperator(task_id="purge", conn_id="warehouse", sql="sql/purge.sql")Or guard the SQL tasks you already have, in airflow_local_settings.py on the
DAG processor and the workers (example):
from vericto.airflow.policy import guard_sql_tasks
def task_policy(task):
guard_sql_tasks(task) # every common-sql operator (SQLExecuteQueryOperator, SQL*Check, subclasses)- The rendered SQL is checked when the task runs (the policy installs the
check as the task's
pre_executehook, after any existing one; the class and serialization are unchanged). Scripts are split with the hook'ssplit_sql_stringand every statement is evaluated before the first runs. - BLOCKED: the task fails with
VerictoTaskBlocked(aVerictoBlockedand anAirflowFailException, so it is not retried) and nothing runs. Mask: the rewrite runs. FLAGGED / MONITORED: run and are logged. Unavailable:fail_mode="closed"fails the task (retried like any error). - Dialect from the connection type (
postgres,redshiftas postgres,mysql,mssql,oracle,snowflake,sqlite),vericto_dialectin the connection extra (for ODBC/JDBC), orvericto_dialect=on the operator. Others fail before running (on_unsupported="skip"runs them unchecked, with a warning). - The guard is built from the worker's
VERICTO_*environment on first use; DAG parsing never calls Vericto.
- Embedded evaluator, and SQLite's
set_authorizeras a second deny layer (ATTACH,load_extension). - dbt: a run-time
on-run-startmacro; LlamaIndex.TS.
uv sync # Python 3.10+; uv.lock pins the dev tools
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv run pytestTests use a local stand-in for the Runtime API and the CI-check API
(pytest-httpserver), the real sqlite3 module, SQLAlchemy on SQLite, and a fake
Snowflake connection (the real connector's statement splitter is used); nothing
connects to Snowflake in CI. The integration tests (tests/test_langchain.py,
test_llamaindex.py, test_airflow.py) skip unless their framework is
installed; CI runs each in its own job against the versions pinned in
ci/requirements-*.txt (Airflow with its official constraints file), with
scripted LLMs and no network calls:
uv venv /tmp/vericto-lc && uv pip install --python /tmp/vericto-lc -e . -r ci/requirements-langchain.txt
/tmp/vericto-lc/bin/python -m pytest tests/test_langchain.pyEvery python block of this README and of docs/frameworks/*.md runs in CI
against scripts/docs_stub.py, a local stand-in for the Runtime API with the real
service's status codes and bodies (it is not the engine: its few rules are string
checks). Each block runs in its own interpreter and temporary directory; a block
marked <!-- docs-check: skip REASON --> is listed but not run, and
<!-- docs-check: requires=module --> skips it unless the module is installed
(--strict makes that a failure, as in the framework jobs).
uv run python scripts/check_docs.py README.md docs/frameworks/dbapi.md docs/frameworks/sqlalchemy.md
uv venv /tmp/vericto-docs-lc && uv pip install --python /tmp/vericto-docs-lc -e . -r ci/requirements-docs-langchain.txt
/tmp/vericto-docs-lc/bin/python scripts/check_docs.py --strict docs/frameworks/langchain.mdpython scripts/docs_stub.py runs the stand-in on its own and prints its URL
(VERICTO_API_URL; any vtro_... key works, vtro_revoked answers 401).
CI measures line and branch coverage and fails a job under 80%
(fail_under in pyproject.toml). The core job reports every module except the
three integrations, whose framework it does not install; each integration job
reports its own module (langchain.py, llamaindex.py, dbt.py, airflow/).
The same checks locally:
uv run coverage run -m pytest
uv run coverage report --omit="*/vericto/langchain.py,*/vericto/llamaindex.py,*/vericto/airflow/*"
uv pip install --python /tmp/vericto-lc coverage==7.17.0
/tmp/vericto-lc/bin/python -m coverage run -m pytest tests/test_langchain.py
/tmp/vericto-lc/bin/python -m coverage report --include=src/vericto/langchain.pyA test that documents a known bug is marked xfail(strict=True) with the reason,
so it fails the run as soon as the bug is fixed and the marker must go.
tests/live/ holds opt-in tests against real services. They skip unless their
variables are set and never run in CI.
test_snowflake_live.py: a real Snowflake account (its own temporary schema, dropped at the end) with the local Runtime API stand-in. SetVERICTO_LIVE_SNOWFLAKE=1and theSNOWFLAKE_*variables it lists.test_sqlite_live.py: a real SQLite file in a temporary directory, guarded byvericto.sqlite.connect()against a real Vericto Runtime API. Benign statements (CREATE TABLE,INSERTwith?binds,executemany, UPSERTON CONFLICT ... DO UPDATE,SELECT,DELETE ... WHERE) must run;DELETEwithoutWHERE(VERICTO-001),ATTACH DATABASE(VERICTO-098),load_extension()(VERICTO-100) andVACUUM INTO(VERICTO-101) must raiseVerictoBlockedbefore SQLite sees them (checked with SQLite's trace callback, the file's SHA-256 and the absence of the attached/copy files);PRAGMA foreign_keys = OFF(VERICTO-099) must run with aVerictoFlaggedWarning; anexecutescriptwith a blocked statement in the middle must run none of it. The database's workspace must use the default rules; a run makes about 30 evaluations, counted against the workspace's monthly quota.
| Variable | Value |
|---|---|
VERICTO_LIVE_SQLITE |
1 |
VERICTO_API_KEY |
a vtro_... key with the proxy:evaluate scope |
VERICTO_API_URL |
https://api-staging.vericto.com (or another server that lists sqlite) |
VERICTO_DATABASE_ID |
the id of a database of dialect sqlite in that workspace |
On the host (Python 3.10+, the SQLite file goes to pytest's temporary directory):
export VERICTO_LIVE_SQLITE=1 VERICTO_API_URL=https://api-staging.vericto.com
export VERICTO_API_KEY=vtro_... VERICTO_DATABASE_ID=...
uv run pytest tests/live/test_sqlite_live.py -vOr in a throwaway python:3.13-slim container (the repository is mounted
read-only and copied; the SQLite file never leaves the container):
docker run --rm -v "$PWD":/src:ro \
-e VERICTO_LIVE_SQLITE=1 -e VERICTO_API_KEY -e VERICTO_API_URL -e VERICTO_DATABASE_ID \
python:3.13-slim sh -c 'cp -r /src /work && cd /work && rm -rf .venv \
&& pip install --quiet --no-cache-dir . pytest==9.1.1 pytest-httpserver==1.2.0 \
&& python -m pytest tests/live/test_sqlite_live.py -v -p no:cacheprovider'A skipped run (1 skipped per test) means a variable is missing.
See CONTRIBUTING.md for the checks CI runs, the coverage gate, and how releases are cut.
See SECURITY.md. Report vulnerabilities to security@vericto.com, not in public issues.
vericto accepts urllib3>=1.26.18,<3 so it fits the environments it is
installed into, but use urllib3 2.8.0 or later: older releases are affected by
GHSA-pq67-6m6q-mj2v, GHSA-gm62-xv2j-4w53, GHSA-2xpw-w6gg-jr37,
GHSA-38jv-5279-wg99, GHSA-qccp-gfcp-xxvc, GHSA-8988-9cw3-xx77 and
GHSA-vxq7-64xx-v4gw. The repository's dev lock (uv.lock) pins 2.8.0.
Open source under the Apache License 2.0 (SPDX Apache-2.0) since
0.4.0. Versions 0.3.2 and earlier were released under the Elastic License 2.0
and remain under it. Copyright 2026 Vericto S.A.S.; see NOTICE. The
license grants no right to the Vericto name or logo: see
TRADEMARKS.md. The Vericto service the SDK calls is offered
under its own terms.
vericto valida cada sentencia SQL con Vericto antes de ejecutarla, envolviendo
el execute() del driver: snowflake-connector-python, sqlite3, SQLAlchemy
(cualquier dialecto) o cualquier driver DB-API.
- BLOCKED: lanza
VerictoBlocked(con hallazgos y corrección sugerida) y la sentencia no llega a la base de datos. - FLAGGED / MONITORED: se ejecuta y se registra (logger
vericto, callbackon_verdict). - Máscara de columnas sensibles: se ejecuta
rewritten_query, nunca la original. - Sin respuesta de Vericto:
fail_mode="closed"(por defecto) lanzaVerictoUnavailableencheck(),enforce()y los wrappers;"open"nunca lanza por eso:check()devuelve un veredictoUNEVALUATEDcon el motivo yenforce()ejecuta y emiteVerictoFailOpenWarning(desde 0.5.0; antescheck()lanzaba también en"open"). Los errores de configuración (clave inválida, permiso, dialecto) nunca se dejan pasar. - Contexto de políticas de negocio:
check,enforce,prepareyenforce_allaceptancontext={"ticket": ..., "tenant": ...}yend_user={...}, validados con los límites del servidor antes de enviarlos. Tienen efecto cuando las políticas de negocio están habilitadas en tu workspace.
Configuración: VERICTO_API_KEY (clave vtro_... con el scope proxy:evaluate),
VERICTO_DATABASE_ID y, opcionalmente, VERICTO_API_URL, VERICTO_FAIL_MODE,
VERICTO_TIMEOUT y VERICTO_CA_BUNDLE. Los valores de los parámetros nunca se
envían a Vericto.
Integraciones oficiales: vericto.langchain y vericto.llamaindex (un
SQLDatabase que valida cada consulta del agente; si se bloquea, el agente recibe
la regla y la corrección sugerida en vez de una excepción), vericto.sqlalchemy,
vericto.wrap para cualquier driver DB-API, vericto-dbt check (valida el SQL
compilado de un proyecto dbt en CI con la API de CI-check, con salida JSON/SARIF)
y vericto.airflow (operador y política de clúster). Para Django, CrewAI, el
OpenAI Agents SDK, Pydantic AI y Vanna no hay módulo de vericto: las guías de
docs/frameworks muestran el patrón (llamar a
guard.prepare(sql, dialecto) justo antes de ejecutar y ejecutar lo que
devuelve), con ejemplos probados y las versiones usadas. Ver
Frameworks.
La API es síncrona: en código asyncio, use
await asyncio.to_thread(guard.prepare, sql, "postgres"). VERICTO_TIMEOUT es
por intento; con los valores por defecto (5 s y 2 reintentos), Vericto responde o
falla cerrado en unos 16 s como máximo. Una variable VERICTO_* vacía cuenta como
no definida (desde 0.4.1 también VERICTO_FAIL_MODE).
Desde 0.3.0, vericto requiere Python 3.10+ y vericto[airflow] requiere
Airflow 3: Airflow 2 llegó al fin de su ciclo de vida
(versiones soportadas)
y ya no se prueba. Quienes usen Python 3.9 o Airflow 2 pueden seguir instalando
0.2.x (pip install "vericto<0.3"). Se recomienda urllib3 2.8.0 o posterior (ver
Security).
SQLite: el servidor de Vericto evalúa el dialecto sqlite (vericto-engine
4.1.0+). vericto.sqlite.connect() igual consulta GET /api/v1/meta/config y,
si un servidor no lo anuncia, falla con VerictoUnsupportedDialect sin ejecutar
nada.
Licencia: código abierto bajo la Apache License 2.0 desde 0.4.0. Las versiones 0.3.2 y anteriores se publicaron bajo la Elastic License 2.0 y siguen bajo ella. La licencia no otorga derechos sobre la marca Vericto (TRADEMARKS.md).