Raj Chhapariya
WorkAboutWriting
ResumeContactGitHub
Raj Chhapariya•© 2026•Bengaluru, India•Privacy
GitHubX (Twitter)LinkedInEmail
Back to All Writing
Systems Research·
August 14, 2026
·
5 min read

Deterministic Guardrails for LLM Agents: Enforcing Safe SQL and Pydantic Schemas without Open-Ended Code Execution

Why unrestricted Python code generation is an unacceptable security hazard, and how to build sandboxed analytical agents with AST parsing and numerical verification.

AI SecurityDuckDBPythonPydanticSQL

1. The Flaw of Arbitrary Code Execution in Agents

Many early LLM analytics frameworks popularized an architectural pattern: prompt the LLM to write arbitrary Python code (using Pandas or exec()), run the code in a local subprocess, and display the output dataframe or plot.

While flexible in simple notebook demos, deploying open-ended code generation into production applications introduces critical vulnerabilities:

  1. Remote Code Execution (RCE) & Sandbox Escapes: An adversarial user prompt (or indirect prompt injection from malicious CSV column names) can trick the model into executing system commands:
python
   # Dangerous payload generated by unconstrained agent
   import os; os.system("rm -rf /") # or exfiltrating environment variables
  1. Non-Deterministic Execution Paths: Language models frequently invent non-existent library methods, invoke deprecated keyword arguments, or generate unbounded while loops that consume 100% CPU and exhaust server memory.
  2. Unverifiable Data Claims: When models summarize output tables, they frequently misread rows or hallucinate percentages that contradict the dataframe itself.

To build reliable data systems, we must replace arbitrary code execution with restricted tool dispatch, AST-level SQL validation, and automated assertion verification.


2. The Four-Tool Deterministic Dispatch Pattern

Instead of allowing the agent to generate free-form Python scripts, restrict the agent's action space to four discrete, immutable tools:

  1. execute_sql_query: Executes read-only SQL queries against an in-process columnar database (DuckDB).
  2. generate_chart: Emits a structured chart specification (specifying chart type, x-column, y-column, aggregation) rendered by a deterministic client charting engine.
  3. get_column_statistics: Returns deterministic summary statistics (mean, median, null count, standard deviation, quartiles) computed directly by the engine.
  4. request_clarification: Triggered when a user query contains ambiguous column references or undefined business logic.

By constraining the model to selecting tools with strict JSON schemas, the attack surface drops by orders of magnitude.


3. AST-Level SQL Safety Validation

Naive SQL validation uses regex blacklist matching (e.g., checking if "DROP" or "DELETE" is present). Regex blacklists are trivially bypassed using SQL comments (/* DROP */), hexadecimal literals, CTE wrappers, or nested dynamic statements.

The proper approach is Abstract Syntax Tree (AST) parsing using sqlglot or database-native parsers:

python
import sqlglot
from sqlglot import exp

class SQLSafetyValidator:
    ALLOWED_STATEMENT_TYPES = (exp.Select, exp.Union)
    FORBIDDEN_EXPRESSIONS = (
        exp.Drop, exp.Delete, exp.Insert, exp.Update, 
        exp.Alter, exp.Create, exp.Command, exp.Transaction
    )

    @classmethod
    def validate_query(cls, sql_str: str) -> str:
        """
        Parses and validates that a SQL query is strictly read-only and free of mutation syntax.
        """
        try:
            parsed = sqlglot.parse_one(sql_str, read="duckdb")
        except Exception as e:
            raise ValueError(f"Malformed SQL syntax: {str(e)}")

        # Enforce that root expression is a SELECT or UNION
        if not isinstance(parsed, cls.ALLOWED_STATEMENT_TYPES):
            raise PermissionError(f"Prohibited query type: {type(parsed).__name__}. Only SELECT queries are permitted.")

        # Traverse entire AST to prevent mutation commands in subqueries or CTEs
        for expression in parsed.find_all(cls.FORBIDDEN_EXPRESSIONS):
            raise PermissionError(f"Forbidden operation detected: {type(expression).__name__}")

        # Return sanitized normalized SQL
        return parsed.sql(dialect="duckdb")

4. Sandboxing with In-Process DuckDB

DuckDB is designed specifically for fast analytical queries (OLAP) directly within the application process. Unlike SQLite, DuckDB is columnar and vectorized; unlike PostgreSQL, it requires no external server process or network sockets.

To ensure physical isolation:

  • Attach datasets in memory: con = duckdb.connect(database=':memory:')
  • Configure read-only execution modes and query memory limits:
python
  con.execute("SET max_memory = '1GB'")
  con.execute("SET threads = 4")
  • Enforce strict query timeouts using background thread watchers or connection interrupt handlers.

