Works with your agent stack.
46 integrations, one policy file. Every client fails closed and runs only the exact string it verified.
Where HaltSQL sits.
The agent writes SQL. HaltSQL decides whether that exact string may run for this caller. Only an allowed query reaches the database, bound with tenant values the model never saw. A denied query goes back to the agent with codes and hints.
SELECT name, email FROM customers LIMIT 10- StatementSELECT
- Tablesapp.customers
- Columnsemail is not readable
- Tenantno tenant predicate
The agent writes a query for the user’s question.
Three ways in.
Embed the verifier, run it as a sidecar, or put it on the wire. The policy and the verdicts are the same in each.
Library
Haltsql.verify / haltsql CLI
Call verify in-process, or shell out to the static CLI from scripts and CI.
Sidecar
POST /v1/verify
haltsql-server next to your agent, with TLS, OIDC, audit and metrics, for any language.
Proxy
Native wire protocol
haltsql-proxy in front of the database, so unmodified drivers cannot skip the check.
Write a policy, check a query.
A policy lists what the agent may read and how rows are scoped to a tenant. Check queries against it from the CLI, an SDK, the HTTP sidecar or the proxy.
apiVersion: haltsql.dev/v1
kind: Policy
metadata: { name: analytics-agent }
search_path: [app]
statements: { allow: [select] }
require_limit: { max: 1000 }
tenancy:
mode: bound
keys: { tenant: "${ctx.tenant_id}" }
tables:
app.orders:
columns: [id, tenant_id, customer_id, status, total_cents, created_at]
tenant_column: tenant_id
app.customers:
columns: [id, tenant_id, name, created_at] # email and phone stay hidden
tenant_column: tenant_id
- CLI
- Python
- HTTP sidecar
- Proxy
haltsql verify --policy policy.yaml \
--context '{"tenant_id":"acme"}' \
--sql 'SELECT status, count(*) FROM orders
WHERE tenant_id = $1 GROUP BY status LIMIT 100'
# exit 0: allowed, bind $1 = 'acme'
haltsql verify --policy policy.yaml \
--context '{"tenant_id":"acme"}' \
--sql 'SELECT email FROM customers LIMIT 10'
# exit 1: E_COLUMN_DENIED, E_TENANT_MISSING
from haltsql import HaltSQLClient, guard
hs = HaltSQLClient.from_env() # talks to the haltsql-server sidecar
# Verify, then run exactly the verified string with HaltSQL's bind values,
# in a READ ONLY transaction with a timeout and a row cap.
result = guard.execute(conn, sql, {"tenant_id": tenant}, verifier=hs,
statement_timeout_ms=5000, max_rows=500)
curl -s http://127.0.0.1:8080/v1/verify \
-H "Authorization: Bearer $HALTSQL_TOKEN" \
-d '{"sql": "SELECT id, status FROM orders WHERE tenant_id = $1 LIMIT 50",
"context": {"tenant_id": "acme"}}'
{ "allowed": true, "reasons": [],
"bind_params": [ { "param": 1, "value": "acme" } ],
"policy": { "name": "analytics-agent", "version": "sha256:4f1c..." }, ... }
# Point any database driver at the proxy instead of the database.
haltsql-proxy run -c proxy.yaml --listen 0.0.0.0:6432
psql "host=localhost port=6432 user=analytics dbname=app"
app=> SELECT email FROM customers LIMIT 5;
ERROR: haltsql: query denied by policy "analytics-agent": column email of app.customers is not readable under this policy
DETAIL: HaltSQL reasons: E_COLUMN_DENIED, E_TENANT_MISSING
Passes the security review.
The controls platform and security teams ask about before an agent reaches production data, all in the MIT-licensed release.
Tenant isolation
A provable tenant predicate on every tenant table, in its own scope. The model never picks the bound value.
Column and function allowlists
Including what * expands to. pg_sleep, dblink and regclass casts are denied.
Hash-chained audit
To files, S3, GCS, Kafka or OpenSearch, and checked with haltsql audit verify.
OIDC, TLS and mTLS
Policies chosen from IdP groups, secret providers, and an optional deny-only OPA check.
Signed policy bundles
Pulled from OCI, S3 or HTTPS, signature-checked and hot-reloaded.
Shadow reports
See what a new policy would deny before you enforce it.
Anything the analyzer does not fully understand is denied with E_UNSUPPORTED, never guessed at. Read the threat model and how HaltSQL is tested.