Official Vericto SDK for Node.js: database guardrails for AI agents. Check SQL before it runs (snowflake-sdk, better-sqlite3, SQL tools for the Vercel AI SDK, the OpenAI Agents SDK and Mastra, LangChain.js SQL agents and chains, and any driver through the Runtime API client).
Check every SQL statement with Vericto before it runs: thin wrappers for
snowflake-sdkandbetter-sqlite3, guarded SQL tools for the Vercel AI SDK, the OpenAI Agents SDK and Mastra, a LangChain.js integration for SQL agents, and a client for the Vericto Runtime API you can put in front of any driver.
Vericto puts database guardrails between AI agents and your database: 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). Deterministic: same statement, same rules,
same verdict.
This package checks SQL before your driver runs it: blocked statements reject
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. To try it with no account,
offline mode checks SQL with the engine's standard rules
in a local vericto serve process instead (the Vericto CLI, installed separately).
- TypeScript types, ESM and CommonJS builds, Node.js 18+ (uses the global
fetch). - No runtime dependencies. Drivers and agent frameworks are optional peer
dependencies and separate entry points: importing
@vericto/sdknever loads them. The Vericto CLI that offline mode starts is an optional peer too (@vericto/vericto-cli>= 2.1.0), never installed by this package. - Asynchronous only: a check is a network call (offline, a round trip to a local process), so every API that checks SQL returns a promise. There is no synchronous variant.
Python (snowflake-connector-python, sqlite3, SQLAlchemy, LangChain, LlamaIndex, dbt and Airflow) lives in vericto-python.
- Install · Configure · Quickstart · Offline mode (no account)
- Core API:
Vericto,Verdict, helpers, options - What each decision does · Fail mode · Business policy context · Timeouts and retries · Errors
- Drivers: snowflake-sdk · better-sqlite3
- Frameworks: the Vercel AI SDK, the OpenAI Agents SDK, Mastra and LangChain.js (official entry points), and tested patterns for Prisma, Drizzle, Knex, node-postgres and mysql2
- Caching · SQLite status · Enforcement boundary · Evaluators · Development
npm install @vericto/sdkCreate an API key with the proxy:evaluate scope in the Vericto dashboard
(API keys) and register the database you are protecting. Then:
| Variable | Option | Meaning | Default |
|---|---|---|---|
VERICTO_API_KEY |
apiKey |
API key (vtro_...), scope proxy:evaluate |
required |
VERICTO_DATABASE_ID |
databaseId |
The registered database (UUID): its rules, sensitive-column tags and agent allowlists apply | required |
VERICTO_API_URL |
apiUrl |
API base URL | https://api.vericto.com |
VERICTO_FAIL_MODE |
failMode |
closed or open (see Fail mode) |
closed |
VERICTO_TIMEOUT |
timeoutMs |
Per-attempt timeout (env in seconds, option in ms) | 5 s |
VERICTO_OFFLINE |
offline |
1/true: offline mode, no key needed |
off |
VERICTO_CLI |
offlineCliPath |
Offline mode: the vericto binary |
see Offline mode |
An option beats its variable. An empty or whitespace-only variable counts as
unset, so it gets the default; an invalid value throws VerictoConfigError from
the constructor. options.env replaces process.env as the source of the
variables (useful in tests and workers).
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
messages, toString() or JSON.stringify(). To trust an extra CA, use Node's
NODE_EXTRA_CA_CERTS; to go through a proxy or a custom agent, pass your own
fetch.
Ask Vericto, then run the SQL it returns. One Vericto per process; it reads the
variables above.
import { Vericto, VerictoBlockedError, sqlToExecute } from '@vericto/sdk';
const vericto = new Vericto();
// Allowed, flagged or masked: run sqlToExecute(verdict), never the original.
const verdict = await vericto.enforce('SELECT id, email FROM users WHERE id = 1', 'postgres');
console.log(verdict.decision, verdict.ruleCode, sqlToExecute(verdict));
// Blocked: enforce() rejects, and the statement must not run.
try {
await vericto.enforce('DELETE FROM users', 'postgres');
} catch (err) {
if (!(err instanceof VerictoBlockedError)) throw err; // unavailable or misconfigured: fail closed
console.log(err.verdict.ruleCode, err.suggestedFix);
}Against the documentation stub (scripts/docs-stub.mjs, which masks email and
blocks a DELETE without WHERE), this prints:
FLAGGED VERICTO-085 SELECT id, '***' AS email FROM users WHERE id = 1
VERICTO-001 DELETE FROM users WHERE <condition>
vericto.prepare(sql, dialect) is enforce resolving the SQL text to run, the
shortest way to put Vericto in front of any driver:
import { Vericto } from '@vericto/sdk';
const vericto = new Vericto();
/** Any driver: check, then run what Vericto returns. */
export async function guardedQuery<T>(run: (sql: string) => Promise<T>, sql: string): Promise<T> {
return run(await vericto.prepare(sql, 'postgres'));
}
console.log(await guardedQuery(async (sql) => `ran: ${sql}`, 'SELECT 1'));For a specific driver or framework, see Drivers and Frameworks.
Check SQL with no account, no API key and no network: offline mode sends each
statement to a local vericto serve --stdio process and gets the verdict of the
Vericto engine's standard rules (42 rules at their default severities). It is
opt-in, never automatic: a missing API key stays an error unless you ask for
offline mode.
It needs the Vericto CLI 2.1.0 or later, a separate program. Add it to your app as a dev dependency (or an optional peer dependency), or install it any other way:
npm install @vericto/sdk
npm install --save-dev @vericto/vericto-cli@^2.1.0 # or: brew install vericto/tap/verictoimport { Vericto, VerictoBlockedError } from '@vericto/sdk';
// Or VERICTO_OFFLINE=1 with no API key: every wrapper and tool built without a guard uses it.
const vericto = new Vericto({ offline: true });
try {
const verdict = await vericto.check('DROP TABLE users', 'postgres');
console.log(verdict.mode, verdict.decision, verdict.ruleCode, verdict.rulesetVersion);
await vericto.enforce('DELETE FROM users', 'postgres');
} catch (err) {
if (!(err instanceof VerictoBlockedError)) throw err; // CLI missing or too old: VerictoConfigError
console.log(err.verdict.ruleCode, err.suggestedFix);
} finally {
await vericto.close(); // optional: the process is also stopped when yours exits
}offline BLOCKED VERICTO-010 standard@engine-5.0.0
VERICTO-001 DELETE FROM users WHERE id = $1
(The output above is from the CI stand-in for vericto serve, scripts/serve-stub.mjs,
which answers these two statements as the real CLI 2.1.0 does.)
- Same API, same verdicts as online. The result is the same
Verdict, built by the same mapping as a Runtime API response, soenforce, the fail mode, the cache,onVerdictand every driver wrapper and agent-framework tool work unchanged (pass{ vericto }).verdict.modeis'offline',engineVersionthe engine version andrulesetVersionstandard@engine-<version>;verdict.raw.resultis the documentvericto check --offline --format jsonprints for the same SQL. - Profiles.
offlineProfile: 'runtime'(default) answers as the Runtime API does for a workspace with no overrides:DROP TABLEis BLOCKED.offlineProfile: 'ci'answers exactly asvericto check --offline: schema DDL (VERICTO-010 to 019) is capped at FLAGGED. - Several statements in one text get one verdict, the strongest finding across
them, as online. To get one verdict per statement, check each statement (for
example with
enforceAll). - What it does not do. No custom rules, severity overrides, sensitive-column
masking, agent allowlists, business policies (
context/endUserare accepted and have no effect) or audit trail: those live in a workspace, online. - Finding the CLI, in this order:
offlineCliPathorVERICTO_CLI; theverictobinary of@vericto/vericto-cliinstalled in your app'snode_modules(found from the package'sbinmetadata);verictoonPATH. The SDK checks that the process speaks protocol 1 and is CLI 2.1.0 or later. It never downloads anything. - Errors. No CLI:
VerictoConfigErrorwithcode: 'CLI_NOT_FOUND'; one older than 2.1.0:code: 'CLI_TOO_OLD'; both carry the install command. Offline together with an API key (apiKeyorVERICTO_API_KEY) or anevaluatoris aVerictoConfigError: choose one. An unknown dialect isVerictoUnsupportedDialectError; a statement over the process's line limit (1 MiB) isVerictoConfigErrorwithcode: 'TOO_LARGE'(nostatus: nothing went over HTTP). When the process fails or crashes, pending checks reject withVerictoUnavailableErrorand the fail mode applies exactly as online. - One process per client, started on the first check (
child_process.spawn; the SDK has no runtime dependencies). It does not keep your process alive while idle, and it is stopped when your process exits. If it exits unexpectedly, the next check starts a new one; after a second failure in a row with no answer in between, it is not restarted (checks getVerictoUnavailableError). A check with no answer withintimeoutMs(30 s by default offline;VERICTO_TIMEOUTis the Runtime API's and does not apply) is unavailable too, and the process is replaced.close()(orawait using vericto = new Vericto({ offline: true })) stops it once the checks already sent are answered. Its stderr goes tologger.debug; the SQL is never logged. - Licenses. This SDK is Apache-2.0. The Vericto CLI and its engine are AGPL-3.0-only and run as a separate process: the SDK does not contain, link or download them, it only starts the binary you installed.
| Method | Resolves | Rejects with |
|---|---|---|
enforce(sql, dialect, options?) |
the Verdict (allowed, flagged, monitored, parse error, masked, or UNEVALUATED under fail-open) |
VerictoBlockedError on BLOCKED; VerictoUnavailableError under failMode: 'closed'; VerictoConfigError |
prepare(sql, dialect, options?) |
the SQL text to run (sqlToExecute(verdict)) |
as enforce |
enforceAll(statements, dialect, options?) |
one verdict per statement, in order; a block anywhere rejects before you run any of them | as enforce |
check(sql, dialect, options?) |
the verdict, whatever it is (BLOCKED included; UNEVALUATED with the reason under fail-open): to ask, not to guard |
VerictoUnavailableError under failMode: 'closed'; VerictoConfigError |
requireDialect(dialect) |
the normalized dialect id | VerictoUnsupportedDialectError when the server does not list it |
clearCache() |
(sync) drops the verdict cache | |
close() |
offline: stops the vericto serve process once the checks already sent are answered (also Symbol.asyncDispose); online: nothing to do |
dialect is a Vericto dialect (postgres, mysql, oracle, mssql, snowflake,
sqlite) or a driver name normalizeDialect maps to one (postgresql, pg,
mysql2, mariadb, oracledb, sqlserver, tedious, sqlite3,
better-sqlite3). KNOWN_DIALECTS lists the six.
options is { context?, endUser? }: the declared context of the evaluation, see
Business policy context.
Options (VerictoOptions; all optional):
| Option | Meaning | Default |
|---|---|---|
apiKey, databaseId, apiUrl, timeoutMs |
See Configure. | environment |
maxRetries |
Retries after the first attempt. | 2 |
backoffMs |
Base of the exponential backoff between retries (with jitter, capped at 2 s). | 200 |
dialectsTtlMs |
How long the server's dialect list is cached. | 10 minutes |
failMode |
closed or open. |
VERICTO_FAIL_MODE, else closed |
onVerdict |
Called with every verdict, BLOCKED and UNEVALUATED included (metrics, your own audit). A throwing callback is logged and ignored. |
none |
logger |
{ info, warn, error, debug? }, or false to silence it. Lines carry the rule code, severity and event id, never the SQL text. debug gets the offline process's lifecycle and stderr. |
console for warnings and errors, silent for info and debug |
cacheTtlMs, cacheSize |
See Caching. | 0 (off), 1024 |
evaluator |
Replace the Runtime API client (see Evaluators). | RuntimeApiEvaluator |
offline |
Check with a local vericto serve process (see Offline mode). Not with apiKey or evaluator. |
VERICTO_OFFLINE, else false |
offlineProfile |
Offline: runtime or ci. |
runtime |
offlineCliPath |
Offline: the vericto binary. |
VERICTO_CLI, then node_modules, then PATH |
fetch |
A fetch implementation (proxies, tests). |
global fetch |
env |
Where the VERICTO_* defaults are read from. |
process.env |
| Field | Meaning |
|---|---|
decision |
ALLOWED, FLAGGED, MONITORED, PARSE_ERROR, BLOCKED, or UNEVALUATED (local: fail-open ran it without a verdict). An unknown decision from the server is read as BLOCKED. |
sql, dialect |
The statement as submitted, and the dialect it was evaluated as. |
ruleCode, severity, message |
The deciding rule, its severity, and the server's explanation when it adds one. |
suggestedFix |
A safer statement Vericto proposes, when it has one. |
rewrittenQuery |
When set, the statement to run instead of sql: the masked rewrite. |
findings |
The rules that matched: { ruleCode, severity?, ruleId?, astNodePath?, suggestedFix? }. |
sensitiveColumns, accessDenied |
The tagged columns the statement reads, and what the key's agent allowlist denied, as the server reports them. |
parseError |
The parser's message, on a parse error. |
eventId |
The audit-trail event, to look the evaluation up in the dashboard. |
rulesetVersion, cached |
Cache bookkeeping (see Caching); offline, standard@engine-<version>. |
mode, engineVersion |
online or offline, and offline the engine version that evaluated it. |
raw |
The response body, for fields this version does not model yet. |
Helpers: sqlToExecute(verdict) (the rewrite when there is one, else sql),
isMasked(verdict), verdictFromResponse(body, sql, dialect) (build a verdict from a
Runtime API body, for a custom evaluator or a test double), VERSION, and the
constants DEFAULT_API_URL, EVALUATE_PATH, CONFIG_PATH.
| Verdict | What the wrapper does |
|---|---|
ALLOWED |
Runs the statement. |
FLAGGED |
Runs it, logs a warning, calls onVerdict. |
MONITORED |
Runs it, logs at info, calls onVerdict. |
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 (rule VERICTO-PARSE-ERROR). |
BLOCKED |
Rejects with VerictoBlockedError (verdict, findings, suggestedFix) before the driver is called. |
mask (rewrittenQuery) |
Runs rewrittenQuery instead of your SQL: the sensitive columns come back masked. Never the original. |
| no verdict | The fail mode decides. |
Every evaluation is recorded in the Vericto audit trail and counts against the workspace's monthly query quota.
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-python 0.5.0:
failMode: 'closed'(default): reject withVerictoUnavailableError; the statement does not run.failMode: 'open': never reject for it.check()resolves anUNEVALUATEDverdict whosemessageis the reason.enforce()resolves one too: the statement runs unevaluated, a process warning of typeVerictoFailOpenWarningis emitted, it is logged, and the verdict goes toonVerdict.VerictoCheckSqlToolanswersUNEVALUATEDwith the reason.
Changed in 0.4.0: check() (and VerictoCheckSqlTool) used to reject with
VerictoUnavailableError under failMode: 'open' too. Code that caught it there now
gets the verdict instead:
import { Vericto } from '@vericto/sdk';
const vericto = new Vericto({ failMode: 'open', apiUrl: 'http://127.0.0.1:9', maxRetries: 0, logger: false });
// 0.3.x: try { await vericto.check(sql, d) } catch (err) { if (err instanceof VerictoUnavailableError) ... }
const verdict = await vericto.check('DELETE FROM orders WHERE id = 7', 'postgres');
if (verdict.decision === 'UNEVALUATED') {
console.log('no verdict:', verdict.message?.startsWith('Vericto did not return a verdict'));
}no verdict: true
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 reject with VerictoConfigError in both modes.
import { Vericto, VerictoUnavailableError } from '@vericto/sdk';
// The server sends its headers and then stalls (the stub's /stall mode): the timeout
// still applies, closed rejects, and nothing runs.
const closed = new Vericto({ timeoutMs: 200, maxRetries: 1, logger: false });
try {
await closed.enforce('UPDATE accounts SET balance = 0 WHERE id = 7', 'postgres');
} catch (err) {
if (!(err instanceof VerictoUnavailableError)) throw err;
console.log(err.name);
}
// Nothing listening at all: the same error.
const unreachable = new Vericto({ apiUrl: 'http://127.0.0.1:9', maxRetries: 0, logger: false });
await unreachable.enforce('SELECT 1', 'postgres').catch((err: unknown) => {
console.log(err instanceof VerictoUnavailableError);
});
// Closed: check() rejects with the same error.
await closed.check('SELECT 1', 'postgres').catch((err: unknown) => {
console.log('check:', err instanceof VerictoUnavailableError);
});
// Open: the statement may run, unevaluated, and you are told; check() never rejects.
const open = new Vericto({ failMode: 'open', timeoutMs: 200, maxRetries: 0, logger: false });
const verdict = await open.enforce('SELECT 1', 'postgres');
console.log(verdict.decision, verdict.sql);
const asked = await open.check('DROP TABLE accounts', 'postgres');
console.log(asked.decision, asked.message?.includes('Timeout'));VerictoUnavailableError
true
check: true
UNEVALUATED SELECT 1
UNEVALUATED true
import { Vericto, VerictoConfigError } from '@vericto/sdk';
// A revoked key (the stub's /401 mode) is a configuration error, even under failMode 'open'.
const vericto = new Vericto({ failMode: 'open', logger: false });
await vericto.enforce('SELECT 1', 'postgres').catch((err: unknown) => {
if (err instanceof VerictoConfigError) console.log(err.name, err.status);
});VerictoConfigError 401
check, enforce, prepare and enforceAll take an optional third argument with the
declared context of the evaluation, which business policies can read:
| Option | 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 |
endUser |
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 rejects with
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. The
LangChain.js wrappers take context and endUser as options, as a value or a
function read for every statement.
import { Vericto } from '@vericto/sdk';
const vericto = new Vericto({ logger: false });
const verdict = await vericto.enforce('UPDATE orders SET status = $1 WHERE id = $2', 'postgres', {
context: { ticket: 'CHG-1042', tenant: 'acme' },
endUser: { id: 'u_829', role: 'support' },
});
console.log(verdict.decision);
// Checked before it is sent: no request is made.
await vericto.check('SELECT 1', 'postgres', { endUser: { 'user-id': 'u_829' } }).catch((err: unknown) => {
console.log((err as Error).name, (err as Error).message);
});ALLOWED
VerictoConfigError endUser: keys are identifiers (letters, digits, _), at most 63 characters.
- Each attempt, response body included, is bounded by
timeoutMs(default 5 s,VERICTO_TIMEOUTin seconds). - Network errors, timeouts, HTTP 5xx and 429
RATE_LIMIT_EXCEEDEDare retriedmaxRetriestimes (default 2), with exponential backoff and jitter frombackoffMs(default 200 ms), honouringRetry-After; every wait is capped at 2 s. - 429
QUOTA_EXCEEDED(the monthly quota) is not retried:VerictoQuotaExceededError(resetsAt,upgradeUrl). 400, 401 and 403 are not retried:VerictoConfigError. - The first check (and the first after
dialectsTtlMs) also asks the server which dialects it evaluates (GET /api/v1/meta/config, one attempt,timeoutMs). If that cannot be answered nothing is cached and the evaluation request decides. - Worst case for one statement while Vericto is unreachable: the dialect request
plus
maxRetries + 1attempts and the waits between them; with the defaults about5 + 3 × 5 + 2 × 2= 24 s. LowertimeoutMsandmaxRetrieson latency-sensitive paths; the fail mode then applies sooner.
All extend VerictoError.
| Error | When | Fields |
|---|---|---|
VerictoBlockedError |
Vericto blocked the statement; it was not executed. | verdict, findings, suggestedFix, sql |
VerictoUnavailableError |
No verdict after the retries and failMode: 'closed' (from check(), enforce() and the wrappers alike). |
status, code |
VerictoQuotaExceededError |
Extends VerictoUnavailableError: monthly quota used (429 QUOTA_EXCEEDED). |
resetsAt, upgradeUrl |
VerictoConfigError |
Key, scope, database id, URL, timeout, request problem, or an invalid context / endUser; offline, a missing or too old CLI (code CLI_NOT_FOUND, CLI_TOO_OLD), offline with a key (OFFLINE_CONFLICT), a too large statement (TOO_LARGE); thrown in both fail modes. |
status, code |
VerictoUnsupportedDialectError |
Extends VerictoConfigError: the server does not evaluate that dialect. |
dialect, supported |
Error messages never contain the key or the SQL's bind values.
import snowflake from 'snowflake-sdk';
import { VerictoBlockedError } from '@vericto/sdk';
import { guardConnection } from '@vericto/sdk/snowflake';
const raw = snowflake.createConnection({
account: 'myorg-myaccount',
username: 'APP',
authenticator: 'SNOWFLAKE_JWT',
privateKeyPath: 'rsa_key.p8',
});
await raw.connectAsync();
const conn = await guardConnection(raw); // rejects if the server does not evaluate snowflake
const rows = await conn.query('SELECT id, email FROM customers WHERE id = ?', [42]);
// Callback style, with the driver's own options:
await conn.execute({
sqlText: 'DELETE FROM customers',
complete: (err) => {
if (err instanceof VerictoBlockedError) console.log(err.verdict.ruleCode); // never reached Snowflake
},
});connection.execute() returns its statement synchronously, but a Vericto check is
a network call, so the guarded execute() resolves the statement. With
complete, a Vericto rejection is delivered to it as err (and the promise
resolves undefined); without it, the promise rejects. query(sqlText, binds?, options?) is a promise-style shortcut that resolves the rows (not for
streamResult). A multi-statement sqlText (MULTI_STATEMENT_COUNT) is evaluated
as one request: a block anywhere means nothing runs. conn.raw is the unguarded
connection.
import Database from 'better-sqlite3';
import { VerictoBlockedError } from '@vericto/sdk';
import { guardDatabase } from '@vericto/sdk/better-sqlite3';
const db = await guardDatabase(new Database(':memory:')); // rejects if the server does not evaluate sqlite
await db.exec('CREATE TABLE sessions (id INTEGER PRIMARY KEY, expires_at INTEGER)'); // whole script checked first
await db.run('INSERT INTO sessions (expires_at) VALUES (?)', 0);
const expire = await db.prepare('DELETE FROM sessions WHERE expires_at < ?'); // checked once
console.log(expire.run(Date.now()).changes); // synchronous, as usual
const { statement, verdict } = await db.prepareWithVerdict('SELECT count(*) AS n FROM sessions WHERE id > ?');
console.log(verdict.decision, statement.get(0));
// better-sqlite3's type arguments: a Statement<[number], { n: number }>.
const since = await db.prepare<[number], { n: number }>('SELECT count(*) AS n FROM sessions WHERE expires_at >= ?');
console.log(since.get(0)?.n);
try {
await db.run('DELETE FROM sessions'); // never reaches SQLite
} catch (err) {
if (!(err instanceof VerictoBlockedError)) throw err;
console.log(err.verdict.ruleCode);
}1
ALLOWED { n: 0 }
0
VERICTO-001
better-sqlite3 is synchronous, so the SQL-carrying entry points (prepare,
prepareWithVerdict, exec, and the run/all/get shortcuts) become async. A
prepared statement's SQL cannot change, so its run/all/get/iterate stay
synchronous: prepare statements before db.raw.transaction(...) and use them inside
it. An exec script is evaluated as one request. Bind values (?, :name, @name,
$name) are never sent. See SQLite status.
prepare and prepareWithVerdict take better-sqlite3's own type arguments
(prepare<BindParameters, Result>) and resolve exactly its Statement type
(Statement<unknown[], unknown> without them); all<Row> and get<Row> type the rows.
One guide per framework, each with a minimal example that CI runs against a local
stand-in of the Runtime API, and the exact versions it was tested with. Official
means an entry point of this package; pattern means a few lines of your own code
around the public API (Vericto.enforce / prepare and the driver wrappers). There is
no package-level integration for the patterns.
| Framework | Status | Approach | Tested with |
|---|---|---|---|
| LangChain.js | official: @vericto/sdk/langchain |
VerictoSqlDatabase (a SqlDatabase), VerictoSqlToolkit, VerictoQuerySqlTool, VerictoCheckSqlTool |
@langchain/classic 1.0.55, @langchain/core 1.3.0, typeorm 1.1.2 |
| Vercel AI SDK | official: @vericto/sdk/ai |
sqlTool (a ready SQL tool()), guardSqlTool (guards yours), throwOnVerictoError |
ai 7.0.159 |
| OpenAI Agents SDK | official: @vericto/sdk/openai-agents |
sqlTool (a ready function tool), verictoToolInputGuardrail (a tool input guardrail for yours) |
@openai/agents 0.21.0 |
| Mastra | official: @vericto/sdk/mastra |
sqlTool (a ready createTool() tool), guardSqlTool (guards yours) |
@mastra/core 1.81.0 |
| Prisma ORM | pattern | wrap the driver adapter: model and raw queries are checked | prisma / @prisma/client 7.10.1 |
| Drizzle ORM | pattern | a guarded pg pool |
drizzle-orm 0.45.4 |
| Knex | pattern | guard each connection in pool.afterCreate |
knex 3.3.0 |
| node-postgres | pattern | guard each pool client on connect |
pg 8.23.1 |
| mysql2 | pattern | query / execute functions in front of the pool |
mysql2 3.24.6 |
For PostgreSQL and MySQL, the Vericto TCP proxy protects every connection without code changes (and enforces masks); the patterns are for when you cannot use it.
npm install @vericto/sdk @langchain/classic @langchain/core typeormVerictoSqlDatabase.fromDataSourceParams({ appDataSource }) is a LangChain
SqlDatabase (@langchain/classic/sql_db; langchain/sql_db no longer exists in
LangChain.js 1.x) whose TypeORM data source checks every query: the agent's queries,
the sample rows getTableInfo puts in the prompt, and the SQL SqlDatabaseChain
generates. Use new VerictoSqlToolkit(db, llm) with createSqlAgent. A block comes
back to the model as a message with the rule and the suggested fix (onBlock: 'return', default) or rejects with VerictoBlockedError (onBlock: 'raise'); Vericto
being unavailable or misconfigured is thrown, never handed to the model. The full,
tested example and the API table are in the LangChain.js guide.
@vericto/sdk/ai, @vericto/sdk/openai-agents and @vericto/sdk/mastra each export
sqlTool({ dialect, execute }): a SQL tool for that framework whose input is { sql }.
Vericto checks each query the model writes; your execute(sql) runs the SQL Vericto
approved. The contract is the same in all three:
BLOCKED: the tool returns a refusal to the model,{ decision: 'BLOCKED', executed: false, rule_code, severity, reason, suggested_fix, ... }, andexecuteis not called (onBlock: 'raise'throwsVerictoBlockedErrorinstead).FLAGGED/MONITORED: the query runs and is reported (verictoin the result, the guard's logger andonVerdict).- an approved query (masked columns and/or a LIMIT bound): it runs instead of the
model's, and the result says so (
vericto.approved_query). - no verdict: the guard's fail mode.
closed(default) throwsVerictoUnavailableErrorand nothing runs;openruns the query unevaluated.
To guard a tool you already have: guardSqlTool(tool, { dialect, sqlKey }) (AI SDK,
Mastra), or verictoToolInputGuardrail({ dialect, sqlKey }) in the tool's
inputGuardrails (OpenAI Agents SDK). Each guide has a tested example with a scripted
model.
import Database from 'better-sqlite3';
import { sqlTool } from '@vericto/sdk/ai';
const db = new Database(':memory:');
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
// Give it to generateText / streamText as `tools: { runSql }`; here it is called directly.
const runSql = sqlTool({ dialect: 'sqlite', execute: (sql) => db.prepare(sql).all() });
const out = await runSql.execute({ sql: 'DELETE FROM users' }, { toolCallId: 'call-1', messages: [], context: {} });
console.log(JSON.stringify(out).includes('"suggested_fix":"DELETE FROM users WHERE <condition>"'));true
Off by default (cacheTtlMs: 0). With cacheTtlMs: 30_000, identical
(sql, dialect) pairs reuse a verdict for 30 seconds (LRU, cacheSize: 1024), and
the cache is dropped when a response reports a new ruleset_version. A cached hit
is not evaluated, so it is not in the Vericto audit trail, and rule changes
apply only when the entry expires (the Runtime API does not report the ruleset
version today). Enable it only for hot, repetitive statements, with a short TTL.
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).
guardDatabase() still asks the server first and rejects with
VerictoUnsupportedDialectError, 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. See Live tests for an end-to-end
check against a real server.
The wrapper protects the SQL that goes through it. db.raw, conn.raw,
rawDataSource, another connection or another process with the same credentials
are 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).
Vericto takes an evaluator: any object with evaluate(sql, dialect, options?) and
supportedDialects() (and optionally apiUrl, shown in messages, and close()). The
guard passes options ({ context, endUser }) only when the caller gives them; an
evaluator that declares evaluate(sql, dialect) still works, and ignores them. This
release ships RuntimeApiEvaluator (the Runtime API) and OfflineEvaluator (a local
vericto serve process; what offline: true builds, with start(), ping(),
close() and pid).
A custom evaluator is also how you test your own code without the network:
import { Vericto, verdictFromResponse, type Evaluator } from '@vericto/sdk';
// Blocks every DELETE, allows the rest. Never use a fake like this in production.
const fake: Evaluator = {
evaluate: async (sql, dialect) =>
verdictFromResponse(
sql.startsWith('DELETE') ? { decision: 'BLOCKED', rule_code: 'VERICTO-001' } : { decision: 'ALLOWED' },
sql,
dialect,
),
supportedDialects: async () => undefined,
};
const vericto = new Vericto({ evaluator: fake, logger: false });
console.log((await vericto.check('DELETE FROM t', 'postgres')).decision);BLOCKED
Not implemented yet; each is meant to be a thin layer over Vericto.enforce:
- LlamaIndex.TS SQL tools.
pgandmysql2pool.querywrappers in this package (until then, the node-postgres and mysql2 guides document tested patterns; the TCP proxy already covers PostgreSQL and MySQL without code changes).- Cloudflare D1 Workers binding helper.
npm ci
npm run lint && npm run typecheck
npm test # vitest: local Runtime API stand-in, real better-sqlite3, fake snowflake-sdk,
# real LangChain.js + TypeORM, AI SDK, OpenAI Agents SDK and Mastra with
# scripted models (no model calls)
npm run test:coverage # the same suite with V8 coverage of src/ (what CI runs); fails under 80%
# lines, statements, functions or branches. Live tests are excluded.
npm run build # tsup → dist/ (ESM + CJS + .d.ts)
npm run smoke # the built package, ESM and CJS, on any Node >= 18 (offline mode: POSIX)
npm run docs:check # README + official integration guides, type-checked and run against the stubDevelopment needs Node 22+ (the tooling; the docs check uses type stripping, 22.6+); the published package runs on Node 18+. Nothing in the test suite connects to Snowflake.
scripts/check-docs-examples.mjs extracts every ```ts block of this README
and of docs/frameworks/*.md, type-checks them (strict) and runs each with Node's type
stripping against scripts/docs-stub.mjs, a local stand-in for the Runtime API with
the documented response shapes (no key, no network). A block can declare extra
environment, an expected output (compared exactly), or a skip with its reason, in
HTML comments (see the script's header). The framework guides run in their own
project, docs/frameworks/ (pinned framework versions, not published), against
PostgreSQL and MySQL (the guides of the official integrations run in the core suite,
with the package's devDependencies):
npm run build && npm run docs:check # core suite
cd docs/frameworks && npm ci && cd ../..
DATABASE_URL=postgres://... MYSQL_URL=mysql://... node scripts/check-docs-examples.mjs frameworksCI runs both (docs job, with PostgreSQL 17 and MySQL 8.4 service containers).
test/live/sqlite.live.test.ts is an opt-in end-to-end test of
@vericto/sdk/better-sqlite3 against a real Vericto Runtime API and a real
SQLite file in a temporary directory. It is skipped unless the variables below
are set, and never runs in CI. Benign statements (CREATE TABLE, INSERT with
? binds, UPSERT ON CONFLICT ... DO UPDATE, SELECT, DELETE ... WHERE) must
run; DELETE without WHERE (VERICTO-001), ATTACH DATABASE (VERICTO-098),
load_extension() (VERICTO-100) and VACUUM INTO (VERICTO-101) must reject with
VerictoBlockedError before SQLite sees them (checked with better-sqlite3's
verbose trace, the file's SHA-256 and the absence of the attached/copy files);
PRAGMA foreign_keys = OFF (VERICTO-099) must run and log a FLAGGED warning; an
exec script with a blocked statement in the middle must run none of it. The
database's workspace must use the default rules; a run makes about 25
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 (Node 22+):
export VERICTO_LIVE_SQLITE=1 VERICTO_API_URL=https://api-staging.vericto.com
export VERICTO_API_KEY=vtro_... VERICTO_DATABASE_ID=...
npm ci && npx vitest run test/liveOr in a throwaway node:22-slim container (the repository is mounted read-only
and copied; better-sqlite3 installs its prebuilt linux binary; 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 \
node:22-slim sh -c 'mkdir /work && cd /src && tar --exclude=node_modules --exclude=dist -cf - . | tar -xf - -C /work \
&& cd /work && npm ci --no-audit --no-fund && npx vitest run test/live'A skipped run (7 skipped) means a variable is missing.
test/offline.test.ts and one offline test per agent-framework adapter run against
scripts/serve-stub.mjs, a stand-in for vericto serve --stdio (POSIX: it is started
through a small /bin/sh launcher). test/offline-cli.test.ts runs against the real
CLI 2.1.0 or later, including parity with vericto check --offline --format json
for 16 statements in the six dialects; it is skipped, and says why, when no such CLI
is found (VERICTO_CLI, @vericto/vericto-cli in node_modules, then PATH). CI's
offline job installs @vericto/vericto-cli and sets VERICTO_CLI_REQUIRED=1, so
there a missing CLI fails instead:
VERICTO_CLI=/path/to/vericto npx vitest run test/offline-cli.test.tsSee 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.
Open source under the Apache License 2.0 (SPDX Apache-2.0) since
0.3.0. Versions 0.2.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.
Guardrails de base de datos para agentes de IA: @vericto/sdk valida cada sentencia
SQL con Vericto antes de ejecutarla:
wrappers para snowflake-sdk y better-sqlite3, herramientas SQL para el Vercel AI
SDK (@vericto/sdk/ai), el OpenAI Agents SDK (@vericto/sdk/openai-agents) y Mastra
(@vericto/sdk/mastra), una integración oficial con LangChain.js
(@vericto/sdk/langchain) y un cliente de la Runtime API para cualquier otro driver. Toda la API es asíncrona: cada validación es una llamada de red (en modo offline, a un proceso local).
- BLOCKED: rechaza con
VerictoBlockedError(hallazgos y corrección sugerida) y la sentencia no llega a la base de datos. - FLAGGED / MONITORED: se ejecuta y se registra (
onVerdict, logger). - Máscara de columnas sensibles: se ejecuta
rewrittenQuery, nunca la original. - Sin respuesta de Vericto:
failMode: 'closed'(por defecto) rechaza conVerictoUnavailableError, encheck(),enforce()y los wrappers;'open'nunca rechaza por eso:check()devuelve un veredictoUNEVALUATEDcon el motivo yenforce()ejecuta y emite el avisoVerictoFailOpenWarning(desde 0.4.0; antescheck()rechazaba también en'open'). Los errores de configuración (clave revocada, permiso, dialecto) nunca se dejan pasar. Cada intento, incluida la lectura de la respuesta, tiene el límite detimeoutMs(5 s por defecto), con 2 reintentos. - Contexto de políticas de negocio:
check,enforce,prepareyenforceAllaceptan{ context: { ticket, tenant }, endUser }, 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. Los valores de los parámetros nunca se envían a Vericto.
Frameworks: el Vercel AI SDK, el OpenAI Agents SDK, Mastra y LangChain.js tienen
integración oficial. En los tres primeros, sqlTool({ dialect, execute }) crea la
herramienta SQL del framework: si Vericto bloquea la query, el modelo recibe un rechazo
estructurado (rule_code, suggested_fix) y la SQL no se ejecuta; si devuelve una
query aprobada (columnas enmascaradas o un LIMIT), se ejecuta esa en lugar de la
original. Para Prisma, Drizzle, Knex, node-postgres y mysql2 hay patrones
documentados y probados (unas pocas líneas propias alrededor de la API pública, no
una integración del paquete), cada uno con su guía en
docs/frameworks y las versiones con las que se probó. Los
ejemplos de este README y de las guías se ejecutan en CI contra un simulador local de
la Runtime API.
Modo offline (sin cuenta): new Vericto({ offline: true }) o VERICTO_OFFLINE=1
valida el SQL sin clave ni red, con las reglas estándar del motor, en un proceso local
vericto serve --stdio. Necesita la CLI de Vericto 2.1.0 o posterior, que se instala
aparte (npm install --save-dev @vericto/vericto-cli@^2.1.0, o VERICTO_CLI con la
ruta del binario). El veredicto es el mismo Verdict que en línea, con
mode: 'offline', y todos los wrappers y herramientas funcionan igual. El perfil
runtime (por defecto) bloquea DROP TABLE como la Runtime API; ci responde igual
que vericto check --offline. Nunca se activa solo, y combinado con una clave de API
es un VerictoConfigError: hay que elegir uno. No incluye reglas propias, máscaras,
allowlists, políticas de negocio ni auditoría. El SDK es Apache-2.0; la CLI y el motor
son AGPL-3.0-only y corren como un proceso aparte: el SDK no los incluye ni los
descarga.
SQLite: el servidor de Vericto evalúa el dialecto sqlite (vericto-engine
4.1.0+). guardDatabase() igual consulta GET /api/v1/meta/config y, si un
servidor no lo anuncia, rechaza con VerictoUnsupportedDialectError sin ejecutar
nada.
Licencia: código abierto bajo la Apache License 2.0 desde 0.3.0. Las versiones 0.2.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).