Get started

Engineering NotesDatabase connections and OwlDB

Per-role database accounts enforce table permissions, not SQL checks

Say an administrator opens only the customer and order tables to the sales role and closes the salary table in the same database. A check that extracts table names from AI-written SQL and compares them with an allowlist looks only at the text of the SQL. The engine reads whatever object that text actually resolves to. SQL where the two disagree passes the check and returns salary rows, and because nothing raises an error, those rows go straight into the AI's answer.

So we leave the table boundary to the database's privilege check. On OwlDB connections, each role gets its own database account, and that account receives SELECT only on the tables the administrator picked. Direct connections, where no such account can exist, are opened per database only. This post explains what OwlAI checks, what the database checks, and why table selection is available only on OwlDB connections.

Extracting table names from SQL disagrees with how the engine resolves them

Parsing SQL to pull out table names has real advantages. It stays inside OwlAI, needs no extra accounts in the database, and gives every connection the same screen. The problem is its premise: that the names the checker reads are the names the engine resolves. SQL that breaks this premise is common.

  • PostgreSQL's query_to_xml('SELECT * FROM hr.salary', true, false, '') runs SQL it receives as a string. The parser sees a function call and a string literal, and no table name.
  • Which schema an unqualified salary belongs to is decided by the connecting account's search_path in PostgreSQL, and by its default schema in Oracle and Tibero. A checker that sees only the SQL cannot know these values.
  • Oracle and Tibero synonyms point to an object in another schema under a different name. If a synonym shares a name with an allowlisted table but points elsewhere, the checker passes it and the engine reads the other table.
  • Case rules for quoted identifiers, table functions and engine-specific syntax differ by engine. Every engine we add means the parser has to follow its grammar too.

The engine resolves all of these names itself, and the privilege check applies to the resolved object. SQL inside a string also runs with the same account's privileges, so if that account has no privilege on the salary table, query_to_xml ends in a permission error as well. There is no reason for OwlAI to rebuild the engine's name resolution.

OwlAI picks the tables the model sees; the database decides which rows come back

An administrator allows database connections per role and picks a scope for each. The scope is either Whole database or Choose tables, and tables are chosen from those whose schema analysis has finished. OwlAI uses this scope to filter what the model sees.

  • Table search and table card lookups return only tables in scope. Asking for an out-of-scope table by name returns a permission denial.
  • Table notes are read and written only for tables in scope.
  • The business summary of a database, placed at the start of a conversation, is included only for databases where every table is open, because the summary can mention tables outside the scope.

Before the AI runs SQL, OwlAI checks whether the asking person's role may use that database. SQL against a database the role is not allowed is rejected here, before it runs. Which tables inside that database can be read is decided by the privileges of the database account that runs the SQL.

The two checks do different jobs. OwlAI's scope makes the model write SQL while seeing only the open tables from the start, so most SQL stays in scope. When the model guesses an out-of-scope table name, or a name resolves to a different object as in the examples above, that SQL stops at the database's privilege check.

Checks a sales query passes: OwlAI's database check and the role account's table grants
Figure 1. Checks a sales query passes: OwlAI's database check and the role account's table grants

On OwlDB connections, each role gets one database account with SELECT only

OwlDB is a database management product that handles database access and execution, and an OwlDB connection is a connection created by picking a database from the list OwlDB manages (“OwlDB connections hold a database ID instead of credentials”). On an OwlDB connection, each OwlAI role corresponds to one database account. When an administrator picks tables for a role, OwlDB issues GRANT SELECT to that account, and removing a table issues REVOKE. Deleting the role retires the account.

The only privilege this account receives is SELECT on the chosen tables. In PostgreSQL 14 and earlier, every account gets CREATE on the public schema by default, so that default is revoked when the account is created. The limits of read-only settings, which differ by engine, are covered in “Read-only and query cancellation have to reach the database server”.