5. Validating Numerical Claims Against Source Data

A major failure mode in LLM analytics is hallucinated summary text. A query might return a sum of 452,100, but the generative model writes: "The total revenue for Q3 was 542,000."

To eliminate this, implement an automated Claim Verifier:

python
import re
from typing import List, Dict, Any

def verify_numerical_claims(
    generated_text: str, 
    query_result_table: List[Dict[str, Any]], 
    tolerance: float = 0.01
) -> bool:
    """
    Extracts numerical figures from generated summary text and verifies
    that every extracted number exists in the query result table.
    """
    # Extract floating-point and integer numbers from text
    raw_numbers = re.findall(r'[-+]?(?:\d*\.\d+|\d+)', generated_text.replace(',', ''))
    extracted_values = [float(n) for n in raw_numbers]
    
    # Flatten all numerical values from query result
    table_numbers = []
    for row in query_result_table:
        for val in row.values():
            if isinstance(val, (int, float)):
                table_numbers.append(float(val))
                
    # Check that each significant extracted number is backed by the table
    for num in extracted_values:
        # Ignore common non-data numbers (e.g. 1, 2 for lists, or years like 2025 if filtered)
        if num in [0.0, 1.0, 2.0]:
            continue
        
        # Verify proximity to at least one cell in the result table
        is_supported = any(abs(num - tbl_val) <= (tolerance * abs(tbl_val) + 1e-5) for tbl_val in table_numbers)
        if not is_supported:
            return False # Flag as unsupported claim
            
    return True

6. Pydantic v2 Structured Output Contracts

To ensure the LLM never outputs unparsed markdown, enforce strict JSON schemas using Pydantic:

python
from pydantic import BaseModel, Field
from typing import Optional, Literal, List

class ChartSpecification(BaseModel):
    chart_type: Literal['bar', 'line', 'scatter', 'pie'] = Field(description="Visual chart representation")
    x_axis: str = Field(description="Column name for horizontal dimension")
    y_axis: str = Field(description="Column name for vertical numerical metric")
    title: str = Field(description="Clean descriptive chart title")

class AnalyticalResponse(BaseModel):
    sql_query: str = Field(description="Validated read-only SQL query")
    reasoning: str = Field(description="Brief explanation of analytical approach")
    key_findings: List[str] = Field(description="Bullet points describing verified results")
    chart: Optional[ChartSpecification] = Field(default=None, description="Optional chart specification if visual is warranted")
    confidence_score: float = Field(ge=0.0, le=1.0, description="Self-assessed evidence confidence score")

7. Production Security & Reliability Checklist

  1. No eval() or exec(): Ban all dynamic string execution in application source code.
  2. Memory Quotas: Restrict DuckDB process allocations to prevent out-of-memory container crashes.
  3. Read-Only Encodings: Reject queries containing DDL or DML statements via AST traversal.
  4. Data Verification: Block LLM responses whose numerical statements fail cross-validation against raw query outputs.

Primary References & Literature

  • [1]
    DuckDB: an Embeddable Analytical Database (SIGMOD 2019)In-process analytical database architecture and SQL parser implementation.https://dl.acm.org/doi/10.1145/3299869.3320212
  • [2]
    OWASP Top 10 for Large Language Model Applications (2025)Security risks covering unconstrained code execution and excessive agency.https://owasp.org/www-project-top-10-for-large-language-model-applications/
  • [3]
    Pydantic v2 Schema Generation & JSON Serialization (Official Documentation)Type validation contracts and structured output schema definitions.https://docs.pydantic.dev/latest/
Related Case Study

AI Data Analyst Agent

Autonomous data analysis agent with DuckDB in-process OLAP, AST SQL safety validation, and post-synthesis numerical faithfulness verification.

View System Architecture & Benchmarks
Previous Article
Hybrid Retrieval Systems in Production: Combining BM25, Dense Embeddings, and Reciprocal Rank Fusion
Next Article
In-Process Columnar OLAP with DuckDB: Architecture, Vectorized Execution, and Analytics Engineering

Table of Contents

  • 1. The Flaw of Arbitrary Code Execution in Agents
  • 2. The Four-Tool Deterministic Dispatch Pattern
  • 3. AST-Level SQL Safety Validation
  • 4. Sandboxing with In-Process DuckDB
  • 5. Validating Numerical Claims Against Source Data
  • 6. Pydantic v2 Structured Output Contracts
  • 7. Production Security & Reliability Checklist
  • 8. Primary References & Literature

Author

Raj Chhapariya

AI / Data Engineer

Specializing in AI evaluation, hybrid information retrieval, in-process columnar analytics, and full-stack web applications.

GitHub Profile