VicroCode
Make Code Create Value
VicroCode is a lightweight online platform for publishing, running, sharing, and monetizing code projects. Launch HTML, Python, SQLite, AI agents, management tools, games, and more without server setup.
Please wait while VicroCode loads. You can also explore the AI programming guide
Loading...

AI MARKET GUIDE

Give Your Agent a SELECT-Only Window, Not the Keys to the Database

One developer handed their model a read-only ES key and DB account. Here's how to rebuild that instinct on VicroCode: a Python endpoint that physically rejects writes.

There's a small detail buried in one developer's long rant about juggling a dozen AI subscriptions that I keep coming back to. They mentioned, almost in passing, that they'd handed Gemini flash the read-only query apikey for their Elasticsearch and a read-only account on their database, so the model could pull system logs and data on its own. Not a write account. Not an admin token. A read-only credential, scoped before the model ever saw it.

That's the whole move. They didn't write a prompt that says "please don't delete anything." They gave the assistant a credential that physically couldn't delete anything. The boundary lives in the access grant, not in the model's good behavior. And that instinct is worth copying, because the alternative — trusting a probabilistic system to respect a sentence in your system prompt — is exactly the kind of thing that works until the one time it doesn't.

Why the prompt is the wrong place to enforce this

If you've spent any time with these tools you already know the failure mode. The same developer described their frustration with models that invent rules, pad their output, or confidently do the wrong thing. One job posting in the same batch of market chatter spelled out what teams actually want from people building LLM features: Function Calling and agent orchestration, plus explicit attention to permission boundaries and sensitive-data protection. Those two things belong together. The moment your agent can call a tool, the tool is your security boundary, not the conversation.

A prompt instruction like "only run SELECT statements" is a suggestion. A tool that parses the incoming SQL, confirms it's a single SELECT, and opens the database in read-only mode is a wall. When the model gets creative and sends an UPDATE, the wall says no and hands back an error. The model learns the contract from the rejection, not from your hopeful phrasing.

What you can actually build on VicroCode

Here's the honest part first, because it shapes everything else. The original setup used Elasticsearch and an external production database. Those sit outside what VicroCode supports — there's no ES connector and no external-DB client in the confirmed stack. If your logs and data live in those systems today, you'd need to get a copy into a SQLite file first, and VicroCode doesn't provide that sync for you. So treat this as rebuilding the *pattern* on a data store you keep inside the platform: a SQLite database that holds whatever you've chosen to make queryable.

With that boundary stated plainly, the pieces line up nicely. You keep your data in a SQLite database and shape it with the built-in SQLite editor, so the schema the agent will query is something you can see and reason about directly. You write the query tool as a Python function and run Python online as a hosted endpoint, which becomes the single door your agent knocks on. Then you wire that endpoint in as an in-platform tool call, which is where AI agent development stops being abstract and turns into a model that can fetch real rows on its own.

Enforcing read-only at the gate

The endpoint does two independent checks, and the redundancy is the point. Belt and suspenders beats either one alone.

First, open the connection in read-only mode. SQLite lets you do this at the URI level, so the connection itself has no capacity to write:

import sqlite3

def open_readonly(db_path: str) -> sqlite3.Connection:
    # mode=ro makes the whole connection physically read-only.
    conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
    conn.execute("PRAGMA query_only = ON;")  # second guard at the engine level
    return conn

Second, validate the statement before it ever reaches the engine. You're not trying to write a perfect SQL parser — you're rejecting the obvious. One statement only, it has to start with SELECT, and no sneaking in extra commands:

import re

FORBIDDEN = re.compile(
    r"\b(insert|update|delete|drop|alter|create|replace|attach|pragma|vacuum)\b",
    re.IGNORECASE,
)

def validate_select(sql: str) -> str:
    stripped = sql.strip().rstrip(";").strip()
    if ";" in stripped:
        raise ValueError("Only a single statement is allowed.")
    if not stripped.lower().startswith("select"):
        raise ValueError("Only SELECT queries are allowed.")
    if FORBIDDEN.search(stripped):
        raise ValueError("Write or schema statements are rejected.")
    return stripped

If the regex and the single-statement check both pass and the connection is still read-only, a malformed or malicious query has to beat all three to do damage. In practice it can't, because `mode=ro` means the engine itself has no write path. The string checks are there mostly to give the model a clean, fast rejection instead of a confusing engine error.

The tool-call contract

An agent needs a contract it can rely on: a named tool, a typed input, a predictable output, and errors it can read. Keep the surface small.

def query_tool(sql: str) -> dict:
    try:
        safe_sql = validate_select(sql)
    except ValueError as e:
        return {"ok": False, "error": str(e)}

    conn = open_readonly("data/app.sqlite")
    try:
        cur = conn.execute(safe_sql)
        rows = [dict(zip([c[0] for c in cur.description], r))
                for r in cur.fetchmany(200)]  # cap the result set
        return {"ok": True, "rows": rows, "truncated": len(rows) == 200}
    finally:
        conn.close()

The row cap matters more than it looks. Models will happily ask for everything, and an unbounded result set is both a cost problem and a context-window problem. Returning `truncated: true` tells the agent to narrow its query rather than assuming it saw the whole table. Expose this function through API Endpoint Hosting, register it as a tool the model can call, and describe it to the agent in one honest sentence: *runs a single read-only SELECT against the database and returns up to 200 rows.* No more capability than that, described exactly.

Where the honest edges are

I want to be straight about what this does and doesn't give you. It gives you an agent that can read data it's allowed to read, with writes rejected at the gate instead of discouraged in a prompt. That's the reproducible part. What it doesn't give you is a drop-in replacement for that developer's ES-plus-production-DB setup — those systems aren't in the supported stack, and pretending otherwise would just set you up to discover the gap at the worst moment. Whether this pattern performs well on your specific data and query shapes is something you'd have to measure yourself; I'd treat any claim about speed or accuracy here as unverified until you've run it.

The idea underneath, though, travels well. The developer in the market evidence got the important thing right on the first try: scope the credential, then let the model be as curious as it wants inside that scope. Rebuild that on a store you control, enforce it in code the model can't talk its way around, and you've turned "please don't break anything" from a wish into a property of the system.