This tax-support prototype treats generated SQL as hostile until three controls agree
Overseas Tax Agent’s new implementation separates AST policy, tenant binding and database enforcement—and its own timeout failure explains why each layer matters.
A small Korean tax-support prototype just made a useful architectural point: text-to-SQL safety is not one filter. It is a chain of independent controls, and each control should still matter if the previous one fails.
On Oct. 27, Overseas Tax Agent added a guarded SQL path for querying brokerage-trade data. The implementation commit reports 19 dedicated SQL-guard tests and 149 tests across the repository. This is still an early, zero-star prototype with no declared license—not production evidence—but the controls are inspectable.
Layer one: constrain the query’s shape
The first layer parses model output with sqlglot. It accepts one SELECT, restricts access to five named tables, rejects writes, DDL, multiple statements and dangerous functions, and caps results at 500 rows. A rejected query gets one regeneration attempt with the failure reason before the agent returns an error. Those checks narrow the generated program before it reaches a database.
Layer two: add identity outside the model
The more important design choice is customer scoping. Rather than asking the model to remember a tenant predicate, the guard rejects literal customer IDs and rewrites customer-bearing tables as subqueries bound to the session’s customer_id. The project documentation says tests cover an attempted cross-customer query and expect only the session customer’s rows.
That is the transferable lesson: authorization context should be injected by trusted code, not generated by the language model whose output is being authorized.
Layer three: make the database refuse anyway
The execution layer opens SQLite in read-only mode, enables query_only, interrupts queries after five seconds and enforces the same 500-row ceiling. The PostgreSQL path uses a dedicated read-only role with a five-second statement timeout. Parser checks can be incomplete; database permissions provide a separate failure boundary.
The same day’s follow-up commits show why operational limits belong in the design. One commit says an Opus-generated query expanded to roughly 1,100 characters with UNION and CASE, contributing to 19 timeouts in a 30-case text-to-SQL run. The maintainer then separated SQL generation onto a smaller model, added simpler-query instructions and full SQL logging; a later commit added phase-level latency records and prompt caching.
The project has not published an independent security review, a tagged release or adoption evidence. Its value here is narrower: it exposes the boundaries that a production NL2SQL system should test separately—syntax policy, identity binding, database privilege and resource limits—instead of calling one prompt or parser a guardrail.
sources
comments · 0