Safe data access

Enforce read and write boundaries with database permissions and transaction settings.

Enforce read-only access with database roles and grants. Use a separate connection with schema-scoped permissions for approved write operations. These controls remain in effect when model output is unexpected.

uv pip install "agno[openai,psycopg,sql]"

Set OPENAI_API_KEY in the environment before running the Python code. Replace the warehouse URLs with your own PostgreSQL connection strings and create the database roles, schemas, and grants described on this page first. The host warehouse, database analytics, and roles such as readonly and dash_writer are placeholders.

Examples using PostgresDb or PgVector also need a separate writable application database. The sample URL assumes a PostgreSQL service at localhost:5532 with database/user/password ai; knowledge examples need the pgvector extension. See PgVector setup. Keep this application's storage credentials separate from the restricted warehouse role.

from agno.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.tools.sql import SQLTools
from sqlalchemy import create_engine

readonly_engine = create_engine(
    "postgresql+psycopg://readonly@warehouse/analytics",
    connect_args={"options": "-c default_transaction_read_only=on"},
)

analyst = Agent(
    name="Analyst",
    model=OpenAIResponses(id="gpt-5.5"),
    tools=[SQLTools(db_engine=readonly_engine)],
    instructions="Answer questions from the public schema. You cannot write.",
)

Create the readonly role without write grants before using this connection. The default_transaction_read_only=on setting blocks ordinary write statements. This setting is a configurable session default. Database ownership and grants remain the security boundary.

Split the roles

Most data-agent questions are read-only. Separate approved writes, such as building a summary table or recording a correction, into agents with dedicated connections.

MemberConnectionCan doCannot do
AnalystRead-only role on source data and selected materialized outputsIntrospect, SELECT, answerAny write, anywhere
EngineerRead on public, read-write on an agent-owned schemaBuild views in its own schemaWrite to or alter public objects
LeaderNo direct database accessRoute the request, compose the answerRun SQL itself

Scope the Engineer's writes to a schema such as dash. Grant the Analyst read access to the materialized objects it should reuse, with no write privileges. The Engineer must not own or inherit ownership of public objects, so it cannot drop those tables. Schema selection and search_path do not enforce these permissions.

Gate the writes that remain

For writes you do allow, add a human in the loop. requires_confirmation produces a paused run that the client must continue with an approval before the function executes.

from agno.tools import tool


@tool(requires_confirmation=True)
def materialize_view(name: str, sql: str) -> str:
    """Create a view in the agent-owned schema after human approval."""
    ...

For a dedicated writer built on SQLTools, set requires_confirmation_tools=["run_sql_query"]. This pauses every call to the tool, including reads. A narrow custom write tool gives finer control. Gate irreversible actions and leave reads ungated so approval fatigue does not set in.

Layers of defense

LayerEnforced by
Read-only answersDatabase role with no write grant
Write isolationSchema-scoped grant on a separate connection
Irreversible actionsHuman approval via requires_confirmation
AuditabilityThe Decision Log records what changed and why

Next steps

TaskGuide
Let the Engineer build reusable viewsMaterialization
Approve sensitive actionsHuman approval

Developer Resources