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 | 5 |
VERICTO_CA_BUNDLE |
Extra CA bundle (PEM) to trust | system store |
Every variable has a keyword-argument equivalent on Vericto(...). The key is
sent only in the X-API-Key header, only over HTTPS (plain HTTP is accepted for
localhost 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 vericto.sqlite
conn = vericto.sqlite.connect("app.db") # a real sqlite3.Connection subclass
conn.execute("DELETE FROM sessions WHERE expires_at < ?", (now,))
conn.executescript(open("migration.sql").read())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
import vericto.sqlalchemy
engine = create_engine("snowflake://...") # or postgresql://, mysql://, oracle://, mssql://, sqlite://
remove = vericto.sqlalchemy.instrument(engine) # AsyncEngine works tooA 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 any| 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, rate limit after
retries, or the monthly quota QUOTA_EXCEEDED):
fail_mode="closed"(default): raiseVerictoUnavailable; the statement does not run.fail_mode="open": run the statement unevaluated, emitVerictoFailOpenWarning, log a warning, and pass anUNEVALUATEDverdict toon_verdict.
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.
Retries: connection errors, timeouts, 5xx and 429 RATE_LIMIT_EXCEEDED are
retried max_retries times (default 2) with exponential backoff and jitter
(capped at 2 s, honouring Retry-After). QUOTA_EXCEEDED is not retried.
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. 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(). 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.
Each is a thin layer over the guard above; the core API is unchanged.
| Integration | Install | What you get | Evaluated by | Tested with |
|---|---|---|---|---|
| LangChain | vericto[langchain] |
VerictoSQLDatabase for QuerySQLDatabaseTool, SQLDatabaseToolkit, create_sql_agent, SQL chains; VerictoCheckSQLTool |
Runtime API | langchain-community 0.4.2, langchain-core 1.6.10, langchain-classic 1.0.8 |
| LlamaIndex | vericto[llamaindex] |
VerictoSQLDatabase for NLSQLTableQueryEngine, SQLTableRetrieverQueryEngine, NLSQLRetriever |
Runtime API | llama-index-core 0.14.25 |
| dbt | vericto[dbt] |
vericto-dbt check: compiled models, snapshots and hooks, table / JSON / SARIF |
CI-check API | dbt manifest v12 fixtures (no dbt runtime) |
| Airflow | vericto[airflow] |
VerictoSQLExecuteQueryOperator; cluster policy guard_sql_tasks |
Runtime API | Airflow 3.3.2 (common-sql 2.1.1), official constraints |
| LangChain.js | @vericto/sdk (Node) |
@vericto/sdk/langchain |
Runtime API | see vericto-node |
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.
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=...)). 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 or misconfigured: raised, never handed to the model
(
fail_mode="open"runs the query unevaluated). - 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. Each call is one Runtime API evaluation.- An engine already instrumented with
vericto.sqlalchemy.instrumentis not checked twice (new helper:vericto.sqlalchemy.is_instrumented).
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.pyCI 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.16.2
/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.
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) bloquea;"open"ejecuta y emiteVerictoFailOpenWarning. Los errores de configuración (clave inválida, permiso, dialecto) nunca se dejan pasar.
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. Los valores de los parámetros nunca se envían a Vericto.
Integraciones (0.2.0): 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-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). Ver
Integrations.
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.