When the AI runs SQL, OwlAI passes OwlDB the account identifier for the asking person's role. OwlAI receives no connection details for OwlDB connections, so it does not know that account's password either. If a sales rep and an HR employee ask the same question, the SQL runs under different accounts, and SQL that reads the salary table ends in a permission error on the sales account. The AI receives that permission error as the tool result.

Grant and revoke results are shown per table

Choosing 200 tables sends 200 GRANT statements. If a table is dropped in the meantime or the database briefly stops responding, some of them can fail. When the screen says the change was saved but the database privileges differ, things drift in two directions. If a grant partly fails, users ask believing the table is open and the SQL ends in a permission error. If a revoke partly fails, a table you believe is closed stays open. The second case is a leak.

So OwlDB returns the result of every grant and revoke per table. Each failed table is marked on the role screen as having a scope that differs from its actual privileges, and the administrator retries only that table. The real boundary is the database privilege, so the value the screen shows is also the result the database returned.

The database audit log records the role; SQL execution history records the person

If every question runs under one account, the database audit log always shows the same account name. A database administrator who opens the audit log cannot tell which department a query came from. With one account per role, the database audit log alone shows which role ran each query.

OwlAI's SQL execution history keeps the record per person. Each time SQL runs, one line records the time, the person who ran it, the database connection, the outcome, the number of rows returned and the original SQL (“Auditing an AI agent: log every query it runs, not the conversation”). Using the account and time from the database audit log to find the matching line in SQL execution history leads to the person who ran the query.

Direct connections open per database only

It might seem possible to give direct connections the same structure: OwlAI would create per-role accounts in the customer database and issue GRANT statements. To do that, OwlAI would have to hold an administrator-level account that can create accounts and grant privileges in the customer database. Write protection on direct connections relies on the connection account having only query privileges (“Read-only and query cancellation have to reach the database server”), and that premise is gone the moment OwlAI holds an administrator-level account. Handing database administrator privileges to an AI product that only runs queries is hard for a database administrator to accept.

So direct connections are allowed only as a whole database, and the boundary inside is set by the privileges of the connection account the administrator created. If you need to divide by table, the database administrator creates a query account for each purpose, registers one connection per account and allows different connections to different roles. Connections imported from Supabase or Amazon RDS behave like direct connections. For such connections, the API rejects any request that chooses tables with RBAC_DB_TABLE_SCOPE_UNSUPPORTED, refusing the whole request and saving none of its items. On the role screen as well, Choose tables is available only for OwlDB connections.

Design choices

We left the table boundary to database account privileges rather than a SQL parser. A parser stays inside OwlAI and gives the same screen regardless of how a database is connected. But places where the parser and the engine resolve names differently appear every time an engine or a piece of syntax is added, and SQL that slips through blends into answers without any error. The database's privilege check applies to the object after the engine has resolved the name, so that disagreement does not arise.

We made table selection available only on connections where a database account can enforce that scope. If table selection appeared on direct connections too, administrators would believe access was blocked, while the only thing that could block the SQL would be a parser inside OwlAI. We chose to keep the permissions you see and the permissions that are enforced the same. The cost is that dividing tables on a direct connection takes several accounts and connections, created by the database administrator.

Accounts are per role, not per person. An account per person would make the database audit log finer, but it would create as many database accounts as there are employees and send DDL on every hire, departure and transfer. A role is the unit at which permissions actually differ, and SQL execution history tracks individuals. The cost is that the database audit log alone cannot tell who within a role ran a query; you have to match it against SQL execution history.

Permissions also mean exactly what the database's rules say. If you open a view to a role, PostgreSQL, Oracle and Tibero all read the base tables with the view owner's privileges by default, so the rows the view shows are open. Database administrators already know this rule, so there is nothing new to learn, but it does mean that opening a view that joins salaries to the sales team opens the salaries too.

If one database mixes tables that different roles should see differently, use table selection on an OwlDB connection. If your databases are already split by business area, or your database administrator already manages a query account for each purpose, database-level permissions on direct connections are enough.