# Safe data access (/use-cases/data-agents/safe-data-access)



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.

```bash
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](/knowledge/vector-stores/pgvector/overview). Keep this application's storage credentials separate from the restricted warehouse role.

```python
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 [#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.

| Member       | Connection                                                      | Can do                                | Cannot do                          |
| ------------ | --------------------------------------------------------------- | ------------------------------------- | ---------------------------------- |
| **Analyst**  | Read-only role on source data and selected materialized outputs | Introspect, SELECT, answer            | Any write, anywhere                |
| **Engineer** | Read on `public`, read-write on an agent-owned schema           | Build views in its own schema         | Write to or alter `public` objects |
| **Leader**   | No direct database access                                       | Route the request, compose the answer | Run 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 [#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.

```python
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 [#layers-of-defense]

| Layer                | Enforced by                                                                    |
| -------------------- | ------------------------------------------------------------------------------ |
| Read-only answers    | Database role with no write grant                                              |
| Write isolation      | Schema-scoped grant on a separate connection                                   |
| Irreversible actions | Human approval via `requires_confirmation`                                     |
| Auditability         | The [Decision Log](/learning/stores/decision-log) records what changed and why |

## Next steps [#next-steps]

| Task                                  | Guide                                                     |
| ------------------------------------- | --------------------------------------------------------- |
| Let the Engineer build reusable views | [Materialization](/use-cases/data-agents/materialization) |
| Approve sensitive actions             | [Human approval](/hitl/overview)                          |

## Developer Resources [#developer-resources]

* [Human approval](/hitl/overview)
* [Dash: dual-schema enforcement](/deploy/templates/dash/overview)
