Database Plugin
Gives the agent general SQL tools — list connections, list tables, describe a table, run a read-only query, and run a gated write — against one or more configured databases. Each connection picks its own backend: a native Python driver or an external database CLI. Engines: Postgres, MySQL/MariaDB, Oracle, SQL Server, plus SQLite.
Switchable backends
A connection's mode decides how it talks to the database:
mode | Behaviour |
|---|---|
native | Use the engine's Python driver. |
cli | Shell out to the engine's command-line tool. |
auto (default) | Native if the driver is importable, otherwise the CLI. |
| Engine | Native driver | CLI fallback | Default port |
|---|---|---|---|
Postgres (postgres, also postgresql / pg) | pg8000 | psql | 5432 |
MySQL / MariaDB (mysql, also mariadb) | PyMySQL | mysql | 3306 |
Oracle (oracle, also oracledb) | oracledb (thin mode) | — | 1521 |
SQL Server (sqlserver, also mssql) | python-tds | go-sqlcmd | 1433 |
SQLite (sqlite) | stdlib sqlite3 | — | — |
The four native drivers ship with Codumentor (pure Python — no libpq, Oracle Instant Client, or ODBC). They are imported only when a connection uses them, so a missing driver does not prevent the plugin from loading; mode: auto then falls back to the CLI.
Use the CLI path when you prefer not to use the bundled driver, or for cases the pure-Python drivers handle poorly — most notably SQL Server with Azure AD / TDS 8.0, where go-sqlcmd (a single static MIT binary) is the better choice. Oracle thin mode covers Oracle fully, so no Oracle CLI ships.
Native connections support bound params; CLI connections do not (use mode: native for parameterized queries).
Tools
| Tool | Approval | Purpose |
|---|---|---|
sql_list_connections | none | List configured connections (name, engine, host, database, read-only). Touches no database. |
sql_list_tables | none | Explore tables/views. Optional schema and pattern (case-insensitive name substring) filters; see below. |
sql_describe_table | none | Column metadata (name, type, nullability, default). |
sql_query | none | Run a read-only statement; rejects anything that isn't a single SELECT/WITH/EXPLAIN. Row-capped; results are JSON. |
sql_execute | database.execute | Run a write/DDL statement. Permission-gated, and refused on read-only connections (no prompt). |
When exactly one connection is configured, the connection argument is optional everywhere and defaults to it.
Who gets which half
Two gates, not one. target_agents (default all) decides whether the tools are offered at all; write_target_agents (default main) then decides who gets sql_execute.
By default a subagent can explore a schema and run read-only queries but cannot change data or DDL — the approval prompt sql_execute raises is meant for a human, and nobody is reading a subagent's tool calls as they happen. Set write_target_agents: all if subagents should also write. A connection's own read_only is the stricter guard and applies to every agent.
sql_query results come back as a JSON envelope — {"columns": [...], "rows": [...], "row_count": N, "truncated": bool} — rather than an ASCII table. When truncated is true, narrow the query.
Who gets the tools at all
target_agents decides which agent; access profiles decide which user. The whole sql_* family declares shared_credentials = True, because the connections it reaches are ones the operator configured — so a profile on a restricted capability policy (chat_only, qa_readonly) holds none of them unless you say otherwise:
access:
profiles:
office-assistant:
repos: none
capabilities: chat_only
tools:
allow: [sql_query, sql_list_tables] # or: shared_credentials: true
This holds even for connections that resolve a per-user password_secret. The declaration is on the family rather than computed per connection for two reasons: connection is a per-call argument, so one tool object spans every configured connection and a mixed deployment has no single answer; and sql_list_connections discloses the inventory — which databases this deployment talks to — whoever can open them. A deployment whose connections really are all per-user says so once, with tools: {shared_credentials: true}.
Choosing the databases per conversation
The welcome screen shows a Databases selector under the repository/branch selector: one checkbox per connection, plus All / None. Whatever the user ticks is what the agent may query in that conversation:
- Some connections: the
[Database]note lists only those and says the user narrowed it. The tools refuse any other connection withconnection_not_selected, even one named explicitly. - None: the agent gets no SQL tools. It gets a short note telling it not to reach a database any other way (shell, CLI clients), so an answer can't quietly come from data the user didn't want consulted. Calls are refused with
database_disabled. - Never chose (a conversation started outside the web UI, such as a scheduled run, a trigger or the API):
default_selectiondecides.all(the default) keeps the pre-selector behaviour.nonemakes database access opt-in.
The choice is enforced when the tool is called, not only in the prompt: a refused call fails before any approval prompt. The selector starts from the user's last choice, and when a conversation is opened it saves what it shows, so the screen and the agent always agree. A conversation that narrowed or switched off databases shows a DB: … banner above the transcript. A connection added to the config later is not added to existing selections, because a selection is an explicit list.
The selector and its routes (/ui/plugins/database/…) are offered only to users who get the SQL tools at all (the database.selection access feature, paired with the family's shared_credentials).
Schema exploration scales down, not up
sql_list_tables adapts its output so a huge catalog cannot silently swallow schemas:
- Unfiltered, small catalog (≤ ~200 objects): names, grouped per schema —
{"schemas": {"main": {"tables": [...], "views": [...]}}, "total": N}. - Unfiltered, large catalog: per-schema counts only —
{"schemas": {"dbo": {"tables": 1953, "views": 170}, ...}, "hint": ...}— with a hint to drill in viaschema=and/orpattern=. - Filtered (
schemaand/orpattern): names, capped atmax_rowswith an explicit"truncated": truewhen the filter is still too broad.
The intended flow on big databases is therefore: counts overview → drill into one schema, or search by name fragment (pattern: "kivonat").
Configuration
plugins:
- module: codumentor.plugins.database
class: DatabasePlugin
args:
enabled: true
target_agents: all # main | subagent | all — the whole family
write_target_agents: main # same values, sql_execute only
default_read_only: true # per-connection default
max_rows: 1000 # hard cap for sql_query
statement_timeout: 30 # seconds
default_selection: all # all | none — before the user picks
connections:
- name: analytics # logical handle the agent uses
engine: postgres # postgres | mysql | oracle | sqlserver | sqlite
mode: auto # native | cli | auto
host: db.example.com
port: 5432
database: analytics
user: reporting
password_secret: pg_analytics_password # per-user secret name
read_only: true
- name: warehouse
engine: sqlserver
mode: cli # go-sqlcmd
host: mssql.example.com
port: 1433
database: warehouse
user: svc_reader
password_secret: mssql_warehouse_password
read_only: true
Load user_secrets alongside this plugin so passwords can resolve per user.
Plugin parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Master on/off. |
target_agents | string | all | Who gets the tools: main, subagent, all, or a subagent role name. |
write_target_agents | string | main | Same values, applied to sql_execute only. Subagents keep the read half. |
default_read_only | bool | true | Applied to any connection that omits read_only. |
max_rows | int | 1000 | Hard cap for sql_query (and the name cap for filtered sql_list_tables). |
statement_timeout | float | 30 | Per-statement timeout in seconds. |
default_selection | string | all | Connections a conversation may use before the user picks in the selector: all or none (opt-in). |
connections | list | [] | Named connections (see below). Names must be unique. |
Connection parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
name | string | (required) | Handle the agent uses in tool calls. |
engine | string | (required) | postgres, mysql, oracle, sqlserver, or sqlite (aliases above). |
mode | string | auto | native, cli, or auto. |
host | string | localhost | Database host. |
port | int | engine default | Omit to use the engine default. |
database | string | — | Database / service name. For SQLite this is the file path (:memory: if omitted). |
user | string | — | Login user. |
password_secret | string | — | Per-user secret name (preferred). |
password | string | — | Shared credential; may contain ${secret:...}. |
read_only | bool | default_read_only | When true, sql_execute is refused. |
connect_kwargs | map | {} | Extra keyword arguments passed to the native driver (for example an Oracle dsn). |
Credentials
Two models are supported, per connection:
- Per-user secret (preferred) — set
password_secret: <name>. The config stores only the secret name; each user supplies their own password in Settings → Extensions → User Secrets, and tools authenticate as the calling user. The pane shows "DB password (<connection>) — needed by Databases". - Shared service account — set
password: ${secret:<name>}(a shared secret resolved server-side). Useful for a read-only reporting replica with a single account.
If a password_secret is declared but unset, tools return a clear, non-retryable message naming the secret — no connection attempt is made. A connection with neither field resolves to "no password" (works for SQLite, trusted/integrated auth, etc.).
Safety
- Read-only by default, enforced in depth:
sql_queryonly accepts a singleSELECT/WITH/EXPLAIN, plus a session-level read-only setting where the engine supports it (Postgres, MySQL, SQLite). That session setting applies on both backends — native connections set it on the DBAPI connection, CLI connections prepend it to the stdin script — so amode: cliconnection is no weaker than amode: nativeone. Oracle and SQL Server have no read-only session mode at all; there the guarantee is the statement guard plus the login's privileges, so grant least privilege, and the server log says so at startup for every such connection. Where the setting does exist, it either takes or the connection is refused: if the server rejects the read-only statement, amode: nativeconnection fails with a non-retryable error naming it (and anERRORin the log) rather than quietly running read-write, and amode: clione fails the same way because the client exits non-zero. - Writes are gated.
sql_executeasks for approval (database.execute) and is refused outright on a read-only connection — at the tool layer and again in the backend, which is the layer that holds on the engines with no read-only session. - No CLI meta-commands. A database CLI reads its own commands from the same stdin as the SQL, and they are not SQL, so the statement guard never sees them:
\!(psql/mysql) and!!/:!!(sqlcmd) run a host shell command,\c/connect/:connectopen a second session that drops the read-only setting, and\i/source/:rread host files. Every CLI path refuses a payload containing one. The check is quote-aware (a backslash inside a string literal is fine) and deliberately conservative: onmysqla line whose first word issystem,source,connect,resetconnection,tee,pageroreditis refused even where the client would have treated it as SQL. Reformat such a query, or usemode: native. - Result caps.
limitis clamped tomax_rows; an extra sentinel row marks truncation; wide cells are truncated. - Statement timeouts applied natively and as a CLI wall-clock kill. Unlike read-only, the server-side cap is best-effort: a server or pooler that rejects the
SETgets aWARNINGin the log and the connection is used anyway (MariaDB, which simply ignoresmax_execution_time, is the same story without the error). - Credential hygiene. CLI passwords go through an environment variable (
PGPASSWORD/MYSQL_PWD/SQLCMDPASSWORD), never the command line; SQL is passed on stdin.
Notes & limitations
- CLI
params: not supported — usemode: nativefor bound parameters. - go-sqlcmd: results are coerced to JSON server-side (
FOR JSON PATH) because its CSV output does not quote/escape. rowcounton a CLI connection comes from the client's own report of the statement: psql's command tag (INSERT 0 5), which is why the exec invocation is not run with-q. A statement whose client prints no count (most DDL) reports-1.- Oracle/SQL Server identifiers in
describe_tablefollow each engine's default case-folding (Oracle uppercases unquoted names).