Read-only SQL access for AI agents
AI agents are only useful when they can touch real data. But handing an LLM a raw SQL connection is one hallucination away from destroying it. The Amplifier database tool gives agents read-only, bounded access instead.
So how big is the thing standing between the agent and the table?
The safety package in src/database_tools/safety/ is five small Python files. In the current checkout, the full suite runs green.
Small and tested — but why not just one good filter?
A single keyword the check overlooks, or a write smuggled past it, means silent data loss. One layer of checking is never enough to trust with production data — the answer has to be layered defense.
The tool stacks independent layers. Here's the first.
It strips comments, then uses case-insensitive word-boundary matching. DDL is always rejected; writes are rejected in the default read-only mode.
The forbidden query never reaches the database. Next: bounding what a permitted read can pull.
The LimitEnforcer rewrites a too-large LIMIT or injects one when it's absent. Default 100 rows, hard maximum 1000.
That bounds size. The next layer bounds time.
A TimeoutHandler wired into SQLite's progress handler checks every 10,000 VM instructions and aborts the query once elapsed time exceeds the limit.
Three independent layers so far. Now the backstop beneath them all.
On connect in the default read-only mode, the adapter runs PRAGMA query_only = ON. The write is made physically impossible at SQLite — the earlier layers become a net over an already-locked door.
Layered checks over an engine-level lock. That's the pattern.
Phase 1 ships three read-only tools. Writes (db_execute) and other engines are explicitly deferred to Phase 2. Layered, independent safeguards with the write path closed by default is how you give agents real data safely.
Read-only, bounded, backstopped — agents get real data, the table stays intact.
Subject: amplifier-module-tool-database (Phase 1), version 0.1.0
Feature status: Active — Phase 1 (SQLite + read-only SELECT); PostgreSQL/MySQL and write ops deferred to Phase 2
Repo history: single initial commit 13856db, Wed Jan 21 14:54:40 2026 -0800, author Sam Schillace (sole commit author)
Research performed (source commands):
git log -1 --format='%H%n%an%n%ae%n%ad%n%s'; git rev-list --count HEAD; git log --format='%an <%ae>' | sort -uwc -l src/database_tools/safety/*.py (450 lines across 5 files: validator 174, timeout 92, modes 90, limits 88, __init__ 6)find src -name '*.py' | xargs wc -l (2079); find tests -name '*.py' | xargs wc -l (1277); git ls-files | grep -c '.py$' (25)uv run --with pytest --with pyyaml pytest -q (67 collected, 67 passed, ~2.6s, Python 3.11.15 / pytest 9.0.2)read_file src/database_tools/safety/validator.py, modes.py (DDL/DML keyword lists, word-boundary regex, comment stripping)read_file src/database_tools/safety/limits.py (default 100, max 1000, LIMIT rewrite/inject)read_file src/database_tools/safety/timeout.py, adapters/sqlite.py (30s default, progress handler every 10000 VM instructions)read_file src/database_tools/adapters/sqlite.py (PRAGMA query_only = ON on connect, default read_only mode)read_file README.md; ls src/database_tools/tools/ (db_connect, db_schema, db_query; db_execute/db_explain = Phase 2)Gaps & caveats: Docs frame "6 layers" (PHASE1_COMPLETE.md / SAFETY.md), but only 5 are enforced in code — the 6th (connection-string / log sanitization) is documented-but-unbuilt, and bundle.md itself lists only 5. Parameter binding is supported but not enforced. Repo docs report 59/67 (88%) as of the commit date; the checked-out code now passes 67/67 because the DDL/DML fix is present. Self-reported effort figures (~40 hours, ~3,500 lines) are not git-derivable; measured src+tests = 3356 lines.
Primary contributor: Sam Schillace — sole commit author (single initial commit).